3 Commits
Author SHA1 Message Date
azykov@mail.ru ea8f251599 player camera gizmo in editor 2026-07-01 10:31:35 +03:00
azykov@mail.ru 7ce9e6dca6 code cleanup 2026-06-27 21:27:35 +03:00
azykov@mail.ru b873286dfd minor stats changes 2026-06-26 20:56:10 +03:00
29 changed files with 155 additions and 359 deletions
-7
View File
@@ -1,7 +0,0 @@
# Скопируй в .env и заполни
SERVER_PORT=3000
CLIENT_PORT=5000
# Добавь свои переменные ниже
# DATABASE_URL=
# JWT_SECRET=
-102
View File
@@ -1,102 +0,0 @@
name: Build & Deploy
on:
push
env:
REGISTRY: git.aggtaa.com
IMAGE_PREFIX: ${{ gitea.repository }}
jobs:
# ── BUILD SERVER ──────────────────────────────────────────────────────────────
build-server:
runs-on: docker1
defaults:
run:
shell: sh
working-directory: /workspace
steps:
- name: Check paths
run: |
ls -la /data/tools/ || echo "no /data/tools"
mount | grep overlay || echo "no overlay"
ls /bin/
ls /usr/bin/ | grep -E "bash|sh"
- name: Check docker
run: |
find /usr -name "docker-buildx" 2>/dev/null || echo "not found in /usr"
find /root -name "docker-buildx" 2>/dev/null || echo "not found in /root"
ls /usr/libexec/ 2>/dev/null || echo "no /usr/libexec"
- name: Checkout
working-directory: /
run: |
mkdir -p /workspace
if [ -d /workspace/.git ]; then
cd /workspace && git fetch origin && git reset --hard origin/${{ gitea.ref_name }}
else
git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git /workspace
fi
- name: Build & push server
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login ${{ env.REGISTRY }} -u ${{ secrets.REGISTRY_USER }} --password-stdin
/data/tools/docker-buildx build \
--cache-from type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/server:cache \
--cache-to type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/server:cache,mode=max \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/server:latest \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/server:${{ gitea.sha }} \
--file server.Dockerfile \
--push \
.
# ── BUILD WEBCLIENT ───────────────────────────────────────────────────────────
build-webclient:
runs-on: docker1
defaults:
run:
working-directory: /workspace
steps:
- name: Checkout
working-directory: /
run: |
mkdir -p /workspace
if [ -d /workspace/.git ]; then
cd /workspace && git fetch origin && git reset --hard origin/${{ gitea.ref_name }}
else
git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git /workspace
fi
- name: Build & push webclient
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login ${{ env.REGISTRY }} -u ${{ secrets.REGISTRY_USER }} --password-stdin
/data/tools/docker-buildx build \
--cache-from type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/webclient:cache \
--cache-to type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/webclient:cache,mode=max \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/webclient:latest \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/webclient:${{ gitea.sha }} \
--file webclient.Dockerfile \
--push \
.
# ── DEPLOY ────────────────────────────────────────────────────────────────────
deploy:
runs-on: docker1
needs: [build-server, build-webclient]
if: always() && !failure() && !cancelled()
steps:
- name: Deploy
run: |
mkdir -p "/app-cache/${{ gitea.repository }}"
cd "/app-cache/${{ gitea.repository }}"
if [ ! -d .git ]; then
git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git .
else
git pull origin main
fi
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f
-34
View File
@@ -1,34 +0,0 @@
services:
server:
build:
context: .
dockerfile: server.Dockerfile
cache_from:
- ${CI_REGISTRY_IMAGE:-blockly3d}/server:cache
image: ${CI_REGISTRY_IMAGE:-blockly3d}/server:${CI_COMMIT_SHA:-latest}
restart: unless-stopped
ports:
- "${SERVER_PORT:-3000}:3000"
environment:
- NODE_ENV=production
env_file:
- .env
networks:
- blockly3d
webclient:
build:
context: .
dockerfile: webclient.Dockerfile
cache_from:
- ${CI_REGISTRY_IMAGE:-blockly3d}/webclient:cache
image: ${CI_REGISTRY_IMAGE:-blockly3d}/webclient:${CI_COMMIT_SHA:-latest}
restart: unless-stopped
ports:
- "${CLIENT_PORT:-5000}:5000"
networks:
- blockly3d
networks:
blockly3d:
driver: bridge
-37
View File
@@ -1,37 +0,0 @@
# syntax=docker/dockerfile:1
FROM node:22.22.3-alpine AS base
RUN corepack enable && corepack prepare pnpm@latest --activate
# ── deps ──────────────────────────────────────────────────────────────────────
FROM base AS deps
WORKDIR /app
# Копируем только манифесты — слой кешируется пока lock не менялся
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY server/package.json ./server/
RUN pnpm install --frozen-lockfile --filter server...
# ── build ─────────────────────────────────────────────────────────────────────
FROM deps AS builder
WORKDIR /app
COPY server ./server
RUN pnpm --filter server build
# ── runtime ───────────────────────────────────────────────────────────────────
FROM node:22.22.3-alpine AS runner
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY server/package.json ./server/
RUN pnpm install --frozen-lockfile --filter server... --prod
COPY --from=builder /app/server/dist ./server/dist
EXPOSE 3000
CMD ["node", "server/dist/index.js"]
-32
View File
@@ -1,32 +0,0 @@
# syntax=docker/dockerfile:1
FROM node:22.22.3-alpine AS base
RUN corepack enable && corepack prepare pnpm@latest --activate
# ── deps ──────────────────────────────────────────────────────────────────────
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY webclient/package.json ./webclient/
RUN pnpm install --frozen-lockfile --filter webclient...
# ── build ─────────────────────────────────────────────────────────────────────
FROM deps AS builder
WORKDIR /app
COPY webclient ./webclient
RUN pnpm --filter webclient build
# ── runtime ───────────────────────────────────────────────────────────────────
FROM node:22.22.3-alpine AS runner
WORKDIR /app
RUN npm install -g serve
COPY --from=builder /app/webclient/dist ./dist
EXPOSE 5000
CMD ["serve", "-s", "dist", "-l", "5000"]
+2 -2
View File
@@ -1,5 +1,5 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import type { ObjectType, RuntimeGameObjectInstance } from "../types"; import type { ObjectType, ObjectInstance } from "../types";
import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal"; import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal";
import { useEffect, useLayoutEffect, useMemo, useRef } from "react"; import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
import { Euler, Quaternion, Vector3 } from "three"; import { Euler, Quaternion, Vector3 } from "three";
@@ -25,7 +25,7 @@ const _charPos = new Vector3();
const _lookAt = new Vector3(); const _lookAt = new Vector3();
type PlayerObjectViewProps = { type PlayerObjectViewProps = {
object: RuntimeGameObjectInstance; object: ObjectInstance;
objectType: ObjectType; objectType: ObjectType;
} }
@@ -2,6 +2,7 @@ import { useEffect } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { debugLabelsStore } from "../state/debugLabelsState"; import { debugLabelsStore } from "../state/debugLabelsState";
import { DebugLabel } from "./DebugLabel"; import { DebugLabel } from "./DebugLabel";
import type { Scene } from "../types";
export const DebugLabelsView = observer(function ({ scene, editMode }: { scene: Scene, editMode?: boolean }) { export const DebugLabelsView = observer(function ({ scene, editMode }: { scene: Scene, editMode?: boolean }) {
useEffect( useEffect(
@@ -2,6 +2,7 @@ import { observer } from "mobx-react-lite";
import { state } from "../state"; import { state } from "../state";
import { ObjectViewInternal } from "./ObjectViewInternal"; import { ObjectViewInternal } from "./ObjectViewInternal";
import { PlayerObjectView } from "./CharacterView"; import { PlayerObjectView } from "./CharacterView";
import type { ObjectInstance } from "../types";
export type GameObjectViewProps = { export type GameObjectViewProps = {
object: ObjectInstance; object: ObjectInstance;
@@ -94,9 +94,6 @@ export const ObjectEditorView = observer(function ({ object, isPlayer }: ObjectE
if (!objectType) if (!objectType)
return null; return null;
// Track cache so observer re-renders when refreshObjectTypeCaches is called
void object.cache.updatedAt;
function handleClick() { function handleClick() {
if (state.worldEditor.isEnabled) { if (state.worldEditor.isEnabled) {
// Reading selection state inside an event handler: not tracked by observer. // Reading selection state inside an event handler: not tracked by observer.
@@ -1,27 +1,12 @@
import type { ObjectInstance, ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types"; import type { ObjectInstance, ObjectType } from "../types";
import type { VoxelGroup } from "../types/runtime/voxelGroup"; import { useEffect, useImperativeHandle, useRef, type Ref } from "react";
import { useEffect, useImperativeHandle, useRef, type Ref, memo, useMemo } from "react";
import { Euler, Quaternion, Mesh, MeshStandardMaterial, type Group } from "three"; import { Euler, Quaternion, Mesh, MeshStandardMaterial, type Group } from "three";
import type { ThreeEvent } from "@react-three/fiber"; import type { ThreeEvent } from "@react-three/fiber";
import { ConvexHullCollider, RapierRigidBody } from "@react-three/rapier"; import { RapierRigidBody } from "@react-three/rapier";
import { SyncRigidBody } from "./SyncRigidBody"; import { SyncRigidBody } from "./SyncRigidBody";
import { reaction, runInAction } from "mobx"; import { reaction, runInAction } from "mobx";
import { v3toRapier } from "../utils"; import { v3toRapier } from "../utils";
import { state } from "../state"; import { state } from "../state";
import { buildVoxelGroupMesh } from "../utils/graphics/voxelMesh";
const VoxelGroupMesh = memo(function VoxelGroupMesh({ vg, opacity }: { vg: VoxelGroup, opacity: number }) {
const geo = useMemo(() => buildVoxelGroupMesh(vg.positions), [vg]);
useEffect(() => () => { geo.dispose(); }, [geo]);
const op = vg.opacity * opacity;
return (
<mesh geometry={geo} userData={{ baseOpacity: vg.opacity }}>
<meshStandardMaterial color={vg.color} opacity={op} transparent={op < 1} />
</mesh>
);
});
export type ObjectViewInternalHandle = { export type ObjectViewInternalHandle = {
group: Group | null; group: Group | null;
@@ -71,8 +56,6 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
[object, object.id, props.isPlayer] [object, object.id, props.isPlayer]
); );
const gameObj = object as RuntimeGameObjectInstance;
useEffect( useEffect(
() => { () => {
return reaction( return reaction(
@@ -98,8 +81,8 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
useEffect( useEffect(
() => { () => {
runInAction(() => { gameObj.rb = () => rbRef.current; }); runInAction(() => { object.rb = () => rbRef.current; });
return () => { runInAction(() => { gameObj.rb = () => null; }); }; return () => { runInAction(() => { object.rb = () => null; }); };
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[object.id], [object.id],
@@ -130,9 +113,11 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
return; return;
const otherObj = Object.values(game.scene.objects) const otherObj = Object.values(game.scene.objects)
.find((o) => o.rb()?.handle === otherHandle); .find((o) => o.rb()?.handle === otherHandle);
game.emitEvent('objectTouchStart', { object: gameObj, other: otherObj });
const context = { object: { ...object, typeId: objectType.id }, other: otherObj };
game.emitEvent('objectTouchStart', context);
if (otherObj?.id === game.scene.playerObjectId) if (otherObj?.id === game.scene.playerObjectId)
game.emitEvent('playerTouchStart', { object: gameObj, other: otherObj }); game.emitEvent('playerTouchStart', context);
}} }}
onSync={(data) => { onSync={(data) => {
runInAction(() => { runInAction(() => {
+1 -1
View File
@@ -4,7 +4,7 @@ import { OrbitControls } from '@react-three/drei';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { state } from '../state'; import { state } from '../state';
import { SceneView } from './SceneView'; import { SceneView } from './SceneView';
import { MOUSE, type OrthographicCamera, type PerspectiveCamera } from 'three'; import { MOUSE } from 'three';
import { useSyncCamera } from '../utils/hooks/useSyncCamera'; import { useSyncCamera } from '../utils/hooks/useSyncCamera';
import { Physics } from '@react-three/rapier'; import { Physics } from '@react-three/rapier';
+3 -3
View File
@@ -1,11 +1,11 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import type { RuntimeScene } from "../types";
import { GameObjectView } from "./GameObjectView"; import { GameObjectView } from "./GameObjectView";
import { ObjectEditorView } from "./ObjectEditorView"; import { ObjectEditorView } from "./ObjectEditorView";
import { DebugLabelsView } from "./DebugLabelsView"; import { DebugLabelsView } from "./DebugLabelsView";
import type { Scene } from "../types";
type SceneViewProps = { type SceneViewProps = {
scene: RuntimeScene, scene: Scene,
isEditor?: boolean; isEditor?: boolean;
debugOverlay?: boolean; debugOverlay?: boolean;
} }
@@ -18,7 +18,7 @@ export const SceneView = observer(function (props: SceneViewProps) {
Object.values(props.scene.objects) Object.values(props.scene.objects)
.map((obj) => ( .map((obj) => (
!props.isEditor !props.isEditor
? <GameObjectView key={obj.id} object={obj as RuntimeGameObjectInstance} isPlayer={props.scene.playerObjectId === obj.id} isSceneEditor={props.isEditor} /> ? <GameObjectView key={obj.id} object={obj} isPlayer={props.scene.playerObjectId === obj.id} isSceneEditor={props.isEditor} />
: <ObjectEditorView key={obj.id} object={obj} isPlayer={props.scene.playerObjectId === obj.id} /> : <ObjectEditorView key={obj.id} object={obj} isPlayer={props.scene.playerObjectId === obj.id} />
)) ))
} }
+1 -18
View File
@@ -8,7 +8,6 @@ import { SceneEditorView } from './SceneEditorView';
import { JoystickView } from './JoystickView'; import { JoystickView } from './JoystickView';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { runInAction } from 'mobx'; import { runInAction } from 'mobx';
import { IconPlayerPlayFilled, IconPlayerPauseFilled, IconPlayerStopFilled, IconPlayerSkipForwardFilled } from '@tabler/icons-react';
export const ThreeView = observer(function () { export const ThreeView = observer(function () {
const isGame = !!state.game; const isGame = !!state.game;
@@ -39,23 +38,7 @@ export const ThreeView = observer(function () {
{isGame ? <GameView /> : <SceneEditorView />} {isGame ? <GameView /> : <SceneEditorView />}
</Canvas> </Canvas>
{isGame && <JoystickView />} {isGame && <JoystickView />}
<div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4, fontSize: 14 }}>
{
state.game
? <>
<button onClick={() => state.stopGame()}><IconPlayerStopFilled size="1em" /></button>
{
state.game!.isPaused
? <>
<button onClick={() => state.game!.resume()}><IconPlayerPlayFilled size="1em" /></button>
<button onClick={() => state.game!.step()}><IconPlayerSkipForwardFilled size="1em" /></button>
</>
: <button onClick={() => state.game!.pause()}><IconPlayerPauseFilled size="1em" /></button>
}
</>
: <button onClick={() => state.startGame()}><IconPlayerPlayFilled size="1em" /></button>
}
</div>
</div> </div>
</KeyboardControls> </KeyboardControls>
); );
@@ -6,19 +6,22 @@ import { SceneEditorObjectSelectionLayer } from "./SceneEditor/SceneEditorObject
import { LightsLayer } from "./LighsLayer"; import { LightsLayer } from "./LighsLayer";
import { StatsLayer } from './StatsLayer'; import { StatsLayer } from './StatsLayer';
import { SceneEditorObjectGizmosLayer } from "./SceneEditor/SceneEditorObjectGizmosLayer"; import { SceneEditorObjectGizmosLayer } from "./SceneEditor/SceneEditorObjectGizmosLayer";
import { PlayButtons } from "./PlayButtons";
import { SceneEditorPlayerGizmosLayer } from "./SceneEditor/SceneEditorPlayerGizmosLayer";
export const LayeredThreeView = observer(function () { export const LayeredThreeView = observer(function () {
return <> return <>
<Canvas> <Canvas>
<LightsLayer /> <LightsLayer />
{/* <Perf position="bottom-right" /> */}
<SceneEditorCameraLayer /> <SceneEditorCameraLayer />
<ObjectGeometriesLayer> <ObjectGeometriesLayer>
<SceneEditorObjectSelectionLayer /> <SceneEditorObjectSelectionLayer />
<SceneEditorObjectGizmosLayer /> <SceneEditorObjectGizmosLayer />
</ObjectGeometriesLayer> </ObjectGeometriesLayer>
<SceneEditorPlayerGizmosLayer />
<StatsLayer /> <StatsLayer />
</Canvas> </Canvas>
<PlayButtons />
</>; </>;
}); });
@@ -0,0 +1,23 @@
import { observer } from "mobx-react-lite";
import { state } from "../../state";
import { IconPlayerPlayFilled, IconPlayerPauseFilled, IconPlayerStopFilled, IconPlayerSkipForwardFilled } from '@tabler/icons-react';
export const PlayButtons = observer(function () {
return <div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4, fontSize: 14 }}>
{
state.game
? <>
<button onClick={() => state.stopGame()}><IconPlayerStopFilled size="1em" /></button>
{
state.game!.isPaused
? <>
<button onClick={() => state.game!.resume()}><IconPlayerPlayFilled size="1em" /></button>
<button onClick={() => state.game!.step()}><IconPlayerSkipForwardFilled size="1em" /></button>
</>
: <button onClick={() => state.game!.pause()}><IconPlayerPauseFilled size="1em" /></button>
}
</>
: <button onClick={() => state.startGame()}><IconPlayerPlayFilled size="1em" /></button>
}
</div>;
});
@@ -0,0 +1,72 @@
import { useEffect, useMemo, useRef } from "react";
import * as THREE from "three";
import { useThree, useFrame } from "@react-three/fiber";
import { observer } from "mobx-react-lite";
import { state } from "../../../state";
import { computeBoundingBox } from "../../../utils/runtime";
const _offset = new THREE.Vector3();
const _lookAt = new THREE.Vector3();
const _euler = new THREE.Euler(0, 0, 0, 'YXZ');
export const SceneEditorPlayerGizmosLayer = observer(function () {
const scene = useThree(s => s.scene);
const helperRef = useRef<THREE.CameraHelper | null>(null);
const playerCam = useMemo(() => new THREE.PerspectiveCamera(75, 16 / 9, 0.1, 1000), []);
const playerObjectId = state.world.data.initialScene.playerObjectId;
const playerObject = playerObjectId
? state.world.data.initialScene.objects[playerObjectId]
: undefined;
const playerType = playerObject
? state.world.data.objectTypes[playerObject.typeId]
: undefined;
useEffect(() => {
if (!playerObject || !playerType)
return;
const helper = new THREE.CameraHelper(playerCam);
scene.add(helper);
helperRef.current = helper;
return () => {
scene.remove(helper);
helper.dispose();
helperRef.current = null;
};
}, [playerObject, playerType, playerCam, scene]);
useFrame(() => {
if (!playerObject || !playerType || !helperRef.current)
return;
const bb = computeBoundingBox(playerType);
const W = bb.max[0] - bb.min[0];
const H = bb.max[1] - bb.min[1];
const D = bb.max[2] - bb.min[2];
const radius = Math.sqrt(W * W + H * H + D * D);
const centerY = (bb.min[1] + bb.max[1]) / 2;
const yaw = playerObject.rotation[1];
_offset.set(W * 0.1, centerY + H * 0.3, bb.max[2] + radius);
_offset.applyEuler(_euler.set(0, yaw, 0, 'YXZ'));
playerCam.position.set(
playerObject.position[0] + _offset.x,
playerObject.position[1] + _offset.y,
playerObject.position[2] + _offset.z,
);
_lookAt.set(
playerObject.position[0],
playerObject.position[1] + centerY,
playerObject.position[2],
);
playerCam.lookAt(_lookAt);
playerCam.updateMatrixWorld(true);
helperRef.current.update();
});
return null;
});
@@ -49,10 +49,15 @@ export const StatsLayer = function () {
]; ];
for (const [name, entry] of Object.entries(layerTimings)) { for (const [name, entry] of Object.entries(layerTimings)) {
const parts: string[] = []; const frameTime = entry.frame.frames > 0 ? (entry.frame.acc / entry.frame.frames) : 0;
parts.push(`frame ${(entry.frame.frames > 0 ? (entry.frame.acc / entry.frame.frames) : 0).toFixed(3)}`); const renderTime = entry.render.frames > 0 ? (entry.render.acc / entry.render.frames) : 0;
parts.push(`render ${(entry.render.frames > 0 ? (entry.render.acc / entry.render.frames) : 0).toFixed(3)}`); lines.push(
lines.push(`${name} ${parts.join(" ")} ms`); `${name} ${(renderTime + frameTime).toFixed(3)} ms` +
(frameTime > 0
? ` (${frameTime.toFixed(3)} frame)`
: ''
)
);
entry.frame.acc = 0; entry.frame.acc = 0;
entry.frame.frames = 0; entry.frame.frames = 0;
+1 -2
View File
@@ -1,7 +1,6 @@
import { toJS } from "mobx"; import { toJS } from "mobx";
import type { Game, Scene, ScriptAction, ScriptCoroutineContext, World } from "../types"; import type { Game, Scene, ScriptAction, ScriptCoroutineContext, World } from "../types";
import { clone } from "../utils"; import { clone } from "../utils";
import { populateRuntimeScene } from "../utils/runtime";
import { ScriptGameEventBus } from "../scripts/eventBus"; import { ScriptGameEventBus } from "../scripts/eventBus";
import * as ScriptActions from "../scripts/actions"; import * as ScriptActions from "../scripts/actions";
import { positionOfObject } from "../scripts/positionOfObject"; import { positionOfObject } from "../scripts/positionOfObject";
@@ -19,7 +18,7 @@ import type { ScriptCoroutines } from "../scripts/coroutines";
export class GameFactory { export class GameFactory {
public static create(world: World): Game { public static create(world: World): Game {
const scene = populateRuntimeScene(clone(world.initialScene), world); const scene = clone(world.initialScene);
const game = { const game = {
paused: false, paused: false,
+8 -9
View File
@@ -1,11 +1,10 @@
import { type RuntimeWorld, type World } from "../types"; import { type World } from "../types";
import { depopulateRuntimeWorld, populateRuntimeWorld } from "../utils/runtime";
import { DEFAULT_VOXEL_TYPES } from "./defaultVoxelTypes"; import { DEFAULT_VOXEL_TYPES } from "./defaultVoxelTypes";
export class WorldFactory { export class WorldFactory {
public static create(): RuntimeWorld { public static create(): World {
return populateRuntimeWorld({ return {
objectTypes: {}, objectTypes: {},
voxelTypes: DEFAULT_VOXEL_TYPES, voxelTypes: DEFAULT_VOXEL_TYPES,
initialScene: { initialScene: {
@@ -19,17 +18,17 @@ export class WorldFactory {
position: [0, 5, 10], position: [0, 5, 10],
look: [0, 0, 0], look: [0, 0, 0],
}, },
}); };
} }
public static load(): RuntimeWorld | undefined { public static load(): World | undefined {
const json = localStorage.getItem("world"); const json = localStorage.getItem("world");
if (json) { if (json) {
return populateRuntimeWorld(JSON.parse(json) as World); return JSON.parse(json) as World;
} }
} }
public static save(world: RuntimeWorld): void { public static save(world: World): void {
localStorage.setItem("world", JSON.stringify(depopulateRuntimeWorld(world))); localStorage.setItem("world", JSON.stringify(world));
} }
} }
+2 -2
View File
@@ -1,6 +1,6 @@
import type { RuntimeGameObjectInstance, V3 } from "../types"; import type { ObjectInstance, V3 } from "../types";
export function positionOfObject(obj: RuntimeGameObjectInstance): V3 { export function positionOfObject(obj: ObjectInstance): V3 {
const t = obj.rb()?.translation(); const t = obj.rb()?.translation();
const result: V3 = t const result: V3 = t
? [t.x, t.y, t.z] ? [t.x, t.y, t.z]
+7
View File
@@ -0,0 +1,7 @@
import { ViewItem } from "./base";
export class PlayerViewItem extends ViewItem {
constructor() {
super();
}
}
+1 -2
View File
@@ -4,7 +4,6 @@ import { type ObjectInstance, type Pos3, type R3, type V3, type ObjectType, type
import { createObjectInstance } from "../utils/object"; import { createObjectInstance } from "../utils/object";
import { randomId } from "../utils"; import { randomId } from "../utils";
import { state } from "./rootState"; import { state } from "./rootState";
import { populateRuntimeObject } from "../utils/runtime";
import { DEFAULT_VOXEL_TYPE } from "../model/defaultVoxelTypes"; import { DEFAULT_VOXEL_TYPE } from "../model/defaultVoxelTypes";
export const ObjectEditModeEnum = [ export const ObjectEditModeEnum = [
@@ -130,7 +129,7 @@ export class WorldEditorState {
typeId: string, typeId: string,
pos: { x: number; y: number; z: number; }, pos: { x: number; y: number; z: number; },
): ObjectInstance { ): ObjectInstance {
const obj = populateRuntimeObject(createObjectInstance(randomId(), typeId), this.world.data); const obj = createObjectInstance(randomId(), typeId);
obj.position = [pos.x, pos.y, pos.z]; obj.position = [pos.x, pos.y, pos.z];
obj.rotation = [0, 0, 0]; obj.rotation = [0, 0, 0];
+2 -3
View File
@@ -5,7 +5,6 @@ import { state } from "./rootState";
import { DEFAULT_VOXEL_TYPES } from "../model/defaultVoxelTypes"; import { DEFAULT_VOXEL_TYPES } from "../model/defaultVoxelTypes";
import { wolf } from "../model/objectPrefabs/wolf"; import { wolf } from "../model/objectPrefabs/wolf";
import { terrainXZ } from "../model/objectPrefabs/terrain"; import { terrainXZ } from "../model/objectPrefabs/terrain";
import { populateRuntimeWorld } from "../utils/runtime";
export class WorldState { export class WorldState {
public data: World = WorldFactory.create(); public data: World = WorldFactory.create();
@@ -58,7 +57,7 @@ export class WorldState {
objects.map((obj) => [obj.id, obj]), objects.map((obj) => [obj.id, obj]),
) as Record<string, ObjectInstance>; ) as Record<string, ObjectInstance>;
this.data = populateRuntimeWorld({ this.data = {
objectTypes: { objectTypes: {
wolf: { wolf: {
id: 'wolf', id: 'wolf',
@@ -97,7 +96,7 @@ export class WorldState {
gameRules: { gameRules: {
gravity: true, gravity: true,
}, },
}); };
console.log(objects); console.log(objects);
state.worldEditor.resetSelection(); state.worldEditor.resetSelection();
+4 -4
View File
@@ -1,4 +1,4 @@
import type { ObjectInstance, ObjectType, RuntimeGameObjectInstance, Scene, World } from "../model"; import type { ObjectInstance, ObjectType, Scene, World } from "../model";
import type { ScriptAction } from "./actions"; import type { ScriptAction } from "./actions";
export type ScriptCoroutine = Generator<ScriptAction, void, void>; export type ScriptCoroutine = Generator<ScriptAction, void, void>;
@@ -9,15 +9,15 @@ export type NamedScriptCoroutine = {
} }
export type ScriptCoroutineContext = { export type ScriptCoroutineContext = {
thisObject: RuntimeGameObjectInstance, thisObject: ObjectInstance,
thisObjectType: ObjectType; thisObjectType: ObjectType;
}; };
export type ScriptContext = ScriptCoroutineContext & { export type ScriptContext = ScriptCoroutineContext & {
world: World; world: World;
scene: Scene; scene: Scene;
object?: RuntimeGameObjectInstance; object?: ObjectInstance;
other?: RuntimeGameObjectInstance; other?: ObjectInstance;
} }
export type ScriptGameEventHandler = (context: ScriptContext) => ScriptCoroutine; export type ScriptGameEventHandler = (context: ScriptContext) => ScriptCoroutine;
+3
View File
@@ -10,5 +10,8 @@ export function createObjectInstance(id: string, typeId: string): ObjectInstance
position: [0, 0, 0], position: [0, 0, 0],
rotation: [0, 0, 0], rotation: [0, 0, 0],
scale: [1, 1, 1], scale: [1, 1, 1],
linearVelocity: [0, 0, 0],
angularVelocity: [0, 0, 0],
}; };
} }
-2
View File
@@ -1,3 +1 @@
export * from './world';
export * from './scene';
export * from './object'; export * from './object';
+1 -35
View File
@@ -1,6 +1,4 @@
import { getObjectVoxelGroups } from "../graphics/voxelGroup"; import type { ObjectType, V3 } from "../../types";
import type { ObjectInstance, ObjectType, RuntimeObjectInstance, RuntimeObjectType, V3, World } from "../../types";
import { buildObjectTrimesh } from "../graphics/mesh";
export function computeBoundingBox(objectType: ObjectType): { min: V3; max: V3 } { export function computeBoundingBox(objectType: ObjectType): { min: V3; max: V3 } {
if (!objectType.voxels.length) if (!objectType.voxels.length)
@@ -17,35 +15,3 @@ export function computeBoundingBox(objectType: ObjectType): { min: V3; max: V3 }
} }
return { min: [minX, minY, minZ], max: [maxX, maxY, maxZ] }; return { min: [minX, minY, minZ], max: [maxX, maxY, maxZ] };
} }
export function populateRuntimeObjectType(value: ObjectType, world: World): RuntimeObjectType {
return {
...value,
runtime: {
voxelGroups: getObjectVoxelGroups(value, world.voxelTypes),
colliderMesh: buildObjectTrimesh(value, world.voxelTypes),
boundingBox: computeBoundingBox(value),
updatedAt: 0,
},
};
}
export function populateRuntimeObject(value: ObjectInstance, _world: World): RuntimeObjectInstance {
return {
...value,
runtime: {
rb: () => null,
},
};
}
export function depopulateRuntimeObjectType(value: RuntimeObjectType, _world: World): ObjectType {
const { runtime: _, ...result } = value;
return result;
}
export function depopulateRuntimeObject(value: RuntimeObjectInstance, _world: World): ObjectInstance {
const { runtime: _, ...result } = value;
return result;
}
-16
View File
@@ -1,16 +0,0 @@
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)])),
};
}
-16
View File
@@ -1,16 +0,0 @@
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),
};
}