initial commit
This commit is contained in:
commit
84309feb0c
25
.eslintrc.js
Normal file
25
.eslintrc.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
};
|
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
73
README.md
Normal file
73
README.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
||||||
|
</p>
|
||||||
|
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
|
||||||
|
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ pnpm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ pnpm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ pnpm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ pnpm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ pnpm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ pnpm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nest is [MIT licensed](LICENSE).
|
8
nest-cli.json
Normal file
8
nest-cli.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
82
package.json
Normal file
82
package.json
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@dimforge/rapier3d-compat": "^0.11.2",
|
||||||
|
"@nestjs/axios": "^3.0.0",
|
||||||
|
"@nestjs/common": "^10.0.0",
|
||||||
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
|
"@nestjs/platform-ws": "^10.0.3",
|
||||||
|
"@nestjs/websockets": "^10.0.3",
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"smart-buffer": "^4.2.0",
|
||||||
|
"three": "^0.153.0",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
|
"ws": "^8.13.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
"@nestjs/config": "^3.0.0",
|
||||||
|
"@nestjs/schematics": "^10.0.0",
|
||||||
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/jest": "^29.5.2",
|
||||||
|
"@types/node": "^20.3.1",
|
||||||
|
"@types/supertest": "^2.0.12",
|
||||||
|
"@types/three": "^0.152.1",
|
||||||
|
"@types/uuid": "^9.0.2",
|
||||||
|
"@types/ws": "^8.5.5",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.59.11",
|
||||||
|
"@typescript-eslint/parser": "^5.59.11",
|
||||||
|
"eslint": "^8.42.0",
|
||||||
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"prettier": "^2.8.8",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^6.3.3",
|
||||||
|
"ts-jest": "^29.1.0",
|
||||||
|
"ts-loader": "^9.4.3",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
5239
pnpm-lock.yaml
Normal file
5239
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
14
src/app.module.ts
Normal file
14
src/app.module.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { EventsModule } from './events/events.module';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { world } from './config/world.config';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
load: [world],
|
||||||
|
}),
|
||||||
|
EventsModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
7
src/config/world.config.ts
Normal file
7
src/config/world.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export const world = registerAs('world', () => ({
|
||||||
|
worldAsset: String(
|
||||||
|
process.env.WORLD_URI || 'https://lunasqu.ee/freeblox/test-level.json',
|
||||||
|
),
|
||||||
|
}));
|
81
src/events/events.gateway.ts
Normal file
81
src/events/events.gateway.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
ConnectedSocket,
|
||||||
|
MessageBody,
|
||||||
|
OnGatewayConnection,
|
||||||
|
OnGatewayDisconnect,
|
||||||
|
OnGatewayInit,
|
||||||
|
SubscribeMessage,
|
||||||
|
WebSocketGateway,
|
||||||
|
WebSocketServer,
|
||||||
|
} from '@nestjs/websockets';
|
||||||
|
import { Packet } from 'src/net/packet';
|
||||||
|
import { PlayerStoreService } from 'src/player/player-store.service';
|
||||||
|
import { PlayerSocket } from 'src/types/data-socket';
|
||||||
|
import { ErrorType } from 'src/types/error-type.enum';
|
||||||
|
import { PacketType } from 'src/types/packet-type.enum';
|
||||||
|
import { getRandomId } from 'src/utils/random';
|
||||||
|
import { WorldService } from 'src/world/world.service';
|
||||||
|
import { Server } from 'ws';
|
||||||
|
|
||||||
|
@WebSocketGateway(8256, {
|
||||||
|
cors: {
|
||||||
|
origin: '*',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export class EventsGateway
|
||||||
|
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly players: PlayerStoreService,
|
||||||
|
private readonly world: WorldService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@WebSocketServer()
|
||||||
|
server: Server;
|
||||||
|
|
||||||
|
@SubscribeMessage('message')
|
||||||
|
async onEvent(
|
||||||
|
@ConnectedSocket() client: PlayerSocket,
|
||||||
|
@MessageBody() data: Packet,
|
||||||
|
) {
|
||||||
|
let player = this.players.getByClient(client);
|
||||||
|
if (!player && data.packet !== PacketType.AUTH) {
|
||||||
|
return client.close(
|
||||||
|
1007,
|
||||||
|
new Packet(PacketType.ERROR)
|
||||||
|
.write(ErrorType.AUTH_FAIL, 'uint8')
|
||||||
|
.toBuffer(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.packet === PacketType.AUTH) {
|
||||||
|
try {
|
||||||
|
player = await this.players.authenticate(client, data);
|
||||||
|
} catch {
|
||||||
|
return client.close(
|
||||||
|
1007,
|
||||||
|
new Packet(PacketType.ERROR)
|
||||||
|
.write(ErrorType.AUTH_FAIL, 'uint8')
|
||||||
|
.toBuffer(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.world.initializePlayer(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.handlePlayerPacket(data)) return false;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterInit(server: Server) {
|
||||||
|
console.log('server init');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConnection(client: PlayerSocket, ...args: any[]) {
|
||||||
|
client.id = getRandomId();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDisconnect(client: PlayerSocket) {
|
||||||
|
this.players.disconnect(client);
|
||||||
|
}
|
||||||
|
}
|
10
src/events/events.module.ts
Normal file
10
src/events/events.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { EventsGateway } from './events.gateway';
|
||||||
|
import { PlayerModule } from 'src/player/player.module';
|
||||||
|
import { WorldModule } from 'src/world/world.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PlayerModule, WorldModule],
|
||||||
|
providers: [EventsGateway],
|
||||||
|
})
|
||||||
|
export class EventsModule {}
|
22
src/game/brick.object.ts
Normal file
22
src/game/brick.object.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { PhysicsObject } from './physics.object';
|
||||||
|
import type Rapier from '@dimforge/rapier3d-compat';
|
||||||
|
import { Vector3 } from 'three';
|
||||||
|
|
||||||
|
export class Brick extends PhysicsObject {
|
||||||
|
public objectType = 'Brick';
|
||||||
|
public name = 'Brick';
|
||||||
|
|
||||||
|
protected override createCollider(
|
||||||
|
factory: typeof Rapier,
|
||||||
|
world: Rapier.World,
|
||||||
|
body?: Rapier.RigidBody,
|
||||||
|
) {
|
||||||
|
const scale = this.getWorldScale(new Vector3()).divideScalar(2);
|
||||||
|
const collider = factory.ColliderDesc.cuboid(
|
||||||
|
Math.abs(scale.x),
|
||||||
|
Math.abs(scale.y),
|
||||||
|
Math.abs(scale.z),
|
||||||
|
);
|
||||||
|
return world.createCollider(collider, body);
|
||||||
|
}
|
||||||
|
}
|
20
src/game/capsule.object.ts
Normal file
20
src/game/capsule.object.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Vector3 } from 'three';
|
||||||
|
import { Brick } from './brick.object';
|
||||||
|
import type Rapier from '@dimforge/rapier3d-compat';
|
||||||
|
|
||||||
|
export class Capsule extends Brick {
|
||||||
|
public objectType = 'Capsule';
|
||||||
|
public name = this.objectType;
|
||||||
|
|
||||||
|
protected override createCollider(
|
||||||
|
factory: typeof Rapier,
|
||||||
|
world: Rapier.World,
|
||||||
|
body?: Rapier.RigidBody,
|
||||||
|
) {
|
||||||
|
const scale = this.getWorldScale(new Vector3());
|
||||||
|
const height = Math.abs(scale.y) / 2;
|
||||||
|
const radius = (Math.abs(scale.x) + Math.abs(scale.z)) / 2;
|
||||||
|
const collider = factory.ColliderDesc.capsule(height, radius);
|
||||||
|
return world.createCollider(collider, body);
|
||||||
|
}
|
||||||
|
}
|
19
src/game/cylinder.object.ts
Normal file
19
src/game/cylinder.object.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Vector3 } from 'three';
|
||||||
|
import { Brick } from './brick.object';
|
||||||
|
import type Rapier from '@dimforge/rapier3d-compat';
|
||||||
|
|
||||||
|
export class Cylinder extends Brick {
|
||||||
|
public objectType = 'Cylinder';
|
||||||
|
public name = this.objectType;
|
||||||
|
protected override createCollider(
|
||||||
|
factory: typeof Rapier,
|
||||||
|
world: Rapier.World,
|
||||||
|
body?: Rapier.RigidBody,
|
||||||
|
) {
|
||||||
|
const scale = this.getWorldScale(new Vector3());
|
||||||
|
const height = Math.abs(scale.y) / 2;
|
||||||
|
const radius = (Math.abs(scale.x) / 2 + Math.abs(scale.z) / 2) / 2;
|
||||||
|
const collider = factory.ColliderDesc.cylinder(height, radius);
|
||||||
|
return world.createCollider(collider, body);
|
||||||
|
}
|
||||||
|
}
|
48
src/game/environment.object.ts
Normal file
48
src/game/environment.object.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
ObjectPropertyExclude,
|
||||||
|
ObjectProperty,
|
||||||
|
} from 'src/types/property.decorator';
|
||||||
|
import { SerializedObject } from 'src/types/serialized';
|
||||||
|
import { Color, Vector3 } from 'three';
|
||||||
|
import { GameObject } from './game-object';
|
||||||
|
|
||||||
|
export class Environment extends GameObject {
|
||||||
|
public objectType = 'Environment';
|
||||||
|
@ObjectPropertyExclude()
|
||||||
|
public name = 'Environment';
|
||||||
|
public virtual = true;
|
||||||
|
|
||||||
|
@ObjectPropertyExclude()
|
||||||
|
public override visible!: boolean;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
sunColor!: Color;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
sunPosition!: Vector3;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
sunStrength!: number;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
ambientColor!: Color;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
ambientStrength!: number;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
clearColor!: Color;
|
||||||
|
|
||||||
|
override serialize() {
|
||||||
|
return super.serialize() as SerializedEnvironment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SerializedEnvironment extends SerializedObject {
|
||||||
|
sunColor: Color;
|
||||||
|
sunPosition: Vector3;
|
||||||
|
sunStrength: number;
|
||||||
|
ambientColor: Color;
|
||||||
|
ambientStrength: number;
|
||||||
|
clearColor: Color;
|
||||||
|
}
|
132
src/game/game-object.ts
Normal file
132
src/game/game-object.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { ObjectProperty } from 'src/types/property.decorator';
|
||||||
|
import { SerializedObject } from 'src/types/serialized';
|
||||||
|
import { readMetadataOf } from 'src/utils/read-metadata';
|
||||||
|
import { Color, Euler, Object3D, Vector3 } from 'three';
|
||||||
|
|
||||||
|
export class GameObject extends Object3D {
|
||||||
|
public objectType = 'GameObject';
|
||||||
|
public virtual = false;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public override uuid!: string;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public override name = '';
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public override visible = true;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public archivable = true;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.name = this.objectType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get excludedProperties() {
|
||||||
|
return readMetadataOf<string>(this, 'excludedProperties');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The exposed properties for this game object, used for the editor
|
||||||
|
*/
|
||||||
|
get properties() {
|
||||||
|
const exclude = this.excludedProperties;
|
||||||
|
const properties = readMetadataOf<string>(this, 'properties');
|
||||||
|
return properties.filter((item) => !exclude.includes(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize GameObject for exporting
|
||||||
|
*/
|
||||||
|
serialize(skipChildren = false) {
|
||||||
|
const object: SerializedObject = {
|
||||||
|
name: this.name,
|
||||||
|
objectType: this.objectType,
|
||||||
|
children: !skipChildren
|
||||||
|
? this.children
|
||||||
|
.filter(
|
||||||
|
(entry) =>
|
||||||
|
entry instanceof GameObject && entry.archivable !== false,
|
||||||
|
)
|
||||||
|
.map((entry) => (entry as GameObject).serialize())
|
||||||
|
: undefined,
|
||||||
|
visible: this.visible,
|
||||||
|
};
|
||||||
|
|
||||||
|
const keys = this.properties.map((property) => property);
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
object,
|
||||||
|
keys.reduce<{ [x: string]: unknown }>((obj, key) => {
|
||||||
|
const indexable = this as Record<string, unknown>;
|
||||||
|
const value = (indexable[key] as any)?.toArray
|
||||||
|
? (indexable[key] as any).toArray()
|
||||||
|
: indexable[key];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...obj,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
override copy(object: Object3D, recursive = true) {
|
||||||
|
super.copy(object as any, recursive);
|
||||||
|
this.properties
|
||||||
|
.map((property) => property)
|
||||||
|
.filter((key) => !['position', 'rotation', 'scale', 'uuid'].includes(key))
|
||||||
|
.forEach((key) => this.setOwnProperty(key, (object as any)[key]));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function can be used to modify any property of the object by key.
|
||||||
|
* @param key Key to set
|
||||||
|
* @param value Value to set
|
||||||
|
*/
|
||||||
|
public setOwnProperty(key: keyof GameObject, value: unknown): void;
|
||||||
|
public setOwnProperty(key: string, value: unknown): void;
|
||||||
|
public setOwnProperty(key: string, value: unknown) {
|
||||||
|
const indexable = this as any;
|
||||||
|
if (indexable[key]?.fromArray && Array.isArray(value)) {
|
||||||
|
indexable[key].fromArray(value);
|
||||||
|
} else if (indexable[key]?.isColor) {
|
||||||
|
indexable[key] = new Color(value as string);
|
||||||
|
} else if (indexable[key]?.copy) {
|
||||||
|
indexable[key].copy(value);
|
||||||
|
} else {
|
||||||
|
indexable[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize a serialized object into properties on this object.
|
||||||
|
* @param input Serialized information
|
||||||
|
*/
|
||||||
|
deserialize(input: SerializedObject) {
|
||||||
|
Object.keys(input)
|
||||||
|
.filter((key) => !['children', 'objectType'].includes(key))
|
||||||
|
.forEach((key) => this.setOwnProperty(key, input[key]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GameObject3D extends GameObject {
|
||||||
|
public objectType = 'GameObject3D';
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public locked = false;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public override position!: Vector3;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public override scale!: Vector3;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public override rotation!: Euler;
|
||||||
|
}
|
43
src/game/geometries.ts
Normal file
43
src/game/geometries.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { BoxGeometry } from 'three';
|
||||||
|
|
||||||
|
export class WedgeGeometry extends BoxGeometry {
|
||||||
|
type = 'WedgeGeometry';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const pos = this.attributes.position;
|
||||||
|
for (let i = 0; i < pos.count; i++) {
|
||||||
|
if (pos.getX(i) < 0 && pos.getY(i) > 0) pos.setY(i, -0.5);
|
||||||
|
}
|
||||||
|
this.computeVertexNormals();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WedgeCornerGeometry extends BoxGeometry {
|
||||||
|
type = 'WedgeCornerGeometry';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const pos = this.attributes.position;
|
||||||
|
for (let i = 0; i < pos.count; i++) {
|
||||||
|
if (pos.getY(i) > 0 && (pos.getX(i) !== 0.5 || pos.getZ(i) !== -0.5)) {
|
||||||
|
pos.setY(i, -0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.computeVertexNormals();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WedgeInnerCornerGeometry extends BoxGeometry {
|
||||||
|
type = 'WedgeInnerCornerGeometry';
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const pos = this.attributes.position;
|
||||||
|
for (let i = 0; i < pos.count; i++) {
|
||||||
|
if (pos.getY(i) > 0 && pos.getX(i) === 0.5 && pos.getZ(i) === 0.5) {
|
||||||
|
pos.setY(i, -0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.computeVertexNormals();
|
||||||
|
}
|
||||||
|
}
|
6
src/game/group.object.ts
Normal file
6
src/game/group.object.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { GameObject3D } from './game-object';
|
||||||
|
|
||||||
|
export class Group extends GameObject3D {
|
||||||
|
public objectType = 'Group';
|
||||||
|
public name = 'Group';
|
||||||
|
}
|
198
src/game/humanoid.object.ts
Normal file
198
src/game/humanoid.object.ts
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
import { Quaternion, Vector3 } from 'three';
|
||||||
|
import type Rapier from '@dimforge/rapier3d-compat';
|
||||||
|
import { PhysicsTicking } from 'src/physics';
|
||||||
|
import { GameObject } from './game-object';
|
||||||
|
import { ObjectProperty } from 'src/types/property.decorator';
|
||||||
|
import { clamp } from 'src/utils/clamp';
|
||||||
|
|
||||||
|
export class Humanoid extends GameObject implements PhysicsTicking {
|
||||||
|
public isTickingObject = true;
|
||||||
|
public objectType = 'Humanoid';
|
||||||
|
public name = 'Humanoid';
|
||||||
|
protected ready = false;
|
||||||
|
|
||||||
|
public characterHeight = 5.5;
|
||||||
|
public characterHalfHeight = this.characterHeight / 2;
|
||||||
|
|
||||||
|
protected shouldJump = false;
|
||||||
|
private _health = 100;
|
||||||
|
private _maxHealth = 100;
|
||||||
|
private _velocity = new Vector3(0, 0, 0);
|
||||||
|
private _appliedGravity = new Vector3(0, 0, 0);
|
||||||
|
private _grounded = true;
|
||||||
|
private _lookAt = new Vector3(0, 0, 1);
|
||||||
|
private _currentLookAt = new Vector3(0, 0, 1);
|
||||||
|
private _animState = 0;
|
||||||
|
|
||||||
|
protected collider?: Rapier.Collider;
|
||||||
|
protected rigidBody?: Rapier.RigidBody;
|
||||||
|
protected physicsWorldRef?: Rapier.World;
|
||||||
|
protected characterControllerRef?: Rapier.KinematicCharacterController;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public mass = -8;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public jumpPower = 8;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
get health() {
|
||||||
|
return this._health;
|
||||||
|
}
|
||||||
|
set health(value: number) {
|
||||||
|
const health = clamp(Math.floor(value), 0, this.maxHealth);
|
||||||
|
if (health === 0) this.die();
|
||||||
|
this.health = health;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
get maxHealth() {
|
||||||
|
return this._maxHealth;
|
||||||
|
}
|
||||||
|
set maxHealth(value: number) {
|
||||||
|
const maxHealth = Math.floor(value);
|
||||||
|
if (this.health > maxHealth) {
|
||||||
|
this.health = maxHealth;
|
||||||
|
}
|
||||||
|
this._maxHealth = maxHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
get alive() {
|
||||||
|
return this.health > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get grounded() {
|
||||||
|
return this._grounded;
|
||||||
|
}
|
||||||
|
set grounded(value: boolean) {
|
||||||
|
this._grounded = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get weight() {
|
||||||
|
return (this.physicsWorldRef?.gravity.y || -9.81) + this.mass;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(
|
||||||
|
physicsEngine: typeof Rapier,
|
||||||
|
physicsWorld: Rapier.World,
|
||||||
|
characterController: Rapier.KinematicCharacterController,
|
||||||
|
): void {
|
||||||
|
if (!this.parent)
|
||||||
|
throw new Error('Cannot initialize Humanoid to empty parent');
|
||||||
|
|
||||||
|
this.ready = true;
|
||||||
|
|
||||||
|
this.physicsWorldRef = physicsWorld;
|
||||||
|
this.characterControllerRef = characterController;
|
||||||
|
|
||||||
|
// Character Physics
|
||||||
|
const halfVec = new Vector3(0, this.characterHalfHeight, 0);
|
||||||
|
const colliderDesc = physicsEngine.ColliderDesc.cuboid(
|
||||||
|
1,
|
||||||
|
this.characterHalfHeight,
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
const rigidBodyDesc = physicsEngine.RigidBodyDesc.kinematicPositionBased()
|
||||||
|
.setTranslation(...this.parent!.position.toArray())
|
||||||
|
.setRotation(this.parent!.quaternion);
|
||||||
|
const rigidBody = physicsWorld.createRigidBody(rigidBodyDesc);
|
||||||
|
const collider = physicsWorld.createCollider(colliderDesc, rigidBody);
|
||||||
|
collider.setTranslationWrtParent(halfVec);
|
||||||
|
|
||||||
|
this.rigidBody = rigidBody;
|
||||||
|
this.collider = collider;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVelocity(velocity: Vector3) {
|
||||||
|
this._velocity.copy(velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLook(vector: Vector3) {
|
||||||
|
this._lookAt.lerp(vector, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
jump() {
|
||||||
|
if (!this.grounded || this.shouldJump) return;
|
||||||
|
this.shouldJump = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
tick(dt: number): void {
|
||||||
|
if (!this.ready) return;
|
||||||
|
|
||||||
|
// Apply rigidbody transforms to object from last process tick
|
||||||
|
if (this.rigidBody) {
|
||||||
|
this.parent!.position.copy(this.rigidBody.translation() as any);
|
||||||
|
this.parent!.quaternion.copy(this.rigidBody.rotation() as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply jumping
|
||||||
|
if (this.shouldJump) {
|
||||||
|
this._appliedGravity.y = Math.sqrt(-2 * this.weight * this.jumpPower);
|
||||||
|
this.grounded = false;
|
||||||
|
this.shouldJump = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply gravity
|
||||||
|
this._appliedGravity.y += this.weight * dt;
|
||||||
|
|
||||||
|
// Apply velocity
|
||||||
|
this.applyVelocity(
|
||||||
|
this._velocity.clone().add(this._appliedGravity).multiplyScalar(dt),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply look vector
|
||||||
|
this._currentLookAt.copy(this.parent!.position);
|
||||||
|
this._currentLookAt.add(this._lookAt);
|
||||||
|
this.parent?.lookAt(this._currentLookAt);
|
||||||
|
|
||||||
|
this.applyRotation(this.parent!.quaternion);
|
||||||
|
|
||||||
|
// Stick to ground
|
||||||
|
if (this.grounded) {
|
||||||
|
this._appliedGravity.y = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
if (!this.alive) return;
|
||||||
|
this.health = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyVelocity(velocity: Vector3) {
|
||||||
|
if (!this.characterControllerRef || !this.parent || !this.rigidBody) return;
|
||||||
|
|
||||||
|
const vec3 = this.parent.position.clone();
|
||||||
|
this.characterControllerRef.computeColliderMovement(
|
||||||
|
this.collider!,
|
||||||
|
velocity,
|
||||||
|
);
|
||||||
|
const computed = this.characterControllerRef.computedMovement();
|
||||||
|
const grounded = this.characterControllerRef.computedGrounded();
|
||||||
|
vec3.copy(computed as Vector3);
|
||||||
|
this.rigidBody?.setNextKinematicTranslation(vec3.add(this.parent.position));
|
||||||
|
|
||||||
|
// After the collider movement calculation is done, we can read the
|
||||||
|
// collision events.
|
||||||
|
// for (let i = 0; i < this.controller.numComputedCollisions(); i++) {
|
||||||
|
// let collision = this.controller.computedCollision(i);
|
||||||
|
// // Do something with that collision information.
|
||||||
|
// console.log(collision);
|
||||||
|
// }
|
||||||
|
|
||||||
|
this._grounded = grounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyRotation(quat: Quaternion) {
|
||||||
|
this.rigidBody?.setRotation(quat, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
if (this.collider && !this.rigidBody) {
|
||||||
|
this.physicsWorldRef?.removeCollider(this.collider, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.rigidBody) {
|
||||||
|
this.physicsWorldRef?.removeRigidBody(this.rigidBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
src/game/index.ts
Normal file
43
src/game/index.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Cylinder } from './cylinder.object';
|
||||||
|
import { Brick } from './brick.object';
|
||||||
|
import { Sphere } from './sphere.object';
|
||||||
|
import { Wedge } from './wedge.object';
|
||||||
|
import { WedgeCorner } from './wedge-corner.object';
|
||||||
|
import { WedgeInnerCorner } from './wedge-inner-corner.object';
|
||||||
|
import { Instancable } from '../types/instancable';
|
||||||
|
import { Group } from './group.object';
|
||||||
|
import { Torus } from './torus.object';
|
||||||
|
import { Capsule } from './capsule.object';
|
||||||
|
import { MeshPart } from './mesh.object';
|
||||||
|
import { Humanoid } from './humanoid.object';
|
||||||
|
import { GameObject } from './game-object';
|
||||||
|
|
||||||
|
export const instancableGameObjects: Record<string, Instancable<GameObject>> = {
|
||||||
|
['Group']: Group,
|
||||||
|
['Brick']: Brick,
|
||||||
|
['Cylinder']: Cylinder,
|
||||||
|
['Sphere']: Sphere,
|
||||||
|
['Torus']: Torus,
|
||||||
|
['Capsule']: Capsule,
|
||||||
|
['Wedge']: Wedge,
|
||||||
|
['WedgeCorner']: WedgeCorner,
|
||||||
|
['WedgeInnerCorner']: WedgeInnerCorner,
|
||||||
|
};
|
||||||
|
|
||||||
|
export * from './environment.object';
|
||||||
|
export * from './world.object';
|
||||||
|
export * from './physical.object';
|
||||||
|
export * from './physics.object';
|
||||||
|
export {
|
||||||
|
Group,
|
||||||
|
Cylinder,
|
||||||
|
Brick,
|
||||||
|
Sphere,
|
||||||
|
Torus,
|
||||||
|
Capsule,
|
||||||
|
Wedge,
|
||||||
|
WedgeCorner,
|
||||||
|
WedgeInnerCorner,
|
||||||
|
MeshPart,
|
||||||
|
Humanoid,
|
||||||
|
};
|
21
src/game/mesh.object.ts
Normal file
21
src/game/mesh.object.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Vector3 } from 'three';
|
||||||
|
import { Brick } from '.';
|
||||||
|
import type Rapier from '@dimforge/rapier3d-compat';
|
||||||
|
|
||||||
|
export class MeshPart extends Brick {
|
||||||
|
public objectType = 'MeshPart';
|
||||||
|
|
||||||
|
protected override createCollider(
|
||||||
|
factory: typeof Rapier,
|
||||||
|
world: Rapier.World,
|
||||||
|
body?: Rapier.RigidBody,
|
||||||
|
) {
|
||||||
|
const scale = this.getWorldScale(new Vector3()).divideScalar(2);
|
||||||
|
const collider = factory.ColliderDesc.cuboid(
|
||||||
|
Math.abs(scale.x),
|
||||||
|
Math.abs(scale.y),
|
||||||
|
Math.abs(scale.z),
|
||||||
|
);
|
||||||
|
return world.createCollider(collider, body);
|
||||||
|
}
|
||||||
|
}
|
25
src/game/physical.object.ts
Normal file
25
src/game/physical.object.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { ObjectProperty } from 'src/types/property.decorator';
|
||||||
|
import { Color } from 'three';
|
||||||
|
import { GameObject3D } from './game-object';
|
||||||
|
|
||||||
|
export class PhysicalObject extends GameObject3D {
|
||||||
|
private texturePath?: string;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public color = new Color(0xffffff);
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public transparency = 0;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
get texture() {
|
||||||
|
return this.texturePath;
|
||||||
|
}
|
||||||
|
set texture(path: string | undefined) {
|
||||||
|
this.texturePath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
105
src/game/physics.object.ts
Normal file
105
src/game/physics.object.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { PhysicsTicking } from '../physics/ticking';
|
||||||
|
import type Rapier from '@dimforge/rapier3d-compat';
|
||||||
|
import { PhysicalObject } from './physical.object';
|
||||||
|
import { Quaternion, Vector3 } from 'three';
|
||||||
|
import { ObjectProperty } from 'src/types/property.decorator';
|
||||||
|
import { GameObject } from './game-object';
|
||||||
|
|
||||||
|
export class PhysicsObject extends PhysicalObject implements PhysicsTicking {
|
||||||
|
public objectType = 'PhysicsObject';
|
||||||
|
isTickingObject = true;
|
||||||
|
|
||||||
|
protected collider?: Rapier.Collider;
|
||||||
|
public rigidBody?: Rapier.RigidBody;
|
||||||
|
protected physicsWorldRef?: Rapier.World;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public canCollide = true;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public anchored = true;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public mass = 1;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public friction = 0.2;
|
||||||
|
|
||||||
|
private _tempVec = new Vector3();
|
||||||
|
private _tempQuat = new Quaternion();
|
||||||
|
private _tempQuat2 = new Quaternion();
|
||||||
|
|
||||||
|
initialize(physicsEngine: typeof Rapier, physicsWorld: Rapier.World): void {
|
||||||
|
if (this.virtual) return;
|
||||||
|
this.physicsWorldRef = physicsWorld;
|
||||||
|
|
||||||
|
// This object has effecively no physics:
|
||||||
|
// doesn't move, doesn't collide
|
||||||
|
if (this.anchored && !this.canCollide) return;
|
||||||
|
|
||||||
|
let bodyDesc: Rapier.RigidBodyDesc;
|
||||||
|
if (this.anchored) bodyDesc = physicsEngine.RigidBodyDesc.fixed();
|
||||||
|
else bodyDesc = physicsEngine.RigidBodyDesc.dynamic();
|
||||||
|
|
||||||
|
const position = this.position.clone();
|
||||||
|
const rotation = this.quaternion.clone();
|
||||||
|
this.getWorldPosition(position);
|
||||||
|
this.getWorldQuaternion(rotation);
|
||||||
|
|
||||||
|
bodyDesc
|
||||||
|
.setTranslation(...position.toArray())
|
||||||
|
.setRotation(rotation)
|
||||||
|
.setAdditionalMass(this.mass)
|
||||||
|
.setLinearDamping(this.friction)
|
||||||
|
.setAngularDamping(this.friction);
|
||||||
|
|
||||||
|
const body = physicsWorld.createRigidBody(bodyDesc);
|
||||||
|
|
||||||
|
let collider: Rapier.Collider | undefined;
|
||||||
|
if (this.canCollide) {
|
||||||
|
collider = this.createCollider(physicsEngine, physicsWorld, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.collider = collider;
|
||||||
|
this.rigidBody = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
tick(dt: number): void {
|
||||||
|
if (!this.rigidBody || this.virtual) return;
|
||||||
|
this._tempVec.copy(this.rigidBody.translation() as any);
|
||||||
|
this._tempQuat.copy(this.rigidBody.rotation() as any);
|
||||||
|
if ((this.parent as GameObject)?.objectType !== 'World') {
|
||||||
|
this.parent?.worldToLocal(this._tempVec);
|
||||||
|
this.parent?.getWorldQuaternion(this._tempQuat2);
|
||||||
|
this._tempQuat2.invert();
|
||||||
|
this._tempQuat.premultiply(this._tempQuat2);
|
||||||
|
}
|
||||||
|
this.position.copy(this._tempVec);
|
||||||
|
this.quaternion.copy(this._tempQuat);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
if (this.collider && !this.rigidBody) {
|
||||||
|
this.physicsWorldRef?.removeCollider(this.collider, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.rigidBody) {
|
||||||
|
this.physicsWorldRef?.removeRigidBody(this.rigidBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create collision shape for physics object.
|
||||||
|
* @internal
|
||||||
|
* @param factory Physics engine
|
||||||
|
* @param world Physics world
|
||||||
|
* @param body RigidBody
|
||||||
|
*/
|
||||||
|
protected createCollider(
|
||||||
|
factory: typeof Rapier,
|
||||||
|
world: Rapier.World,
|
||||||
|
body?: Rapier.RigidBody,
|
||||||
|
): Rapier.Collider | undefined {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
56
src/game/player.ts
Normal file
56
src/game/player.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { Packet } from 'src/net/packet';
|
||||||
|
import { PlayerSocket } from 'src/types/data-socket';
|
||||||
|
import { Humanoid } from './humanoid.object';
|
||||||
|
import { Group } from './group.object';
|
||||||
|
import { Vector3 } from 'three';
|
||||||
|
import { PacketType } from 'src/types/packet-type.enum';
|
||||||
|
|
||||||
|
export class Player {
|
||||||
|
public character: Group;
|
||||||
|
public controller: Humanoid;
|
||||||
|
public initialized = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public socket: PlayerSocket,
|
||||||
|
public id: string,
|
||||||
|
public name: string,
|
||||||
|
private token: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public send(packet: Packet): void;
|
||||||
|
public send(packet: Buffer): void;
|
||||||
|
public send(packet: unknown): void {
|
||||||
|
if (packet instanceof Packet) {
|
||||||
|
return this.socket.send(packet.toBuffer());
|
||||||
|
}
|
||||||
|
this.socket.send(packet as Buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public createPlayerCharacter(pos = new Vector3(0, 1, 0)) {
|
||||||
|
const group = new Group();
|
||||||
|
group.name = this.name;
|
||||||
|
group.position.copy(pos);
|
||||||
|
this.character = group;
|
||||||
|
|
||||||
|
const humanoid = new Humanoid();
|
||||||
|
humanoid.uuid = this.id;
|
||||||
|
group.add(humanoid);
|
||||||
|
this.controller = humanoid;
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
public handlePlayerPacket(packet: Packet) {
|
||||||
|
if (packet.packet === PacketType.PLAYER_MOVEMENT) {
|
||||||
|
const velocity = packet.read<Vector3>('vec3');
|
||||||
|
const lookAt = packet.read<Vector3>('vec3');
|
||||||
|
if (this.controller) {
|
||||||
|
this.controller.setVelocity(velocity);
|
||||||
|
this.controller.setLook(lookAt);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
20
src/game/sphere.object.ts
Normal file
20
src/game/sphere.object.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Vector3 } from 'three';
|
||||||
|
import { Brick } from './brick.object';
|
||||||
|
import type Rapier from '@dimforge/rapier3d-compat';
|
||||||
|
|
||||||
|
export class Sphere extends Brick {
|
||||||
|
public objectType = 'Sphere';
|
||||||
|
public name = this.objectType;
|
||||||
|
|
||||||
|
protected override createCollider(
|
||||||
|
factory: typeof Rapier,
|
||||||
|
world: Rapier.World,
|
||||||
|
body?: Rapier.RigidBody,
|
||||||
|
) {
|
||||||
|
const scale = this.getWorldScale(new Vector3());
|
||||||
|
const radius =
|
||||||
|
Math.max(Math.abs(scale.x), Math.abs(scale.y), Math.abs(scale.z)) / 2;
|
||||||
|
const collider = factory.ColliderDesc.ball(radius);
|
||||||
|
return world.createCollider(collider, body);
|
||||||
|
}
|
||||||
|
}
|
25
src/game/torus.object.ts
Normal file
25
src/game/torus.object.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Quaternion, Vector3 } from 'three';
|
||||||
|
import { Brick } from './brick.object';
|
||||||
|
import type Rapier from '@dimforge/rapier3d-compat';
|
||||||
|
|
||||||
|
export class Torus extends Brick {
|
||||||
|
public objectType = 'Torus';
|
||||||
|
public name = this.objectType;
|
||||||
|
|
||||||
|
protected override createCollider(
|
||||||
|
factory: typeof Rapier,
|
||||||
|
world: Rapier.World,
|
||||||
|
body?: Rapier.RigidBody,
|
||||||
|
) {
|
||||||
|
const scale = this.getWorldScale(new Vector3());
|
||||||
|
const height = Math.abs(scale.y) * 0.4;
|
||||||
|
const radius = (Math.abs(scale.x) + Math.abs(scale.z)) / 2;
|
||||||
|
const collider = factory.ColliderDesc.cylinder(
|
||||||
|
height,
|
||||||
|
radius * 1.4,
|
||||||
|
).setRotation(
|
||||||
|
new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), Math.PI / 2),
|
||||||
|
);
|
||||||
|
return world.createCollider(collider, body);
|
||||||
|
}
|
||||||
|
}
|
27
src/game/wedge-corner.object.ts
Normal file
27
src/game/wedge-corner.object.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Matrix4, Vector3 } from 'three';
|
||||||
|
import { Brick } from './brick.object';
|
||||||
|
import type Rapier from '@dimforge/rapier3d-compat';
|
||||||
|
import { WedgeCornerGeometry } from './geometries';
|
||||||
|
|
||||||
|
export class WedgeCorner extends Brick {
|
||||||
|
public objectType = 'WedgeCorner';
|
||||||
|
public name = this.objectType;
|
||||||
|
|
||||||
|
public static geometry = new WedgeCornerGeometry();
|
||||||
|
|
||||||
|
protected override createCollider(
|
||||||
|
factory: typeof Rapier,
|
||||||
|
world: Rapier.World,
|
||||||
|
body?: Rapier.RigidBody,
|
||||||
|
) {
|
||||||
|
const scale = this.getWorldScale(new Vector3());
|
||||||
|
const mat = new Matrix4();
|
||||||
|
mat.makeScale(...scale.toArray());
|
||||||
|
const points = WedgeCorner.geometry
|
||||||
|
.getAttribute('position')
|
||||||
|
.clone()
|
||||||
|
.applyMatrix4(mat)?.array as Float32Array;
|
||||||
|
const collider = factory.ColliderDesc.convexMesh(points)!;
|
||||||
|
return world.createCollider(collider, body);
|
||||||
|
}
|
||||||
|
}
|
27
src/game/wedge-inner-corner.object.ts
Normal file
27
src/game/wedge-inner-corner.object.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Matrix4, Vector3 } from 'three';
|
||||||
|
import { Brick } from './brick.object';
|
||||||
|
import type Rapier from '@dimforge/rapier3d-compat';
|
||||||
|
import { WedgeInnerCornerGeometry } from './geometries';
|
||||||
|
|
||||||
|
export class WedgeInnerCorner extends Brick {
|
||||||
|
public objectType = 'WedgeInnerCorner';
|
||||||
|
public name = this.objectType;
|
||||||
|
|
||||||
|
public static geometry = new WedgeInnerCornerGeometry();
|
||||||
|
|
||||||
|
protected override createCollider(
|
||||||
|
factory: typeof Rapier,
|
||||||
|
world: Rapier.World,
|
||||||
|
body?: Rapier.RigidBody,
|
||||||
|
) {
|
||||||
|
const scale = this.getWorldScale(new Vector3());
|
||||||
|
const mat = new Matrix4();
|
||||||
|
mat.makeScale(...scale.toArray());
|
||||||
|
const points = WedgeInnerCorner.geometry
|
||||||
|
.getAttribute('position')
|
||||||
|
.clone()
|
||||||
|
.applyMatrix4(mat)?.array as Float32Array;
|
||||||
|
const collider = factory.ColliderDesc.convexMesh(points)!;
|
||||||
|
return world.createCollider(collider, body);
|
||||||
|
}
|
||||||
|
}
|
27
src/game/wedge.object.ts
Normal file
27
src/game/wedge.object.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Matrix4, Vector3 } from 'three';
|
||||||
|
import { Brick } from './brick.object';
|
||||||
|
import type Rapier from '@dimforge/rapier3d-compat';
|
||||||
|
import { WedgeGeometry } from './geometries';
|
||||||
|
|
||||||
|
export class Wedge extends Brick {
|
||||||
|
public objectType = 'Wedge';
|
||||||
|
public name = this.objectType;
|
||||||
|
|
||||||
|
public static geometry = new WedgeGeometry();
|
||||||
|
|
||||||
|
protected override createCollider(
|
||||||
|
factory: typeof Rapier,
|
||||||
|
world: Rapier.World,
|
||||||
|
body?: Rapier.RigidBody,
|
||||||
|
) {
|
||||||
|
const scale = this.getWorldScale(new Vector3());
|
||||||
|
const mat = new Matrix4();
|
||||||
|
mat.makeScale(...scale.toArray());
|
||||||
|
const points = Wedge.geometry
|
||||||
|
.getAttribute('position')
|
||||||
|
.clone()
|
||||||
|
.applyMatrix4(mat)?.array as Float32Array;
|
||||||
|
const collider = factory.ColliderDesc.convexMesh(points)!;
|
||||||
|
return world.createCollider(collider, body);
|
||||||
|
}
|
||||||
|
}
|
16
src/game/world.object.ts
Normal file
16
src/game/world.object.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ObjectProperty } from 'src/types/property.decorator';
|
||||||
|
import { GameObject } from './game-object';
|
||||||
|
|
||||||
|
export class World extends GameObject {
|
||||||
|
public objectType = 'World';
|
||||||
|
public name = 'World';
|
||||||
|
public virtual = true;
|
||||||
|
|
||||||
|
@ObjectProperty()
|
||||||
|
public gravity = -9.81;
|
||||||
|
|
||||||
|
override get properties() {
|
||||||
|
const properties = super.properties;
|
||||||
|
return properties.filter((prop) => ['gravity'].includes(prop));
|
||||||
|
}
|
||||||
|
}
|
12
src/main.ts
Normal file
12
src/main.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import { WsAdapter } from './net/ws-adapter';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
app.useWebSocketAdapter(new WsAdapter(app));
|
||||||
|
|
||||||
|
await app.listen(3000);
|
||||||
|
console.log(`Application is running on: ${await app.getUrl()}`);
|
||||||
|
}
|
||||||
|
bootstrap();
|
97
src/net/packet.ts
Normal file
97
src/net/packet.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { PacketType } from '../types/packet-type.enum';
|
||||||
|
import { SmartBuffer } from 'smart-buffer';
|
||||||
|
import { Instancable } from '../types/instancable';
|
||||||
|
import { Vector3, Quaternion } from 'three';
|
||||||
|
|
||||||
|
export class Packet {
|
||||||
|
private buffer = new SmartBuffer();
|
||||||
|
constructor(public packet?: PacketType) {}
|
||||||
|
|
||||||
|
write(data: any, type: Instancable<any> | string) {
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
case String:
|
||||||
|
this.buffer.writeStringNT(data);
|
||||||
|
break;
|
||||||
|
case 'bool':
|
||||||
|
case Boolean:
|
||||||
|
this.buffer.writeUInt8(data ? 1 : 0);
|
||||||
|
break;
|
||||||
|
case 'float':
|
||||||
|
case Number:
|
||||||
|
this.buffer.writeFloatLE(data);
|
||||||
|
break;
|
||||||
|
case 'uint8':
|
||||||
|
this.buffer.writeUInt8(data);
|
||||||
|
break;
|
||||||
|
case 'int32':
|
||||||
|
this.buffer.writeInt32LE(data);
|
||||||
|
break;
|
||||||
|
case 'uint32':
|
||||||
|
this.buffer.writeUInt32LE(data);
|
||||||
|
break;
|
||||||
|
case 'vec3':
|
||||||
|
this.buffer.writeFloatLE(data.x);
|
||||||
|
this.buffer.writeFloatLE(data.y);
|
||||||
|
this.buffer.writeFloatLE(data.z);
|
||||||
|
break;
|
||||||
|
case 'quat':
|
||||||
|
this.buffer.writeFloatLE(data.x);
|
||||||
|
this.buffer.writeFloatLE(data.y);
|
||||||
|
this.buffer.writeFloatLE(data.z);
|
||||||
|
this.buffer.writeFloatLE(data.w);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
read<T = string>(type: Instancable<any> | string) {
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
case String:
|
||||||
|
return this.buffer.readStringNT() as T;
|
||||||
|
case 'bool':
|
||||||
|
case Boolean:
|
||||||
|
return this.buffer.readUInt8() as T;
|
||||||
|
case 'float':
|
||||||
|
case Number:
|
||||||
|
return this.buffer.readFloatLE() as T;
|
||||||
|
case 'uint8':
|
||||||
|
return this.buffer.readUInt8() as T;
|
||||||
|
case 'int32':
|
||||||
|
return this.buffer.readInt32LE() as T;
|
||||||
|
case 'uint32':
|
||||||
|
return this.buffer.readUInt32LE() as T;
|
||||||
|
case 'vec3':
|
||||||
|
return new Vector3(
|
||||||
|
this.buffer.readFloatLE(),
|
||||||
|
this.buffer.readFloatLE(),
|
||||||
|
this.buffer.readFloatLE(),
|
||||||
|
) as T;
|
||||||
|
case 'quat':
|
||||||
|
return new Quaternion(
|
||||||
|
this.buffer.readFloatLE(),
|
||||||
|
this.buffer.readFloatLE(),
|
||||||
|
this.buffer.readFloatLE(),
|
||||||
|
this.buffer.readFloatLE(),
|
||||||
|
) as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeHeader() {
|
||||||
|
if (this.packet === undefined) return;
|
||||||
|
this.buffer.insertUInt8(this.packet, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBuffer() {
|
||||||
|
this.writeHeader();
|
||||||
|
return this.buffer.toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
static from(buffer: Buffer): Packet {
|
||||||
|
const packet = new Packet();
|
||||||
|
packet.buffer = SmartBuffer.fromBuffer(buffer);
|
||||||
|
packet.packet = packet.buffer.readUInt8();
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
}
|
0
src/net/packets/world.ts
Normal file
0
src/net/packets/world.ts
Normal file
52
src/net/ws-adapter.ts
Normal file
52
src/net/ws-adapter.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import * as WebSocket from 'ws';
|
||||||
|
import { WebSocketAdapter, INestApplicationContext } from '@nestjs/common';
|
||||||
|
import { MessageMappingProperties } from '@nestjs/websockets';
|
||||||
|
import { Observable, fromEvent, EMPTY } from 'rxjs';
|
||||||
|
import { mergeMap, filter } from 'rxjs/operators';
|
||||||
|
import { Packet } from './packet';
|
||||||
|
|
||||||
|
export class WsAdapter implements WebSocketAdapter {
|
||||||
|
constructor(private app: INestApplicationContext) {}
|
||||||
|
|
||||||
|
create(port: number, options: any = {}): any {
|
||||||
|
return new WebSocket.Server({ port, ...options });
|
||||||
|
}
|
||||||
|
|
||||||
|
bindClientConnect(
|
||||||
|
server: WebSocket.Server,
|
||||||
|
callback: (...args: any[]) => void,
|
||||||
|
) {
|
||||||
|
server.on('connection', callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
bindMessageHandlers(
|
||||||
|
client: WebSocket,
|
||||||
|
handlers: MessageMappingProperties[],
|
||||||
|
process: (data: any) => Observable<any>,
|
||||||
|
) {
|
||||||
|
fromEvent(client, 'message')
|
||||||
|
.pipe(
|
||||||
|
mergeMap((data: WebSocket.MessageEvent) =>
|
||||||
|
this.bindMessageHandler(data, handlers, process),
|
||||||
|
),
|
||||||
|
filter((result) => result),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindMessageHandler(
|
||||||
|
buffer: WebSocket.MessageEvent,
|
||||||
|
handlers: MessageMappingProperties[],
|
||||||
|
process: (data: any) => Observable<any>,
|
||||||
|
): Observable<any> {
|
||||||
|
const messageHandler = handlers[0];
|
||||||
|
if (!messageHandler) {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
return process(messageHandler.callback(Packet.from(buffer.data as Buffer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
close(server: WebSocket.Server) {
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
}
|
2
src/physics/index.ts
Normal file
2
src/physics/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './rapier';
|
||||||
|
export * from './ticking';
|
5
src/physics/rapier.ts
Normal file
5
src/physics/rapier.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type Rapier = typeof import('@dimforge/rapier3d-compat');
|
||||||
|
|
||||||
|
export function getRapier() {
|
||||||
|
return import('@dimforge/rapier3d-compat');
|
||||||
|
}
|
30
src/physics/ticking.ts
Normal file
30
src/physics/ticking.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import type Rapier from '@dimforge/rapier3d-compat';
|
||||||
|
import { Quaternion, Vector3 } from 'three';
|
||||||
|
|
||||||
|
export interface PhysicsTicking {
|
||||||
|
position: Vector3;
|
||||||
|
quaternion: Quaternion;
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
isTickingObject: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
uuid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
initialize(
|
||||||
|
physicsEngine: typeof Rapier,
|
||||||
|
physicsWorld: Rapier.World,
|
||||||
|
controller?: Rapier.KinematicCharacterController,
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
tick(dt: number): void;
|
||||||
|
}
|
4
src/player/player-auth.service.ts
Normal file
4
src/player/player-auth.service.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PlayerAuthService {}
|
79
src/player/player-store.service.ts
Normal file
79
src/player/player-store.service.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Player } from 'src/game/player';
|
||||||
|
import { PlayerSocket } from 'src/types/data-socket';
|
||||||
|
import { PlayerAuthService } from './player-auth.service';
|
||||||
|
import { Packet } from 'src/net/packet';
|
||||||
|
import { getRandomName } from 'src/utils/random';
|
||||||
|
import { PacketType } from 'src/types/packet-type.enum';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PlayerStoreService {
|
||||||
|
constructor(private readonly auth: PlayerAuthService) {}
|
||||||
|
|
||||||
|
private players = new Map<string, Player>();
|
||||||
|
|
||||||
|
getByClient(client: PlayerSocket) {
|
||||||
|
return this.players.get(client.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(client: PlayerSocket) {
|
||||||
|
return this.players.delete(client.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayerList() {
|
||||||
|
const all = Array.from(this.players.values());
|
||||||
|
return all
|
||||||
|
.filter((object) => object.initialized)
|
||||||
|
.map((player) => `${player.id}:${player.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUninitializedIds() {
|
||||||
|
const all = Array.from(this.players.values());
|
||||||
|
return all
|
||||||
|
.filter((object) => !object.initialized)
|
||||||
|
.map((player) => player.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async authenticate(client: PlayerSocket, packet: Packet) {
|
||||||
|
const playerId = packet.read(String);
|
||||||
|
const playerToken = packet.read(String);
|
||||||
|
const clientVersion = packet.read<number>('uint8');
|
||||||
|
if (clientVersion !== 0) throw new Error('Bad client');
|
||||||
|
// TODO: actually something
|
||||||
|
const player = new Player(client, playerId, getRandomName(), playerToken);
|
||||||
|
this.players.set(client.id, player);
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast(data: Buffer) {
|
||||||
|
this.players.forEach((value) => value.send(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastExcept(data: Buffer, players: string[]) {
|
||||||
|
Array.from(this.players.values())
|
||||||
|
.filter((player) => !players.includes(player.id))
|
||||||
|
.forEach((player) => player.send(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayerListPacket() {
|
||||||
|
const packet = new Packet(PacketType.PLAYER_LIST);
|
||||||
|
const players = this.getPlayerList();
|
||||||
|
packet.write(players.length, 'uint32');
|
||||||
|
players.forEach((player) => packet.write(player, String));
|
||||||
|
return packet.toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayerCharacterPackets(exclude: Player) {
|
||||||
|
const players = Array.from(this.players.values());
|
||||||
|
return players
|
||||||
|
.filter((player) => !!player.character && player.id !== exclude.id)
|
||||||
|
.map((player) => {
|
||||||
|
return new Packet(PacketType.PLAYER_CHARACTER)
|
||||||
|
.write(player.id, String)
|
||||||
|
.write(player.name, String)
|
||||||
|
.write(player.character.position, 'vec3')
|
||||||
|
.write(player.character.quaternion, 'quat')
|
||||||
|
.toBuffer();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
9
src/player/player.module.ts
Normal file
9
src/player/player.module.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PlayerAuthService } from './player-auth.service';
|
||||||
|
import { PlayerStoreService } from './player-store.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [PlayerAuthService, PlayerStoreService],
|
||||||
|
exports: [PlayerAuthService, PlayerStoreService],
|
||||||
|
})
|
||||||
|
export class PlayerModule {}
|
9
src/types/asset.ts
Normal file
9
src/types/asset.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export type AssetType = 'Texture' | 'CubeTexture' | 'Mesh';
|
||||||
|
|
||||||
|
export interface Asset {
|
||||||
|
name: string;
|
||||||
|
path?: string;
|
||||||
|
type: AssetType;
|
||||||
|
data: any;
|
||||||
|
remote?: boolean;
|
||||||
|
}
|
4
src/types/chat-type.enum.ts
Normal file
4
src/types/chat-type.enum.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum ChatType {
|
||||||
|
MESSAGE = 0,
|
||||||
|
COMMAND,
|
||||||
|
}
|
5
src/types/data-socket.ts
Normal file
5
src/types/data-socket.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { WebSocket } from 'ws';
|
||||||
|
|
||||||
|
export interface PlayerSocket extends WebSocket {
|
||||||
|
id: string;
|
||||||
|
}
|
3
src/types/error-type.enum.ts
Normal file
3
src/types/error-type.enum.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export enum ErrorType {
|
||||||
|
AUTH_FAIL = 0,
|
||||||
|
}
|
3
src/types/instancable.ts
Normal file
3
src/types/instancable.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface Instancable<T> {
|
||||||
|
new (...args: any[]): T;
|
||||||
|
}
|
67
src/types/packet-type.enum.ts
Normal file
67
src/types/packet-type.enum.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
export enum PacketType {
|
||||||
|
/**
|
||||||
|
* [][Player ID][Session Token][Client Version]
|
||||||
|
*/
|
||||||
|
AUTH = 0,
|
||||||
|
KEEPALIVE,
|
||||||
|
/**
|
||||||
|
* [][World name]
|
||||||
|
*/
|
||||||
|
STREAM_START,
|
||||||
|
/**
|
||||||
|
* [][Asset Name][Asset Type][Buffer]
|
||||||
|
*/
|
||||||
|
STREAM_ASSET,
|
||||||
|
/**
|
||||||
|
* [][Object UUID][Parent UUID][Object Type][Object data JSON]
|
||||||
|
*/
|
||||||
|
STREAM_OBJECT,
|
||||||
|
/**
|
||||||
|
* [][Object UUID]
|
||||||
|
*/
|
||||||
|
STREAM_DESTROY,
|
||||||
|
/**
|
||||||
|
* [][Object UUID][Event][JSON]
|
||||||
|
*/
|
||||||
|
STREAM_EVENT,
|
||||||
|
/**
|
||||||
|
* [][World name]
|
||||||
|
*/
|
||||||
|
STREAM_FINISH,
|
||||||
|
/**
|
||||||
|
* [][UUID][x y z Position][x y z w Rotation][x y z linvel][x y z angvel]
|
||||||
|
*/
|
||||||
|
STREAM_TRANSFORM,
|
||||||
|
/**
|
||||||
|
* [][Player ID][Chat Type][Chat Message]
|
||||||
|
*/
|
||||||
|
STREAM_CHAT,
|
||||||
|
/**
|
||||||
|
* [][Player ID:Player Name]
|
||||||
|
*/
|
||||||
|
PLAYER_LIST,
|
||||||
|
/**
|
||||||
|
* [][Player ID][Player Name]
|
||||||
|
*/
|
||||||
|
PLAYER_JOIN,
|
||||||
|
/**
|
||||||
|
* [][Player ID][Player Name][Reason]
|
||||||
|
*/
|
||||||
|
PLAYER_QUIT,
|
||||||
|
/**
|
||||||
|
* [][Player ID][Player Name][x y z Position][Data...]
|
||||||
|
*/
|
||||||
|
PLAYER_CHARACTER,
|
||||||
|
/**
|
||||||
|
* [][Velocity][LookAt][AnimState]
|
||||||
|
*/
|
||||||
|
PLAYER_MOVEMENT,
|
||||||
|
/**
|
||||||
|
* [][Chat Type][Chat Message]
|
||||||
|
*/
|
||||||
|
PLAYER_CHAT,
|
||||||
|
/**
|
||||||
|
* [][Error type]
|
||||||
|
*/
|
||||||
|
ERROR,
|
||||||
|
}
|
31
src/types/property.decorator.ts
Normal file
31
src/types/property.decorator.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
|
|
||||||
|
export function ObjectProperty(): PropertyDecorator {
|
||||||
|
return (target, propertyKey): void => {
|
||||||
|
let properties: Array<string> = Reflect.getOwnMetadata(
|
||||||
|
'properties',
|
||||||
|
target,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!properties) {
|
||||||
|
Reflect.defineMetadata('properties', (properties = []), target);
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.push(String(propertyKey));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ObjectPropertyExclude(): PropertyDecorator {
|
||||||
|
return (target, propertyKey): void => {
|
||||||
|
let excluded: Array<string> = Reflect.getOwnMetadata(
|
||||||
|
'excludedProperties',
|
||||||
|
target,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!excluded) {
|
||||||
|
Reflect.defineMetadata('excludedProperties', (excluded = []), target);
|
||||||
|
}
|
||||||
|
|
||||||
|
excluded.push(String(propertyKey));
|
||||||
|
};
|
||||||
|
}
|
7
src/types/serialized.ts
Normal file
7
src/types/serialized.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface SerializedObject {
|
||||||
|
[x: string]: unknown;
|
||||||
|
name: string;
|
||||||
|
objectType: string;
|
||||||
|
children: SerializedObject[];
|
||||||
|
visible: boolean;
|
||||||
|
}
|
10
src/types/world-file.ts
Normal file
10
src/types/world-file.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { SerializedEnvironment } from 'src/game/environment.object';
|
||||||
|
import { Asset } from './asset';
|
||||||
|
import { SerializedObject } from './serialized';
|
||||||
|
|
||||||
|
export interface WorldFile {
|
||||||
|
name: string;
|
||||||
|
world: SerializedObject;
|
||||||
|
environment: SerializedEnvironment;
|
||||||
|
assets: Asset[];
|
||||||
|
}
|
3
src/utils/clamp.ts
Normal file
3
src/utils/clamp.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function clamp(value: number, min: number, max: number) {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
}
|
4
src/utils/random.ts
Normal file
4
src/utils/random.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
export const getRandomId = v4;
|
||||||
|
export const getRandomName = () => `Player${Date.now()}`;
|
12
src/utils/read-metadata.ts
Normal file
12
src/utils/read-metadata.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
|
|
||||||
|
export const readMetadataOf = <T>(object: any, key: string) => {
|
||||||
|
const metadata: T[] = [];
|
||||||
|
let target = Object.getPrototypeOf(object);
|
||||||
|
while (target != Object.prototype) {
|
||||||
|
const childFields = Reflect.getOwnMetadata(key, target) || [];
|
||||||
|
metadata.unshift(...childFields);
|
||||||
|
target = Object.getPrototypeOf(target);
|
||||||
|
}
|
||||||
|
return metadata;
|
||||||
|
};
|
113
src/world/physics.service.ts
Normal file
113
src/world/physics.service.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Humanoid, PhysicsObject, World } from 'src/game';
|
||||||
|
import RAPIER from '@dimforge/rapier3d-compat';
|
||||||
|
import { Packet } from 'src/net/packet';
|
||||||
|
import { PacketType } from 'src/types/packet-type.enum';
|
||||||
|
import { Object3D, Quaternion, SkinnedMesh, Vector3 } from 'three';
|
||||||
|
import { GameObject } from 'src/game/game-object';
|
||||||
|
import { PhysicsTicking } from 'src/physics';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PhysicsService {
|
||||||
|
private world!: World;
|
||||||
|
private physicsWorld!: RAPIER.World;
|
||||||
|
private characterPhysics!: RAPIER.KinematicCharacterController;
|
||||||
|
|
||||||
|
private running = false;
|
||||||
|
private sceneInitialized = false;
|
||||||
|
private physicsTicker: ReturnType<typeof setTimeout>;
|
||||||
|
private trackedObjects: PhysicsTicking[] = [];
|
||||||
|
|
||||||
|
loop() {
|
||||||
|
if (!this.running) return;
|
||||||
|
this.physicsTicker = setTimeout(() => this.loop(), 20);
|
||||||
|
this.physicsWorld.step();
|
||||||
|
for (const object of this.trackedObjects) object.tick(0.02); // TODO: DT
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(world: World) {
|
||||||
|
this.world = world;
|
||||||
|
await RAPIER.init();
|
||||||
|
this.physicsWorld = new RAPIER.World(new Vector3(0, this.world.gravity, 0));
|
||||||
|
this.running = true;
|
||||||
|
this.initializePhysicsScene();
|
||||||
|
this.loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getObjectPackets() {
|
||||||
|
const data: Buffer[] = [];
|
||||||
|
for (const object of this.trackedObjects) {
|
||||||
|
const body = (object as PhysicsObject)?.rigidBody;
|
||||||
|
if (!body) continue;
|
||||||
|
data.push(
|
||||||
|
new Packet(PacketType.STREAM_TRANSFORM)
|
||||||
|
.write(object.uuid, String)
|
||||||
|
.write(body.translation(), 'vec3')
|
||||||
|
.write(body.rotation(), 'quat')
|
||||||
|
.write(body.linvel(), 'vec3')
|
||||||
|
.write(body.angvel(), 'vec3')
|
||||||
|
.toBuffer(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPhysics(root: Object3D) {
|
||||||
|
root.traverse((object) => {
|
||||||
|
// Prevent double-init
|
||||||
|
if (this.trackedObjects.some((entry) => entry.uuid === object.uuid))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Initialize humanoids
|
||||||
|
if (object instanceof Humanoid) {
|
||||||
|
object.initialize(RAPIER, this.physicsWorld, this.characterPhysics);
|
||||||
|
this.trackedObjects.push(object);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only track physics object instances
|
||||||
|
if (!(object instanceof PhysicsObject)) return;
|
||||||
|
|
||||||
|
// Do not add physics to virtual objects
|
||||||
|
if ((object as GameObject).virtual) return;
|
||||||
|
|
||||||
|
// Initialize object physics
|
||||||
|
object.initialize(RAPIER, this.physicsWorld);
|
||||||
|
this.trackedObjects.push(object);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removePhysics(root: Object3D) {
|
||||||
|
const trackedObjects = [...this.trackedObjects];
|
||||||
|
const physicsLeaveUUIDs: string[] = [];
|
||||||
|
|
||||||
|
root.traverse((object) => {
|
||||||
|
if (trackedObjects.some((item) => item.uuid === object.uuid)) {
|
||||||
|
physicsLeaveUUIDs.push(object.uuid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.trackedObjects = this.trackedObjects.filter((object) =>
|
||||||
|
physicsLeaveUUIDs.includes(object.uuid),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initializePhysicsScene() {
|
||||||
|
if (this.sceneInitialized) return;
|
||||||
|
|
||||||
|
this.characterPhysics = this.physicsWorld.createCharacterController(0.01);
|
||||||
|
this.characterPhysics.setApplyImpulsesToDynamicBodies(true);
|
||||||
|
this.characterPhysics.setMaxSlopeClimbAngle((75 * Math.PI) / 180);
|
||||||
|
this.characterPhysics.enableAutostep(1, 0.5, true);
|
||||||
|
// Automatically slide down on slopes smaller than 30 degrees.
|
||||||
|
// this.characterPhysics.setMinSlopeSlideAngle((30 * Math.PI) / 180);
|
||||||
|
|
||||||
|
this.applyPhysics(this.world);
|
||||||
|
|
||||||
|
this.sceneInitialized = true;
|
||||||
|
}
|
||||||
|
}
|
13
src/world/world.module.ts
Normal file
13
src/world/world.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { WorldService } from './world.service';
|
||||||
|
import { PlayerModule } from 'src/player/player.module';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
import { PhysicsService } from './physics.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PlayerModule, ConfigModule, HttpModule],
|
||||||
|
providers: [WorldService, PhysicsService],
|
||||||
|
exports: [WorldService],
|
||||||
|
})
|
||||||
|
export class WorldModule {}
|
190
src/world/world.service.ts
Normal file
190
src/world/world.service.ts
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { PlayerStoreService } from 'src/player/player-store.service';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { WorldFile } from 'src/types/world-file';
|
||||||
|
import { Environment, World, instancableGameObjects } from 'src/game';
|
||||||
|
import { SerializedObject } from 'src/types/serialized';
|
||||||
|
import { Object3D } from 'three';
|
||||||
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
import { Packet } from 'src/net/packet';
|
||||||
|
import { PacketType } from 'src/types/packet-type.enum';
|
||||||
|
import { Player } from 'src/game/player';
|
||||||
|
import { GameObject } from 'src/game/game-object';
|
||||||
|
import { PhysicsService } from './physics.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WorldService implements OnModuleInit {
|
||||||
|
private logger = new Logger(WorldService.name);
|
||||||
|
private world = new World();
|
||||||
|
private environment = new Environment();
|
||||||
|
private broadcastWorldStateTicker!: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly players: PlayerStoreService,
|
||||||
|
private readonly physics: PhysicsService,
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly http: HttpService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.logger.log('Loading world file from environment');
|
||||||
|
this.loadWorld()
|
||||||
|
.then(() => {
|
||||||
|
this.logger.log('World file loaded');
|
||||||
|
this.physics.start(this.world);
|
||||||
|
this.startUpdateTick();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.logger.error('Failed to load world:', err.stack);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadWorld() {
|
||||||
|
const path = this.config.get('world').worldAsset;
|
||||||
|
const file = await lastValueFrom(this.http.get(path));
|
||||||
|
await this.deserializeLevelSave(file.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendWorldToPlayer(player: Player) {
|
||||||
|
const packets: Buffer[] = [];
|
||||||
|
this.world.traverse((object) => {
|
||||||
|
if (!(object instanceof GameObject)) return;
|
||||||
|
packets.push(
|
||||||
|
new Packet(PacketType.STREAM_OBJECT)
|
||||||
|
.write(object.uuid, String)
|
||||||
|
.write(object.parent?.uuid || '', String)
|
||||||
|
.write(object.objectType, String)
|
||||||
|
.write(JSON.stringify(object.serialize(true)), String)
|
||||||
|
.toBuffer(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
packets.forEach((packet) => player.send(packet));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initializePlayer(player: Player) {
|
||||||
|
// Streaming start
|
||||||
|
player.send(
|
||||||
|
new Packet(PacketType.STREAM_START)
|
||||||
|
.write(this.world.name, String)
|
||||||
|
.toBuffer(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: assets
|
||||||
|
|
||||||
|
// Send world objects
|
||||||
|
await this.sendWorldToPlayer(player);
|
||||||
|
|
||||||
|
// Player is initialized
|
||||||
|
player.initialized = true;
|
||||||
|
|
||||||
|
// Send player list
|
||||||
|
player.send(this.players.getPlayerListPacket());
|
||||||
|
|
||||||
|
// Broadcast join
|
||||||
|
this.players.broadcast(
|
||||||
|
new Packet(PacketType.PLAYER_JOIN)
|
||||||
|
.write(player.id, String)
|
||||||
|
.write(player.name, String)
|
||||||
|
.toBuffer(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tell player they're ready to play
|
||||||
|
player.send(
|
||||||
|
new Packet(PacketType.STREAM_FINISH)
|
||||||
|
.write(this.world.name, String)
|
||||||
|
.toBuffer(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Spawn player character
|
||||||
|
const character = player.createPlayerCharacter();
|
||||||
|
this.world.add(character);
|
||||||
|
this.physics.applyPhysics(character);
|
||||||
|
// TODO: position
|
||||||
|
this.players.broadcast(
|
||||||
|
new Packet(PacketType.PLAYER_CHARACTER)
|
||||||
|
.write(player.id, String)
|
||||||
|
.write(player.name, String)
|
||||||
|
.write(character.position, 'vec3')
|
||||||
|
.write(character.quaternion, 'quat')
|
||||||
|
.toBuffer(),
|
||||||
|
);
|
||||||
|
this.players
|
||||||
|
.getPlayerCharacterPackets(player)
|
||||||
|
.forEach((packet) => player.send(packet));
|
||||||
|
}
|
||||||
|
|
||||||
|
public createObject(object: string, setParent?: Object3D, skipEvent = false) {
|
||||||
|
const parent = setParent || this.world;
|
||||||
|
const ObjectType = instancableGameObjects[object];
|
||||||
|
if (!ObjectType) return;
|
||||||
|
const newObject = new ObjectType();
|
||||||
|
parent.add(newObject);
|
||||||
|
|
||||||
|
if (!skipEvent) {
|
||||||
|
this.players.broadcast(
|
||||||
|
new Packet(PacketType.STREAM_OBJECT)
|
||||||
|
.write(newObject.uuid, String)
|
||||||
|
.write(newObject.parent?.uuid || '', String)
|
||||||
|
.write(newObject.objectType, String)
|
||||||
|
.write(JSON.stringify(newObject.serialize()), String)
|
||||||
|
.toBuffer(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public deserializeObject(root: SerializedObject, setParent?: Object3D) {
|
||||||
|
const parent = setParent || this.world;
|
||||||
|
|
||||||
|
if (root.objectType === 'World') {
|
||||||
|
root.children.forEach((entry) => this.recursiveCreate(entry, parent));
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.recursiveCreate(root, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public serializeLevelSave(name: string): WorldFile {
|
||||||
|
const world = this.world.serialize();
|
||||||
|
const environment = this.environment.serialize();
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
world,
|
||||||
|
environment,
|
||||||
|
assets: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deserializeLevelSave(save: WorldFile) {
|
||||||
|
this.environment.deserialize(save.environment);
|
||||||
|
|
||||||
|
// Load world
|
||||||
|
this.deserializeObject(save.world);
|
||||||
|
this.world.deserialize(save.world);
|
||||||
|
}
|
||||||
|
|
||||||
|
private recursiveCreate(entry: SerializedObject, setParent?: Object3D) {
|
||||||
|
const parent = setParent || this.world;
|
||||||
|
const newObject = this.createObject(entry.objectType, parent, true);
|
||||||
|
newObject?.deserialize(entry);
|
||||||
|
entry.children.forEach((child) => this.recursiveCreate(child, newObject));
|
||||||
|
return newObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
private broadcastWorldState() {
|
||||||
|
const data = this.physics.getObjectPackets();
|
||||||
|
data.forEach((data) =>
|
||||||
|
this.players.broadcastExcept(data, this.players.getUninitializedIds()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startUpdateTick() {
|
||||||
|
this.broadcastWorldStateTicker = setInterval(
|
||||||
|
() => this.broadcastWorldState(),
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
24
test/app.e2e-spec.ts
Normal file
24
test/app.e2e-spec.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import * as request from 'supertest';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
9
test/jest-e2e.json
Normal file
9
test/jest-e2e.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2021",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user