Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea8f251599 | ||
|
|
7ce9e6dca6 | ||
|
|
b873286dfd |
@@ -1,7 +0,0 @@
|
|||||||
# Скопируй в .env и заполни
|
|
||||||
SERVER_PORT=3000
|
|
||||||
CLIENT_PORT=5000
|
|
||||||
|
|
||||||
# Добавь свои переменные ниже
|
|
||||||
# DATABASE_URL=
|
|
||||||
# JWT_SECRET=
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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} />
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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,
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { ViewItem } from "./base";
|
||||||
|
|
||||||
|
export class PlayerViewItem extends ViewItem {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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];
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
export * from './world';
|
|
||||||
export * from './scene';
|
|
||||||
export * from './object';
|
export * from './object';
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)])),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user