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