├── .gitignore ├── front ├── public │ ├── .gitignore │ ├── Logo.png │ ├── font.ttf │ ├── LogoFlat.png │ ├── favicon.ico │ ├── BigPreview.webp │ ├── assets │ │ ├── Car.glb │ │ └── EzCar.glb │ ├── Logo-Removed.png │ ├── PreviewObbyGame.webp │ ├── PreviewTestGame.webp │ ├── PreviewPetSimGame.webp │ ├── PreviewFootballGame.webp │ ├── draco │ │ ├── draco_decoder.wasm │ │ ├── gltf │ │ │ └── draco_decoder.wasm │ │ └── README.md │ ├── sky │ │ ├── rustig_koppie_puresky.webp │ │ └── kloofendal_48d_partly_cloudy_puresky.webp │ ├── vercel.svg │ └── next.svg ├── postcss.config.js ├── .env.local ├── lib │ └── utils.ts ├── .eslintrc.json ├── game │ ├── ecs │ │ ├── component │ │ │ ├── CurrentPlayerComponent.ts │ │ │ ├── MeshComponent.ts │ │ │ ├── CameraFollowComponent.ts │ │ │ └── AnimationComponent.ts │ │ ├── entity │ │ │ ├── Chat.ts │ │ │ ├── Cube.ts │ │ │ ├── Player.ts │ │ │ ├── Sphere.ts │ │ │ └── FloatingText.ts │ │ └── system │ │ │ ├── index.ts │ │ │ ├── SleepCheckSystem.ts │ │ │ ├── SyncSizeSystem.ts │ │ │ ├── SyncPositionSystem.ts │ │ │ ├── SyncColorSystem.ts │ │ │ ├── SyncRotationSystem.ts │ │ │ ├── DestroySystem.ts │ │ │ ├── AnimationSystem.ts │ │ │ ├── InvisibilitySystem.ts │ │ │ ├── ServerMeshSystem.ts │ │ │ ├── MeshSystem.ts │ │ │ ├── ChatSystem.ts │ │ │ └── VehicleSystem.ts │ ├── Camera.ts │ ├── Hud.ts │ └── LoadManager.ts ├── next-env.d.ts ├── types.ts ├── next.config.js ├── app │ ├── layout.tsx │ ├── play │ │ └── [slug] │ │ │ └── page.tsx │ └── page.tsx ├── components.json ├── tsconfig.json ├── .gitignore ├── components │ ├── LoadingScreen.tsx │ ├── GamePlayer.tsx │ ├── ui │ │ ├── card.tsx │ │ └── button.tsx │ ├── Navbar.tsx │ └── KeyboardLayout.tsx ├── package.json ├── tailwind.config.js ├── README.md └── styles │ └── globals.css ├── Githack.webp ├── GameScreen1.webp ├── GameScreen2.webp ├── GameScreen3.webp ├── .dockerignore ├── .vscode └── settings.json ├── shared ├── network │ ├── server │ │ ├── index.ts │ │ ├── base.ts │ │ ├── connection.ts │ │ └── serialized.ts │ ├── client │ │ ├── chatMessage.ts │ │ ├── index.ts │ │ ├── base.ts │ │ ├── setPlayerNameMessage.ts │ │ ├── proximityPromptMessage.ts │ │ └── inputMessage.ts │ ├── config.ts │ ├── NetworkComponent.ts │ └── NetworkDataComponent.ts ├── component │ ├── Component.ts │ ├── events │ │ ├── ComponentWrapper.ts │ │ ├── ComponentAddedEvent.ts │ │ ├── ComponentUpdatedEvent.ts │ │ ├── EntityDestroyedEvent.ts │ │ ├── ComponentRemovedEvent.ts │ │ └── EventListComponent.ts │ ├── InvisibleComponent.ts │ ├── SingleSizeComponent.ts │ ├── StateComponent.ts │ ├── PlayerComponent.ts │ ├── ColorComponent.ts │ ├── PositionComponent.ts │ ├── VehicleOccupancyComponent.ts │ ├── SizeComponent.ts │ ├── ServerMeshComponent.ts │ ├── RotationComponent.ts │ ├── TextComponent.ts │ ├── VehicleComponent.ts │ ├── ProximityPromptComponent.ts │ └── MessageComponent.ts ├── entity │ └── EventQueue.ts └── system │ └── EntityManager.ts ├── back ├── src │ ├── physics │ │ └── rapier.ts │ ├── ecs │ │ ├── component │ │ │ ├── ZombieComponent.ts │ │ │ ├── tag │ │ │ │ └── TagChatComponent.ts │ │ │ ├── events │ │ │ │ ├── ProximityPromptInteractEvent.ts │ │ │ │ ├── ColorEvent.ts │ │ │ │ ├── SingleSizeEvent.ts │ │ │ │ ├── SizeEvent.ts │ │ │ │ ├── MessageEvent.ts │ │ │ │ ├── OnCollisionExitEvent.ts │ │ │ │ └── OnCollisionEnterEvent.ts │ │ │ ├── physics │ │ │ │ ├── ColliderComponent.ts │ │ │ │ ├── BoxColliderComponent.ts │ │ │ │ ├── SphereColliderComponent.ts │ │ │ │ ├── CapsuleColliderComponent.ts │ │ │ │ ├── DynamicRigidBodyComponent.ts │ │ │ │ ├── KinematicRigidBodyComponent.ts │ │ │ │ ├── VehicleRayCastComponent.ts │ │ │ │ ├── PhysicsPropertiesComponent.ts │ │ │ │ ├── ColliderPropertiesComponent.ts │ │ │ │ ├── TrimeshColliderComponent.ts │ │ │ │ └── ConvexHullColliderComponent.ts │ │ │ ├── InputComponent.ts │ │ │ ├── FollowTargetComponent.ts │ │ │ ├── RandomizeComponent.ts │ │ │ ├── GroundedComponent.ts │ │ │ ├── LockedRotationComponent.ts │ │ │ ├── SpawnPositionComponent.ts │ │ │ └── WebsocketComponent.ts │ │ ├── system │ │ │ ├── ScriptableSystem.ts │ │ │ ├── events │ │ │ │ ├── ColorEventSystem.ts │ │ │ │ ├── SizeEventSystem.ts │ │ │ │ ├── DestroyEventSystem.ts │ │ │ │ ├── SingleSizeEventSystem.ts │ │ │ │ ├── MessageEventSystem.ts │ │ │ │ └── ProximityPromptEventSystem.ts │ │ │ ├── InputProcessingSystem.ts │ │ │ ├── physics │ │ │ │ ├── SyncPositionSystem.ts │ │ │ │ ├── SyncRotationSystem.ts │ │ │ │ ├── PhysicsSystem.ts │ │ │ │ ├── SleepCheckSystem.ts │ │ │ │ ├── GroundedCheckSystem.ts │ │ │ │ ├── BoundaryCheckSystem.ts │ │ │ │ ├── LockRotationSystem.ts │ │ │ │ ├── CollisionSystem.ts │ │ │ │ ├── SphereColliderSystem.ts │ │ │ │ ├── BoxColliderSystem.ts │ │ │ │ ├── KinematicRigidBodySystem.ts │ │ │ │ ├── DynamicRigidBodySystem.ts │ │ │ │ └── CapsuleColliderSystem.ts │ │ │ ├── RandomizeSystem.ts │ │ │ └── VehicleCreationSystem.ts │ │ └── entity │ │ │ ├── FloatingText.ts │ │ │ ├── Chat.ts │ │ │ ├── MapWorld.ts │ │ │ ├── Mesh.ts │ │ │ ├── TriggerCube.ts │ │ │ └── OrbitalCompanion.ts │ └── GLTFLoaderManager.ts ├── .env ├── tsconfig.json ├── README.md ├── .eslintrc ├── package.json └── .gitignore ├── .prettierrc ├── monitor ├── .env.example ├── Dockerfile ├── tsconfig.json ├── package.json ├── package-lock.json └── .gitignore ├── Dockerfile ├── .github └── workflows │ └── deploy.yml └── LICENCE.md /.gitignore: -------------------------------------------------------------------------------- 1 | data/ 2 | -------------------------------------------------------------------------------- /front/public/.gitignore: -------------------------------------------------------------------------------- 1 | *.bl 2 | 3 | -------------------------------------------------------------------------------- /Githack.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/Githack.webp -------------------------------------------------------------------------------- /GameScreen1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/GameScreen1.webp -------------------------------------------------------------------------------- /GameScreen2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/GameScreen2.webp -------------------------------------------------------------------------------- /GameScreen3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/GameScreen3.webp -------------------------------------------------------------------------------- /front/public/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/Logo.png -------------------------------------------------------------------------------- /front/public/font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/font.ttf -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | 3 | **/dist 4 | **/.next 5 | *.png 6 | *.jpg 7 | *.webp 8 | -------------------------------------------------------------------------------- /front/public/LogoFlat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/LogoFlat.png -------------------------------------------------------------------------------- /front/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/favicon.ico -------------------------------------------------------------------------------- /front/public/BigPreview.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/BigPreview.webp -------------------------------------------------------------------------------- /front/public/assets/Car.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/assets/Car.glb -------------------------------------------------------------------------------- /front/public/Logo-Removed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/Logo-Removed.png -------------------------------------------------------------------------------- /front/public/assets/EzCar.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/assets/EzCar.glb -------------------------------------------------------------------------------- /front/public/PreviewObbyGame.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/PreviewObbyGame.webp -------------------------------------------------------------------------------- /front/public/PreviewTestGame.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/PreviewTestGame.webp -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "launch": { 3 | "configurations": [], 4 | "compounds": [] 5 | } 6 | } -------------------------------------------------------------------------------- /front/public/PreviewPetSimGame.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/PreviewPetSimGame.webp -------------------------------------------------------------------------------- /front/public/PreviewFootballGame.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/PreviewFootballGame.webp -------------------------------------------------------------------------------- /front/public/draco/draco_decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/draco/draco_decoder.wasm -------------------------------------------------------------------------------- /front/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /shared/network/server/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base.js' 2 | export * from './connection.js' 3 | export * from './serialized.js' 4 | -------------------------------------------------------------------------------- /back/src/physics/rapier.ts: -------------------------------------------------------------------------------- 1 | import Rapier from '@dimforge/rapier3d-compat' 2 | 3 | await Rapier.init() 4 | 5 | export default Rapier 6 | -------------------------------------------------------------------------------- /front/public/draco/gltf/draco_decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/draco/gltf/draco_decoder.wasm -------------------------------------------------------------------------------- /front/public/sky/rustig_koppie_puresky.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/sky/rustig_koppie_puresky.webp -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 100 7 | } -------------------------------------------------------------------------------- /front/.env.local: -------------------------------------------------------------------------------- 1 | # Development 2 | NEXT_PUBLIC_SERVER_URL=ws://localhost 3 | 4 | # Production (SSL Required) 5 | # NEXT_PUBLIC_SERVER_URL=wss://back.notblox.online -------------------------------------------------------------------------------- /monitor/.env.example: -------------------------------------------------------------------------------- 1 | DISCORD_WEBHOOK_URL=your_webhook_url_here 2 | DOCKER_GATEWAY=https://172.17.0.1 3 | CHECK_INTERVAL=10000 # Check interval in milliseconds 4 | -------------------------------------------------------------------------------- /front/public/sky/kloofendal_48d_partly_cloudy_puresky.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErcann/NotBlox/HEAD/front/public/sky/kloofendal_48d_partly_cloudy_puresky.webp -------------------------------------------------------------------------------- /shared/network/client/chatMessage.ts: -------------------------------------------------------------------------------- 1 | import { ClientMessage } from './base' 2 | 3 | export interface ChatMessage extends ClientMessage { 4 | content: string 5 | } 6 | -------------------------------------------------------------------------------- /shared/network/server/base.ts: -------------------------------------------------------------------------------- 1 | export enum ServerMessageType { 2 | SNAPSHOT = 0, 3 | FIRST_CONNECTION = 1, 4 | } 5 | 6 | export interface ServerMessage { 7 | t: ServerMessageType 8 | } 9 | -------------------------------------------------------------------------------- /front/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /shared/network/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base.js' 2 | export * from './inputMessage.js' 3 | export * from './chatMessage.js' 4 | export * from './proximityPromptMessage.js' 5 | export * from './setPlayerNameMessage.js' 6 | -------------------------------------------------------------------------------- /back/src/ecs/component/ZombieComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../../../shared/component/Component.js' 2 | 3 | export class ZombieComponent extends Component { 4 | constructor(entityId: number) { 5 | super(entityId) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /front/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "next/typescript" 5 | ], 6 | "rules": { 7 | "@next/next/no-html-link-for-pages": "off", 8 | "@next/next/no-img-element": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /front/game/ecs/component/CurrentPlayerComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@shared/component/Component' 2 | 3 | export class CurrentPlayerComponent extends Component { 4 | constructor(entityId: number) { 5 | super(entityId) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /front/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /shared/component/Component.ts: -------------------------------------------------------------------------------- 1 | export type ComponentConstructor = new ( 2 | entityId: number, 3 | ...args: any[] 4 | ) => T 5 | 6 | export class Component { 7 | constructor(public entityId: number) {} 8 | } 9 | -------------------------------------------------------------------------------- /back/src/ecs/component/tag/TagChatComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../../../../shared/component/Component.js' 2 | 3 | export class ChatComponent extends Component { 4 | constructor(entityId: number) { 5 | super(entityId) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /shared/component/events/ComponentWrapper.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../Component.js' 2 | 3 | export class ComponentWrapper extends Component { 4 | constructor(public component: T) { 5 | super(component.entityId) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /shared/network/client/base.ts: -------------------------------------------------------------------------------- 1 | export enum ClientMessageType { 2 | INPUT = 1, 3 | CHAT_MESSAGE = 2, 4 | PROXIMITY_PROMPT_INTERACT = 3, 5 | SET_PLAYER_NAME = 4, 6 | } 7 | 8 | export interface ClientMessage { 9 | t: ClientMessageType 10 | } 11 | -------------------------------------------------------------------------------- /back/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development # or production 2 | GAME_TICKRATE=20 # Game tickrate in Hz (20Hz = 50ms) 3 | GAME_SCRIPT=defaultScript.js # Script to run 4 | 5 | # Commented in dev mode : 6 | # FRONTEND_URL=https://www.notblox.online # Only accept connections from this URL -------------------------------------------------------------------------------- /shared/network/client/setPlayerNameMessage.ts: -------------------------------------------------------------------------------- 1 | import { ClientMessage, ClientMessageType } from './base.js' 2 | 3 | export interface SetPlayerNameMessage extends ClientMessage { 4 | t: ClientMessageType.SET_PLAYER_NAME 5 | // Player name to set 6 | name: string 7 | } 8 | -------------------------------------------------------------------------------- /shared/network/server/connection.ts: -------------------------------------------------------------------------------- 1 | // FIRST_CONNECTION 2 | import { ServerMessage } from './base' 3 | export interface ConnectionMessage extends ServerMessage { 4 | // Connected player entity id 5 | id: number 6 | // Server tick rate in Hz 7 | tickRate: number 8 | } 9 | -------------------------------------------------------------------------------- /front/types.ts: -------------------------------------------------------------------------------- 1 | export interface GameInfo { 2 | title: string 3 | slug: string 4 | imageUrl: string 5 | websocketPort: number 6 | images?: { url: string; width: number; height: number; alt: string; type: string }[] 7 | metaDescription: string 8 | markdown: string 9 | } 10 | -------------------------------------------------------------------------------- /front/game/ecs/component/MeshComponent.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Component } from '@shared/component/Component' 3 | 4 | export class MeshComponent extends Component { 5 | constructor(entityId: number, public mesh = new THREE.Mesh()) { 6 | super(entityId) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /back/src/ecs/component/events/ProximityPromptInteractEvent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../../../../shared/component/Component.js' 2 | 3 | export class ProximityPromptInteractEvent extends Component { 4 | constructor(entityId: number, public otherEntity: number) { 5 | super(entityId) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /front/game/ecs/component/CameraFollowComponent.ts: -------------------------------------------------------------------------------- 1 | import { Camera } from '@/game/Camera' 2 | import { Component } from '@shared/component/Component' 3 | 4 | export class CameraFollowComponent extends Component { 5 | constructor(entityId: number, public camera: Camera) { 6 | super(entityId) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /shared/component/events/ComponentAddedEvent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../Component.js' 2 | import { ComponentWrapper } from './ComponentWrapper.js' 3 | 4 | export class ComponentAddedEvent extends ComponentWrapper { 5 | constructor(component: T) { 6 | super(component) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /shared/component/events/ComponentUpdatedEvent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../Component.js' 2 | import { ComponentWrapper } from './ComponentWrapper.js' 3 | 4 | export class ComponentUpdatedEvent extends ComponentWrapper { 5 | constructor(component: T) { 6 | super(component) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /shared/network/client/proximityPromptMessage.ts: -------------------------------------------------------------------------------- 1 | import { ClientMessage } from './base' 2 | 3 | // When a player interacts with a proximity prompt, we send this message to the server 4 | export interface ProximityPromptInteractMessage extends ClientMessage { 5 | // Which entity is being interacted with 6 | eId: number 7 | } 8 | -------------------------------------------------------------------------------- /back/src/ecs/component/physics/ColliderComponent.ts: -------------------------------------------------------------------------------- 1 | import Rapier from '../../../physics/rapier.js' 2 | import { Component } from '../../../../../shared/component/Component.js' 3 | 4 | export class ColliderComponent extends Component { 5 | constructor(entityId: number, public collider: Rapier.Collider) { 6 | super(entityId) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /back/src/ecs/component/physics/BoxColliderComponent.ts: -------------------------------------------------------------------------------- 1 | import Rapier from '../../../physics/rapier.js' 2 | import { Component } from '../../../../../shared/component/Component.js' 3 | 4 | export class BoxColliderComponent extends Component { 5 | constructor(entityId: number, public collider?: Rapier.Collider) { 6 | super(entityId) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /monitor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | # Install app dependencies 7 | COPY package*.json ./ 8 | RUN npm install 9 | 10 | # Bundle app source 11 | COPY . . 12 | 13 | # Compile TypeScript 14 | RUN npm run build 15 | 16 | # Run the app 17 | CMD [ "node", "dist/monitor.js" ] 18 | -------------------------------------------------------------------------------- /back/src/ecs/component/physics/SphereColliderComponent.ts: -------------------------------------------------------------------------------- 1 | import Rapier from '../../../physics/rapier.js' 2 | import { Component } from '../../../../../shared/component/Component.js' 3 | 4 | export class SphereColliderComponent extends Component { 5 | constructor(entityId: number, public collider?: Rapier.Collider) { 6 | super(entityId) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /back/src/ecs/component/physics/CapsuleColliderComponent.ts: -------------------------------------------------------------------------------- 1 | import Rapier from '../../../physics/rapier.js' 2 | import { Component } from '../../../../../shared/component/Component.js' 3 | 4 | export class CapsuleColliderComponent extends Component { 5 | constructor(entityId: number, public collider?: Rapier.Collider) { 6 | super(entityId) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /back/src/ecs/component/physics/DynamicRigidBodyComponent.ts: -------------------------------------------------------------------------------- 1 | import Rapier from '../../../physics/rapier.js' 2 | import { Component } from '../../../../../shared/component/Component.js' 3 | 4 | export class DynamicRigidBodyComponent extends Component { 5 | constructor(entityId: number, public body?: Rapier.RigidBody) { 6 | super(entityId) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /back/src/ecs/component/physics/KinematicRigidBodyComponent.ts: -------------------------------------------------------------------------------- 1 | import Rapier from '../../../physics/rapier.js' 2 | import { Component } from '../../../../../shared/component/Component.js' 3 | 4 | export class KinematicRigidBodyComponent extends Component { 5 | constructor(entityId: number, public body?: Rapier.RigidBody) { 6 | super(entityId) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /monitor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true 10 | }, 11 | "include": ["*.ts"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /back/src/ecs/component/events/ColorEvent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../../../../shared/component/Component.js' 2 | 3 | /** 4 | * Event to change the color of an entity. 5 | * Used by ColorEventSystem. 6 | */ 7 | export class ColorEvent extends Component { 8 | constructor(entityId: number, public color: string) { 9 | super(entityId) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /back/src/ecs/component/InputComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../../../shared/component/Component.js' 2 | 3 | export class InputComponent extends Component { 4 | up: boolean = false 5 | down: boolean = false 6 | left: boolean = false 7 | right: boolean = false 8 | space: boolean = false 9 | lookingYAngle = 0 10 | interact: boolean = false 11 | } 12 | -------------------------------------------------------------------------------- /back/src/ecs/component/FollowTargetComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../../../shared/component/Component.js' 2 | export class FollowTargetComponent extends Component { 3 | constructor( 4 | public entityId: number, 5 | public targetEntityId: number, 6 | public offset: { x: number; y: number; z: number } 7 | ) { 8 | super(entityId) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /back/src/ecs/component/RandomizeComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../../../shared/component/Component.js' 2 | 3 | /** 4 | * Randomize the entity's behavior. 5 | * For testing purposes mostly. 6 | * Used by RandomizeSystem. 7 | */ 8 | export class RandomizeComponent extends Component { 9 | constructor(entityId: number) { 10 | super(entityId) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /back/src/ecs/component/events/SingleSizeEvent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../../../../shared/component/Component.js' 2 | 3 | /** 4 | * Event to change the size of an entity. 5 | * Used by SingleSizeEventSystem. 6 | */ 7 | export class SingleSizeEvent extends Component { 8 | constructor(entityId: number, public size: number) { 9 | super(entityId) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /back/src/ecs/component/physics/VehicleRayCastComponent.ts: -------------------------------------------------------------------------------- 1 | import Rapier from '../../../physics/rapier.js' 2 | import { Component } from '../../../../../shared/component/Component.js' 3 | 4 | export class VehicleRayCastComponent extends Component { 5 | constructor(entityId: number, public raycastController: Rapier.DynamicRayCastVehicleController) { 6 | super(entityId) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /front/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, 4 | experimental: { 5 | externalDir: true, 6 | }, 7 | webpack: (config) => { 8 | config.resolve.extensionAlias = { 9 | '.js': ['.ts', '.tsx', '.js'], 10 | } 11 | 12 | return config 13 | }, 14 | } 15 | 16 | module.exports = nextConfig 17 | -------------------------------------------------------------------------------- /shared/network/client/inputMessage.ts: -------------------------------------------------------------------------------- 1 | import { ClientMessage } from './base' 2 | 3 | export interface InputMessage extends ClientMessage { 4 | // UP 5 | u: boolean 6 | // DOWN 7 | d: boolean 8 | // LEFT 9 | l: boolean 10 | // RIGHT 11 | r: boolean 12 | // SPACE 13 | s: boolean 14 | // Y angle 15 | y: number 16 | // INTERACTION 17 | i: boolean 18 | } 19 | -------------------------------------------------------------------------------- /back/src/ecs/system/ScriptableSystem.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Entity } from '../../../../shared/entity/Entity' 3 | 4 | export class ScriptableSystem { 5 | // Static function that can be overridden by a script 6 | public static update: (dt: number, entities: Entity[]) => void = ( 7 | dt: number, 8 | entities: Entity[] 9 | ) => {} 10 | } 11 | -------------------------------------------------------------------------------- /back/src/ecs/component/events/SizeEvent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../../../../shared/component/Component.js' 2 | 3 | /** 4 | * Event to change the size of an entity. 5 | * Used by SizeEventSystem. 6 | */ 7 | export class SizeEvent extends Component { 8 | constructor(entityId: number, public width: number, public height: number, public depth: number) { 9 | super(entityId) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /front/game/ecs/entity/Chat.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '@shared/entity/Entity' 2 | import { SerializedEntityType } from '@shared/network/server/serialized' 3 | import { EntityManager } from '@shared/system/EntityManager' 4 | 5 | export class Chat { 6 | entity: Entity 7 | constructor(entityId: number) { 8 | this.entity = EntityManager.createEntity(SerializedEntityType.CHAT, entityId) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /front/game/ecs/entity/Cube.ts: -------------------------------------------------------------------------------- 1 | import { SerializedEntityType } from '@shared/network/server/serialized' 2 | import { Entity } from '@shared/entity/Entity' 3 | import { EntityManager } from '@shared/system/EntityManager.js' 4 | 5 | export class Cube { 6 | entity: Entity 7 | constructor(entityId: number) { 8 | this.entity = EntityManager.createEntity(SerializedEntityType.CUBE, entityId) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /front/game/ecs/entity/Player.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '@shared/entity/Entity' 2 | import { SerializedEntityType } from '@shared/network/server/serialized' 3 | import { EntityManager } from '@shared/system/EntityManager' 4 | 5 | export class Player { 6 | entity: Entity 7 | 8 | constructor(entityId: number) { 9 | this.entity = EntityManager.createEntity(SerializedEntityType.PLAYER, entityId) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /front/game/ecs/entity/Sphere.ts: -------------------------------------------------------------------------------- 1 | import { SerializedEntityType } from '@shared/network/server/serialized' 2 | import { Entity } from '@shared/entity/Entity' 3 | import { EntityManager } from '@shared/system/EntityManager.js' 4 | 5 | export class Sphere { 6 | entity: Entity 7 | constructor(entityId: number) { 8 | this.entity = EntityManager.createEntity(SerializedEntityType.SPHERE, entityId) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /back/src/ecs/component/GroundedComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../../../shared/component/Component.js' 2 | 3 | /** 4 | * 5 | * Launches a raycast downwards to check if the entity is grounded. 6 | * 7 | * Checked by `GroundCheckSystem`. 8 | */ 9 | export class GroundCheckComponent extends Component { 10 | constructor(entityId: number, public grounded: boolean = false) { 11 | super(entityId) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /front/game/ecs/entity/FloatingText.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '@shared/entity/Entity' 2 | import { SerializedEntityType } from '@shared/network/server/serialized' 3 | import { EntityManager } from '@shared/system/EntityManager' 4 | 5 | export class FloatingText { 6 | entity: Entity 7 | constructor(entityId: number) { 8 | this.entity = EntityManager.createEntity(SerializedEntityType.FLOATING_TEXT, entityId) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /front/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | 3 | export default function RootLayout({ 4 | // Layouts must accept a children prop. 5 | // This will be populated with nested layouts or pages 6 | children, 7 | }: { 8 | children: React.ReactNode 9 | }) { 10 | return ( 11 | 12 | 13 |
{children}
14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /monitor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notblox-monitor", 3 | "version": "1.0.0", 4 | "description": "Health monitoring for Notblox game servers (Sends to Discord)", 5 | "main": "dist/monitor.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node dist/monitor.js" 9 | }, 10 | "dependencies": { 11 | "dotenv": "^16.0.3" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "^22.0.1", 15 | "typescript": "^5.0.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /front/game/ecs/component/AnimationComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@shared/component/Component' 2 | import * as THREE from 'three' 3 | 4 | export class AnimationComponent extends Component { 5 | mixer: THREE.AnimationMixer 6 | animationState: number = 0 7 | constructor( 8 | public entityId: number, 9 | public mesh: THREE.Mesh, 10 | public animations: THREE.AnimationClip[] 11 | ) { 12 | super(entityId) 13 | this.mixer = new THREE.AnimationMixer(mesh) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /back/src/ecs/component/LockedRotationComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../../../shared/component/Component.js' 2 | 3 | /** 4 | * Put this on a entity to lock its rotation 5 | * 6 | * Checked by `LockRotationSystem` 7 | * 8 | * Uses `rigidBodyDesc.lockRotations()` to lock the rotation 9 | * 10 | * Entity must have a `DynamicRigidBodyComponent` 11 | */ 12 | export class LockedRotationComponent extends Component { 13 | constructor(entityId: number) { 14 | super(entityId) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /back/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | 7 | "sourceMap": true, 8 | "declaration": false, 9 | 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "outDir": "dist", 15 | "rootDir": "..", 16 | "baseUrl": "..", 17 | "typeRoots": ["node_modules/@types"], 18 | } 19 | } -------------------------------------------------------------------------------- /back/src/ecs/component/physics/PhysicsPropertiesComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../../../../shared/component/Component.js' 2 | 3 | export interface PhysicsPropertiesComponentData { 4 | mass?: number 5 | enableCcd?: boolean 6 | angularDamping?: number 7 | linearDamping?: number 8 | gravityScale?: number 9 | } 10 | 11 | export class PhysicsPropertiesComponent extends Component { 12 | constructor(entityId: number, public data: PhysicsPropertiesComponentData) { 13 | super(entityId) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /back/src/ecs/component/physics/ColliderPropertiesComponent.ts: -------------------------------------------------------------------------------- 1 | // back/src/ecs/component/physics/ColliderPropertiesComponent.ts 2 | import { Component } from '../../../../../shared/component/Component.js' 3 | 4 | export interface ColliderPropertiesComponentData { 5 | isSensor?: boolean 6 | friction?: number 7 | restitution?: number 8 | } 9 | 10 | export class ColliderPropertiesComponent extends Component { 11 | constructor(entityId: number, public data: ColliderPropertiesComponentData) { 12 | super(entityId) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /back/README.md: -------------------------------------------------------------------------------- 1 | # Backend of NotBlox.online 2 | 3 | https://github.com/iErcann/Notblox 4 | 5 | ## How to run 6 | 7 | ```bash 8 | cd back 9 | npm install 10 | npm run dev 11 | ``` 12 | 13 | ## How to build 14 | 15 | ```bash 16 | cd back 17 | npm run build 18 | ``` 19 | 20 | ## How to run the build 21 | 22 | ```bash 23 | cd back 24 | npm run start 25 | ``` 26 | 27 | ## Production: SSL Websocket (WSS) 28 | 29 | Be sure to have SSL certificates for the websocket. 30 | Check the `WebsocketSystem.ts` file for the SSL certificates. 31 | -------------------------------------------------------------------------------- /front/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /back/src/ecs/component/physics/TrimeshColliderComponent.ts: -------------------------------------------------------------------------------- 1 | import Rapier from '../../../physics/rapier.js' 2 | import { Component } from '../../../../../shared/component/Component.js' 3 | 4 | export class TrimeshColliderComponent extends Component { 5 | constructor(entityId: number, public collider?: Rapier.Collider) { 6 | super(entityId) 7 | } 8 | } 9 | 10 | export class TrimeshCollidersComponent extends Component { 11 | constructor( 12 | entityId: number, 13 | public filePath: string, 14 | public colliders?: TrimeshColliderComponent[] 15 | ) { 16 | super(entityId) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /front/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /back/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:@typescript-eslint/eslint-recommended" 6 | ], 7 | "parserOptions": { 8 | "ecmaVersion": 2022, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "@typescript-eslint/no-explicit-any": "off" 13 | }, 14 | "settings": { 15 | "import/resolver": { 16 | "typescript": { 17 | "alwaysTryTypes": true, 18 | "project": "./tsconfig.json" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /back/src/ecs/component/SpawnPositionComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../../../shared/component/Component.js' 2 | 3 | /** 4 | * This component is used to set the spawn position of an entity. 5 | * 6 | * @param entityId - The ID of the entity that this component is attached to. 7 | * @param x - The x coordinate of the spawn position. 8 | * @param y - The y coordinate of the spawn position. 9 | * @param z - The z coordinate of the spawn position. 10 | */ 11 | export class SpawnPositionComponent extends Component { 12 | constructor(entityId: number, public x: number, public y: number, public z: number) { 13 | super(entityId) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /front/game/ecs/system/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SyncComponentsSystem' 2 | export * from './SyncPositionSystem' 3 | export * from './SyncRotationSystem' 4 | export * from './SyncColorSystem' 5 | export * from './SyncSizeSystem' 6 | export * from './AnimationSystem' 7 | export * from './DestroySystem' 8 | export * from './SleepCheckSystem' 9 | export * from './ChatSystem' 10 | export * from './OrbitCameraFollowSystem' 11 | export * from './MeshSystem' 12 | export * from './ServerMeshSystem' 13 | export * from './IdentifyFollowedMeshSystem' 14 | export * from './TextComponentSystem' 15 | export * from './InvisibilitySystem' 16 | export * from './VehicleSystem' 17 | -------------------------------------------------------------------------------- /front/game/ecs/system/SleepCheckSystem.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '@shared/entity/Entity' 2 | import { NetworkComponent } from '@shared/network/NetworkComponent' 3 | 4 | export class SleepCheckSystem { 5 | update(entities: Entity[]) { 6 | for (const entity of entities) { 7 | this.sleepNetworkComponent(entity) 8 | } 9 | } 10 | sleepNetworkComponent(entity: Entity) { 11 | const components = entity.getAllComponents() 12 | // Check if component is a NetworkComponent 13 | for (const component of components) { 14 | if (component instanceof NetworkComponent) { 15 | component.updated = false 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /shared/component/InvisibleComponent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedComponent, SerializedComponentType } from '../network/server/serialized.js' 2 | import { NetworkComponent } from '../network/NetworkComponent.js' 3 | 4 | // Attach this to an entity to make it invisible (Applies on the MeshComponent) 5 | export class InvisibleComponent extends NetworkComponent { 6 | constructor(entityId: number) { 7 | super(entityId, SerializedComponentType.INVISIBLE) 8 | } 9 | 10 | serialize(): SerializedInvisibleComponent { 11 | return {} 12 | } 13 | 14 | deserialize(data: SerializedInvisibleComponent): void {} 15 | } 16 | 17 | export interface SerializedInvisibleComponent extends SerializedComponent {} 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:22 AS build 3 | 4 | WORKDIR /app 5 | 6 | # Copy only necessary files for build 7 | COPY back/package*.json ./back/ 8 | COPY shared ./shared/ 9 | 10 | WORKDIR /app/back 11 | 12 | RUN npm ci 13 | 14 | COPY back ./ 15 | 16 | RUN npm run build 17 | 18 | # Production stage 19 | FROM node:22-slim 20 | 21 | WORKDIR /app/back 22 | 23 | # Copy package files 24 | COPY --from=build /app/back/package*.json ./ 25 | 26 | # Install production dependencies only 27 | RUN npm ci --omit=dev 28 | 29 | # Copy built files and scripts 30 | COPY --from=build /app/back/dist ./dist 31 | COPY --from=build /app/back/src/scripts ./src/scripts 32 | 33 | CMD ["node", "dist/back/src/sandbox.js"] 34 | -------------------------------------------------------------------------------- /shared/entity/EventQueue.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '../system/EntityManager.js' 2 | import { Entity } from './Entity.js' 3 | import { SerializedEntityType } from '../network/server/serialized.js' 4 | import { NetworkDataComponent } from '../network/NetworkDataComponent.js' 5 | import { EventListComponent } from '../component/events/EventListComponent.js' 6 | 7 | export class EventQueue { 8 | entity: Entity 9 | constructor() { 10 | this.entity = EntityManager.createEntity(SerializedEntityType.EVENT_QUEUE) 11 | 12 | this.entity.addComponent(new EventListComponent(this.entity.id), false) 13 | this.entity.addComponent(new NetworkDataComponent(this.entity.id, this.entity.type, []), false) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /shared/component/SingleSizeComponent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedComponent, SerializedComponentType } from '../network/server/serialized.js' 2 | 3 | import { NetworkComponent } from '../network/NetworkComponent.js' 4 | 5 | export class SingleSizeComponent extends NetworkComponent { 6 | constructor(entityId: number, public size: number) { 7 | super(entityId, SerializedComponentType.SINGLE_SIZE) 8 | } 9 | deserialize(data: SerializedSingleSizeComponent): void { 10 | this.size = data.size 11 | } 12 | serialize(): SerializedSingleSizeComponent { 13 | return { 14 | size: Number(this.size.toFixed(2)), 15 | } 16 | } 17 | } 18 | 19 | export interface SerializedSingleSizeComponent extends SerializedComponent { 20 | size: number 21 | } 22 | -------------------------------------------------------------------------------- /shared/component/StateComponent.ts: -------------------------------------------------------------------------------- 1 | import { NetworkComponent } from '../network/NetworkComponent.js' 2 | import { 3 | SerializedComponent, 4 | SerializedComponentType, 5 | SerializedStateType, 6 | } from '../network/server/serialized.js' 7 | 8 | export class StateComponent extends NetworkComponent { 9 | constructor(entityId: number, public state: SerializedStateType) { 10 | super(entityId, SerializedComponentType.STATE) 11 | } 12 | deserialize(data: SerializedStateComponent) { 13 | this.state = data.state 14 | } 15 | serialize(): SerializedStateComponent { 16 | return { state: this.state } 17 | } 18 | } 19 | 20 | export interface SerializedStateComponent extends SerializedComponent { 21 | state: SerializedStateType 22 | } 23 | -------------------------------------------------------------------------------- /shared/network/config.ts: -------------------------------------------------------------------------------- 1 | // Check if the environment is Node.js (server) 2 | const isServer = 3 | typeof process !== 'undefined' && process.versions != null && process.versions.node != null 4 | 5 | export const config = { 6 | /** 7 | * The server's tickrate is determined by the GAME_TICKRATE environment variable, defaulting to 20 if not set. 8 | * The client initially uses a default tickrate of 20, but this is updated when receiving the first 9 | * connection message from the server to match the server's actual tickrate. 10 | * See the ConnectionMessage type in connection.ts for details. 11 | */ 12 | SERVER_TICKRATE: isServer ? Number(process.env.GAME_TICKRATE) || 20 : 20, 13 | IS_SERVER: isServer, 14 | MAX_MESSAGE_CONTENT_LENGTH: 300, 15 | } 16 | -------------------------------------------------------------------------------- /shared/component/PlayerComponent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedComponent, SerializedComponentType } from '../network/server/serialized.js' 2 | import { NetworkComponent } from '../network/NetworkComponent.js' 3 | 4 | export class PlayerComponent extends NetworkComponent { 5 | constructor(entityId: number, public name: string = 'Player' + entityId) { 6 | super(entityId, SerializedComponentType.PLAYER) 7 | } 8 | 9 | serialize(): SerializedPlayerComponent { 10 | return { 11 | n: this.name, 12 | } 13 | } 14 | 15 | deserialize(data: SerializedPlayerComponent): void { 16 | if (data && data.n) { 17 | this.name = data.n 18 | } 19 | } 20 | } 21 | 22 | export interface SerializedPlayerComponent extends SerializedComponent { 23 | n: string 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /shared/component/ColorComponent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedComponent, SerializedComponentType } from '../network/server/serialized.js' 2 | import { NetworkComponent } from '../network/NetworkComponent.js' 3 | 4 | // Define a ColorComponent class 5 | export class ColorComponent extends NetworkComponent { 6 | constructor(entityId: number, public color: string) { 7 | super(entityId, SerializedComponentType.COLOR) // Call the parent constructor with the entityId 8 | } 9 | deserialize(data: SerializedColorComponent): void { 10 | this.color = data.color 11 | } 12 | serialize(): SerializedColorComponent { 13 | return { 14 | color: this.color, 15 | } 16 | } 17 | } 18 | 19 | export interface SerializedColorComponent extends SerializedComponent { 20 | color: string 21 | } 22 | -------------------------------------------------------------------------------- /front/game/Camera.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { OrbitCameraFollowSystem } from './ecs/system/OrbitCameraFollowSystem' 3 | import { Entity } from '@shared/entity/Entity' 4 | import { InputMessage } from '@shared/network/client/inputMessage' 5 | 6 | export class Camera extends THREE.PerspectiveCamera { 7 | defaultOffset = new THREE.Vector3(0, 5, 15) 8 | controlSystem: OrbitCameraFollowSystem 9 | 10 | constructor(renderer: THREE.WebGLRenderer) { 11 | super(70, window.innerWidth / window.innerHeight) 12 | this.position.copy(this.defaultOffset) 13 | this.controlSystem = new OrbitCameraFollowSystem(this, renderer) 14 | } 15 | 16 | update(dt: number, entities: Entity[], inputMessage: InputMessage) { 17 | this.controlSystem.update(dt, entities, inputMessage) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /back/src/ecs/component/WebsocketComponent.ts: -------------------------------------------------------------------------------- 1 | // WebSocketComponent.ts 2 | 3 | import { Component } from '../../../../shared/component/Component.js' 4 | 5 | export class WebSocketComponent extends Component { 6 | /** 7 | * Constructor for the WebSocketComponent class. 8 | * @param entityId - The ID of the entity this component is attached to. 9 | * @param ws - The WebSocket connection associated with the entity. 10 | * @param isFirstSnapshotSent - A flag indicating whether the first snapshot has been sent over the WebSocket connection. 11 | * On the first snapshot, all NetworkComponents are sent, regardless of their `updated` flag. 12 | */ 13 | constructor(entityId: number, public ws: any, public isFirstSnapshotSent = false) { 14 | super(entityId) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /back/src/ecs/component/physics/ConvexHullColliderComponent.ts: -------------------------------------------------------------------------------- 1 | import Rapier from '../../../physics/rapier.js' 2 | import { Component } from '../../../../../shared/component/Component.js' 3 | 4 | /** 5 | * https://rapier.rs/docs/user_guides/javascript/colliders/#convex-meshes 6 | * it will automatically compute the convex hull of the given set of points. A convex hull is the smallest convex shape that contains all the given points. 7 | */ 8 | 9 | /** 10 | * @param entityId Entity ID. 11 | * @param meshUrl Compute the convex hull from the given mesh .glb file (Path to the mesh file). 12 | * @param collider Rapier collider. 13 | */ 14 | export class ConvexHullColliderComponent extends Component { 15 | constructor(entityId: number, public meshUrl: string, public collider?: Rapier.Collider) { 16 | super(entityId) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /back/src/ecs/component/events/MessageEvent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedMessageType } from '../../../../../shared/network/server/serialized.js' 2 | import { Component } from '../../../../../shared/component/Component.js' 3 | 4 | 5 | /** 6 | * This event is triggered when a message is sent 7 | * 8 | * Messages can be : 9 | * - Global chat messages 10 | * - Targeted chat messages (to specific players) 11 | * - Global notifications 12 | * - Targeted notifications (to specific players) 13 | */ 14 | export class MessageEvent extends Component { 15 | constructor( 16 | entityId: number, 17 | public sender: string, 18 | public content: string, 19 | public messageType: SerializedMessageType = SerializedMessageType.GLOBAL_CHAT, 20 | public targetPlayerIds: number[] = [] // Only used for targeted messages 21 | ) { 22 | super(entityId) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /shared/component/events/EntityDestroyedEvent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedComponentType } from '../../network/server/serialized.js' 2 | import { NetworkComponent } from '../../network/NetworkComponent.js' 3 | 4 | /** 5 | * NetworkEvent that is sent when an entity is destroyed 6 | * 7 | * Used to clean up the entity on the client side (remove mesh, etc) 8 | */ 9 | export class EntityDestroyedEvent extends NetworkComponent { 10 | constructor(entityId: number) { 11 | super(entityId, SerializedComponentType.ENTITY_DESTROYED_EVENT) 12 | } 13 | 14 | deserialize(data: SerializedEntityDestroyedEvent): void { 15 | this.entityId = data.id 16 | } 17 | serialize(): SerializedEntityDestroyedEvent { 18 | return { 19 | id: this.entityId, 20 | } 21 | } 22 | } 23 | 24 | export interface SerializedEntityDestroyedEvent { 25 | // Destroyed entity id 26 | id: number 27 | } 28 | -------------------------------------------------------------------------------- /back/src/ecs/component/events/OnCollisionExitEvent.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../../../../../shared/entity/Entity.js' 2 | import { Component } from '../../../../../shared/component/Component.js' 3 | 4 | /** 5 | * This component is used to handle the collision exit event. 6 | * Used by CollisionSystem. 7 | * 8 | * @param entityId - The ID of the entity that this component is attached to. 9 | * @param callback - The callback function to be called when the collision exit event occurs. 10 | */ 11 | export class OnCollisionExitEvent extends Component { 12 | constructor(entityId: number, public callback: (collidedWithEntity: Entity) => void) { 13 | super(entityId) 14 | } 15 | 16 | onCollisionExit(collidedWithEntity: Entity) { 17 | try { 18 | this.callback(collidedWithEntity) 19 | } catch (error) { 20 | console.error('Error in collision exit callback:', error) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /shared/component/PositionComponent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedComponent, SerializedComponentType } from '../network/server/serialized.js' 2 | import { NetworkComponent } from '../network/NetworkComponent.js' 3 | 4 | export class PositionComponent extends NetworkComponent { 5 | constructor(entityId: number, public x: number, public y: number, public z: number) { 6 | super(entityId, SerializedComponentType.POSITION) 7 | } 8 | deserialize(data: SerializedPositionComponent): void { 9 | this.x = data.x 10 | this.y = data.y 11 | this.z = data.z 12 | } 13 | serialize(): SerializedPositionComponent { 14 | return { 15 | x: Number(this.x.toFixed(2)), 16 | y: Number(this.y.toFixed(2)), 17 | z: Number(this.z.toFixed(2)), 18 | } 19 | } 20 | } 21 | 22 | export interface SerializedPositionComponent extends SerializedComponent { 23 | x: number 24 | y: number 25 | z: number 26 | } 27 | -------------------------------------------------------------------------------- /back/src/ecs/component/events/OnCollisionEnterEvent.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../../../../../shared/entity/Entity.js' 2 | import { Component } from '../../../../../shared/component/Component.js' 3 | 4 | /** 5 | * This component is used to handle the collision enter event. 6 | * Used by CollisionSystem. 7 | * 8 | * @param entityId - The ID of the entity that this component is attached to. 9 | * @param callback - The callback function to be called when the collision enter event occurs. 10 | */ 11 | export class OnCollisionEnterEvent extends Component { 12 | constructor(entityId: number, public callback: (collidedWithEntity: Entity) => void) { 13 | super(entityId) 14 | } 15 | 16 | onCollisionEnter(collidedWithEntity: Entity) { 17 | try { 18 | this.callback(collidedWithEntity) 19 | } catch (error) { 20 | console.error('Error in collision enter callback:', error) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /front/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "baseUrl": ".", 21 | "paths": { 22 | "@/*": [ 23 | "./*" 24 | ], 25 | "@shared/*": [ 26 | "../shared/*" 27 | ] 28 | }, 29 | "plugins": [ 30 | { 31 | "name": "next" 32 | } 33 | ] 34 | }, 35 | "include": [ 36 | "next-env.d.ts", 37 | "**/*.ts", 38 | "**/*.tsx", 39 | ".next/types/**/*.ts" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /shared/component/VehicleOccupancyComponent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedComponent, SerializedComponentType } from '../network/server/serialized.js' 2 | import { NetworkComponent } from '../network/NetworkComponent.js' 3 | 4 | /** 5 | * Attach this component to an entity to track its occupancy in a vehicle. 6 | * Mostly attached to players when they enter a vehicle. 7 | */ 8 | 9 | export class VehicleOccupancyComponent extends NetworkComponent { 10 | constructor(entityId: number, public vehicleEntityId: number) { 11 | super(entityId, SerializedComponentType.VEHICLE_OCCUPANCY) 12 | } 13 | 14 | serialize(): SerializedVehicleOccupancyComponent { 15 | return { 16 | vId: this.vehicleEntityId, 17 | } 18 | } 19 | 20 | deserialize(data: SerializedVehicleOccupancyComponent): void { 21 | this.vehicleEntityId = data.vId 22 | } 23 | } 24 | 25 | export interface SerializedVehicleOccupancyComponent extends SerializedComponent { 26 | vId: number 27 | } 28 | -------------------------------------------------------------------------------- /shared/component/SizeComponent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedComponent, SerializedComponentType } from '../network/server/serialized.js' 2 | 3 | import { NetworkComponent } from '../network/NetworkComponent.js' 4 | 5 | export class SizeComponent extends NetworkComponent { 6 | constructor(entityId: number, public width: number, public height: number, public depth: number) { 7 | super(entityId, SerializedComponentType.SIZE) 8 | } 9 | deserialize(data: SerializedSizeComponent): void { 10 | this.width = data.width 11 | this.height = data.height 12 | this.depth = data.depth 13 | } 14 | serialize(): SerializedSizeComponent { 15 | return { 16 | width: Number(this.width.toFixed(2)), 17 | height: Number(this.height.toFixed(2)), 18 | depth: Number(this.depth.toFixed(2)), 19 | } 20 | } 21 | } 22 | 23 | export interface SerializedSizeComponent extends SerializedComponent { 24 | width: number 25 | height: number 26 | depth: number 27 | } 28 | -------------------------------------------------------------------------------- /shared/network/NetworkComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../component/Component.js' 2 | import { SerializedComponentType } from './server/serialized.js' 3 | 4 | /** 5 | * `NetworkComponent` is an abstract class for components that need network synchronization. 6 | * It extends the base `Component` class. 7 | * 8 | * It has an `updated` property which, when true, indicates the component needs to be sent over the network. 9 | * The `SleepCheckSystem` resets this property to false at the end of each ECS loop. 10 | * 11 | * Always set `updated` to true when the component changes to ensure it is synchronized. 12 | * 13 | */ 14 | export abstract class NetworkComponent extends Component { 15 | updated: boolean = true 16 | constructor( 17 | entityId: number, 18 | public type: SerializedComponentType = SerializedComponentType.NONE 19 | ) { 20 | super(entityId) 21 | } 22 | 23 | abstract serialize(): any 24 | abstract deserialize(data: any): void 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Game Server 2 | on: 3 | push: 4 | branches: [main] 5 | paths-ignore: 6 | - 'front/**' 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: iercann/notblox-game-server 11 | 12 | jobs: 13 | build-push: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Log in to Registry 24 | uses: docker/login-action@v3 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Build and Push 31 | uses: docker/build-push-action@v5 32 | with: 33 | context: . 34 | push: true 35 | tags: | 36 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest 37 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} 38 | -------------------------------------------------------------------------------- /back/src/ecs/system/events/ColorEventSystem.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '../../../../../shared/system/EntityManager.js' 2 | import { ColorComponent } from '../../../../../shared/component/ColorComponent.js' 3 | import { Entity } from '../../../../../shared/entity/Entity.js' 4 | import { ColorEvent } from '../../component/events/ColorEvent.js' 5 | import { EventSystem } from '../../../../../shared/system/EventSystem.js' 6 | 7 | export class ColorEventSystem { 8 | update(entities: Entity[]) { 9 | const eventColors = EventSystem.getEvents(ColorEvent) 10 | 11 | for (const eventColor of eventColors) { 12 | const entity = EntityManager.getEntityById(entities, eventColor.entityId) 13 | if (!entity) return 14 | 15 | const colorComponent = entity.getComponent(ColorComponent) 16 | if (!colorComponent) return 17 | 18 | if (colorComponent && eventColor) { 19 | colorComponent.color = eventColor.color 20 | colorComponent.updated = true 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /back/src/ecs/system/InputProcessingSystem.ts: -------------------------------------------------------------------------------- 1 | import { InputComponent } from '../component/InputComponent.js' 2 | import { Entity } from '../../../../shared/entity/Entity.js' 3 | import { InputMessage } from '../../../../shared/network/client/inputMessage.js' 4 | 5 | export class InputProcessingSystem { 6 | constructor() {} 7 | 8 | receiveInputPacket(playerEntity: Entity, inputMessage: InputMessage) { 9 | let inputComponent = playerEntity.getComponent(InputComponent) 10 | 11 | if (!inputComponent) { 12 | inputComponent = new InputComponent(playerEntity.id) 13 | playerEntity.addComponent(inputComponent) 14 | } 15 | 16 | // Update the InputComponent based on the received packet 17 | inputComponent.down = inputMessage.d 18 | inputComponent.up = inputMessage.u 19 | inputComponent.left = inputMessage.l 20 | inputComponent.right = inputMessage.r 21 | inputComponent.space = inputMessage.s 22 | inputComponent.lookingYAngle = inputMessage.y 23 | inputComponent.interact = inputMessage.i 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /shared/component/ServerMeshComponent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedComponent, SerializedComponentType } from '../network/server/serialized.js' 2 | import { NetworkComponent } from '../network/NetworkComponent.js' 3 | 4 | // TODO: Fix the async load of the mesh in the front 5 | // This will fix the color of the mesh not being set because we need for the mesh to be loaded before setting the color 6 | /** 7 | * Holds the path to the rendered mesh file 8 | * Will be rendered by the client with a MeshComponent 9 | */ 10 | export class ServerMeshComponent extends NetworkComponent { 11 | constructor(entityId: number, public filePath: string) { 12 | super(entityId, SerializedComponentType.SERVER_MESH) 13 | } 14 | serialize(): SerializedMeshComponent { 15 | return { 16 | p: this.filePath, 17 | } 18 | } 19 | deserialize(data: SerializedMeshComponent): void { 20 | this.filePath = data.p 21 | } 22 | } 23 | 24 | export interface SerializedMeshComponent extends SerializedComponent { 25 | /* Path to the mesh file */ 26 | p: string 27 | } 28 | -------------------------------------------------------------------------------- /front/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | .next/ 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | 33 | # node.js 34 | # 35 | node_modules/ 36 | npm-debug.log 37 | yarn-error.log 38 | 39 | # BUCK 40 | buck-out/ 41 | \.buckd/ 42 | *.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 50 | 51 | fastlane/report.xml 52 | fastlane/Preview.html 53 | fastlane/screenshots 54 | 55 | *.glb 56 | *.blend 57 | -------------------------------------------------------------------------------- /back/src/ecs/system/physics/SyncPositionSystem.ts: -------------------------------------------------------------------------------- 1 | import { PositionComponent } from '../../../../../shared/component/PositionComponent.js' 2 | import { Entity } from '../../../../../shared/entity/Entity.js' 3 | import { DynamicRigidBodyComponent } from '../../component/physics/DynamicRigidBodyComponent.js' 4 | import { KinematicRigidBodyComponent } from '../../component/physics/KinematicRigidBodyComponent.js' 5 | 6 | export class SyncPositionSystem { 7 | update(entities: Entity[]) { 8 | for (const entity of entities) { 9 | const bodyComponent = 10 | entity.getComponent(DynamicRigidBodyComponent) || 11 | entity.getComponent(KinematicRigidBodyComponent) 12 | const positionComponent = entity.getComponent(PositionComponent) 13 | 14 | if (bodyComponent && positionComponent && bodyComponent.body) { 15 | const position = bodyComponent.body.translation() 16 | positionComponent.x = position.x 17 | positionComponent.y = position.y 18 | positionComponent.z = position.z 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /back/src/GLTFLoaderManager.ts: -------------------------------------------------------------------------------- 1 | import { DRACOLoader, GLTF, GLTFLoader } from 'node-three-gltf' 2 | 3 | export class GLTFLoaderManager { 4 | private static instance: GLTFLoaderManager 5 | private gltfLoader: GLTFLoader 6 | 7 | private constructor() { 8 | this.gltfLoader = new GLTFLoader() 9 | this.gltfLoader.setDRACOLoader(new DRACOLoader()) 10 | } 11 | 12 | static getInstance(): GLTFLoaderManager { 13 | if (!GLTFLoaderManager.instance) { 14 | GLTFLoaderManager.instance = new GLTFLoaderManager() 15 | } 16 | return GLTFLoaderManager.instance 17 | } 18 | 19 | static loadGLTFModel(url: string): Promise { 20 | return new Promise((resolve, reject) => { 21 | GLTFLoaderManager.getInstance().gltfLoader.load( 22 | url, 23 | (gltf) => { 24 | resolve(gltf) // Resolve the promise when loading is successful 25 | }, 26 | undefined, // onProgress callback (you can add one if needed) 27 | (error) => { 28 | reject(error) // Reject the promise on error 29 | } 30 | ) 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /back/src/ecs/system/physics/SyncRotationSystem.ts: -------------------------------------------------------------------------------- 1 | import { RotationComponent } from '../../../../../shared/component/RotationComponent.js' 2 | import { Entity } from '../../../../../shared/entity/Entity.js' 3 | import { DynamicRigidBodyComponent } from '../../component/physics/DynamicRigidBodyComponent.js' 4 | import { KinematicRigidBodyComponent } from '../../component/physics/KinematicRigidBodyComponent.js' 5 | 6 | export class SyncRotationSystem { 7 | update(entities: Entity[]) { 8 | for (const entity of entities) { 9 | const bodyComponent = 10 | entity.getComponent(DynamicRigidBodyComponent) || 11 | entity.getComponent(KinematicRigidBodyComponent) 12 | const rotationComponent = entity.getComponent(RotationComponent) 13 | 14 | if (bodyComponent && rotationComponent && bodyComponent.body) { 15 | const rotation = bodyComponent.body.rotation() 16 | rotationComponent.x = rotation.x 17 | rotationComponent.y = rotation.y 18 | rotationComponent.z = rotation.z 19 | rotationComponent.w = rotation.w 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /front/game/Hud.ts: -------------------------------------------------------------------------------- 1 | // Binding react states - game 2 | 3 | import { Dispatch, SetStateAction } from 'react' 4 | import { MessageComponent } from '@shared/component/MessageComponent' 5 | import { Game } from './Game' 6 | import { ClientMessageType } from '@shared/network/client/base' 7 | import { ChatMessage } from '@shared/network/client/chatMessage' 8 | import { config } from '@shared/network/config' 9 | 10 | // Props drill 11 | export class Hud { 12 | updateChat: Dispatch> | undefined 13 | passChatState(updateChat: Dispatch>) { 14 | // Update the type of setChat 15 | this.updateChat = updateChat 16 | } 17 | 18 | sendMessageToServer(message: string) { 19 | if (message === '') return 20 | // Limit message length to 300 characters 21 | const trimmedMessage = message.slice(0, config.MAX_MESSAGE_CONTENT_LENGTH) 22 | const chatMessage: ChatMessage = { 23 | t: ClientMessageType.CHAT_MESSAGE, 24 | content: trimmedMessage, 25 | } 26 | Game.getInstance().websocketManager.send(chatMessage) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /front/game/ecs/system/SyncSizeSystem.ts: -------------------------------------------------------------------------------- 1 | import { SingleSizeComponent } from '@shared/component/SingleSizeComponent' 2 | import { MeshComponent } from '../component/MeshComponent' 3 | import { SizeComponent } from '@shared/component/SizeComponent' 4 | import { Entity } from '@shared/entity/Entity' 5 | 6 | export class SyncSizeSystem { 7 | update(entities: Entity[]) { 8 | for (const entity of entities) { 9 | const meshComponent = entity.getComponent(MeshComponent) 10 | if (!meshComponent) continue 11 | 12 | const sizeComponent = entity.getComponent(SizeComponent) 13 | if (sizeComponent && sizeComponent.updated) { 14 | meshComponent.mesh.scale.set(sizeComponent.width, sizeComponent.height, sizeComponent.depth) 15 | continue 16 | } 17 | 18 | const singleSizeComponent = entity.getComponent(SingleSizeComponent) 19 | if (singleSizeComponent && singleSizeComponent.updated) { 20 | meshComponent.mesh.scale.set( 21 | singleSizeComponent.size, 22 | singleSizeComponent.size, 23 | singleSizeComponent.size 24 | ) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /front/game/ecs/system/SyncPositionSystem.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Entity } from '@shared/entity/Entity' 3 | import { MeshComponent } from '../component/MeshComponent' 4 | import { PositionComponent } from '@shared/component/PositionComponent' 5 | import { VehicleComponent } from '@shared/component/VehicleComponent' 6 | export class SyncPositionSystem { 7 | update(entities: Entity[], interpolationFactor: number) { 8 | for (const entity of entities) { 9 | const meshComponent = entity.getComponent(MeshComponent) 10 | const positionComponent = entity.getComponent(PositionComponent) 11 | const vehicleComponent = entity.getComponent(VehicleComponent) 12 | if (meshComponent && positionComponent) { 13 | const targetPosition = new THREE.Vector3( 14 | positionComponent.x, 15 | positionComponent.y, 16 | positionComponent.z 17 | ) 18 | // Smooth vehicle more than other entities 19 | meshComponent.mesh.position.lerp( 20 | targetPosition, 21 | vehicleComponent ? 0.1 : interpolationFactor 22 | ) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /front/game/ecs/system/SyncColorSystem.ts: -------------------------------------------------------------------------------- 1 | import { ColorComponent } from '@shared/component/ColorComponent' 2 | import { Entity } from '@shared/entity/Entity' 3 | import * as THREE from 'three' 4 | import { MeshComponent } from '../component/MeshComponent' 5 | 6 | export class SyncColorSystem { 7 | update(entities: Entity[]) { 8 | for (const entity of entities) { 9 | const colorComponent = entity.getComponent(ColorComponent) 10 | const meshComponent = entity.getComponent(MeshComponent) 11 | if (colorComponent && meshComponent && colorComponent.updated) { 12 | // Iterate over all children of the mesh 13 | // and update the material color 14 | meshComponent.mesh.traverse((child) => { 15 | if (child instanceof THREE.Mesh) { 16 | child.material.skinning = true 17 | 18 | if (colorComponent.color !== 'default') { 19 | child.material.color = new THREE.Color(colorComponent.color) 20 | } 21 | } 22 | }) 23 | meshComponent.mesh.material = new THREE.MeshPhongMaterial({ 24 | color: colorComponent.color, 25 | }) 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /back/src/ecs/system/physics/PhysicsSystem.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../../../../../shared/network/config.js' 2 | import Rapier from '../../../physics/rapier.js' 3 | import { CollisionSystem } from './CollisionSystem.js' 4 | import { Entity } from '../../../../../shared/entity/Entity.js' 5 | 6 | export class PhysicsSystem { 7 | world: Rapier.World // Rapier World 8 | private static instance: PhysicsSystem 9 | private collisionSystem = new CollisionSystem() 10 | private eventQueue: Rapier.EventQueue 11 | 12 | constructor() { 13 | const gravity = { x: 0.0, y: -9.81 * 10, z: 0.0 } 14 | this.world = new Rapier.World(gravity) 15 | this.world.timestep = 1 / config.SERVER_TICKRATE 16 | this.eventQueue = new Rapier.EventQueue(true) 17 | console.log(`Physics World constructed with tick rate: ${config.SERVER_TICKRATE}`) 18 | } 19 | 20 | update(entities: Entity[]) { 21 | this.world.step(this.eventQueue) 22 | this.collisionSystem.update(entities, this.world, this.eventQueue) 23 | } 24 | static getInstance(): PhysicsSystem { 25 | if (!PhysicsSystem.instance) { 26 | PhysicsSystem.instance = new PhysicsSystem() 27 | } 28 | return PhysicsSystem.instance 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /front/game/ecs/system/SyncRotationSystem.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Entity } from '@shared/entity/Entity' 3 | import { MeshComponent } from '../component/MeshComponent' 4 | import { RotationComponent } from '@shared/component/RotationComponent' 5 | import { VehicleComponent } from '@shared/component/VehicleComponent' 6 | export class SyncRotationSystem { 7 | update(entities: Entity[], interpolationFactor: number) { 8 | for (const entity of entities) { 9 | const meshComponent = entity.getComponent(MeshComponent) 10 | const rotationComponent = entity.getComponent(RotationComponent) 11 | const vehicleComponent = entity.getComponent(VehicleComponent) 12 | 13 | if (meshComponent && rotationComponent) { 14 | const targetQuaternion = new THREE.Quaternion( 15 | rotationComponent.x, 16 | rotationComponent.y, 17 | rotationComponent.z, 18 | rotationComponent.w 19 | ) 20 | 21 | // Interpolate rotation using slerp (spherical linear interpolation) 22 | meshComponent.mesh.quaternion.slerp( 23 | targetQuaternion, 24 | vehicleComponent ? 0.5 : interpolationFactor 25 | ) 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /front/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front/game/ecs/system/DestroySystem.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '@shared/system/EntityManager.js' 2 | import { Renderer } from '@/game/Renderer.js' 3 | import { EntityDestroyedEvent } from '@shared/component/events/EntityDestroyedEvent.js' 4 | import { Entity } from '@shared/entity/Entity.js' 5 | import { EventSystem } from '@shared/system/EventSystem.js' 6 | 7 | export class DestroySystem { 8 | update(entities: Entity[], renderer: Renderer) { 9 | const destroyedEvents = EventSystem.getEvents(EntityDestroyedEvent) 10 | 11 | for (const destroyedEvent of destroyedEvents) { 12 | const entity = EntityManager.getEntityById(entities, destroyedEvent.entityId) 13 | if (!entity) { 14 | console.error('Update : DestroySystem: Entity not found with id', destroyedEvent.entityId) 15 | continue 16 | } 17 | 18 | entity.removeAllComponents() 19 | } 20 | } 21 | afterUpdate(entities: Entity[]) { 22 | const destroyedEvents = EventSystem.getEvents(EntityDestroyedEvent) 23 | 24 | for (const destroyedEvent of destroyedEvents) { 25 | const entity = EntityManager.getEntityById(entities, destroyedEvent.entityId) 26 | if (!entity) { 27 | console.error('Update : DestroySystem: Entity not found with id', destroyedEvent.entityId) 28 | continue 29 | } 30 | 31 | EntityManager.removeEntity(entity) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /back/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notblox.online", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "debug": "tsc-watch --onSuccess \"node --inspect=7000 dist/back/src/sandbox.js\"", 8 | "dev": "tsc-watch --onSuccess \"node dist/back/src/sandbox.js\"", 9 | "build": "tsc --build tsconfig.json", 10 | "start": "node dist/back/src/sandbox.js", 11 | "lint": "eslint src/**/*.ts", 12 | "format": "eslint src/**/*.ts --fix" 13 | }, 14 | "keywords": [], 15 | "author": "iercann", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "@eslint/js": "^9.3.0", 19 | "@types/uws": "^0.13.6", 20 | "@typescript-eslint/eslint-plugin": "^8.29.0", 21 | "@typescript-eslint/parser": "^8.29.0", 22 | "eslint": "^8.57.0", 23 | "globals": "^15.3.0", 24 | "tsc-watch": "^6.2.0", 25 | "tsconfig-paths": "^4.2.0", 26 | "typescript-eslint": "^8.29.0" 27 | }, 28 | "type": "module", 29 | "dependencies": { 30 | "@dimforge/rapier3d-compat": "^0.14.0", 31 | "@types/node": "^22.10.1", 32 | "@types/pako": "^2.0.3", 33 | "@types/three": "^0.172.0", 34 | "dotenv": "^16.3.1", 35 | "msgpackr": "^1.9.9", 36 | "node-three-gltf": "^1.8.3", 37 | "pako": "^2.1.0", 38 | "rate-limiter-flexible": "^5.0.3", 39 | "three": "^0.172.0", 40 | "typescript": "^5.7.3", 41 | "uWebSockets.js": "github:uNetworking/uWebSockets.js#binaries" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /back/src/ecs/entity/FloatingText.ts: -------------------------------------------------------------------------------- 1 | import { PositionComponent } from '../../../../shared/component/PositionComponent.js' 2 | import { Entity } from '../../../../shared/entity/Entity.js' 3 | import { SerializedEntityType } from '../../../../shared/network/server/serialized.js' 4 | import { EntityManager } from '../../../../shared/system/EntityManager.js' 5 | import { TextComponent } from '../../../../shared/component/TextComponent.js' 6 | import { NetworkDataComponent } from '../../../../shared/network/NetworkDataComponent.js' 7 | 8 | export class FloatingText { 9 | entity: Entity 10 | textComponent: TextComponent 11 | constructor(text: string, x: number, y: number, z: number, displayDistance: number = 1000) { 12 | this.entity = EntityManager.createEntity(SerializedEntityType.FLOATING_TEXT) 13 | 14 | const positionComponent = new PositionComponent(this.entity.id, x, y, z) 15 | this.entity.addComponent(positionComponent) 16 | 17 | this.textComponent = new TextComponent(this.entity.id, text, 0, 0, 0, displayDistance) 18 | this.entity.addComponent(this.textComponent) 19 | 20 | // Network data component 21 | const networkDataComponent = new NetworkDataComponent(this.entity.id, this.entity.type, [ 22 | positionComponent, 23 | this.textComponent, 24 | ]) 25 | this.entity.addComponent(networkDataComponent) 26 | } 27 | 28 | updateText(text: string) { 29 | this.textComponent.text = text 30 | this.textComponent.updated = true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /front/components/LoadingScreen.tsx: -------------------------------------------------------------------------------- 1 | export default function LoadingScreen() { 2 | return ( 3 |
4 |
5 |
6 |
7 | Loading.. 8 |

Connecting to server...

9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 22 |
23 |
24 |
25 |
26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /shared/component/RotationComponent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedComponent, SerializedComponentType } from '../network/server/serialized.js' 2 | import { NetworkComponent } from '../network/NetworkComponent.js' 3 | 4 | // Define a RotationComponent class 5 | export class RotationComponent extends NetworkComponent { 6 | constructor( 7 | entityId: number, 8 | public x: number, 9 | public y: number, 10 | public z: number, 11 | public w = 0 12 | ) { 13 | super(entityId, SerializedComponentType.ROTATION) 14 | } 15 | 16 | getForwardDirection() { 17 | // Forward vector rotated by quaternion math 18 | const x = 2 * (this.x * this.z + this.w * this.y) 19 | const z = 1 - 2 * (this.x * this.x + this.y * this.y) 20 | z 21 | // Normalize the vector 22 | const length = Math.sqrt(x * x + z * z) 23 | return { 24 | x: x / length, 25 | z: z / length, 26 | } 27 | } 28 | deserialize(data: SerializedRotationComponent): void { 29 | this.x = data.x 30 | this.y = data.y 31 | this.z = data.z 32 | this.w = data.w 33 | } 34 | 35 | serialize(): SerializedRotationComponent { 36 | return { 37 | x: Number(this.x.toFixed(2)), 38 | y: Number(this.y.toFixed(2)), 39 | z: Number(this.z.toFixed(2)), 40 | w: Number(this.w.toFixed(2)), 41 | } 42 | } 43 | } 44 | 45 | export interface SerializedRotationComponent extends SerializedComponent { 46 | x: number 47 | y: number 48 | z: number 49 | w: number 50 | } 51 | -------------------------------------------------------------------------------- /front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NotBlox", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "concurrently \"next dev -p 4000\" \"serve -s public/assets -l 4001 --cors\"", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-dropdown-menu": "^2.1.5", 13 | "@radix-ui/react-slot": "^1.1.1", 14 | "@types/dompurify": "^3.2.0", 15 | "camera-controls": "^2.8.3", 16 | "class-variance-authority": "^0.7.1", 17 | "clsx": "^2.1.1", 18 | "dompurify": "^3.2.4", 19 | "lucide-react": "^0.473.0", 20 | "msgpackr": "^1.9.9", 21 | "next": "^15.4.7", 22 | "pako": "^2.1.0", 23 | "react": "^19.0.0", 24 | "react-dom": "^19.0.0", 25 | "react-joystick-component": "^6.2.1", 26 | "react-markdown": "^9.0.3", 27 | "tailwind-merge": "^2.6.0", 28 | "tailwindcss-animate": "^1.0.7", 29 | "three": "^0.172.0" 30 | }, 31 | "devDependencies": { 32 | "@tailwindcss/typography": "^0.5.16", 33 | "@types/node": "latest", 34 | "@types/pako": "^2.0.3", 35 | "@types/react": "latest", 36 | "@types/react-dom": "latest", 37 | "@types/three": "^0.172.0", 38 | "autoprefixer": "latest", 39 | "concurrently": "^8.2.2", 40 | "eslint": "latest", 41 | "eslint-config-next": "^15.1.5", 42 | "postcss": "latest", 43 | "serve": "^14.2.4", 44 | "tailwindcss": "3.4.18", 45 | "typescript": "latest" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /back/src/ecs/entity/Chat.ts: -------------------------------------------------------------------------------- 1 | import { MessageListComponent } from '../../../../shared/component/MessageComponent.js' 2 | import { Entity } from '../../../../shared/entity/Entity.js' 3 | import { EntityManager } from '../../../../shared/system/EntityManager.js' 4 | import { EventSystem } from '../../../../shared/system/EventSystem.js' 5 | import { SerializedEntityType } from '../../../../shared/network/server/serialized.js' 6 | import { NetworkDataComponent } from '../../../../shared/network/NetworkDataComponent.js' 7 | import { MessageEvent } from '../component/events/MessageEvent.js' 8 | import { ChatComponent } from '../component/tag/TagChatComponent.js' 9 | 10 | export class Chat { 11 | entity: Entity 12 | 13 | constructor() { 14 | this.entity = EntityManager.createEntity(SerializedEntityType.CHAT) 15 | 16 | this.entity.addComponent(new ChatComponent(this.entity.id)) 17 | 18 | EventSystem.addEvent( 19 | new MessageEvent(this.entity.id, '🖥️ [SERVER]', `Started ${new Date().toLocaleString()}`) 20 | ) 21 | 22 | EventSystem.addEvent( 23 | new MessageEvent(this.entity.id, '🖥️ [SERVER]', 'Welcome to the chat !') 24 | ) 25 | 26 | const chatListComponent = new MessageListComponent(this.entity.id, []) 27 | this.entity.addComponent(chatListComponent) 28 | 29 | const networkDataComponent = new NetworkDataComponent(this.entity.id, this.entity.type, [ 30 | chatListComponent, 31 | ]) 32 | this.entity.addComponent(networkDataComponent) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /shared/component/TextComponent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedComponent, SerializedComponentType } from '../network/server/serialized.js' 2 | import { NetworkComponent } from '../network/NetworkComponent.js' 3 | 4 | export class TextComponent extends NetworkComponent { 5 | constructor( 6 | entityId: number, 7 | public text: string = 'No text set', 8 | public offsetX: number = 0, 9 | public offsetY: number = 0, 10 | public offsetZ: number = 0, 11 | public displayDistance: number = 10 12 | ) { 13 | super(entityId, SerializedComponentType.TEXT) 14 | } 15 | 16 | serialize(): SerializedTextComponent { 17 | return { 18 | m: this.text, 19 | p: { 20 | x: this.offsetX, 21 | y: this.offsetY, 22 | z: this.offsetZ, 23 | }, 24 | d: this.displayDistance, 25 | } 26 | } 27 | 28 | deserialize(data: SerializedTextComponent): void { 29 | this.text = data.m 30 | this.offsetX = data.p.x 31 | this.offsetY = data.p.y 32 | this.offsetZ = data.p.z 33 | this.displayDistance = data.d 34 | } 35 | } 36 | 37 | export interface SerializedTextComponent extends SerializedComponent { 38 | /* The text content to display */ 39 | m: string 40 | /* Offset position relative to the entity */ 41 | p: { 42 | x: number 43 | y: number 44 | z: number 45 | } 46 | /** 47 | * Display distance from the player 48 | */ 49 | d: number 50 | /** 51 | * May have more properties in the future like CSS styles 52 | */ 53 | } 54 | -------------------------------------------------------------------------------- /shared/component/events/ComponentRemovedEvent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedComponentType } from '../../network/server/serialized.js' 2 | import { NetworkComponent } from '../../network/NetworkComponent.js' 3 | import { Component } from '../Component.js' 4 | import { ComponentWrapper } from './ComponentWrapper.js' 5 | 6 | export class ComponentRemovedEvent extends ComponentWrapper { 7 | constructor(component: T) { 8 | super(component) 9 | } 10 | } 11 | 12 | /** 13 | * ComponentRemovedEvent that is sent to the client when a component is removed 14 | * Since there is component unicity, the type of the component is enough to know which component is removed 15 | * @param removedComponentType The type of the component that was removed 16 | */ 17 | export class SerializableComponentRemovedEvent extends NetworkComponent { 18 | constructor(entityId: number, public removedComponentType: SerializedComponentType) { 19 | super(entityId, SerializedComponentType.COMPONENT_REMOVED_EVENT) 20 | } 21 | 22 | serialize(): SerializedComponentRemovedEvent { 23 | return { 24 | cT: this.removedComponentType, 25 | eId: this.entityId, 26 | } 27 | } 28 | 29 | deserialize(data: SerializedComponentRemovedEvent): void { 30 | this.removedComponentType = data.cT 31 | // Be careful to override the event entity id 32 | this.entityId = data.eId 33 | } 34 | } 35 | 36 | // Interface for serialized data 37 | export interface SerializedComponentRemovedEvent { 38 | cT: SerializedComponentType 39 | eId: number 40 | } 41 | -------------------------------------------------------------------------------- /front/public/draco/README.md: -------------------------------------------------------------------------------- 1 | # Draco 3D Data Compression 2 | 3 | Draco is an open-source library for compressing and decompressing 3D geometric meshes and point clouds. It is intended to improve the storage and transmission of 3D graphics. 4 | 5 | [Website](https://google.github.io/draco/) | [GitHub](https://github.com/google/draco) 6 | 7 | ## Contents 8 | 9 | This folder contains three utilities: 10 | 11 | * `draco_decoder.js` — Emscripten-compiled decoder, compatible with any modern browser. 12 | * `draco_decoder.wasm` — WebAssembly decoder, compatible with newer browsers and devices. 13 | * `draco_wasm_wrapper.js` — JavaScript wrapper for the WASM decoder. 14 | 15 | Each file is provided in two variations: 16 | 17 | * **Default:** Latest stable builds, tracking the project's [master branch](https://github.com/google/draco). 18 | * **glTF:** Builds targeted by the [glTF mesh compression extension](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression), tracking the [corresponding Draco branch](https://github.com/google/draco/tree/gltf_2.0_draco_extension). 19 | 20 | Either variation may be used with `THREE.DRACOLoader`: 21 | 22 | ```js 23 | var dracoLoader = new THREE.DRACOLoader(); 24 | dracoLoader.setDecoderPath('path/to/decoders/'); 25 | dracoLoader.setDecoderConfig({type: 'js'}); // (Optional) Override detection of WASM support. 26 | ``` 27 | 28 | Further [documentation on GitHub](https://github.com/google/draco/tree/master/javascript/example#static-loading-javascript-decoder). 29 | 30 | ## License 31 | 32 | [Apache License 2.0](https://github.com/google/draco/blob/master/LICENSE) 33 | -------------------------------------------------------------------------------- /shared/network/NetworkDataComponent.ts: -------------------------------------------------------------------------------- 1 | import { NetworkComponent } from './NetworkComponent.js' 2 | import { 3 | SerializedComponentType, 4 | SerializedEntity, 5 | SerializedEntityType, 6 | } from './server/serialized.js' 7 | import { Component } from '../component/Component.js' 8 | 9 | export class NetworkDataComponent extends Component { 10 | type = SerializedComponentType.NONE 11 | 12 | constructor( 13 | entityId: number, 14 | public entityType: SerializedEntityType, 15 | public components: NetworkComponent[] 16 | ) { 17 | super(entityId) 18 | } 19 | 20 | getComponents(): NetworkComponent[] { 21 | return this.components 22 | } 23 | 24 | removeComponent(componentType: typeof NetworkComponent) { 25 | this.components = this.components.filter((c) => !(c instanceof componentType)) 26 | } 27 | 28 | addComponent(component: NetworkComponent) { 29 | this.components.push(component) 30 | } 31 | 32 | removeAllComponents() { 33 | this.components = [] 34 | } 35 | 36 | serialize(serializeAll = false): SerializedEntity | null { 37 | const components = this.getComponents() 38 | const serializedComponents = components 39 | .filter((component) => serializeAll || component.updated === true) 40 | .map((component: NetworkComponent) => { 41 | return { t: component.type, ...component.serialize() } 42 | }) 43 | 44 | if (serializedComponents.length === 0) { 45 | return null 46 | } 47 | 48 | const broadcastMessage = { 49 | id: this.entityId, 50 | t: this.entityType, 51 | c: serializedComponents, 52 | } 53 | 54 | return broadcastMessage 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /back/src/ecs/system/physics/SleepCheckSystem.ts: -------------------------------------------------------------------------------- 1 | import { PositionComponent } from '../../../../../shared/component/PositionComponent.js' 2 | import { RotationComponent } from '../../../../../shared/component/RotationComponent.js' 3 | import { Entity } from '../../../../../shared/entity/Entity.js' 4 | import { NetworkComponent } from '../../../../../shared/network/NetworkComponent.js' 5 | import { DynamicRigidBodyComponent } from '../../component/physics/DynamicRigidBodyComponent.js' 6 | import { KinematicRigidBodyComponent } from '../../component/physics/KinematicRigidBodyComponent.js' 7 | 8 | export class SleepCheckSystem { 9 | update(entities: Entity[]) { 10 | for (const entity of entities) { 11 | const bodyComponent = 12 | entity.getComponent(DynamicRigidBodyComponent) || 13 | entity.getComponent(KinematicRigidBodyComponent) 14 | 15 | this.sleepNetworkComponent(entity) 16 | 17 | if (bodyComponent) { 18 | const sleeping = bodyComponent.body?.isSleeping() 19 | const positionComponent = entity.getComponent(PositionComponent) 20 | if (positionComponent) { 21 | positionComponent.updated = !sleeping 22 | } 23 | const rotationComponent = entity.getComponent(RotationComponent) 24 | if (rotationComponent) { 25 | rotationComponent.updated = !sleeping 26 | } 27 | } 28 | } 29 | } 30 | sleepNetworkComponent(entity: Entity) { 31 | const components = entity.getAllComponents() 32 | // Check if component is a NetworkComponent 33 | for (const component of components) { 34 | if (component instanceof NetworkComponent) { 35 | component.updated = false 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /back/src/ecs/system/events/SizeEventSystem.ts: -------------------------------------------------------------------------------- 1 | import { SizeComponent } from '../../../../../shared/component/SizeComponent.js' 2 | import { Entity } from '../../../../../shared/entity/Entity.js' 3 | import { EntityManager } from '../../../../../shared/system/EntityManager.js' 4 | import { EventSystem } from '../../../../../shared/system/EventSystem.js' 5 | import Rapier from '../../../physics/rapier.js' 6 | import { SizeEvent } from '../../component/events/SizeEvent.js' 7 | import { BoxColliderComponent } from '../../component/physics/BoxColliderComponent.js' 8 | 9 | export class SizeEventSystem { 10 | update(entities: Entity[]) { 11 | const eventSizes = EventSystem.getEvents(SizeEvent) 12 | 13 | for (const eventSize of eventSizes) { 14 | const entity = EntityManager.getEntityById(entities, eventSize.entityId) 15 | 16 | if (!entity) continue 17 | // Request new size 18 | const { width, height, depth } = eventSize 19 | 20 | const sizeComponent = entity.getComponent(SizeComponent) 21 | if (!sizeComponent) { 22 | console.error('SizeComponent not found') 23 | continue 24 | } 25 | 26 | const boxColliderComponent = entity.getComponent(BoxColliderComponent) 27 | if (boxColliderComponent) { 28 | const colliderDesc = Rapier.ColliderDesc.cuboid(width, height, depth) 29 | boxColliderComponent.collider?.setShape(colliderDesc.shape) 30 | } 31 | 32 | // This will rebroadcast the update to all clients. 33 | if (sizeComponent) { 34 | sizeComponent.width = width 35 | sizeComponent.height = height 36 | sizeComponent.depth = depth 37 | sizeComponent.updated = true 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /front/game/ecs/system/AnimationSystem.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '@shared/entity/Entity' 2 | import { AnimationComponent } from '../component/AnimationComponent' 3 | import { StateComponent } from '@shared/component/StateComponent' 4 | import { MeshComponent } from '../component/MeshComponent' 5 | 6 | export class AnimationSystem { 7 | update(dt: number, entities: Entity[]) { 8 | for (const entity of entities) { 9 | const animationComponent = entity.getComponent(AnimationComponent) 10 | const meshComponent = entity.getComponent(MeshComponent) 11 | const stateComponent = entity.getComponent(StateComponent) 12 | 13 | if (animationComponent && stateComponent && meshComponent) { 14 | const mesh = meshComponent.mesh 15 | const animations = mesh.animations 16 | 17 | const isNotPlaying = animationComponent.mixer.time === 0 18 | 19 | if (stateComponent.updated || isNotPlaying) { 20 | // Find the animation that corresponds to the current state 21 | const requestAnimationName = stateComponent.state 22 | 23 | for (const clip of animations) { 24 | const action = animationComponent.mixer.clipAction(clip) 25 | if (clip.name !== requestAnimationName) { 26 | // Fade out all animations except the one corresponding to the current state 27 | action.fadeOut(0.2) 28 | } else { 29 | // Fade in and play the animation corresponding to the current state 30 | action.reset() 31 | 32 | action.fadeIn(0.1) 33 | 34 | action.play() 35 | } 36 | } 37 | } 38 | 39 | animationComponent.mixer.update(dt / 1000) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /front/app/play/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | // app/play/[slug]/page.tsx 2 | import gameData from '../../../public/gameData.json' 3 | import { GameInfo } from '@/types' 4 | import { Metadata } from 'next' 5 | import GameContent from '@/components/GameContent' 6 | 7 | export async function generateStaticParams(): Promise<{ slug: string }[]> { 8 | const games = gameData as GameInfo[] 9 | 10 | return games.map((game) => ({ 11 | slug: game.slug, 12 | })) 13 | } 14 | 15 | function getGamesBySlug(slug: string): GameInfo { 16 | const game = gameData.find((game) => game.slug === slug) 17 | if (!game) { 18 | throw new Error(`Game with slug "${slug}" not found`) 19 | } 20 | return game 21 | } 22 | 23 | // https://nextjs.org/docs/app/building-your-application/upgrading/version-15#params--searchparams 24 | type Params = Promise<{ slug: string }> 25 | 26 | export async function generateMetadata({ params }: { params: Params }): Promise { 27 | const { slug } = await params 28 | const gameInfo = getGamesBySlug(slug) 29 | 30 | return { 31 | title: `Play ${gameInfo.title} - NotBlox`, 32 | description: gameInfo.metaDescription, 33 | openGraph: { 34 | title: `Play ${gameInfo.title} - NotBlox`, 35 | description: gameInfo.metaDescription, 36 | images: gameInfo.images ?? [], 37 | siteName: 'NotBlox Online', 38 | }, 39 | twitter: { 40 | card: 'summary_large_image', 41 | site: '@iercan_', 42 | creator: '@iercan_', 43 | }, 44 | alternates: { 45 | canonical: `https://www.notblox.online/play/${gameInfo.slug}`, 46 | }, 47 | } 48 | } 49 | 50 | export default async function GamePage({ params }: { params: Params }) { 51 | const { slug } = await params 52 | const gameInfo = getGamesBySlug(slug) 53 | 54 | return 55 | } 56 | -------------------------------------------------------------------------------- /shared/network/server/serialized.ts: -------------------------------------------------------------------------------- 1 | import { ServerMessage } from './base' 2 | export enum SerializedComponentType { 3 | NONE = 0, 4 | POSITION = 1, 5 | ROTATION = 2, 6 | SIZE = 3, 7 | COLOR = 4, 8 | ENTITY_DESTROYED_EVENT = 5, 9 | SINGLE_SIZE = 6, 10 | 11 | // Used for animations mostly 12 | STATE = 7, 13 | 14 | CHAT_LIST = 8, 15 | MESSAGE = 9, 16 | 17 | SERVER_MESH = 10, 18 | PROXIMITY_PROMPT = 11, 19 | TEXT = 12, 20 | VEHICLE = 13, 21 | PLAYER = 14, 22 | VEHICLE_OCCUPANCY = 15, 23 | COMPONENT_REMOVED_EVENT = 16, 24 | WHEEL = 17, 25 | INVISIBLE = 18, 26 | } 27 | 28 | export enum SerializedEntityType { 29 | NONE = 0, 30 | PLAYER = 1, 31 | CUBE = 2, 32 | WORLD = 3, 33 | SPHERE = 4, 34 | CHAT = 5, 35 | EVENT_QUEUE = 6, 36 | FLOATING_TEXT = 7, 37 | VEHICLE = 8, 38 | ORBITAL_COMPANION = 9, 39 | } 40 | 41 | // Movement states 42 | export enum SerializedStateType { 43 | IDLE = 'Idle', 44 | WALK = 'Walk', 45 | RUN = 'Run', 46 | JUMP = 'Jump', 47 | FALL = 'Fall', 48 | DEATH = 'Death', 49 | } 50 | 51 | export interface SerializedComponent { 52 | t?: SerializedComponentType 53 | } 54 | 55 | export interface SerializedEntity { 56 | id: number 57 | // Type 58 | t: SerializedEntityType 59 | // Components 60 | c: SerializedComponent[] 61 | } 62 | 63 | export interface SnapshotMessage extends ServerMessage { 64 | e: SerializedEntity[] 65 | } 66 | 67 | /** 68 | * Message types for different kinds of chat messages 69 | */ 70 | export enum SerializedMessageType { 71 | // Chat messages 72 | GLOBAL_CHAT = 1, // Regular chat message 73 | TARGETED_CHAT = 2, // Message to specific players 74 | 75 | // Notifications 76 | GLOBAL_NOTIFICATION = 3, // Global notification at top of screen 77 | TARGETED_NOTIFICATION = 4, // Message to specific players 78 | } 79 | -------------------------------------------------------------------------------- /front/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ['class'], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | // eslint-disable-next-line @typescript-eslint/no-require-imports 11 | plugins: [require('@tailwindcss/typography')], 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: '2rem', 16 | screens: { 17 | '2xl': '1400px', 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | border: 'hsl(var(--border))', 23 | input: 'hsl(var(--input))', 24 | ring: 'hsl(var(--ring))', 25 | background: 'hsl(var(--background))', 26 | foreground: 'hsl(var(--foreground))', 27 | primary: { 28 | DEFAULT: 'hsl(var(--primary))', 29 | foreground: 'hsl(var(--primary-foreground))', 30 | }, 31 | secondary: { 32 | DEFAULT: 'hsl(var(--secondary))', 33 | foreground: 'hsl(var(--secondary-foreground))', 34 | }, 35 | destructive: { 36 | DEFAULT: 'hsl(var(--destructive))', 37 | foreground: 'hsl(var(--destructive-foreground))', 38 | }, 39 | muted: { 40 | DEFAULT: 'hsl(var(--muted))', 41 | foreground: 'hsl(var(--muted-foreground))', 42 | }, 43 | accent: { 44 | DEFAULT: 'hsl(var(--accent))', 45 | foreground: 'hsl(var(--accent-foreground))', 46 | }, 47 | popover: { 48 | DEFAULT: 'hsl(var(--popover))', 49 | foreground: 'hsl(var(--popover-foreground))', 50 | }, 51 | card: { 52 | DEFAULT: 'hsl(var(--card))', 53 | foreground: 'hsl(var(--card-foreground))', 54 | }, 55 | }, 56 | }, 57 | }, 58 | } 59 | -------------------------------------------------------------------------------- /front/game/ecs/system/InvisibilitySystem.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '@shared/entity/Entity.js' 2 | import { InvisibleComponent } from '@shared/component/InvisibleComponent.js' 3 | import { MeshComponent } from '../component/MeshComponent.js' 4 | import { EventSystem } from '@shared/system/EventSystem.js' 5 | import { ComponentAddedEvent } from '@shared/component/events/ComponentAddedEvent.js' 6 | import { ComponentRemovedEvent } from '@shared/component/events/ComponentRemovedEvent.js' 7 | import { EntityManager } from '@shared/system/EntityManager.js' 8 | 9 | /** 10 | * On InvisibleComponent added, make the entity invisible 11 | * On InvisibleComponent removed, make the entity visible again 12 | */ 13 | export class InvisibilitySystem { 14 | update(entities: Entity[]): void { 15 | // Handle added invisible components 16 | const addedInvisibleEvents = EventSystem.getEventsWrapped( 17 | ComponentAddedEvent, 18 | InvisibleComponent 19 | ) 20 | for (const addedEvent of addedInvisibleEvents) { 21 | const entity = EntityManager.getEntityById(entities, addedEvent.entityId) 22 | if (entity) { 23 | const meshComponent = entity.getComponent(MeshComponent) 24 | if (meshComponent) { 25 | meshComponent.mesh.visible = false 26 | } 27 | } 28 | } 29 | 30 | // Handle removed invisible components 31 | const removedInvisibleEvents = EventSystem.getEventsWrapped( 32 | ComponentRemovedEvent, 33 | InvisibleComponent 34 | ) 35 | for (const removedEvent of removedInvisibleEvents) { 36 | const entity = EntityManager.getEntityById(entities, removedEvent.entityId) 37 | if (entity) { 38 | const meshComponent = entity.getComponent(MeshComponent) 39 | if (meshComponent) { 40 | meshComponent.mesh.visible = true 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 iErcann - (https://github.com/iErcann/Notblox) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | **Restriction on Use in Blockchain and Cryptocurrency Technologies** 25 | 26 | This project is licensed under the MIT License, with the following additional restriction: 27 | 28 | The software may **not** be used for any purpose related to blockchain, cryptocurrency, or distributed ledger technologies. This includes, but is not limited to: 29 | - Creating, mining, trading, or promoting cryptocurrencies. 30 | - Using in any application that facilitates cryptocurrency transactions. 31 | - Employing the software in mining, staking, or token-related activities. 32 | 33 | Any use of this project in the cryptocurrency or blockchain industry is strictly prohibited without prior written consent from the author. 34 | -------------------------------------------------------------------------------- /front/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 20 | 21 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 22 | 23 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 24 | 25 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 26 | 27 | ## Learn More 28 | 29 | To learn more about Next.js, take a look at the following resources: 30 | 31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 32 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 33 | 34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 35 | 36 | ## Deploy on Vercel 37 | 38 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 39 | 40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 41 | -------------------------------------------------------------------------------- /front/components/GamePlayer.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useEffect, useRef, useState } from 'react' 3 | import { Game } from '@/game/Game' 4 | import GameHud from '@/components/GameHud' 5 | import LoadingScreen from '@/components/LoadingScreen' 6 | import { MessageComponent } from '@shared/component/MessageComponent' 7 | import { GameInfo } from '@/types' 8 | 9 | interface GamePlayerProps extends GameInfo { 10 | playerName?: string 11 | } 12 | 13 | export default function GamePlayer({ playerName, ...gameInfo }: GamePlayerProps) { 14 | const [isLoading, setIsLoading] = useState(true) 15 | const [messages, setMessages] = useState([]) 16 | const [gameInstance, setGameInstance] = useState(null) // Initialize as null 17 | const refContainer = useRef(null) 18 | 19 | useEffect(() => { 20 | async function initializeGame() { 21 | const game = Game.getInstance(gameInfo.websocketPort, refContainer) 22 | game.hud.passChatState(setMessages) 23 | setGameInstance(game) 24 | try { 25 | await game.start() 26 | 27 | // Set player name if provided 28 | if (playerName && playerName.trim()) { 29 | game.setPlayerName(playerName.trim()) 30 | } 31 | 32 | setIsLoading(false) 33 | } catch (error) { 34 | console.error('Error connecting to WebSocket:', error) 35 | } 36 | } 37 | 38 | initializeGame() 39 | }, [gameInfo.websocketPort, playerName]) 40 | 41 | return ( 42 |
43 | {isLoading && } 44 | {gameInstance && ( // Only render if gameInstance is defined 45 |
46 | 51 |
52 | )} 53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /back/src/ecs/entity/MapWorld.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '../../../../shared/system/EntityManager.js' 2 | import { Entity } from '../../../../shared/entity/Entity.js' 3 | import { SerializedEntityType } from '../../../../shared/network/server/serialized.js' 4 | import { TrimeshCollidersComponent } from '../component/physics/TrimeshColliderComponent.js' 5 | import { KinematicRigidBodyComponent } from '../component/physics/KinematicRigidBodyComponent.js' 6 | import { PositionComponent } from '../../../../shared/component/PositionComponent.js' 7 | import { ServerMeshComponent } from '../../../../shared/component/ServerMeshComponent.js' 8 | import { NetworkDataComponent } from '../../../../shared/network/NetworkDataComponent.js' 9 | import { PhysicsPropertiesComponent } from '../component/physics/PhysicsPropertiesComponent.js' 10 | import { ColliderPropertiesComponent } from '../component/physics/ColliderPropertiesComponent.js' 11 | 12 | export class MapWorld { 13 | entity: Entity 14 | constructor(mapUrl: string) { 15 | this.entity = EntityManager.createEntity(SerializedEntityType.WORLD) 16 | 17 | const serverMeshComponent = new ServerMeshComponent(this.entity.id, mapUrl) 18 | this.entity.addComponent(serverMeshComponent) 19 | 20 | this.entity.addComponent(new PositionComponent(this.entity.id, 0, 0, 0)) 21 | 22 | this.entity.addComponent( 23 | new ColliderPropertiesComponent(this.entity.id, { 24 | friction: 0.0, 25 | restitution: 0.1, 26 | }) 27 | ) 28 | this.entity.addComponent( 29 | new PhysicsPropertiesComponent(this.entity.id, { 30 | enableCcd: true, 31 | angularDamping: 0.05, 32 | linearDamping: 0.05, 33 | }) 34 | ) 35 | this.entity.addComponent(new KinematicRigidBodyComponent(this.entity.id)) 36 | 37 | this.entity.addComponent(new TrimeshCollidersComponent(this.entity.id, mapUrl)) 38 | this.entity.addComponent( 39 | new NetworkDataComponent(this.entity.id, this.entity.type, [serverMeshComponent]) 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /front/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | const Card = React.forwardRef>( 6 | ({ className, ...props }, ref) => ( 7 |
12 | ) 13 | ) 14 | Card.displayName = 'Card' 15 | 16 | const CardHeader = React.forwardRef>( 17 | ({ className, ...props }, ref) => ( 18 |
19 | ) 20 | ) 21 | CardHeader.displayName = 'CardHeader' 22 | 23 | const CardTitle = React.forwardRef>( 24 | ({ className, ...props }, ref) => ( 25 |
30 | ) 31 | ) 32 | CardTitle.displayName = 'CardTitle' 33 | 34 | const CardDescription = React.forwardRef>( 35 | ({ className, ...props }, ref) => ( 36 |
37 | ) 38 | ) 39 | CardDescription.displayName = 'CardDescription' 40 | 41 | const CardContent = React.forwardRef>( 42 | ({ className, ...props }, ref) => ( 43 |
44 | ) 45 | ) 46 | CardContent.displayName = 'CardContent' 47 | 48 | const CardFooter = React.forwardRef>( 49 | ({ className, ...props }, ref) => ( 50 |
51 | ) 52 | ) 53 | CardFooter.displayName = 'CardFooter' 54 | 55 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 56 | -------------------------------------------------------------------------------- /back/src/ecs/system/physics/GroundedCheckSystem.ts: -------------------------------------------------------------------------------- 1 | import { PositionComponent } from '../../../../../shared/component/PositionComponent.js' 2 | import { SingleSizeComponent } from '../../../../../shared/component/SingleSizeComponent.js' 3 | import { Entity } from '../../../../../shared/entity/Entity.js' 4 | import Rapier from '../../../physics/rapier.js' 5 | import { GroundCheckComponent } from '../../component/GroundedComponent.js' 6 | import { BoxColliderComponent } from '../../component/physics/BoxColliderComponent.js' 7 | import { CapsuleColliderComponent } from '../../component/physics/CapsuleColliderComponent.js' 8 | import { DynamicRigidBodyComponent } from '../../component/physics/DynamicRigidBodyComponent.js' 9 | 10 | export class GroundedCheckSystem { 11 | update(entities: Entity[], world: Rapier.World) { 12 | for (const entity of entities) { 13 | const groundedComponent = entity.getComponent(GroundCheckComponent) 14 | const bodyComponent = entity.getComponent(DynamicRigidBodyComponent) 15 | const positionComponent = entity.getComponent(PositionComponent) 16 | const colliderComponent = 17 | entity.getComponent(CapsuleColliderComponent) || entity.getComponent(BoxColliderComponent) 18 | 19 | if (groundedComponent && bodyComponent && positionComponent && colliderComponent) { 20 | const sizeComponent = entity.getComponent(SingleSizeComponent) 21 | const size = sizeComponent?.size || 1.0 22 | 23 | const ray = new Rapier.Ray( 24 | { 25 | x: positionComponent.x, 26 | y: positionComponent.y - 1, 27 | z: positionComponent.z, 28 | }, 29 | { 30 | x: 0, 31 | y: -1, 32 | z: 0, 33 | } 34 | ) 35 | const hit = world.castRay( 36 | ray, 37 | size * 2, 38 | false, 39 | undefined, 40 | undefined, 41 | colliderComponent.collider 42 | ) 43 | groundedComponent.grounded = hit ? true : false 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /front/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /monitor/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notblox-monitor", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "notblox-monitor", 9 | "version": "1.0.0", 10 | "dependencies": { 11 | "dotenv": "^16.0.3" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "^22.0.1", 15 | "typescript": "^5.0.4" 16 | } 17 | }, 18 | "node_modules/@types/node": { 19 | "version": "22.0.1", 20 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.1.tgz", 21 | "integrity": "sha512-RVKWL+s4ax6syie/ev3FXFIs38mke4ZsCDPBcLF2Gu6MbQXKe9Fo9iU0EPUxDB1mDVvC0vCgkV3lKa2f6xIuHg==", 22 | "dev": true, 23 | "license": "MIT", 24 | "dependencies": { 25 | "undici-types": "~6.11.1" 26 | } 27 | }, 28 | "node_modules/dotenv": { 29 | "version": "16.4.7", 30 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", 31 | "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", 32 | "license": "BSD-2-Clause", 33 | "engines": { 34 | "node": ">=12" 35 | }, 36 | "funding": { 37 | "url": "https://dotenvx.com" 38 | } 39 | }, 40 | "node_modules/typescript": { 41 | "version": "5.8.3", 42 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 43 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 44 | "dev": true, 45 | "license": "Apache-2.0", 46 | "bin": { 47 | "tsc": "bin/tsc", 48 | "tsserver": "bin/tsserver" 49 | }, 50 | "engines": { 51 | "node": ">=14.17" 52 | } 53 | }, 54 | "node_modules/undici-types": { 55 | "version": "6.11.1", 56 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.11.1.tgz", 57 | "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==", 58 | "dev": true, 59 | "license": "MIT" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /back/src/ecs/system/physics/BoundaryCheckSystem.ts: -------------------------------------------------------------------------------- 1 | import { PositionComponent } from '../../../../../shared/component/PositionComponent.js' 2 | import { Entity } from '../../../../../shared/entity/Entity.js' 3 | import Rapier from '../../../physics/rapier.js' 4 | import { LockedRotationComponent } from '../../component/LockedRotationComponent.js' 5 | import { SpawnPositionComponent } from '../../component/SpawnPositionComponent.js' 6 | import { DynamicRigidBodyComponent } from '../../component/physics/DynamicRigidBodyComponent.js' 7 | import { PlayerComponent } from '../../../../../shared/component/PlayerComponent.js' 8 | 9 | export class BoundaryCheckSystem { 10 | lowerBound = -100 11 | update(entities: Entity[]) { 12 | for (const entity of entities) { 13 | const bodyComponent = entity.getComponent(DynamicRigidBodyComponent) 14 | const positionComponent = entity.getComponent(PositionComponent) 15 | 16 | if (bodyComponent && positionComponent && positionComponent.y < this.lowerBound) { 17 | if (!bodyComponent.body) { 18 | continue 19 | } 20 | const spawnPositionComponent = entity.getComponent(SpawnPositionComponent) 21 | if (spawnPositionComponent) { 22 | bodyComponent.body.setTranslation( 23 | { 24 | x: spawnPositionComponent.x, 25 | y: spawnPositionComponent.y, 26 | z: spawnPositionComponent.z, 27 | }, 28 | true 29 | ) 30 | } else { 31 | bodyComponent.body.setTranslation( 32 | { 33 | x: 0, 34 | y: 10, 35 | z: 0, 36 | }, 37 | true 38 | ) 39 | } 40 | bodyComponent.body.setLinvel(new Rapier.Vector3(0, 0, 0), true) 41 | bodyComponent.body.setAngvel(new Rapier.Vector3(0, 0, 0), true) 42 | 43 | if (entity.getComponent(PlayerComponent)) { 44 | if (entity.getComponent(LockedRotationComponent)) { 45 | entity.removeComponent(LockedRotationComponent) 46 | } else { 47 | entity.addComponent(new LockedRotationComponent(entity.id)) 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /shared/component/VehicleComponent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedComponent, SerializedComponentType } from '../network/server/serialized.js' 2 | import { NetworkComponent } from '../network/NetworkComponent.js' 3 | import { SerializedWheelComponent, WheelComponent } from './WheelComponent.js' 4 | import { PositionComponent } from './PositionComponent.js' 5 | import { RotationComponent } from './RotationComponent.js' 6 | 7 | /** 8 | * Attach this component to an entity to make it a vehicle. 9 | * The entity will also automatically get a VehicleRayCastComponent attached to it. 10 | */ 11 | export class VehicleComponent extends NetworkComponent { 12 | public driverEntityId?: number 13 | public passengerEntityIds: number[] = [] 14 | public wheels: WheelComponent[] = [] 15 | constructor(entityId: number, wheels: WheelComponent[]) { 16 | super(entityId, SerializedComponentType.VEHICLE) 17 | this.wheels = wheels 18 | } 19 | serialize() { 20 | return { 21 | d: this.driverEntityId, 22 | p: this.passengerEntityIds, 23 | w: this.wheels.map((wheel) => wheel.serialize()), 24 | } 25 | } 26 | deserialize(data: SerializedVehicleComponent): void { 27 | this.driverEntityId = data.d 28 | this.passengerEntityIds = data.p 29 | for (let i = 0; i < data.w.length; i++) { 30 | if (!this.wheels[i]) { 31 | this.wheels[i] = new WheelComponent({ 32 | entityId: this.entityId, 33 | positionComponent: new PositionComponent( 34 | this.entityId, 35 | data.w[i].pC.x, 36 | data.w[i].pC.y, 37 | data.w[i].pC.z 38 | ), 39 | rotationComponent: new RotationComponent( 40 | this.entityId, 41 | data.w[i].rC.x, 42 | data.w[i].rC.y, 43 | data.w[i].rC.z 44 | ), 45 | radius: data.w[i].r, 46 | }) 47 | } else { 48 | this.wheels[i].deserialize(data.w[i]) 49 | } 50 | } 51 | } 52 | } 53 | 54 | export interface SerializedVehicleComponent extends SerializedComponent { 55 | // Driver entity id 56 | d: number 57 | // Passenger entity ids 58 | p: number[] 59 | // Wheels (Debugging purpose for now.) 60 | w: SerializedWheelComponent[] 61 | } 62 | -------------------------------------------------------------------------------- /back/src/ecs/system/events/DestroyEventSystem.ts: -------------------------------------------------------------------------------- 1 | import { EntityDestroyedEvent } from '../../../../../shared/component/events/EntityDestroyedEvent.js' 2 | import { Entity } from '../../../../../shared/entity/Entity.js' 3 | import { EntityManager } from '../../../../../shared/system/EntityManager.js' 4 | import { EventSystem } from '../../../../../shared/system/EventSystem.js' 5 | import { MessageEvent } from '../../component/events/MessageEvent.js' 6 | import { PlayerComponent } from '../../../../../shared/component/PlayerComponent.js' 7 | import { SerializedMessageType } from '../../../../../shared/network/server/serialized.js' 8 | 9 | // In DestroyEventSystem.ts 10 | export class DestroyEventSystem { 11 | // Mark entities for destruction but don't remove them yet 12 | update(entities: Entity[]) { 13 | const destroyedEvents = EventSystem.getEvents(EntityDestroyedEvent) 14 | for (const destroyedEvent of destroyedEvents) { 15 | const entity = EntityManager.getEntityById(entities, destroyedEvent.entityId) 16 | if (!entity) { 17 | console.error('Update : DestroySystem: Entity not found with id', destroyedEvent.entityId) 18 | continue 19 | } 20 | 21 | // Send a message to all players when a player disconnects 22 | const playerComponent = entity.getComponent(PlayerComponent) 23 | if (playerComponent) { 24 | EventSystem.addEvent( 25 | new MessageEvent( 26 | entity.id, 27 | '🖥️ [SERVER]', 28 | `Player ${playerComponent.name} disconnected at ${new Date().toLocaleString()}`, 29 | SerializedMessageType.GLOBAL_CHAT 30 | ) 31 | ) 32 | } 33 | 34 | // This will create ComponentRemovedEvent for each component 35 | entity.removeAllComponents() 36 | } 37 | } 38 | 39 | // Actually remove the entities at the end of the update cycle 40 | afterUpdate(entities: Entity[]) { 41 | const destroyedEvents = EventSystem.getEvents(EntityDestroyedEvent) 42 | 43 | for (const destroyedEvent of destroyedEvents) { 44 | const entity = EntityManager.getEntityById(entities, destroyedEvent.entityId) 45 | if (!entity) { 46 | console.error('Update : DestroySystem: Entity not found with id', destroyedEvent.entityId) 47 | continue 48 | } 49 | 50 | EntityManager.removeEntity(entity) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /back/src/ecs/system/events/SingleSizeEventSystem.ts: -------------------------------------------------------------------------------- 1 | import { SingleSizeComponent } from '../../../../../shared/component/SingleSizeComponent.js' 2 | import { Entity } from '../../../../../shared/entity/Entity.js' 3 | import { EntityManager } from '../../../../../shared/system/EntityManager.js' 4 | import { EventSystem } from '../../../../../shared/system/EventSystem.js' 5 | import Rapier from '../../../physics/rapier.js' 6 | import { SingleSizeEvent } from '../../component/events/SingleSizeEvent.js' 7 | import { BoxColliderComponent } from '../../component/physics/BoxColliderComponent.js' 8 | import { CapsuleColliderComponent } from '../../component/physics/CapsuleColliderComponent.js' 9 | import { SphereColliderComponent } from '../../component/physics/SphereColliderComponent.js' 10 | 11 | export class SingleSizeEventSystem { 12 | update(entities: Entity[]) { 13 | const eventSizes = EventSystem.getEvents(SingleSizeEvent) 14 | 15 | for (const eventSingleSize of eventSizes) { 16 | const entity = EntityManager.getEntityById(entities, eventSingleSize.entityId) 17 | 18 | if (!entity) continue 19 | // Request new size 20 | const { size: newSize } = eventSingleSize 21 | 22 | const singleSizeComponent = entity.getComponent(SingleSizeComponent) 23 | if (!singleSizeComponent) { 24 | console.error('SingleSizeComponent not found') 25 | continue 26 | } 27 | 28 | const boxColliderComponent = entity.getComponent(BoxColliderComponent) 29 | if (boxColliderComponent) { 30 | const colliderDesc = Rapier.ColliderDesc.cuboid(newSize, newSize, newSize) 31 | boxColliderComponent.collider?.setShape(colliderDesc.shape) 32 | } 33 | 34 | const sphereColliderComponent = entity.getComponent(SphereColliderComponent) 35 | if (sphereColliderComponent) { 36 | const colliderDesc = Rapier.ColliderDesc.ball(newSize) 37 | sphereColliderComponent.collider?.setShape(colliderDesc.shape) 38 | } 39 | 40 | const capsuleColliderComponent = entity.getComponent(CapsuleColliderComponent) 41 | if (capsuleColliderComponent) { 42 | const colliderDesc = Rapier.ColliderDesc.capsule(newSize, newSize) 43 | capsuleColliderComponent.collider?.setShape(colliderDesc.shape) 44 | } 45 | 46 | // This will rebroadcast the update to all clients. 47 | if (singleSizeComponent) { 48 | singleSizeComponent.size = newSize 49 | singleSizeComponent.updated = true 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /front/game/ecs/system/ServerMeshSystem.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '@shared/entity/Entity' 2 | import { MeshComponent } from '../component/MeshComponent' 3 | import { EventSystem } from '@shared/system/EventSystem' 4 | import { ComponentAddedEvent } from '@shared/component/events/ComponentAddedEvent' 5 | import { ServerMeshComponent } from '@shared/component/ServerMeshComponent' 6 | import { EntityManager } from '@shared/system/EntityManager' 7 | import { LoadManager } from '@/game/LoadManager' 8 | import { AnimationComponent } from '../component/AnimationComponent' 9 | import { SerializedEntityType } from '@shared/network/server/serialized' 10 | 11 | export class ServerMeshSystem { 12 | async update(entities: Entity[]): Promise { 13 | const createEvents = EventSystem.getEventsWrapped(ComponentAddedEvent, ServerMeshComponent) 14 | const promises = createEvents.map((event: ComponentAddedEvent) => { 15 | const entity = EntityManager.getEntityById(entities, event.entityId) 16 | 17 | if (!entity) { 18 | console.error('ServerMeshSystem: Entity not found') 19 | return Promise.resolve() // Return a resolved promise if the entity is not found 20 | } 21 | // Hack to load the world in parallel 22 | // Initial load is faster 23 | if (entity.type === SerializedEntityType.WORLD) { 24 | this.onServerMeshReceived(event, entity) 25 | return Promise.resolve() 26 | } 27 | return this.onServerMeshReceived(event, entity) 28 | }) 29 | 30 | await Promise.all(promises) 31 | } 32 | 33 | async onServerMeshReceived( 34 | event: ComponentAddedEvent, 35 | entity: Entity 36 | ): Promise { 37 | const serverMeshComponent = event.component 38 | 39 | // Load the mesh from the serverMeshComponent 40 | const mesh = await LoadManager.glTFLoad(serverMeshComponent.filePath) 41 | const meshComponent = new MeshComponent(entity.id, mesh) 42 | 43 | // // Debug : Add a box helper around the mesh (if player) 44 | // const geometry = new THREE.CapsuleGeometry(1, 1, 32) 45 | // const material = new THREE.MeshBasicMaterial({ wireframe: true }) 46 | // meshComponent.mesh.geometry = geometry 47 | // meshComponent.mesh.material = material 48 | entity.addComponent(meshComponent) 49 | 50 | if (mesh.animations && mesh.animations.length > 0) { 51 | entity.addComponent( 52 | new AnimationComponent(entity.id, meshComponent.mesh, meshComponent.mesh.animations) 53 | ) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /shared/component/ProximityPromptComponent.ts: -------------------------------------------------------------------------------- 1 | import { SerializedComponent, SerializedComponentType } from '../network/server/serialized.js' 2 | import { NetworkComponent } from '../network/NetworkComponent.js' 3 | import { Entity } from '../entity/Entity.js' 4 | import { SerializedTextComponent, TextComponent } from './TextComponent.js' 5 | 6 | /** 7 | * Interface for ProximityPromptComponent parameters 8 | */ 9 | export interface ProximityPromptParams { 10 | text?: string 11 | onInteract: (interactingEntity: Entity) => void 12 | maxInteractDistance?: number 13 | interactionCooldown?: number 14 | holdDuration?: number 15 | } 16 | 17 | /** 18 | * Component for interactible entities 19 | * Will be rendered by the client with a TextComponent 20 | */ 21 | export class ProximityPromptComponent extends NetworkComponent { 22 | public textComponent: TextComponent 23 | 24 | // Map to track interaction timing for each entity (used to check if the player has interacted with the entity recently) 25 | // Entity -> Accumulator 26 | public accumulatorPerEntity: Map = new Map() 27 | 28 | constructor(entityId: number, params: ProximityPromptParams) { 29 | super(entityId, SerializedComponentType.PROXIMITY_PROMPT) 30 | this.textComponent = new TextComponent( 31 | entityId, 32 | params.text ?? 'Interact', 33 | 0, 34 | 0, 35 | 0, 36 | params.maxInteractDistance ?? 10 37 | ) 38 | this.onInteract = params.onInteract 39 | this.maxInteractDistance = params.maxInteractDistance ?? 10 40 | this.interactionCooldown = params.interactionCooldown ?? 1000 41 | // Unused for now, but could be used for hold interactions in the future 42 | this.holdDuration = params.holdDuration ?? 1000 43 | } 44 | 45 | public onInteract: (interactingEntity: Entity) => void 46 | public maxInteractDistance: number 47 | public interactionCooldown: number 48 | 49 | // Unused for now, but could be used for hold interactions in the future 50 | public holdDuration: number 51 | 52 | serialize(): SerializedProximityPromptComponent { 53 | return { 54 | tC: this.textComponent.serialize(), 55 | iC: this.interactionCooldown, 56 | } 57 | } 58 | 59 | deserialize(data: SerializedProximityPromptComponent): void { 60 | this.textComponent.deserialize(data.tC) 61 | this.interactionCooldown = data.iC 62 | } 63 | 64 | interact(interactingEntity: Entity) { 65 | this.onInteract(interactingEntity) 66 | } 67 | } 68 | 69 | export interface SerializedProximityPromptComponent extends SerializedComponent { 70 | tC: SerializedTextComponent 71 | iC: number 72 | } 73 | -------------------------------------------------------------------------------- /back/src/ecs/system/RandomizeSystem.ts: -------------------------------------------------------------------------------- 1 | import { SingleSizeComponent } from '../../../../shared/component/SingleSizeComponent.js' 2 | import { ColorComponent } from '../../../../shared/component/ColorComponent.js' 3 | import { SizeComponent } from '../../../../shared/component/SizeComponent.js' 4 | import { Entity } from '../../../../shared/entity/Entity.js' 5 | import { EventSystem } from '../../../../shared/system/EventSystem.js' 6 | import Rapier from '../../physics/rapier.js' 7 | import { DynamicRigidBodyComponent } from '../component/physics/DynamicRigidBodyComponent.js' 8 | import { RandomizeComponent } from '../component/RandomizeComponent.js' 9 | import { ColorEvent } from '../component/events/ColorEvent.js' 10 | import { SizeEvent } from '../component/events/SizeEvent.js' 11 | import { SingleSizeEvent } from '../component/events/SingleSizeEvent.js' 12 | 13 | export class RandomizeSystem { 14 | update(entities: Entity[]) { 15 | for (const entity of entities) { 16 | if (!entity.getComponent(RandomizeComponent)) continue 17 | 18 | const sizeComponent = entity.getComponent(SizeComponent) 19 | if (sizeComponent) { 20 | if (Math.random() < 0.01) { 21 | EventSystem.addEvent( 22 | new SizeEvent( 23 | entity.id, 24 | 1 + Math.random() * 4, 25 | 1 + Math.random() * 4, 26 | 1 + Math.random() * 4 27 | ) 28 | ) 29 | } 30 | } 31 | 32 | const singleSizeComponent = entity.getComponent(SingleSizeComponent) 33 | if (singleSizeComponent) { 34 | if (Math.random() < 0.01) { 35 | EventSystem.addEvent(new SingleSizeEvent(entity.id, Math.max(2, Math.random() * 3))) 36 | } 37 | } 38 | 39 | const colorComponent = entity.getComponent(ColorComponent) 40 | 41 | if (colorComponent) { 42 | if (Math.random() < 0.01) { 43 | const randomHex = Math.floor(Math.random() * 16777215).toString(16) 44 | EventSystem.addEvent(new ColorEvent(entity.id, '#' + randomHex)) 45 | } 46 | } 47 | 48 | const rigidBodyComponent = entity.getComponent(DynamicRigidBodyComponent) 49 | 50 | if (rigidBodyComponent) { 51 | if (!rigidBodyComponent.body) { 52 | continue 53 | } 54 | if (Math.random() < 0.05) { 55 | rigidBodyComponent.body.applyImpulse( 56 | new Rapier.Vector3( 57 | (Math.random() - 1 / 2) * 500, 58 | (Math.random() - 1 / 2) * 500, 59 | (Math.random() - 1 / 2) * 500 60 | ), 61 | true 62 | ) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /back/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env.development.local 77 | .env.test.local 78 | .env.production.local 79 | .env.local 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | .parcel-cache 84 | 85 | # Next.js build output 86 | .next 87 | out 88 | 89 | # Nuxt.js build / generate output 90 | .nuxt 91 | dist 92 | 93 | # Gatsby files 94 | .cache/ 95 | # Comment in the public line in if your project uses Gatsby and not Next.js 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # public 98 | 99 | # vuepress build output 100 | .vuepress/dist 101 | 102 | # vuepress v2.x temp and cache directory 103 | .temp 104 | .cache 105 | 106 | # Docusaurus cache and generated files 107 | .docusaurus 108 | 109 | # Serverless directories 110 | .serverless/ 111 | 112 | # FuseBox cache 113 | .fusebox/ 114 | 115 | # DynamoDB Local files 116 | .dynamodb/ 117 | 118 | # TernJS port file 119 | .tern-port 120 | 121 | # Stores VSCode versions used for testing VSCode extensions 122 | .vscode-test 123 | 124 | # yarn v2 125 | .yarn/cache 126 | .yarn/unplugged 127 | .yarn/build-state.yml 128 | .yarn/install-state.gz 129 | .pnp.* 130 | -------------------------------------------------------------------------------- /monitor/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env.development.local 77 | .env.test.local 78 | .env.production.local 79 | .env.local 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | .parcel-cache 84 | 85 | # Next.js build output 86 | .next 87 | out 88 | 89 | # Nuxt.js build / generate output 90 | .nuxt 91 | dist 92 | 93 | # Gatsby files 94 | .cache/ 95 | # Comment in the public line in if your project uses Gatsby and not Next.js 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # public 98 | 99 | # vuepress build output 100 | .vuepress/dist 101 | 102 | # vuepress v2.x temp and cache directory 103 | .temp 104 | .cache 105 | 106 | # Docusaurus cache and generated files 107 | .docusaurus 108 | 109 | # Serverless directories 110 | .serverless/ 111 | 112 | # FuseBox cache 113 | .fusebox/ 114 | 115 | # DynamoDB Local files 116 | .dynamodb/ 117 | 118 | # TernJS port file 119 | .tern-port 120 | 121 | # Stores VSCode versions used for testing VSCode extensions 122 | .vscode-test 123 | 124 | # yarn v2 125 | .yarn/cache 126 | .yarn/unplugged 127 | .yarn/build-state.yml 128 | .yarn/install-state.gz 129 | .pnp.* 130 | 131 | .env -------------------------------------------------------------------------------- /shared/system/EntityManager.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../entity/Entity.js' 2 | import { SerializedEntityType } from '../network/server/serialized.js' 3 | import { Component } from '../component/Component.js' 4 | 5 | export class EntityManager { 6 | private static instance: EntityManager 7 | private entities: Entity[] = [] 8 | private static nextId = 1 9 | private constructor() {} 10 | 11 | // Singleton pattern to get the single instance of EntityManager 12 | static getInstance(): EntityManager { 13 | if (!EntityManager.instance) { 14 | EntityManager.instance = new EntityManager() 15 | } 16 | return EntityManager.instance 17 | } 18 | 19 | // Create a new entity and add it to the list 20 | static createEntity(type: SerializedEntityType, id?: number): Entity { 21 | const entityManager = EntityManager.getInstance() 22 | const entityId = id ?? EntityManager.nextId++ 23 | const entity = new Entity(type, entityId) 24 | entityManager.entities.push(entity) 25 | return entity 26 | } 27 | 28 | // Get all entities 29 | getAllEntities(): Entity[] { 30 | return this.entities 31 | } 32 | 33 | // Get entities by type 34 | static getEntitiesByType(entities: Entity[], type: SerializedEntityType): Entity[] { 35 | return entities.filter((entity) => entity.type === type) 36 | } 37 | 38 | // Get the first entity by type 39 | static getFirstEntityByType(entities: Entity[], type: SerializedEntityType): Entity | undefined { 40 | return entities.find((entity) => entity.type === type) 41 | } 42 | 43 | // Get entity by id 44 | static getEntityById(entities: Entity[], id: number): Entity | undefined { 45 | return entities.find((entity) => entity.id === id) 46 | } 47 | 48 | // Get the first entity with a specific component 49 | static getFirstEntityWithComponent( 50 | entities: Entity[], 51 | componentType: new (entityId: number, ...args: any[]) => T 52 | ): Entity | undefined { 53 | return entities.find((entity) => entity.getComponent(componentType)) 54 | } 55 | 56 | // Remove an entity 57 | static removeEntity(entity: Entity): void { 58 | const entityManager = EntityManager.getInstance() 59 | 60 | const index = entityManager.entities.indexOf(entity) 61 | if (index !== -1) { 62 | entityManager.entities.splice(index, 1) 63 | } else { 64 | console.error('Entity not found in EntityManager') 65 | } 66 | } 67 | 68 | // Remove an entity by id 69 | static removeEntityById(id: number): void { 70 | const entityManager = EntityManager.getInstance() 71 | 72 | const index = entityManager.entities.findIndex((entity) => entity.id === id) 73 | console.log('Removing entity', id, 'from EntityManager') 74 | if (index !== -1) { 75 | entityManager.entities.splice(index, 1) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /back/src/ecs/system/physics/LockRotationSystem.ts: -------------------------------------------------------------------------------- 1 | import Rapier from '../../../physics/rapier.js' 2 | import { ComponentAddedEvent } from '../../../../../shared/component/events/ComponentAddedEvent.js' 3 | import { Entity } from '../../../../../shared/entity/Entity.js' 4 | import { EventSystem } from '../../../../../shared/system/EventSystem.js' 5 | import { ComponentRemovedEvent } from '../../../../../shared/component/events/ComponentRemovedEvent.js' 6 | import { EntityManager } from '../../../../../shared/system/EntityManager.js' 7 | import { DynamicRigidBodyComponent } from '../../component/physics/DynamicRigidBodyComponent.js' 8 | import { LockedRotationComponent } from '../../component/LockedRotationComponent.js' 9 | 10 | export class LockRotationSystem { 11 | update(entities: Entity[]) { 12 | const createEvents = EventSystem.getEventsWrapped(ComponentAddedEvent, LockedRotationComponent) 13 | 14 | for (const event of createEvents) { 15 | const entity = EntityManager.getEntityById(entities, event.entityId) 16 | if (!entity) { 17 | console.error('LockRotationSystem: Entity not found') 18 | continue 19 | } 20 | this.onRotationLocked(entity) 21 | } 22 | 23 | const removedEvents = EventSystem.getEventsWrapped( 24 | ComponentRemovedEvent, 25 | LockedRotationComponent 26 | ) 27 | 28 | for (const event of removedEvents) { 29 | const entity = EntityManager.getEntityById(entities, event.entityId) 30 | if (!entity) { 31 | console.error('LockRotationSystem: Entity not found') 32 | continue 33 | } 34 | this.onRotationUnlocked(entity) 35 | } 36 | } 37 | getDynamicBodyComponent(entity: Entity) { 38 | const dynamicBodyComponent = entity.getComponent(DynamicRigidBodyComponent) 39 | if (!dynamicBodyComponent) { 40 | console.error('LockRotationSystem: Entity does not have a DynamicRigidBodyComponent') 41 | return null 42 | } 43 | if (!dynamicBodyComponent.body) { 44 | console.error('LockRotationSystem: Entity does not have a body') 45 | return null 46 | } 47 | return dynamicBodyComponent 48 | } 49 | onRotationLocked(entity: Entity) { 50 | const dynamicBodyComponent = this.getDynamicBodyComponent(entity) 51 | if (dynamicBodyComponent && dynamicBodyComponent.body) { 52 | dynamicBodyComponent.body.setLinvel(new Rapier.Vector3(0, 0, 0), true) 53 | dynamicBodyComponent.body.setAngvel(new Rapier.Vector3(0, 0, 0), true) 54 | dynamicBodyComponent.body.setEnabledRotations(false, false, false, true) 55 | } 56 | } 57 | 58 | onRotationUnlocked(entity: Entity) { 59 | const dynamicBodyComponent = this.getDynamicBodyComponent(entity) 60 | if (dynamicBodyComponent && dynamicBodyComponent.body) { 61 | dynamicBodyComponent.body.setEnabledRotations(true, true, true, true) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /front/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Image from 'next/image' 3 | import { Github, Twitter } from 'lucide-react' 4 | import Link from 'next/link' 5 | 6 | export default function Navbar() { 7 | return ( 8 |
9 |
10 |
11 | Logo 18 |

19 | 20 | NotBlox.online 21 | 22 |

23 |
24 | 25 | {/* Social Icons (GitHub, Discord, Twitter) */} 26 | 43 |
44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /back/src/ecs/system/physics/CollisionSystem.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../../../../../shared/entity/Entity.js' 2 | import Rapier from '../../../physics/rapier.js' 3 | import { OnCollisionEnterEvent } from '../../component/events/OnCollisionEnterEvent.js' 4 | import { OnCollisionExitEvent } from '../../component/events/OnCollisionExitEvent.js' 5 | import { BoxColliderComponent } from '../../component/physics/BoxColliderComponent.js' 6 | import { CapsuleColliderComponent } from '../../component/physics/CapsuleColliderComponent.js' 7 | import { ConvexHullColliderComponent } from '../../component/physics/ConvexHullColliderComponent.js' 8 | import { SphereColliderComponent } from '../../component/physics/SphereColliderComponent.js' 9 | 10 | export class CollisionSystem { 11 | update(entities: Entity[], world: Rapier.World, eventQueue: Rapier.EventQueue) { 12 | // Create a mapping of handles to entities 13 | // TODO: Use a more efficient way to map handles to entities, this is a temporary solution 14 | const handleToEntityMap = new Map() 15 | 16 | for (const entity of entities) { 17 | const colliderTypes = [ 18 | BoxColliderComponent, 19 | SphereColliderComponent, 20 | CapsuleColliderComponent, 21 | ConvexHullColliderComponent, 22 | ] 23 | 24 | for (const ColliderType of colliderTypes) { 25 | const handle = entity.getComponent(ColliderType)?.collider?.handle 26 | if (handle != null) { 27 | handleToEntityMap.set(handle, entity) 28 | break 29 | } 30 | } 31 | } 32 | 33 | // Handle collision events 34 | eventQueue.drainCollisionEvents((handle1, handle2, started) => { 35 | const entityFirst = handleToEntityMap.get(handle1) 36 | const entitySecond = handleToEntityMap.get(handle2) 37 | // console.log('entityFirst', entityFirst) 38 | // console.log('entitySecond', entitySecond) 39 | if (entityFirst && entitySecond) { 40 | if (started) { 41 | const onCollisionEnterFirst = entityFirst.getComponent(OnCollisionEnterEvent) 42 | if (onCollisionEnterFirst) { 43 | onCollisionEnterFirst.onCollisionEnter(entitySecond) 44 | } 45 | const onCollisionEnterSecond = entitySecond.getComponent(OnCollisionEnterEvent) 46 | if (onCollisionEnterSecond) { 47 | onCollisionEnterSecond.onCollisionEnter(entityFirst) 48 | } 49 | } else { 50 | const onCollisionExitFirst = entityFirst.getComponent(OnCollisionExitEvent) 51 | if (onCollisionExitFirst) { 52 | onCollisionExitFirst.onCollisionExit(entitySecond) 53 | } 54 | const onCollisionExitSecond = entitySecond.getComponent(OnCollisionExitEvent) 55 | if (onCollisionExitSecond) { 56 | onCollisionExitSecond.onCollisionExit(entityFirst) 57 | } 58 | } 59 | } 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /front/app/page.tsx: -------------------------------------------------------------------------------- 1 | import GameCard from '@/components/GameCard' 2 | import KeyboardLayout from '@/components/KeyboardLayout' 3 | import Navbar from '@/components/Navbar' 4 | import { ExternalLink, Github, Twitter } from 'lucide-react' 5 | import Link from 'next/link' 6 | import { GameInfo } from '../types' 7 | import gameData from '../public/gameData.json' 8 | import { Metadata } from 'next' 9 | 10 | export async function generateMetadata(): Promise { 11 | return { 12 | title: 'NotBlox - Play multiplayer games in your browser', 13 | description: 14 | 'Play multiplayer games in your browser. Create your own games and share them with your friends.', 15 | openGraph: { 16 | title: 'NotBlox - Play multiplayer games in your browser', 17 | description: 18 | 'Play multiplayer games in your browser. Create your own games and share them with your friends.', 19 | images: ['/PreviewTestGame.webp'], 20 | siteName: 'NotBlox Online', 21 | }, 22 | twitter: { 23 | card: 'summary_large_image', 24 | site: '@iercan_', 25 | creator: '@iercan_', 26 | }, 27 | } 28 | } 29 | 30 | export default async function Home() { 31 | const games = gameData as GameInfo[] 32 | return ( 33 |
34 | 35 |
36 | {games && 37 | games.map((game, index) => ( 38 |
45 | 46 |
47 | ))} 48 |
49 | 50 | 51 |
52 | 56 | 57 | Project Discord 58 | 59 | 63 | 64 | Twitter 65 | 66 | 70 | 71 | Source Code 72 | 73 |
74 |
75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /back/src/ecs/system/VehicleCreationSystem.ts: -------------------------------------------------------------------------------- 1 | import { EventSystem } from '../../../../shared/system/EventSystem.js' 2 | import { ComponentAddedEvent } from '../../../../shared/component/events/ComponentAddedEvent.js' 3 | import { Entity } from '../../../../shared/entity/Entity.js' 4 | import { VehicleComponent } from '../../../../shared/component/VehicleComponent.js' 5 | import { EntityManager } from '../../../../shared/system/EntityManager.js' 6 | import { DynamicRigidBodyComponent } from '../component/physics/DynamicRigidBodyComponent.js' 7 | import Rapier from '../../physics/rapier.js' 8 | import { VehicleRayCastComponent } from '../component/physics/VehicleRayCastComponent.js' 9 | import { WheelComponent } from '../../../../shared/component/WheelComponent.js' 10 | 11 | export class VehicleCreationSystem { 12 | update(entities: Entity[], world: Rapier.World): void { 13 | this.handleVehicleCreation(entities, world) 14 | } 15 | 16 | private handleVehicleCreation(entities: Entity[], world: Rapier.World): void { 17 | const createEvents = EventSystem.getEventsWrapped(ComponentAddedEvent, VehicleComponent) 18 | for (const event of createEvents) { 19 | const vehicleComponent: VehicleComponent = event.component 20 | const entity = EntityManager.getEntityById(entities, event.entityId) 21 | if (!entity) { 22 | console.error('VehicleCreationSystem: Entity not found') 23 | continue 24 | } 25 | 26 | const rigidbody = entity.getComponent(DynamicRigidBodyComponent)?.body 27 | if (!rigidbody) { 28 | console.error('VehicleCreationSystem: No rigidbody found') 29 | continue 30 | } 31 | 32 | const vehicleController = world.createVehicleController(rigidbody) 33 | const wheelsComponents: WheelComponent[] = vehicleComponent.wheels 34 | 35 | for (let i = 0; i < wheelsComponents.length; i++) { 36 | const wheelComponent = wheelsComponents[i] 37 | vehicleController.addWheel( 38 | new Rapier.Vector3( 39 | wheelComponent.positionComponent.x, 40 | wheelComponent.positionComponent.y, 41 | wheelComponent.positionComponent.z 42 | ), 43 | new Rapier.Vector3(0, -1, 0), 44 | new Rapier.Vector3(-1, 0, 0), 45 | wheelComponent.suspensionLength, 46 | wheelComponent.radius 47 | ) 48 | vehicleController.setWheelSuspensionCompression(i, wheelComponent.suspensionCompression) 49 | vehicleController.setWheelSuspensionStiffness(i, wheelComponent.suspensionStiffness) 50 | vehicleController.setWheelSuspensionRelaxation(i, wheelComponent.suspensionRelaxation) 51 | vehicleController.setWheelSideFrictionStiffness(i, wheelComponent.sideFrictionStiffness) 52 | vehicleController.setWheelFrictionSlip(i, wheelComponent.frictionSlip) 53 | vehicleController.setWheelMaxSuspensionForce(i, wheelComponent.maxSuspensionForce) 54 | vehicleController.setWheelMaxSuspensionTravel(i, wheelComponent.maxSuspensionTravel) 55 | } 56 | 57 | entity.addComponent(new VehicleRayCastComponent(entity.id, vehicleController)) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /front/game/LoadManager.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js' 3 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' 4 | import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js' 5 | 6 | export class LoadManager { 7 | private static instance: LoadManager 8 | private cache = new Map() 9 | dracoLoader = new DRACOLoader() 10 | gltfLoader = new GLTFLoader() 11 | 12 | private constructor() { 13 | this.dracoLoader.setDecoderPath('/draco/') // Replace with the actual path to the Draco decoder 14 | this.gltfLoader.setDRACOLoader(this.dracoLoader) 15 | } 16 | 17 | static getInstance(): LoadManager { 18 | if (!LoadManager.instance) { 19 | LoadManager.instance = new LoadManager() 20 | } 21 | return LoadManager.instance 22 | } 23 | 24 | static glTFLoad(path: string): Promise { 25 | const instance = LoadManager.getInstance() 26 | 27 | // // Check if the mesh is already in the cache 28 | if (instance.cache.has(path)) { 29 | const cachedMesh = instance.cache.get(path)! 30 | const clonedMesh = instance.cloneMesh(cachedMesh) 31 | return Promise.resolve(clonedMesh) 32 | } 33 | 34 | // If not, load the model and store the mesh in the cache 35 | return new Promise((resolve, reject) => { 36 | instance.gltfLoader.load( 37 | path, 38 | (gltf) => { 39 | // Extract the first mesh from the loaded model 40 | const mesh = instance.extractMesh(gltf) 41 | if (mesh) { 42 | // Cache the original mesh 43 | instance.cache.set(path, mesh) 44 | // Resolve with a clone of the mesh 45 | const clonedMesh = instance.cloneMesh(mesh) 46 | resolve(clonedMesh) 47 | } else { 48 | reject(new Error('No mesh found in the GLTF model')) 49 | } 50 | }, 51 | // called as loading progresses 52 | (xhr) => { 53 | console.log((xhr.loaded / xhr.total) * 100 + '% loaded') 54 | }, 55 | // called when loading has errors 56 | (error) => { 57 | console.error('An error happened', error) 58 | reject(error) 59 | } 60 | ) 61 | }) 62 | } 63 | 64 | private cloneMesh(mesh: THREE.Mesh): THREE.Mesh { 65 | const clonedMesh = SkeletonUtils.clone(mesh) 66 | clonedMesh.animations = mesh.animations 67 | // Clone materials to avoid sharing the same material instance 68 | clonedMesh.traverse((child) => { 69 | if (child instanceof THREE.Mesh) { 70 | const material = child.material 71 | if (Array.isArray(material)) { 72 | child.material = material.map((m) => m.clone()) 73 | } else { 74 | child.material = material.clone() 75 | } 76 | } 77 | }) 78 | return clonedMesh as THREE.Mesh 79 | } 80 | 81 | private extractMesh(gltf: any): THREE.Mesh | null { 82 | let mesh: THREE.Mesh = new THREE.Mesh() 83 | mesh.add(gltf.scene) 84 | mesh.animations = gltf.animations 85 | return mesh 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /front/game/ecs/system/MeshSystem.ts: -------------------------------------------------------------------------------- 1 | import { Renderer } from '@/game/Renderer.js' 2 | import { Entity } from '@shared/entity/Entity.js' 3 | import { EventSystem } from '@shared/system/EventSystem.js' 4 | import { MeshComponent } from '../component/MeshComponent.js' 5 | import { ComponentAddedEvent } from '@shared/component/events/ComponentAddedEvent.js' 6 | import { ComponentRemovedEvent } from '@shared/component/events/ComponentRemovedEvent.js' 7 | import * as THREE from 'three' 8 | import { PositionComponent } from '@shared/component/PositionComponent.js' 9 | import { RotationComponent } from '@shared/component/RotationComponent.js' 10 | import { EntityManager } from '@shared/system/EntityManager.js' 11 | 12 | export class MeshSystem { 13 | update(entities: Entity[], renderer: Renderer) { 14 | this.handleAddedMeshes(entities, renderer) 15 | this.handleRemovedMeshes(renderer) 16 | } 17 | 18 | private handleAddedMeshes(entities: Entity[], renderer: Renderer) { 19 | const addedMeshEvents = EventSystem.getEventsWrapped(ComponentAddedEvent, MeshComponent) 20 | 21 | for (const addedEvent of addedMeshEvents) { 22 | const entity = EntityManager.getEntityById(entities, addedEvent.entityId) 23 | if (!entity) { 24 | console.error('MeshSystem: Entity not found') 25 | continue 26 | } 27 | 28 | const meshComponent = addedEvent.component 29 | this.updateMeshTransform(entity, meshComponent) 30 | this.addMeshToScene(meshComponent, renderer) 31 | } 32 | } 33 | 34 | private handleRemovedMeshes(renderer: Renderer) { 35 | const removedMeshEvents = EventSystem.getEventsWrapped(ComponentRemovedEvent, MeshComponent) 36 | 37 | for (const removedEvent of removedMeshEvents) { 38 | const meshComponent = removedEvent.component 39 | renderer.scene.remove(meshComponent.mesh) 40 | } 41 | } 42 | 43 | private updateMeshTransform(entity: Entity, meshComponent: MeshComponent) { 44 | const positionComponent = entity.getComponent(PositionComponent) 45 | if (positionComponent) { 46 | meshComponent.mesh.position.set(positionComponent.x, positionComponent.y, positionComponent.z) 47 | } 48 | 49 | const rotationComponent = entity.getComponent(RotationComponent) 50 | if (rotationComponent) { 51 | meshComponent.mesh.quaternion.set( 52 | rotationComponent.x, 53 | rotationComponent.y, 54 | rotationComponent.z, 55 | rotationComponent.w 56 | ) 57 | } 58 | } 59 | 60 | private addMeshToScene(meshComponent: MeshComponent, renderer: Renderer) { 61 | console.log('MeshSystem: Adding mesh to scene') 62 | this.activateShadows(meshComponent) 63 | renderer.scene.add(meshComponent.mesh) 64 | } 65 | 66 | private activateShadows(meshComponent: MeshComponent) { 67 | const object3D = meshComponent.mesh 68 | object3D.castShadow = true 69 | object3D.receiveShadow = true 70 | 71 | object3D.traverse((child) => { 72 | if (child instanceof THREE.Mesh) { 73 | child.material.metalness = 0 74 | child.castShadow = true 75 | child.receiveShadow = true 76 | } 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /shared/component/events/EventListComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentConstructor } from '../Component.js' 2 | 3 | /** 4 | * Holds a list of events, each of which is a component 5 | * @extends Component 6 | * @property {Map} events Map of event components 7 | * @example 8 | * // Create a new event list component 9 | * const eventListComponent = new EventListComponent(1) 10 | * // Add an event to the list 11 | * eventListComponent.addEvent(new MessageEvent(1, 'Player', 'Hello, world!')) 12 | * // Get all events in the list 13 | * const allEvents = eventListComponent.getAllEvents() 14 | * // Get an event of a certain type 15 | * const messages = eventListComponent.getEvents(MessageEvent) 16 | */ 17 | export class EventListComponent extends Component { 18 | events: Map = new Map() 19 | 20 | constructor(entityId: number) { 21 | super(entityId) 22 | } 23 | 24 | /** 25 | * Add an event to the list 26 | * @param event The event component to add 27 | */ 28 | addEvent(event: Component): void { 29 | const componentType = event.constructor as ComponentConstructor 30 | if (!this.events.has(componentType)) { 31 | this.events.set(componentType, []) 32 | } 33 | this.events.get(componentType)!.push(event) 34 | } 35 | 36 | /** 37 | * Remove an event from the list 38 | * @param event The event component to remove 39 | */ 40 | removeEvent(event: Component): void { 41 | const componentType = event.constructor as ComponentConstructor 42 | const eventArray = this.events.get(componentType) 43 | if (eventArray) { 44 | this.events.set( 45 | componentType, 46 | eventArray.filter((e) => e !== event) 47 | ) 48 | if (this.events.get(componentType)!.length === 0) { 49 | this.events.delete(componentType) 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * Remove all events from the list 56 | */ 57 | removeAllEvents(): void { 58 | this.events.clear() 59 | } 60 | 61 | /** 62 | * Get all events in the list 63 | * @returns Array of event components 64 | */ 65 | getAllEvents(): Component[] { 66 | const allEvents: Component[] = [] 67 | this.events.forEach((eventArray) => { 68 | allEvents.push(...eventArray) 69 | }) 70 | return allEvents 71 | } 72 | 73 | /** 74 | * Get an event from the list 75 | * @param event The event component to get 76 | * @returns The event if it exists, otherwise undefined 77 | */ 78 | getEvent(event: Component): Component | undefined { 79 | const componentType = event.constructor as ComponentConstructor 80 | const eventArray = this.events.get(componentType) 81 | return eventArray ? eventArray.find((e) => e === event) : undefined 82 | } 83 | 84 | /** 85 | * Get all events of a certain type 86 | * @param componentType The type of the event component to get 87 | * @returns Array of event components of the specified type 88 | */ 89 | getEvents(componentType: ComponentConstructor): T[] { 90 | return (this.events.get(componentType) as T[]) || [] 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /front/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* Firefox */ 6 | * { 7 | scrollbar-width: thin; 8 | scrollbar-color: var(--secondary) var(--primary); 9 | } 10 | 11 | 12 | 13 | @layer base { 14 | :root { 15 | --background: 0 0% 100%; 16 | --foreground: 0 0% 3.9%; 17 | 18 | --card: 0 0% 100%; 19 | --card-foreground: 0 0% 3.9%; 20 | 21 | --popover: 0 0% 100%; 22 | --popover-foreground: 0 0% 3.9%; 23 | 24 | --primary: 0 0% 9%; 25 | --primary-foreground: 0 0% 98%; 26 | 27 | --secondary: 0 0% 96.1%; 28 | --secondary-foreground: 0 0% 9%; 29 | 30 | --muted: 0 0% 96.1%; 31 | --muted-foreground: 0 0% 45.1%; 32 | 33 | --accent: 0 0% 96.1%; 34 | --accent-foreground: 0 0% 9%; 35 | 36 | --destructive: 0 84.2% 60.2%; 37 | --destructive-foreground: 0 0% 98%; 38 | 39 | --border: 0 0% 89.8%; 40 | --input: 0 0% 89.8%; 41 | --ring: 0 0% 3.9%; 42 | 43 | --radius: 0.5rem; 44 | 45 | --chart-1: 12 76% 61%; 46 | 47 | --chart-2: 173 58% 39%; 48 | 49 | --chart-3: 197 37% 24%; 50 | 51 | --chart-4: 43 74% 66%; 52 | 53 | --chart-5: 27 87% 67%; 54 | } 55 | .dark { 56 | --background: 0 0% 3.9%; 57 | --foreground: 0 0% 98%; 58 | --card: 0 0% 3.9%; 59 | --card-foreground: 0 0% 98%; 60 | --popover: 0 0% 3.9%; 61 | --popover-foreground: 0 0% 98%; 62 | --primary: 0 0% 98%; 63 | --primary-foreground: 0 0% 9%; 64 | --secondary: 0 0% 14.9%; 65 | --secondary-foreground: 0 0% 98%; 66 | --muted: 0 0% 14.9%; 67 | --muted-foreground: 0 0% 63.9%; 68 | --accent: 0 0% 14.9%; 69 | --accent-foreground: 0 0% 98%; 70 | --destructive: 0 62.8% 30.6%; 71 | --destructive-foreground: 0 0% 98%; 72 | --border: 0 0% 14.9%; 73 | --input: 0 0% 14.9%; 74 | --ring: 0 0% 83.1%; 75 | --chart-1: 220 70% 50%; 76 | --chart-2: 160 60% 45%; 77 | --chart-3: 30 80% 55%; 78 | --chart-4: 280 65% 60%; 79 | --chart-5: 340 75% 55%; 80 | } 81 | 82 | 83 | 84 | } 85 | 86 | 87 | * { 88 | scrollbar-color: rgba(250, 250, 255, 0.1) rgba(0, 0, 0, 0.2); /* Thumb color followed by track color */ 89 | } 90 | 91 | 92 | @layer base { 93 | * { 94 | @apply border-border; 95 | } 96 | body { 97 | @apply bg-background text-foreground; 98 | } 99 | } 100 | 101 | 102 | 103 | @keyframes fadeIn { 104 | from { opacity: 0; transform: translateY(-20px); } 105 | to { opacity: 1; transform: translateY(0); } 106 | } 107 | 108 | @keyframes bounceIn { 109 | 0% { transform: scale(0.8) translateY(0); opacity: 0.9; } 110 | 20% { transform: scale(1.05) translateY(-10px); opacity: 1; } 111 | 40% { transform: scale(0.95) translateY(-5px); } 112 | 60% { transform: scale(1.02) translateY(-3px); } 113 | 80% { transform: scale(0.98) translateY(-1px); } 114 | 100% { transform: scale(1) translateY(0); opacity: 1; } 115 | } 116 | 117 | @keyframes fadeOut { 118 | from { opacity: 1; transform: translateY(0); } 119 | to { opacity: 0; transform: translateY(-20px); } 120 | } -------------------------------------------------------------------------------- /back/src/ecs/system/physics/SphereColliderSystem.ts: -------------------------------------------------------------------------------- 1 | import { SingleSizeComponent } from '../../../../../shared/component/SingleSizeComponent.js' 2 | import { ComponentAddedEvent } from '../../../../../shared/component/events/ComponentAddedEvent.js' 3 | import { Entity } from '../../../../../shared/entity/Entity.js' 4 | import { EntityManager } from '../../../../../shared/system/EntityManager.js' 5 | import { EventSystem } from '../../../../../shared/system/EventSystem.js' 6 | import Rapier from '../../../physics/rapier.js' 7 | import { ColliderPropertiesComponent } from '../../component/physics/ColliderPropertiesComponent.js' 8 | import { DynamicRigidBodyComponent } from '../../component/physics/DynamicRigidBodyComponent.js' 9 | import { KinematicRigidBodyComponent } from '../../component/physics/KinematicRigidBodyComponent.js' 10 | import { SphereColliderComponent } from '../../component/physics/SphereColliderComponent.js' 11 | 12 | export class SphereColliderSystem { 13 | async update(entities: Entity[], world: Rapier.World) { 14 | const createEvents = EventSystem.getEventsWrapped(ComponentAddedEvent, SphereColliderComponent) 15 | for (const event of createEvents) { 16 | const entity = EntityManager.getEntityById(entities, event.entityId) 17 | 18 | if (!entity) { 19 | console.error('SphereColliderSystem: Entity not found') 20 | continue 21 | } 22 | 23 | this.onComponentAdded(entity, event, world) 24 | } 25 | } 26 | 27 | onComponentAdded( 28 | entity: Entity, 29 | event: ComponentAddedEvent, 30 | world: Rapier.World 31 | ) { 32 | const { component: sphereColliderComponent } = event 33 | let singleSizeComponent = entity.getComponent(SingleSizeComponent) 34 | const rigidBodyComponent = 35 | entity.getComponent(DynamicRigidBodyComponent) || 36 | entity.getComponent(KinematicRigidBodyComponent) 37 | 38 | if (!rigidBodyComponent) { 39 | console.error('SphereColliderSystem : No RigidBodyComponent found on entity.') 40 | return 41 | } 42 | 43 | if (!singleSizeComponent) { 44 | singleSizeComponent = new SingleSizeComponent(entity.id, 1) 45 | entity.addComponent(singleSizeComponent) 46 | 47 | console.warn( 48 | 'SphereColliderSystem : No SingleSizeComponent found on entity. Using a default size of 1.0.' 49 | ) 50 | } 51 | 52 | const colliderDesc = Rapier.ColliderDesc.ball(singleSizeComponent.size) 53 | const colliderProperties = entity.getComponent(ColliderPropertiesComponent) 54 | 55 | if (colliderProperties) { 56 | if (colliderProperties.data.isSensor !== undefined) { 57 | colliderDesc.setSensor(colliderProperties.data.isSensor) 58 | } 59 | if (colliderProperties.data.friction !== undefined) { 60 | colliderDesc.setFriction(colliderProperties.data.friction) 61 | } 62 | if (colliderProperties.data.restitution !== undefined) { 63 | colliderDesc.setRestitution(colliderProperties.data.restitution) 64 | } 65 | } 66 | colliderDesc.setActiveEvents(Rapier.ActiveEvents.COLLISION_EVENTS) 67 | sphereColliderComponent.collider = world.createCollider(colliderDesc, rigidBodyComponent.body) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /back/src/ecs/system/physics/BoxColliderSystem.ts: -------------------------------------------------------------------------------- 1 | import { SizeComponent } from '../../../../../shared/component/SizeComponent.js' 2 | import { ComponentAddedEvent } from '../../../../../shared/component/events/ComponentAddedEvent.js' 3 | import { Entity } from '../../../../../shared/entity/Entity.js' 4 | import { EntityManager } from '../../../../../shared/system/EntityManager.js' 5 | import { EventSystem } from '../../../../../shared/system/EventSystem.js' 6 | import Rapier from '../../../physics/rapier.js' 7 | import { BoxColliderComponent } from '../../component/physics/BoxColliderComponent.js' 8 | import { ColliderPropertiesComponent } from '../../component/physics/ColliderPropertiesComponent.js' 9 | import { DynamicRigidBodyComponent } from '../../component/physics/DynamicRigidBodyComponent.js' 10 | import { KinematicRigidBodyComponent } from '../../component/physics/KinematicRigidBodyComponent.js' 11 | 12 | export class BoxColliderSystem { 13 | async update(entities: Entity[], world: Rapier.World) { 14 | const createEvents = EventSystem.getEventsWrapped(ComponentAddedEvent, BoxColliderComponent) 15 | for (const event of createEvents) { 16 | const entity = EntityManager.getEntityById(entities, event.entityId) 17 | 18 | if (!entity) { 19 | console.error('BoxColliderSystem: Entity not found') 20 | continue 21 | } 22 | 23 | this.onComponentAdded(entity, event, world) 24 | } 25 | } 26 | 27 | onComponentAdded( 28 | entity: Entity, 29 | event: ComponentAddedEvent, 30 | world: Rapier.World 31 | ) { 32 | // Collider 33 | const { component: boxColliderComponent } = event 34 | let sizeComponent = entity.getComponent(SizeComponent) 35 | const rigidBodyComponent = 36 | entity.getComponent(DynamicRigidBodyComponent) || 37 | entity.getComponent(KinematicRigidBodyComponent) 38 | 39 | if (!rigidBodyComponent) { 40 | console.error('BoxColliderSystem : No RigidBodyComponent found on entity.') 41 | return 42 | } 43 | 44 | if (!sizeComponent) { 45 | sizeComponent = new SizeComponent(entity.id, 1, 1, 1) 46 | entity.addComponent(sizeComponent) 47 | 48 | console.warn( 49 | 'BoxColliderSystem : No SizeComponent found on entity. Using a default size of 1.0.' 50 | ) 51 | } 52 | 53 | const colliderDesc = Rapier.ColliderDesc.cuboid( 54 | sizeComponent.width, 55 | sizeComponent.height, 56 | sizeComponent.depth 57 | ) 58 | 59 | const colliderProperties = entity.getComponent(ColliderPropertiesComponent) 60 | 61 | if (colliderProperties) { 62 | if (colliderProperties.data.isSensor !== undefined) { 63 | colliderDesc.setSensor(colliderProperties.data.isSensor) 64 | } 65 | if (colliderProperties.data.friction !== undefined) { 66 | colliderDesc.setFriction(colliderProperties.data.friction) 67 | } 68 | if (colliderProperties.data.restitution !== undefined) { 69 | colliderDesc.setRestitution(colliderProperties.data.restitution) 70 | } 71 | } 72 | 73 | colliderDesc.setActiveEvents(Rapier.ActiveEvents.COLLISION_EVENTS) 74 | boxColliderComponent.collider = world.createCollider(colliderDesc, rigidBodyComponent.body) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /front/game/ecs/system/ChatSystem.ts: -------------------------------------------------------------------------------- 1 | import { Hud } from '@/game/Hud' 2 | import { MessageListComponent } from '@shared/component/MessageComponent' 3 | import { Entity } from '@shared/entity/Entity' 4 | 5 | /** 6 | * ChatSystem handles the synchronization of messages between the server and client. 7 | * It's responsible for merging new messages from the server with the existing client-side message history. 8 | */ 9 | export class ChatSystem { 10 | /** 11 | * Updates the chat system by processing new messages from the server and merging them with the existing client history. 12 | */ 13 | update(entities: Entity[], hud: Hud) { 14 | for (const entity of entities) { 15 | /** 16 | * Message Flow Architecture: 17 | * 18 | * SERVER SIDE: 19 | * 1. Message Creation: 20 | * - Messages are created through MessageEvent events (chat messages, notifications) 21 | * - MessageEventSystem processes these events and adds them to a MessageListComponent 22 | * - The component MessageListComponent is marked as 'updated' when new messages are added 23 | * 24 | * 2. Network Transmission: 25 | * - MessageListComponent is serialized in the snapshot message 26 | * - Only the most recent messages are sent to save bandwidth (e.g 20 messages) 27 | * 28 | * CLIENT SIDE: 29 | * 3. Message Reception & Processing: 30 | * - This system receives the MessageListComponent from network snapshots 31 | * - New messages are merged with existing client-side history 32 | * - Duplicate messages are filtered using timestamp as unique identifier 33 | * - Message history is capped at 250 messages to prevent memory issues 34 | * 35 | * 4. UI Rendering: 36 | * - The merged messages are passed to GameHud component via Hud props 37 | * - Messages are filtered by type (chat vs. notification) and target 38 | * - Global and targeted notifications are displayed at the top of the screen 39 | * - Chat messages appear in the chat window 40 | */ 41 | const chatListComponent = entity.getComponent(MessageListComponent) 42 | if (chatListComponent && chatListComponent.updated) { 43 | if (hud.updateChat === undefined) { 44 | console.error('HUD not initialized for the ChatSystem.') 45 | return 46 | } 47 | 48 | // Pass only new messages instead of the entire list 49 | const newMessages = chatListComponent.list.slice() 50 | 51 | // Use updateChat with a callback to access previous messages 52 | hud.updateChat((prevMessages) => { 53 | // Filter out duplicate messages by comparing timestamps 54 | const uniqueNewMessages = newMessages.filter( 55 | (newMsg) => !prevMessages.some((prevMsg) => prevMsg.timestamp === newMsg.timestamp) 56 | ) 57 | 58 | // Only add new messages if there are any 59 | if (uniqueNewMessages.length > 0) { 60 | // Limit front history to 250 messages 61 | const combinedMessages = [...prevMessages, ...uniqueNewMessages] 62 | return combinedMessages.slice(-250) 63 | } 64 | 65 | return prevMessages 66 | }) 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /front/components/KeyboardLayout.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useState, useEffect } from 'react' 3 | import { KeyboardLanguage } from '@/game/InputManager' 4 | 5 | export default function KeyboardLayout() { 6 | const [keyboardLayout, setKeyboardLayout] = useState(KeyboardLanguage.EN) 7 | 8 | useEffect(() => { 9 | const savedLayout = localStorage.getItem('keyboardLayout') 10 | if (savedLayout) { 11 | setKeyboardLayout(savedLayout as KeyboardLanguage) 12 | } 13 | }, []) 14 | 15 | const toggleKeyboardLayout = () => { 16 | const newLayout = 17 | keyboardLayout === KeyboardLanguage.EN ? KeyboardLanguage.FR : KeyboardLanguage.EN 18 | setKeyboardLayout(newLayout) 19 | localStorage.setItem('keyboardLanguage', newLayout) 20 | } 21 | 22 | return ( 23 |
24 |

25 | Current controls {keyboardLayout === KeyboardLanguage.EN ? 'QWERTY' : 'AZERTY (French)'} 26 |

27 |

Move

28 |
29 |
30 | {keyboardLayout === KeyboardLanguage.EN ? ( 31 | <> 32 | 33 | W 34 | 35 | 36 | A 37 | 38 | 39 | S 40 | 41 | 42 | D 43 | 44 | 45 | ) : ( 46 | <> 47 | 48 | Z 49 | 50 | 51 | Q 52 | 53 | 54 | S 55 | 56 | 57 | D 58 | 59 | 60 | )} 61 |
62 |
63 | 64 | 67 |
68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /shared/component/MessageComponent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SerializedComponent, 3 | SerializedComponentType, 4 | SerializedMessageType, 5 | } from '../network/server/serialized.js' 6 | import { NetworkComponent } from '../network/NetworkComponent.js' 7 | 8 | export class MessageComponent extends NetworkComponent { 9 | public author: string 10 | public content: string 11 | public messageType: SerializedMessageType 12 | public timestamp: number 13 | public targetPlayerIds: number[] 14 | 15 | constructor(entityId: number, message: SerializedMessageComponent) { 16 | super(entityId, SerializedComponentType.MESSAGE) 17 | this.author = message.a 18 | this.content = message.c 19 | this.messageType = message.mT 20 | this.timestamp = message.ts 21 | this.targetPlayerIds = message.tpIds || [] 22 | } 23 | 24 | deserialize(data: SerializedMessageComponent) { 25 | this.author = data.a 26 | this.content = data.c 27 | this.messageType = data.mT 28 | this.timestamp = data.ts 29 | this.targetPlayerIds = data.tpIds || [] 30 | } 31 | 32 | serialize(): SerializedMessageComponent { 33 | return { 34 | mT: this.messageType, 35 | c: this.content, 36 | a: this.author, 37 | ts: this.timestamp, 38 | tpIds: this.targetPlayerIds, 39 | } 40 | } 41 | } 42 | 43 | export interface SerializedMessageComponent extends SerializedComponent { 44 | // Message type 45 | mT: SerializedMessageType 46 | // Message content 47 | c: string 48 | // Author 49 | a: string 50 | // Creation Timestamp 51 | ts: number 52 | // Target player entity IDs 53 | tpIds?: number[] 54 | } 55 | 56 | export class MessageListComponent extends NetworkComponent { 57 | constructor(entityId: number, public list: MessageComponent[]) { 58 | super(entityId, SerializedComponentType.CHAT_LIST) 59 | } 60 | deserialize(data: SerializedMessageListComponent): void { 61 | this.list = data.messages.map((message) => new MessageComponent(this.entityId, message)) 62 | } 63 | serialize(): SerializedMessageListComponent { 64 | const messages = this.list 65 | .filter((message) => message.updated) 66 | .map((message) => message.serialize()) 67 | return { messages } 68 | } 69 | /** 70 | * Add a message to the chat list 71 | * @param author The name of the message sender 72 | * @param content The text content of the message 73 | * @param messageType The type of message (GLOBAL_CHAT, TARGETED_CHAT, GLOBAL_NOTIFICATION, TARGETED_NOTIFICATION) 74 | * @param targetPlayerIds Array of player IDs for targeted messages (only used when messageType is TARGETED_CHAT or TARGETED_NOTIFICATION) 75 | */ 76 | addMessage( 77 | author: string, 78 | content: string, 79 | messageType = SerializedMessageType.GLOBAL_CHAT, 80 | targetPlayerIds: number[] = [] 81 | ) { 82 | this.list.push( 83 | new MessageComponent(this.entityId, { 84 | a: author, 85 | c: content, 86 | mT: messageType, 87 | tpIds: targetPlayerIds, 88 | ts: Date.now(), 89 | }) 90 | ) 91 | 92 | // Updated set to true so new messages are sent to the clients. 93 | // TODO: Only set update booleans of MessageComponents 94 | this.updated = true 95 | } 96 | } 97 | 98 | export interface SerializedMessageListComponent extends SerializedComponent { 99 | messages: SerializedMessageComponent[] 100 | } 101 | -------------------------------------------------------------------------------- /back/src/ecs/system/events/MessageEventSystem.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '../../../../../shared/system/EntityManager.js' 2 | import { MessageListComponent } from '../../../../../shared/component/MessageComponent.js' 3 | import { Entity } from '../../../../../shared/entity/Entity.js' 4 | import { MessageEvent } from '../../component/events/MessageEvent.js' 5 | import { ChatComponent } from '../../component/tag/TagChatComponent.js' 6 | import { EventSystem } from '../../../../../shared/system/EventSystem.js' 7 | import { SerializedMessageType } from '../../../../../shared/network/server/serialized.js' 8 | import { config } from '../../../../../shared/network/config.js' 9 | 10 | export class MessageEventSystem { 11 | private MAX_MESSAGES: number = 20 12 | 13 | update(entities: Entity[]) { 14 | const chatMessageEvents = EventSystem.getEvents(MessageEvent) 15 | 16 | // Check if there are any chat messages to process 17 | if (chatMessageEvents.length === 0) { 18 | return 19 | } 20 | 21 | // Find Chat Entity 22 | // For now, we assume there is only one chat entity, but we could have multiple chat entities (local, group, etc) 23 | const chatEntity = EntityManager.getFirstEntityWithComponent(entities, ChatComponent) 24 | 25 | if (!chatEntity) { 26 | console.error('ChatEventSystem : A chat entity is required to send messages.') 27 | return 28 | } 29 | 30 | for (const chatMessageEvent of chatMessageEvents) { 31 | const messageListComponent = chatEntity.getComponent(MessageListComponent) 32 | if (messageListComponent) { 33 | let content = chatMessageEvent.content 34 | const sender = chatMessageEvent.sender 35 | const messageType = chatMessageEvent.messageType 36 | const targetPlayerIds = chatMessageEvent.targetPlayerIds 37 | 38 | // Limit content length 39 | content = content.slice(0, config.MAX_MESSAGE_CONTENT_LENGTH) 40 | // Limit message history (bandwidth) 41 | if (messageListComponent.list.length >= this.MAX_MESSAGES) { 42 | messageListComponent.list.shift() 43 | } 44 | 45 | // Handle different message types 46 | /** 47 | * Those messages are broadcasted to everybody, and front end handles the targeting. 48 | */ 49 | switch (messageType) { 50 | case SerializedMessageType.GLOBAL_NOTIFICATION: 51 | // Add global notification 52 | messageListComponent.addMessage( 53 | sender, 54 | content, 55 | SerializedMessageType.GLOBAL_NOTIFICATION 56 | ) 57 | break 58 | 59 | case SerializedMessageType.TARGETED_NOTIFICATION: 60 | case SerializedMessageType.TARGETED_CHAT: 61 | // Check if we have valid target player IDs 62 | if (targetPlayerIds && targetPlayerIds.length > 0) { 63 | // Add targeted message with appropriate message type 64 | messageListComponent.addMessage(sender, content, messageType, targetPlayerIds) 65 | } else { 66 | console.warn(`ChatEventSystem: ${messageType} without target player IDs`) 67 | // Fall back to regular chat message 68 | messageListComponent.addMessage(sender, content, SerializedMessageType.GLOBAL_CHAT) 69 | } 70 | break 71 | 72 | case SerializedMessageType.GLOBAL_CHAT: 73 | default: 74 | // Regular chat message 75 | messageListComponent.addMessage(sender, content, SerializedMessageType.GLOBAL_CHAT) 76 | break 77 | } 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /back/src/ecs/entity/Mesh.ts: -------------------------------------------------------------------------------- 1 | import { PositionComponent } from '../../../../shared/component/PositionComponent.js' 2 | import { RotationComponent } from '../../../../shared/component/RotationComponent.js' 3 | 4 | import { Entity } from '../../../../shared/entity/Entity.js' 5 | import { SerializedEntityType } from '../../../../shared/network/server/serialized.js' 6 | 7 | import { EntityManager } from '../../../../shared/system/EntityManager.js' 8 | import { NetworkDataComponent } from '../../../../shared/network/NetworkDataComponent.js' 9 | import { DynamicRigidBodyComponent } from '../component/physics/DynamicRigidBodyComponent.js' 10 | import { ServerMeshComponent } from '../../../../shared/component/ServerMeshComponent.js' 11 | import { 12 | PhysicsPropertiesComponent, 13 | PhysicsPropertiesComponentData, 14 | } from '../component/physics/PhysicsPropertiesComponent.js' 15 | import { 16 | ColliderPropertiesComponent, 17 | ColliderPropertiesComponentData, 18 | } from '../component/physics/ColliderPropertiesComponent.js' 19 | import { ConvexHullColliderComponent } from '../component/physics/ConvexHullColliderComponent.js' 20 | 21 | export interface MeshParams { 22 | position: { 23 | x: number 24 | y: number 25 | z: number 26 | } 27 | /** 28 | * @default { width: 1, height: 1, depth: 1 } 29 | */ 30 | size?: { 31 | width: number 32 | height: number 33 | depth: number 34 | } 35 | /** 36 | * @default "default" (Mesh color is unchanged) 37 | */ 38 | color?: string 39 | /** 40 | * @default "https://notbloxo.fra1.cdn.digitaloceanspaces.com/Notblox-Assets/base/Crate.glb" 41 | */ 42 | meshUrl?: string 43 | /** 44 | * @default {} 45 | */ 46 | physicsProperties?: PhysicsPropertiesComponentData 47 | /** 48 | * @default {} 49 | */ 50 | colliderProperties?: ColliderPropertiesComponentData 51 | } 52 | 53 | export class Mesh { 54 | entity: Entity 55 | 56 | constructor(params: MeshParams) { 57 | const { position, meshUrl, physicsProperties, colliderProperties } = params 58 | 59 | this.entity = EntityManager.createEntity(SerializedEntityType.CUBE) 60 | 61 | const positionComponent = new PositionComponent( 62 | this.entity.id, 63 | position.x, 64 | position.y, 65 | position.z 66 | ) 67 | this.entity.addComponent(positionComponent) 68 | 69 | const rotationComponent = new RotationComponent(this.entity.id, 0, 0, 0, 0) 70 | this.entity.addComponent(rotationComponent) 71 | 72 | const serverMeshComponent = new ServerMeshComponent( 73 | this.entity.id, 74 | meshUrl ?? 'https://notbloxo.fra1.cdn.digitaloceanspaces.com/Notblox-Assets/base/Crate.glb' 75 | ) 76 | this.entity.addComponent(serverMeshComponent) 77 | 78 | this.entity.addComponent( 79 | new ColliderPropertiesComponent(this.entity.id, colliderProperties ?? {}) 80 | ) 81 | this.entity.addComponent( 82 | new ConvexHullColliderComponent( 83 | this.entity.id, 84 | meshUrl ?? 'https://notbloxo.fra1.cdn.digitaloceanspaces.com/Notblox-Assets/base/Crate.glb' 85 | ) 86 | ) 87 | this.entity.addComponent( 88 | new PhysicsPropertiesComponent(this.entity.id, physicsProperties ?? {}) 89 | ) 90 | this.entity.addComponent(new DynamicRigidBodyComponent(this.entity.id)) 91 | 92 | const networkDataComponent = new NetworkDataComponent(this.entity.id, this.entity.type, [ 93 | positionComponent, 94 | rotationComponent, 95 | serverMeshComponent, 96 | ]) 97 | this.entity.addComponent(networkDataComponent) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /back/src/ecs/entity/TriggerCube.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../../../../shared/entity/Entity.js' 2 | import { SerializedEntityType } from '../../../../shared/network/server/serialized.js' 3 | import { EntityManager } from '../../../../shared/system/EntityManager.js' 4 | import { PositionComponent } from '../../../../shared/component/PositionComponent.js' 5 | import { SizeComponent } from '../../../../shared/component/SizeComponent.js' 6 | import { BoxColliderComponent } from '../component/physics/BoxColliderComponent.js' 7 | import { KinematicRigidBodyComponent } from '../component/physics/KinematicRigidBodyComponent.js' 8 | import { OnCollisionEnterEvent } from '../component/events/OnCollisionEnterEvent.js' 9 | import { OnCollisionExitEvent } from '../component/events/OnCollisionExitEvent.js' 10 | import { ColliderPropertiesComponent } from '../component/physics/ColliderPropertiesComponent.js' 11 | import { NetworkDataComponent } from '../../../../shared/network/NetworkDataComponent.js' 12 | import { ServerMeshComponent } from '../../../../shared/component/ServerMeshComponent.js' 13 | import { ColorComponent } from '../../../../shared/component/ColorComponent.js' 14 | 15 | /** 16 | * A trigger cube area to trigger events when a player enters or exits it. 17 | * It is a kinematic rigid body and a box collider with sensor enabled. (Invisible & non-collidable) 18 | */ 19 | export class TriggerCube { 20 | entity: Entity 21 | 22 | constructor( 23 | x: number, 24 | y: number, 25 | z: number, 26 | width: number, 27 | height: number, 28 | depth: number, 29 | onEnter: (entity: Entity) => void, 30 | onExit: (entity: Entity) => void, 31 | showDebug: boolean = false 32 | ) { 33 | this.entity = EntityManager.createEntity(SerializedEntityType.CUBE) 34 | 35 | const positionComponent = new PositionComponent(this.entity.id, x, y, z) 36 | this.entity.addComponent(positionComponent) 37 | 38 | const sizeComponent = new SizeComponent(this.entity.id, width, height, depth) 39 | this.entity.addComponent(sizeComponent) 40 | 41 | // Add kinematic rigid body 42 | this.entity.addComponent(new KinematicRigidBodyComponent(this.entity.id)) 43 | 44 | // Make it a sensor to be traversable by other entities 45 | const colliderProperties = new ColliderPropertiesComponent(this.entity.id, { 46 | isSensor: true, 47 | friction: 0, 48 | restitution: 0, 49 | }) 50 | this.entity.addComponent(colliderProperties) 51 | 52 | // Add box collider with sensor enabled 53 | const boxCollider = new BoxColliderComponent(this.entity.id) 54 | this.entity.addComponent(boxCollider) 55 | 56 | this.entity.addComponent(new OnCollisionEnterEvent(this.entity.id, onEnter)) 57 | this.entity.addComponent(new OnCollisionExitEvent(this.entity.id, onExit)) 58 | 59 | // Show the trigger cube for debugging purposes. 60 | // Will show a red cube. 61 | if (showDebug) { 62 | // Debug mesh 63 | const serverMeshComponent = new ServerMeshComponent( 64 | this.entity.id, 65 | 'https://notbloxo.fra1.cdn.digitaloceanspaces.com/Notblox-Assets/base/Crate.glb' 66 | ) 67 | this.entity.addComponent(serverMeshComponent) 68 | 69 | // Debug color 70 | const colorComponent = new ColorComponent(this.entity.id, '#ff0000') 71 | this.entity.addComponent(colorComponent) 72 | 73 | const networkDataComponent = new NetworkDataComponent(this.entity.id, this.entity.type, [ 74 | positionComponent, 75 | sizeComponent, 76 | serverMeshComponent, 77 | colorComponent, 78 | ]) 79 | this.entity.addComponent(networkDataComponent) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /back/src/ecs/system/physics/KinematicRigidBodySystem.ts: -------------------------------------------------------------------------------- 1 | import Rapier from '../../../physics/rapier.js' 2 | import { ComponentAddedEvent } from '../../../../../shared/component/events/ComponentAddedEvent.js' 3 | import { EventSystem } from '../../../../../shared/system/EventSystem.js' 4 | import { KinematicRigidBodyComponent } from '../../component/physics/KinematicRigidBodyComponent.js' 5 | import { ComponentRemovedEvent } from '../../../../../shared/component/events/ComponentRemovedEvent.js' 6 | import { PositionComponent } from '../../../../../shared/component/PositionComponent.js' 7 | import { Entity } from '../../../../../shared/entity/Entity.js' 8 | import { EntityManager } from '../../../../../shared/system/EntityManager.js' 9 | import { PhysicsPropertiesComponent } from '../../component/physics/PhysicsPropertiesComponent.js' 10 | 11 | export class KinematicRigidBodySystem { 12 | update(entities: Entity[], world: Rapier.World) { 13 | const createEvents = EventSystem.getEventsWrapped( 14 | ComponentAddedEvent, 15 | KinematicRigidBodyComponent 16 | ) 17 | for (const event of createEvents) { 18 | const entity = EntityManager.getEntityById(entities, event.entityId) 19 | if (!entity) { 20 | console.error('DynamicRigidBodySystem: Entity not found') 21 | continue 22 | } 23 | this.onComponentAdded(entity, event, world) 24 | } 25 | 26 | const removedEvents = EventSystem.getEventsWrapped( 27 | ComponentRemovedEvent, 28 | KinematicRigidBodyComponent 29 | ) 30 | 31 | for (const event of removedEvents) { 32 | this.onComonentRemoved(event, world) 33 | } 34 | } 35 | onComponentAdded( 36 | entity: Entity, 37 | event: ComponentAddedEvent, 38 | world: Rapier.World 39 | ) { 40 | // No position component here, we move the body directly, so it's at the origin 41 | const physicsBodyComponent = event.component 42 | const kinematic = Rapier.RigidBodyDesc.kinematicPositionBased() 43 | kinematic.setCcdEnabled(true) 44 | 45 | const rigidBody = world.createRigidBody(kinematic) 46 | physicsBodyComponent.body = rigidBody 47 | 48 | const positionComponent = entity.getComponent(PositionComponent) 49 | if (positionComponent) { 50 | rigidBody.setTranslation( 51 | new Rapier.Vector3(positionComponent.x, positionComponent.y, positionComponent.z), 52 | false 53 | ) 54 | } 55 | 56 | const physicsPropertiesComponent = entity.getComponent(PhysicsPropertiesComponent) 57 | if (physicsPropertiesComponent) { 58 | if (physicsPropertiesComponent.data.mass) { 59 | physicsBodyComponent.body.setAdditionalMass(physicsPropertiesComponent.data.mass, true) 60 | } 61 | if (physicsPropertiesComponent.data.angularDamping) { 62 | physicsBodyComponent.body.setAngularDamping(physicsPropertiesComponent.data.angularDamping) 63 | } 64 | if (physicsPropertiesComponent.data.enableCcd != undefined) { 65 | physicsBodyComponent.body.enableCcd(physicsPropertiesComponent.data.enableCcd) 66 | } 67 | if (physicsPropertiesComponent.data.linearDamping) { 68 | physicsBodyComponent.body.setLinearDamping(physicsPropertiesComponent.data.linearDamping) 69 | } 70 | if (physicsPropertiesComponent.data.gravityScale) { 71 | physicsBodyComponent.body.setGravityScale( 72 | physicsPropertiesComponent.data.gravityScale, 73 | true 74 | ) 75 | } 76 | } 77 | } 78 | 79 | onComonentRemoved( 80 | event: ComponentRemovedEvent, 81 | world: Rapier.World 82 | ) { 83 | const physicsBodyComponent = event.component 84 | if (physicsBodyComponent.body) world.removeRigidBody(physicsBodyComponent.body) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /back/src/ecs/system/physics/DynamicRigidBodySystem.ts: -------------------------------------------------------------------------------- 1 | import Rapier from '../../../physics/rapier.js' 2 | import { ComponentAddedEvent } from '../../../../../shared/component/events/ComponentAddedEvent.js' 3 | import { Entity } from '../../../../../shared/entity/Entity.js' 4 | import { EventSystem } from '../../../../../shared/system/EventSystem.js' 5 | import { ComponentRemovedEvent } from '../../../../../shared/component/events/ComponentRemovedEvent.js' 6 | import { EntityManager } from '../../../../../shared/system/EntityManager.js' 7 | import { PositionComponent } from '../../../../../shared/component/PositionComponent.js' 8 | import { DynamicRigidBodyComponent } from '../../component/physics/DynamicRigidBodyComponent.js' 9 | import { PhysicsPropertiesComponent } from '../../component/physics/PhysicsPropertiesComponent.js' 10 | 11 | export class DynamicRigidBodySystem { 12 | update(entities: Entity[], world: Rapier.World) { 13 | const createEvents = EventSystem.getEventsWrapped( 14 | ComponentAddedEvent, 15 | DynamicRigidBodyComponent 16 | ) 17 | 18 | for (const event of createEvents) { 19 | const entity = EntityManager.getEntityById(entities, event.entityId) 20 | if (!entity) { 21 | console.error('DynamicRigidBodySystem: Entity not found') 22 | continue 23 | } 24 | this.onComponentAdded(entity, event, world) 25 | } 26 | 27 | const removedEvents = EventSystem.getEventsWrapped( 28 | ComponentRemovedEvent, 29 | DynamicRigidBodyComponent 30 | ) 31 | 32 | for (const event of removedEvents) { 33 | this.onComponentRemoved(event, world) 34 | } 35 | } 36 | onComponentAdded( 37 | entity: Entity, 38 | event: ComponentAddedEvent, 39 | world: Rapier.World 40 | ) { 41 | const physicsBodyComponent = event.component 42 | const rbDesc = Rapier.RigidBodyDesc.dynamic() 43 | 44 | const positionComponent = entity.getComponent(PositionComponent) 45 | const rigidBody = world.createRigidBody(rbDesc) 46 | 47 | if (positionComponent) { 48 | rigidBody.setTranslation( 49 | new Rapier.Vector3(positionComponent.x, positionComponent.y, positionComponent.z), 50 | false 51 | ) 52 | } 53 | 54 | physicsBodyComponent.body = rigidBody 55 | const physicsPropertiesComponent = entity.getComponent(PhysicsPropertiesComponent) 56 | if (physicsPropertiesComponent) { 57 | if (physicsPropertiesComponent.data.mass !== undefined) { 58 | physicsBodyComponent.body.setAdditionalMass(physicsPropertiesComponent.data.mass, true) 59 | } 60 | if (physicsPropertiesComponent.data.angularDamping !== undefined) { 61 | physicsBodyComponent.body.setAngularDamping(physicsPropertiesComponent.data.angularDamping) 62 | } 63 | if (physicsPropertiesComponent.data.enableCcd != undefined) { 64 | physicsBodyComponent.body.enableCcd(physicsPropertiesComponent.data.enableCcd) 65 | } 66 | if (physicsPropertiesComponent.data.linearDamping !== undefined) { 67 | physicsBodyComponent.body.setLinearDamping(physicsPropertiesComponent.data.linearDamping) 68 | } 69 | if (physicsPropertiesComponent.data.gravityScale !== undefined) { 70 | physicsBodyComponent.body.setGravityScale( 71 | physicsPropertiesComponent.data.gravityScale, 72 | true 73 | ) 74 | } 75 | } 76 | } 77 | 78 | // TODO: Check if we need to remove the colliders too. 79 | onComponentRemoved(event: ComponentRemovedEvent, world: Rapier.World) { 80 | console.log('DynamicRigidBodySystem: Component removed') 81 | const physicsBodyComponent = event.component 82 | if (physicsBodyComponent.body) { 83 | world.removeRigidBody(physicsBodyComponent.body) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /back/src/ecs/system/events/ProximityPromptEventSystem.ts: -------------------------------------------------------------------------------- 1 | import { EventSystem } from '../../../../../shared/system/EventSystem.js' 2 | import { PositionComponent } from '../../../../../shared/component/PositionComponent.js' 3 | import { ProximityPromptComponent } from '../../../../../shared/component/ProximityPromptComponent.js' 4 | import { Entity } from '../../../../../shared/entity/Entity.js' 5 | import * as THREE from 'three' 6 | import { EntityManager } from '../../../../../shared/system/EntityManager.js' 7 | import { ProximityPromptInteractEvent } from '../../component/events/ProximityPromptInteractEvent.js' 8 | 9 | export class ProximityPromptSystem { 10 | update(entities: Entity[], dt: number) { 11 | // Update the interaction accumulator for each entity 12 | for (const entity of entities) { 13 | const proximityPromptComponent = entity.getComponent(ProximityPromptComponent) 14 | // Update the interaction accumulator for each entity 15 | if (proximityPromptComponent) { 16 | proximityPromptComponent.accumulatorPerEntity.forEach((accumulator, playerEntity) => { 17 | const cappedAccumulator = Math.min( 18 | accumulator + dt, 19 | proximityPromptComponent.interactionCooldown * 2 20 | ) 21 | proximityPromptComponent.accumulatorPerEntity.set(playerEntity, cappedAccumulator) 22 | }) 23 | } 24 | } 25 | 26 | // Get all the proximity prompt interact events 27 | const proximityEvents = EventSystem.getEvents(ProximityPromptInteractEvent) 28 | 29 | for (const event of proximityEvents) { 30 | // Find the entity that triggered the event 31 | const playerEntity = EntityManager.getEntityById(entities, event.entityId) 32 | // Find the entity that the player interacted with 33 | const targetEntity = EntityManager.getEntityById(entities, event.otherEntity) 34 | // Get the proximity prompt component of the target entity 35 | const proximityPromptComponent = targetEntity?.getComponent(ProximityPromptComponent) 36 | // Get the position component of the player entity 37 | const playerPos = playerEntity?.getComponent(PositionComponent) 38 | // Get the position component of the target entity 39 | const targetPos = targetEntity?.getComponent(PositionComponent) 40 | 41 | if (!playerEntity || !targetEntity || !proximityPromptComponent || !playerPos || !targetPos) { 42 | continue 43 | } 44 | 45 | if (this.isOnCooldown(playerEntity, proximityPromptComponent)) { 46 | continue 47 | } 48 | 49 | // Check if the player is in range of the target entity 50 | if (this.isInRange(playerPos, targetPos, proximityPromptComponent.maxInteractDistance)) { 51 | // Interact with the target entity 52 | proximityPromptComponent.interact(playerEntity) 53 | // Reset the interaction accumulator 54 | proximityPromptComponent.accumulatorPerEntity.set(playerEntity, 0) 55 | } 56 | } 57 | } 58 | private isOnCooldown( 59 | playerEntity: Entity, 60 | proximityPromptComponent: ProximityPromptComponent 61 | ): boolean { 62 | const interactionAccumulator = proximityPromptComponent.accumulatorPerEntity.get(playerEntity) 63 | if (interactionAccumulator === undefined) { 64 | proximityPromptComponent.accumulatorPerEntity.set(playerEntity, 0) 65 | return false 66 | } 67 | 68 | return interactionAccumulator < proximityPromptComponent.interactionCooldown 69 | } 70 | 71 | private isInRange( 72 | pos1: PositionComponent, 73 | pos2: PositionComponent, 74 | maxDistance: number 75 | ): boolean { 76 | const posVec1 = new THREE.Vector3(pos1.x, pos1.y, pos1.z) 77 | const posVec2 = new THREE.Vector3(pos2.x, pos2.y, pos2.z) 78 | return posVec1.distanceTo(posVec2) <= maxDistance 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /front/game/ecs/system/VehicleSystem.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '@shared/entity/Entity' 2 | import * as THREE from 'three' 3 | 4 | import { WheelComponent } from '@shared/component/WheelComponent' 5 | import { MeshComponent } from '../component/MeshComponent' 6 | import { VehicleComponent } from '@shared/component/VehicleComponent' 7 | import { ComponentAddedEvent } from '@shared/component/events/ComponentAddedEvent' 8 | import { EntityManager } from '@shared/system/EntityManager' 9 | import { EventSystem } from '@shared/system/EventSystem' 10 | import { LoadManager } from '@/game/LoadManager' 11 | export class VehicleSystem { 12 | private entityWheels: Map = new Map() 13 | private wheelModelUrl = 14 | 'https://notbloxo.fra1.cdn.digitaloceanspaces.com/Notblox-Assets/vehicle/Wheel.glb' 15 | 16 | update(entities: Entity[]) { 17 | // Catch vehicle creation, add wheels to it. 18 | const addedVehicleEvents = EventSystem.getEventsWrapped(ComponentAddedEvent, VehicleComponent) 19 | for (const addedEvent of addedVehicleEvents) { 20 | const vehicleEntity = EntityManager.getEntityById(entities, addedEvent.entityId) 21 | if (!vehicleEntity) continue 22 | 23 | // Load wheel model 24 | LoadManager.glTFLoad(this.wheelModelUrl).then((wheelModel) => { 25 | const vehicleComponent: VehicleComponent = addedEvent.component 26 | const meshComponent = vehicleEntity.getComponent(MeshComponent) 27 | const wheelComponents: WheelComponent[] = vehicleComponent.wheels 28 | if (vehicleComponent && meshComponent) { 29 | const wheelMeshes: THREE.Mesh[] = [] 30 | for (const wheel of wheelComponents) { 31 | const wheelMesh = wheelModel.clone() 32 | console.log('VehicleSystem: Adding wheel', wheel.radius) 33 | wheelMesh.position.set( 34 | wheel.positionComponent.x, 35 | wheel.positionComponent.y, 36 | wheel.positionComponent.z 37 | ) 38 | wheelMesh.rotation.setFromQuaternion( 39 | new THREE.Quaternion( 40 | wheel.rotationComponent.x, 41 | wheel.rotationComponent.y, 42 | wheel.rotationComponent.z, 43 | wheel.rotationComponent.w 44 | ) 45 | ) 46 | wheelMesh.scale.set(wheel.radius, wheel.radius, wheel.radius) 47 | //Game.getInstance().renderer.scene.add(wheelMesh) 48 | meshComponent.mesh.add(wheelMesh) 49 | wheelMeshes.push(wheelMesh) 50 | } 51 | this.entityWheels.set(vehicleEntity.id, wheelMeshes) 52 | } 53 | }) 54 | } 55 | 56 | // Update the wheels position and rotation 57 | for (const entity of entities) { 58 | const vehicleComponent = entity.getComponent(VehicleComponent) 59 | const meshComponent = entity.getComponent(MeshComponent) 60 | if (vehicleComponent && meshComponent) { 61 | const wheelMeshes = this.entityWheels.get(entity.id) 62 | if (wheelMeshes) { 63 | for (let i = 0; i < vehicleComponent.wheels.length; i++) { 64 | const wheel = vehicleComponent.wheels[i] 65 | const wheelMesh = wheelMeshes[i] 66 | wheelMesh.position.lerp( 67 | new THREE.Vector3( 68 | wheel.positionComponent.x, 69 | wheel.positionComponent.y, 70 | wheel.positionComponent.z 71 | ), 72 | 0.1 73 | ) 74 | const targetQuat = new THREE.Quaternion( 75 | wheel.rotationComponent.x, 76 | wheel.rotationComponent.y, 77 | wheel.rotationComponent.z, 78 | wheel.rotationComponent.w 79 | ) 80 | wheelMesh.quaternion.slerp(targetQuat, 0.1) 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /back/src/ecs/entity/OrbitalCompanion.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../../../../shared/entity/Entity.js' 2 | import { PositionComponent } from '../../../../shared/component/PositionComponent.js' 3 | import { RotationComponent } from '../../../../shared/component/RotationComponent.js' 4 | import { ServerMeshComponent } from '../../../../shared/component/ServerMeshComponent.js' 5 | import { NetworkDataComponent } from '../../../../shared/network/NetworkDataComponent.js' 6 | import { SerializedEntityType } from '../../../../shared/network/server/serialized.js' 7 | import { EntityManager } from '../../../../shared/system/EntityManager.js' 8 | import { FollowTargetComponent } from '../component/FollowTargetComponent.js' 9 | import { TextComponent } from '../../../../shared/component/TextComponent.js' 10 | import { SingleSizeComponent } from '../../../../shared/component/SingleSizeComponent.js' 11 | 12 | export interface OrbitalCompanionParams { 13 | /** 14 | * Position of the following item 15 | */ 16 | position: { 17 | x: number 18 | y: number 19 | z: number 20 | } 21 | /** 22 | * URL to the mesh that will be used for this item 23 | */ 24 | meshUrl: string 25 | /** 26 | * Entity ID of the target to follow 27 | */ 28 | targetEntityId: number 29 | /** 30 | * Offset from the target entity's position 31 | * @default { x: 0, y: 2, z: 0 } 32 | */ 33 | offset?: { 34 | x: number 35 | y: number 36 | z: number 37 | } 38 | /** 39 | * Size of the following item 40 | */ 41 | size?: number 42 | /** 43 | * Name of the following item 44 | */ 45 | name?: string 46 | /** 47 | * Display distance of the text component 48 | */ 49 | displayDistance?: number 50 | } 51 | 52 | /** 53 | * Creates an orbital companion that follows a target entity with a specified offset. 54 | Used for creating floating pets, companion items, or visual effects that trail behind players. 55 | */ 56 | export class OrbitalCompanion { 57 | entity: Entity 58 | 59 | constructor(params: OrbitalCompanionParams) { 60 | const { 61 | position, 62 | meshUrl, 63 | targetEntityId, 64 | offset = { x: 0, y: 2, z: 0 }, 65 | displayDistance, 66 | name, 67 | size, 68 | } = params 69 | 70 | this.entity = EntityManager.createEntity(SerializedEntityType.ORBITAL_COMPANION) 71 | 72 | const positionComponent = new PositionComponent( 73 | this.entity.id, 74 | position.x, 75 | position.y, 76 | position.z 77 | ) 78 | this.entity.addComponent(positionComponent) 79 | 80 | const rotationComponent = new RotationComponent(this.entity.id, 0, 0, 0) 81 | this.entity.addComponent(rotationComponent) 82 | 83 | const serverMeshComponent = new ServerMeshComponent(this.entity.id, meshUrl) 84 | this.entity.addComponent(serverMeshComponent) 85 | 86 | // Network data component 87 | const networkDataComponent = new NetworkDataComponent(this.entity.id, this.entity.type, [ 88 | positionComponent, 89 | rotationComponent, 90 | serverMeshComponent, 91 | ]) 92 | 93 | if (size) { 94 | const sizeComponent = new SingleSizeComponent(this.entity.id, size) 95 | this.entity.addComponent(sizeComponent) 96 | networkDataComponent.components.push(sizeComponent) 97 | } 98 | 99 | if (name) { 100 | const textComponent = new TextComponent( 101 | this.entity.id, 102 | name, 103 | offset.x, 104 | offset.y, 105 | offset.z, 106 | displayDistance 107 | ) 108 | this.entity.addComponent(textComponent) 109 | networkDataComponent.components.push(textComponent) 110 | } 111 | 112 | this.entity.addComponent(networkDataComponent) 113 | this.entity.addComponent(new FollowTargetComponent(this.entity.id, targetEntityId, offset)) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /back/src/ecs/system/physics/CapsuleColliderSystem.ts: -------------------------------------------------------------------------------- 1 | import { SingleSizeComponent } from '../../../../../shared/component/SingleSizeComponent.js' 2 | import { ComponentAddedEvent } from '../../../../../shared/component/events/ComponentAddedEvent.js' 3 | import { Entity } from '../../../../../shared/entity/Entity.js' 4 | import { EntityManager } from '../../../../../shared/system/EntityManager.js' 5 | import { EventSystem } from '../../../../../shared/system/EventSystem.js' 6 | import Rapier from '../../../physics/rapier.js' 7 | import { CapsuleColliderComponent } from '../../component/physics/CapsuleColliderComponent.js' 8 | import { ColliderPropertiesComponent } from '../../component/physics/ColliderPropertiesComponent.js' 9 | import { DynamicRigidBodyComponent } from '../../component/physics/DynamicRigidBodyComponent.js' 10 | import { KinematicRigidBodyComponent } from '../../component/physics/KinematicRigidBodyComponent.js' 11 | 12 | export class CapsuleColliderSystem { 13 | async update(entities: Entity[], world: Rapier.World) { 14 | const createEvents = EventSystem.getEventsWrapped(ComponentAddedEvent, CapsuleColliderComponent) 15 | for (const event of createEvents) { 16 | const entity = EntityManager.getEntityById(entities, event.entityId) 17 | 18 | if (!entity) { 19 | console.error('CapsuleColliderSystem: Entity not found') 20 | continue 21 | } 22 | 23 | this.onComponentAdded(entity, event, world) 24 | } 25 | } 26 | 27 | onComponentAdded( 28 | entity: Entity, 29 | event: ComponentAddedEvent, 30 | world: Rapier.World 31 | ) { 32 | // Collider 33 | const { component: capsuleColliderComponent } = event 34 | let sizeComponent = entity.getComponent(SingleSizeComponent) 35 | const rigidBodyComponent = 36 | entity.getComponent(DynamicRigidBodyComponent) || 37 | entity.getComponent(KinematicRigidBodyComponent) 38 | 39 | if (!rigidBodyComponent) { 40 | console.error('CapsuleColliderSystem : No RigidBodyComponent found on entity.') 41 | return 42 | } 43 | 44 | if (!sizeComponent) { 45 | sizeComponent = new SingleSizeComponent(entity.id, 1) 46 | entity.addComponent(sizeComponent) 47 | 48 | console.warn( 49 | 'CapsuleColliderSystem : No SizeComponent found on entity. Using a default size of 1.0.' 50 | ) 51 | } 52 | 53 | const colliderDesc = Rapier.ColliderDesc.capsule(sizeComponent.size / 2, sizeComponent.size) 54 | // Set the friction combine rule to control how friction is combined with other contacts 55 | colliderDesc.setFrictionCombineRule(Rapier.CoefficientCombineRule.Max) 56 | // Set friction to control how slippery the player is when colliding with surfaces 57 | colliderDesc.setFriction(0.0) // Adjust the value as needed 58 | 59 | // Set restitution to control how bouncy the player is when colliding with surfaces 60 | // colliderDesc.setRestitution(0.0); // Adjust the value as needed 61 | 62 | // Set the restitution combine rule to control how restitution is combined with other contacts 63 | colliderDesc.setRestitutionCombineRule(Rapier.CoefficientCombineRule.Max) 64 | 65 | const colliderProperties = entity.getComponent(ColliderPropertiesComponent) 66 | 67 | if (colliderProperties) { 68 | if (colliderProperties.data.isSensor !== undefined) { 69 | colliderDesc.setSensor(colliderProperties.data.isSensor) 70 | } 71 | if (colliderProperties.data.friction !== undefined) { 72 | colliderDesc.setFriction(colliderProperties.data.friction) 73 | } 74 | if (colliderProperties.data.restitution !== undefined) { 75 | colliderDesc.setRestitution(colliderProperties.data.restitution) 76 | } 77 | } 78 | 79 | colliderDesc.setActiveEvents(Rapier.ActiveEvents.COLLISION_EVENTS) 80 | capsuleColliderComponent.collider = world.createCollider(colliderDesc, rigidBodyComponent.body) 81 | } 82 | } 83 | --------------------------------------------------------------------------------