diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c7526ec..1fc7e1e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,7 +3,7 @@ name: CI
on:
push:
branches:
- - "**"
+ - '**'
pull_request:
branches:
- main
@@ -24,7 +24,7 @@ jobs:
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
- deno-version: "latest"
+ deno-version: 'latest'
- name: Build project
run: deno task build
- name: List build directory contents
@@ -39,7 +39,7 @@ jobs:
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
- deno-version: "latest"
+ deno-version: 'latest'
- name: Build project
run: deno task build
- name: Run lint
@@ -53,25 +53,11 @@ jobs:
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
- deno-version: "latest"
+ deno-version: 'latest'
- name: install
run: deno install
- name: check src
run: deno check ./src/client ./src/server
- # Format Job
- format:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
- - name: Set up Deno
- uses: denoland/setup-deno@v1
- with:
- deno-version: "latest"
- - name: install
- run: deno install
- - name: fmt check src
- run: deno fmt --check src
# Healthcheck Job
health:
@@ -82,7 +68,7 @@ jobs:
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
- deno-version: "latest"
+ deno-version: 'latest'
- name: Start server
run: deno task start &
- name: Health check
@@ -136,7 +122,7 @@ jobs:
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
- deno-version: "latest"
+ deno-version: 'latest'
- name: Run security audit
run: npm audit --audit-level=high
@@ -151,7 +137,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
with:
- persist-credentials: "false"
+ persist-credentials: 'false'
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v3
@@ -167,3 +153,4 @@ jobs:
GIT_COMMITTER_NAME: "github-actions[bot]"
GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com"
run: npx semantic-release --extends ./release.config.js
+
diff --git a/deno.json b/deno.json
index b99aba1..e0bd509 100644
--- a/deno.json
+++ b/deno.json
@@ -1,29 +1,23 @@
{
- "tasks": {
- "dev": "deno run -A --node-modules-dir npm:vite",
- "build": "deno run -A --node-modules-dir npm:vite build",
- "preview": "deno run -A --node-modules-dir npm:vite preview",
- "serve": "deno run --allow-net --allow-read jsr:@std/http@1/file-server dist/",
- "start": "deno task build && deno run --allow-read --allow-env --allow-net --allow-write main.ts"
- },
- "compilerOptions": {
- "lib": ["ES2020", "DOM", "DOM.Iterable", "deno.ns"]
- },
- "imports": {
- "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.0",
- "@oak/oak": "jsr:@oak/oak@^17.1.3",
- "@std/http": "jsr:@std/http@^1.0.10",
- "vite": "npm:vite@^5.4.8"
- },
- "lint": {
- "include": ["src/"],
- "exclude": ["dist/", "node_modules/", "public/"]
- },
- "fmt": {
- "options": {
- "lineWidth": 120,
- "useTabs": true,
- "singleQuote": true
- }
- }
-}
+ "tasks": {
+ "dev": "deno run -A --node-modules-dir npm:vite",
+ "build": "deno run -A --node-modules-dir npm:vite build",
+ "preview": "deno run -A --node-modules-dir npm:vite preview",
+ "serve": "deno run --allow-net --allow-read jsr:@std/http@1/file-server dist/",
+ "start": "deno task build && deno run --allow-read --allow-env --allow-net --allow-write main.ts"
+ },
+ "compilerOptions": {
+ "lib": ["ES2020", "DOM", "DOM.Iterable", "deno.ns"]
+ },
+ "imports": {
+ "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.0",
+ "@oak/oak": "jsr:@oak/oak@^17.1.3",
+ "@std/http": "jsr:@std/http@^1.0.10",
+ "vite": "npm:vite@^5.4.8"
+ },
+ "lint": {
+ "include": ["src/"],
+ "exclude": ["dist/", "node_modules/", "public/"]
+
+ }
+}
\ No newline at end of file
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 812d0c0..83d3238 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -2,9 +2,9 @@ import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
- selector: 'app-root',
- standalone: true,
- imports: [RouterOutlet],
- template: ``,
+ selector: 'app-root',
+ standalone: true,
+ imports: [RouterOutlet],
+ template: ``,
})
export class AppComponent {}
diff --git a/src/app/app.config.server.ts b/src/app/app.config.server.ts
index b872e63..de174d7 100644
--- a/src/app/app.config.server.ts
+++ b/src/app/app.config.server.ts
@@ -1,10 +1,10 @@
-import { ApplicationConfig, mergeApplicationConfig } from '@angular/core';
+import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
-import { appConfig } from './app.config.ts';
+import { appConfig } from "./app.config.ts";
const serverConfig: ApplicationConfig = {
- providers: [provideServerRendering()],
+ providers: [provideServerRendering()],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);
diff --git a/src/app/app.config.ts b/src/app/app.config.ts
index 0a47c40..a1a58ee 100644
--- a/src/app/app.config.ts
+++ b/src/app/app.config.ts
@@ -1,16 +1,20 @@
-import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
+import {
+ provideHttpClient,
+ withFetch,
+ withInterceptors,
+} from '@angular/common/http';
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';
import { provideFileRouter, requestContextInterceptor } from '@analogjs/router';
export const appConfig: ApplicationConfig = {
- providers: [
- provideZoneChangeDetection({ eventCoalescing: true }),
- provideFileRouter(),
- provideHttpClient(
- withFetch(),
- withInterceptors([requestContextInterceptor]),
- ),
- provideClientHydration(),
- ],
+ providers: [
+ provideZoneChangeDetection({ eventCoalescing: true }),
+ provideFileRouter(),
+ provideHttpClient(
+ withFetch(),
+ withInterceptors([requestContextInterceptor])
+ ),
+ provideClientHydration(),
+ ],
};
diff --git a/src/app/game/game.component.css b/src/app/game/game.component.css
index 8b13789..e69de29 100644
--- a/src/app/game/game.component.css
+++ b/src/app/game/game.component.css
@@ -1 +0,0 @@
-
diff --git a/src/app/game/game.component.html b/src/app/game/game.component.html
index 8485102..ba88aad 100644
--- a/src/app/game/game.component.html
+++ b/src/app/game/game.component.html
@@ -1,2 +1,2 @@
game works!
-
+
\ No newline at end of file
diff --git a/src/app/game/game.component.spec.ts b/src/app/game/game.component.spec.ts
new file mode 100644
index 0000000..47c16c9
--- /dev/null
+++ b/src/app/game/game.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GameComponent } from './game.component';
+
+describe('GameComponent', () => {
+ let component: GameComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [GameComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(GameComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/game/game.component.ts b/src/app/game/game.component.ts
index 2680403..0b06268 100644
--- a/src/app/game/game.component.ts
+++ b/src/app/game/game.component.ts
@@ -1,23 +1,22 @@
// game.component.ts
-import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
-import { Game } from '../../client/core/Game.ts';
+import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
+import { Game } from "../../client/core/Game.ts";
@Component({
- selector: 'app-game',
- templateUrl: './game.component.html',
- standalone: true,
+ selector: 'app-game',
+ templateUrl: './game.component.html',
+ standalone: true,
})
export class GameComponent implements AfterViewInit {
- @ViewChild('rendererContainer')
- rendererContainer!: ElementRef;
- private game?: Game;
+ @ViewChild('rendererContainer') rendererContainer!: ElementRef;
+ private game?: Game;
- ngAfterViewInit() {
- this.game = new Game(this.rendererContainer.nativeElement);
- this.game.start();
- }
+ ngAfterViewInit() {
+ this.game = new Game(this.rendererContainer.nativeElement);
+ this.game.start();
+ }
- ngOnDestroy() {
- // Add cleanup if needed
- }
+ ngOnDestroy() {
+ // Add cleanup if needed
+ }
}
diff --git a/src/app/pages/index.page.ts b/src/app/pages/index.page.ts
index 9441705..749676b 100644
--- a/src/app/pages/index.page.ts
+++ b/src/app/pages/index.page.ts
@@ -1,13 +1,13 @@
import { Component } from '@angular/core';
-import { GameComponent } from '../game/game.component.ts';
+import {GameComponent} from "../game/game.component.ts";
@Component({
- selector: 'app-home',
- standalone: true,
- template: ``,
- styles: ``,
- imports: [
- GameComponent,
- ],
+ selector: 'app-home',
+ standalone: true,
+ template: ``,
+ styles: ``,
+ imports: [
+ GameComponent
+ ]
})
export default class HomeComponent {}
diff --git a/src/client/core/AssetManager.ts b/src/client/core/AssetManager.ts
index 6acfa9c..11b66b1 100644
--- a/src/client/core/AssetManager.ts
+++ b/src/client/core/AssetManager.ts
@@ -3,87 +3,87 @@ import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
import * as THREE from 'three';
interface AssetEntry {
- scene: THREE.Group;
- isLoaded: boolean;
- callbacks: Array<(scene: THREE.Group) => void>;
+ scene: THREE.Group;
+ isLoaded: boolean;
+ callbacks: Array<(scene: THREE.Group) => void>;
}
export class AssetManager {
- private static instance: AssetManager;
- private assets: Map;
- private gltfLoader: GLTFLoader;
+ private static instance: AssetManager;
+ private assets: Map;
+ private gltfLoader: GLTFLoader;
- private constructor() {
- this.assets = new Map();
- this.gltfLoader = new GLTFLoader();
- const dracoLoader = new DRACOLoader();
- dracoLoader.setDecoderPath('/draco/');
- this.gltfLoader.setDRACOLoader(dracoLoader);
- }
+ private constructor() {
+ this.assets = new Map();
+ this.gltfLoader = new GLTFLoader();
+ const dracoLoader = new DRACOLoader();
+ dracoLoader.setDecoderPath('/draco/');
+ this.gltfLoader.setDRACOLoader(dracoLoader);
+ }
- public static getInstance(): AssetManager {
- if (!AssetManager.instance) {
- AssetManager.instance = new AssetManager();
- }
- return AssetManager.instance;
- }
+ public static getInstance(): AssetManager {
+ if (!AssetManager.instance) {
+ AssetManager.instance = new AssetManager();
+ }
+ return AssetManager.instance;
+ }
- private cloneWithNewMaterials(scene: THREE.Group): THREE.Group {
- const clonedScene = scene.clone();
+ private cloneWithNewMaterials(scene: THREE.Group): THREE.Group {
+ const clonedScene = scene.clone();
- clonedScene.traverse((node) => {
- if ((node as THREE.Mesh).isMesh) {
- const mesh = node as THREE.Mesh;
- if (Array.isArray(mesh.material)) {
- mesh.material = mesh.material.map((mat) => mat.clone());
- } else {
- mesh.material = mesh.material.clone();
- }
- }
- });
+ clonedScene.traverse((node) => {
+ if ((node as THREE.Mesh).isMesh) {
+ const mesh = node as THREE.Mesh;
+ if (Array.isArray(mesh.material)) {
+ mesh.material = mesh.material.map(mat => mat.clone());
+ } else {
+ mesh.material = mesh.material.clone();
+ }
+ }
+ });
- return clonedScene;
- }
+ return clonedScene;
+ }
- public loadAsset(url: string, callback: (scene: THREE.Group) => void): void {
- if (this.assets.has(url)) {
- const assetEntry = this.assets.get(url)!;
- if (assetEntry.isLoaded) {
- // Asset is already loaded, clone with new materials
- callback(this.cloneWithNewMaterials(assetEntry.scene));
- } else {
- // Asset is loading, add callback to the list
- assetEntry.callbacks.push(callback);
- }
- } else {
- // Asset not loaded yet, start loading
- this.assets.set(url, { scene: new THREE.Group(), isLoaded: false, callbacks: [callback] });
- this.gltfLoader.load(
- url,
- (gltf) => {
- const assetEntry = this.assets.get(url)!;
- assetEntry.scene = gltf.scene;
- assetEntry.isLoaded = true;
+ public loadAsset(url: string, callback: (scene: THREE.Group) => void): void {
+ if (this.assets.has(url)) {
+ const assetEntry = this.assets.get(url)!;
+ if (assetEntry.isLoaded) {
+ // Asset is already loaded, clone with new materials
+ callback(this.cloneWithNewMaterials(assetEntry.scene));
+ } else {
+ // Asset is loading, add callback to the list
+ assetEntry.callbacks.push(callback);
+ }
+ } else {
+ // Asset not loaded yet, start loading
+ this.assets.set(url, { scene: new THREE.Group(), isLoaded: false, callbacks: [callback] });
+ this.gltfLoader.load(
+ url,
+ (gltf) => {
+ const assetEntry = this.assets.get(url)!;
+ assetEntry.scene = gltf.scene;
+ assetEntry.isLoaded = true;
- // Call all callbacks waiting for this asset with new material clones
- assetEntry.callbacks.forEach((cb) => {
- cb(this.cloneWithNewMaterials(gltf.scene));
- });
- assetEntry.callbacks = [];
- },
- undefined,
- (error) => {
- console.error(`Error loading asset ${url}:`, error);
- },
- );
- }
- }
+ // Call all callbacks waiting for this asset with new material clones
+ assetEntry.callbacks.forEach((cb) => {
+ cb(this.cloneWithNewMaterials(gltf.scene));
+ });
+ assetEntry.callbacks = [];
+ },
+ undefined,
+ (error) => {
+ console.error(`Error loading asset ${url}:`, error);
+ }
+ );
+ }
+ }
- public preloadAsset(url: string): void {
- this.loadAsset(url, () => {});
- }
+ public preloadAsset(url: string): void {
+ this.loadAsset(url, () => {});
+ }
- public clearCache(): void {
- this.assets.clear();
- }
+ public clearCache(): void {
+ this.assets.clear();
+ }
}
diff --git a/src/client/core/CommandManager.ts b/src/client/core/CommandManager.ts
index 44344fe..6661249 100644
--- a/src/client/core/CommandManager.ts
+++ b/src/client/core/CommandManager.ts
@@ -1,190 +1,175 @@
-import { Player } from './Player.ts';
-import { ChatOverlay } from '../ui/ChatOverlay.ts';
-import { SettingsManager } from './SettingsManager.ts';
+import {Player} from "./Player.ts";
+import {ChatOverlay} from "../ui/ChatOverlay.ts";
+import {SettingsManager} from "./SettingsManager.ts";
export class CommandManager {
- private localPlayer: Player;
- private chatOverlay: ChatOverlay;
- private readonly commands: Command[];
-
- constructor(localPlayer: Player, chatOverlay: ChatOverlay) {
- this.localPlayer = localPlayer;
- this.chatOverlay = chatOverlay;
- this.commands = [];
- this.init();
- }
-
- public init() {
- this.commands.push(
- new Command('sense', (args: string[]): string => {
- if (args[1] == null) {
- return 'sensitivity is currently ' + (SettingsManager.settings.sense);
- }
- const sense: number = Number(args[1]);
- if (Number.isNaN(sense)) {
- return args[1] + ' is not a number';
- }
- if (sense > 10 || sense <= 0) {
- return 'sensitivity is not in the valid range of 0 to 10';
- }
- SettingsManager.settings.sense = sense;
- SettingsManager.write();
- return 'sensitivity is now set to ' + sense;
- }),
- );
-
- this.commands.push(
- new Command('resetSettings', (): string => {
- SettingsManager.reset();
- SettingsManager.write();
- return 'settings have been reverted to their default states';
- }),
- );
-
- this.commands.push(
- new Command('controllerSense', (args: string[]): string => {
- if (args[1] == null) {
- return 'controller sensitivity is currently ' + (SettingsManager.settings.controllerSense);
- }
- const sense = Number(args[1]);
- if (Number.isNaN(sense)) {
- return args[1] + ' is not a number';
- }
- if (sense > 10 || sense <= 0) {
- return 'controller sensitivity is not in the valid range of 0 to 10';
- }
- SettingsManager.settings.controllerSense = sense;
- SettingsManager.write();
- return 'controller sensitivity is now set to ' + sense;
- }),
- );
-
- this.commands.push(
- new Command('crosshairColor', (args: string[]): string => {
- if ((args[1] && args[2] && args[3])) {
- for (let i = 1; i < args.length; i++) {
- if (Number.isNaN(Number(args[i]))) return args[i] + ' is not a number';
- if (Number(args[i]) < 0 || Number(args[i]) > 255) {
- return args[i] + ' is not in range 0-255';
- }
- }
- SettingsManager.settings.crosshairColor = '#' + componentToHex(Number(args[1])) +
- componentToHex(Number(args[2])) + componentToHex(Number(args[3]));
- } else if (args[1]) {
- const color: string | null = cssToHex(args[1]);
- if (!color) {
- return args[1] + ' is not a valid color';
- }
- SettingsManager.settings.crosshairColor = color;
- } else {
- return 'invalid input';
- }
- SettingsManager.write();
- return 'crosshair color set to ' + SettingsManager.settings.crosshairColor;
- }),
- );
-
- this.commands.push(
- new Command('crosshairType', (args: string[]): string => {
- if (args[1] == 'cross') SettingsManager.settings.crosshairType = 0;
- else if (args[1] == 'dot') SettingsManager.settings.crosshairType = 1;
- else return 'not a valid type (dot or cross)';
- SettingsManager.write();
- return 'crosshair type set to ' + args[1];
- }),
- );
-
- this.commands.push(
- new Command('bobbing', (args: string[]): string => {
- const bobbing = Number(args[1]);
- if (Number.isNaN(bobbing)) {
- return args[1] + ' is not a number';
- }
- if (bobbing < 0 || bobbing > 2) {
- return args[1] + ' is not in range 0 to 2';
- }
- SettingsManager.settings.viewBobbingStrength = bobbing;
- SettingsManager.write();
- return 'view bobbing strength is now set to ' + args[1];
- }),
- );
-
- this.commands.push(
- new Command('prettyText', (args: string[]): string => {
- if (args[1] == null) return 'prettyText is currently ' + SettingsManager.settings.doPrettyText;
- if (args[1] == 'true') SettingsManager.settings.doPrettyText = true;
- else if (args[1] == 'false') SettingsManager.settings.doPrettyText = false;
- else return 'invalid input (true/false)';
- SettingsManager.write();
- return 'prettyText set to ' + args[1];
- }),
- );
- }
-
- public runCmd(cmd: string): boolean {
- let match: boolean = false;
- const args: string[] = cmd.substring(1).split(' ');
- for (let i = 0; i < this.commands.length; i++) {
- if (args[0].toLowerCase() === this.commands[i].getCmdName()) {
- match = true;
- const msg = this.commands[i].run(args);
-
- const chatMessage = {
- id: this.localPlayer.id,
- name: '',
- message: cmd + ' -> ' + msg,
- };
-
- this.chatOverlay.addChatMessage(chatMessage);
- break;
- }
- }
- return match;
- }
+ private localPlayer: Player;
+ private chatOverlay: ChatOverlay;
+ private readonly commands: Command[];
+
+ constructor(localPlayer: Player, chatOverlay: ChatOverlay) {
+ this.localPlayer = localPlayer;
+ this.chatOverlay = chatOverlay;
+ this.commands = [];
+ this.init();
+ }
+
+ public init() {
+ this.commands.push(new Command('sense', (args: string[]): string => {
+ if (args[1] == null) {
+ return "sensitivity is currently " + (SettingsManager.settings.sense);
+ }
+ const sense: number = Number(args[1]);
+ if (Number.isNaN(sense)) {
+ return args[1] + " is not a number";
+ }
+ if (sense > 10 || sense <= 0) {
+ return "sensitivity is not in the valid range of 0 to 10";
+ }
+ SettingsManager.settings.sense = sense;
+ SettingsManager.write();
+ return "sensitivity is now set to " + (sense);
+ }));
+
+ this.commands.push(new Command('resetSettings', (): string => {
+ SettingsManager.reset();
+ SettingsManager.write();
+ return "settings have been reverted to their default states";
+ }));
+
+ this.commands.push(new Command('controllerSense', (args: string[]): string => {
+ if (args[1] == null) {
+ return "controller sensitivity is currently " + (SettingsManager.settings.controllerSense);
+ }
+ const sense = Number(args[1]);
+ if (Number.isNaN(sense)) {
+ return args[1] + " is not a number";
+ }
+ if (sense > 10 || sense <= 0) {
+ return "controller sensitivity is not in the valid range of 0 to 10";
+ }
+ SettingsManager.settings.controllerSense = sense;
+ SettingsManager.write();
+ return "controller sensitivity is now set to " + (sense);
+ }));
+
+ this.commands.push(new Command('crosshairColor', (args: string[]): string => {
+ if ((args[1] && args[2] && args[3])) {
+ for (let i = 1; i < args.length; i++) {
+ if (Number.isNaN(Number(args[i]))) return args[i] + " is not a number";
+ if (Number(args[i]) < 0 || Number(args[i]) > 255) {
+ return args[i] + ' is not in range 0-255';
+ }
+ }
+ SettingsManager.settings.crosshairColor = '#' + componentToHex(Number(args[1])) + componentToHex(Number(args[2])) + componentToHex(Number(args[3]));
+ } else if (args[1]) {
+ const color : string | null = cssToHex(args[1]);
+ if(!color) {
+ return args[1] + ' is not a valid color';
+ }
+ SettingsManager.settings.crosshairColor = color;
+ } else {
+ return 'invalid input';
+ }
+ SettingsManager.write();
+ return 'crosshair color set to ' + SettingsManager.settings.crosshairColor;
+ }));
+
+ this.commands.push(new Command('crosshairType', (args: string[]): string => {
+ if (args[1] == 'cross') SettingsManager.settings.crosshairType = 0;
+ else if (args[1] == 'dot') SettingsManager.settings.crosshairType = 1;
+ else return 'not a valid type (dot or cross)';
+ SettingsManager.write();
+ return 'crosshair type set to ' + args[1];
+ } ));
+
+ this.commands.push(new Command('bobbing', (args: string[]) : string => {
+ const bobbing = Number(args[1]);
+ if (Number.isNaN(bobbing)) {
+ return args[1] + ' is not a number'
+ }
+ if (bobbing < 0 || bobbing > 2) {
+ return args[1] + ' is not in range 0 to 2';
+ }
+ SettingsManager.settings.viewBobbingStrength = bobbing;
+ SettingsManager.write();
+ return 'view bobbing strength is now set to ' + args[1];
+ }));
+
+ this.commands.push(new Command('prettyText', (args: string[]): string => {
+ if(args[1] == null) return 'prettyText is currently ' + SettingsManager.settings.doPrettyText;
+ if (args[1] == 'true') SettingsManager.settings.doPrettyText = true;
+ else if (args[1] == 'false') SettingsManager.settings.doPrettyText = false;
+ else return 'invalid input (true/false)';
+ SettingsManager.write();
+ return 'prettyText set to ' + args[1];
+ } ));
+ }
+
+ public runCmd(cmd: string): boolean {
+ let match: boolean = false;
+ const args: string[] = cmd.substring(1).split(" ")
+ for (let i = 0; i < this.commands.length; i++) {
+ if (args[0].toLowerCase() === this.commands[i].getCmdName()) {
+ match = true;
+ const msg = this.commands[i].run(args);
+
+ const chatMessage = {
+ id: this.localPlayer.id,
+ name: '',
+ message: cmd + " -> " + msg
+ };
+
+ this.chatOverlay.addChatMessage(chatMessage);
+ break;
+ }
+ }
+ return match;
+ }
}
class Command {
- private readonly cmdName: string;
- private readonly func: (args: string[]) => string;
- constructor(cmd: string, func: (args: string[]) => string) {
- this.cmdName = cmd.toLowerCase();
- this.func = func;
- }
- public run(args: string[]): string {
- return this.func(args);
- }
-
- public getCmdName() {
- return this.cmdName;
- }
+ private readonly cmdName: string;
+ private readonly func: (args: string[]) => string;
+ constructor(cmd: string, func: (args: string[]) => string) {
+ this.cmdName = cmd.toLowerCase();
+ this.func = func;
+ }
+ public run(args: string[]): string {
+ return this.func(args);
+ }
+
+ public getCmdName() {
+ return this.cmdName;
+ }
}
function componentToHex(c: number): string {
- const hex = c.toString(16);
- return hex.length === 1 ? '0' + hex : hex;
+ const hex = c.toString(16);
+ return hex.length === 1 ? "0" + hex : hex;
}
function cssToHex(color: string) {
- // Create a dummy div to get computed color style
- if (!isColor(color)) return null;
- const div = document.createElement('div');
- div.style.color = color;
- document.body.appendChild(div);
- const computedColor = getComputedStyle(div).color;
- document.body.removeChild(div);
-
- // Extract rgb values
- const rgbMatch = computedColor.match(/\d+/g);
- if (rgbMatch) {
- const [r, g, b] = rgbMatch.map(Number);
- return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b);
- }
- return null;
+ // Create a dummy div to get computed color style
+ if (!isColor(color)) return null;
+ const div = document.createElement('div');
+ div.style.color = color;
+ document.body.appendChild(div);
+ const computedColor = getComputedStyle(div).color;
+ document.body.removeChild(div);
+
+ // Extract rgb values
+ const rgbMatch = computedColor.match(/\d+/g);
+ if (rgbMatch) {
+ const [r, g, b] = rgbMatch.map(Number);
+ return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b);
+ }
+ return null;
}
function isColor(strColor: string) {
- const s = new Option().style;
- s.color = strColor;
- // If the color is recognized, s.color won't be empty
- return s.color !== '';
-}
+ const s = new Option().style;
+ s.color = strColor;
+ // If the color is recognized, s.color won't be empty
+ return s.color !== '';
+}
\ No newline at end of file
diff --git a/src/client/core/Game.ts b/src/client/core/Game.ts
index aa449bf..dbc9143 100644
--- a/src/client/core/Game.ts
+++ b/src/client/core/Game.ts
@@ -1,73 +1,73 @@
-import { Player } from './Player.ts';
-import { Renderer } from './Renderer.ts';
-import { ChatOverlay } from '../ui/ChatOverlay.ts';
-import { InputHandler } from '../input/InputHandler.ts';
-import { Networking } from './Networking.ts';
-import { CollisionManager } from '../input/CollisionManager.ts';
-import { Inventory } from './Inventory.ts';
-import { HealthIndicator } from '../ui/HealthIndicator.ts';
-import { MapLoader } from './MapLoader.ts';
-import { RemoteItemRenderer } from './RemoteItemRenderer.ts';
-import { TouchInputHandler } from '../input/TouchInputHandler.ts';
+import {Player} from './Player.ts';
+import {Renderer} from './Renderer.ts';
+import {ChatOverlay} from '../ui/ChatOverlay.ts';
+import {InputHandler} from '../input/InputHandler.ts';
+import {Networking} from './Networking.ts';
+import {CollisionManager} from '../input/CollisionManager.ts';
+import {Inventory} from './Inventory.ts';
+import {HealthIndicator} from '../ui/HealthIndicator.ts';
+import {MapLoader} from './MapLoader.ts';
+import {RemoteItemRenderer} from "./RemoteItemRenderer.ts";
+import { TouchInputHandler } from "../input/TouchInputHandler.ts";
export class Game {
- private localPlayer: Player;
- private renderer: Renderer;
- private chatOverlay: ChatOverlay;
- private inputHandler: InputHandler;
- private touchInputHandler: TouchInputHandler;
- private networking: Networking;
- private collisionManager: CollisionManager;
- private inventoryManager: Inventory;
- private mapLoader: MapLoader;
- private healthIndicator: HealthIndicator;
- private remoteItemRenderer: RemoteItemRenderer;
- private gameIndex: number;
- private static nextGameIndex: number = 0;
+ private localPlayer: Player;
+ private renderer: Renderer;
+ private chatOverlay: ChatOverlay;
+ private inputHandler: InputHandler;
+ private touchInputHandler: TouchInputHandler;
+ private networking: Networking;
+ private collisionManager: CollisionManager;
+ private inventoryManager: Inventory;
+ private mapLoader: MapLoader;
+ private healthIndicator: HealthIndicator;
+ private remoteItemRenderer: RemoteItemRenderer;
+ private gameIndex: number;
+ private static nextGameIndex: number = 0;
- constructor(container: HTMLElement) {
- this.gameIndex = Game.nextGameIndex++;
- this.localPlayer = new Player();
- this.chatOverlay = new ChatOverlay(container, this.localPlayer);
- this.networking = new Networking(this.localPlayer, this.chatOverlay);
- this.renderer = new Renderer(container, this.networking, this.localPlayer, this.chatOverlay);
- this.chatOverlay.setRenderer(this.renderer);
- this.inputHandler = new InputHandler(this.renderer, this.localPlayer, this.gameIndex);
- this.touchInputHandler = new TouchInputHandler(this.inputHandler, this.chatOverlay);
- this.renderer.setInputHandler(this.inputHandler);
- this.collisionManager = new CollisionManager(this.inputHandler);
- this.renderer.setCollisionManager(this.collisionManager);
- this.inventoryManager = new Inventory(this.renderer, this.inputHandler, this.networking, this.localPlayer);
- this.chatOverlay.setNetworking(this.networking);
- this.chatOverlay.setInputHandler(this.inputHandler);
- this.mapLoader = new MapLoader(this.renderer);
- this.healthIndicator = new HealthIndicator(this.renderer, this.localPlayer, this.networking);
- this.remoteItemRenderer = new RemoteItemRenderer(this.networking, this.renderer);
- }
- init() {
- this.inventoryManager.init();
- this.healthIndicator.init();
- }
+ constructor(container: HTMLElement) {
+ this.gameIndex = Game.nextGameIndex++;
+ this.localPlayer = new Player();
+ this.chatOverlay = new ChatOverlay(container,this.localPlayer);
+ this.networking = new Networking(this.localPlayer, this.chatOverlay);
+ this.renderer = new Renderer(container, this.networking, this.localPlayer, this.chatOverlay);
+ this.chatOverlay.setRenderer(this.renderer);
+ this.inputHandler = new InputHandler(this.renderer, this.localPlayer, this.gameIndex);
+ this.touchInputHandler = new TouchInputHandler(this.inputHandler, this.chatOverlay);
+ this.renderer.setInputHandler(this.inputHandler);
+ this.collisionManager = new CollisionManager(this.inputHandler);
+ this.renderer.setCollisionManager(this.collisionManager);
+ this.inventoryManager = new Inventory(this.renderer, this.inputHandler, this.networking, this.localPlayer);
+ this.chatOverlay.setNetworking(this.networking);
+ this.chatOverlay.setInputHandler(this.inputHandler);
+ this.mapLoader = new MapLoader(this.renderer);
+ this.healthIndicator = new HealthIndicator(this.renderer,this.localPlayer, this.networking);
+ this.remoteItemRenderer = new RemoteItemRenderer(this.networking, this.renderer);
+ }
- animate() {
- this.inputHandler.handleInputs();
- this.touchInputHandler.onFrame();
- this.collisionManager.collisionPeriodic(this.localPlayer);
- this.networking.updatePlayerData();
- this.chatOverlay.onFrame();
- this.inventoryManager.onFrame();
- this.healthIndicator.onFrame();
- this.renderer.onFrame(this.localPlayer);
- if (this.networking.getServerInfo().mapName) {
- this.mapLoader.load('/maps/' + this.networking.getServerInfo().mapName + '/map.glb');
- }
- this.remoteItemRenderer.onFrame();
- requestAnimationFrame(this.animate.bind(this));
- }
+ init() {
+ this.inventoryManager.init();
+ this.healthIndicator.init();
+ }
- start() {
- this.init();
- this.animate();
- }
+ animate() {
+ this.inputHandler.handleInputs();
+ this.touchInputHandler.onFrame();
+ this.collisionManager.collisionPeriodic(this.localPlayer);
+ this.networking.updatePlayerData();
+ this.chatOverlay.onFrame();
+ this.inventoryManager.onFrame();
+ this.healthIndicator.onFrame();
+ this.renderer.onFrame(this.localPlayer);
+ if(this.networking.getServerInfo().mapName)
+ this.mapLoader.load('/maps/' + this.networking.getServerInfo().mapName + '/map.glb');
+ this.remoteItemRenderer.onFrame();
+ requestAnimationFrame(this.animate.bind(this));
+ }
+
+ start() {
+ this.init();
+ this.animate();
+ }
}
diff --git a/src/client/core/Inventory.ts b/src/client/core/Inventory.ts
index 48a7471..65c3a15 100644
--- a/src/client/core/Inventory.ts
+++ b/src/client/core/Inventory.ts
@@ -3,175 +3,172 @@ import { Renderer } from './Renderer.ts';
import { InputHandler } from '../input/InputHandler.ts';
import { BananaGun } from '../items/BananaGun.ts';
import { HeldItemInput } from '../input/HeldItemInput.ts';
-import { Networking } from './Networking.ts';
-import { Player } from './Player.ts';
-import { ItemBase, ItemType } from '../items/ItemBase.ts';
-import { FishGun } from '../items/FishGun.ts';
+import {Networking} from "./Networking.ts";
+import {Player} from "./Player.ts";
+import {ItemBase, ItemType} from "../items/ItemBase.ts";
+import {FishGun} from "../items/FishGun.ts";
export class Inventory {
- private inventoryItems: ItemBase[] = [];
- private renderer: Renderer;
- private inputHandler: InputHandler;
- private networking: Networking;
- private inventoryScene: THREE.Scene;
- private selectedInventoryItem: number = 0;
- private lastSelectedInventoryItem: number = 0;
- private cameraY: number = 0;
- private cameraX: number = 0;
- private clock: THREE.Clock;
- private camera: THREE.Camera;
- private lastInventoryTouchTime: number = 0;
- private localPlayer: Player;
- private oldInventory: number[] = [];
-
- private oldDownPressed: boolean = false;
- private oldUpPressed: boolean = false;
- private oldQPressed: boolean = false;
- private oldNumsPressed: boolean[] = new Array(10).fill(false);
-
- constructor(renderer: Renderer, inputHandler: InputHandler, networking: Networking, localPlayer: Player) {
- this.renderer = renderer;
- this.inputHandler = inputHandler;
- this.networking = networking;
- this.inventoryScene = renderer.getInventoryMenuScene();
- this.clock = new THREE.Clock();
- this.camera = renderer.getInventoryMenuCamera();
- this.localPlayer = localPlayer;
- }
-
- public init() {
- }
-
- private updateInventoryItems() {
- if (!this.arraysEqual(this.oldInventory, this.localPlayer.inventory)) {
- for (let i = this.inventoryItems.length - 1; i >= 0; i--) {
- this.inventoryItems[i].destroy();
- this.inventoryItems.splice(i, 1);
- }
-
- //iterate through every number in localPlayer.inventory
- for (let i = 0; i < this.localPlayer.inventory.length; i++) {
- const num = this.localPlayer.inventory[i];
- switch (num) {
- case 1: {
- const banana = new BananaGun(this.renderer, this.networking, i, ItemType.InventoryItem);
- this.inventoryItems.push(banana);
- break;
- }
- case 2: {
- const fish = new FishGun(this.renderer, this.networking, i, ItemType.InventoryItem);
- this.inventoryItems.push(fish);
- break;
- }
- default: {
- const testItem = new ItemBase(
- ItemType.InventoryItem,
- this.renderer.getHeldItemScene(),
- this.inventoryScene,
- i,
- );
- this.inventoryItems.push(testItem);
- break;
- }
- }
- }
- }
-
- this.oldInventory = this.localPlayer.inventory;
- }
-
- public arraysEqual(a: number[], b: number[]) {
- if (a === b) return true;
- if (a == null || b == null) return false;
- if (a.length != b.length) return false;
- for (let i = 0; i < a.length; ++i) {
- if (a[i] !== b[i]) return false;
- }
- return true;
- }
-
- public onFrame() {
- this.updateInventoryItems();
- const gamepadInputs = this.inputHandler.getGamepadInputs();
- const heldItemInput = new HeldItemInput(this.inputHandler.getShoot(), this.inputHandler.getAim(), false);
- let downPressed = (this.inputHandler.getKey('[') || this.inputHandler.getInventoryIterationTouched()) &&
- !this.localPlayer.chatActive;
- let upPressed = this.inputHandler.getKey(']') && !this.localPlayer.chatActive;
- const qPressed = this.inputHandler.getKey('q') && !this.localPlayer.chatActive;
- if (gamepadInputs.leftShoulder && !this.localPlayer.chatActive) upPressed = true;
- if (gamepadInputs.rightShoulder && !this.localPlayer.chatActive) downPressed = true;
- const lastScroll = this.inputHandler.getScrollClicks();
- if (lastScroll > 0) upPressed = true;
- if (lastScroll < 0) downPressed = true;
-
- if (!this.localPlayer.chatActive) {
- const nums = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
- for (let i = 0; i < nums.length; i++) {
- const numPressed = this.inputHandler.getKey(nums[i]);
- if (numPressed && !this.oldNumsPressed[i]) {
- this.lastSelectedInventoryItem = this.selectedInventoryItem;
- this.selectedInventoryItem = i;
- this.lastInventoryTouchTime = Date.now() / 1000;
- break;
- }
- }
-
- for (let i = 0; i < nums.length; i++) {
- this.oldNumsPressed[i] = this.inputHandler.getKey(nums[i]);
- }
- }
-
- if (downPressed || upPressed) this.lastInventoryTouchTime = Date.now() / 1000;
- const deltaTime = this.clock.getDelta();
-
- if (downPressed && !this.oldDownPressed) {
- this.lastSelectedInventoryItem = this.selectedInventoryItem;
- this.selectedInventoryItem++;
- }
- if (upPressed && !this.oldUpPressed) {
- this.lastSelectedInventoryItem = this.selectedInventoryItem;
- this.selectedInventoryItem--;
- }
- if (this.inputHandler.getKey('enter')) {
- this.lastInventoryTouchTime = 0; //hide inventory
- }
-
- if (qPressed && !this.oldQPressed) {
- const temp = this.selectedInventoryItem;
- this.selectedInventoryItem = this.lastSelectedInventoryItem;
- this.lastSelectedInventoryItem = temp;
- //this.lastInventoryTouchTime = Date.now() / 1000 - 1.25;
- }
-
- if (this.selectedInventoryItem < 0) {
- this.selectedInventoryItem = this.inventoryItems.length - 1;
- }
- if (this.selectedInventoryItem >= this.inventoryItems.length) {
- this.selectedInventoryItem = 0;
- }
-
- if (this.lastSelectedInventoryItem < 0) {
- this.lastSelectedInventoryItem = this.inventoryItems.length - 1;
- }
- if (this.lastSelectedInventoryItem >= this.inventoryItems.length) {
- this.lastSelectedInventoryItem = 0;
- }
-
- this.cameraY = this.selectedInventoryItem; //might be backwards
- if (Date.now() / 1000 - this.lastInventoryTouchTime > 2) {
- this.cameraX = -1;
- } else {
- this.cameraX = 0;
- }
-
- this.camera.position.lerp(new THREE.Vector3(this.cameraX, this.selectedInventoryItem, 5), 0.4 * deltaTime * 60);
-
- for (const item of this.inventoryItems) {
- item.onFrame(heldItemInput, this.selectedInventoryItem);
- }
-
- this.oldDownPressed = downPressed;
- this.oldUpPressed = upPressed;
- this.oldQPressed = qPressed;
- }
-}
+ private inventoryItems: ItemBase[] = [];
+ private renderer: Renderer;
+ private inputHandler: InputHandler;
+ private networking: Networking;
+ private inventoryScene: THREE.Scene;
+ private selectedInventoryItem: number = 0;
+ private lastSelectedInventoryItem: number = 0;
+ private cameraY: number = 0;
+ private cameraX: number = 0;
+ private clock: THREE.Clock;
+ private camera: THREE.Camera;
+ private lastInventoryTouchTime: number = 0;
+ private localPlayer: Player;
+ private oldInventory: number[] = [];
+
+
+ private oldDownPressed: boolean = false;
+ private oldUpPressed: boolean = false;
+ private oldQPressed: boolean = false;
+ private oldNumsPressed: boolean[] = new Array(10).fill(false);
+
+
+
+ constructor(renderer: Renderer, inputHandler: InputHandler, networking:Networking, localPlayer:Player) {
+ this.renderer = renderer;
+ this.inputHandler = inputHandler;
+ this.networking = networking;
+ this.inventoryScene = renderer.getInventoryMenuScene();
+ this.clock = new THREE.Clock();
+ this.camera = renderer.getInventoryMenuCamera();
+ this.localPlayer = localPlayer;
+ }
+
+ public init() {
+
+ }
+
+ private updateInventoryItems(){
+ if(!this.arraysEqual(this.oldInventory, this.localPlayer.inventory)) {
+ for(let i = this.inventoryItems.length - 1; i >= 0; i--) {
+ this.inventoryItems[i].destroy();
+ this.inventoryItems.splice(i, 1);
+ }
+
+ //iterate through every number in localPlayer.inventory
+ for(let i = 0; i < this.localPlayer.inventory.length; i++) {
+ const num = this.localPlayer.inventory[i];
+ switch(num) {
+ case 1: {
+ const banana = new BananaGun(this.renderer, this.networking, i, ItemType.InventoryItem);
+ this.inventoryItems.push(banana);
+ break;
+ }
+ case 2: {
+ const fish = new FishGun(this.renderer, this.networking, i, ItemType.InventoryItem);
+ this.inventoryItems.push(fish);
+ break;
+ }
+ default: {
+ const testItem = new ItemBase(ItemType.InventoryItem, this.renderer.getHeldItemScene(), this.inventoryScene, i);
+ this.inventoryItems.push(testItem);
+ break;
+ }
+ }
+
+ }
+ }
+
+ this.oldInventory = this.localPlayer.inventory;
+ }
+
+ public arraysEqual(a: number[], b: number[]) {
+ if (a === b) return true;
+ if (a == null || b == null) return false;
+ if (a.length != b.length) return false;
+ for (let i = 0; i < a.length; ++i)
+ if (a[i] !== b[i]) return false;
+ return true;
+ }
+
+ public onFrame() {
+ this.updateInventoryItems();
+ const gamepadInputs = this.inputHandler.getGamepadInputs();
+ const heldItemInput = new HeldItemInput(this.inputHandler.getShoot(), this.inputHandler.getAim(), false);
+ let downPressed =( this.inputHandler.getKey('[') || this.inputHandler.getInventoryIterationTouched()) && !this.localPlayer.chatActive;
+ let upPressed = this.inputHandler.getKey(']') && !this.localPlayer.chatActive;
+ const qPressed = this.inputHandler.getKey('q') && !this.localPlayer.chatActive;
+ if (gamepadInputs.leftShoulder && !this.localPlayer.chatActive) upPressed = true;
+ if (gamepadInputs.rightShoulder && !this.localPlayer.chatActive) downPressed = true;
+ const lastScroll = this.inputHandler.getScrollClicks();
+ if(lastScroll > 0) upPressed = true;
+ if(lastScroll < 0) downPressed = true;
+
+
+ if(!this.localPlayer.chatActive){
+ const nums = ['1','2','3','4','5','6','7','8','9','0'];
+ for(let i = 0; i < nums.length; i++) {
+ const numPressed = this.inputHandler.getKey(nums[i]);
+ if(numPressed && !this.oldNumsPressed[i]) {
+ this.lastSelectedInventoryItem = this.selectedInventoryItem;
+ this.selectedInventoryItem = i;
+ this.lastInventoryTouchTime = Date.now() / 1000;
+ break;
+ }
+ }
+
+ for(let i = 0; i < nums.length; i++) {
+ this.oldNumsPressed[i] = this.inputHandler.getKey(nums[i]);
+ }
+ }
+
+
+ if(downPressed || upPressed) this.lastInventoryTouchTime = Date.now() / 1000;
+ const deltaTime = this.clock.getDelta();
+
+ if(downPressed && !this.oldDownPressed){
+ this.lastSelectedInventoryItem = this.selectedInventoryItem;
+ this.selectedInventoryItem++;
+ }
+ if(upPressed && !this.oldUpPressed){
+ this.lastSelectedInventoryItem = this.selectedInventoryItem;
+ this.selectedInventoryItem--;
+ }
+ if(this.inputHandler.getKey('enter'))
+ this.lastInventoryTouchTime = 0; //hide inventory
+
+ if(qPressed && !this.oldQPressed) {
+ const temp = this.selectedInventoryItem;
+ this.selectedInventoryItem = this.lastSelectedInventoryItem;
+ this.lastSelectedInventoryItem = temp;
+ //this.lastInventoryTouchTime = Date.now() / 1000 - 1.25;
+ }
+
+
+
+ if(this.selectedInventoryItem < 0)
+ this.selectedInventoryItem = this.inventoryItems.length - 1;
+ if(this.selectedInventoryItem >= this.inventoryItems.length)
+ this.selectedInventoryItem = 0;
+
+ if(this.lastSelectedInventoryItem < 0)
+ this.lastSelectedInventoryItem = this.inventoryItems.length - 1;
+ if(this.lastSelectedInventoryItem >= this.inventoryItems.length)
+ this.lastSelectedInventoryItem = 0;
+
+ this.cameraY = this.selectedInventoryItem; //might be backwards
+ if(Date.now()/1000 - this.lastInventoryTouchTime > 2)
+ this.cameraX = -1;
+ else
+ this.cameraX = 0;
+
+
+ this.camera.position.lerp(new THREE.Vector3(this.cameraX, this.selectedInventoryItem, 5), 0.4 * deltaTime * 60);
+
+ for(const item of this.inventoryItems) {
+ item.onFrame(heldItemInput, this.selectedInventoryItem);
+ }
+
+ this.oldDownPressed = downPressed;
+ this.oldUpPressed = upPressed;
+ this.oldQPressed = qPressed;
+ }
+}
\ No newline at end of file
diff --git a/src/client/core/MapLoader.ts b/src/client/core/MapLoader.ts
index 6587db3..d17ebb3 100644
--- a/src/client/core/MapLoader.ts
+++ b/src/client/core/MapLoader.ts
@@ -1,28 +1,29 @@
import * as THREE from 'three';
import { Renderer } from './Renderer.ts';
-import { computeBoundsTree } from 'three-mesh-bvh';
-import { CollisionManager } from '../input/CollisionManager.ts';
-import { AssetManager } from './AssetManager.ts';
+import {computeBoundsTree} from "three-mesh-bvh";
+import {CollisionManager} from "../input/CollisionManager.ts";
+import { AssetManager } from "./AssetManager.ts";
THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
export class MapLoader {
- private scene: THREE.Scene;
- private mapObject: THREE.Group | undefined;
- private mapUrl: string = '';
+ private scene: THREE.Scene;
+ private mapObject: THREE.Group | undefined;
+ private mapUrl: string = '';
- constructor(renderer: Renderer) {
- this.scene = renderer.getScene();
- }
+ constructor(renderer: Renderer) {
+ this.scene = renderer.getScene();
+ }
- public load(mapUrl: string) {
- if (mapUrl === this.mapUrl) return;
- this.mapUrl = mapUrl;
- AssetManager.getInstance().loadAsset(mapUrl, (scene) => {
- if (this.mapObject) this.scene.remove(this.mapObject);
- this.mapObject = scene;
- CollisionManager.staticGeometry(scene);
- this.scene.add(this.mapObject);
- });
- }
-}
+ public load(mapUrl: string) {
+ if(mapUrl === this.mapUrl) return;
+ this.mapUrl = mapUrl;
+ AssetManager.getInstance().loadAsset(mapUrl, (scene) => {
+ if(this.mapObject) this.scene.remove(this.mapObject);
+ this.mapObject = scene;
+ CollisionManager.staticGeometry(scene);
+ this.scene.add(this.mapObject);
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/src/client/core/Networking.ts b/src/client/core/Networking.ts
index e6e79ac..d1a7be0 100644
--- a/src/client/core/Networking.ts
+++ b/src/client/core/Networking.ts
@@ -4,256 +4,251 @@ import { ChatOverlay } from '../ui/ChatOverlay.ts';
import * as THREE from 'three';
export interface RemotePlayer {
- idLastDamagedBy: number;
- latency: number;
- id: number;
- position: { x: number; y: number; z: number };
- velocity: { x: number; y: number; z: number };
- lookQuaternion: [number, number, number, number];
- name: string;
- gravity: number;
- forced: boolean;
- health: number;
- inventory: number[];
- chatActive: boolean;
- chatMsg: string;
- playerSpectating: number;
- gameMsgs: string[];
- gameMsgs2: string[];
+ idLastDamagedBy: number;
+ latency: number;
+ id: number;
+ position: { x: number, y: number, z: number };
+ velocity: { x: number, y: number, z: number };
+ lookQuaternion: [number, number, number, number];
+ name: string;
+ gravity: number;
+ forced: boolean;
+ health: number;
+ inventory: number[];
+ chatActive: boolean;
+ chatMsg: string;
+ playerSpectating: number;
+ gameMsgs: string[];
+ gameMsgs2: string[];
}
interface WorldItem {
- vector: { x: number; y: number; z: number };
- id: number;
- itemType: number;
+ vector: { x: number, y: number, z: number };
+ id: number;
+ itemType: number;
}
interface ServerInfo {
- name: string;
- maxPlayers: number;
- currentPlayers: number;
- mapName: string;
- tickRate: number;
- version: string;
- gameMode: string;
- playerMaxHealth: number;
+ name: string;
+ maxPlayers: number;
+ currentPlayers: number;
+ mapName: string;
+ tickRate: number;
+ version: string;
+ gameMode: string;
+ playerMaxHealth: number;
}
interface LastUploadedLocalPlayer {
- position: THREE.Vector3;
- quaternion: THREE.Quaternion;
- chatMsg: string;
- velocity: THREE.Vector3;
- name: string;
+ position: THREE.Vector3;
+ quaternion: THREE.Quaternion;
+ chatMsg: string;
+ velocity: THREE.Vector3;
+ name: string;
}
export class Networking {
- private socket: Socket;
- private gameVersion: string = '';
- private remotePlayers: RemotePlayer[] = [];
- private worldItems: WorldItem[] = [];
- private lastUploadedLocalPlayer: LastUploadedLocalPlayer | null = null;
- private lastUploadTime: number;
- private uploadWait: number;
- private lastLatencyTestEmit: number;
- private lastLatencyTestGotResponse: boolean;
- private latencyTestWait: number;
- private messagesBeingTyped: string[] = [];
- private localPlayer: Player;
- private chatOverlay: ChatOverlay;
- private damagedTimestamp: number = 0;
- private serverInfo: ServerInfo;
-
- constructor(localPlayer: Player, chatOverlay: ChatOverlay) {
- this.localPlayer = localPlayer;
- this.chatOverlay = chatOverlay;
-
- this.socket = io();
- this.fetchVersion();
-
- this.lastUploadTime = Date.now() / 1000;
- this.uploadWait = 0.05; //gets replaced by server info
- this.lastLatencyTestEmit = 0;
- this.lastLatencyTestGotResponse = false;
- this.latencyTestWait = 5;
-
- this.serverInfo = {
- name: '',
- maxPlayers: 0,
- currentPlayers: 0,
- mapName: '',
- tickRate: 0,
- version: '',
- gameMode: '',
- playerMaxHealth: 0,
- };
-
- this.setupSocketListeners();
- }
-
- private async fetchVersion() {
- try {
- const response = await fetch('gameVersion.json');
- const data = await response.json();
- this.gameVersion = data['version'];
- } catch (e) {
- console.error(e);
- }
- }
-
- private setupSocketListeners() {
- this.socket.on('latencyTest', () => {
- this.localPlayer.latency = (Date.now() / 1000 - this.lastLatencyTestEmit) * 1000;
- this.lastLatencyTestGotResponse = true;
- });
-
- this.socket.on('remotePlayerData', (data: RemotePlayer[]) => {
- this.remotePlayers = data;
- this.processRemotePlayerData();
- });
-
- this.socket.on('worldItemData', (data: WorldItem[]) => {
- this.worldItems = data;
- this.processWorldItemData();
- });
-
- this.socket.on('chatMsg', (data: { id: number; name: string; message: string }) => {
- if (data.id !== this.localPlayer.id) this.chatOverlay.addChatMessage(data);
- });
-
- this.socket.on('serverInfo', (data: ServerInfo) => {
- this.serverInfo = data;
- this.onServerInfo();
- });
- }
-
- private onServerInfo() {
- this.uploadWait = 1 / this.serverInfo.tickRate;
- }
- public updatePlayerData() {
- const currentTime = Date.now() / 1000;
- this.localPlayer.gameVersion = this.gameVersion;
- if (currentTime - this.lastUploadTime < this.uploadWait) return;
-
- if (this.localPlayer.gameVersion === '') return;
-
- if (
- this.playersAreEqualEnough(this.localPlayer, this.lastUploadedLocalPlayer) &&
- currentTime - this.lastUploadTime < 4
- ) {
- return;
- }
-
- this.socket.emit('playerData', this.localPlayer);
- this.lastUploadedLocalPlayer = {
- position: this.localPlayer.position.clone(),
- quaternion: this.localPlayer.quaternion.clone(),
- chatMsg: this.localPlayer.chatMsg,
- velocity: this.localPlayer.velocity.clone(),
- name: this.localPlayer.name,
- };
-
- this.lastUploadTime = currentTime;
-
- if (currentTime - this.lastLatencyTestEmit > this.latencyTestWait) {
- this.socket.emit('latencyTest');
- this.lastLatencyTestEmit = currentTime;
- if (!this.lastLatencyTestGotResponse) {
- this.localPlayer.latency = 999;
- }
- this.lastLatencyTestGotResponse = false;
- }
- }
-
- public processWorldItemData() {
- // Implementation for processing world items
- }
-
- public getServerInfo() {
- return this.serverInfo;
- }
-
- private processRemotePlayerData() {
- this.messagesBeingTyped = [];
- for (const remotePlayer of this.remotePlayers) {
- if (remotePlayer.id === this.localPlayer.id) {
- if (remotePlayer.forced) {
- this.localPlayer.position.set(remotePlayer.position.x, remotePlayer.position.y, remotePlayer.position.z);
- this.localPlayer.velocity.set(remotePlayer.velocity.x, remotePlayer.velocity.y, remotePlayer.velocity.z);
- this.localPlayer.lookQuaternion.set(...remotePlayer.lookQuaternion);
- //this.localPlayer.name = remotePlayer.name;
- this.localPlayer.gravity = remotePlayer.gravity;
- this.localPlayer.forcedAcknowledged = true;
- } else {
- this.localPlayer.forcedAcknowledged = false;
- }
- if (remotePlayer.health < this.localPlayer.health) this.damagedTimestamp = Date.now() / 1000;
- this.localPlayer.health = remotePlayer.health;
- this.localPlayer.idLastDamagedBy = remotePlayer.idLastDamagedBy;
- this.localPlayer.inventory = remotePlayer.inventory;
- this.localPlayer.playerSpectating = remotePlayer.playerSpectating;
- this.localPlayer.gameMsgs = remotePlayer.gameMsgs;
- this.localPlayer.gameMsgs2 = remotePlayer.gameMsgs2;
- continue;
- }
- if (remotePlayer.chatActive) {
- this.messagesBeingTyped.push(`${remotePlayer.name}: ${remotePlayer.chatMsg}`);
- }
- }
- }
-
- private playersAreEqualEnough(player1: Player, player2: LastUploadedLocalPlayer | null) {
- if (player1 === null || player2 === null) return false;
- let out = true;
- out = out && player1.position.equals(player2.position);
- out = out && player1.quaternion.equals(player2.quaternion);
- out = out && player1.chatMsg === player2.chatMsg;
- out = out && player1.velocity.equals(player2.velocity);
- out = out && player1.name === player2.name;
-
- return out;
- }
-
- public getDamagedTimestamp() {
- return this.damagedTimestamp;
- }
-
- public getMessagesBeingTyped() {
- return this.messagesBeingTyped;
- }
-
- public getRemotePlayerData(): RemotePlayer[] {
- return this.remotePlayers;
- }
-
- public sendMessage(msg: string) {
- const chatMessage = {
- message: msg,
- id: this.localPlayer.id,
- name: this.localPlayer.name,
- };
- if (msg.length < 1) return;
- if (chatMessage.message.startsWith('>')) chatMessage.message = '&2' + chatMessage.message;
- if (msg.charAt(0) === '/') {
- this.socket.emit('chatMsg', chatMessage);
- return;
- }
- chatMessage.message = '&f' + chatMessage.message;
- this.chatOverlay.addChatMessage(chatMessage);
- this.socket.emit('chatMsg', chatMessage);
- }
-
- public applyDamage(id: number, damage: number) {
- const player2 = this.remotePlayers.find((player) => player.id === id);
- const damageRequest = {
- localPlayer: this.localPlayer,
- targetPlayer: player2,
- damage: damage,
- };
- this.socket.emit('applyDamage', damageRequest);
- }
-
- public getWorldItemsData() {
- return this.worldItems;
- }
+ private socket: Socket;
+ private gameVersion: string = '';
+ private remotePlayers: RemotePlayer[] = [];
+ private worldItems: WorldItem[] = [];
+ private lastUploadedLocalPlayer: LastUploadedLocalPlayer | null = null;
+ private lastUploadTime: number;
+ private uploadWait: number;
+ private lastLatencyTestEmit: number;
+ private lastLatencyTestGotResponse: boolean;
+ private latencyTestWait: number;
+ private messagesBeingTyped: string[] = [];
+ private localPlayer: Player;
+ private chatOverlay: ChatOverlay;
+ private damagedTimestamp: number = 0;
+ private serverInfo: ServerInfo;
+
+ constructor(localPlayer: Player, chatOverlay: ChatOverlay) {
+ this.localPlayer = localPlayer;
+ this.chatOverlay = chatOverlay;
+
+ this.socket = io();
+ this.fetchVersion();
+
+ this.lastUploadTime = Date.now() / 1000;
+ this.uploadWait = 0.05; //gets replaced by server info
+ this.lastLatencyTestEmit = 0;
+ this.lastLatencyTestGotResponse = false;
+ this.latencyTestWait = 5;
+
+ this.serverInfo = {
+ name: '',
+ maxPlayers: 0,
+ currentPlayers: 0,
+ mapName: '',
+ tickRate: 0,
+ version: '',
+ gameMode: '',
+ playerMaxHealth: 0
+ }
+
+ this.setupSocketListeners();
+ }
+
+ private async fetchVersion() {
+ try {
+ const response = await fetch('gameVersion.json');
+ const data = await response.json();
+ this.gameVersion = data['version'];
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ private setupSocketListeners() {
+ this.socket.on('latencyTest', () => {
+ this.localPlayer.latency = (Date.now() / 1000 - this.lastLatencyTestEmit) * 1000;
+ this.lastLatencyTestGotResponse = true;
+ });
+
+ this.socket.on('remotePlayerData', (data: RemotePlayer[]) => {
+ this.remotePlayers = data;
+ this.processRemotePlayerData();
+ });
+
+ this.socket.on('worldItemData', (data: WorldItem[]) => {
+ this.worldItems = data;
+ this.processWorldItemData();
+ });
+
+ this.socket.on('chatMsg', (data: { id: number, name: string, message: string }) => {
+ if (data.id !== this.localPlayer.id) this.chatOverlay.addChatMessage(data);
+ });
+
+ this.socket.on('serverInfo', (data: ServerInfo) => {
+ this.serverInfo = data;
+ this.onServerInfo();
+ });
+ }
+
+ private onServerInfo() {
+ this.uploadWait = 1 / this.serverInfo.tickRate;
+ }
+ public updatePlayerData() {
+ const currentTime = Date.now() / 1000;
+ this.localPlayer.gameVersion = this.gameVersion;
+ if (currentTime - this.lastUploadTime < this.uploadWait) return;
+
+ if (this.localPlayer.gameVersion === '') return;
+
+ if (this.playersAreEqualEnough(this.localPlayer, this.lastUploadedLocalPlayer) && currentTime - this.lastUploadTime < 4)
+ return;
+
+ this.socket.emit('playerData', this.localPlayer);
+ this.lastUploadedLocalPlayer = {
+ position: this.localPlayer.position.clone(),
+ quaternion: this.localPlayer.quaternion.clone(),
+ chatMsg: this.localPlayer.chatMsg,
+ velocity: this.localPlayer.velocity.clone(),
+ name: this.localPlayer.name,
+ };
+
+ this.lastUploadTime = currentTime;
+
+ if (currentTime - this.lastLatencyTestEmit > this.latencyTestWait) {
+ this.socket.emit('latencyTest');
+ this.lastLatencyTestEmit = currentTime;
+ if (!this.lastLatencyTestGotResponse) {
+ this.localPlayer.latency = 999;
+ }
+ this.lastLatencyTestGotResponse = false;
+ }
+ }
+
+ public processWorldItemData() {
+ // Implementation for processing world items
+ }
+
+ public getServerInfo() {
+ return this.serverInfo;
+ }
+
+ private processRemotePlayerData() {
+ this.messagesBeingTyped = [];
+ for (const remotePlayer of this.remotePlayers) {
+ if (remotePlayer.id === this.localPlayer.id) {
+ if (remotePlayer.forced) {
+ this.localPlayer.position.set(remotePlayer.position.x, remotePlayer.position.y, remotePlayer.position.z);
+ this.localPlayer.velocity.set(remotePlayer.velocity.x, remotePlayer.velocity.y, remotePlayer.velocity.z);
+ this.localPlayer.lookQuaternion.set(...remotePlayer.lookQuaternion);
+ //this.localPlayer.name = remotePlayer.name;
+ this.localPlayer.gravity = remotePlayer.gravity;
+ this.localPlayer.forcedAcknowledged = true;
+ } else {
+ this.localPlayer.forcedAcknowledged = false;
+ }
+ if (remotePlayer.health < this.localPlayer.health) this.damagedTimestamp = Date.now() / 1000;
+ this.localPlayer.health = remotePlayer.health;
+ this.localPlayer.idLastDamagedBy = remotePlayer.idLastDamagedBy;
+ this.localPlayer.inventory = remotePlayer.inventory;
+ this.localPlayer.playerSpectating = remotePlayer.playerSpectating;
+ this.localPlayer.gameMsgs = remotePlayer.gameMsgs;
+ this.localPlayer.gameMsgs2 = remotePlayer.gameMsgs2;
+ continue;
+ }
+ if (remotePlayer.chatActive)
+ this.messagesBeingTyped.push(`${remotePlayer.name}: ${remotePlayer.chatMsg}`);
+ }
+ }
+
+ private playersAreEqualEnough(player1: Player, player2: LastUploadedLocalPlayer | null) {
+ if (player1 === null || player2 === null) return false;
+ let out = true;
+ out = out && player1.position.equals(player2.position);
+ out = out && player1.quaternion.equals(player2.quaternion);
+ out = out && player1.chatMsg === player2.chatMsg;
+ out = out && player1.velocity.equals(player2.velocity);
+ out = out && player1.name === player2.name;
+
+ return out;
+ }
+
+ public getDamagedTimestamp() {
+ return this.damagedTimestamp;
+ }
+
+ public getMessagesBeingTyped() {
+ return this.messagesBeingTyped;
+ }
+
+ public getRemotePlayerData(): RemotePlayer[] {
+ return this.remotePlayers;
+ }
+
+ public sendMessage(msg: string) {
+ const chatMessage = {
+ message: msg,
+ id: this.localPlayer.id,
+ name: this.localPlayer.name
+ };
+ if (msg.length < 1) return;
+ if(chatMessage.message.startsWith('>')) chatMessage.message = '&2'+chatMessage.message;
+ if (msg.charAt(0) === '/'){
+ this.socket.emit('chatMsg', chatMessage);
+ return;
+ }
+ chatMessage.message = '&f' + chatMessage.message;
+ this.chatOverlay.addChatMessage(chatMessage);
+ this.socket.emit('chatMsg', chatMessage);
+ }
+
+ public applyDamage(id: number, damage: number) {
+ const player2 = this.remotePlayers.find(player => player.id === id);
+ const damageRequest = {
+ localPlayer: this.localPlayer,
+ targetPlayer: player2,
+ damage: damage
+ };
+ this.socket.emit('applyDamage', damageRequest);
+ }
+
+ public getWorldItemsData() {
+ return this.worldItems;
+ }
}
diff --git a/src/client/core/Player.ts b/src/client/core/Player.ts
index c88b55b..a875dee 100644
--- a/src/client/core/Player.ts
+++ b/src/client/core/Player.ts
@@ -1,55 +1,55 @@
import * as THREE from 'three';
-import { SettingsManager } from './SettingsManager.ts';
+import {SettingsManager} from "./SettingsManager.ts";
export class Player {
- public position: THREE.Vector3;
- public velocity: THREE.Vector3;
- public inputVelocity: THREE.Vector3;
- public gravity: number;
- public lookQuaternion: THREE.Quaternion;
- public quaternion: THREE.Quaternion;
- public id: number;
- public gameVersion: string;
- public name: string;
- public speed: number;
- public acceleration: number;
- public chatActive: boolean;
- public chatMsg: string;
- public latency: number;
- public health: number;
- public forced: boolean;
- public forcedAcknowledged: boolean;
- public inventory: number[];
- public idLastDamagedBy: number;
- public playerSpectating: number;
- public gameMsgs: string[];
- public gameMsgs2: string[];
+ public position: THREE.Vector3;
+ public velocity: THREE.Vector3;
+ public inputVelocity: THREE.Vector3;
+ public gravity: number;
+ public lookQuaternion: THREE.Quaternion;
+ public quaternion: THREE.Quaternion;
+ public id: number;
+ public gameVersion: string;
+ public name: string;
+ public speed: number;
+ public acceleration: number;
+ public chatActive: boolean;
+ public chatMsg: string;
+ public latency: number;
+ public health: number;
+ public forced: boolean;
+ public forcedAcknowledged: boolean;
+ public inventory: number[];
+ public idLastDamagedBy: number;
+ public playerSpectating: number;
+ public gameMsgs: string[];
+ public gameMsgs2: string[];
- constructor() {
- this.position = new THREE.Vector3(0, 100, 0);
- this.velocity = new THREE.Vector3(0, 0, 0);
- this.inputVelocity = new THREE.Vector3();
- this.gravity = 0;
- this.lookQuaternion = new THREE.Quaternion();
- this.quaternion = new THREE.Quaternion();
- this.id = Math.floor(Math.random() * 1000000000);
- this.gameVersion = '';
- this.name = '';
- this.speed = 5;
- this.acceleration = 100;
- this.chatActive = false;
- this.chatMsg = '';
- this.latency = 1000;
- this.health = 100;
- this.forced = false;
- this.forcedAcknowledged = false;
- this.inventory = [];
- this.idLastDamagedBy = -1;
- this.playerSpectating = -1;
- this.gameMsgs = [];
- this.gameMsgs2 = [];
+ constructor() {
+ this.position = new THREE.Vector3(0,100,0);
+ this.velocity = new THREE.Vector3(0,0,0);
+ this.inputVelocity = new THREE.Vector3();
+ this.gravity = 0;
+ this.lookQuaternion = new THREE.Quaternion();
+ this.quaternion = new THREE.Quaternion();
+ this.id = Math.floor(Math.random() * 1000000000);
+ this.gameVersion = '';
+ this.name = '';
+ this.speed = 5;
+ this.acceleration = 100;
+ this.chatActive = false;
+ this.chatMsg = '';
+ this.latency = 1000;
+ this.health = 100;
+ this.forced = false;
+ this.forcedAcknowledged = false;
+ this.inventory = [];
+ this.idLastDamagedBy = -1;
+ this.playerSpectating = -1;
+ this.gameMsgs = [];
+ this.gameMsgs2 = [];
- const storedName = SettingsManager.settings.name;
- if (storedName) this.name = storedName;
- }
-}
+ const storedName = SettingsManager.settings.name;
+ if (storedName) this.name = storedName;
+ }
+}
\ No newline at end of file
diff --git a/src/client/core/RemoteItemRenderer.ts b/src/client/core/RemoteItemRenderer.ts
index ddff5e2..b3e87ad 100644
--- a/src/client/core/RemoteItemRenderer.ts
+++ b/src/client/core/RemoteItemRenderer.ts
@@ -7,103 +7,94 @@ import { FishGun } from '../items/FishGun.ts';
// Custom types
type Vector3Data = {
- x: number;
- y: number;
- z: number;
+ x: number;
+ y: number;
+ z: number;
};
type WorldItemData = {
- id: number;
- itemType: number;
- vector: Vector3Data;
+ id: number;
+ itemType: number;
+ vector: Vector3Data;
};
type ItemsToRenderEntry = {
- id: number;
- item: ItemBase;
+ id: number;
+ item: ItemBase;
};
export class RemoteItemRenderer {
- private networking: Networking;
- private renderer: Renderer;
- private itemsToRender: ItemsToRenderEntry[] = [];
- private worldItemsData: WorldItemData[] = [];
+ private networking: Networking;
+ private renderer: Renderer;
+ private itemsToRender: ItemsToRenderEntry[] = [];
+ private worldItemsData: WorldItemData[] = [];
- constructor(networking: Networking, renderer: Renderer) {
- this.networking = networking;
- this.renderer = renderer;
- }
+ constructor(networking: Networking, renderer: Renderer) {
+ this.networking = networking;
+ this.renderer = renderer;
+ }
- public update() {
- // Get the latest world items data from networking
- const newWorldItemsData: WorldItemData[] = this.networking.getWorldItemsData();
+ public update() {
+ // Get the latest world items data from networking
+ const newWorldItemsData: WorldItemData[] = this.networking.getWorldItemsData();
- // Process the new data to update itemsToRender
- this.updateWorldItems(newWorldItemsData);
- }
+ // Process the new data to update itemsToRender
+ this.updateWorldItems(newWorldItemsData);
+ }
- private updateWorldItems(newWorldItemsData: WorldItemData[]) {
- // Update existing items and add new items
- newWorldItemsData.forEach((worldItemData) => {
- const existingItem = this.itemsToRender.find((item) => item.id === worldItemData.id);
- if (existingItem) {
- // Update position
- existingItem.item.setWorldPosition(
- new THREE.Vector3(
- worldItemData.vector.x,
- worldItemData.vector.y,
- worldItemData.vector.z,
- ),
- );
- } else {
- // Create new item
- const item = this.createItemByType(worldItemData.itemType);
- if (item) {
- item.setWorldPosition(
- new THREE.Vector3(
- worldItemData.vector.x,
- worldItemData.vector.y,
- worldItemData.vector.z,
- ),
- );
- this.itemsToRender.push({ id: worldItemData.id, item });
- }
- }
- });
+ private updateWorldItems(newWorldItemsData: WorldItemData[]) {
+ // Update existing items and add new items
+ newWorldItemsData.forEach((worldItemData) => {
+ const existingItem = this.itemsToRender.find(item => item.id === worldItemData.id);
+ if (existingItem) {
+ // Update position
+ existingItem.item.setWorldPosition(new THREE.Vector3(
+ worldItemData.vector.x,
+ worldItemData.vector.y,
+ worldItemData.vector.z
+ ));
+ } else {
+ // Create new item
+ const item = this.createItemByType(worldItemData.itemType);
+ if (item) {
+ item.setWorldPosition(new THREE.Vector3(
+ worldItemData.vector.x,
+ worldItemData.vector.y,
+ worldItemData.vector.z
+ ));
+ this.itemsToRender.push({ id: worldItemData.id, item });
+ }
+ }
+ });
- // Remove items that are no longer in the newWorldItemsData
- this.itemsToRender = this.itemsToRender.filter((item) => {
- const existsInNewData = newWorldItemsData.some((worldItemData) => worldItemData.id === item.id);
- if (!existsInNewData) {
- // Remove item from scene
- item.item.destroy();
- }
- return existsInNewData;
- });
- }
+ // Remove items that are no longer in the newWorldItemsData
+ this.itemsToRender = this.itemsToRender.filter(item => {
+ const existsInNewData = newWorldItemsData.some(worldItemData => worldItemData.id === item.id);
+ if (!existsInNewData) {
+ // Remove item from scene
+ item.item.destroy();
+ }
+ return existsInNewData;
+ });
+ }
- private createItemByType(itemType: number): ItemBase | null {
- // Create item based on itemType
- switch (itemType) {
- case 1:
- return new BananaGun(this.renderer, this.networking, 0, ItemType.WorldItem);
- case 2:
- return new FishGun(this.renderer, this.networking, 0, ItemType.WorldItem);
- default:
- // Return a generic item
- return new ItemBase(
- ItemType.WorldItem,
- this.renderer.getEntityScene(),
- this.renderer.getInventoryMenuScene(),
- 0,
- );
- }
- }
+ private createItemByType(itemType: number): ItemBase | null {
+ // Create item based on itemType
+ switch (itemType) {
+ case 1:
+ return new BananaGun(this.renderer, this.networking, 0, ItemType.WorldItem);
+ case 2:
+ return new FishGun(this.renderer, this.networking, 0, ItemType.WorldItem);
+ default:
+ // Return a generic item
+ return new ItemBase(ItemType.WorldItem, this.renderer.getEntityScene(), this.renderer.getInventoryMenuScene(), 0);
+ }
+ }
- public onFrame() {
- this.update();
- this.itemsToRender.forEach((itemEntry) => {
- itemEntry.item.onFrame(undefined, undefined); // Passing null for input and selectedIndex
- });
- }
+ public onFrame() {
+ this.update();
+ this.itemsToRender.forEach(itemEntry => {
+ itemEntry.item.onFrame(undefined, undefined); // Passing null for input and selectedIndex
+ });
+ }
}
diff --git a/src/client/core/RemotePlayerRenderer.ts b/src/client/core/RemotePlayerRenderer.ts
index 437ffd0..d180586 100644
--- a/src/client/core/RemotePlayerRenderer.ts
+++ b/src/client/core/RemotePlayerRenderer.ts
@@ -3,531 +3,521 @@ import { Networking } from './Networking.ts';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
import { Player } from './Player.ts';
-import { acceleratedRaycast, computeBoundsTree } from 'three-mesh-bvh';
+import {acceleratedRaycast, computeBoundsTree} from "three-mesh-bvh";
THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
THREE.Mesh.prototype.raycast = acceleratedRaycast;
interface RemotePlayerData {
- health: number;
- id: number;
- velocity: { x: number; y: number; z: number };
- position: { x: number; y: number; z: number };
- quaternion: [number, number, number, number]; // Add quaternion as required
- forced: boolean;
- name: string;
- playerSpectating: number;
+ health: number;
+ id: number;
+ velocity: { x: number; y: number; z: number };
+ position: { x: number; y: number; z: number };
+ quaternion: [number, number, number, number]; // Add quaternion as required
+ forced: boolean;
+ name: string;
+ playerSpectating: number;
}
interface RemotePlayer extends Omit {
- quaternion?: [number, number, number, number]; // Optional in case it's missing
+ quaternion?: [number, number, number, number]; // Optional in case it's missing
}
interface PlayerToRender {
- id: number;
- object: THREE.Object3D;
- objectUUID: string;
- sphere: THREE.Object3D;
- nameLabel: THREE.Sprite;
- name: string;
+ id: number;
+ object: THREE.Object3D;
+ objectUUID: string;
+ sphere: THREE.Object3D;
+ nameLabel: THREE.Sprite;
+ name: string;
}
export class RemotePlayerRenderer {
- private entityScene: THREE.Scene;
- private playersToRender: PlayerToRender[];
- private possumMesh: THREE.Mesh | undefined;
- private loader: GLTFLoader;
- private dracoLoader: DRACOLoader;
-
- private sphere: THREE.Mesh;
- private sphereScene: THREE.Scene;
-
- private raycaster: THREE.Raycaster;
- private camera: THREE.Camera;
- private scene: THREE.Scene;
-
- private isAnimating: { [id: number]: boolean };
- private animationPhase: { [id: number]: number };
- private previousVelocity: { [id: number]: number };
- private lastRunningYOffset: { [id: number]: number };
-
- private groundTruthPositions: { [id: number]: THREE.Vector3 };
-
- private networking: Networking;
- private localPlayer: Player;
- private deltaTime: number = 0; // Initialize deltaTime to avoid Deno error
- private static minVelocityToAnimate = 0.1;
- private static map: THREE.Mesh = new THREE.Mesh();
-
- private crosshairVec = new THREE.Vector2();
-
- constructor(
- networking: Networking,
- localPlayer: Player,
- raycaster: THREE.Raycaster,
- camera: THREE.Camera,
- scene: THREE.Scene,
- ) {
- this.networking = networking;
- this.localPlayer = localPlayer;
- this.raycaster = raycaster;
- this.camera = camera;
- this.scene = scene;
-
- this.entityScene = new THREE.Scene();
-
- this.sphere = new THREE.Mesh(new THREE.SphereGeometry(.6), new THREE.MeshBasicMaterial({ color: 0xffffff }));
- this.sphere.geometry.computeBoundsTree();
- this.sphereScene = new THREE.Scene();
-
- this.loader = new GLTFLoader();
- this.dracoLoader = new DRACOLoader();
- this.dracoLoader.setDecoderPath('/draco/');
- this.loader.setDRACOLoader(this.dracoLoader);
-
- this.possumMesh = undefined;
- this.loader.load(
- 'models/simplified_possum.glb',
- (gltf) => {
- console.time('Computing possum BVH');
- ( gltf.scene.children[0]).geometry.computeBoundsTree();
- console.timeEnd('Computing possum BVH');
- this.possumMesh = gltf.scene.children[0];
- },
- undefined,
- () => {
- console.log('possum loading error');
- },
- );
-
- this.playersToRender = [];
-
- this.isAnimating = {};
- this.animationPhase = {};
- this.previousVelocity = {};
- this.lastRunningYOffset = {};
-
- this.groundTruthPositions = {};
- }
-
- public getEntityScene(): THREE.Scene {
- return this.entityScene;
- }
-
- public update(deltaTime: number): void {
- this.deltaTime = deltaTime;
- this.updateRemotePlayers();
- }
-
- private updateRemotePlayers(): void {
- if (!this.possumMesh) return;
-
- const remotePlayerData: RemotePlayer[] = this.networking.getRemotePlayerData();
- const localPlayerId = this.localPlayer.id;
-
- // First, remove all players that should be hidden
- this.playersToRender = this.playersToRender.filter((player) => {
- const remotePlayer = remotePlayerData.find((rp) => rp.id === player.id);
- const shouldHide = !remotePlayer || // Player no longer exists
- remotePlayer.id === localPlayerId || // Is local player
- remotePlayer.id === this.localPlayer.playerSpectating || // Is being spectated
- remotePlayer.playerSpectating !== -1; // Is spectating someone
-
- if (shouldHide) {
- // Remove the player's objects from scenes
- this.entityScene.remove(player.object);
- this.entityScene.remove(player.nameLabel);
- this.sphereScene.remove(player.sphere);
-
- // Clean up associated data
- delete this.groundTruthPositions[player.id];
- delete this.isAnimating[player.id];
- delete this.animationPhase[player.id];
- delete this.previousVelocity[player.id];
- delete this.lastRunningYOffset[player.id];
- }
-
- return !shouldHide;
- });
-
- // Then, update or add remaining valid players
- remotePlayerData.forEach((remotePlayer) => {
- // Skip if player should be hidden
- if (
- remotePlayer.id === localPlayerId ||
- remotePlayer.id === this.localPlayer.playerSpectating ||
- remotePlayer.playerSpectating !== -1
- ) {
- return;
- }
-
- const playerDataWithQuaternion: RemotePlayerData = {
- ...remotePlayer,
- quaternion: remotePlayer.quaternion || [0, 0, 0, 1],
- };
-
- const existingPlayer = this.playersToRender.find((player) => player.id === remotePlayer.id);
- if (existingPlayer) {
- this.updatePlayerPosition(existingPlayer.object, existingPlayer.sphere, playerDataWithQuaternion);
- } else {
- this.addNewPlayer(playerDataWithQuaternion);
- }
- });
- }
-
- private updatePlayerPosition(
- playerObject: THREE.Object3D,
- playerSphere: THREE.Object3D,
- remotePlayerData: RemotePlayerData,
- ): void {
- const velocity = Math.sqrt(
- Math.pow(remotePlayerData.velocity.x, 2) +
- Math.pow(remotePlayerData.velocity.y, 2) +
- Math.pow(remotePlayerData.velocity.z, 2),
- );
-
- const playerId = remotePlayerData.id;
- const prevVelocity = this.previousVelocity[playerId] || 0;
-
- if (
- prevVelocity <= RemotePlayerRenderer.minVelocityToAnimate && velocity > RemotePlayerRenderer.minVelocityToAnimate
- ) {
- this.isAnimating[playerId] = true;
- this.animationPhase[playerId] = 0;
- }
-
- this.previousVelocity[playerId] = velocity;
-
- // Get or initialize groundTruthPosition
- if (!this.groundTruthPositions[playerId]) {
- this.groundTruthPositions[playerId] = new THREE.Vector3(
- remotePlayerData.position.x,
- remotePlayerData.position.y,
- remotePlayerData.position.z,
- );
- }
-
- const groundTruthPosition = this.groundTruthPositions[playerId];
-
- // Apply velocity to groundTruthPosition
- groundTruthPosition.x += remotePlayerData.velocity.x * this.deltaTime;
- groundTruthPosition.y += remotePlayerData.velocity.y * this.deltaTime;
- groundTruthPosition.z += remotePlayerData.velocity.z * this.deltaTime;
-
- // If forced, set groundTruthPosition to remotePlayerData.position
- if (remotePlayerData.forced) {
- groundTruthPosition.set(
- remotePlayerData.position.x,
- remotePlayerData.position.y,
- remotePlayerData.position.z,
- );
- }
-
- // Lerp groundTruthPosition towards remotePlayerData.position
- groundTruthPosition.lerp(
- new THREE.Vector3(
- remotePlayerData.position.x,
- remotePlayerData.position.y,
- remotePlayerData.position.z,
- ),
- 0.1 * this.deltaTime * 60,
- );
-
- // Set playerObject.position to groundTruthPosition.clone()
- playerObject.position.copy(groundTruthPosition);
- playerSphere.position.copy(groundTruthPosition.clone());
-
- // Apply animation offsets (e.g., yOffset)
- if (this.isAnimating[playerId]) {
- const frequency = 25;
- this.animationPhase[playerId] += this.deltaTime * frequency;
-
- const amplitude = 0.08;
- const yOffset = amplitude * (1 + Math.cos(this.animationPhase[playerId]));
-
- playerObject.position.y += yOffset;
- playerSphere.position.y += yOffset;
- this.lastRunningYOffset[playerId] = yOffset;
-
- if (velocity <= RemotePlayerRenderer.minVelocityToAnimate && Math.cos(this.animationPhase[playerId]) <= 0) {
- this.isAnimating[playerId] = false;
- this.lastRunningYOffset[playerId] = 0;
- }
- } else {
- this.lastRunningYOffset[playerId] = 0;
- }
-
- // Apply scared effect
- const scaredLevel = 1 - Math.pow(remotePlayerData.health / this.networking.getServerInfo().playerMaxHealth, 2); // 0-1
- playerObject.position.x += (Math.random() - 0.5) * 0.05 * scaredLevel;
- playerObject.position.y += (Math.random() - 0.5) * 0.05 * scaredLevel;
- playerObject.position.z += (Math.random() - 0.5) * 0.05 * scaredLevel;
-
- // Apply quaternion slerp as before
- const targetQuaternion = new THREE.Quaternion(
- remotePlayerData.quaternion[0],
- remotePlayerData.quaternion[1],
- remotePlayerData.quaternion[2],
- remotePlayerData.quaternion[3],
- );
- const rotationQuaternion = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
- targetQuaternion.multiply(rotationQuaternion);
-
- playerObject.quaternion.slerp(targetQuaternion, 0.5 * this.deltaTime * 60);
-
- // Update the position of the name label
- const player = this.playersToRender.find((p) => p.id === remotePlayerData.id);
- if (player) {
- player.nameLabel.position.set(
- playerObject.position.x,
- playerObject.position.y + 0.40, // Adjust the Y offset as needed
- playerObject.position.z,
- );
- player.nameLabel.lookAt(this.camera.position);
-
- // Check if the name has changed
- if (player.name !== remotePlayerData.name) {
- player.name = remotePlayerData.name; // Update stored name
- // Remove old label
- this.entityScene.remove(player.nameLabel);
- // Create and add new label
- player.nameLabel = this.createTextSprite(remotePlayerData.name.toString());
- player.nameLabel.position.set(
- playerObject.position.x,
- playerObject.position.y + 0.40,
- playerObject.position.z,
- );
- this.entityScene.add(player.nameLabel);
- }
- }
- }
-
- private addNewPlayer(remotePlayerData: RemotePlayerData): void {
- const object = this.possumMesh!.clone();
- const sphere = this.sphere.clone();
-
- // Create a text sprite for the player's name
- const nameLabel = this.createTextSprite(remotePlayerData.name.toString());
-
- const newPlayer: PlayerToRender = {
- id: remotePlayerData.id,
- object: object,
- objectUUID: object.uuid,
- sphere: sphere,
- nameLabel: nameLabel,
- name: remotePlayerData.name,
- };
-
- this.playersToRender.push(newPlayer);
- this.entityScene.add(newPlayer.object);
- this.sphereScene.add(newPlayer.sphere);
- this.entityScene.add(newPlayer.nameLabel);
-
- // Initialize groundTruthPosition for the new player
- this.groundTruthPositions[remotePlayerData.id] = new THREE.Vector3(
- remotePlayerData.position.x,
- remotePlayerData.position.y,
- remotePlayerData.position.z,
- );
- }
-
- private removeInactivePlayers(remotePlayerData: RemotePlayer[]): void {
- this.playersToRender = this.playersToRender.filter((player) => {
- const isActive = remotePlayerData.some((remotePlayer) => remotePlayer.id === player.id);
- if (!isActive) {
- this.entityScene.remove(player.object);
- this.entityScene.remove(player.nameLabel);
- this.sphereScene.remove(player.sphere);
- // Remove associated data for the player
- delete this.groundTruthPositions[player.id];
- delete this.isAnimating[player.id];
- delete this.animationPhase[player.id];
- delete this.previousVelocity[player.id];
- delete this.lastRunningYOffset[player.id];
- }
- return isActive;
- });
- }
-
- private createTextSprite(text: string): THREE.Sprite {
- text = text.replace(/&[0123456789abcdef]/g, ''); // Remove color codes
- const canvas = document.createElement('canvas');
- const context = canvas.getContext('2d')!;
- const fontSize = 64;
- context.font = `${fontSize}px Comic Sans MS`;
-
- // Measure the text width and set canvas size accordingly
- const textWidth = context.measureText(text).width;
- canvas.width = textWidth * 2; // Increase resolution
- canvas.height = fontSize * 2; // Increase resolution
-
- // Redraw the text on the canvas
- context.font = `${fontSize}px Comic Sans MS`;
- context.fillStyle = 'rgba(255,255,255,1)';
- context.textAlign = 'center';
- context.textBaseline = 'middle';
- context.fillText(text, canvas.width / 2, canvas.height / 2);
-
- const texture = new THREE.CanvasTexture(canvas);
- const spriteMaterial = new THREE.SpriteMaterial({ map: texture });
- const sprite = new THREE.Sprite(spriteMaterial);
-
- // Adjust the sprite scale to match the canvas aspect ratio
- sprite.scale.set((textWidth / fontSize) * 0.4, 0.4, 0.4);
-
- return sprite;
- }
-
- public getRemotePlayerIDsInCrosshair(): number[] {
- const shotVectors = this.getShotVectorsToPlayersInCrosshair();
- const playerIDs = shotVectors.map((shot) => shot.playerID);
- return playerIDs;
- }
-
- public getShotVectorsToPlayersInCrosshair(): { playerID: number; vector: THREE.Vector3; hitPoint: THREE.Vector3 }[] {
- const shotVectors: { playerID: number; vector: THREE.Vector3; hitPoint: THREE.Vector3 }[] = [];
- const objectsInCrosshair = this.getPlayersInCrosshairWithWalls();
-
- for (const object of objectsInCrosshair) {
- for (const player of this.playersToRender) {
- if (player.objectUUID === object.uuid) {
- // Find the intersection point on the player
- const intersection = this.findIntersectionOnPlayer(object);
- if (intersection) {
- const vector = new THREE.Vector3().subVectors(intersection.point, this.camera.position);
- const hitPoint = intersection.point.clone(); // World coordinates of the hit
- shotVectors.push({ playerID: player.id, vector, hitPoint });
- }
- break;
- }
- }
- }
-
- return shotVectors;
- }
-
- private findIntersectionOnPlayer(playerObject: THREE.Object3D): THREE.Intersection | null {
- this.raycaster.setFromCamera(this.crosshairVec, this.camera);
-
- const intersects = this.raycaster.intersectObject(playerObject, true);
- if (intersects.length > 0) {
- return intersects[0]; // Return the first intersection point
- }
- return null;
- }
-
- private getPlayersInCrosshairWithWalls(): THREE.Object3D[] {
- this.raycaster.setFromCamera(this.crosshairVec, this.camera);
-
- const playerIntersects = this.raycaster.intersectObjects(this.entityScene.children);
- this.raycaster.firstHitOnly = true;
- const wallIntersects = this.raycaster.intersectObjects([RemotePlayerRenderer.map]);
- this.raycaster.firstHitOnly = false;
-
- const filteredIntersects = playerIntersects.filter((playerIntersect) => {
- for (const wallIntersect of wallIntersects) {
- if (wallIntersect.distance < playerIntersect.distance) {
- return false;
- }
- }
- return true;
- });
-
- return filteredIntersects.map((intersect) => intersect.object);
- }
-
- public getPlayerSpheresInCrosshairWithWalls(): THREE.Object3D[] {
- this.raycaster.setFromCamera(this.crosshairVec, this.camera);
-
- this.sphereScene.updateMatrixWorld();
-
- this.raycaster.firstHitOnly = true;
- const playerIntersects = this.raycaster.intersectObjects(this.sphereScene.children);
- const wallIntersects = this.raycaster.intersectObjects([RemotePlayerRenderer.map]);
- this.raycaster.firstHitOnly = false;
-
- const filteredIntersects = playerIntersects.filter((playerIntersect) => {
- for (const wallIntersect of wallIntersects) {
- if (wallIntersect.distance < playerIntersect.distance) {
- return false;
- }
- }
- return true;
- });
-
- return filteredIntersects.map((intersect) => intersect.object);
- }
-
- public getShotVectorsToPlayersWithOffset(
- yawOffset: number,
- pitchOffset: number,
- ): { playerID: number; vector: THREE.Vector3; hitPoint: THREE.Vector3 }[] {
- const shotVectors: { playerID: number; vector: THREE.Vector3; hitPoint: THREE.Vector3 }[] = [];
- const offsetDirection = this.calculateOffsetDirection(yawOffset, pitchOffset);
-
- // Set the raycaster with the offset direction
- this.raycaster.set(this.camera.position, offsetDirection);
-
- // Intersect with all potential targets (players and walls)
- const playerIntersects = this.raycaster.intersectObjects(this.playersToRender.map((p) => p.object), true);
- this.raycaster.firstHitOnly = true;
- const wallIntersects = this.raycaster.intersectObjects([RemotePlayerRenderer.map]);
- this.raycaster.firstHitOnly = false;
-
- // Filter player intersections based on wall intersections
- const filteredPlayerIntersects = playerIntersects.filter((playerIntersect) => {
- for (const wallIntersect of wallIntersects) {
- if (wallIntersect.distance < playerIntersect.distance) {
- return false; // A wall is blocking the player
- }
- }
- return true; // No wall is blocking the player
- });
-
- // Process the filtered player intersections
- for (const intersect of filteredPlayerIntersects) {
- const player = this.playersToRender.find((p) => p.object === intersect.object);
- if (player) {
- const vector = new THREE.Vector3().subVectors(intersect.point, this.camera.position);
- const hitPoint = intersect.point.clone(); // World coordinates of the hit
- shotVectors.push({ playerID: player.id, vector, hitPoint });
- }
- }
-
- return shotVectors;
- }
-
- private calculateOffsetDirection(yawOffset: number, pitchOffset: number): THREE.Vector3 {
- // Get the camera's current direction
- const direction = new THREE.Vector3();
- this.camera.getWorldDirection(direction);
-
- // Create a quaternion for the yaw and pitch offsets
- const yawQuaternion = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yawOffset);
- const pitchQuaternion = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitchOffset);
-
- // Apply the rotations
- direction.applyQuaternion(yawQuaternion);
- direction.applyQuaternion(pitchQuaternion);
-
- return direction.normalize();
- }
-
- private findIntersectionOnPlayerWithOffset(
- playerObject: THREE.Object3D,
- offsetDirection: THREE.Vector3,
- ): THREE.Intersection | null {
- // Set the raycaster with the offset direction
- this.raycaster.set(this.camera.position, offsetDirection);
-
- const intersects = this.raycaster.intersectObject(playerObject, true);
- if (intersects.length > 0) {
- return intersects[0]; // Return the first intersection point
- }
- return null;
- }
-
- public static setMap(map: THREE.Mesh) {
- this.map = map;
- }
+ private entityScene: THREE.Scene;
+ private playersToRender: PlayerToRender[];
+ private possumMesh: THREE.Mesh | undefined;
+ private loader: GLTFLoader;
+ private dracoLoader: DRACOLoader;
+
+ private sphere: THREE.Mesh;
+ private sphereScene: THREE.Scene;
+
+ private raycaster: THREE.Raycaster;
+ private camera: THREE.Camera;
+ private scene: THREE.Scene
+
+ private isAnimating: { [id: number]: boolean };
+ private animationPhase: { [id: number]: number };
+ private previousVelocity: { [id: number]: number };
+ private lastRunningYOffset: { [id: number]: number };
+
+ private groundTruthPositions: { [id: number]: THREE.Vector3 };
+
+ private networking: Networking;
+ private localPlayer: Player;
+ private deltaTime: number = 0; // Initialize deltaTime to avoid Deno error
+ private static minVelocityToAnimate = 0.1;
+ private static map: THREE.Mesh = new THREE.Mesh();
+
+ private crosshairVec = new THREE.Vector2();
+
+ constructor(
+ networking: Networking,
+ localPlayer: Player,
+ raycaster: THREE.Raycaster,
+ camera: THREE.Camera,
+ scene: THREE.Scene
+ ) {
+ this.networking = networking;
+ this.localPlayer = localPlayer;
+ this.raycaster = raycaster;
+ this.camera = camera;
+ this.scene = scene;
+
+ this.entityScene = new THREE.Scene();
+
+ this.sphere = new THREE.Mesh(new THREE.SphereGeometry(.6), new THREE.MeshBasicMaterial({color: 0xffffff}));
+ this.sphere.geometry.computeBoundsTree();
+ this.sphereScene = new THREE.Scene();
+
+ this.loader = new GLTFLoader();
+ this.dracoLoader = new DRACOLoader();
+ this.dracoLoader.setDecoderPath('/draco/');
+ this.loader.setDRACOLoader(this.dracoLoader);
+
+ this.possumMesh = undefined;
+ this.loader.load(
+ 'models/simplified_possum.glb',
+ (gltf) => {
+ console.time("Computing possum BVH");
+ (gltf.scene.children[0]).geometry.computeBoundsTree();
+ console.timeEnd("Computing possum BVH");
+ this.possumMesh = (gltf.scene.children[0]);
+ },
+ undefined,
+ () => {
+ console.log('possum loading error');
+ }
+ );
+
+ this.playersToRender = [];
+
+ this.isAnimating = {};
+ this.animationPhase = {};
+ this.previousVelocity = {};
+ this.lastRunningYOffset = {};
+
+ this.groundTruthPositions = {};
+ }
+
+ public getEntityScene(): THREE.Scene {
+ return this.entityScene;
+ }
+
+ public update(deltaTime: number): void {
+ this.deltaTime = deltaTime;
+ this.updateRemotePlayers();
+ }
+
+ private updateRemotePlayers(): void {
+ if (!this.possumMesh) return;
+
+ const remotePlayerData: RemotePlayer[] = this.networking.getRemotePlayerData();
+ const localPlayerId = this.localPlayer.id;
+
+ // First, remove all players that should be hidden
+ this.playersToRender = this.playersToRender.filter((player) => {
+ const remotePlayer = remotePlayerData.find(rp => rp.id === player.id);
+ const shouldHide =
+ !remotePlayer || // Player no longer exists
+ remotePlayer.id === localPlayerId || // Is local player
+ remotePlayer.id === this.localPlayer.playerSpectating || // Is being spectated
+ remotePlayer.playerSpectating !== -1; // Is spectating someone
+
+ if (shouldHide) {
+ // Remove the player's objects from scenes
+ this.entityScene.remove(player.object);
+ this.entityScene.remove(player.nameLabel);
+ this.sphereScene.remove(player.sphere);
+
+ // Clean up associated data
+ delete this.groundTruthPositions[player.id];
+ delete this.isAnimating[player.id];
+ delete this.animationPhase[player.id];
+ delete this.previousVelocity[player.id];
+ delete this.lastRunningYOffset[player.id];
+ }
+
+ return !shouldHide;
+ });
+
+ // Then, update or add remaining valid players
+ remotePlayerData.forEach((remotePlayer) => {
+ // Skip if player should be hidden
+ if (remotePlayer.id === localPlayerId ||
+ remotePlayer.id === this.localPlayer.playerSpectating ||
+ remotePlayer.playerSpectating !== -1) {
+ return;
+ }
+
+ const playerDataWithQuaternion: RemotePlayerData = {
+ ...remotePlayer,
+ quaternion: remotePlayer.quaternion || [0, 0, 0, 1],
+ };
+
+ const existingPlayer = this.playersToRender.find((player) => player.id === remotePlayer.id);
+ if (existingPlayer) {
+ this.updatePlayerPosition(existingPlayer.object, existingPlayer.sphere, playerDataWithQuaternion);
+ } else {
+ this.addNewPlayer(playerDataWithQuaternion);
+ }
+ });
+ }
+
+
+ private updatePlayerPosition(playerObject: THREE.Object3D, playerSphere: THREE.Object3D, remotePlayerData: RemotePlayerData): void {
+ const velocity = Math.sqrt(
+ Math.pow(remotePlayerData.velocity.x, 2) +
+ Math.pow(remotePlayerData.velocity.y, 2) +
+ Math.pow(remotePlayerData.velocity.z, 2)
+ );
+
+ const playerId = remotePlayerData.id;
+ const prevVelocity = this.previousVelocity[playerId] || 0;
+
+ if (prevVelocity <= RemotePlayerRenderer.minVelocityToAnimate && velocity > RemotePlayerRenderer.minVelocityToAnimate) {
+ this.isAnimating[playerId] = true;
+ this.animationPhase[playerId] = 0;
+ }
+
+ this.previousVelocity[playerId] = velocity;
+
+ // Get or initialize groundTruthPosition
+ if (!this.groundTruthPositions[playerId]) {
+ this.groundTruthPositions[playerId] = new THREE.Vector3(
+ remotePlayerData.position.x,
+ remotePlayerData.position.y,
+ remotePlayerData.position.z
+ );
+ }
+
+ const groundTruthPosition = this.groundTruthPositions[playerId];
+
+ // Apply velocity to groundTruthPosition
+ groundTruthPosition.x += remotePlayerData.velocity.x * this.deltaTime;
+ groundTruthPosition.y += remotePlayerData.velocity.y * this.deltaTime;
+ groundTruthPosition.z += remotePlayerData.velocity.z * this.deltaTime;
+
+ // If forced, set groundTruthPosition to remotePlayerData.position
+ if (remotePlayerData.forced) {
+ groundTruthPosition.set(
+ remotePlayerData.position.x,
+ remotePlayerData.position.y,
+ remotePlayerData.position.z
+ );
+ }
+
+ // Lerp groundTruthPosition towards remotePlayerData.position
+ groundTruthPosition.lerp(
+ new THREE.Vector3(
+ remotePlayerData.position.x,
+ remotePlayerData.position.y,
+ remotePlayerData.position.z
+ ),
+ 0.1 * this.deltaTime * 60
+ );
+
+ // Set playerObject.position to groundTruthPosition.clone()
+ playerObject.position.copy(groundTruthPosition);
+ playerSphere.position.copy(groundTruthPosition.clone());
+
+ // Apply animation offsets (e.g., yOffset)
+ if (this.isAnimating[playerId]) {
+ const frequency = 25;
+ this.animationPhase[playerId] += this.deltaTime * frequency;
+
+ const amplitude = 0.08;
+ const yOffset = amplitude * (1 + Math.cos(this.animationPhase[playerId]));
+
+ playerObject.position.y += yOffset;
+ playerSphere.position.y += yOffset;
+ this.lastRunningYOffset[playerId] = yOffset;
+
+ if (velocity <= RemotePlayerRenderer.minVelocityToAnimate && Math.cos(this.animationPhase[playerId]) <= 0) {
+ this.isAnimating[playerId] = false;
+ this.lastRunningYOffset[playerId] = 0;
+ }
+ } else {
+ this.lastRunningYOffset[playerId] = 0;
+ }
+
+ // Apply scared effect
+ const scaredLevel = 1 - Math.pow(remotePlayerData.health / this.networking.getServerInfo().playerMaxHealth, 2); // 0-1
+ playerObject.position.x += (Math.random() - 0.5) * 0.05 * scaredLevel;
+ playerObject.position.y += (Math.random() - 0.5) * 0.05 * scaredLevel;
+ playerObject.position.z += (Math.random() - 0.5) * 0.05 * scaredLevel;
+
+ // Apply quaternion slerp as before
+ const targetQuaternion = new THREE.Quaternion(
+ remotePlayerData.quaternion[0],
+ remotePlayerData.quaternion[1],
+ remotePlayerData.quaternion[2],
+ remotePlayerData.quaternion[3]
+ );
+ const rotationQuaternion = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
+ targetQuaternion.multiply(rotationQuaternion);
+
+ playerObject.quaternion.slerp(targetQuaternion, 0.5 * this.deltaTime * 60);
+
+ // Update the position of the name label
+ const player = this.playersToRender.find(p => p.id === remotePlayerData.id);
+ if (player) {
+ player.nameLabel.position.set(
+ playerObject.position.x,
+ playerObject.position.y + 0.40, // Adjust the Y offset as needed
+ playerObject.position.z
+ );
+ player.nameLabel.lookAt(this.camera.position);
+
+ // Check if the name has changed
+ if (player.name !== remotePlayerData.name) {
+ player.name = remotePlayerData.name; // Update stored name
+ // Remove old label
+ this.entityScene.remove(player.nameLabel);
+ // Create and add new label
+ player.nameLabel = this.createTextSprite(remotePlayerData.name.toString());
+ player.nameLabel.position.set(
+ playerObject.position.x,
+ playerObject.position.y + 0.40,
+ playerObject.position.z
+ );
+ this.entityScene.add(player.nameLabel);
+ }
+ }
+ }
+
+ private addNewPlayer(remotePlayerData: RemotePlayerData): void {
+ const object = this.possumMesh!.clone();
+ const sphere = this.sphere.clone();
+
+ // Create a text sprite for the player's name
+ const nameLabel = this.createTextSprite(remotePlayerData.name.toString());
+
+ const newPlayer: PlayerToRender = {
+ id: remotePlayerData.id,
+ object: object,
+ objectUUID: object.uuid,
+ sphere: sphere,
+ nameLabel: nameLabel,
+ name: remotePlayerData.name,
+ };
+
+ this.playersToRender.push(newPlayer);
+ this.entityScene.add(newPlayer.object);
+ this.sphereScene.add(newPlayer.sphere);
+ this.entityScene.add(newPlayer.nameLabel);
+
+ // Initialize groundTruthPosition for the new player
+ this.groundTruthPositions[remotePlayerData.id] = new THREE.Vector3(
+ remotePlayerData.position.x,
+ remotePlayerData.position.y,
+ remotePlayerData.position.z
+ );
+ }
+
+ private removeInactivePlayers(remotePlayerData: RemotePlayer[]): void {
+ this.playersToRender = this.playersToRender.filter((player) => {
+ const isActive = remotePlayerData.some((remotePlayer) => remotePlayer.id === player.id);
+ if (!isActive) {
+ this.entityScene.remove(player.object);
+ this.entityScene.remove(player.nameLabel);
+ this.sphereScene.remove(player.sphere);
+ // Remove associated data for the player
+ delete this.groundTruthPositions[player.id];
+ delete this.isAnimating[player.id];
+ delete this.animationPhase[player.id];
+ delete this.previousVelocity[player.id];
+ delete this.lastRunningYOffset[player.id];
+ }
+ return isActive;
+ });
+ }
+
+ private createTextSprite(text: string): THREE.Sprite {
+ text = text.replace(/&[0123456789abcdef]/g, ''); // Remove color codes
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d')!;
+ const fontSize = 64;
+ context.font = `${fontSize}px Comic Sans MS`;
+
+ // Measure the text width and set canvas size accordingly
+ const textWidth = context.measureText(text).width;
+ canvas.width = textWidth * 2; // Increase resolution
+ canvas.height = fontSize * 2; // Increase resolution
+
+ // Redraw the text on the canvas
+ context.font = `${fontSize}px Comic Sans MS`;
+ context.fillStyle = 'rgba(255,255,255,1)';
+ context.textAlign = 'center';
+ context.textBaseline = 'middle';
+ context.fillText(text, canvas.width / 2, canvas.height / 2);
+
+ const texture = new THREE.CanvasTexture(canvas);
+ const spriteMaterial = new THREE.SpriteMaterial({ map: texture });
+ const sprite = new THREE.Sprite(spriteMaterial);
+
+ // Adjust the sprite scale to match the canvas aspect ratio
+ sprite.scale.set((textWidth / fontSize ) * 0.4 , 0.4, 0.4);
+
+ return sprite;
+ }
+
+
+ public getRemotePlayerIDsInCrosshair(): number[] {
+ const shotVectors = this.getShotVectorsToPlayersInCrosshair();
+ const playerIDs = shotVectors.map(shot => shot.playerID);
+ return playerIDs;
+ }
+
+ public getShotVectorsToPlayersInCrosshair(): { playerID: number, vector: THREE.Vector3, hitPoint: THREE.Vector3 }[] {
+ const shotVectors: { playerID: number, vector: THREE.Vector3, hitPoint: THREE.Vector3 }[] = [];
+ const objectsInCrosshair = this.getPlayersInCrosshairWithWalls();
+
+ for (const object of objectsInCrosshair) {
+ for (const player of this.playersToRender) {
+ if (player.objectUUID === object.uuid) {
+ // Find the intersection point on the player
+ const intersection = this.findIntersectionOnPlayer(object);
+ if (intersection) {
+ const vector = new THREE.Vector3().subVectors(intersection.point, this.camera.position);
+ const hitPoint = intersection.point.clone(); // World coordinates of the hit
+ shotVectors.push({ playerID: player.id, vector, hitPoint });
+ }
+ break;
+ }
+ }
+ }
+
+ return shotVectors;
+ }
+
+ private findIntersectionOnPlayer(playerObject: THREE.Object3D): THREE.Intersection | null {
+ this.raycaster.setFromCamera(this.crosshairVec, this.camera);
+
+ const intersects = this.raycaster.intersectObject(playerObject, true);
+ if (intersects.length > 0) {
+ return intersects[0]; // Return the first intersection point
+ }
+ return null;
+ }
+
+ private getPlayersInCrosshairWithWalls(): THREE.Object3D[] {
+ this.raycaster.setFromCamera(this.crosshairVec, this.camera);
+
+ const playerIntersects = this.raycaster.intersectObjects(this.entityScene.children);
+ this.raycaster.firstHitOnly = true;
+ const wallIntersects = this.raycaster.intersectObjects([RemotePlayerRenderer.map]);
+ this.raycaster.firstHitOnly = false;
+
+ const filteredIntersects = playerIntersects.filter((playerIntersect) => {
+ for (const wallIntersect of wallIntersects) {
+ if (wallIntersect.distance < playerIntersect.distance) {
+ return false;
+ }
+ }
+ return true;
+ });
+
+ return filteredIntersects.map((intersect) => intersect.object);
+ }
+
+ public getPlayerSpheresInCrosshairWithWalls(): THREE.Object3D[] {
+ this.raycaster.setFromCamera(this.crosshairVec, this.camera);
+
+ this.sphereScene.updateMatrixWorld();
+
+ this.raycaster.firstHitOnly = true;
+ const playerIntersects = this.raycaster.intersectObjects(this.sphereScene.children);
+ const wallIntersects = this.raycaster.intersectObjects([RemotePlayerRenderer.map]);
+ this.raycaster.firstHitOnly = false;
+
+ const filteredIntersects = playerIntersects.filter((playerIntersect) => {
+ for (const wallIntersect of wallIntersects) {
+ if (wallIntersect.distance < playerIntersect.distance) {
+ return false;
+ }
+ }
+ return true;
+ });
+
+ return filteredIntersects.map((intersect) => intersect.object);
+ }
+
+ public getShotVectorsToPlayersWithOffset(yawOffset: number, pitchOffset: number): { playerID: number, vector: THREE.Vector3, hitPoint: THREE.Vector3 }[] {
+ const shotVectors: { playerID: number, vector: THREE.Vector3, hitPoint: THREE.Vector3 }[] = [];
+ const offsetDirection = this.calculateOffsetDirection(yawOffset, pitchOffset);
+
+ // Set the raycaster with the offset direction
+ this.raycaster.set(this.camera.position, offsetDirection);
+
+ // Intersect with all potential targets (players and walls)
+ const playerIntersects = this.raycaster.intersectObjects(this.playersToRender.map(p => p.object), true);
+ this.raycaster.firstHitOnly = true;
+ const wallIntersects = this.raycaster.intersectObjects([RemotePlayerRenderer.map]);
+ this.raycaster.firstHitOnly = false;
+
+ // Filter player intersections based on wall intersections
+ const filteredPlayerIntersects = playerIntersects.filter((playerIntersect) => {
+ for (const wallIntersect of wallIntersects) {
+ if (wallIntersect.distance < playerIntersect.distance) {
+ return false; // A wall is blocking the player
+ }
+ }
+ return true; // No wall is blocking the player
+ });
+
+ // Process the filtered player intersections
+ for (const intersect of filteredPlayerIntersects) {
+ const player = this.playersToRender.find(p => p.object === intersect.object);
+ if (player) {
+ const vector = new THREE.Vector3().subVectors(intersect.point, this.camera.position);
+ const hitPoint = intersect.point.clone(); // World coordinates of the hit
+ shotVectors.push({ playerID: player.id, vector, hitPoint });
+ }
+ }
+
+ return shotVectors;
+ }
+
+
+ private calculateOffsetDirection(yawOffset: number, pitchOffset: number): THREE.Vector3 {
+ // Get the camera's current direction
+ const direction = new THREE.Vector3();
+ this.camera.getWorldDirection(direction);
+
+ // Create a quaternion for the yaw and pitch offsets
+ const yawQuaternion = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yawOffset);
+ const pitchQuaternion = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitchOffset);
+
+ // Apply the rotations
+ direction.applyQuaternion(yawQuaternion);
+ direction.applyQuaternion(pitchQuaternion);
+
+ return direction.normalize();
+ }
+
+ private findIntersectionOnPlayerWithOffset(playerObject: THREE.Object3D, offsetDirection: THREE.Vector3): THREE.Intersection | null {
+ // Set the raycaster with the offset direction
+ this.raycaster.set(this.camera.position, offsetDirection);
+
+ const intersects = this.raycaster.intersectObject(playerObject, true);
+ if (intersects.length > 0) {
+ return intersects[0]; // Return the first intersection point
+ }
+ return null;
+ }
+
+ public static setMap(map: THREE.Mesh) {
+ this.map = map;
+ }
}
diff --git a/src/client/core/Renderer.ts b/src/client/core/Renderer.ts
index 6a46c16..58fd3b8 100644
--- a/src/client/core/Renderer.ts
+++ b/src/client/core/Renderer.ts
@@ -1,415 +1,401 @@
import * as THREE from 'three';
import { Networking, type RemotePlayer } from './Networking.ts';
import { Player } from './Player.ts';
-import { ChatOverlay } from '../ui/ChatOverlay.ts';
+import { ChatOverlay } from "../ui/ChatOverlay.ts";
import { RemotePlayerRenderer } from './RemotePlayerRenderer.ts';
-import { InputHandler } from '../input/InputHandler.ts';
-import { SettingsManager } from './SettingsManager.ts';
-import { CollisionManager } from '../input/CollisionManager.ts';
+import {InputHandler} from "../input/InputHandler.ts";
+import {SettingsManager} from "./SettingsManager.ts";
+import {CollisionManager} from "../input/CollisionManager.ts";
export class Renderer {
- private clock: THREE.Clock;
- private deltaTime: number = 0;
- private chatOverlay: ChatOverlay;
- private scene: THREE.Scene;
- private camera: THREE.PerspectiveCamera;
- private renderer: THREE.WebGLRenderer;
- private heldItemScene: THREE.Scene;
- private heldItemCamera: THREE.PerspectiveCamera;
- private ambientLight: THREE.AmbientLight;
- private framerate: number;
- private framesInFramerateSample: number;
- private sampleOn: number;
- private lastFramerateCalculation: number;
- private networking: Networking;
- private localPlayer: Player;
- private raycaster: THREE.Raycaster;
- public scaredLevel: number = 0;
- private lastPlayerHealth: number = 100;
- private knockbackVector: THREE.Vector3 = new THREE.Vector3();
- private bobCycle: number;
- private lastCameraRoll: number;
-
- public crosshairIsFlashing: boolean = false;
- public lastShotSomeoneTimestamp: number = 0;
- public playerHitMarkers: { hitPoint: THREE.Vector3; shotVector: THREE.Vector3; timestamp: number }[] = [];
- private healthIndicatorScene: THREE.Scene;
- private healthIndicatorCamera: THREE.PerspectiveCamera;
- private screenPixelsInGamePixel: number = 1;
- private inventoryMenuScene: THREE.Scene;
- private inventoryMenuCamera: THREE.OrthographicCamera;
- private remotePlayerRenderer: RemotePlayerRenderer;
- private inputHandler!: InputHandler;
- private collisionManager!: CollisionManager;
-
- private spectateGroundTruthPosition: THREE.Vector3 | null = null;
-
- constructor(container: HTMLElement, networking: Networking, localPlayer: Player, chatOverlay: ChatOverlay) {
- this.networking = networking;
- this.localPlayer = localPlayer;
- this.chatOverlay = chatOverlay;
-
- this.clock = new THREE.Clock();
- this.scene = new THREE.Scene();
- this.camera = new THREE.PerspectiveCamera(90, globalThis.innerWidth / globalThis.innerHeight, 0.01, 1000);
- this.renderer = new THREE.WebGLRenderer();
- //document.body.appendChild(this.renderer.domElement);
- container.appendChild(this.renderer.domElement);
- this.renderer.domElement.style.imageRendering = 'pixelated';
- this.renderer.setAnimationLoop(null);
-
- // Create a new scene and camera for the held item
- this.heldItemScene = new THREE.Scene();
- this.heldItemCamera = new THREE.PerspectiveCamera(90, globalThis.innerWidth / globalThis.innerHeight, 0.01, 1000);
- this.heldItemCamera.position.set(0, 0, 5);
- this.heldItemCamera.lookAt(0, 0, 0);
-
- // Create a new scene and camera for the health indicator
- this.healthIndicatorScene = new THREE.Scene();
- this.healthIndicatorCamera = new THREE.PerspectiveCamera(70, 1, 0.01, 1000);
- this.healthIndicatorCamera.position.set(0, 0, 0);
- this.healthIndicatorCamera.lookAt(0, 0, 1);
-
- this.inventoryMenuScene = new THREE.Scene();
- this.inventoryMenuCamera = new THREE.OrthographicCamera(-0.5, 0.5, 2.5, -2.5, 0.01, 10);
- this.inventoryMenuCamera.position.set(0, 0, 5);
- this.inventoryMenuCamera.lookAt(0, 0, 0);
- this.inventoryMenuScene.add(this.inventoryMenuCamera);
- this.inventoryMenuScene.add(new THREE.AmbientLight(0xffffff, 0.5));
-
- // Ambient lights
- this.ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
- const ambientLight2 = new THREE.AmbientLight(0xffffff, 0.5);
- const ambientLight3 = new THREE.AmbientLight(0xffffff, 0.5); // Ambient light for remote players scene
-
- this.scene.add(this.ambientLight);
- this.heldItemScene.add(ambientLight2);
-
- // Fog settings
- this.scene.fog = new THREE.FogExp2('#111111', 0.05);
- this.heldItemScene.fog = new THREE.FogExp2('#111111', 0.05);
- this.healthIndicatorScene.fog = new THREE.FogExp2('#111111', 0.05); // Add fog to health indicator scene
-
- this.framerate = 0;
- this.framesInFramerateSample = 30;
- this.sampleOn = 0;
- this.lastFramerateCalculation = 0;
-
- this.bobCycle = 0;
- this.lastCameraRoll = 0;
-
- this.raycaster = new THREE.Raycaster();
-
- // Initialize remotePlayerRenderer
- this.remotePlayerRenderer = new RemotePlayerRenderer(
- this.networking,
- this.localPlayer,
- this.raycaster,
- this.camera,
- this.scene,
- );
- this.remotePlayerRenderer.getEntityScene().fog = new THREE.FogExp2('#111111', 0.1); // Add fog to remote players scene
- this.remotePlayerRenderer.getEntityScene().add(ambientLight3); // Add ambient light to remote players scene
- this.renderer.domElement.style.touchAction = 'none';
- this.renderer.domElement.style.position = 'absolute';
- this.onWindowResize();
- globalThis.addEventListener('resize', this.onWindowResize.bind(this), false);
- globalThis.addEventListener('orientationchange', this.onWindowResize.bind(this), false);
- }
-
- public onFrame(localPlayer: Player) {
- this.deltaTime = this.clock.getDelta();
-
- // Ensure the renderer clears the buffers before the first render
- this.renderer.autoClear = true;
-
- // Render the main scene
- this.renderer.render(this.scene, this.camera);
-
- // Prevent clearing the buffers in subsequent renders
- this.renderer.autoClear = false;
-
- // Update and render remote players
- this.remotePlayerRenderer.update(this.deltaTime);
- this.renderer.render(this.remotePlayerRenderer.getEntityScene(), this.camera);
-
- // Render the held item scene normally (full screen)
- this.renderer.render(this.heldItemScene, this.heldItemCamera);
-
- // Set up the scissor and viewport for the health indicator scene rendering
- const screenWidth = globalThis.innerWidth;
- const screenHeight = globalThis.innerHeight;
-
- const healthIndicatorWidth = 60; // native
- const healthIndicatorHeight = healthIndicatorWidth; // 1:1 aspect ratio
-
- // Set up scissor and viewport for a region from (0, 0) to (50, 50)
- this.renderer.setScissorTest(true);
- this.renderer.setScissor(
- 2 * this.screenPixelsInGamePixel,
- screenHeight - (healthIndicatorHeight + 1 + this.chatOverlay.getDebugTextHeight()) * this.screenPixelsInGamePixel,
- healthIndicatorWidth * this.screenPixelsInGamePixel,
- healthIndicatorHeight * this.screenPixelsInGamePixel,
- );
- this.renderer.setViewport(
- 2 * this.screenPixelsInGamePixel,
- screenHeight - (healthIndicatorHeight + 1 + this.chatOverlay.getDebugTextHeight()) * this.screenPixelsInGamePixel,
- healthIndicatorWidth * this.screenPixelsInGamePixel,
- healthIndicatorHeight * this.screenPixelsInGamePixel,
- );
-
- // Render the health indicator scene
- this.renderer.render(this.healthIndicatorScene, this.healthIndicatorCamera);
-
- // Render inventory view
- const inventoryWidth = 20;
- const inventoryHeight = inventoryWidth * 5;
- this.renderer.setScissorTest(true);
- this.renderer.setScissor(
- screenWidth - (inventoryWidth + 4) * this.screenPixelsInGamePixel,
- screenHeight / 2 - (inventoryHeight / 2) * this.screenPixelsInGamePixel,
- inventoryWidth * this.screenPixelsInGamePixel,
- inventoryHeight * this.screenPixelsInGamePixel,
- );
- this.renderer.setViewport(
- screenWidth - (inventoryWidth + 4) * this.screenPixelsInGamePixel,
- screenHeight / 2 - (inventoryHeight / 2) * this.screenPixelsInGamePixel,
- inventoryWidth * this.screenPixelsInGamePixel,
- inventoryHeight * this.screenPixelsInGamePixel,
- );
- this.renderer.render(this.inventoryMenuScene, this.inventoryMenuCamera);
-
- // Reset scissor test and viewport after rendering the health indicator
- this.renderer.setScissorTest(false);
- this.renderer.setViewport(0, 0, screenWidth, screenHeight);
-
- // Restore autoClear to true if necessary
- this.renderer.autoClear = true;
-
- if (localPlayer.playerSpectating !== -1) {
- const remotePlayer = this.networking.getRemotePlayerData().find((player) =>
- player.id === localPlayer.playerSpectating
- );
- if (remotePlayer !== undefined) {
- // Initialize ground truth position if not set
- if (!this.spectateGroundTruthPosition) {
- this.spectateGroundTruthPosition = new THREE.Vector3(
- remotePlayer.position.x,
- remotePlayer.position.y,
- remotePlayer.position.z,
- );
- }
-
- // Update ground truth position based on velocity
- this.spectateGroundTruthPosition.x += remotePlayer.velocity.x * this.deltaTime;
- this.spectateGroundTruthPosition.y += remotePlayer.velocity.y * this.deltaTime;
- this.spectateGroundTruthPosition.z += remotePlayer.velocity.z * this.deltaTime;
-
- // If forced update, set directly to remote position
- if (remotePlayer.forced) {
- this.spectateGroundTruthPosition.set(
- remotePlayer.position.x,
- remotePlayer.position.y,
- remotePlayer.position.z,
- );
- }
-
- // Lerp ground truth position towards actual position
- this.spectateGroundTruthPosition.lerp(
- new THREE.Vector3(
- remotePlayer.position.x,
- remotePlayer.position.y,
- remotePlayer.position.z,
- ),
- 0.1 * this.deltaTime * 60,
- );
-
- // Update camera position and rotation
- this.camera.position.copy(this.spectateGroundTruthPosition);
-
- // Simple quaternion slerp
- this.camera.quaternion.slerp(
- new THREE.Quaternion(
- remotePlayer.lookQuaternion[0],
- remotePlayer.lookQuaternion[1],
- remotePlayer.lookQuaternion[2],
- remotePlayer.lookQuaternion[3],
- ),
- 0.3 * this.deltaTime * 60,
- );
- }
- } else {
- // Reset spectate position when not spectating
- this.spectateGroundTruthPosition = null;
- this.camera.position.copy(localPlayer.position);
- this.camera.setRotationFromQuaternion(this.localPlayer.lookQuaternion);
- }
-
- this.camera.position.add(this.knockbackVector);
- this.knockbackVector.lerp(new THREE.Vector3(), 0.05 * this.deltaTime * 60);
-
- if (this.localPlayer.health < this.lastPlayerHealth) {
- const remotePlayer: RemotePlayer | undefined = this.networking.getRemotePlayerData().find((player) =>
- player.id === this.localPlayer.idLastDamagedBy
- );
- if (remotePlayer !== undefined) {
- //console.log("Player was damaged by " + remotePlayer.name);
- const diff = new THREE.Vector3().subVectors(this.localPlayer.position, remotePlayer.position);
- this.knockbackVector.copy(diff.normalize().multiplyScalar(0.2));
- }
- }
-
- const shakeAmount = 0.08 * Math.pow(this.scaredLevel, 5);
- this.camera.position.add(
- new THREE.Vector3(
- (Math.random() - 0.5) * shakeAmount,
- (Math.random() - 0.5) * shakeAmount,
- (Math.random() - 0.5) * shakeAmount,
- ),
- );
- this.camera.rotation.x += (Math.random() - 0.5) * shakeAmount * 0.12;
- this.camera.rotation.y += (Math.random() - 0.5) * shakeAmount * 0.12;
- this.camera.rotation.z += (Math.random() - 0.5) * shakeAmount * 0.12;
-
- this.heldItemCamera.rotation.set(
- (Math.random() - 0.5) * shakeAmount,
- (Math.random() - 0.5) * shakeAmount,
- (Math.random() - 0.5) * shakeAmount,
- );
-
- this.lastPlayerHealth = this.localPlayer.health;
-
- const vel = Math.sqrt(
- Math.pow(this.localPlayer.inputVelocity.x, 2) + Math.pow(this.localPlayer.inputVelocity.z, 2),
- );
-
- if (vel == 0 || this.collisionManager.isPlayerInAir() || this.localPlayer.playerSpectating !== -1) {
- this.bobCycle = 0;
- } else {
- this.bobCycle += this.deltaTime * 4.8 * vel;
- this.camera.position.y = this.camera.position.y +
- (Math.sin(this.bobCycle) * .03 * SettingsManager.settings.viewBobbingStrength);
- //console.log(this.camera.position.y);
- }
-
- let newHandX = Math.sin(this.bobCycle / 1.9) * .02 * SettingsManager.settings.viewBobbingStrength;
- let newHandY = -(Math.sin(this.bobCycle) * .07 * SettingsManager.settings.viewBobbingStrength);
- let newHandZ = Math.sin(this.bobCycle / 1.8) * .015 * SettingsManager.settings.viewBobbingStrength;
- newHandY += localPlayer.velocity.y * 0.04 * SettingsManager.settings.viewBobbingStrength; //move hand up when falling, down when jumping
-
- //banana lags behind player slightly
- const playerVelocity = new THREE.Vector3().copy(localPlayer.velocity);
- playerVelocity.applyQuaternion(localPlayer.lookQuaternion.clone().invert());
- newHandX += playerVelocity.x * 0.02 * SettingsManager.settings.viewBobbingStrength;
- newHandZ -= -playerVelocity.z * 0.02 * SettingsManager.settings.viewBobbingStrength;
-
- if (this.inputHandler.getAim()) {
- newHandX *= 0.25;
- newHandY *= 0.25;
- newHandZ *= 0.25;
- }
-
- this.heldItemCamera.position.lerp(new THREE.Vector3(newHandX, newHandY, 5 + newHandZ), 0.15 * this.deltaTime * 60);
-
- const maxRollAmount = this.inputHandler.getInputX() * -.007 * SettingsManager.settings.viewBobbingStrength;
- const maxRollSpeed = this.deltaTime * .4;
- let roll: number = this.lastCameraRoll;
- roll = Renderer.approachNumber(roll, maxRollSpeed, maxRollAmount);
- const euler = new THREE.Euler().setFromQuaternion(this.camera.quaternion, 'YXZ');
- euler.z += roll;
- this.lastCameraRoll = roll;
-
- this.camera.quaternion.setFromEuler(euler);
-
- this.updateFramerate();
- }
-
- private updateFramerate() {
- this.sampleOn++;
- if (this.sampleOn >= this.framesInFramerateSample) {
- this.framerate = this.framesInFramerateSample / (Date.now() / 1000 - this.lastFramerateCalculation);
- this.sampleOn = 0;
- this.lastFramerateCalculation = Date.now() / 1000;
- }
- }
-
- public getFramerate(): number {
- return this.framerate;
- }
-
- public getScene(): THREE.Scene {
- return this.scene;
- }
-
- public getCamera(): THREE.PerspectiveCamera {
- return this.camera;
- }
-
- public getHeldItemScene(): THREE.Scene {
- return this.heldItemScene;
- }
-
- public getHealthIndicatorScene(): THREE.Scene {
- return this.healthIndicatorScene;
- }
-
- public getInventoryMenuScene(): THREE.Scene {
- return this.inventoryMenuScene;
- }
-
- public getInventoryMenuCamera(): THREE.OrthographicCamera {
- return this.inventoryMenuCamera;
- }
-
- private onWindowResize() {
- this.camera.aspect = globalThis.innerWidth / globalThis.innerHeight;
- this.camera.updateProjectionMatrix();
- this.renderer.setSize(globalThis.innerWidth, globalThis.innerHeight);
- this.renderer.setPixelRatio(200 / globalThis.innerHeight);
-
- this.screenPixelsInGamePixel = globalThis.innerHeight / 200;
- // Update held item camera aspect ratio
- this.heldItemCamera.aspect = globalThis.innerWidth / globalThis.innerHeight;
- this.heldItemCamera.updateProjectionMatrix();
- }
-
- public getShotVectorsToPlayersInCrosshair(): { playerID: number; vector: THREE.Vector3; hitPoint: THREE.Vector3 }[] {
- return this.remotePlayerRenderer.getShotVectorsToPlayersInCrosshair();
- }
-
- public getPlayerSpheresInCrosshairWithWalls() {
- return this.remotePlayerRenderer.getPlayerSpheresInCrosshairWithWalls();
- }
-
- public getShotVectorsToPlayersWithOffset(
- yawOffset: number,
- pitchOffset: number,
- ): { playerID: number; vector: THREE.Vector3; hitPoint: THREE.Vector3 }[] {
- return this.remotePlayerRenderer.getShotVectorsToPlayersWithOffset(yawOffset, pitchOffset);
- }
-
- public getEntityScene(): THREE.Scene {
- return this.remotePlayerRenderer.getEntityScene();
- }
-
- public setInputHandler(inputHandler: InputHandler) {
- this.inputHandler = inputHandler;
- }
-
- public setCollisionManager(collisionManager: CollisionManager) {
- this.collisionManager = collisionManager;
- }
-
- private static approachNumber(input: number, step: number, approach: number): number {
- if (input == approach) return approach;
- let output: number;
- if (input > approach) {
- output = input - step;
- if (output <= approach) return approach;
- } else {
- output = input + step;
- if (output >= approach) return approach;
- }
- return output;
- }
+ private clock: THREE.Clock;
+ private deltaTime: number = 0;
+ private chatOverlay: ChatOverlay;
+ private scene: THREE.Scene;
+ private camera: THREE.PerspectiveCamera;
+ private renderer: THREE.WebGLRenderer;
+ private heldItemScene: THREE.Scene;
+ private heldItemCamera: THREE.PerspectiveCamera;
+ private ambientLight: THREE.AmbientLight;
+ private framerate: number;
+ private framesInFramerateSample: number;
+ private sampleOn: number;
+ private lastFramerateCalculation: number;
+ private networking: Networking;
+ private localPlayer: Player;
+ private raycaster: THREE.Raycaster;
+ public scaredLevel: number = 0;
+ private lastPlayerHealth: number = 100;
+ private knockbackVector: THREE.Vector3 = new THREE.Vector3();
+ private bobCycle: number;
+ private lastCameraRoll: number
+
+ public crosshairIsFlashing: boolean = false;
+ public lastShotSomeoneTimestamp: number = 0;
+ public playerHitMarkers: { hitPoint: THREE.Vector3, shotVector: THREE.Vector3, timestamp: number }[] = [];
+ private healthIndicatorScene: THREE.Scene;
+ private healthIndicatorCamera: THREE.PerspectiveCamera;
+ private screenPixelsInGamePixel: number = 1;
+ private inventoryMenuScene: THREE.Scene;
+ private inventoryMenuCamera: THREE.OrthographicCamera;
+ private remotePlayerRenderer: RemotePlayerRenderer;
+ private inputHandler!: InputHandler;
+ private collisionManager!: CollisionManager;
+
+ private spectateGroundTruthPosition: THREE.Vector3 | null = null;
+
+
+ constructor(container: HTMLElement, networking: Networking, localPlayer: Player, chatOverlay: ChatOverlay) {
+ this.networking = networking;
+ this.localPlayer = localPlayer;
+ this.chatOverlay = chatOverlay;
+
+ this.clock = new THREE.Clock();
+ this.scene = new THREE.Scene();
+ this.camera = new THREE.PerspectiveCamera(90, globalThis.innerWidth / globalThis.innerHeight, 0.01, 1000);
+ this.renderer = new THREE.WebGLRenderer();
+ //document.body.appendChild(this.renderer.domElement);
+ container.appendChild(this.renderer.domElement);
+ this.renderer.domElement.style.imageRendering = 'pixelated';
+ this.renderer.setAnimationLoop(null);
+
+ // Create a new scene and camera for the held item
+ this.heldItemScene = new THREE.Scene();
+ this.heldItemCamera = new THREE.PerspectiveCamera(90, globalThis.innerWidth / globalThis.innerHeight, 0.01, 1000);
+ this.heldItemCamera.position.set(0, 0, 5);
+ this.heldItemCamera.lookAt(0, 0, 0);
+
+ // Create a new scene and camera for the health indicator
+ this.healthIndicatorScene = new THREE.Scene();
+ this.healthIndicatorCamera = new THREE.PerspectiveCamera(70, 1, 0.01, 1000);
+ this.healthIndicatorCamera.position.set(0, 0, 0);
+ this.healthIndicatorCamera.lookAt(0, 0, 1);
+
+ this.inventoryMenuScene = new THREE.Scene();
+ this.inventoryMenuCamera = new THREE.OrthographicCamera(-0.5, 0.5, 2.5, -2.5, 0.01, 10);
+ this.inventoryMenuCamera.position.set(0, 0, 5);
+ this.inventoryMenuCamera.lookAt(0, 0, 0);
+ this.inventoryMenuScene.add(this.inventoryMenuCamera);
+ this.inventoryMenuScene.add(new THREE.AmbientLight(0xffffff, 0.5));
+
+ // Ambient lights
+ this.ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
+ const ambientLight2 = new THREE.AmbientLight(0xffffff, 0.5);
+ const ambientLight3 = new THREE.AmbientLight(0xffffff, 0.5); // Ambient light for remote players scene
+
+ this.scene.add(this.ambientLight);
+ this.heldItemScene.add(ambientLight2);
+
+ // Fog settings
+ this.scene.fog = new THREE.FogExp2('#111111', 0.05);
+ this.heldItemScene.fog = new THREE.FogExp2('#111111', 0.05);
+ this.healthIndicatorScene.fog = new THREE.FogExp2('#111111', 0.05); // Add fog to health indicator scene
+
+ this.framerate = 0;
+ this.framesInFramerateSample = 30;
+ this.sampleOn = 0;
+ this.lastFramerateCalculation = 0;
+
+ this.bobCycle = 0;
+ this.lastCameraRoll = 0;
+
+ this.raycaster = new THREE.Raycaster();
+
+ // Initialize remotePlayerRenderer
+ this.remotePlayerRenderer = new RemotePlayerRenderer(
+ this.networking,
+ this.localPlayer,
+ this.raycaster,
+ this.camera,
+ this.scene
+ );
+ this.remotePlayerRenderer.getEntityScene().fog = new THREE.FogExp2('#111111', 0.1); // Add fog to remote players scene
+ this.remotePlayerRenderer.getEntityScene().add(ambientLight3); // Add ambient light to remote players scene
+ this.renderer.domElement.style.touchAction = 'none';
+ this.renderer.domElement.style.position = 'absolute';
+ this.onWindowResize();
+ globalThis.addEventListener('resize', this.onWindowResize.bind(this), false);
+ globalThis.addEventListener('orientationchange', this.onWindowResize.bind(this), false);
+ }
+
+ public onFrame(localPlayer: Player) {
+ this.deltaTime = this.clock.getDelta();
+
+ // Ensure the renderer clears the buffers before the first render
+ this.renderer.autoClear = true;
+
+ // Render the main scene
+ this.renderer.render(this.scene, this.camera);
+
+ // Prevent clearing the buffers in subsequent renders
+ this.renderer.autoClear = false;
+
+ // Update and render remote players
+ this.remotePlayerRenderer.update(this.deltaTime);
+ this.renderer.render(this.remotePlayerRenderer.getEntityScene(), this.camera);
+
+ // Render the held item scene normally (full screen)
+ this.renderer.render(this.heldItemScene, this.heldItemCamera);
+
+ // Set up the scissor and viewport for the health indicator scene rendering
+ const screenWidth = globalThis.innerWidth;
+ const screenHeight = globalThis.innerHeight;
+
+ const healthIndicatorWidth = 60; // native
+ const healthIndicatorHeight = healthIndicatorWidth; // 1:1 aspect ratio
+
+ // Set up scissor and viewport for a region from (0, 0) to (50, 50)
+ this.renderer.setScissorTest(true);
+ this.renderer.setScissor(
+ 2 * this.screenPixelsInGamePixel,
+ screenHeight - (healthIndicatorHeight + 1 + this.chatOverlay.getDebugTextHeight()) * this.screenPixelsInGamePixel,
+ healthIndicatorWidth * this.screenPixelsInGamePixel,
+ healthIndicatorHeight * this.screenPixelsInGamePixel
+ );
+ this.renderer.setViewport(
+ 2 * this.screenPixelsInGamePixel,
+ screenHeight - (healthIndicatorHeight + 1 + this.chatOverlay.getDebugTextHeight()) * this.screenPixelsInGamePixel,
+ healthIndicatorWidth * this.screenPixelsInGamePixel,
+ healthIndicatorHeight * this.screenPixelsInGamePixel
+ );
+
+ // Render the health indicator scene
+ this.renderer.render(this.healthIndicatorScene, this.healthIndicatorCamera);
+
+ // Render inventory view
+ const inventoryWidth = 20;
+ const inventoryHeight = inventoryWidth * 5;
+ this.renderer.setScissorTest(true);
+ this.renderer.setScissor(
+ screenWidth - (inventoryWidth + 4) * this.screenPixelsInGamePixel,
+ screenHeight / 2 - (inventoryHeight / 2) * this.screenPixelsInGamePixel,
+ inventoryWidth * this.screenPixelsInGamePixel,
+ inventoryHeight * this.screenPixelsInGamePixel
+ );
+ this.renderer.setViewport(
+ screenWidth - (inventoryWidth + 4) * this.screenPixelsInGamePixel,
+ screenHeight / 2 - (inventoryHeight / 2) * this.screenPixelsInGamePixel,
+ inventoryWidth * this.screenPixelsInGamePixel,
+ inventoryHeight * this.screenPixelsInGamePixel
+ );
+ this.renderer.render(this.inventoryMenuScene, this.inventoryMenuCamera);
+
+ // Reset scissor test and viewport after rendering the health indicator
+ this.renderer.setScissorTest(false);
+ this.renderer.setViewport(0, 0, screenWidth, screenHeight);
+
+
+ // Restore autoClear to true if necessary
+ this.renderer.autoClear = true;
+
+ if(localPlayer.playerSpectating !== -1) {
+ const remotePlayer = this.networking.getRemotePlayerData().find((player) => player.id === localPlayer.playerSpectating);
+ if(remotePlayer !== undefined) {
+ // Initialize ground truth position if not set
+ if (!this.spectateGroundTruthPosition) {
+ this.spectateGroundTruthPosition = new THREE.Vector3(
+ remotePlayer.position.x,
+ remotePlayer.position.y,
+ remotePlayer.position.z
+ );
+ }
+
+ // Update ground truth position based on velocity
+ this.spectateGroundTruthPosition.x += remotePlayer.velocity.x * this.deltaTime;
+ this.spectateGroundTruthPosition.y += remotePlayer.velocity.y * this.deltaTime;
+ this.spectateGroundTruthPosition.z += remotePlayer.velocity.z * this.deltaTime;
+
+ // If forced update, set directly to remote position
+ if (remotePlayer.forced) {
+ this.spectateGroundTruthPosition.set(
+ remotePlayer.position.x,
+ remotePlayer.position.y,
+ remotePlayer.position.z
+ );
+ }
+
+ // Lerp ground truth position towards actual position
+ this.spectateGroundTruthPosition.lerp(
+ new THREE.Vector3(
+ remotePlayer.position.x,
+ remotePlayer.position.y,
+ remotePlayer.position.z
+ ),
+ 0.1 * this.deltaTime * 60
+ );
+
+ // Update camera position and rotation
+ this.camera.position.copy(this.spectateGroundTruthPosition);
+
+ // Simple quaternion slerp
+ this.camera.quaternion.slerp(new THREE.Quaternion(
+ remotePlayer.lookQuaternion[0],
+ remotePlayer.lookQuaternion[1],
+ remotePlayer.lookQuaternion[2],
+ remotePlayer.lookQuaternion[3]
+ ), 0.3 * this.deltaTime * 60);
+ }
+ } else {
+ // Reset spectate position when not spectating
+ this.spectateGroundTruthPosition = null;
+ this.camera.position.copy(localPlayer.position);
+ this.camera.setRotationFromQuaternion(this.localPlayer.lookQuaternion);
+ }
+
+
+
+
+
+ this.camera.position.add(this.knockbackVector);
+ this.knockbackVector.lerp(new THREE.Vector3(), 0.05 * this.deltaTime * 60);
+
+
+ if(this.localPlayer.health < this.lastPlayerHealth) {
+ const remotePlayer: RemotePlayer | undefined = this.networking.getRemotePlayerData().find((player) => player.id === this.localPlayer.idLastDamagedBy);
+ if(remotePlayer !== undefined) {
+ //console.log("Player was damaged by " + remotePlayer.name);
+ const diff = new THREE.Vector3().subVectors(this.localPlayer.position, remotePlayer.position);
+ this.knockbackVector.copy(diff.normalize().multiplyScalar(0.2));
+ }
+ }
+
+
+ const shakeAmount = 0.08 * Math.pow(this.scaredLevel,5);
+ this.camera.position.add(new THREE.Vector3((Math.random()-0.5) * shakeAmount, (Math.random()-0.5) *shakeAmount, (Math.random()-0.5) * shakeAmount));
+ this.camera.rotation.x += (Math.random()-0.5) * shakeAmount * 0.12;
+ this.camera.rotation.y += (Math.random()-0.5) * shakeAmount * 0.12;
+ this.camera.rotation.z += (Math.random()-0.5) * shakeAmount * 0.12;
+
+ this.heldItemCamera.rotation.set((Math.random()-0.5) * shakeAmount, (Math.random()-0.5) * shakeAmount, (Math.random()-0.5) * shakeAmount );
+
+ this.lastPlayerHealth = this.localPlayer.health;
+
+ const vel = Math.sqrt(Math.pow(this.localPlayer.inputVelocity.x,2) + Math.pow(this.localPlayer.inputVelocity.z,2))
+
+ if(vel == 0 || this.collisionManager.isPlayerInAir() || this.localPlayer.playerSpectating !== -1) {
+ this.bobCycle = 0;
+ } else {
+ this.bobCycle += this.deltaTime * 4.8 * vel;
+ this.camera.position.y = this.camera.position.y + (Math.sin(this.bobCycle) * .03 * SettingsManager.settings.viewBobbingStrength);
+ //console.log(this.camera.position.y);
+ }
+
+ let newHandX = Math.sin(this.bobCycle / 1.9) * .02 * SettingsManager.settings.viewBobbingStrength;
+ let newHandY = -(Math.sin(this.bobCycle) * .07 * SettingsManager.settings.viewBobbingStrength);
+ let newHandZ = Math.sin(this.bobCycle / 1.8) * .015 * SettingsManager.settings.viewBobbingStrength;
+ newHandY += localPlayer.velocity.y * 0.04 * SettingsManager.settings.viewBobbingStrength; //move hand up when falling, down when jumping
+
+ //banana lags behind player slightly
+ const playerVelocity = new THREE.Vector3().copy(localPlayer.velocity);
+ playerVelocity.applyQuaternion(localPlayer.lookQuaternion.clone().invert());
+ newHandX += playerVelocity.x * 0.02 * SettingsManager.settings.viewBobbingStrength;
+ newHandZ -= -playerVelocity.z * 0.02 * SettingsManager.settings.viewBobbingStrength;
+
+ if(this.inputHandler.getAim()) {
+ newHandX *= 0.25;
+ newHandY *= 0.25;
+ newHandZ *= 0.25;
+ }
+
+ this.heldItemCamera.position.lerp(new THREE.Vector3(newHandX, newHandY, 5 + newHandZ),0.15 * this.deltaTime * 60);
+
+ const maxRollAmount = this.inputHandler.getInputX() * -.007 * SettingsManager.settings.viewBobbingStrength;
+ const maxRollSpeed = this.deltaTime * .4;
+ let roll: number = this.lastCameraRoll;
+ roll = Renderer.approachNumber(roll, maxRollSpeed, maxRollAmount);
+ const euler = new THREE.Euler().setFromQuaternion(this.camera.quaternion, 'YXZ');
+ euler.z += roll;
+ this.lastCameraRoll = roll;
+
+ this.camera.quaternion.setFromEuler(euler);
+
+ this.updateFramerate();
+ }
+
+ private updateFramerate() {
+ this.sampleOn++;
+ if (this.sampleOn >= this.framesInFramerateSample) {
+ this.framerate = this.framesInFramerateSample / (Date.now() / 1000 - this.lastFramerateCalculation);
+ this.sampleOn = 0;
+ this.lastFramerateCalculation = Date.now() / 1000;
+ }
+ }
+
+ public getFramerate(): number {
+ return this.framerate;
+ }
+
+ public getScene(): THREE.Scene {
+ return this.scene;
+ }
+
+ public getCamera(): THREE.PerspectiveCamera {
+ return this.camera;
+ }
+
+ public getHeldItemScene(): THREE.Scene {
+ return this.heldItemScene;
+ }
+
+ public getHealthIndicatorScene(): THREE.Scene {
+ return this.healthIndicatorScene;
+ }
+
+ public getInventoryMenuScene(): THREE.Scene {
+ return this.inventoryMenuScene;
+ }
+
+ public getInventoryMenuCamera(): THREE.OrthographicCamera {
+ return this.inventoryMenuCamera;
+ }
+
+ private onWindowResize() {
+ this.camera.aspect = globalThis.innerWidth / globalThis.innerHeight;
+ this.camera.updateProjectionMatrix();
+ this.renderer.setSize(globalThis.innerWidth, globalThis.innerHeight);
+ this.renderer.setPixelRatio(200 / globalThis.innerHeight);
+
+ this.screenPixelsInGamePixel = globalThis.innerHeight / 200;
+ // Update held item camera aspect ratio
+ this.heldItemCamera.aspect = globalThis.innerWidth / globalThis.innerHeight;
+ this.heldItemCamera.updateProjectionMatrix();
+ }
+
+
+ public getShotVectorsToPlayersInCrosshair(): { playerID: number, vector: THREE.Vector3, hitPoint: THREE.Vector3 }[] {
+ return this.remotePlayerRenderer.getShotVectorsToPlayersInCrosshair();
+ }
+
+ public getPlayerSpheresInCrosshairWithWalls() {
+ return this.remotePlayerRenderer.getPlayerSpheresInCrosshairWithWalls();
+ }
+
+ public getShotVectorsToPlayersWithOffset(yawOffset: number, pitchOffset: number): { playerID: number, vector: THREE.Vector3, hitPoint: THREE.Vector3 }[] {
+ return this.remotePlayerRenderer.getShotVectorsToPlayersWithOffset(yawOffset, pitchOffset);
+ }
+
+ public getEntityScene(): THREE.Scene {
+ return this.remotePlayerRenderer.getEntityScene();
+ }
+
+ public setInputHandler(inputHandler: InputHandler) {
+ this.inputHandler = inputHandler;
+ }
+
+ public setCollisionManager(collisionManager: CollisionManager) {
+ this.collisionManager = collisionManager;
+ }
+
+ private static approachNumber(input: number, step: number, approach: number): number {
+ if (input == approach) {return approach;}
+ let output: number;
+ if (input > approach) {
+ output = input - step;
+ if (output <= approach) {return approach;}
+ } else {
+ output = input + step;
+ if (output >= approach) {return approach;}
+ }
+ return output;
+ }
}
diff --git a/src/client/core/SettingsManager.ts b/src/client/core/SettingsManager.ts
index 0e028ef..6a326dd 100644
--- a/src/client/core/SettingsManager.ts
+++ b/src/client/core/SettingsManager.ts
@@ -1,39 +1,39 @@
export class SettingsManager {
- static settings: Settings;
+ static settings: Settings;
- static {
- SettingsManager.reset();
- const settingsJson = localStorage.getItem('settings');
- if (settingsJson) SettingsManager.settings = JSON.parse(settingsJson);
- }
+ static {
+ SettingsManager.reset()
+ const settingsJson = localStorage.getItem('settings');
+ if (settingsJson) SettingsManager.settings = JSON.parse(settingsJson);
+ }
- public static write(): void {
- localStorage.setItem('settings', JSON.stringify(SettingsManager.settings));
- }
+ public static write(): void {
+ localStorage.setItem('settings', JSON.stringify(SettingsManager.settings));
+ }
- public static reset() {
- SettingsManager.settings = {
- sense: 1,
- controllerSense: 1,
- name: null,
- crosshairColor: 'rgb(0,255,255)',
- crosshairType: 1,
- viewBobbingStrength: 1,
- doPrettyText: false,
- };
- }
+ public static reset() {
+ SettingsManager.settings = {
+ sense: 1,
+ controllerSense: 1,
+ name: null,
+ crosshairColor: 'rgb(0,255,255)',
+ crosshairType: 1,
+ viewBobbingStrength: 1,
+ doPrettyText: false,
+ };
+ }
- constructor() {
- throw Error('Settings class is static.');
- }
+ constructor() {
+ throw Error('Settings class is static.');
+ }
}
interface Settings {
- sense: number;
- controllerSense: number;
- name: null | string;
- crosshairColor: string;
- crosshairType: number;
- viewBobbingStrength: number;
- doPrettyText: boolean;
-}
+ sense: number;
+ controllerSense: number;
+ name: null | string;
+ crosshairColor: string;
+ crosshairType: number;
+ viewBobbingStrength: number;
+ doPrettyText: boolean;
+}
\ No newline at end of file
diff --git a/src/client/input/CollisionManager.ts b/src/client/input/CollisionManager.ts
index fe855d1..4cd8e33 100644
--- a/src/client/input/CollisionManager.ts
+++ b/src/client/input/CollisionManager.ts
@@ -1,150 +1,142 @@
import * as THREE from 'three';
import { Player } from '../core/Player.ts';
-import {
- acceleratedRaycast,
- computeBoundsTree,
- disposeBoundsTree,
- MeshBVH,
- StaticGeometryGenerator,
-} from 'three-mesh-bvh';
-import { InputHandler } from './InputHandler.ts';
-import { RemotePlayerRenderer } from '../core/RemotePlayerRenderer.ts';
+import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree, StaticGeometryGenerator, MeshBVH } from 'three-mesh-bvh';
+import { InputHandler } from "./InputHandler.ts";
+import { RemotePlayerRenderer} from "../core/RemotePlayerRenderer.ts";
THREE.Mesh.prototype.raycast = acceleratedRaycast;
THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
export class CollisionManager {
- private clock: THREE.Clock;
- private readonly colliderSphere: THREE.Sphere;
- private readonly deltaVec: THREE.Vector3;
- private readonly prevPosition: THREE.Vector3;
- public static mapLoaded: boolean = false;
- private static colliderGeom?: THREE.BufferGeometry; // Mark as possibly undefined
- private inputHandler: InputHandler;
- private static readonly maxAngle: number = Math.cos(45 * Math.PI / 180);
- private readonly triNormal: THREE.Vector3;
- private readonly upVector: THREE.Vector3;
- private coyoteTime: number;
- private jumped: boolean;
- private collided: boolean;
-
- constructor(inputHandler: InputHandler) {
- this.inputHandler = inputHandler;
- this.clock = new THREE.Clock();
- this.colliderSphere = new THREE.Sphere(new THREE.Vector3(), .2);
- this.deltaVec = new THREE.Vector3();
- this.prevPosition = new THREE.Vector3();
- this.triNormal = new THREE.Vector3();
- this.upVector = new THREE.Vector3(0, 1, 0);
- this.coyoteTime = 0;
- this.jumped = false;
- this.collided = false;
- }
-
- public collisionPeriodic(localPlayer: Player) {
- if (!CollisionManager.mapLoaded || !CollisionManager.colliderGeom || !CollisionManager.colliderGeom.boundsTree) {
- return; // Add checks
- }
- let deltaTime: number = this.clock.getDelta();
- let steps: number = 1;
- while (deltaTime >= 1 / 120) {
- deltaTime = deltaTime / 2;
- steps = steps * 2;
- }
- for (let i = 0; i < steps; i++) {
- this.physics(localPlayer, deltaTime);
- }
- }
-
- private physics(localPlayer: Player, deltaTime: number) {
- this.prevPosition.copy(localPlayer.position);
- const jump: boolean = this.inputHandler.jump;
-
- localPlayer.gravity += deltaTime * -30;
- localPlayer.inputVelocity.y += localPlayer.gravity;
- localPlayer.inputVelocity.y = (localPlayer.inputVelocity.y + this.inputHandler.prevInputVelocity.y) * .25;
- localPlayer.position.add(localPlayer.inputVelocity.clone().multiplyScalar(deltaTime));
-
- const bvh: MeshBVH | undefined = CollisionManager.colliderGeom?.boundsTree;
- if (!bvh) return; // Ensure bvh exists
-
- this.colliderSphere.center = localPlayer.position.clone();
-
- this.collided = false;
-
- bvh.shapecast({
- intersectsBounds: (box: THREE.Box3) => {
- return box.intersectsSphere(this.colliderSphere);
- },
-
- intersectsTriangle: (tri: THREE.Triangle) => {
- // Get delta between the closest point and center
- tri.closestPointToPoint(this.colliderSphere.center, this.deltaVec);
- this.deltaVec.sub(this.colliderSphere.center);
- const distance: number = this.deltaVec.length();
-
- if (distance < this.colliderSphere.radius) {
- // Move the sphere position to be outside the triangle
- const radius: number = this.colliderSphere.radius;
- const depth: number = distance - radius;
- this.deltaVec.multiplyScalar(1 / distance);
-
- tri.getNormal(this.triNormal);
- const angle: number = this.triNormal.dot(this.upVector);
-
- if (angle >= CollisionManager.maxAngle) {
- localPlayer.position.addScaledVector(this.deltaVec, depth);
- localPlayer.inputVelocity.y = 0;
- localPlayer.gravity = 0;
- this.coyoteTime = 0;
- this.collided = true;
- } else {
- localPlayer.position.addScaledVector(this.deltaVec, depth);
- }
- }
-
- this.colliderSphere.center = localPlayer.position.clone();
- return false;
- },
-
- boundsTraverseOrder: (box: THREE.Box3) => {
- return box.distanceToPoint(this.colliderSphere.center) - this.colliderSphere.radius;
- },
- });
-
- if (!this.collided) {
- this.coyoteTime += deltaTime;
- if (jump && this.coyoteTime < 6 / 60 && !this.jumped) {
- localPlayer.gravity = 8;
- this.jumped = true;
- }
- } else {
- if (jump) {
- localPlayer.gravity = 8;
- } else {
- this.jumped = false;
- }
- }
- if (!(deltaTime == 0)) {
- localPlayer.velocity.copy(localPlayer.position.clone().sub(this.prevPosition).divideScalar(deltaTime));
- }
- }
-
- public static staticGeometry(group: THREE.Group) {
- //if (!this.mapLoaded) {
- console.time('Building static geometry BVH');
- const staticGenerator = new StaticGeometryGenerator(group);
- staticGenerator.attributes = ['position'];
- this.colliderGeom = staticGenerator.generate();
- this.colliderGeom.computeBoundsTree({ maxDepth: 1000000, maxLeafTris: 4 });
- RemotePlayerRenderer.setMap(new THREE.Mesh(this.colliderGeom));
- this.mapLoaded = true;
- console.timeEnd('Building static geometry BVH');
- // }
- }
-
- public isPlayerInAir(): boolean {
- return !this.collided;
- }
-}
+ private clock: THREE.Clock;
+ private readonly colliderSphere: THREE.Sphere;
+ private readonly deltaVec: THREE.Vector3;
+ private readonly prevPosition: THREE.Vector3;
+ public static mapLoaded: boolean = false;
+ private static colliderGeom?: THREE.BufferGeometry; // Mark as possibly undefined
+ private inputHandler: InputHandler;
+ private static readonly maxAngle: number = Math.cos(45 * Math.PI / 180);
+ private readonly triNormal: THREE.Vector3;
+ private readonly upVector: THREE.Vector3;
+ private coyoteTime: number;
+ private jumped: boolean;
+ private collided: boolean;
+
+ constructor(inputHandler: InputHandler) {
+ this.inputHandler = inputHandler;
+ this.clock = new THREE.Clock();
+ this.colliderSphere = new THREE.Sphere(new THREE.Vector3(), .2);
+ this.deltaVec = new THREE.Vector3();
+ this.prevPosition = new THREE.Vector3();
+ this.triNormal = new THREE.Vector3();
+ this.upVector = new THREE.Vector3(0, 1, 0)
+ this.coyoteTime = 0;
+ this.jumped = false;
+ this.collided = false;
+ }
+
+ public collisionPeriodic(localPlayer: Player) {
+ if (!CollisionManager.mapLoaded || !CollisionManager.colliderGeom || !CollisionManager.colliderGeom.boundsTree) return; // Add checks
+ let deltaTime: number = this.clock.getDelta();
+ let steps: number = 1;
+ while (deltaTime >= 1 / 120) {
+ deltaTime = deltaTime / 2
+ steps = steps * 2;
+ }
+ for (let i = 0; i < steps; i++) {
+ this.physics(localPlayer, deltaTime);
+ }
+ }
+
+ private physics(localPlayer: Player, deltaTime: number) {
+ this.prevPosition.copy(localPlayer.position);
+ const jump: boolean = this.inputHandler.jump;
+
+ localPlayer.gravity += deltaTime * -30;
+ localPlayer.inputVelocity.y += localPlayer.gravity;
+ localPlayer.inputVelocity.y = (localPlayer.inputVelocity.y + this.inputHandler.prevInputVelocity.y) * .25;
+ localPlayer.position.add(localPlayer.inputVelocity.clone().multiplyScalar(deltaTime));
+
+ const bvh: MeshBVH | undefined = CollisionManager.colliderGeom?.boundsTree;
+ if (!bvh) return; // Ensure bvh exists
+
+ this.colliderSphere.center = localPlayer.position.clone();
+
+ this.collided = false;
+
+ bvh.shapecast({
+ intersectsBounds: (box: THREE.Box3) => {
+ return box.intersectsSphere(this.colliderSphere);
+ },
+
+ intersectsTriangle: (tri: THREE.Triangle) => {
+ // Get delta between the closest point and center
+ tri.closestPointToPoint(this.colliderSphere.center, this.deltaVec);
+ this.deltaVec.sub(this.colliderSphere.center);
+ const distance: number = this.deltaVec.length();
+
+ if (distance < this.colliderSphere.radius) {
+ // Move the sphere position to be outside the triangle
+ const radius: number = this.colliderSphere.radius;
+ const depth: number = distance - radius;
+ this.deltaVec.multiplyScalar(1 / distance);
+
+ tri.getNormal(this.triNormal);
+ const angle: number = this.triNormal.dot(this.upVector);
+
+ if (angle >= CollisionManager.maxAngle) {
+ localPlayer.position.addScaledVector(this.deltaVec, depth);
+ localPlayer.inputVelocity.y = 0;
+ localPlayer.gravity = 0;
+ this.coyoteTime = 0;
+ this.collided = true;
+ } else {
+ localPlayer.position.addScaledVector(this.deltaVec, depth);
+ }
+ }
+
+ this.colliderSphere.center = localPlayer.position.clone();
+ return false;
+ },
+
+ boundsTraverseOrder: (box: THREE.Box3) => {
+ return box.distanceToPoint(this.colliderSphere.center) - this.colliderSphere.radius;
+ }
+ });
+
+ if (!this.collided) {
+ this.coyoteTime += deltaTime;
+ if (jump && this.coyoteTime < 6 / 60 && !this.jumped) {
+ localPlayer.gravity = 8;
+ this.jumped = true;
+ }
+ } else {
+ if (jump) {
+ localPlayer.gravity = 8;
+ } else {
+ this.jumped = false;
+ }
+ }
+ if (!(deltaTime == 0)) {
+ localPlayer.velocity.copy(localPlayer.position.clone().sub(this.prevPosition).divideScalar(deltaTime));
+ }
+ }
+
+ public static staticGeometry(group: THREE.Group) {
+ //if (!this.mapLoaded) {
+ console.time("Building static geometry BVH");
+ const staticGenerator = new StaticGeometryGenerator(group);
+ staticGenerator.attributes = ['position'];
+ this.colliderGeom = staticGenerator.generate();
+ this.colliderGeom.computeBoundsTree({maxDepth: 1000000, maxLeafTris: 4});
+ RemotePlayerRenderer.setMap(new THREE.Mesh(this.colliderGeom));
+ this.mapLoaded = true;
+ console.timeEnd("Building static geometry BVH");
+ // }
+ }
+
+ public isPlayerInAir(): boolean {
+ return !this.collided;
+ }
+}
\ No newline at end of file
diff --git a/src/client/input/HeldItemInput.ts b/src/client/input/HeldItemInput.ts
index 062b2aa..916b486 100644
--- a/src/client/input/HeldItemInput.ts
+++ b/src/client/input/HeldItemInput.ts
@@ -1,11 +1,11 @@
export class HeldItemInput {
- public leftClick: boolean = false;
- public rightClick: boolean = false;
- public shiftKey: boolean = false;
+ public leftClick: boolean = false;
+ public rightClick: boolean = false;
+ public shiftKey: boolean = false;
- constructor(leftClick: boolean = false, rightClick: boolean = false, shiftKey: boolean = false) {
- this.leftClick = leftClick;
- this.rightClick = rightClick;
- this.shiftKey = shiftKey;
- }
-}
+ constructor(leftClick: boolean = false, rightClick: boolean = false, shiftKey: boolean = false) {
+ this.leftClick = leftClick;
+ this.rightClick = rightClick;
+ this.shiftKey = shiftKey;
+ }
+}
\ No newline at end of file
diff --git a/src/client/input/InputHandler.ts b/src/client/input/InputHandler.ts
index f3fbba1..b50c4c3 100644
--- a/src/client/input/InputHandler.ts
+++ b/src/client/input/InputHandler.ts
@@ -2,359 +2,342 @@ import * as THREE from 'three';
import { PointerLockControls } from './PointerLockControl.ts';
import { Renderer } from '../core/Renderer.ts';
import { Player } from '../core/Player.ts';
-import { SettingsManager } from '../core/SettingsManager.ts';
+import {SettingsManager} from "../core/SettingsManager.ts";
export class InputHandler {
- private readonly gameIndex: number;
- private mouse: PointerLockControls;
- private gamepad: Gamepad | null = null;
- private readonly gamepadEuler;
- private clock: THREE.Clock;
- private keys: { [key: string]: boolean } = {};
- private leftMouseDown: boolean = false;
- private rightMouseDown: boolean = false;
- private renderer: Renderer;
- private readonly localPlayer: Player;
- private inputX: number = 0;
- private inputZ: number = 0;
- public jump = false;
- public prevInputVelocity: THREE.Vector3;
- private scrollClicksSinceLastCheck: number = 0;
- private readonly gamepadInputs: GamepadInputs;
- private shoot: boolean = false;
- private aim: boolean = false;
- public nameSettingActive: boolean = false;
- private touchJoyX: number = 0;
- private touchJoyY: number = 0;
- private touchLookX: number = 0;
- private touchLookY: number = 0;
- private inventoryIterationTouched: boolean = false;
- private touchButtons: number[] = [];
-
- constructor(renderer: Renderer, localPlayer: Player, nextGameIndex: number) {
- this.renderer = renderer;
- this.localPlayer = localPlayer;
- this.prevInputVelocity = new THREE.Vector3();
- this.gamepadEuler = new THREE.Euler(0, 0, 0, 'YXZ');
-
- this.clock = new THREE.Clock();
- this.mouse = new PointerLockControls(this.localPlayer, document.body);
-
- this.gamepadInputs = new GamepadInputs();
-
- this.gameIndex = nextGameIndex;
-
- if (!navigator.getGamepads()) {
- console.log('Browser does not support Gamepad API.');
- }
-
- this.setupEventListeners();
- }
-
- private setupEventListeners() {
- document.addEventListener('keydown', this.onKeyDown.bind(this));
- document.addEventListener('keyup', this.onKeyUp.bind(this));
- document.addEventListener('mousedown', this.onMouseDown.bind(this));
- document.addEventListener('mouseup', this.onMouseUp.bind(this));
- document.addEventListener('mouseleave', this.onMouseUp.bind(this));
- document.addEventListener('blur', this.deregisterAllKeys.bind(this), false);
- document.addEventListener('pointerlockchange', this.deregisterAllKeys.bind(this), false);
- document.addEventListener('visibilitychange', this.deregisterAllKeys.bind(this), false);
-
- document.addEventListener('click', () => {
- this.mouse.lock();
- });
-
- document.addEventListener('contextmenu', (event) => {
- event.preventDefault();
- });
-
- document.addEventListener('wheel', this.processScroll.bind(this));
- }
-
- private processScroll(e: WheelEvent) {
- if (e.deltaY >= 4) {
- this.scrollClicksSinceLastCheck++;
- }
- if (e.deltaY <= -4) {
- this.scrollClicksSinceLastCheck--;
- }
- }
-
- public getScrollClicks() {
- const clicks = this.scrollClicksSinceLastCheck;
- this.scrollClicksSinceLastCheck = 0;
- return clicks;
- }
-
- public handleInputs() {
- const deltaTime: number = this.clock.getDelta();
- const deltaTimeAcceleration = this.localPlayer.acceleration * deltaTime;
-
- let dist = 1;
- let speedMultiplier: number = 1;
- this.jump = false;
- this.aim = false;
- this.shoot = false;
-
- const oldInputZ = this.inputZ;
- const oldInputX = this.inputX;
-
- this.gamepad = navigator.getGamepads()[this.gameIndex];
- if (this.gamepad) {
- if (this.gamepad.connected) {
- this.updateGamepadInputArray(this.gamepad);
- this.gamepadEuler.setFromQuaternion(this.localPlayer.lookQuaternion);
- if (Math.abs(this.gamepadInputs.leftJoyX) >= .1) {
- this.inputX += deltaTimeAcceleration * this.gamepadInputs.leftJoyX;
- }
- if (Math.abs(this.gamepadInputs.leftJoyY) >= .1) {
- this.inputZ += deltaTimeAcceleration * this.gamepadInputs.leftJoyY;
- }
- const vectorLength = Math.sqrt(
- (this.gamepadInputs.leftJoyX * this.gamepadInputs.leftJoyX) +
- (this.gamepadInputs.leftJoyY * this.gamepadInputs.leftJoyY),
- );
- if (vectorLength >= .1) speedMultiplier = Math.min(Math.max(vectorLength, 0), 1);
- if (this.gamepadInputs.A) this.jump = true;
- if (this.gamepadInputs.leftTrigger > .5) this.aim = true;
- if (this.gamepadInputs.rightTrigger > .5) this.shoot = true;
- const aimAdjust = this.calculateAimAssist();
- this.gamepadEuler.y -= this.gamepadInputs.rightJoyX * SettingsManager.settings.controllerSense * deltaTime *
- aimAdjust * 4;
- this.gamepadEuler.x -= this.gamepadInputs.rightJoyY * SettingsManager.settings.controllerSense * deltaTime *
- aimAdjust * 4;
- this.gamepadEuler.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.gamepadEuler.x));
- this.localPlayer.lookQuaternion.setFromEuler(this.gamepadEuler);
- }
- }
-
- //touch joystick controls
- if (Math.hypot(this.touchJoyY, this.touchJoyX) > 0.1) {
- const touchVectorLength = Math.hypot(this.touchJoyX, this.touchJoyY);
- speedMultiplier = Math.min(Math.max(touchVectorLength, 0), 1);
- this.inputX += deltaTimeAcceleration * this.touchJoyX;
- this.inputZ += deltaTimeAcceleration * this.touchJoyY;
- }
-
- const touchSensitivity = 0.03; // Adjust sensitivity as needed
- this.gamepadEuler.setFromQuaternion(this.localPlayer.lookQuaternion);
-
- if (!this.localPlayer.chatActive && !this.nameSettingActive) {
- if (this.getKey('w')) this.inputZ -= deltaTimeAcceleration;
- if (this.getKey('s')) this.inputZ += deltaTimeAcceleration;
- if (this.getKey('a')) this.inputX -= deltaTimeAcceleration;
- if (this.getKey('d')) this.inputX += deltaTimeAcceleration;
- const aimAdjust = this.calculateAimAssist();
- if (this.getKey('arrowright')) {
- this.gamepadEuler.y -= SettingsManager.settings.controllerSense * deltaTime * aimAdjust * 4;
- }
- if (this.getKey('arrowleft')) {
- this.gamepadEuler.y += SettingsManager.settings.controllerSense * deltaTime * aimAdjust * 4;
- }
- if (this.getKey('arrowup')) {
- this.gamepadEuler.x += SettingsManager.settings.controllerSense * deltaTime * aimAdjust * 4;
- }
- if (this.getKey('arrowdown')) {
- this.gamepadEuler.x -= SettingsManager.settings.controllerSense * deltaTime * aimAdjust * 4;
- }
- if (this.getKey(' ')) this.jump = true;
- }
-
- this.gamepadEuler.y -= this.touchLookX * touchSensitivity;
- this.gamepadEuler.x -= this.touchLookY * touchSensitivity;
- this.gamepadEuler.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.gamepadEuler.x));
- this.localPlayer.lookQuaternion.setFromEuler(this.gamepadEuler);
-
- switch (this.inputZ - oldInputZ) {
- case 0:
- this.inputZ = InputHandler.approachZero(this.inputZ, deltaTimeAcceleration);
- }
-
- switch (this.inputX - oldInputX) {
- case 0:
- this.inputX = InputHandler.approachZero(this.inputX, deltaTimeAcceleration);
- }
-
- if (this.localPlayer.health <= 0) dist = 0; //don't allow movement when health = 0
-
- this.prevInputVelocity.copy(this.localPlayer.inputVelocity);
-
- this.localPlayer.inputVelocity.z = dist * this.inputZ;
- this.localPlayer.inputVelocity.x = dist * this.inputX;
- this.localPlayer.inputVelocity.y = 0;
- this.localPlayer.inputVelocity.clampLength(0, this.localPlayer.speed * speedMultiplier);
- this.localPlayer.inputVelocity.y = this.prevInputVelocity.y;
- this.inputZ = this.localPlayer.inputVelocity.z;
- this.inputX = this.localPlayer.inputVelocity.x;
-
- const euler = new THREE.Euler().setFromQuaternion(this.localPlayer.lookQuaternion, 'YXZ');
- euler.x = 0;
- euler.z = 0;
- this.localPlayer.quaternion.setFromEuler(euler);
- this.localPlayer.inputVelocity.applyQuaternion(this.localPlayer.quaternion);
-
- if (this.leftMouseDown || this.touchButtons.includes(0)) this.shoot = true;
- if (this.rightMouseDown) this.aim = true;
-
- if (this.localPlayer.playerSpectating !== -1) {
- this.inputX = 0;
- this.inputZ = 0;
- this.jump = false;
- this.shoot = false;
- this.aim = false;
- }
- }
-
- public getKey(key: string): boolean {
- return this.keys[key];
- }
-
- public setTouchJoyInput(x: number, y: number) {
- this.touchJoyX = x;
- this.touchJoyY = y;
- }
-
- public setLastTouchLookDelta(x: number, y: number) {
- this.touchLookX = x;
- this.touchLookY = y;
- }
- public setButtonsHeld(buttons: number[]) {
- this.touchButtons = buttons;
- this.jump = this.jump || buttons.includes(-1);
- this.inventoryIterationTouched = buttons.includes(1);
- }
-
- public getInventoryIterationTouched() {
- return this.inventoryIterationTouched;
- }
-
- private onKeyDown(event: KeyboardEvent) {
- //event.preventDefault();
- if (event.key === 'Tab' || event.key === "'" || event.key === '/') event.preventDefault();
- const key = event.key.toLowerCase();
- this.keys[key] = true;
-
- if (!this.localPlayer.chatActive && !this.nameSettingActive) {
- if (key === 'c') {
- this.leftMouseDown = true;
- } else if (key === 'z') {
- this.rightMouseDown = true;
- }
- }
- }
-
- private onKeyUp(event: KeyboardEvent) {
- const key = event.key.toLowerCase();
- this.keys[key] = false;
-
- if (!this.localPlayer.chatActive && !this.nameSettingActive) {
- if (key === 'c') {
- this.leftMouseDown = false;
- } else if (key === 'z') {
- this.rightMouseDown = false;
- }
- }
- }
-
- private onMouseDown(event: MouseEvent) {
- if (event.button === 0) {
- this.leftMouseDown = true;
- } else if (event.button === 2) {
- this.rightMouseDown = true;
- }
- }
-
- private onMouseUp(event: MouseEvent) {
- if (event.button === 0) {
- this.leftMouseDown = false;
- } else if (event.button === 2) {
- this.rightMouseDown = false;
- }
- }
-
- public getShoot() {
- return this.shoot;
- }
-
- public getAim() {
- return this.aim;
- }
-
- public getGamepadInputs(): GamepadInputs {
- return this.gamepadInputs;
- }
-
- public deregisterAllKeys() {
- const locked = document.pointerLockElement === document.body;
- if (!locked) {
- this.keys = {};
- }
- }
-
- public getInputX() {
- return this.inputX;
- }
-
- public getInputZ() {
- return this.inputZ;
- }
-
- private static approachZero(input: number, step: number): number {
- if (input == 0) return 0;
- let sign: number = 1;
- if (input < 0) sign = -1;
- const output: number = Math.abs(input) - step;
- if (output <= 0) return 0;
- return sign * output;
- }
-
- private updateGamepadInputArray(gamepad: Gamepad) {
- if (gamepad.axes[4]) {
- this.gamepadInputs.leftTrigger = gamepad.axes[4];
- this.gamepadInputs.rightTrigger = gamepad.axes[5];
- } else {
- this.gamepadInputs.leftTrigger = gamepad.buttons[6].value;
- this.gamepadInputs.rightTrigger = gamepad.buttons[7].value;
- }
- this.gamepadInputs.leftJoyX = gamepad.axes[0];
- this.gamepadInputs.leftJoyY = gamepad.axes[1];
- this.gamepadInputs.A = gamepad.buttons[0].pressed;
- this.gamepadInputs.rightJoyX = gamepad.axes[2];
- this.gamepadInputs.rightJoyY = gamepad.axes[3];
- this.gamepadInputs.leftShoulder = gamepad.buttons[4].pressed;
- this.gamepadInputs.rightShoulder = gamepad.buttons[5].pressed;
- }
-
- private calculateAimAssist(): number {
- if (this.gamepad) {
- if ((Math.abs(this.gamepadInputs.rightJoyX) >= .1 || Math.abs(this.gamepadInputs.rightJoyY) >= .1)) {
- if (this.renderer.getPlayerSpheresInCrosshairWithWalls().length > 0) {
- return .5;
- }
- }
- } else if (
- this.getKey('arrowup') || this.getKey('arrowdown') || this.getKey('arrowleft') || this.getKey('arrowright')
- ) {
- if (this.renderer.getPlayerSpheresInCrosshairWithWalls().length > 0) {
- return .5;
- }
- }
- return 1;
- }
+ private readonly gameIndex: number;
+ private mouse: PointerLockControls;
+ private gamepad: (Gamepad | null) = null;
+ private readonly gamepadEuler ;
+ private clock: THREE.Clock;
+ private keys: { [key: string]: boolean } = {};
+ private leftMouseDown: boolean = false;
+ private rightMouseDown: boolean = false;
+ private renderer: Renderer;
+ private readonly localPlayer: Player;
+ private inputX: number = 0;
+ private inputZ: number = 0;
+ public jump = false;
+ public prevInputVelocity: THREE.Vector3;
+ private scrollClicksSinceLastCheck: number = 0;
+ private readonly gamepadInputs: GamepadInputs;
+ private shoot: boolean = false;
+ private aim: boolean = false;
+ public nameSettingActive: boolean = false;
+ private touchJoyX: number = 0;
+ private touchJoyY: number = 0;
+ private touchLookX: number = 0;
+ private touchLookY: number = 0;
+ private inventoryIterationTouched: boolean = false;
+ private touchButtons: number[] = [];
+
+ constructor(renderer: Renderer, localPlayer: Player, nextGameIndex: number) {
+ this.renderer = renderer;
+ this.localPlayer = localPlayer;
+ this.prevInputVelocity = new THREE.Vector3();
+ this.gamepadEuler = new THREE.Euler(0, 0, 0, 'YXZ');
+
+ this.clock = new THREE.Clock();
+ this.mouse = new PointerLockControls(this.localPlayer, document.body);
+
+ this.gamepadInputs = new GamepadInputs();
+
+ this.gameIndex = nextGameIndex;
+
+ if(!navigator.getGamepads()) {
+ console.log("Browser does not support Gamepad API.")
+ }
+
+ this.setupEventListeners();
+ }
+
+ private setupEventListeners() {
+ document.addEventListener('keydown', this.onKeyDown.bind(this));
+ document.addEventListener('keyup', this.onKeyUp.bind(this));
+ document.addEventListener('mousedown', this.onMouseDown.bind(this));
+ document.addEventListener('mouseup', this.onMouseUp.bind(this));
+ document.addEventListener('mouseleave', this.onMouseUp.bind(this));
+ document.addEventListener('blur', this.deregisterAllKeys.bind(this), false);
+ document.addEventListener('pointerlockchange', this.deregisterAllKeys.bind(this), false);
+ document.addEventListener('visibilitychange', this.deregisterAllKeys.bind(this), false);
+
+ document.addEventListener('click', () => {
+ this.mouse.lock();
+ });
+
+ document.addEventListener('contextmenu', (event) => {event.preventDefault();});
+
+ document.addEventListener('wheel', this.processScroll.bind(this));
+ }
+
+ private processScroll(e :WheelEvent) {
+ if(e.deltaY >= 4)
+ this.scrollClicksSinceLastCheck++;
+ if(e.deltaY <= -4)
+ this.scrollClicksSinceLastCheck--;
+ }
+
+ public getScrollClicks() {
+ const clicks = this.scrollClicksSinceLastCheck;
+ this.scrollClicksSinceLastCheck = 0;
+ return clicks;
+
+ }
+
+ public handleInputs() {
+ const deltaTime: number = this.clock.getDelta();
+ const deltaTimeAcceleration = this.localPlayer.acceleration * deltaTime;
+
+ let dist = 1;
+ let speedMultiplier: number = 1;
+ this.jump = false;
+ this.aim = false;
+ this.shoot = false;
+
+ const oldInputZ = this.inputZ;
+ const oldInputX = this.inputX;
+
+ this.gamepad = navigator.getGamepads()[this.gameIndex];
+ if(this.gamepad) {
+ if(this.gamepad.connected) {
+ this.updateGamepadInputArray(this.gamepad);
+ this.gamepadEuler.setFromQuaternion(this.localPlayer.lookQuaternion);
+ if (Math.abs(this.gamepadInputs.leftJoyX) >= .1) this.inputX += deltaTimeAcceleration * this.gamepadInputs.leftJoyX;
+ if (Math.abs(this.gamepadInputs.leftJoyY) >= .1) this.inputZ += deltaTimeAcceleration * this.gamepadInputs.leftJoyY;
+ const vectorLength = Math.sqrt((this.gamepadInputs.leftJoyX * this.gamepadInputs.leftJoyX) + (this.gamepadInputs.leftJoyY * this.gamepadInputs.leftJoyY));
+ if (vectorLength >= .1) speedMultiplier = Math.min(Math.max(vectorLength, 0), 1);
+ if (this.gamepadInputs.A) this.jump = true;
+ if (this.gamepadInputs.leftTrigger > .5) this.aim = true;
+ if (this.gamepadInputs.rightTrigger > .5) this.shoot = true;
+ const aimAdjust = this.calculateAimAssist();
+ this.gamepadEuler.y -= this.gamepadInputs.rightJoyX * SettingsManager.settings.controllerSense * deltaTime * aimAdjust * 4;
+ this.gamepadEuler.x -= this.gamepadInputs.rightJoyY * SettingsManager.settings.controllerSense * deltaTime * aimAdjust * 4;
+ this.gamepadEuler.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.gamepadEuler.x));
+ this.localPlayer.lookQuaternion.setFromEuler(this.gamepadEuler);
+ }
+ }
+
+ //touch joystick controls
+ if(Math.hypot(this.touchJoyY, this.touchJoyX) > 0.1) {
+ const touchVectorLength = Math.hypot(this.touchJoyX, this.touchJoyY);
+ speedMultiplier = Math.min(Math.max(touchVectorLength, 0), 1);
+ this.inputX += deltaTimeAcceleration * this.touchJoyX;
+ this.inputZ += deltaTimeAcceleration * this.touchJoyY;
+ }
+
+
+
+
+ const touchSensitivity = 0.03; // Adjust sensitivity as needed
+ this.gamepadEuler.setFromQuaternion(this.localPlayer.lookQuaternion);
+
+ if (!this.localPlayer.chatActive && !this.nameSettingActive) {
+ if (this.getKey('w')) this.inputZ -= deltaTimeAcceleration;
+ if (this.getKey('s')) this.inputZ += deltaTimeAcceleration;
+ if (this.getKey('a')) this.inputX -= deltaTimeAcceleration;
+ if (this.getKey('d')) this.inputX += deltaTimeAcceleration;
+ const aimAdjust = this.calculateAimAssist();
+ if (this.getKey('arrowright')) this.gamepadEuler.y -= SettingsManager.settings.controllerSense * deltaTime * aimAdjust * 4;
+ if (this.getKey('arrowleft')) this.gamepadEuler.y += SettingsManager.settings.controllerSense * deltaTime * aimAdjust * 4;
+ if (this.getKey('arrowup')) this.gamepadEuler.x += SettingsManager.settings.controllerSense * deltaTime * aimAdjust * 4;
+ if (this.getKey('arrowdown')) this.gamepadEuler.x -= SettingsManager.settings.controllerSense * deltaTime * aimAdjust * 4;
+ if (this.getKey(' ')) this.jump = true;
+
+ }
+
+ this.gamepadEuler.y -= this.touchLookX * touchSensitivity;
+ this.gamepadEuler.x -= this.touchLookY * touchSensitivity;
+ this.gamepadEuler.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.gamepadEuler.x));
+ this.localPlayer.lookQuaternion.setFromEuler(this.gamepadEuler);
+
+ switch (this.inputZ - oldInputZ) {
+ case 0:
+ this.inputZ = InputHandler.approachZero(this.inputZ, deltaTimeAcceleration);
+ }
+
+ switch (this.inputX - oldInputX) {
+ case 0:
+ this.inputX = InputHandler.approachZero(this.inputX, deltaTimeAcceleration);
+ }
+
+ if(this.localPlayer.health <= 0) dist = 0; //don't allow movement when health = 0
+
+ this.prevInputVelocity.copy(this.localPlayer.inputVelocity);
+
+ this.localPlayer.inputVelocity.z = dist * this.inputZ;
+ this.localPlayer.inputVelocity.x = dist * this.inputX;
+ this.localPlayer.inputVelocity.y = 0;
+ this.localPlayer.inputVelocity.clampLength(0, this.localPlayer.speed * speedMultiplier);
+ this.localPlayer.inputVelocity.y = this.prevInputVelocity.y;
+ this.inputZ = this.localPlayer.inputVelocity.z;
+ this.inputX = this.localPlayer.inputVelocity.x;
+
+ const euler = new THREE.Euler().setFromQuaternion(this.localPlayer.lookQuaternion, 'YXZ');
+ euler.x = 0;
+ euler.z = 0;
+ this.localPlayer.quaternion.setFromEuler(euler);
+ this.localPlayer.inputVelocity.applyQuaternion(this.localPlayer.quaternion);
+
+ if (this.leftMouseDown || this.touchButtons.includes(0)) this.shoot = true;
+ if (this.rightMouseDown) this.aim = true;
+
+ if(this.localPlayer.playerSpectating !== -1) {
+ this.inputX = 0;
+ this.inputZ = 0;
+ this.jump = false;
+ this.shoot = false;
+ this.aim = false;
+ }
+
+ }
+
+ public getKey(key: string):boolean {
+ return this.keys[key];
+ }
+
+ public setTouchJoyInput(x: number, y: number) {
+ this.touchJoyX = x;
+ this.touchJoyY = y;
+ }
+
+ public setLastTouchLookDelta(x: number, y: number) {
+ this.touchLookX = x;
+ this.touchLookY = y;
+ }
+ public setButtonsHeld(buttons: number[]) {
+ this.touchButtons = buttons;
+ this.jump = this.jump || buttons.includes(-1);
+ this.inventoryIterationTouched = buttons.includes(1);
+
+ }
+
+ public getInventoryIterationTouched() {
+ return this.inventoryIterationTouched;
+ }
+
+ private onKeyDown(event: KeyboardEvent) {
+ //event.preventDefault();
+ if(event.key === 'Tab' || event.key === "'"|| event.key === '/') event.preventDefault();
+ const key = event.key.toLowerCase();
+ this.keys[key] = true;
+
+ if (!this.localPlayer.chatActive && !this.nameSettingActive) {
+ if (key === 'c') {
+ this.leftMouseDown = true;
+ } else if (key === 'z') {
+ this.rightMouseDown = true;
+ }
+ }
+ }
+
+ private onKeyUp(event: KeyboardEvent) {
+ const key = event.key.toLowerCase();
+ this.keys[key] = false;
+
+ if (!this.localPlayer.chatActive && !this.nameSettingActive) {
+ if (key === 'c') {
+ this.leftMouseDown = false;
+ } else if (key === 'z') {
+ this.rightMouseDown = false;
+ }
+ }
+ }
+
+ private onMouseDown(event: MouseEvent) {
+ if (event.button === 0) {
+ this.leftMouseDown = true;
+ } else if (event.button === 2) {
+ this.rightMouseDown = true;
+ }
+ }
+
+ private onMouseUp(event: MouseEvent) {
+ if (event.button === 0) {
+ this.leftMouseDown = false;
+ } else if (event.button === 2) {
+ this.rightMouseDown = false;
+ }
+ }
+
+ public getShoot() {
+ return this.shoot;
+ }
+
+ public getAim() {
+ return this.aim;
+ }
+
+ public getGamepadInputs(): GamepadInputs {
+ return this.gamepadInputs;
+ }
+
+ public deregisterAllKeys(){
+ const locked = document.pointerLockElement === document.body;
+ if(!locked)
+ this.keys = {};
+ }
+
+ public getInputX() {
+ return this.inputX;
+ }
+
+ public getInputZ() {
+ return this.inputZ;
+ }
+
+ private static approachZero(input: number, step: number): number {
+ if (input == 0) {return 0;}
+ let sign: number = 1;
+ if (input < 0) {sign = -1;}
+ const output: number = Math.abs(input) - step;
+ if (output <= 0) {return 0;}
+ return sign * output;
+ }
+
+ private updateGamepadInputArray(gamepad: Gamepad) {
+ if (gamepad.axes[4]) {
+ this.gamepadInputs.leftTrigger = gamepad.axes[4];
+ this.gamepadInputs.rightTrigger = gamepad.axes[5];
+ } else {
+ this.gamepadInputs.leftTrigger = gamepad.buttons[6].value;
+ this.gamepadInputs.rightTrigger = gamepad.buttons[7].value;
+ }
+ this.gamepadInputs.leftJoyX = gamepad.axes[0];
+ this.gamepadInputs.leftJoyY = gamepad.axes[1];
+ this.gamepadInputs.A = gamepad.buttons[0].pressed;
+ this.gamepadInputs.rightJoyX = gamepad.axes[2];
+ this.gamepadInputs.rightJoyY = gamepad.axes[3];
+ this.gamepadInputs.leftShoulder = gamepad.buttons[4].pressed
+ this.gamepadInputs.rightShoulder= gamepad.buttons[5].pressed
+ }
+
+ private calculateAimAssist(): number {
+ if(this.gamepad) {
+ if ((Math.abs(this.gamepadInputs.rightJoyX) >= .1 || Math.abs(this.gamepadInputs.rightJoyY) >= .1)) {
+ if (this.renderer.getPlayerSpheresInCrosshairWithWalls().length > 0) {
+ return .5;
+ }
+ }
+ } else if (this.getKey('arrowup') || this.getKey('arrowdown') || this.getKey('arrowleft') || this.getKey('arrowright')) {
+ if (this.renderer.getPlayerSpheresInCrosshairWithWalls().length > 0) {
+ return .5;
+ }
+ }
+ return 1;
+ }
}
class GamepadInputs {
- leftJoyX: number = 0;
- leftJoyY: number = 0;
- rightJoyX: number = 0;
- rightJoyY: number = 0;
- leftTrigger: number = 0;
- rightTrigger: number = 0;
- leftShoulder: boolean = false;
- rightShoulder: boolean = false;
- A: boolean = false;
- B: boolean = false;
- X: boolean = false;
- Y: boolean = false;
+ leftJoyX: number = 0;
+ leftJoyY: number = 0;
+ rightJoyX: number = 0;
+ rightJoyY: number = 0;
+ leftTrigger: number= 0;
+ rightTrigger: number = 0;
+ leftShoulder: boolean = false;
+ rightShoulder: boolean = false;
+ A: boolean = false;
+ B: boolean = false;
+ X: boolean = false;
+ Y: boolean = false;
}
diff --git a/src/client/input/PointerLockControl.ts b/src/client/input/PointerLockControl.ts
index b761c2b..718ef38 100644
--- a/src/client/input/PointerLockControl.ts
+++ b/src/client/input/PointerLockControl.ts
@@ -1,92 +1,92 @@
import * as THREE from 'three';
-import { Player } from '../core/Player.ts';
-import { SettingsManager } from '../core/SettingsManager.ts';
+import { Player } from "../core/Player.ts";
+import {SettingsManager} from "../core/SettingsManager.ts";
// Define a custom event map interface
interface PointerLockControlEventMap {
- change: Event;
- lock: Event;
- unlock: Event;
+ change: Event;
+ lock: Event;
+ unlock: Event;
}
// Extend the EventDispatcher class to use our custom event map
export class PointerLockControls extends THREE.EventDispatcher {
- public localPlayer: Player;
- public domElement: Element;
- public isLocked: boolean = false;
+ public localPlayer: Player;
+ public domElement: Element;
+ public isLocked: boolean = false;
- constructor(localPlayer: Player, domElement: Element) {
- super();
+ constructor(localPlayer: Player, domElement: Element) {
+ super();
- if (domElement === undefined) {
- console.warn('THREE.PointerLockControls: The second parameter "domElement" is now mandatory.');
- domElement = document.body;
- }
+ if (domElement === undefined) {
+ console.warn('THREE.PointerLockControls: The second parameter "domElement" is now mandatory.');
+ domElement = document.body;
+ }
- this.localPlayer = localPlayer;
- this.domElement = domElement;
+ this.localPlayer = localPlayer;
+ this.domElement = domElement;
- this.connect();
- }
+ this.connect();
+ }
- public connect(): void {
- document.addEventListener('mousemove', this.onMouseMove, false);
- document.addEventListener('pointerlockchange', this.onPointerLockChange, false);
- document.addEventListener('pointerlockerror', this.onPointerLockError, false);
- }
+ public connect(): void {
+ document.addEventListener('mousemove', this.onMouseMove, false);
+ document.addEventListener('pointerlockchange', this.onPointerLockChange, false);
+ document.addEventListener('pointerlockerror', this.onPointerLockError, false);
+ }
- public disconnect(): void {
- document.removeEventListener('mousemove', this.onMouseMove, false);
- document.removeEventListener('pointerlockchange', this.onPointerLockChange, false);
- document.removeEventListener('pointerlockerror', this.onPointerLockError, false);
- }
+ public disconnect(): void {
+ document.removeEventListener('mousemove', this.onMouseMove, false);
+ document.removeEventListener('pointerlockchange', this.onPointerLockChange, false);
+ document.removeEventListener('pointerlockerror', this.onPointerLockError, false);
+ }
- public dispose(): void {
- this.disconnect();
- }
+ public dispose(): void {
+ this.disconnect();
+ }
- public getObject(): Player {
- return this.localPlayer;
- }
+ public getObject(): Player {
+ return this.localPlayer;
+ }
- public getDirection = (v: THREE.Vector3): THREE.Vector3 => {
- return v.copy(new THREE.Vector3(0, 0, -1)).applyQuaternion(this.localPlayer.lookQuaternion);
- };
+ public getDirection = (v: THREE.Vector3): THREE.Vector3 => {
+ return v.copy(new THREE.Vector3(0, 0, -1)).applyQuaternion(this.localPlayer.lookQuaternion);
+ };
- public lock(): void {
- this.domElement.requestPointerLock();
- }
+ public lock(): void {
+ this.domElement.requestPointerLock();
+ }
- public unlock(): void {
- document.exitPointerLock();
- }
+ public unlock(): void {
+ document.exitPointerLock();
+ }
- private onMouseMove = (event: MouseEvent): void => {
- if (!this.isLocked) return;
+ private onMouseMove = (event: MouseEvent): void => {
+ if (!this.isLocked) return;
- // deno-lint-ignore no-explicit-any
- const movementX = event.movementX || (event as any).mozMovementX || (event as any).webkitMovementX || 0;
- // deno-lint-ignore no-explicit-any
- const movementY = event.movementY || (event as any).mozMovementY || (event as any).webkitMovementY || 0;
+ // deno-lint-ignore no-explicit-any
+ const movementX = event.movementX || (event as any).mozMovementX || (event as any).webkitMovementX || 0;
+ // deno-lint-ignore no-explicit-any
+ const movementY = event.movementY || (event as any).mozMovementY || (event as any).webkitMovementY || 0;
- const euler = new THREE.Euler(0, 0, 0, 'YXZ');
- euler.setFromQuaternion(this.localPlayer.lookQuaternion);
+ const euler = new THREE.Euler(0, 0, 0, 'YXZ');
+ euler.setFromQuaternion(this.localPlayer.lookQuaternion);
- euler.y -= movementX * SettingsManager.settings.sense * .002;
- euler.x -= movementY * SettingsManager.settings.sense * .002;
+ euler.y -= movementX * SettingsManager.settings.sense * .002;
+ euler.x -= movementY * SettingsManager.settings.sense * .002;
- euler.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, euler.x));
+ euler.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, euler.x));
- this.localPlayer.lookQuaternion.setFromEuler(euler);
+ this.localPlayer.lookQuaternion.setFromEuler(euler);
- // this.dispatchEvent({ type: 'change' });
- };
+ // this.dispatchEvent({ type: 'change' });
+ };
- private onPointerLockChange = (): void => {
- this.isLocked = document.pointerLockElement === this.domElement;
- };
+ private onPointerLockChange = (): void => {
+ this.isLocked = document.pointerLockElement === this.domElement;
+ };
- private onPointerLockError = (): void => {
- console.error('THREE.PointerLockControls: Unable to use Pointer Lock API');
- };
+ private onPointerLockError = (): void => {
+ console.error('THREE.PointerLockControls: Unable to use Pointer Lock API');
+ };
}
diff --git a/src/client/input/TouchInputHandler.ts b/src/client/input/TouchInputHandler.ts
index 36f1103..ffb6bbf 100644
--- a/src/client/input/TouchInputHandler.ts
+++ b/src/client/input/TouchInputHandler.ts
@@ -1,174 +1,190 @@
-import { InputHandler } from './InputHandler.ts';
-import { ChatOverlay } from '../ui/ChatOverlay.ts';
+import {InputHandler} from "./InputHandler.ts";
+import {ChatOverlay} from "../ui/ChatOverlay.ts";
export class TouchInputHandler {
- public static joystickRadius = 30;
-
- private lastTouchTimestamp: number = 0;
- private inputHandler: InputHandler;
- private chatOverlay: ChatOverlay;
-
- private joystickX: number = 0;
- private joystickY: number = 0;
-
- private joystickInputX: number = 0;
- private joystickInputY: number = 0;
-
- private joystickFingerId: number = -1;
- private lookFingerId: number = -1;
-
- private lastLookX: number = 0;
- private lastLookY: number = 0;
-
- private lastLookChangeX: number = 0;
- private lastLookChangeY: number = 0;
-
- private buttonsHeld: { button: number; fingerId: number }[] = [];
-
- constructor(inputHandler: InputHandler, chatOverlay: ChatOverlay) {
- this.inputHandler = inputHandler;
- this.chatOverlay = chatOverlay;
- this.setupEventListeners();
- }
-
- public onFrame() {
- this.chatOverlay.setLastTouchTimestamp(this.lastTouchTimestamp);
- this.chatOverlay.setTouchJoystickEngaged(this.joystickFingerId !== -1);
- this.chatOverlay.setJoystickPosition(this.joystickX, this.joystickY);
- this.chatOverlay.setJoystickInput(this.joystickInputX, this.joystickInputY);
- this.chatOverlay.setButtonsHeld(this.buttonsHeld.map((button) => button.button));
-
- this.inputHandler.setTouchJoyInput(this.joystickInputX, this.joystickInputY);
- this.inputHandler.setLastTouchLookDelta(this.lastLookChangeX, this.lastLookChangeY);
- this.inputHandler.setButtonsHeld(this.buttonsHeld.map((button) => button.button));
- this.lastLookChangeX = 0;
- this.lastLookChangeY = 0;
- if (this.joystickFingerId == -1) {
- this.joystickInputX = 0;
- this.joystickInputY = 0;
- }
- }
-
- private setupEventListeners() {
- document.addEventListener('touchstart', this.onTouchStart.bind(this), false);
- document.addEventListener('touchmove', this.onTouchMove.bind(this), false);
- document.addEventListener('touchend', this.onTouchEnd.bind(this), false);
- }
-
- private onTouchStart(event: TouchEvent) {
- this.lastTouchTimestamp = Date.now() / 1000;
-
- for (let i = 0; i < event.touches.length; i++) {
- const pixelRatio = this.getPixelRatio();
- const touchX = event.touches[i].clientX * pixelRatio;
- const touchY = event.touches[i].clientY * pixelRatio;
- // console.log(touchX, touchY);
-
- if (this.joystickFingerId == -1) {
- if (touchX < globalThis.innerWidth * pixelRatio / 2.5) {
- this.joystickFingerId = event.touches[i].identifier;
- this.joystickX = touchX;
- this.joystickY = touchY;
- continue;
- }
- }
- if (this.lookFingerId == -1) {
- if (touchX > globalThis.innerWidth * pixelRatio / 2.5 && touchX < globalThis.innerWidth * pixelRatio - 30) {
- this.lookFingerId = event.touches[i].identifier;
- this.lastLookX = touchX;
- this.lastLookY = touchY;
- continue;
- }
- }
- }
- this.onTouchMove(event);
- }
-
- private onTouchMove(event: TouchEvent) {
- this.lastTouchTimestamp = Date.now() / 1000;
-
- for (let i = 0; i < event.touches.length; i++) {
- const pixelRatio = this.getPixelRatio();
- const touchX = event.touches[i].clientX * pixelRatio;
- const touchY = event.touches[i].clientY * pixelRatio;
-
- if (event.touches[i].identifier === this.joystickFingerId) {
- this.joystickInputX = (touchX - this.joystickX) / TouchInputHandler.joystickRadius;
- this.joystickInputY = (touchY - this.joystickY) / TouchInputHandler.joystickRadius;
-
- const mag = Math.sqrt(this.joystickInputX * this.joystickInputX + this.joystickInputY * this.joystickInputY);
- const dir = Math.atan2(this.joystickInputY, this.joystickInputX);
- if (mag > 1) {
- this.joystickInputX = Math.cos(dir);
- this.joystickInputY = Math.sin(dir);
- }
- continue;
- }
-
- if (event.touches[i].identifier === this.lookFingerId) {
- this.lastLookChangeX = touchX - this.lastLookX;
- this.lastLookChangeY = touchY - this.lastLookY;
-
- this.lastLookX = touchX;
- this.lastLookY = touchY;
- continue;
- }
-
- if (touchX > globalThis.innerWidth * pixelRatio - 30) {
- const buttonClosestTo = Math.round((touchY - 100) / 30);
-
- let found = false;
- for (let j = 0; j < this.buttonsHeld.length; j++) {
- if (this.buttonsHeld[j].button === buttonClosestTo) {
- this.buttonsHeld[j].fingerId = event.touches[i].identifier;
- found = true;
- }
- }
- if (!found) {
- this.buttonsHeld.push({ button: buttonClosestTo, fingerId: event.touches[i].identifier });
- }
- }
- }
- }
-
- private onTouchEnd(event: TouchEvent) {
- if (event.touches.length === 0) {
- this.joystickFingerId = -1;
- this.lookFingerId = -1;
- }
- for (let i = 0; i < event.changedTouches.length; i++) {
- if (event.changedTouches[i].identifier === this.joystickFingerId) {
- this.joystickFingerId = -1;
- }
- if (event.changedTouches[i].identifier === this.lookFingerId) {
- this.lookFingerId = -1;
- }
-
- const touchY = event.changedTouches[i].clientY * this.getPixelRatio();
- const buttonClosestTo = Math.round((touchY - 100) / 30);
-
- // Iterate backwards to safely remove elements
- for (let j = this.buttonsHeld.length - 1; j >= 0; j--) {
- if (
- this.buttonsHeld[j].fingerId === event.changedTouches[i].identifier ||
- this.buttonsHeld[j].button === buttonClosestTo
- ) {
- this.buttonsHeld.splice(j, 1);
- }
- }
- }
- }
-
- public getButtonState(button: number): boolean {
- for (let i = 0; i < this.buttonsHeld.length; i++) {
- if (this.buttonsHeld[i].button === button && this.buttonsHeld[i].fingerId !== -1) {
- return true;
- }
- }
- return false;
- }
-
- private getPixelRatio(): number {
- return 200 / globalThis.innerHeight;
- }
+ public static joystickRadius = 30;
+
+ private lastTouchTimestamp: number = 0;
+ private inputHandler: InputHandler;
+ private chatOverlay: ChatOverlay;
+
+ private joystickX: number = 0;
+ private joystickY: number = 0;
+
+ private joystickInputX: number = 0;
+ private joystickInputY: number = 0;
+
+ private joystickFingerId: number = -1;
+ private lookFingerId: number = -1;
+
+ private lastLookX: number = 0;
+ private lastLookY: number = 0;
+
+ private lastLookChangeX: number = 0;
+ private lastLookChangeY: number = 0;
+
+ private buttonsHeld: {button:number, fingerId: number}[] = [];
+
+
+ constructor(inputHandler: InputHandler, chatOverlay: ChatOverlay) {
+ this.inputHandler = inputHandler;
+ this.chatOverlay = chatOverlay
+ this.setupEventListeners();
+
+
+ }
+
+ public onFrame() {
+ this.chatOverlay.setLastTouchTimestamp(this.lastTouchTimestamp);
+ this.chatOverlay.setTouchJoystickEngaged(this.joystickFingerId !== -1);
+ this.chatOverlay.setJoystickPosition(this.joystickX, this.joystickY);
+ this.chatOverlay.setJoystickInput(this.joystickInputX, this.joystickInputY);
+ this.chatOverlay.setButtonsHeld(this.buttonsHeld.map((button) => button.button));
+
+ this.inputHandler.setTouchJoyInput(this.joystickInputX, this.joystickInputY);
+ this.inputHandler.setLastTouchLookDelta(this.lastLookChangeX, this.lastLookChangeY);
+ this.inputHandler.setButtonsHeld(this.buttonsHeld.map((button) => button.button));
+ this.lastLookChangeX = 0; this.lastLookChangeY = 0;
+ if(this.joystickFingerId == -1) {
+ this.joystickInputX = 0;
+ this.joystickInputY = 0;
+ }
+ }
+
+ private setupEventListeners() {
+ document.addEventListener('touchstart', this.onTouchStart.bind(this), false);
+ document.addEventListener('touchmove', this.onTouchMove.bind(this), false);
+ document.addEventListener('touchend', this.onTouchEnd.bind(this), false);
+ }
+
+ private onTouchStart(event: TouchEvent) {
+ this.lastTouchTimestamp = Date.now()/1000;
+
+ for (let i = 0; i < event.touches.length; i++) {
+
+ const pixelRatio = this.getPixelRatio();
+ const touchX = event.touches[i].clientX * pixelRatio;
+ const touchY = event.touches[i].clientY * pixelRatio;
+ // console.log(touchX, touchY);
+
+ if(this.joystickFingerId == -1){
+ if (touchX < globalThis.innerWidth * pixelRatio / 2.5) {
+
+ this.joystickFingerId = event.touches[i].identifier;
+ this.joystickX = touchX;
+ this.joystickY = touchY;
+ continue;
+ }
+ }
+ if(this.lookFingerId == -1){
+ if (touchX > globalThis.innerWidth * pixelRatio / 2.5 && touchX < globalThis.innerWidth * pixelRatio - 30) {
+ this.lookFingerId = event.touches[i].identifier;
+ this.lastLookX = touchX;
+ this.lastLookY = touchY;
+ continue;
+ }
+ }
+
+
+ }
+ this.onTouchMove(event);
+ }
+
+ private onTouchMove(event: TouchEvent) {
+ this.lastTouchTimestamp = Date.now()/1000;
+
+ for (let i = 0; i < event.touches.length; i++) {
+
+ const pixelRatio = this.getPixelRatio();
+ const touchX = event.touches[i].clientX * pixelRatio;
+ const touchY = event.touches[i].clientY * pixelRatio;
+
+ if (event.touches[i].identifier === this.joystickFingerId) {
+ this.joystickInputX = (touchX - this.joystickX) / TouchInputHandler.joystickRadius;
+ this.joystickInputY = (touchY - this.joystickY) / TouchInputHandler.joystickRadius;
+
+ const mag = Math.sqrt(this.joystickInputX * this.joystickInputX + this.joystickInputY * this.joystickInputY);
+ const dir = Math.atan2(this.joystickInputY, this.joystickInputX);
+ if(mag > 1) {
+ this.joystickInputX = Math.cos(dir);
+ this.joystickInputY = Math.sin(dir);
+ }
+ continue;
+ }
+
+ if (event.touches[i].identifier === this.lookFingerId) {
+
+ this.lastLookChangeX = touchX - this.lastLookX;
+ this.lastLookChangeY = touchY - this.lastLookY;
+
+ this.lastLookX = touchX;
+ this.lastLookY = touchY;
+ continue;
+ }
+
+ if(touchX > globalThis.innerWidth * pixelRatio - 30) {
+ const buttonClosestTo = Math.round((touchY - 100) / 30);
+
+ let found = false;
+ for(let j = 0; j < this.buttonsHeld.length; j++)
+ if(this.buttonsHeld[j].button === buttonClosestTo){
+ this.buttonsHeld[j].fingerId = event.touches[i].identifier;
+ found = true;
+ }
+ if(!found)
+ this.buttonsHeld.push({button: buttonClosestTo, fingerId: event.touches[i].identifier});
+
+
+
+ }
+
+
+
+
+ }
+
+ }
+
+ private onTouchEnd(event: TouchEvent) {
+ if(event.touches.length === 0) {
+ this.joystickFingerId = -1;
+ this.lookFingerId = -1;
+ }
+ for (let i = 0; i < event.changedTouches.length; i++) {
+ if (event.changedTouches[i].identifier === this.joystickFingerId) {
+ this.joystickFingerId = -1;
+ }
+ if (event.changedTouches[i].identifier === this.lookFingerId) {
+ this.lookFingerId = -1;
+ }
+
+ const touchY = event.changedTouches[i].clientY * this.getPixelRatio();
+ const buttonClosestTo = Math.round((touchY - 100) / 30);
+
+ // Iterate backwards to safely remove elements
+ for (let j = this.buttonsHeld.length - 1; j >= 0; j--) {
+ if (this.buttonsHeld[j].fingerId === event.changedTouches[i].identifier || this.buttonsHeld[j].button === buttonClosestTo) {
+ this.buttonsHeld.splice(j, 1);
+ }
+ }
+ }
+
+
+ }
+
+ public getButtonState(button: number): boolean {
+ for(let i = 0; i < this.buttonsHeld.length; i++)
+ if(this.buttonsHeld[i].button === button && this.buttonsHeld[i].fingerId !== -1)
+ return true;
+ return false;
+
+ }
+
+ private getPixelRatio(): number {
+ return 200 / globalThis.innerHeight;
+ }
+
+
+
+
}
diff --git a/src/client/items/BananaGun.ts b/src/client/items/BananaGun.ts
index 81a54ad..9fba619 100644
--- a/src/client/items/BananaGun.ts
+++ b/src/client/items/BananaGun.ts
@@ -1,9 +1,9 @@
-import { ItemBase, ItemType } from './ItemBase.ts';
-import { HeldItemInput } from '../input/HeldItemInput.ts';
+import {ItemBase, ItemType} from './ItemBase.ts';
+import {HeldItemInput} from '../input/HeldItemInput.ts';
import * as THREE from 'three';
-import { Renderer } from '../core/Renderer.ts';
-import { Networking } from '../core/Networking.ts';
-import { AssetManager } from '../core/AssetManager.ts';
+import {Renderer} from '../core/Renderer.ts';
+import {Networking} from '../core/Networking.ts';
+import { AssetManager } from "../core/AssetManager.ts";
const firingDelay = 0.12;
const firingDelayHeld = 0.225; //longer firing delay when mouse is held down
@@ -16,194 +16,196 @@ const scopedQuaternion = new THREE.Quaternion(0.64, 0.22, -0.69, -0.22);
const inventoryQuaternionBase = new THREE.Quaternion(0, 0, 0, 1);
export class BananaGun extends ItemBase {
- private renderer!: Renderer;
- private networking!: Networking;
- private lastInput: HeldItemInput;
- private lastFired: number;
- private addedToHandScene: boolean;
-
- // deno-lint-ignore constructor-super
- constructor(renderer: Renderer, networking: Networking, index: number, itemType: ItemType) {
- if (itemType === ItemType.WorldItem) {
- super(itemType, renderer.getEntityScene(), renderer.getInventoryMenuScene(), index);
- } else {
- super(itemType, renderer.getHeldItemScene(), renderer.getInventoryMenuScene(), index);
- }
- this.renderer = renderer;
- this.networking = networking;
- this.lastInput = new HeldItemInput();
- this.addedToHandScene = false;
- this.lastFired = 0;
- }
-
- public override init() {
- AssetManager.getInstance().loadAsset('models/simplified_banana_1.glb', (scene) => {
- this.object = scene;
- if (this.itemType === ItemType.InventoryItem) {
- this.object.traverse((child) => {
- if ((child as THREE.Mesh).isMesh) {
- child.renderOrder = 999;
- const mesh = child as THREE.Mesh;
- if (Array.isArray(mesh.material)) {
- mesh.material.forEach((mat) => mat.depthTest = false);
- } else {
- mesh.material.depthTest = false;
- }
- }
- });
- }
-
- if (this.itemType === ItemType.WorldItem) {
- this.object.scale.set(0.66, 0.66, 0.66);
- }
-
- this.inventoryMenuObject = this.object.clone();
- this.inventoryMenuObject.scale.set(0.8, 0.8, 0.8);
-
- if (this.itemType === ItemType.WorldItem) {
- this.object.scale.set(0.45, 0.45, 0.45);
- }
- });
- }
-
- public override onFrame(input: HeldItemInput, selectedIndex: number) {
- if (!this.object) return;
- const deltaTime = this.clock.getDelta();
- this.timeAccum += deltaTime;
- this.angleAccum += deltaTime;
-
- if (this.itemType === ItemType.WorldItem) {
- this.worldOnFrame(deltaTime);
- } else if (this.itemType === ItemType.InventoryItem) {
- this.inventoryOnFrame(deltaTime, selectedIndex);
- this.handOnFrame(deltaTime, input);
- }
- }
-
- // No need to override worldOnFrame if default behavior is sufficient
- // If specific behavior is needed, you can override it here
-
- public override inventoryOnFrame(deltaTime: number, selectedIndex: number) {
- if (!this.addedToInventoryItemScenes) {
- this.inventoryMenuScene.add(this.inventoryMenuObject);
- this.addedToInventoryItemScenes = true;
- }
-
- this.angleAccum += deltaTime;
- this.inventoryMenuObject.position.set(0, this.index, 0);
-
- const targetQuaternion = inventoryQuaternionBase.clone();
- if (this.index === selectedIndex) {
- rotateAroundWorldAxis(targetQuaternion, new THREE.Vector3(0, 1, 0), this.angleAccum * 4);
- this.showInHand();
- } else {
- this.hideInHand();
- }
- this.inventoryMenuObject.quaternion.slerp(targetQuaternion, 0.1 * 60 * deltaTime);
- }
-
- public override handOnFrame(deltaTime: number, input: HeldItemInput) {
- if (!this.object) return;
-
- if (this.shownInHand && !this.addedToHandScene) {
- this.scene.add(this.object);
- this.addedToHandScene = true;
- }
-
- if (this.shownInHand && Date.now() / 1000 - this.shownInHandTimestamp > showInHandDelay) {
- this.handleInput(input, deltaTime);
- } else {
- this.handPosition.lerp(hiddenPosition, 0.1 * 60 * deltaTime);
- this.object.position.copy(this.handPosition);
- // Remove the object after it has slid out of view
- if (this.handPosition.distanceTo(hiddenPosition) < 0.1) {
- if (this.addedToHandScene) {
- this.scene.remove(this.object);
- this.addedToHandScene = false;
- }
- }
- }
-
- // Update crosshair flashing based on last shot timestamp
- this.renderer.crosshairIsFlashing = Date.now() / 1000 - this.renderer.lastShotSomeoneTimestamp < 0.05;
- }
-
- private handleInput(input: HeldItemInput, deltaTime: number) {
- if (input.rightClick) {
- moveTowardsPos(this.handPosition, scopedPosition, 0.3 * deltaTime * 60);
- } else {
- moveTowardsPos(this.handPosition, unscopedPosition, 0.1 * deltaTime * 60);
- }
-
- this.object.position.copy(this.handPosition);
-
- moveTowardsRot(this.object.quaternion, scopedQuaternion, 0.1 * deltaTime * 60);
-
- if (input.leftClick && (!this.lastInput.leftClick || Date.now() / 1000 - this.lastFired > firingDelayHeld)) {
- if (Date.now() / 1000 - this.lastFired > firingDelay) {
- this.lastFired = Date.now() / 1000;
- this.shootBanana();
- this.handPosition.add(new THREE.Vector3(0, 0, 0.6));
- rotateAroundWorldAxis(this.object.quaternion, new THREE.Vector3(1, 0, 0), Math.PI / 16);
- }
- }
-
- this.lastInput = input;
- }
-
- public override showInHand() {
- if (this.shownInHand) return;
- this.shownInHand = true;
- this.shownInHandTimestamp = Date.now() / 1000;
- if (!this.addedToHandScene && this.object) {
- this.scene.add(this.object);
- this.addedToHandScene = true;
- }
- }
-
- public override hideInHand() {
- if (!this.shownInHand) return;
- this.shownInHand = false;
- }
- public itemDepleted(): boolean {
- return false;
- }
-
- private shootBanana() {
- const processShots = () => {
- const shotVectors = this.renderer.getShotVectorsToPlayersInCrosshair();
- if (shotVectors.length > 0) {
- for (const shot of shotVectors) {
- const { playerID, hitPoint } = shot;
- this.networking.applyDamage(playerID, 17);
- this.renderer.playerHitMarkers.push({ hitPoint: hitPoint, shotVector: shot.vector, timestamp: -1 });
- }
- this.renderer.lastShotSomeoneTimestamp = Date.now() / 1000;
- }
- };
-
- if (typeof requestIdleCallback === 'function') {
- requestIdleCallback(processShots, { timeout: 150 });
- } else {
- setTimeout(processShots, 0);
- }
- }
-
- // Method to set world position when used as WorldItem
- public override setWorldPosition(vector: THREE.Vector3) {
- super.setWorldPosition(vector);
- }
+ private renderer!: Renderer;
+ private networking!: Networking;
+ private lastInput: HeldItemInput;
+ private lastFired: number;
+ private addedToHandScene: boolean;
+
+ // deno-lint-ignore constructor-super
+ constructor(renderer: Renderer, networking: Networking, index: number, itemType: ItemType) {
+ if(itemType === ItemType.WorldItem)
+ super(itemType, renderer.getEntityScene(), renderer.getInventoryMenuScene(), index);
+ else
+ super(itemType, renderer.getHeldItemScene(), renderer.getInventoryMenuScene(), index);
+ this.renderer = renderer;
+ this.networking = networking;
+ this.lastInput = new HeldItemInput();
+ this.addedToHandScene = false;
+ this.lastFired = 0;
+
+
+ }
+
+ public override init() {
+ AssetManager.getInstance().loadAsset('models/simplified_banana_1.glb', (scene) => {
+ this.object = scene;
+ if (this.itemType === ItemType.InventoryItem) {
+ this.object.traverse((child) => {
+ if ((child as THREE.Mesh).isMesh) {
+ child.renderOrder = 999;
+ const mesh = child as THREE.Mesh;
+ if (Array.isArray(mesh.material)) {
+ mesh.material.forEach(mat => mat.depthTest = false);
+ } else {
+ mesh.material.depthTest = false;
+ }
+ }
+ });
+ }
+
+ if(this.itemType === ItemType.WorldItem)
+ this.object.scale.set(0.66, 0.66, 0.66);
+
+ this.inventoryMenuObject = this.object.clone();
+ this.inventoryMenuObject.scale.set(0.8, 0.8, 0.8);
+
+ if(this.itemType === ItemType.WorldItem)
+ this.object.scale.set(0.45, 0.45, 0.45);
+ });
+ }
+
+
+ public override onFrame(input: HeldItemInput, selectedIndex: number) {
+ if (!this.object) return;
+ const deltaTime = this.clock.getDelta();
+ this.timeAccum += deltaTime;
+ this.angleAccum += deltaTime;
+
+ if (this.itemType === ItemType.WorldItem) {
+ this.worldOnFrame(deltaTime);
+ } else if (this.itemType === ItemType.InventoryItem) {
+ this.inventoryOnFrame(deltaTime, selectedIndex);
+ this.handOnFrame(deltaTime, input);
+ }
+ }
+
+ // No need to override worldOnFrame if default behavior is sufficient
+ // If specific behavior is needed, you can override it here
+
+ public override inventoryOnFrame(deltaTime: number, selectedIndex: number) {
+ if (!this.addedToInventoryItemScenes) {
+ this.inventoryMenuScene.add(this.inventoryMenuObject);
+ this.addedToInventoryItemScenes = true;
+ }
+
+ this.angleAccum += deltaTime;
+ this.inventoryMenuObject.position.set(0, this.index, 0);
+
+ const targetQuaternion = inventoryQuaternionBase.clone();
+ if (this.index === selectedIndex) {
+ rotateAroundWorldAxis(targetQuaternion, new THREE.Vector3(0, 1, 0), this.angleAccum * 4);
+ this.showInHand();
+ } else {
+ this.hideInHand();
+ }
+ this.inventoryMenuObject.quaternion.slerp(targetQuaternion, 0.1 * 60 * deltaTime);
+ }
+
+ public override handOnFrame(deltaTime: number, input: HeldItemInput) {
+ if (!this.object) return;
+
+ if (this.shownInHand && !this.addedToHandScene) {
+ this.scene.add(this.object);
+ this.addedToHandScene = true;
+ }
+
+ if (this.shownInHand && Date.now() / 1000 - this.shownInHandTimestamp > showInHandDelay) {
+ this.handleInput(input, deltaTime);
+ } else {
+ this.handPosition.lerp(hiddenPosition, 0.1 * 60 * deltaTime);
+ this.object.position.copy(this.handPosition);
+ // Remove the object after it has slid out of view
+ if (this.handPosition.distanceTo(hiddenPosition) < 0.1) {
+ if (this.addedToHandScene) {
+ this.scene.remove(this.object);
+ this.addedToHandScene = false;
+ }
+ }
+ }
+
+ // Update crosshair flashing based on last shot timestamp
+ this.renderer.crosshairIsFlashing = Date.now() / 1000 - this.renderer.lastShotSomeoneTimestamp < 0.05;
+ }
+
+ private handleInput(input: HeldItemInput, deltaTime: number) {
+ if (input.rightClick)
+ moveTowardsPos(this.handPosition, scopedPosition, 0.3 * deltaTime * 60);
+ else
+ moveTowardsPos(this.handPosition, unscopedPosition, 0.1 * deltaTime * 60);
+
+ this.object.position.copy(this.handPosition);
+
+ moveTowardsRot(this.object.quaternion, scopedQuaternion, 0.1 * deltaTime * 60);
+
+ if (input.leftClick && (!this.lastInput.leftClick || Date.now() / 1000 - this.lastFired > firingDelayHeld)) {
+ if (Date.now() / 1000 - this.lastFired > firingDelay) {
+ this.lastFired = Date.now() / 1000;
+ this.shootBanana();
+ this.handPosition.add(new THREE.Vector3(0, 0, 0.6));
+ rotateAroundWorldAxis(this.object.quaternion, new THREE.Vector3(1, 0, 0), Math.PI / 16);
+ }
+ }
+
+ this.lastInput = input;
+ }
+
+ public override showInHand() {
+ if (this.shownInHand) return;
+ this.shownInHand = true;
+ this.shownInHandTimestamp = Date.now() / 1000;
+ if (!this.addedToHandScene && this.object) {
+ this.scene.add(this.object);
+ this.addedToHandScene = true;
+ }
+ }
+
+ public override hideInHand() {
+ if (!this.shownInHand) return;
+ this.shownInHand = false;
+ }
+ public itemDepleted(): boolean {
+ return false;
+ }
+
+ private shootBanana() {
+ const processShots = () => {
+ const shotVectors = this.renderer.getShotVectorsToPlayersInCrosshair();
+ if (shotVectors.length > 0) {
+ for (const shot of shotVectors) {
+ const { playerID, hitPoint } = shot;
+ this.networking.applyDamage(playerID, 17);
+ this.renderer.playerHitMarkers.push({hitPoint: hitPoint, shotVector: shot.vector, timestamp: -1});
+ }
+ this.renderer.lastShotSomeoneTimestamp = Date.now() / 1000;
+ }
+ };
+
+ if (typeof requestIdleCallback === 'function') {
+ requestIdleCallback(processShots, { timeout: 150 });
+ } else {
+ setTimeout(processShots, 0);
+ }
+ }
+
+
+
+
+ // Method to set world position when used as WorldItem
+ public override setWorldPosition(vector: THREE.Vector3) {
+ super.setWorldPosition(vector);
+ }
}
function rotateAroundWorldAxis(source: THREE.Quaternion, axis: THREE.Vector3, angle: number) {
- const rotationQuat = new THREE.Quaternion().setFromAxisAngle(axis, angle);
- source.multiplyQuaternions(rotationQuat, source);
+ const rotationQuat = new THREE.Quaternion().setFromAxisAngle(axis, angle);
+ source.multiplyQuaternions(rotationQuat, source);
}
function moveTowardsPos(source: THREE.Vector3, target: THREE.Vector3, frac: number) {
- source.lerp(target, frac);
+ source.lerp(target, frac);
}
function moveTowardsRot(source: THREE.Quaternion, target: THREE.Quaternion, frac: number) {
- source.slerp(target, frac);
+ source.slerp(target, frac);
}
diff --git a/src/client/items/FishGun.ts b/src/client/items/FishGun.ts
index 42d5ab2..010bc07 100644
--- a/src/client/items/FishGun.ts
+++ b/src/client/items/FishGun.ts
@@ -1,10 +1,10 @@
-import { ItemBase, ItemType } from './ItemBase.ts';
-import { HeldItemInput } from '../input/HeldItemInput.ts';
+import {ItemBase, ItemType} from './ItemBase.ts';
+import {HeldItemInput} from '../input/HeldItemInput.ts';
import * as THREE from 'three';
-import { Renderer } from '../core/Renderer.ts';
-import { Networking } from '../core/Networking.ts';
-import { AssetManager } from '../core/AssetManager.ts';
+import {Renderer} from '../core/Renderer.ts';
+import {Networking} from '../core/Networking.ts';
+import { AssetManager } from "../core/AssetManager.ts";
const firingDelay = 0.45;
const firingDelayHeld = 0.45; //longer firing delay when mouse is held down
@@ -13,235 +13,241 @@ const showInHandDelay = 0.1;
const scopedPosition = new THREE.Vector3(0, -0.6, 3.5);
const unscopedPosition = new THREE.Vector3(0.75, -0.9, 3.2);
const hiddenPosition = new THREE.Vector3(0.85, -2.7, 3.2);
-const scopedQuaternion = new THREE.Quaternion(0, 0.707, 0, 0.707);
+const scopedQuaternion = new THREE.Quaternion(0,0.707,0,0.707);
const inventoryQuaternionBase = new THREE.Quaternion(0, 0, 0, 1);
export class FishGun extends ItemBase {
- private renderer!: Renderer;
- private networking!: Networking;
- private lastInput: HeldItemInput;
- private lastFired: number;
- private addedToHandScene: boolean;
-
- // deno-lint-ignore constructor-super
- constructor(renderer: Renderer, networking: Networking, index: number, itemType: ItemType) {
- if (itemType === ItemType.WorldItem) {
- super(itemType, renderer.getEntityScene(), renderer.getInventoryMenuScene(), index);
- } else {
- super(itemType, renderer.getHeldItemScene(), renderer.getInventoryMenuScene(), index);
- }
- this.renderer = renderer;
- this.networking = networking;
- this.lastInput = new HeldItemInput();
- this.addedToHandScene = false;
- this.lastFired = 0;
- }
-
- public override init() {
- AssetManager.getInstance().loadAsset('models/simplified_fish.glb', (scene) => {
- this.object = scene;
- if (this.itemType === ItemType.InventoryItem) {
- this.object.traverse((child) => {
- if ((child as THREE.Mesh).isMesh) {
- child.renderOrder = 999;
- const mesh = child as THREE.Mesh;
- if (Array.isArray(mesh.material)) {
- mesh.material.forEach((mat) => mat.depthTest = false);
- } else {
- mesh.material.depthTest = false;
- }
- }
- });
- if (this.itemType === ItemType.InventoryItem) {
- this.object.scale.set(1.5, 1.5, 1.5);
- }
- }
-
- this.inventoryMenuObject = this.object.clone();
- this.inventoryMenuObject.scale.set(0.8, 0.8, 0.8);
-
- if (this.itemType === ItemType.WorldItem) {
- this.object.scale.set(0.45, 0.45, 0.45);
- }
- });
- }
-
- public override onFrame(input: HeldItemInput, selectedIndex: number) {
- if (!this.object) return;
- const deltaTime = this.clock.getDelta();
- this.timeAccum += deltaTime;
- this.angleAccum += deltaTime;
-
- if (this.itemType === ItemType.WorldItem) {
- this.worldOnFrame(deltaTime);
- } else if (this.itemType === ItemType.InventoryItem) {
- this.inventoryOnFrame(deltaTime, selectedIndex);
- this.handOnFrame(deltaTime, input);
- }
- }
-
- // No need to override worldOnFrame if default behavior is sufficient
- // If specific behavior is needed, you can override it here
-
- public override inventoryOnFrame(deltaTime: number, selectedIndex: number) {
- if (!this.addedToInventoryItemScenes) {
- this.inventoryMenuScene.add(this.inventoryMenuObject);
- this.addedToInventoryItemScenes = true;
- }
-
- this.angleAccum += deltaTime;
- this.inventoryMenuObject.position.set(0, this.index, 0);
-
- const targetQuaternion = inventoryQuaternionBase.clone();
- if (this.index === selectedIndex) {
- rotateAroundWorldAxis(targetQuaternion, new THREE.Vector3(0, 1, 0), this.angleAccum * 4);
- this.showInHand();
- } else {
- this.hideInHand();
- }
- this.inventoryMenuObject.quaternion.slerp(targetQuaternion, 0.1 * 60 * deltaTime);
- }
-
- public override handOnFrame(deltaTime: number, input: HeldItemInput) {
- if (!this.object) return;
-
- if (this.shownInHand && !this.addedToHandScene) {
- this.scene.add(this.object);
- this.addedToHandScene = true;
- }
-
- if (this.shownInHand && Date.now() / 1000 - this.shownInHandTimestamp > showInHandDelay) {
- this.handleInput(input, deltaTime);
- } else {
- this.handPosition.lerp(hiddenPosition, 0.1 * 60 * deltaTime);
- this.object.position.copy(this.handPosition);
- // Remove the object after it has slid out of view
- if (this.handPosition.distanceTo(hiddenPosition) < 0.1) {
- if (this.addedToHandScene) {
- this.scene.remove(this.object);
- this.addedToHandScene = false;
- }
- }
- }
-
- // Update crosshair flashing based on last shot timestamp
- this.renderer.crosshairIsFlashing = Date.now() / 1000 - this.renderer.lastShotSomeoneTimestamp < 0.05;
- }
-
- private handleInput(input: HeldItemInput, deltaTime: number) {
- if (input.rightClick) {
- moveTowardsPos(this.handPosition, scopedPosition, 0.3 * deltaTime * 60);
- } else {
- moveTowardsPos(this.handPosition, unscopedPosition, 0.1 * deltaTime * 60);
- }
-
- this.object.position.copy(this.handPosition);
-
- moveTowardsRot(this.object.quaternion, scopedQuaternion, 0.1 * deltaTime * 60);
-
- if (input.leftClick && (!this.lastInput.leftClick || Date.now() / 1000 - this.lastFired > firingDelayHeld)) {
- if (Date.now() / 1000 - this.lastFired > firingDelay) {
- this.lastFired = Date.now() / 1000;
- this.shootFish();
- this.handPosition.add(new THREE.Vector3(0, 0, 2));
- rotateAroundWorldAxis(this.object.quaternion, new THREE.Vector3(1, 0, 0), Math.PI / 16);
- }
- }
-
- this.lastInput = input;
- }
-
- public override showInHand() {
- if (this.shownInHand) return;
- this.shownInHand = true;
- this.shownInHandTimestamp = Date.now() / 1000;
- if (!this.addedToHandScene && this.object) {
- this.scene.add(this.object);
- this.addedToHandScene = true;
- }
- }
-
- public override hideInHand() {
- if (!this.shownInHand) return;
- this.shownInHand = false;
- }
- public itemDepleted(): boolean {
- return false;
- }
-
- private shootFish() {
- const totalShots = 25;
- let processedShots = 0;
- const TIMEOUT = 150;
-
- const processShots = (deadline?: IdleDeadline) => {
- const timeRemaining = deadline ? deadline.timeRemaining() : 16;
-
- while (processedShots < totalShots && timeRemaining > 0) {
- const shotVectors = this.renderer.getShotVectorsToPlayersWithOffset(
- (Math.random() - 0.5) * 0.30,
- (Math.random() - 0.5) * 0.30,
- );
- if (shotVectors.length > 0) {
- for (const shot of shotVectors) {
- const { playerID, hitPoint } = shot;
- this.networking.applyDamage(playerID, 3);
- this.renderer.playerHitMarkers.push({
- hitPoint: hitPoint,
- shotVector: shot.vector,
- timestamp: -1,
- });
- }
- this.renderer.lastShotSomeoneTimestamp = Date.now() / 1000;
- }
- processedShots++;
- }
-
- // If we still have shots to process, schedule the next batch
- if (processedShots < totalShots) {
- if (typeof requestIdleCallback === 'function') {
- const idleCallbackId = requestIdleCallback(processShots, { timeout: TIMEOUT });
-
- // Ensure completion within timeout
- setTimeout(() => {
- cancelIdleCallback(idleCallbackId);
- processShots();
- }, TIMEOUT);
- } else {
- setTimeout(() => processShots(), 0);
- }
- }
- };
-
- // Initial call
- if (typeof requestIdleCallback === 'function') {
- const idleCallbackId = requestIdleCallback(processShots, { timeout: TIMEOUT });
-
- // Ensure first batch starts within timeout
- setTimeout(() => {
- cancelIdleCallback(idleCallbackId);
- processShots();
- }, TIMEOUT);
- } else {
- setTimeout(() => processShots(), 0);
- }
- }
-
- // Method to set world position when used as WorldItem
- public override setWorldPosition(vector: THREE.Vector3) {
- super.setWorldPosition(vector);
- }
+ private renderer!: Renderer;
+ private networking!: Networking;
+ private lastInput: HeldItemInput;
+ private lastFired: number;
+ private addedToHandScene: boolean;
+
+ // deno-lint-ignore constructor-super
+ constructor(renderer: Renderer, networking: Networking, index: number, itemType: ItemType) {
+ if(itemType === ItemType.WorldItem)
+ super(itemType, renderer.getEntityScene(), renderer.getInventoryMenuScene(), index);
+ else
+ super(itemType, renderer.getHeldItemScene(), renderer.getInventoryMenuScene(), index);
+ this.renderer = renderer;
+ this.networking = networking;
+ this.lastInput = new HeldItemInput();
+ this.addedToHandScene = false;
+ this.lastFired = 0;
+ }
+
+ public override init() {
+ AssetManager.getInstance().loadAsset('models/simplified_fish.glb', (scene) => {
+ this.object = scene;
+ if (this.itemType === ItemType.InventoryItem) {
+ this.object.traverse((child) => {
+ if ((child as THREE.Mesh).isMesh) {
+ child.renderOrder = 999;
+ const mesh = child as THREE.Mesh;
+ if (Array.isArray(mesh.material)) {
+ mesh.material.forEach(mat => mat.depthTest = false);
+ } else {
+ mesh.material.depthTest = false;
+ }
+ }
+ });
+ if(this.itemType === ItemType.InventoryItem)
+ this.object.scale.set(1.5, 1.5, 1.5);
+ }
+
+ this.inventoryMenuObject = this.object.clone();
+ this.inventoryMenuObject.scale.set(0.8, 0.8, 0.8);
+
+ if(this.itemType === ItemType.WorldItem)
+ this.object.scale.set(0.45, 0.45, 0.45);
+ });
+ }
+
+
+
+ public override onFrame(input: HeldItemInput, selectedIndex: number) {
+ if (!this.object) return;
+ const deltaTime = this.clock.getDelta();
+ this.timeAccum += deltaTime;
+ this.angleAccum += deltaTime;
+
+ if (this.itemType === ItemType.WorldItem) {
+ this.worldOnFrame(deltaTime);
+ } else if (this.itemType === ItemType.InventoryItem) {
+ this.inventoryOnFrame(deltaTime, selectedIndex);
+ this.handOnFrame(deltaTime, input);
+ }
+ }
+
+ // No need to override worldOnFrame if default behavior is sufficient
+ // If specific behavior is needed, you can override it here
+
+ public override inventoryOnFrame(deltaTime: number, selectedIndex: number) {
+ if (!this.addedToInventoryItemScenes) {
+ this.inventoryMenuScene.add(this.inventoryMenuObject);
+ this.addedToInventoryItemScenes = true;
+ }
+
+ this.angleAccum += deltaTime;
+ this.inventoryMenuObject.position.set(0, this.index, 0);
+
+ const targetQuaternion = inventoryQuaternionBase.clone();
+ if (this.index === selectedIndex) {
+ rotateAroundWorldAxis(targetQuaternion, new THREE.Vector3(0, 1, 0), this.angleAccum * 4);
+ this.showInHand();
+ } else {
+ this.hideInHand();
+ }
+ this.inventoryMenuObject.quaternion.slerp(targetQuaternion, 0.1 * 60 * deltaTime);
+ }
+
+ public override handOnFrame(deltaTime: number, input: HeldItemInput) {
+ if (!this.object) return;
+
+ if (this.shownInHand && !this.addedToHandScene) {
+ this.scene.add(this.object);
+ this.addedToHandScene = true;
+ }
+
+ if (this.shownInHand && Date.now() / 1000 - this.shownInHandTimestamp > showInHandDelay) {
+ this.handleInput(input, deltaTime);
+ } else {
+ this.handPosition.lerp(hiddenPosition, 0.1 * 60 * deltaTime);
+ this.object.position.copy(this.handPosition);
+ // Remove the object after it has slid out of view
+ if (this.handPosition.distanceTo(hiddenPosition) < 0.1) {
+ if (this.addedToHandScene) {
+ this.scene.remove(this.object);
+ this.addedToHandScene = false;
+ }
+ }
+ }
+
+ // Update crosshair flashing based on last shot timestamp
+ this.renderer.crosshairIsFlashing = Date.now() / 1000 - this.renderer.lastShotSomeoneTimestamp < 0.05;
+ }
+
+ private handleInput(input: HeldItemInput, deltaTime: number) {
+ if (input.rightClick)
+ moveTowardsPos(this.handPosition, scopedPosition, 0.3 * deltaTime * 60);
+ else
+ moveTowardsPos(this.handPosition, unscopedPosition, 0.1 * deltaTime * 60);
+
+ this.object.position.copy(this.handPosition);
+
+ moveTowardsRot(this.object.quaternion, scopedQuaternion, 0.1 * deltaTime * 60);
+
+ if (input.leftClick && (!this.lastInput.leftClick || Date.now() / 1000 - this.lastFired > firingDelayHeld)) {
+ if (Date.now() / 1000 - this.lastFired > firingDelay) {
+ this.lastFired = Date.now() / 1000;
+ this.shootFish();
+ this.handPosition.add(new THREE.Vector3(0, 0, 2));
+ rotateAroundWorldAxis(this.object.quaternion, new THREE.Vector3(1, 0, 0), Math.PI / 16);
+ }
+ }
+
+ this.lastInput = input;
+ }
+
+ public override showInHand() {
+ if (this.shownInHand) return;
+ this.shownInHand = true;
+ this.shownInHandTimestamp = Date.now() / 1000;
+ if (!this.addedToHandScene && this.object) {
+ this.scene.add(this.object);
+ this.addedToHandScene = true;
+ }
+ }
+
+ public override hideInHand() {
+ if (!this.shownInHand) return;
+ this.shownInHand = false;
+ }
+ public itemDepleted(): boolean {
+ return false;
+ }
+
+ private shootFish() {
+ const totalShots = 25;
+ let processedShots = 0;
+ const TIMEOUT = 150;
+
+ const processShots = (deadline?: IdleDeadline) => {
+ const timeRemaining = deadline ? deadline.timeRemaining() : 16;
+
+ while (processedShots < totalShots && timeRemaining > 0) {
+ const shotVectors = this.renderer.getShotVectorsToPlayersWithOffset(
+ (Math.random() - 0.5) * 0.30,
+ (Math.random() - 0.5) * 0.30
+ );
+ if (shotVectors.length > 0) {
+ for (const shot of shotVectors) {
+ const { playerID, hitPoint } = shot;
+ this.networking.applyDamage(playerID, 3);
+ this.renderer.playerHitMarkers.push({
+ hitPoint: hitPoint,
+ shotVector: shot.vector,
+ timestamp: -1,
+ });
+ }
+ this.renderer.lastShotSomeoneTimestamp = Date.now() / 1000;
+ }
+ processedShots++;
+ }
+
+ // If we still have shots to process, schedule the next batch
+ if (processedShots < totalShots) {
+ if (typeof requestIdleCallback === 'function') {
+ const idleCallbackId = requestIdleCallback(processShots, { timeout: TIMEOUT });
+
+ // Ensure completion within timeout
+ setTimeout(() => {
+ cancelIdleCallback(idleCallbackId);
+ processShots();
+ }, TIMEOUT);
+ } else {
+ setTimeout(() => processShots(), 0);
+ }
+ }
+ };
+
+ // Initial call
+ if (typeof requestIdleCallback === 'function') {
+ const idleCallbackId = requestIdleCallback(processShots, { timeout: TIMEOUT });
+
+ // Ensure first batch starts within timeout
+ setTimeout(() => {
+ cancelIdleCallback(idleCallbackId);
+ processShots();
+ }, TIMEOUT);
+ } else {
+ setTimeout(() => processShots(), 0);
+ }
+ }
+
+
+
+
+
+
+
+
+
+ // Method to set world position when used as WorldItem
+ public override setWorldPosition(vector: THREE.Vector3) {
+ super.setWorldPosition(vector);
+ }
}
function rotateAroundWorldAxis(source: THREE.Quaternion, axis: THREE.Vector3, angle: number) {
- const rotationQuat = new THREE.Quaternion().setFromAxisAngle(axis, angle);
- source.multiplyQuaternions(rotationQuat, source);
+ const rotationQuat = new THREE.Quaternion().setFromAxisAngle(axis, angle);
+ source.multiplyQuaternions(rotationQuat, source);
}
function moveTowardsPos(source: THREE.Vector3, target: THREE.Vector3, frac: number) {
- source.lerp(target, frac);
+ source.lerp(target, frac);
}
function moveTowardsRot(source: THREE.Quaternion, target: THREE.Quaternion, frac: number) {
- source.slerp(target, frac);
+ source.slerp(target, frac);
}
diff --git a/src/client/items/ItemBase.ts b/src/client/items/ItemBase.ts
index 54f0070..49ff6b0 100644
--- a/src/client/items/ItemBase.ts
+++ b/src/client/items/ItemBase.ts
@@ -4,152 +4,158 @@ import * as THREE from 'three';
const showInHandDelay = 0.1;
export class ItemBase {
- protected timeAccum: number = 0;
- protected clock: THREE.Clock = new THREE.Clock();
-
- protected object!: THREE.Object3D;
- protected itemType: ItemType;
-
- protected scene: THREE.Scene; // The scene to put the item in
-
- protected inventoryMenuScene: THREE.Scene; //Inventory menu scene
- protected inventoryMenuObject!: THREE.Object3D; //The object shown in the inventory menu (he do spin)
- protected index: number; //The index of the item in the inventory
- protected shownInHand: boolean = false;
- protected angleAccum: number = 0;
- protected handPosition: THREE.Vector3 = new THREE.Vector3(0.85, -0.8, 3.2);
- protected shownInHandTimestamp: number = 0;
-
- constructor(itemType: ItemType, scene: THREE.Scene, inventoryMenuScene: THREE.Scene, index: number) {
- this.itemType = itemType;
- this.scene = scene;
- this.inventoryMenuScene = inventoryMenuScene;
- this.index = index;
-
- this.init();
- }
-
- protected init() {
- // Init should be responsible for creating object and inventoryMenuObject
- // For this class, we'll just create a simple cube
- const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
- const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
- this.object = new THREE.Mesh(geometry, material);
- this.inventoryMenuObject = this.object.clone();
-
- if (this.itemType === ItemType.InventoryItem) {
- this.object.traverse((child) => {
- if ((child as THREE.Mesh).isMesh) {
- child.renderOrder = 999;
- const applyDepthTest = (material: THREE.Material | THREE.Material[]) => {
- if (Array.isArray(material)) {
- material.forEach((mat) => applyDepthTest(mat)); // Recursively handle array elements
- } else {
- material.depthTest = false;
- }
- };
- const mesh = child as THREE.Mesh;
- applyDepthTest(mesh.material);
- }
- });
- }
- }
-
- onFrame(input: HeldItemInput | undefined, selectedIndex: number | undefined) {
- if (!this.object) return; //return if object hasn't loaded
- const deltaTime = this.clock.getDelta();
- this.timeAccum += deltaTime;
-
- if (this.itemType === ItemType.WorldItem) {
- this.worldOnFrame(deltaTime);
- }
- if (this.itemType === ItemType.InventoryItem && selectedIndex !== undefined && input !== undefined) {
- this.inventoryOnFrame(deltaTime, selectedIndex);
- this.handOnFrame(deltaTime, input);
- }
- }
-
- /** -- World Items -- */
- protected addedToWorldScene: boolean = false;
- protected worldPosition: THREE.Vector3 = new THREE.Vector3();
-
- protected worldOnFrame(deltaTime: number) { // This function is called every frame for world items
- if (!this.addedToWorldScene) {
- this.scene.add(this.object);
- this.addedToWorldScene = true;
- }
- this.object.position.copy(this.worldPosition);
- this.object.position.add(new THREE.Vector3(0, Math.sin(this.timeAccum * 2) * 0.1, 0));
- this.object.rotation.y += deltaTime * 2;
- }
-
- setWorldPosition(vector: THREE.Vector3) {
- this.worldPosition = vector;
- }
-
- /** -- Inventory Items -- */
- protected addedToInventoryItemScenes: boolean = false;
-
- protected inventoryOnFrame(deltaTime: number, selectedIndex: number) {
- if (!this.addedToInventoryItemScenes) {
- this.scene.add(this.object);
- this.inventoryMenuScene.add(this.inventoryMenuObject);
- }
- this.angleAccum += deltaTime;
- this.inventoryMenuObject.position.set(0, this.index, 0);
-
- const targetQuaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0));
- if (this.index === selectedIndex) {
- rotateAroundWorldAxis(targetQuaternion, new THREE.Vector3(0, 1, 0), this.angleAccum * 6);
- this.showInHand();
- } else {
- this.hideInHand();
- }
- rotateAroundWorldAxis(targetQuaternion, new THREE.Vector3(1, 0, 0), Math.PI / 4);
- this.inventoryMenuObject.quaternion.slerp(targetQuaternion, 0.1 * 60 * deltaTime);
- }
-
- protected handOnFrame(deltaTime: number, input: HeldItemInput) {
- if (this.shownInHand && Date.now() / 1000 - this.shownInHandTimestamp > showInHandDelay) {
- this.handPosition.lerp(heldPosition, 0.1 * 60 * deltaTime);
- } else {
- this.handPosition.lerp(hiddenPosition, 0.1 * 60 * deltaTime);
- }
-
- this.object.position.copy(this.handPosition);
- if (this.shownInHand && input.leftClick) {
- this.object.position.add(new THREE.Vector3(Math.random() * 0.2, Math.random() * 0.2, Math.random() * 0.2));
- this.object.quaternion.slerp(new THREE.Quaternion().random(), 0.1);
- }
- }
-
- protected showInHand() {
- if (this.shownInHand) return;
- this.shownInHand = true;
- this.shownInHandTimestamp = Date.now() / 1000;
- }
-
- protected hideInHand() {
- if (!this.shownInHand) return;
- this.shownInHand = false;
- }
-
- public destroy() {
- this.scene.remove(this.object);
- this.inventoryMenuScene.remove(this.inventoryMenuObject);
- }
+ protected timeAccum:number = 0;
+ protected clock:THREE.Clock = new THREE.Clock();
+
+ protected object!: THREE.Object3D;
+ protected itemType: ItemType;
+
+ protected scene: THREE.Scene; // The scene to put the item in
+
+ protected inventoryMenuScene: THREE.Scene; //Inventory menu scene
+ protected inventoryMenuObject!:THREE.Object3D; //The object shown in the inventory menu (he do spin)
+ protected index:number; //The index of the item in the inventory
+ protected shownInHand:boolean = false;
+ protected angleAccum: number = 0;
+ protected handPosition:THREE.Vector3 = new THREE.Vector3(0.85, -0.8, 3.2);
+ protected shownInHandTimestamp:number = 0;
+
+
+ constructor(itemType:ItemType, scene:THREE.Scene, inventoryMenuScene:THREE.Scene, index:number){
+ this.itemType = itemType;
+ this.scene = scene;
+ this.inventoryMenuScene = inventoryMenuScene;
+ this.index = index;
+
+ this.init();
+ }
+
+ protected init() {
+ // Init should be responsible for creating object and inventoryMenuObject
+ // For this class, we'll just create a simple cube
+ const geometry = new THREE.BoxGeometry(0.5,0.5,0.5);
+ const material = new THREE.MeshBasicMaterial({color: 0x00ff00});
+ this.object = new THREE.Mesh(geometry, material);
+ this.inventoryMenuObject = this.object.clone();
+
+ if(this.itemType === ItemType.InventoryItem)
+ this.object.traverse((child) => {
+ if ((child as THREE.Mesh).isMesh) {
+ child.renderOrder = 999;
+ const applyDepthTest = (material: THREE.Material | THREE.Material[]) => {
+ if (Array.isArray(material))
+ material.forEach((mat) => applyDepthTest(mat)); // Recursively handle array elements
+ else
+ material.depthTest = false;
+ };
+ const mesh = child as THREE.Mesh;
+ applyDepthTest(mesh.material);
+ }
+ });
+ }
+
+ onFrame(input: HeldItemInput | undefined, selectedIndex: number | undefined) {
+ if(!this.object) return; //return if object hasn't loaded
+ const deltaTime = this.clock.getDelta();
+ this.timeAccum += deltaTime;
+
+ if(this.itemType === ItemType.WorldItem)
+ this.worldOnFrame(deltaTime);
+ if(this.itemType === ItemType.InventoryItem && selectedIndex !== undefined && input !== undefined){
+ this.inventoryOnFrame(deltaTime, selectedIndex);
+ this.handOnFrame(deltaTime, input);
+ }
+ }
+
+
+ /** -- World Items -- */
+ protected addedToWorldScene:boolean = false;
+ protected worldPosition:THREE.Vector3 = new THREE.Vector3();
+
+
+ protected worldOnFrame(deltaTime:number){ // This function is called every frame for world items
+ if(!this.addedToWorldScene){
+ this.scene.add(this.object);
+ this.addedToWorldScene = true;
+
+ }
+ this.object.position.copy(this.worldPosition);
+ this.object.position.add(new THREE.Vector3(0, Math.sin(this.timeAccum*2) * 0.1, 0));
+ this.object.rotation.y += deltaTime * 2;
+
+ }
+
+ setWorldPosition(vector:THREE.Vector3){
+ this.worldPosition = vector;
+ }
+
+ /** -- Inventory Items -- */
+ protected addedToInventoryItemScenes:boolean = false;
+
+ protected inventoryOnFrame(deltaTime:number, selectedIndex:number){
+ if(!this.addedToInventoryItemScenes){
+ this.scene.add(this.object);
+ this.inventoryMenuScene.add(this.inventoryMenuObject);
+ }
+ this.angleAccum+=deltaTime;
+ this.inventoryMenuObject.position.set(0, this.index, 0);
+
+ const targetQuaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler(0,0,0));
+ if(this.index === selectedIndex){
+ rotateAroundWorldAxis(targetQuaternion, new THREE.Vector3(0,1,0), this.angleAccum * 6);
+ this.showInHand();
+ }else{
+ this.hideInHand();
+ }
+ rotateAroundWorldAxis(targetQuaternion, new THREE.Vector3(1,0,0), Math.PI / 4);
+ this.inventoryMenuObject.quaternion.slerp(targetQuaternion, 0.1 * 60 * deltaTime);
+ }
+
+ protected handOnFrame(deltaTime:number, input:HeldItemInput){
+ if(this.shownInHand && Date.now() / 1000 - this.shownInHandTimestamp > showInHandDelay)
+ this.handPosition.lerp(heldPosition, 0.1 * 60 *deltaTime);
+ else
+ this.handPosition.lerp(hiddenPosition, 0.1 * 60 *deltaTime);
+
+ this.object.position.copy(this.handPosition);
+ if(this.shownInHand && input.leftClick){
+ this.object.position.add(new THREE.Vector3(Math.random()*0.2, Math.random()*0.2, Math.random()*0.2));
+ this.object.quaternion.slerp(new THREE.Quaternion().random(),0.1);
+ }
+ }
+
+ protected showInHand(){
+ if(this.shownInHand) return;
+ this.shownInHand = true;
+ this.shownInHandTimestamp = Date.now() / 1000;
+
+ }
+
+ protected hideInHand(){
+ if(!this.shownInHand) return;
+ this.shownInHand = false;
+ }
+
+
+ public destroy(){
+ this.scene.remove(this.object);
+ this.inventoryMenuScene.remove(this.inventoryMenuObject);
+ }
+
+
+
}
const heldPosition = new THREE.Vector3(0.85, -0.8, 3.2);
const hiddenPosition = new THREE.Vector3(0.85, -2.5, 3.2);
export enum ItemType {
- //TODO diagnose lint being weird here?
- // eslint-disable-next-line no-unused-vars
- WorldItem = 1,
- // eslint-disable-next-line no-unused-vars
- InventoryItem = 2,
+ //TODO diagnose lint being weird here?
+ // eslint-disable-next-line no-unused-vars
+ WorldItem = 1,
+ // eslint-disable-next-line no-unused-vars
+ InventoryItem = 2,
}
function rotateAroundWorldAxis(source: THREE.Quaternion, axis: THREE.Vector3, angle: number) {
- const rotationQuat = new THREE.Quaternion().setFromAxisAngle(axis, angle);
- source.multiplyQuaternions(rotationQuat, source);
-}
+ const rotationQuat = new THREE.Quaternion().setFromAxisAngle(axis, angle);
+ source.multiplyQuaternions(rotationQuat, source);
+}
\ No newline at end of file
diff --git a/src/client/main.ts b/src/client/main.ts
index 164a464..65f1a9e 100644
--- a/src/client/main.ts
+++ b/src/client/main.ts
@@ -1,4 +1,4 @@
// import {Game} from "./core/Game.ts";
//
// const game = new Game();
-// game.start();
+// game.start();
\ No newline at end of file
diff --git a/src/client/ui/ChatOverlay.ts b/src/client/ui/ChatOverlay.ts
index 560c464..d9c0092 100644
--- a/src/client/ui/ChatOverlay.ts
+++ b/src/client/ui/ChatOverlay.ts
@@ -1,948 +1,901 @@
-import { Player } from '../core/Player.ts';
-import { Renderer } from '../core/Renderer.ts';
-import { Networking } from '../core/Networking.ts';
-import { InputHandler } from '../input/InputHandler.ts';
-import { CommandManager } from '../core/CommandManager.ts';
-import { SettingsManager } from '../core/SettingsManager.ts';
-import { TouchInputHandler } from '../input/TouchInputHandler.ts';
+import {Player} from '../core/Player.ts';
+import {Renderer} from '../core/Renderer.ts';
+import {Networking} from '../core/Networking.ts';
+import {InputHandler} from '../input/InputHandler.ts';
+import {CommandManager} from "../core/CommandManager.ts";
+import {SettingsManager} from "../core/SettingsManager.ts";
+import {TouchInputHandler} from "../input/TouchInputHandler.ts";
interface ChatMessage {
- id: number;
- message: string;
- name: string;
- timestamp: number;
+ id: number;
+ message: string;
+ name: string;
+ timestamp: number;
}
interface AnimatedGameMessage {
- id: string; // Unique identifier
- message: string;
- state: 'animatingIn' | 'animatingOut' | 'idle';
- animationProgress: number; // Ranges from 0 to 1
- timestamp: number; // Time when the current animation state started
+ id: string; // Unique identifier
+ message: string;
+ state: 'animatingIn' | 'animatingOut' | 'idle';
+ animationProgress: number; // Ranges from 0 to 1
+ timestamp: number; // Time when the current animation state started
}
interface LineMessage {
- currentMessage: AnimatedGameMessage | null;
- pendingMessage: string | null;
+ currentMessage: AnimatedGameMessage | null;
+ pendingMessage: string | null;
}
const hitMarkerLifetime = 0.3;
export class ChatOverlay {
- private chatCanvas: HTMLCanvasElement;
- private chatCtx: CanvasRenderingContext2D;
- private chatMessages: ChatMessage[]; // Typed as ChatMessage[]
- private chatMessageLifespan: number;
- private charsToRemovePerSecond: number;
- private maxMessagesOnScreen: number;
- private nameSettingActive: boolean;
- private localPlayer: Player;
- private renderer!: Renderer;
- private networking!: Networking;
- private screenWidth: number;
- private inputHandler!: InputHandler;
- private debugTextHeight!: number;
- private oldScreenWidth: number = 0;
- private readonly commandManager: CommandManager;
- private lastTouchTimestamp: number = 0;
- private touchJoystickEngaged: boolean = false;
- private joystickX: number = 0;
- private joystickY: number = 0;
- private joystickInputX: number = 0;
- private joystickInputY: number = 0;
- private buttonsHeld: number[] = [];
- private lastRoutineMs = 0;
-
- private offscreenCanvas: HTMLCanvasElement;
- private offscreenCtx: CanvasRenderingContext2D;
-
- public gameMessages: string[] = [];
- private previousGameMessages: string[] = [];
-
- // Removed animatedGameMessages in favor of per-line management
- private lines: LineMessage[] = [];
- private animationDuration: number = 1; // Adjusted for smoother animation
-
- // Color code mapping
- COLOR_CODES: { [key: string]: string } = {
- '0': '#000000', // Black
- '1': '#0000AA', // Dark Blue
- '2': '#00AA00', // Dark Green
- '3': '#00AAAA', // Dark Aqua
- '4': '#AA0000', // Dark Red
- '5': '#AA00AA', // Dark Purple
- '6': '#FFAA00', // Gold
- '7': '#AAAAAA', // Gray
- '8': '#555555', // Dark Gray
- '9': '#5555FF', // Blue
- 'a': '#55FF55', // Green
- 'b': '#55FFFF', // Aqua
- 'c': '#FF5555', // Red
- 'd': '#FF55FF', // Light Purple
- 'e': '#FFFF55', // Yellow
- 'f': '#FFFFFF', // White
- 'g': this.getRainbowColor(),
- };
-
- private getColorCode(code: string): string | false {
- if (code === 'g') {
- return this.getRainbowColor();
- }
- return this.COLOR_CODES[code] || false;
- }
-
- private getRainbowColor(): string {
- const hue = (Date.now() / 20) % 360;
- return `hsl(${hue}, 100%, 50%)`;
- }
-
- constructor(container: HTMLElement, localPlayer: Player) {
- this.localPlayer = localPlayer;
- this.chatCanvas = document.createElement('canvas');
- this.chatCtx = this.chatCanvas.getContext('2d') as CanvasRenderingContext2D;
- this.chatCtx.imageSmoothingEnabled = false;
-
- this.chatCanvas.width = 400;
- this.chatCanvas.height = 200;
-
- this.chatMessages = [];
- this.chatMessageLifespan = 40; // 40 seconds
- this.charsToRemovePerSecond = 30;
- this.maxMessagesOnScreen = 12;
-
- this.nameSettingActive = false;
- this.screenWidth = 100;
-
- this.commandManager = new CommandManager(this.localPlayer, this);
-
- this.setupEventListeners();
-
- this.chatCanvas.style.position = 'absolute';
- this.chatCanvas.style.display = 'block';
- this.chatCanvas.style.zIndex = '100';
- this.chatCanvas.style.top = '0';
- this.chatCanvas.style.left = '0';
-
- this.chatCanvas.style.height = '100vh';
- document.body.style.margin = '0';
- this.chatCanvas.style.imageRendering = 'pixelated';
- this.chatCanvas.style.textRendering = 'pixelated';
-
- this.chatCanvas.style.touchAction = 'none';
-
- this.offscreenCanvas = document.createElement('canvas');
- this.offscreenCtx = this.offscreenCanvas.getContext('2d') as CanvasRenderingContext2D;
-
- // Initialize lines for per-line message management
- this.lines = Array(this.maxMessagesOnScreen).fill(null).map(() => ({
- currentMessage: null,
- pendingMessage: null,
- }));
-
- //document.body.appendChild(this.chatCanvas);
- container.appendChild(this.chatCanvas);
-
- globalThis.addEventListener('resize', this.onWindowResize.bind(this));
- globalThis.addEventListener('orientationchange', this.onWindowResize.bind(this));
- }
-
- public setRenderer(renderer: Renderer) {
- this.renderer = renderer;
- }
-
- public setNetworking(networking: Networking) {
- this.networking = networking;
- }
-
- public setInputHandler(inputHandler: InputHandler) {
- this.inputHandler = inputHandler;
- }
-
- private setupEventListeners() {
- document.addEventListener('keydown', this.onKeyDown.bind(this));
- }
-
- public onFrame() {
- const startTime = Date.now();
- const now = Date.now() / 1000;
-
- this.gameMessages = this.localPlayer.gameMsgs;
- this.detectGameMessagesChanges(now);
- this.updateAnimatedGameMessages(now);
-
- this.clearOldMessages();
- this.chatCtx.clearRect(0, 0, this.chatCanvas.width, this.chatCanvas.height);
- this.renderHitMarkers();
- this.renderChatMessages();
- this.renderGameText();
- this.renderDebugText();
- if (this.inputHandler.getKey('tab')) {
- this.renderPlayerList();
- }
- this.renderEvil();
- this.renderCrosshair();
- this.renderTouchControls();
-
- this.screenWidth = Math.floor(this.renderer.getCamera().aspect * 200);
-
- if (this.oldScreenWidth !== this.screenWidth) {
- //if(this.chatCanvas.width < this.screenWidth)
- this.chatCanvas.width = this.screenWidth;
- this.oldScreenWidth = this.screenWidth;
- }
-
- // this.chatCanvas.width = this.screenWidth;
- // this.chatCtx.fillRect(0,0,10,10);
-
- this.onWindowResize();
-
- this.inputHandler.nameSettingActive = this.nameSettingActive;
- if (Math.random() < 0.03) {
- this.lastRoutineMs = Date.now() - startTime;
- }
- }
-
- private onWindowResize() {
- this.chatCanvas.style.width = globalThis.innerWidth + 'px';
- this.chatCanvas.style.height = globalThis.innerHeight + 'px';
- }
-
- private renderChatMessages() {
- const ctx = this.chatCtx;
-
- this.offscreenCtx.font = '8px Tiny5';
- this.offscreenCtx.fillStyle = 'white';
-
- const usermsg = this.localPlayer.chatMsg;
- let cursor = '';
- if ((Date.now() / 1000) % 0.7 < 0.35) cursor = '|';
-
- const linesToRender: string[] = [];
- const pixOffsets: number[] = [];
- const messagesBeingTyped = this.networking.getMessagesBeingTyped();
-
- for (let i = 0; i < this.chatMessages.length; i++) {
- let msg = this.chatMessages[i].message;
- const name = this.chatMessages[i].name;
- if (name.length > 0) msg = `${name}: ${msg}`;
-
- const duplicateFromPlayerData = messagesBeingTyped.includes(msg);
-
- let charsToRemove = Date.now() / 1000 - this.chatMessages[i].timestamp - this.chatMessageLifespan;
- charsToRemove = Math.max(0, charsToRemove * this.charsToRemovePerSecond);
- charsToRemove = Math.floor(charsToRemove);
-
- let removedSubstring = '';
- let remainingMsg = msg;
- if (charsToRemove > 0) {
- let charsRemoved = 0;
- while (charsRemoved < charsToRemove && remainingMsg.length > 0) {
- const char = remainingMsg.charAt(0);
- removedSubstring += char;
- remainingMsg = remainingMsg.substring(1);
- charsRemoved++;
- }
- }
-
- if (!duplicateFromPlayerData) {
- linesToRender.push(remainingMsg);
- pixOffsets.push(this.offscreenCtx.measureText(removedSubstring).width);
- }
- }
-
- for (const msg of messagesBeingTyped) {
- linesToRender.push(msg + cursor);
- pixOffsets.push(0);
- }
-
- if (this.localPlayer.chatActive) {
- if (this.localPlayer.chatMsg.startsWith('>')) {
- linesToRender.push('&2' + usermsg + cursor);
- } else {
- linesToRender.push(usermsg + cursor);
- }
- pixOffsets.push(0);
- }
-
- if (this.nameSettingActive) {
- linesToRender.push('Enter your name: ' + usermsg + cursor);
- pixOffsets.push(0);
- this.localPlayer.name = usermsg + cursor;
- if (this.localPlayer.name.length == 0) this.localPlayer.name = ' ';
- }
-
- const wrappedLines: string[] = [];
- const lineOrigins: number[] = [];
- const isFirstWrappedLine: boolean[] = [];
-
- for (let i = 0; i < linesToRender.length; i++) {
- const wrapped = this.doTextWrapping(this.offscreenCtx, [linesToRender[i]], this.screenWidth - 10);
- for (let j = 0; j < wrapped.length; j++) {
- wrappedLines.push(wrapped[j]);
- lineOrigins.push(i);
- isFirstWrappedLine.push(j === 0);
- }
- }
-
- const totalLines = wrappedLines.length;
- for (let i = 0; i < totalLines; i++) {
- const lineIndex = totalLines - i - 1;
- const text = wrappedLines[lineIndex];
- const originIndex = lineOrigins[lineIndex];
- const pixOffset = isFirstWrappedLine[lineIndex] ? pixOffsets[originIndex] : 0;
-
- this.renderPixelText(text, 3 + pixOffset, 200 - 20 - 8 * i, 'white');
- }
-
- if ((usermsg !== '' && this.localPlayer.chatActive) || this.nameSettingActive) {
- ctx.fillStyle = 'rgba(145,142,118,0.3)';
- let width = ctx.measureText(usermsg).width;
- if (this.nameSettingActive) {
- width = ctx.measureText('Enter your name: ' + usermsg).width;
- }
- ctx.fillRect(2, 200 - 20 - 7, width + 1, 9);
- }
- }
-
- private renderPrettyText(text: string, x: number, y: number, defaultColor: string) {
- let currentX = x;
- const segments: { text: string; color: string }[] = [];
- let currentColor = defaultColor;
- let currentSegment = '';
-
- // Parse color codes and split into segments
- for (let i = 0; i < text.length; i++) {
- if (text[i] === '&' && i + 1 < text.length && this.getColorCode(text[i + 1])) {
- if (currentSegment) {
- segments.push({ text: currentSegment, color: currentColor });
- }
- currentColor = this.getColorCode(text[i + 1]);
- currentSegment = '';
- i++; // Skip the color code character
- } else {
- currentSegment += text[i];
- }
- }
-
- if (currentSegment) {
- segments.push({ text: currentSegment, color: currentColor });
- }
-
- // Render each segment
- for (const segment of segments) {
- this.offscreenCtx.font = '8px Tiny5';
- const textMetrics = this.offscreenCtx.measureText(segment.text);
- const textWidth = Math.max(Math.ceil(textMetrics.width), 1);
- const textHeight = 8;
-
- if (this.offscreenCanvas.width !== textWidth || this.offscreenCanvas.height !== textHeight) {
- this.offscreenCanvas.width = textWidth;
- this.offscreenCanvas.height = textHeight;
- }
-
- this.offscreenCtx.clearRect(0, 0, textWidth, textHeight);
- this.offscreenCtx.font = '8px Tiny5';
- this.offscreenCtx.fillStyle = segment.color;
- this.offscreenCtx.fillText(segment.text, 0, textHeight - 1);
-
- const imageData = this.offscreenCtx.getImageData(0, 0, textWidth, textHeight);
- const data = imageData.data;
-
- for (let i = 0; i < data.length; i += 4) {
- data[i + 3] = data[i + 3] > 170 ? 255 : 0;
- }
-
- this.offscreenCtx.putImageData(imageData, 0, 0);
- this.chatCtx.drawImage(this.offscreenCanvas, currentX, y - textHeight + 1);
- currentX += textWidth;
- }
- }
-
- private renderUglyText(text: string, x: number, y: number, defaultColor: string) {
- let currentX = x;
- let currentColor = defaultColor;
- let currentSegment = '';
-
- for (let i = 0; i < text.length; i++) {
- if (text[i] === '&' && i + 1 < text.length && this.getColorCode(text[i + 1])) {
- if (currentSegment) {
- this.chatCtx.font = '8px Tiny5';
- this.chatCtx.fillStyle = currentColor;
- this.chatCtx.fillText(currentSegment, currentX, y);
- currentX += this.chatCtx.measureText(currentSegment).width;
- }
- currentColor = this.getColorCode(text[i + 1]);
- currentSegment = '';
- i++; // Skip the color code character
- } else {
- currentSegment += text[i];
- }
- }
-
- if (currentSegment) {
- this.chatCtx.font = '8px Tiny5';
- this.chatCtx.fillStyle = currentColor;
- this.chatCtx.fillText(currentSegment, currentX, y);
- }
- }
-
- private renderPixelText(text: string, x: number, y: number, color: string) {
- if (SettingsManager.settings.doPrettyText) {
- this.renderPrettyText(text, x, y, color);
- } else {
- this.renderUglyText(text, x, y, color);
- }
- }
-
- private renderDebugText() {
- const ctx = this.chatCtx;
- ctx.font = '8px Tiny5';
- ctx.fillStyle = 'teal';
-
- const linesToRender = [];
- const framerate = this.renderer.getFramerate();
-
- if (this.localPlayer.latency >= 999) {
- linesToRender.push('disconnected :(');
- }
-
- //const playerX = Math.round(this.localPlayer.position.x);
-
- linesToRender.push(
- 'candiru ' + this.localPlayer.gameVersion + ' @ ' + Math.round(framerate) + 'fps, ' +
- Math.round(this.localPlayer.latency) + 'ms',
- );
- //linesToRender.push('connected to: ' + this.networking.getServerInfo().name);
- //linesToRender.push('players: ' + this.networking.getServerInfo().currentPlayers + '/' + this.networking.getServerInfo().maxPlayers);
- //linesToRender.push('map: ' + this.networking.getServerInfo().mapName);
- //linesToRender.push('mode: ' + this.networking.getServerInfo().gameMode);
- //linesToRender.push('serverVersion: ' + this.networking.getServerInfo().version);
- //linesToRender.push('tickRate: ' + this.networking.getServerInfo().tickRate);
- //linesToRender.push('playerMaxHealth: ' + this.networking.getServerInfo().playerMaxHealth);
- //linesToRender.push('health: ' + this.localPlayer.health);
-
- for (const msg of this.localPlayer.gameMsgs2) {
- linesToRender.push(msg);
- }
-
- //linesToRender.push('routineTime: ' + this.lastRoutineMs + 'ms');
-
- for (let i = 0; i < linesToRender.length; i++) {
- this.renderPixelText(linesToRender[i], 2, 7 + 7 * i, 'teal');
- }
-
- this.debugTextHeight = 7 * linesToRender.length;
- }
-
- private detectGameMessagesChanges(now: number) {
- const current = this.gameMessages;
-
- for (let i = 0; i < this.maxMessagesOnScreen; i++) {
- const line = this.lines[i];
- const currentMessage = current[i] || null;
-
- if (!line.currentMessage) {
- if (currentMessage) {
- line.currentMessage = {
- id: this.generateUniqueId(),
- message: currentMessage,
- state: 'animatingIn',
- animationProgress: 0,
- timestamp: now,
- };
- }
- continue;
- }
-
- if (currentMessage !== line.currentMessage.message) {
- if (line.currentMessage.state === 'idle') {
- line.currentMessage.state = 'animatingOut';
- line.currentMessage.timestamp = now;
- line.pendingMessage = currentMessage;
- } else {
- line.pendingMessage = currentMessage;
- }
- }
- }
-
- this.previousGameMessages = [...current];
- }
-
- private updateAnimatedGameMessages(now: number) {
- for (let i = 0; i < this.maxMessagesOnScreen; i++) {
- const line = this.lines[i];
- if (!line.currentMessage) continue; // Early return if null
-
- const elapsed = now - line.currentMessage.timestamp;
- let progress = Math.min(elapsed / this.animationDuration, 1);
- progress = this.easeOut(progress);
-
- line.currentMessage.animationProgress = progress;
-
- if (line.currentMessage.state === 'animatingOut' && progress >= 1) {
- // Remove the message after fade-out
- if (line.pendingMessage) {
- line.currentMessage = {
- id: this.generateUniqueId(),
- message: line.pendingMessage,
- state: 'animatingIn',
- animationProgress: 0,
- timestamp: now,
- };
- line.pendingMessage = null;
- } else {
- line.currentMessage = null;
- }
- continue;
- }
-
- if (line.currentMessage.state === 'animatingIn' && progress >= 1) {
- line.currentMessage.state = 'idle';
- line.currentMessage.animationProgress = 1;
- }
- }
- }
-
- private renderGameText() {
- const ctx = this.chatCtx;
- ctx.font = '8px Tiny5';
- const centerY = this.chatCanvas.height / 2 + 48;
-
- for (let i = 0; i < this.maxMessagesOnScreen; i++) {
- const line = this.lines[i];
- if (!line.currentMessage) continue;
-
- let visibleText = line.currentMessage.message;
-
- // Check if we should skip animation
- const shouldSkipAnimation = line.currentMessage.state === 'animatingOut' &&
- line.pendingMessage !== null &&
- line.currentMessage.message.includes('seconds') &&
- line.pendingMessage.includes('seconds');
-
- if (shouldSkipAnimation && line.pendingMessage) {
- // Directly update to the pending message
- line.currentMessage = {
- id: this.generateUniqueId(),
- message: line.pendingMessage,
- state: 'idle',
- animationProgress: 1,
- timestamp: Date.now() / 1000,
- };
- line.pendingMessage = null;
- visibleText = line.currentMessage.message;
- } else if (line.currentMessage.state === 'animatingIn' || line.currentMessage.state === 'animatingOut') {
- visibleText = this.getVisibleText(
- line.currentMessage.message,
- line.currentMessage.state,
- line.currentMessage.animationProgress,
- );
- }
-
- // Calculate the actual width of the rendered text, including color codes
- const textWidth = this.getRenderedTextWidth(visibleText);
- const x = Math.floor((this.screenWidth - textWidth) / 2);
- const y = Math.floor(centerY + (i * 10));
-
- this.renderPixelText(visibleText, x, y, 'white');
- }
- }
-
- private getRenderedTextWidth(text: string): number {
- let totalWidth = 0;
- let currentSegment = '';
- const ctx = this.chatCtx;
-
- for (let i = 0; i < text.length; i++) {
- if (text[i] === '&' && i + 1 < text.length && this.getColorCode(text[i + 1])) {
- // Measure the current segment before switching color
- if (currentSegment) {
- totalWidth += ctx.measureText(currentSegment).width;
- currentSegment = '';
- }
- i++; // Skip the color code character
- } else {
- currentSegment += text[i];
- }
- }
-
- // Measure the last segment
- if (currentSegment) {
- totalWidth += ctx.measureText(currentSegment).width;
- }
-
- return totalWidth;
- }
-
- private getVisibleText(message: string, state: 'animatingIn' | 'animatingOut' | 'idle', progress: number): string {
- if (state === 'idle') {
- return message;
- }
-
- const length = message.length;
- if (length === 0) return '';
-
- let charsToShow = 0;
-
- if (state === 'animatingIn') {
- charsToShow = Math.floor(progress * length);
- charsToShow = Math.min(charsToShow, length); // Ensure it doesn't exceed the message length
- return message.substring(0, charsToShow);
- } else if (state === 'animatingOut') {
- charsToShow = Math.floor((1 - progress) * length);
- charsToShow = Math.max(charsToShow, 0); // Ensure it doesn't go below zero
- return message.substring(0, charsToShow);
- }
-
- return message;
- }
-
- private easeOut(progress: number): number {
- return 1 - Math.pow(1 - progress, 1.5);
- }
-
- public renderTouchControls() {
- if (Date.now() / 1000 - this.lastTouchTimestamp > 10) return;
- if (this.touchJoystickEngaged) {
- // Draw circle for movement
- this.chatCtx.fillStyle = 'rgba(255,255,255,0.25)';
- this.chatCtx.beginPath();
- this.chatCtx.arc(this.joystickX, this.joystickY, TouchInputHandler.joystickRadius, 0, 2 * Math.PI);
- this.chatCtx.fill();
-
- // Smaller circle for joystick-- offset from center
- this.chatCtx.fillStyle = 'rgba(255,255,255,0.5)';
- this.chatCtx.beginPath();
- this.chatCtx.arc(
- this.joystickX + this.joystickInputX * TouchInputHandler.joystickRadius,
- this.joystickY + this.joystickInputY * TouchInputHandler.joystickRadius,
- 10,
- 0,
- 2 * Math.PI,
- );
- this.chatCtx.fill();
- }
-
- // Draw rounded square center right for jumping
- const squareWidth = 24;
- const squareHeight = 24;
- const cornerRadius = 6;
- const x = this.chatCanvas.width - squareWidth - 12; // 12px from the right edge
- let y = (this.chatCanvas.height - squareHeight) / 2; // Center vertically
-
- this.drawButton(x, y, squareWidth, squareHeight, cornerRadius, '●', 1, 0);
- y -= squareHeight + 4;
- this.drawButton(x, y, squareWidth, squareHeight, cornerRadius, '↑', 1, -1);
- y += squareHeight + 4;
- y += squareHeight + 4;
- this.drawButton(x, y, squareWidth, squareHeight, cornerRadius, '[]', 1, 1);
- }
-
- public setButtonsHeld(buttons: number[]) {
- this.buttonsHeld = buttons;
- }
-
- private drawButton(
- x: number,
- y: number,
- width: number,
- height: number,
- cornerRadius: number,
- text: string,
- textOffset: number,
- index: number,
- ) {
- if (this.buttonsHeld.includes(index)) {
- this.chatCtx.fillStyle = 'rgba(100,100,100,0.3)';
- } else {
- this.chatCtx.fillStyle = 'rgba(255,255,255,0.15)';
- }
-
- this.drawRoundedSquare(x, y, width, height, cornerRadius);
- // Draw character inside square
- this.chatCtx.fillStyle = 'rgba(0,0,0,0.5)';
- this.chatCtx.font = '16px Tiny5';
- const textWidth = this.chatCtx.measureText(text).width;
- this.chatCtx.fillText(
- text,
- Math.floor(x + width / 2 - textWidth / 2 + textOffset),
- Math.floor(y + height / 2 + 16 / 2 - 2),
- );
- }
-
- private drawRoundedSquare(x: number, y: number, width: number, height: number, cornerRadius: number) {
- this.chatCtx.beginPath();
- this.chatCtx.moveTo(x + cornerRadius, y);
- this.chatCtx.lineTo(x + width - cornerRadius, y);
- this.chatCtx.quadraticCurveTo(x + width, y, x + width, y + cornerRadius);
- this.chatCtx.lineTo(x + width, y + height - cornerRadius);
- this.chatCtx.quadraticCurveTo(x + width, y + height, x + width - cornerRadius, y + height);
- this.chatCtx.lineTo(x + cornerRadius, y + height);
- this.chatCtx.quadraticCurveTo(x, y + height, x, y + height - cornerRadius);
- this.chatCtx.lineTo(x, y + cornerRadius);
- this.chatCtx.quadraticCurveTo(x, y, x + cornerRadius, y);
- this.chatCtx.closePath();
- this.chatCtx.fill();
- }
-
- public setLastTouchTimestamp(timestamp: number) {
- this.lastTouchTimestamp = timestamp;
- }
-
- public setTouchJoystickEngaged(value: boolean) {
- this.touchJoystickEngaged = value;
- }
-
- public setJoystickPosition(x: number, y: number) {
- this.joystickX = x;
- this.joystickY = y;
- }
-
- public setJoystickInput(x: number, y: number) {
- this.joystickInputX = x;
- this.joystickInputY = y;
- }
-
- public renderHitMarkers() {
- const numDots = 10; // Number of dots to render around each hit point
-
- for (let i = this.renderer.playerHitMarkers.length - 1; i >= 0; i--) {
- if (this.renderer.playerHitMarkers[i].timestamp === -1) {
- this.renderer.playerHitMarkers[i].timestamp = Date.now() / 1000; // Set timestamp if not set
- }
-
- const timeSinceHit = Date.now() / 1000 - this.renderer.playerHitMarkers[i].timestamp;
- const lifePercent = timeSinceHit / hitMarkerLifetime;
-
- if (timeSinceHit > hitMarkerLifetime) {
- this.renderer.playerHitMarkers.splice(i, 1);
- continue;
- }
-
- const hitVec = this.renderer.playerHitMarkers[i].hitPoint;
- const projected = hitVec.clone().project(this.renderer.getCamera());
- const projectedX = Math.round((projected.x + 1) * this.screenWidth / 2);
- const projectedY = Math.round((-projected.y + 1) * 200 / 2);
-
- if (projected.z < 1) {
- this.chatCtx.fillStyle = 'rgba(255,0,0,' + (1 - Math.pow(lifePercent, 1.25)) + ')';
-
- // Calculate sizeMultiplier
- const sizeMultiplier = 1 + 2 / this.renderer.playerHitMarkers[i].shotVector.length();
-
- // Calculate and render dots
- const radius = Math.pow(lifePercent, 0.7) * 7 * sizeMultiplier; // Radius of the circle in which dots are placed
- for (let j = 0; j < numDots; j++) {
- const angle = (Math.PI * 2 / numDots) * j;
- const dotX = Math.round(projectedX + radius * Math.cos(angle));
- const dotY = Math.round(projectedY + radius * Math.sin(angle));
-
- this.chatCtx.fillRect(dotX, dotY, 1, 1); // Render a 1px by 1px dot
- }
- }
- }
- }
-
- public getDebugTextHeight(): number {
- return this.debugTextHeight;
- }
-
- private renderPlayerList() {
- const ctx = this.chatCtx;
- const linesToRender: string[] = [];
- const colorsToRender: string[] = [];
- const playerData = this.networking.getRemotePlayerData();
-
- linesToRender.push(playerData.length + ' online - ' + Math.round(this.localPlayer.latency) + 'ms');
- colorsToRender.push('white');
- for (let i = 0; i < playerData.length; i++) {
- linesToRender.push(playerData[i].name);
- if (playerData[i].latency > 200) {
- colorsToRender.push('red');
- } else if (playerData[i].latency > 50) {
- colorsToRender.push('orange');
- } else {
- colorsToRender.push('green');
- }
- }
-
- ctx.font = '8px Tiny5';
-
- let longestLinePix = 0;
- for (let i = 0; i < linesToRender.length; i++) {
- longestLinePix = Math.max(longestLinePix, ctx.measureText(linesToRender[i]).width);
- }
-
- ctx.fillStyle = 'rgba(0,0,0,0.5)';
- ctx.fillRect(
- Math.floor(this.screenWidth / 2 - longestLinePix / 2),
- 4,
- longestLinePix + 3,
- linesToRender.length * 7 + 2,
- );
-
- for (let i = 0; i < linesToRender.length; i++) {
- this.renderPixelText(
- linesToRender[i],
- Math.floor(this.screenWidth / 2 - longestLinePix / 2 + 2),
- 11 + 7 * i,
- colorsToRender[i],
- );
- }
- }
-
- private renderEvil() {
- const ctx = this.chatCtx;
- if (Date.now() / 1000 - this.networking.getDamagedTimestamp() < 0.05) {
- ctx.fillStyle = 'rgba(255,0,0,0.1)';
- ctx.fillRect(0, 0, this.chatCanvas.width, this.chatCanvas.height);
- }
- }
-
- private renderCrosshair() {
- const ctx = this.chatCtx;
- ctx.fillStyle = SettingsManager.settings.crosshairColor;
- if (this.renderer.crosshairIsFlashing) {
- ctx.fillStyle = '#FF0000';
- }
- switch (SettingsManager.settings.crosshairType) {
- case 0:
- ctx.fillRect(Math.floor(this.screenWidth / 2), 100 - 3, 1, 7);
- ctx.fillRect(Math.floor(this.screenWidth / 2 - 3), 100, 7, 1);
- break;
- case 1:
- ctx.fillRect(Math.floor(this.screenWidth / 2), 100, 1, 1);
- break;
- }
- }
-
- private onKeyDown(e: KeyboardEvent) {
- if (e.key === 'Backspace' && (this.localPlayer.chatActive || this.nameSettingActive)) {
- this.localPlayer.chatMsg = this.localPlayer.chatMsg.slice(0, -1);
- return;
- }
-
- if (e.key === 'Enter') {
- if (this.localPlayer.chatActive) {
- if (!this.commandManager.runCmd(this.localPlayer.chatMsg)) {
- this.networking.sendMessage(this.localPlayer.chatMsg);
- }
- }
- if (this.nameSettingActive) {
- this.localPlayer.name = this.localPlayer.chatMsg.toString();
- SettingsManager.settings.name = this.localPlayer.chatMsg.toString();
- SettingsManager.write();
- }
- this.localPlayer.chatMsg = '';
- this.localPlayer.chatActive = false;
- this.nameSettingActive = false;
- }
-
- if (e.key === 'Escape' || e.key === 'Enter') {
- this.localPlayer.chatMsg = '';
- this.localPlayer.chatActive = false;
- this.nameSettingActive = false;
- }
-
- if ((this.localPlayer.chatActive) && e.key.length === 1 && this.localPlayer.chatMsg.length < 300) {
- this.localPlayer.chatMsg += e.key;
- }
-
- if ((this.nameSettingActive) && e.key.length === 1 && this.localPlayer.chatMsg.length < 42) {
- this.localPlayer.chatMsg += e.key;
- }
-
- if (e.key.toLowerCase() === 't' && !this.nameSettingActive) {
- if (this.localPlayer.name.length > 0) this.localPlayer.chatActive = true;
- else this.nameSettingActive = true;
- }
-
- if (e.key === '/' && !this.nameSettingActive && !this.localPlayer.chatActive) {
- if (this.localPlayer.name.length > 0) {
- this.localPlayer.chatActive = true;
- this.localPlayer.chatMsg = '/';
- } else {
- this.nameSettingActive = true;
- }
- }
-
- if (e.key.toLowerCase() === 'n' && !this.localPlayer.chatActive) {
- this.nameSettingActive = true;
- }
- }
-
- public addChatMessage(msg: { id: number; name: string; message: string }) {
- const chatMessage: ChatMessage = {
- id: msg.id,
- name: msg.name,
- message: msg.message,
- timestamp: Date.now() / 1000,
- };
- this.chatMessages.push(chatMessage);
- }
-
- private clearOldMessages() {
- for (let i = 0; i < this.chatMessages.length; i++) {
- if (Date.now() / 1000 - this.chatMessages[i].timestamp > this.chatMessageLifespan + 5) {
- this.chatMessages.splice(i, 1);
- }
- }
-
- for (let i = this.chatMessages.length - 1; i >= 0; i--) {
- if (i < this.chatMessages.length - this.maxMessagesOnScreen) {
- this.chatMessages[i].timestamp = Math.min(
- Date.now() / 1000 - this.chatMessageLifespan,
- this.chatMessages[i].timestamp,
- );
- }
- }
- }
-
- private doTextWrapping(
- ctx: CanvasRenderingContext2D,
- text: string[],
- maxWidth: number,
- initialOffset: number = 0,
- ): string[] {
- ctx.font = '8px Tiny5';
- const resultLines: string[] = [];
-
- for (const line of text) {
- if (line === '' || ctx.measureText(line).width <= maxWidth) {
- resultLines.push(line);
- continue;
- }
-
- const words = line.split(' ');
- let currentLine = '';
- let isFirstLine = true;
-
- for (const word of words) {
- const testLine = currentLine ? `${currentLine} ${word}` : word;
- const testWidth = ctx.measureText(testLine).width;
-
- const availableWidth = isFirstLine ? maxWidth - initialOffset : maxWidth;
-
- if (testWidth <= availableWidth) {
- currentLine = testLine;
- } else {
- if (currentLine) {
- resultLines.push(currentLine);
- }
- currentLine = word;
- isFirstLine = false;
- }
- }
-
- if (currentLine) {
- resultLines.push(currentLine);
- }
- }
-
- return resultLines;
- }
-
- private generateUniqueId(): string {
- return Math.random().toString(36).substr(2, 9);
- }
-}
+ private chatCanvas: HTMLCanvasElement;
+ private chatCtx: CanvasRenderingContext2D;
+ private chatMessages: ChatMessage[]; // Typed as ChatMessage[]
+ private chatMessageLifespan: number;
+ private charsToRemovePerSecond: number;
+ private maxMessagesOnScreen: number;
+ private nameSettingActive: boolean;
+ private localPlayer: Player;
+ private renderer!: Renderer;
+ private networking!: Networking;
+ private screenWidth: number;
+ private inputHandler!: InputHandler;
+ private debugTextHeight!: number;
+ private oldScreenWidth: number = 0;
+ private readonly commandManager: CommandManager;
+ private lastTouchTimestamp: number = 0;
+ private touchJoystickEngaged: boolean = false;
+ private joystickX: number = 0;
+ private joystickY: number = 0;
+ private joystickInputX: number = 0;
+ private joystickInputY: number = 0;
+ private buttonsHeld: number[] = [];
+ private lastRoutineMs = 0;
+
+ private offscreenCanvas: HTMLCanvasElement;
+ private offscreenCtx: CanvasRenderingContext2D;
+
+ public gameMessages: string[] = [];
+ private previousGameMessages: string[] = [];
+
+ // Removed animatedGameMessages in favor of per-line management
+ private lines: LineMessage[] = [];
+ private animationDuration: number = 1; // Adjusted for smoother animation
+
+ // Color code mapping
+ COLOR_CODES: { [key: string]: string } = {
+ '0': '#000000', // Black
+ '1': '#0000AA', // Dark Blue
+ '2': '#00AA00', // Dark Green
+ '3': '#00AAAA', // Dark Aqua
+ '4': '#AA0000', // Dark Red
+ '5': '#AA00AA', // Dark Purple
+ '6': '#FFAA00', // Gold
+ '7': '#AAAAAA', // Gray
+ '8': '#555555', // Dark Gray
+ '9': '#5555FF', // Blue
+ 'a': '#55FF55', // Green
+ 'b': '#55FFFF', // Aqua
+ 'c': '#FF5555', // Red
+ 'd': '#FF55FF', // Light Purple
+ 'e': '#FFFF55', // Yellow
+ 'f': '#FFFFFF', // White
+ 'g': this.getRainbowColor()
+ };
+
+ private getColorCode(code: string): string | false {
+ if (code === 'g') {
+ return this.getRainbowColor();
+ }
+ return this.COLOR_CODES[code] || false;
+ }
+
+
+ private getRainbowColor(): string {
+ const hue = (Date.now() / 20) % 360;
+ return `hsl(${hue}, 100%, 50%)`;
+ }
+
+
+ constructor(container: HTMLElement, localPlayer: Player) {
+ this.localPlayer = localPlayer;
+ this.chatCanvas = document.createElement('canvas');
+ this.chatCtx = this.chatCanvas.getContext('2d') as CanvasRenderingContext2D;
+ this.chatCtx.imageSmoothingEnabled = false;
+
+
+ this.chatCanvas.width = 400;
+ this.chatCanvas.height = 200;
+
+ this.chatMessages = [];
+ this.chatMessageLifespan = 40; // 40 seconds
+ this.charsToRemovePerSecond = 30;
+ this.maxMessagesOnScreen = 12;
+
+ this.nameSettingActive = false;
+ this.screenWidth = 100;
+
+ this.commandManager = new CommandManager(this.localPlayer, this);
+
+ this.setupEventListeners();
+
+ this.chatCanvas.style.position = 'absolute';
+ this.chatCanvas.style.display = 'block';
+ this.chatCanvas.style.zIndex = '100';
+ this.chatCanvas.style.top = '0';
+ this.chatCanvas.style.left = '0';
+
+ this.chatCanvas.style.height = '100vh';
+ document.body.style.margin = '0';
+ this.chatCanvas.style.imageRendering = 'pixelated';
+ this.chatCanvas.style.textRendering = 'pixelated';
+
+ this.chatCanvas.style.touchAction = 'none';
+
+ this.offscreenCanvas = document.createElement('canvas');
+ this.offscreenCtx = this.offscreenCanvas.getContext('2d') as CanvasRenderingContext2D;
+
+ // Initialize lines for per-line message management
+ this.lines = Array(this.maxMessagesOnScreen).fill(null).map(() => ({
+ currentMessage: null,
+ pendingMessage: null
+ }));
+
+ //document.body.appendChild(this.chatCanvas);
+ container.appendChild(this.chatCanvas);
+
+ globalThis.addEventListener('resize', this.onWindowResize.bind(this));
+ globalThis.addEventListener('orientationchange', this.onWindowResize.bind(this));
+ }
+
+ public setRenderer(renderer: Renderer) {
+ this.renderer = renderer;
+ }
+
+ public setNetworking(networking: Networking) {
+ this.networking = networking;
+ }
+
+ public setInputHandler(inputHandler: InputHandler) {
+ this.inputHandler = inputHandler;
+ }
+
+ private setupEventListeners() {
+ document.addEventListener('keydown', this.onKeyDown.bind(this));
+ }
+
+ public onFrame() {
+ const startTime = Date.now();
+ const now = Date.now() / 1000;
+
+ this.gameMessages = this.localPlayer.gameMsgs;
+ this.detectGameMessagesChanges(now);
+ this.updateAnimatedGameMessages(now);
+
+ this.clearOldMessages();
+ this.chatCtx.clearRect(0, 0, this.chatCanvas.width, this.chatCanvas.height);
+ this.renderHitMarkers();
+ this.renderChatMessages();
+ this.renderGameText();
+ this.renderDebugText();
+ if (this.inputHandler.getKey('tab'))
+ this.renderPlayerList();
+ this.renderEvil();
+ this.renderCrosshair();
+ this.renderTouchControls();
+
+ this.screenWidth = Math.floor(this.renderer.getCamera().aspect * 200);
+
+ if (this.oldScreenWidth !== this.screenWidth) {
+ //if(this.chatCanvas.width < this.screenWidth)
+ this.chatCanvas.width = this.screenWidth;
+ this.oldScreenWidth = this.screenWidth;
+ }
+
+ // this.chatCanvas.width = this.screenWidth;
+ // this.chatCtx.fillRect(0,0,10,10);
+
+ this.onWindowResize();
+
+ this.inputHandler.nameSettingActive = this.nameSettingActive;
+ if (Math.random() < 0.03)
+ this.lastRoutineMs = Date.now() - startTime;
+ }
+
+ private onWindowResize() {
+ this.chatCanvas.style.width = globalThis.innerWidth + 'px';
+ this.chatCanvas.style.height = globalThis.innerHeight + 'px';
+ }
+
+ private renderChatMessages() {
+ const ctx = this.chatCtx;
+
+ this.offscreenCtx.font = '8px Tiny5';
+ this.offscreenCtx.fillStyle = 'white';
+
+ const usermsg = this.localPlayer.chatMsg;
+ let cursor = '';
+ if ((Date.now() / 1000) % 0.7 < 0.35) cursor = '|';
+
+ const linesToRender: string[] = [];
+ const pixOffsets: number[] = [];
+ const messagesBeingTyped = this.networking.getMessagesBeingTyped();
+
+ for (let i = 0; i < this.chatMessages.length; i++) {
+ let msg = this.chatMessages[i].message;
+ const name = this.chatMessages[i].name;
+ if (name.length > 0) msg = `${name}: ${msg}`;
+
+ const duplicateFromPlayerData = messagesBeingTyped.includes(msg);
+
+ let charsToRemove = Date.now() / 1000 - this.chatMessages[i].timestamp - this.chatMessageLifespan;
+ charsToRemove = Math.max(0, charsToRemove * this.charsToRemovePerSecond);
+ charsToRemove = Math.floor(charsToRemove);
+
+ let removedSubstring = '';
+ let remainingMsg = msg;
+ if (charsToRemove > 0) {
+ let charsRemoved = 0;
+ while (charsRemoved < charsToRemove && remainingMsg.length > 0) {
+ const char = remainingMsg.charAt(0);
+ removedSubstring += char;
+ remainingMsg = remainingMsg.substring(1);
+ charsRemoved++;
+ }
+ }
+
+ if (!duplicateFromPlayerData) {
+ linesToRender.push(remainingMsg);
+ pixOffsets.push(this.offscreenCtx.measureText(removedSubstring).width);
+ }
+ }
+
+ for (const msg of messagesBeingTyped) {
+ linesToRender.push(msg + cursor);
+ pixOffsets.push(0);
+ }
+
+ if (this.localPlayer.chatActive) {
+ if (this.localPlayer.chatMsg.startsWith('>'))
+ linesToRender.push('&2' + usermsg + cursor);
+ else
+ linesToRender.push(usermsg + cursor);
+ pixOffsets.push(0);
+ }
+
+ if (this.nameSettingActive) {
+ linesToRender.push('Enter your name: ' + usermsg + cursor);
+ pixOffsets.push(0);
+ this.localPlayer.name = usermsg + cursor;
+ if (this.localPlayer.name.length == 0) this.localPlayer.name = ' ';
+ }
+
+ const wrappedLines: string[] = [];
+ const lineOrigins: number[] = [];
+ const isFirstWrappedLine: boolean[] = [];
+
+ for (let i = 0; i < linesToRender.length; i++) {
+ const wrapped = this.doTextWrapping(this.offscreenCtx, [linesToRender[i]], this.screenWidth - 10);
+ for (let j = 0; j < wrapped.length; j++) {
+ wrappedLines.push(wrapped[j]);
+ lineOrigins.push(i);
+ isFirstWrappedLine.push(j === 0);
+ }
+ }
+
+ const totalLines = wrappedLines.length;
+ for (let i = 0; i < totalLines; i++) {
+ const lineIndex = totalLines - i - 1;
+ const text = wrappedLines[lineIndex];
+ const originIndex = lineOrigins[lineIndex];
+ const pixOffset = isFirstWrappedLine[lineIndex] ? pixOffsets[originIndex] : 0;
+
+ this.renderPixelText(text, 3 + pixOffset, 200 - 20 - 8 * i, 'white');
+ }
+
+ if ((usermsg !== '' && this.localPlayer.chatActive) || this.nameSettingActive) {
+ ctx.fillStyle = 'rgba(145,142,118,0.3)';
+ let width = ctx.measureText(usermsg).width;
+ if (this.nameSettingActive) {
+ width = ctx.measureText('Enter your name: ' + usermsg).width;
+ }
+ ctx.fillRect(2, 200 - 20 - 7, width + 1, 9);
+ }
+ }
+
+ private renderPrettyText(text: string, x: number, y: number, defaultColor: string) {
+ let currentX = x;
+ const segments: { text: string, color: string }[] = [];
+ let currentColor = defaultColor;
+ let currentSegment = '';
+
+ // Parse color codes and split into segments
+ for (let i = 0; i < text.length; i++) {
+ if (text[i] === '&' && i + 1 < text.length && this.getColorCode(text[i + 1])) {
+ if (currentSegment) {
+ segments.push({ text: currentSegment, color: currentColor });
+ }
+ currentColor = this.getColorCode(text[i + 1]);
+ currentSegment = '';
+ i++; // Skip the color code character
+ } else {
+ currentSegment += text[i];
+ }
+ }
+
+ if (currentSegment) {
+ segments.push({ text: currentSegment, color: currentColor });
+ }
+
+ // Render each segment
+ for (const segment of segments) {
+ this.offscreenCtx.font = '8px Tiny5';
+ const textMetrics = this.offscreenCtx.measureText(segment.text);
+ const textWidth = Math.max(Math.ceil(textMetrics.width), 1);
+ const textHeight = 8;
+
+ if (this.offscreenCanvas.width !== textWidth || this.offscreenCanvas.height !== textHeight) {
+ this.offscreenCanvas.width = textWidth;
+ this.offscreenCanvas.height = textHeight;
+ }
+
+ this.offscreenCtx.clearRect(0, 0, textWidth, textHeight);
+ this.offscreenCtx.font = '8px Tiny5';
+ this.offscreenCtx.fillStyle = segment.color;
+ this.offscreenCtx.fillText(segment.text, 0, textHeight - 1);
+
+ const imageData = this.offscreenCtx.getImageData(0, 0, textWidth, textHeight);
+ const data = imageData.data;
+
+ for (let i = 0; i < data.length; i += 4) {
+ data[i + 3] = data[i + 3] > 170 ? 255 : 0;
+ }
+
+ this.offscreenCtx.putImageData(imageData, 0, 0);
+ this.chatCtx.drawImage(this.offscreenCanvas, currentX, y - textHeight + 1);
+ currentX += textWidth;
+ }
+ }
+
+ private renderUglyText(text: string, x: number, y: number, defaultColor: string) {
+ let currentX = x;
+ let currentColor = defaultColor;
+ let currentSegment = '';
+
+ for (let i = 0; i < text.length; i++) {
+ if (text[i] === '&' && i + 1 < text.length && this.getColorCode(text[i + 1])) {
+ if (currentSegment) {
+ this.chatCtx.font = '8px Tiny5';
+ this.chatCtx.fillStyle = currentColor;
+ this.chatCtx.fillText(currentSegment, currentX, y);
+ currentX += this.chatCtx.measureText(currentSegment).width;
+ }
+ currentColor = this.getColorCode(text[i + 1]);
+ currentSegment = '';
+ i++; // Skip the color code character
+ } else {
+ currentSegment += text[i];
+ }
+ }
+
+ if (currentSegment) {
+ this.chatCtx.font = '8px Tiny5';
+ this.chatCtx.fillStyle = currentColor;
+ this.chatCtx.fillText(currentSegment, currentX, y);
+ }
+ }
+
+ private renderPixelText(text: string, x: number, y: number, color: string) {
+ if (SettingsManager.settings.doPrettyText)
+ this.renderPrettyText(text, x, y, color);
+ else
+ this.renderUglyText(text, x, y, color);
+ }
+
+ private renderDebugText() {
+ const ctx = this.chatCtx;
+ ctx.font = '8px Tiny5';
+ ctx.fillStyle = 'teal';
+
+ const linesToRender = [];
+ const framerate = this.renderer.getFramerate();
+
+ if (this.localPlayer.latency >= 999)
+ linesToRender.push('disconnected :(');
+
+ //const playerX = Math.round(this.localPlayer.position.x);
+
+ linesToRender.push('candiru ' + this.localPlayer.gameVersion + ' @ ' + Math.round(framerate) + 'fps, ' + Math.round(this.localPlayer.latency) + 'ms');
+ //linesToRender.push('connected to: ' + this.networking.getServerInfo().name);
+ //linesToRender.push('players: ' + this.networking.getServerInfo().currentPlayers + '/' + this.networking.getServerInfo().maxPlayers);
+ //linesToRender.push('map: ' + this.networking.getServerInfo().mapName);
+ //linesToRender.push('mode: ' + this.networking.getServerInfo().gameMode);
+ //linesToRender.push('serverVersion: ' + this.networking.getServerInfo().version);
+ //linesToRender.push('tickRate: ' + this.networking.getServerInfo().tickRate);
+ //linesToRender.push('playerMaxHealth: ' + this.networking.getServerInfo().playerMaxHealth);
+ //linesToRender.push('health: ' + this.localPlayer.health);
+
+ for(const msg of this.localPlayer.gameMsgs2)
+ linesToRender.push(msg)
+
+ //linesToRender.push('routineTime: ' + this.lastRoutineMs + 'ms');
+
+ for (let i = 0; i < linesToRender.length; i++) {
+ this.renderPixelText(linesToRender[i], 2, 7 + 7 * i, 'teal');
+ }
+
+ this.debugTextHeight = 7 * linesToRender.length;
+ }
+
+ private detectGameMessagesChanges(now: number) {
+ const current = this.gameMessages;
+
+ for (let i = 0; i < this.maxMessagesOnScreen; i++) {
+ const line = this.lines[i];
+ const currentMessage = current[i] || null;
+
+ if (!line.currentMessage) {
+ if (currentMessage) {
+ line.currentMessage = {
+ id: this.generateUniqueId(),
+ message: currentMessage,
+ state: 'animatingIn',
+ animationProgress: 0,
+ timestamp: now,
+ };
+ }
+ continue;
+ }
+
+ if (currentMessage !== line.currentMessage.message) {
+ if (line.currentMessage.state === 'idle') {
+ line.currentMessage.state = 'animatingOut';
+ line.currentMessage.timestamp = now;
+ line.pendingMessage = currentMessage;
+ } else {
+ line.pendingMessage = currentMessage;
+ }
+ }
+ }
+
+ this.previousGameMessages = [...current];
+ }
+
+
+ private updateAnimatedGameMessages(now: number) {
+ for (let i = 0; i < this.maxMessagesOnScreen; i++) {
+ const line = this.lines[i];
+ if (!line.currentMessage) continue; // Early return if null
+
+ const elapsed = now - line.currentMessage.timestamp;
+ let progress = Math.min(elapsed / this.animationDuration, 1);
+ progress = this.easeOut(progress);
+
+ line.currentMessage.animationProgress = progress;
+
+ if (line.currentMessage.state === 'animatingOut' && progress >= 1) {
+ // Remove the message after fade-out
+ if (line.pendingMessage) {
+ line.currentMessage = {
+ id: this.generateUniqueId(),
+ message: line.pendingMessage,
+ state: 'animatingIn',
+ animationProgress: 0,
+ timestamp: now,
+ };
+ line.pendingMessage = null;
+ } else {
+ line.currentMessage = null;
+ }
+ continue;
+ }
+
+ if (line.currentMessage.state === 'animatingIn' && progress >= 1) {
+ line.currentMessage.state = 'idle';
+ line.currentMessage.animationProgress = 1;
+ }
+ }
+ }
+
+
+ private renderGameText() {
+ const ctx = this.chatCtx;
+ ctx.font = '8px Tiny5';
+ const centerY = this.chatCanvas.height / 2 + 48;
+
+ for (let i = 0; i < this.maxMessagesOnScreen; i++) {
+ const line = this.lines[i];
+ if (!line.currentMessage) continue;
+
+ let visibleText = line.currentMessage.message;
+
+ // Check if we should skip animation
+ const shouldSkipAnimation =
+ line.currentMessage.state === 'animatingOut' &&
+ line.pendingMessage !== null &&
+ line.currentMessage.message.includes('seconds') &&
+ line.pendingMessage.includes('seconds');
+
+ if (shouldSkipAnimation && line.pendingMessage) {
+ // Directly update to the pending message
+ line.currentMessage = {
+ id: this.generateUniqueId(),
+ message: line.pendingMessage,
+ state: 'idle',
+ animationProgress: 1,
+ timestamp: Date.now() / 1000,
+ };
+ line.pendingMessage = null;
+ visibleText = line.currentMessage.message;
+ } else if (line.currentMessage.state === 'animatingIn' || line.currentMessage.state === 'animatingOut') {
+ visibleText = this.getVisibleText(line.currentMessage.message, line.currentMessage.state, line.currentMessage.animationProgress);
+ }
+
+ // Calculate the actual width of the rendered text, including color codes
+ const textWidth = this.getRenderedTextWidth(visibleText);
+ const x = Math.floor((this.screenWidth - textWidth) / 2);
+ const y = Math.floor(centerY + (i * 10));
+
+ this.renderPixelText(visibleText, x, y, 'white');
+ }
+ }
+
+
+ private getRenderedTextWidth(text: string): number {
+ let totalWidth = 0;
+ let currentSegment = '';
+ const ctx = this.chatCtx;
+
+ for (let i = 0; i < text.length; i++) {
+ if (text[i] === '&' && i + 1 < text.length && this.getColorCode(text[i + 1])) {
+ // Measure the current segment before switching color
+ if (currentSegment) {
+ totalWidth += ctx.measureText(currentSegment).width;
+ currentSegment = '';
+ }
+ i++; // Skip the color code character
+ } else {
+ currentSegment += text[i];
+ }
+ }
+
+ // Measure the last segment
+ if (currentSegment) {
+ totalWidth += ctx.measureText(currentSegment).width;
+ }
+
+ return totalWidth;
+ }
+
+
+
+
+ private getVisibleText(message: string, state: 'animatingIn' | 'animatingOut' | 'idle', progress: number): string {
+ if (state === 'idle') {
+ return message;
+ }
+
+ const length = message.length;
+ if (length === 0) return '';
+
+ let charsToShow = 0;
+
+ if (state === 'animatingIn') {
+ charsToShow = Math.floor(progress * length);
+ charsToShow = Math.min(charsToShow, length); // Ensure it doesn't exceed the message length
+ return message.substring(0, charsToShow);
+ } else if (state === 'animatingOut') {
+ charsToShow = Math.floor((1 - progress) * length);
+ charsToShow = Math.max(charsToShow, 0); // Ensure it doesn't go below zero
+ return message.substring(0, charsToShow);
+ }
+
+ return message;
+ }
+
+ private easeOut(progress: number): number {
+ return 1 - Math.pow(1 - progress, 1.5);
+ }
+
+ public renderTouchControls() {
+ if (Date.now() / 1000 - this.lastTouchTimestamp > 10) return;
+ if (this.touchJoystickEngaged) {
+ // Draw circle for movement
+ this.chatCtx.fillStyle = 'rgba(255,255,255,0.25)';
+ this.chatCtx.beginPath();
+ this.chatCtx.arc(this.joystickX, this.joystickY, TouchInputHandler.joystickRadius, 0, 2 * Math.PI);
+ this.chatCtx.fill();
+
+ // Smaller circle for joystick-- offset from center
+ this.chatCtx.fillStyle = 'rgba(255,255,255,0.5)';
+ this.chatCtx.beginPath();
+ this.chatCtx.arc(
+ this.joystickX + this.joystickInputX * TouchInputHandler.joystickRadius,
+ this.joystickY + this.joystickInputY * TouchInputHandler.joystickRadius,
+ 10,
+ 0,
+ 2 * Math.PI
+ );
+ this.chatCtx.fill();
+ }
+
+ // Draw rounded square center right for jumping
+ const squareWidth = 24;
+ const squareHeight = 24;
+ const cornerRadius = 6;
+ const x = this.chatCanvas.width - squareWidth - 12; // 12px from the right edge
+ let y = (this.chatCanvas.height - squareHeight) / 2; // Center vertically
+
+ this.drawButton(x, y, squareWidth, squareHeight, cornerRadius, '●', 1, 0);
+ y -= squareHeight + 4;
+ this.drawButton(x, y, squareWidth, squareHeight, cornerRadius, '↑', 1, -1);
+ y += squareHeight + 4;
+ y += squareHeight + 4;
+ this.drawButton(x, y, squareWidth, squareHeight, cornerRadius, '[]', 1, 1);
+ }
+
+ public setButtonsHeld(buttons: number[]) {
+ this.buttonsHeld = buttons;
+ }
+
+ private drawButton(x: number, y: number, width: number, height: number, cornerRadius: number, text: string, textOffset: number, index: number) {
+ if (this.buttonsHeld.includes(index))
+ this.chatCtx.fillStyle = 'rgba(100,100,100,0.3)';
+ else
+ this.chatCtx.fillStyle = 'rgba(255,255,255,0.15)';
+
+ this.drawRoundedSquare(x, y, width, height, cornerRadius);
+ // Draw character inside square
+ this.chatCtx.fillStyle = 'rgba(0,0,0,0.5)';
+ this.chatCtx.font = '16px Tiny5';
+ const textWidth = this.chatCtx.measureText(text).width;
+ this.chatCtx.fillText(text, Math.floor(x + width / 2 - textWidth / 2 + textOffset), Math.floor(y + height / 2 + 16 / 2 - 2));
+ }
+
+ private drawRoundedSquare(x: number, y: number, width: number, height: number, cornerRadius: number) {
+ this.chatCtx.beginPath();
+ this.chatCtx.moveTo(x + cornerRadius, y);
+ this.chatCtx.lineTo(x + width - cornerRadius, y);
+ this.chatCtx.quadraticCurveTo(x + width, y, x + width, y + cornerRadius);
+ this.chatCtx.lineTo(x + width, y + height - cornerRadius);
+ this.chatCtx.quadraticCurveTo(x + width, y + height, x + width - cornerRadius, y + height);
+ this.chatCtx.lineTo(x + cornerRadius, y + height);
+ this.chatCtx.quadraticCurveTo(x, y + height, x, y + height - cornerRadius);
+ this.chatCtx.lineTo(x, y + cornerRadius);
+ this.chatCtx.quadraticCurveTo(x, y, x + cornerRadius, y);
+ this.chatCtx.closePath();
+ this.chatCtx.fill();
+ }
+
+ public setLastTouchTimestamp(timestamp: number) {
+ this.lastTouchTimestamp = timestamp;
+ }
+
+ public setTouchJoystickEngaged(value: boolean) {
+ this.touchJoystickEngaged = value;
+ }
+
+ public setJoystickPosition(x: number, y: number) {
+ this.joystickX = x;
+ this.joystickY = y;
+ }
+
+ public setJoystickInput(x: number, y: number) {
+ this.joystickInputX = x;
+ this.joystickInputY = y;
+ }
+
+ public renderHitMarkers() {
+ const numDots = 10; // Number of dots to render around each hit point
+
+ for (let i = this.renderer.playerHitMarkers.length - 1; i >= 0; i--) {
+ if (this.renderer.playerHitMarkers[i].timestamp === -1)
+ this.renderer.playerHitMarkers[i].timestamp = Date.now() / 1000; // Set timestamp if not set
+
+ const timeSinceHit = Date.now() / 1000 - this.renderer.playerHitMarkers[i].timestamp;
+ const lifePercent = timeSinceHit / hitMarkerLifetime;
+
+ if (timeSinceHit > hitMarkerLifetime) {
+ this.renderer.playerHitMarkers.splice(i, 1);
+ continue;
+ }
+
+ const hitVec = this.renderer.playerHitMarkers[i].hitPoint;
+ const projected = hitVec.clone().project(this.renderer.getCamera());
+ const projectedX = Math.round((projected.x + 1) * this.screenWidth / 2);
+ const projectedY = Math.round((-projected.y + 1) * 200 / 2);
+
+ if (projected.z < 1) {
+ this.chatCtx.fillStyle = 'rgba(255,0,0,' + (1 - Math.pow(lifePercent, 1.25)) + ')';
+
+ // Calculate sizeMultiplier
+ const sizeMultiplier = 1 + 2 / this.renderer.playerHitMarkers[i].shotVector.length();
+
+ // Calculate and render dots
+ const radius = Math.pow(lifePercent, 0.7) * 7 * sizeMultiplier; // Radius of the circle in which dots are placed
+ for (let j = 0; j < numDots; j++) {
+ const angle = (Math.PI * 2 / numDots) * j;
+ const dotX = Math.round(projectedX + radius * Math.cos(angle));
+ const dotY = Math.round(projectedY + radius * Math.sin(angle));
+
+ this.chatCtx.fillRect(dotX, dotY, 1, 1); // Render a 1px by 1px dot
+ }
+ }
+ }
+ }
+
+ public getDebugTextHeight(): number {
+ return this.debugTextHeight;
+ }
+
+ private renderPlayerList() {
+ const ctx = this.chatCtx;
+ const linesToRender: string[] = [];
+ const colorsToRender: string[] = [];
+ const playerData = this.networking.getRemotePlayerData();
+
+ linesToRender.push(playerData.length + ' online - ' + Math.round(this.localPlayer.latency) + 'ms');
+ colorsToRender.push('white');
+ for (let i = 0; i < playerData.length; i++) {
+ linesToRender.push(playerData[i].name);
+ if (playerData[i].latency > 200)
+ colorsToRender.push('red');
+ else if (playerData[i].latency > 50)
+ colorsToRender.push('orange');
+ else
+ colorsToRender.push('green');
+ }
+
+ ctx.font = '8px Tiny5';
+
+ let longestLinePix = 0;
+ for (let i = 0; i < linesToRender.length; i++)
+ longestLinePix = Math.max(longestLinePix, ctx.measureText(linesToRender[i]).width);
+
+ ctx.fillStyle = 'rgba(0,0,0,0.5)';
+ ctx.fillRect(Math.floor(this.screenWidth / 2 - longestLinePix / 2), 4, longestLinePix + 3, linesToRender.length * 7 + 2);
+
+ for (let i = 0; i < linesToRender.length; i++) {
+ this.renderPixelText(linesToRender[i], Math.floor(this.screenWidth / 2 - longestLinePix / 2 + 2), 11 + 7 * i, colorsToRender[i]);
+ }
+ }
+
+ private renderEvil() {
+ const ctx = this.chatCtx;
+ if (Date.now() / 1000 - this.networking.getDamagedTimestamp() < 0.05) {
+ ctx.fillStyle = 'rgba(255,0,0,0.1)';
+ ctx.fillRect(0, 0, this.chatCanvas.width, this.chatCanvas.height);
+ }
+ }
+
+ private renderCrosshair() {
+ const ctx = this.chatCtx;
+ ctx.fillStyle = SettingsManager.settings.crosshairColor;
+ if (this.renderer.crosshairIsFlashing)
+ ctx.fillStyle = '#FF0000';
+ switch (SettingsManager.settings.crosshairType) {
+ case 0:
+ ctx.fillRect(Math.floor(this.screenWidth / 2), 100 - 3, 1, 7);
+ ctx.fillRect(Math.floor(this.screenWidth / 2 - 3), 100, 7, 1);
+ break;
+ case 1:
+ ctx.fillRect(Math.floor(this.screenWidth / 2), 100, 1, 1);
+ break;
+ }
+ }
+
+ private onKeyDown(e: KeyboardEvent) {
+ if (e.key === 'Backspace' && (this.localPlayer.chatActive || this.nameSettingActive)) {
+ this.localPlayer.chatMsg = this.localPlayer.chatMsg.slice(0, -1);
+ return;
+ }
+
+ if (e.key === 'Enter') {
+ if (this.localPlayer.chatActive) {
+ if (!this.commandManager.runCmd(this.localPlayer.chatMsg)) this.networking.sendMessage(this.localPlayer.chatMsg);
+ }
+ if (this.nameSettingActive) {
+ this.localPlayer.name = this.localPlayer.chatMsg.toString();
+ SettingsManager.settings.name = this.localPlayer.chatMsg.toString();
+ SettingsManager.write();
+ }
+ this.localPlayer.chatMsg = '';
+ this.localPlayer.chatActive = false;
+ this.nameSettingActive = false;
+ }
+
+ if (e.key === 'Escape' || e.key === 'Enter') {
+ this.localPlayer.chatMsg = '';
+ this.localPlayer.chatActive = false;
+ this.nameSettingActive = false;
+ }
+
+ if ((this.localPlayer.chatActive) && e.key.length === 1 && this.localPlayer.chatMsg.length < 300)
+ this.localPlayer.chatMsg += e.key;
+
+ if ((this.nameSettingActive) && e.key.length === 1 && this.localPlayer.chatMsg.length < 42)
+ this.localPlayer.chatMsg += e.key;
+
+ if (e.key.toLowerCase() === 't' && !this.nameSettingActive) {
+ if (this.localPlayer.name.length > 0) this.localPlayer.chatActive = true;
+ else this.nameSettingActive = true;
+ }
+
+ if (e.key === '/' && !this.nameSettingActive && !this.localPlayer.chatActive) {
+ if (this.localPlayer.name.length > 0) {
+ this.localPlayer.chatActive = true;
+ this.localPlayer.chatMsg = '/';
+ } else {
+ this.nameSettingActive = true;
+ }
+ }
+
+ if (e.key.toLowerCase() === 'n' && !this.localPlayer.chatActive)
+ this.nameSettingActive = true;
+ }
+
+ public addChatMessage(msg: { id: number; name: string; message: string; }) {
+ const chatMessage: ChatMessage = {
+ id: msg.id,
+ name: msg.name,
+ message: msg.message,
+ timestamp: Date.now() / 1000,
+ };
+ this.chatMessages.push(chatMessage);
+ }
+
+ private clearOldMessages() {
+ for (let i = 0; i < this.chatMessages.length; i++)
+ if (Date.now() / 1000 - this.chatMessages[i].timestamp > this.chatMessageLifespan + 5)
+ this.chatMessages.splice(i, 1);
+
+ for (let i = this.chatMessages.length - 1; i >= 0; i--) {
+ if (i < this.chatMessages.length - this.maxMessagesOnScreen)
+ this.chatMessages[i].timestamp = Math.min(Date.now() / 1000 - this.chatMessageLifespan, this.chatMessages[i].timestamp);
+ }
+ }
+
+ private doTextWrapping(ctx: CanvasRenderingContext2D, text: string[], maxWidth: number, initialOffset: number = 0): string[] {
+ ctx.font = '8px Tiny5';
+ const resultLines: string[] = [];
+
+ for (const line of text) {
+ if (line === '' || ctx.measureText(line).width <= maxWidth) {
+ resultLines.push(line);
+ continue;
+ }
+
+ const words = line.split(' ');
+ let currentLine = '';
+ let isFirstLine = true;
+
+ for (const word of words) {
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
+ const testWidth = ctx.measureText(testLine).width;
+
+ const availableWidth = isFirstLine ? maxWidth - initialOffset : maxWidth;
+
+ if (testWidth <= availableWidth) {
+ currentLine = testLine;
+ } else {
+ if (currentLine) {
+ resultLines.push(currentLine);
+ }
+ currentLine = word;
+ isFirstLine = false;
+ }
+ }
+
+ if (currentLine) {
+ resultLines.push(currentLine);
+ }
+ }
+
+ return resultLines;
+ }
+
+ private generateUniqueId(): string {
+ return Math.random().toString(36).substr(2, 9);
+ }
+}
\ No newline at end of file
diff --git a/src/client/ui/HealthIndicator.ts b/src/client/ui/HealthIndicator.ts
index ecc1885..6883a78 100644
--- a/src/client/ui/HealthIndicator.ts
+++ b/src/client/ui/HealthIndicator.ts
@@ -1,134 +1,132 @@
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
import * as THREE from 'three';
-import { Renderer } from '../core/Renderer.ts';
-import { Player } from '../core/Player.ts';
-import { Networking } from '../core/Networking.ts';
+import {Renderer} from "../core/Renderer.ts";
+import {Player} from "../core/Player.ts";
+import {Networking} from "../core/Networking.ts";
+
const clock = new THREE.Clock();
export class HealthIndicator {
- private scene: THREE.Scene;
- private possumObject!: THREE.Object3D;
- private sceneAdded: boolean = false;
- private targetQuaternion: THREE.Quaternion = new THREE.Quaternion(0, 0, 0, 1);
- private targetPosition: THREE.Vector3 = new THREE.Vector3(0, 0, 0);
- private rotatedAngle: number = 0;
- private ambientLight: THREE.AmbientLight;
- private lastHealth: number = 0;
- private lastHealthChangeWasDamage: boolean = false;
- private lightRGBI: number[] = [0, 0, 0, 0];
-
- constructor(private renderer: Renderer, private localPlayer: Player, private networking: Networking) {
- this.scene = renderer.getHealthIndicatorScene();
- this.ambientLight = new THREE.AmbientLight(rgbToHex(0, 0, 0), 0);
- this.scene.add(this.ambientLight);
- }
-
- public init() {
- const loader = new GLTFLoader();
- const dracoLoader = new DRACOLoader();
- dracoLoader.setDecoderPath('/draco/');
- loader.setDRACOLoader(dracoLoader);
- loader.load(
- 'models/simplified_possum.glb',
- (gltf) => {
- this.possumObject = gltf.scene;
- this.possumObject.traverse((child) => {
- if ((child as THREE.Mesh).isMesh) {
- child.renderOrder = 999;
- const applyDepthTest = (material: THREE.Material | THREE.Material[]) => {
- if (Array.isArray(material)) {
- material.forEach((mat) => applyDepthTest(mat)); // Recursively handle array elements
- } else {
- material.depthTest = false;
- }
- };
- const mesh = child as THREE.Mesh;
- applyDepthTest(mesh.material);
- }
- });
- },
- undefined,
- () => {
- console.log('overlay possum loading error');
- },
- );
- }
-
- public onFrame() {
- if (!this.possumObject) return;
- if (!this.sceneAdded) {
- this.scene.add(this.possumObject);
- this.sceneAdded = true;
- }
-
- let maxHealth = this.networking.getServerInfo().playerMaxHealth;
- if (maxHealth === 0) maxHealth = 0.001;
-
- const deltaTime = clock.getDelta();
- const scaredLevel = 1 - Math.pow(this.localPlayer.health / maxHealth, 1); //0-1
- this.renderer.scaredLevel = scaredLevel;
-
- this.targetPosition.copy(basePosition);
- this.targetPosition.y += scaredLevel * 0.5 * Math.sin(1.1 * Math.PI * this.rotatedAngle);
- this.targetPosition.y += (Math.random() - 0.5) * 0.2 * scaredLevel;
- this.targetPosition.x += (Math.random() - 0.5) * 0.2 * scaredLevel;
- this.targetPosition.z += (Math.random() - 0.5) * 0.2 * scaredLevel;
-
- this.targetQuaternion.copy(baseQuaternion);
- rotateAroundWorldAxis(
- this.targetQuaternion,
- new THREE.Vector3(0, 0, 1),
- Math.PI - this.localPlayer.health * Math.PI / maxHealth,
- );
-
- this.rotatedAngle += 4 * deltaTime / (Math.max(0.001, (1 - scaredLevel) * 3));
- rotateAroundWorldAxis(this.targetQuaternion, new THREE.Vector3(0, 1, 0), this.rotatedAngle);
-
- moveTowardsPos(this.possumObject.position, this.targetPosition, 0.8 * deltaTime * 60);
- moveTowardsRot(this.possumObject.quaternion, this.targetQuaternion, 0.5 * deltaTime * 60);
-
- let targetRGBI: number[];
-
- if (!this.lastHealthChangeWasDamage && this.localPlayer.health < maxHealth && this.rotatedAngle % 2 > 1) {
- targetRGBI = [125, 255, 125, 1.2];
- } else {
- targetRGBI = [255, 255, 255, 0.5];
- }
-
- for (let i = 0; i < 4; i++) {
- this.lightRGBI[i] = this.lightRGBI[i] + (targetRGBI[i] - this.lightRGBI[i]) * 0.4 * deltaTime * 60;
- }
- this.ambientLight.copy(
- new THREE.AmbientLight(rgbToHex(this.lightRGBI[0], this.lightRGBI[1], this.lightRGBI[2]), this.lightRGBI[3]),
- );
-
- if (this.lastHealth < this.localPlayer.health) {
- this.lastHealthChangeWasDamage = false;
- } else if (this.lastHealth > this.localPlayer.health) {
- this.lastHealthChangeWasDamage = true;
- }
- this.lastHealth = this.localPlayer.health;
- }
+ private scene: THREE.Scene;
+ private possumObject!: THREE.Object3D;
+ private sceneAdded: boolean = false;
+ private targetQuaternion: THREE.Quaternion = new THREE.Quaternion(0,0,0,1);
+ private targetPosition: THREE.Vector3 = new THREE.Vector3(0,0,0);
+ private rotatedAngle:number = 0;
+ private ambientLight: THREE.AmbientLight;
+ private lastHealth:number = 0;
+ private lastHealthChangeWasDamage:boolean = false;
+ private lightRGBI:number[] = [0,0,0,0];
+
+ constructor(private renderer: Renderer, private localPlayer:Player, private networking: Networking) {
+ this.scene = renderer.getHealthIndicatorScene();
+ this.ambientLight = new THREE.AmbientLight(rgbToHex(0,0,0), 0);
+ this.scene.add(this.ambientLight);
+ }
+
+ public init() {
+ const loader = new GLTFLoader();
+ const dracoLoader = new DRACOLoader();
+ dracoLoader.setDecoderPath('/draco/');
+ loader.setDRACOLoader(dracoLoader);
+ loader.load(
+ 'models/simplified_possum.glb',
+ (gltf) => {
+ this.possumObject = gltf.scene;
+ this.possumObject.traverse((child) => {
+ if ((child as THREE.Mesh).isMesh) {
+ child.renderOrder = 999;
+ const applyDepthTest = (material: THREE.Material | THREE.Material[]) => {
+ if (Array.isArray(material))
+ material.forEach((mat) => applyDepthTest(mat)); // Recursively handle array elements
+ else
+ material.depthTest = false;
+ };
+ const mesh = child as THREE.Mesh;
+ applyDepthTest(mesh.material);
+ }
+ });
+ },
+ undefined,
+ () => {
+ console.log('overlay possum loading error');
+ }
+ );
+ }
+
+ public onFrame() {
+ if (!this.possumObject) return;
+ if (!this.sceneAdded) {
+ this.scene.add(this.possumObject);
+ this.sceneAdded = true;
+ }
+
+ let maxHealth = this.networking.getServerInfo().playerMaxHealth;
+ if(maxHealth === 0) maxHealth = 0.001;
+
+ const deltaTime = clock.getDelta();
+ const scaredLevel = 1-Math.pow(this.localPlayer.health / maxHealth,1); //0-1
+ this.renderer.scaredLevel = scaredLevel;
+
+ this.targetPosition.copy(basePosition);
+ this.targetPosition.y += scaredLevel * 0.5 * Math.sin(1.1 * Math.PI * this.rotatedAngle);
+ this.targetPosition.y += (Math.random() - 0.5 ) * 0.2 * scaredLevel;
+ this.targetPosition.x += (Math.random() - 0.5 ) * 0.2 * scaredLevel;
+ this.targetPosition.z += (Math.random() - 0.5 ) * 0.2 * scaredLevel;
+
+ this.targetQuaternion.copy(baseQuaternion);
+ rotateAroundWorldAxis(this.targetQuaternion, new THREE.Vector3(0, 0, 1), Math.PI - this.localPlayer.health * Math.PI / maxHealth);
+
+ this.rotatedAngle += 4 * deltaTime / (Math.max(0.001, (1-scaredLevel)*3));
+ rotateAroundWorldAxis(this.targetQuaternion, new THREE.Vector3(0, 1, 0), this.rotatedAngle);
+
+ moveTowardsPos(this.possumObject.position, this.targetPosition, 0.8 * deltaTime * 60);
+ moveTowardsRot(this.possumObject.quaternion, this.targetQuaternion, 0.5 * deltaTime * 60);
+
+ let targetRGBI: number[];
+
+ if(!this.lastHealthChangeWasDamage && this.localPlayer.health < maxHealth && this.rotatedAngle % 2 > 1)
+ targetRGBI = [125,255,125,1.2];
+ else
+ targetRGBI = [255,255,255,0.5];
+
+
+ for(let i = 0; i < 4; i++)
+ this.lightRGBI[i] = this.lightRGBI[i] + (targetRGBI[i] - this.lightRGBI[i]) * 0.4 * deltaTime * 60;
+ this.ambientLight.copy(new THREE.AmbientLight(rgbToHex(this.lightRGBI[0], this.lightRGBI[1], this.lightRGBI[2]),this.lightRGBI[3]));
+
+ if(this.lastHealththis.localPlayer.health)
+ this.lastHealthChangeWasDamage = true;
+ this.lastHealth = this.localPlayer.health;
+ }
+
+
+
}
+
+
+
function rotateAroundWorldAxis(source: THREE.Quaternion, axis: THREE.Vector3, angle: number) {
- const rotationQuat = new THREE.Quaternion().setFromAxisAngle(axis, angle);
- source.multiplyQuaternions(rotationQuat, source);
+ const rotationQuat = new THREE.Quaternion().setFromAxisAngle(axis, angle);
+ source.multiplyQuaternions(rotationQuat, source);
}
function moveTowardsPos(source: THREE.Vector3, target: THREE.Vector3, frac: number) {
- source.lerp(target, frac);
+ source.lerp(target, frac);
}
function moveTowardsRot(source: THREE.Quaternion, target: THREE.Quaternion, frac: number) {
- source.slerp(target, frac);
+ source.slerp(target, frac);
}
-function rgbToHex(r: number, g: number, b: number) {
- return (r << 16) + (g << 8) + b;
+function rgbToHex(r:number, g:number, b:number) {
+ return (r << 16) + (g << 8) + b;
}
const basePosition = new THREE.Vector3(0, 0, 1.2);
-const baseQuaternion = new THREE.Quaternion(0, 0, 0, 1);
+const baseQuaternion = new THREE.Quaternion(0,0,0,1);
diff --git a/src/main.server.ts b/src/main.server.ts
index a73e662..6a60a09 100644
--- a/src/main.server.ts
+++ b/src/main.server.ts
@@ -6,27 +6,27 @@ import { renderApplication } from '@angular/platform-server';
import { provideServerContext } from '@analogjs/router/server';
import { ServerContext } from '@analogjs/router/tokens';
-import { AppComponent } from './app/app.component.ts';
-import { config } from './app/app.config.server.ts';
+import { AppComponent } from "./app/app.component.ts";
+import { config } from "./app/app.config.server.ts";
if (Deno.env.has('PRODUCTION')) {
- enableProdMode();
+ enableProdMode();
}
export function bootstrap() {
- return bootstrapApplication(AppComponent, config);
+ return bootstrapApplication(AppComponent, config);
}
export default async function render(
- url: string,
- document: string,
- serverContext: ServerContext,
+ url: string,
+ document: string,
+ serverContext: ServerContext
) {
- const html = await renderApplication(bootstrap, {
- document,
- url,
- platformProviders: [provideServerContext(serverContext)],
- });
+ const html = await renderApplication(bootstrap, {
+ document,
+ url,
+ platformProviders: [provideServerContext(serverContext)],
+ });
- return html;
+ return html;
}
diff --git a/src/main.ts b/src/main.ts
index 98f615b..af4e47a 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,7 +1,7 @@
import 'zone.js';
import { bootstrapApplication } from '@angular/platform-browser';
-import { AppComponent } from './app/app.component.ts';
-import { appConfig } from './app/app.config.ts';
+import { AppComponent } from "./app/app.component.ts";
+import { appConfig } from "./app/app.config.ts";
bootstrapApplication(AppComponent, appConfig);
diff --git a/src/server/DataValidator.ts b/src/server/DataValidator.ts
index 6219126..153bd05 100644
--- a/src/server/DataValidator.ts
+++ b/src/server/DataValidator.ts
@@ -1,78 +1,81 @@
-import { z } from 'https://deno.land/x/zod@v3.23.8/mod.ts';
-import { Player } from './models/Player.ts';
-import { ChatMessage } from './models/ChatMessage.ts';
-import { DamageRequest } from './models/DamageRequest.ts';
+import { z } from "https://deno.land/x/zod@v3.23.8/mod.ts";
+import { Player } from "./models/Player.ts";
+import { ChatMessage } from "./models/ChatMessage.ts";
+import { DamageRequest } from "./models/DamageRequest.ts";
+
+
export class DataValidator {
- private static SERVER_VERSION = '';
+ private static SERVER_VERSION = '';
+
+ public static async updateServerVersion() {
+ const versionFile = await Deno.readTextFile("public/gameVersion.json");
+ const versionData = JSON.parse(versionFile);
+ this.SERVER_VERSION = versionData.version;
+ return this.SERVER_VERSION;
+ }
- public static async updateServerVersion() {
- const versionFile = await Deno.readTextFile('public/gameVersion.json');
- const versionData = JSON.parse(versionFile);
- this.SERVER_VERSION = versionData.version;
- return this.SERVER_VERSION;
- }
+ public static getServerVersion() {
+ return this.SERVER_VERSION;
+ }
- public static getServerVersion() {
- return this.SERVER_VERSION;
- }
- private static vector3Schema = z.object({
- x: z.number(),
- y: z.number(),
- z: z.number(),
- }).strict();
+ private static vector3Schema = z.object({
+ x: z.number(),
+ y: z.number(),
+ z: z.number(),
+ }).strict();
- private static playerDataSchema = z.object({
- id: z.number(),
- speed: z.number(),
- acceleration: z.number(),
- name: z.string().max(42),
- gameVersion: z.string().refine((val) => val === this.SERVER_VERSION, {
- message: `Game version must be ${this.SERVER_VERSION}`,
- }),
- position: DataValidator.vector3Schema,
- velocity: DataValidator.vector3Schema,
- inputVelocity: DataValidator.vector3Schema,
- gravity: z.number(),
- lookQuaternion: z.array(z.number()).length(4),
- quaternion: z.array(z.number()).length(4),
- chatActive: z.boolean(),
- chatMsg: z.string().max(300),
- latency: z.number(),
- health: z.number(),
- forced: z.boolean(),
- forcedAcknowledged: z.boolean(),
- updateTimestamp: z.number().optional(),
- lastDamageTime: z.number().optional(),
- inventory: z.array(z.number()),
- idLastDamagedBy: z.number().optional(),
- playerSpectating: z.number(),
- gameMsgs: z.array(z.string()),
- gameMsgs2: z.array(z.string()),
- }).strict();
+ private static playerDataSchema = z.object({
+ id: z.number(),
+ speed: z.number(),
+ acceleration: z.number(),
+ name: z.string().max(42),
+ gameVersion: z.string().refine(val => val === this.SERVER_VERSION, {
+ message: `Game version must be ${this.SERVER_VERSION}`,
+ }),
+ position: DataValidator.vector3Schema,
+ velocity: DataValidator.vector3Schema,
+ inputVelocity: DataValidator.vector3Schema,
+ gravity: z.number(),
+ lookQuaternion: z.array(z.number()).length(4),
+ quaternion: z.array(z.number()).length(4),
+ chatActive: z.boolean(),
+ chatMsg: z.string().max(300),
+ latency: z.number(),
+ health: z.number(),
+ forced: z.boolean(),
+ forcedAcknowledged: z.boolean(),
+ updateTimestamp: z.number().optional(),
+ lastDamageTime: z.number().optional(),
+ inventory: z.array(z.number()),
+ idLastDamagedBy: z.number().optional(),
+ playerSpectating: z.number(),
+ gameMsgs: z.array(z.string()),
+ gameMsgs2: z.array(z.string()),
+ }).strict();
- private static chatMsgSchema = z.object({
- id: z.number(),
- name: z.string().max(42),
- message: z.string().max(300),
- }).strict();
+ private static chatMsgSchema = z.object({
+ id: z.number(),
+ name: z.string().max(42),
+ message: z.string().max(300),
+ }).strict();
- private static damageRequestSchema = z.object({
- localPlayer: DataValidator.playerDataSchema,
- targetPlayer: DataValidator.playerDataSchema,
- damage: z.number(),
- }).strict();
+ private static damageRequestSchema = z.object({
+ localPlayer: DataValidator.playerDataSchema,
+ targetPlayer: DataValidator.playerDataSchema,
+ damage: z.number(),
+ }).strict();
- static validatePlayerData(data: Player) {
- return DataValidator.playerDataSchema.safeParse(data);
- }
+ static validatePlayerData(data: Player) {
+ return DataValidator.playerDataSchema.safeParse(data);
+ }
- static validateChatMessage(data: ChatMessage) {
- return DataValidator.chatMsgSchema.safeParse(data);
- }
+ static validateChatMessage(data: ChatMessage) {
+ return DataValidator.chatMsgSchema.safeParse(data);
+ }
- static validateDamageRequest(data: DamageRequest) {
- return DataValidator.damageRequestSchema.safeParse(data);
- }
+ static validateDamageRequest(data: DamageRequest) {
+ return DataValidator.damageRequestSchema.safeParse(data);
+ }
}
diff --git a/src/server/GameEngine.ts b/src/server/GameEngine.ts
index c5cd63c..486e966 100644
--- a/src/server/GameEngine.ts
+++ b/src/server/GameEngine.ts
@@ -1,155 +1,162 @@
-import { ChatManager } from './managers/ChatManager.ts';
-import { DamageSystem } from './managers/DamageSystem.ts';
-import { ItemManager } from './managers/ItemManager.ts';
-import { PlayerManager } from './managers/PlayerManager.ts';
-import { Server } from 'https://deno.land/x/socket_io@0.2.0/mod.ts';
-import config from './config.ts';
-import { Vector3 } from './models/Vector3.ts';
-import { ServerInfo } from './models/ServerInfo.ts';
-import { DataValidator } from './DataValidator.ts';
-import { Player } from './models/Player.ts';
-import { Gamemode } from './gamemodes/Gamemode.ts';
-import { FFAGamemode } from './gamemodes/FFAGamemode.ts';
+import { ChatManager } from "./managers/ChatManager.ts";
+import { DamageSystem } from "./managers/DamageSystem.ts";
+import { ItemManager } from "./managers/ItemManager.ts";
+import { PlayerManager } from "./managers/PlayerManager.ts";
+import { Server } from "https://deno.land/x/socket_io@0.2.0/mod.ts";
+import config from "./config.ts";
+import { Vector3 } from "./models/Vector3.ts";
+import { ServerInfo } from "./models/ServerInfo.ts";
+import {DataValidator} from "./DataValidator.ts";
+import {Player} from "./models/Player.ts";
+import {Gamemode} from "./gamemodes/Gamemode.ts";
+import {FFAGamemode} from "./gamemodes/FFAGamemode.ts";
export class GameEngine {
- private lastPlayerTickTimestamp: number = Date.now() / 1000;
- private lastItemUpdateTimestamp: number = Date.now() / 1000;
- public playerUpdateSinceLastEmit: boolean = false;
- private itemUpdateSinceLastEmit: boolean = false;
- private serverInfo: ServerInfo = new ServerInfo();
- public gamemode: Gamemode | false = false;
-
- constructor(
- public playerManager: PlayerManager,
- private itemManager: ItemManager,
- public chatManager: ChatManager,
- private damageSystem: DamageSystem,
- private io: Server,
- ) {}
-
- start() {
- setInterval(() => this.serverTick(), 1000 / config.server.tickRate);
- setInterval(() => this.periodicCleanup(), config.server.cleanupInterval);
- setInterval(() => this.emitServerInfo(), config.server.cleanupInterval);
- this.initGamemode();
- }
-
- private serverTick() {
- try {
- const currentTime = Date.now() / 1000;
- this.playerManager.regenerateHealth();
- this.itemManager.tick(currentTime);
- if (this.gamemode) this.gamemode.tick();
-
- // Emit player data if there are updates or enough time has passed
- if (this.playerUpdateSinceLastEmit || currentTime - this.lastPlayerTickTimestamp > 1 / config.server.tickRate) {
- try {
- this.io.emit('remotePlayerData', this.playerManager.getAllPlayers());
- this.playerUpdateSinceLastEmit = false;
- this.lastPlayerTickTimestamp = currentTime;
- } catch (err) {
- console.error('⚠ error emitting player data:', err);
- }
- }
-
- // Emit item data if there are updates
- if (this.itemUpdateSinceLastEmit || this.itemManager.hasUpdates()) {
- try {
- this.io.emit('worldItemData', this.itemManager.getAllItems());
- this.itemUpdateSinceLastEmit = false;
- } catch (err) {
- console.error('⚠ error emitting item data:', err);
- }
- }
- } catch (error) {
- console.error('⚠ error in serverTick:', error);
- }
- }
-
- public periodicCleanup() {
- // for(const player of this.playerManager.getAllPlayers())
- // console.log(player.gameMsgs)
-
- try {
- const currentTime = Date.now() / 1000;
- const players = this.playerManager.getAllPlayers();
-
- players.forEach((player) => {
- if (player.position.y < -150) {
- player.health = 0;
- player.velocity = new Vector3(0, 0, 0);
- this.chatManager.broadcastChat(`${player.name} fell off :'(`);
- console.log(`💔 ${player.name}(${player.id}) fell off the map`);
- }
-
- if (player.health <= 0) {
- if (this.gamemode) this.gamemode.onPlayerDeath(player); //gamemode now handles
- else this.playerManager.respawnPlayer(player);
- }
-
- if ((player.updateTimestamp || 0) + config.player.disconnectTime < currentTime) {
- if (this.gamemode) this.gamemode.onPlayerDisconnect(player);
- console.log(`🟠 ${player.name}(${player.id}) left`);
- this.chatManager.broadcastChat(`${player.name} left`);
- this.playerManager.removePlayer(player.id);
- }
- });
-
- const playerData = this.playerManager.getAllPlayerData();
- playerData.forEach((playerData) => {
- for (let i = 0; i < playerData.extras.gameMsgsTimeouts.length; i++) {
- if (
- playerData.extras.gameMsgsTimeouts[i] &&
- currentTime > playerData.extras.gameMsgsTimeouts[i] && playerData.extras.gameMsgsTimeouts[i] !== -1
- ) {
- playerData.player.gameMsgs[i] = '';
- playerData.extras.gameMsgsTimeouts[i] = -1;
- this.playerUpdateSinceLastEmit = true;
- }
- }
- });
-
- const items = this.itemManager.getAllItems();
- items.forEach((item) => {
- if (item.vector.y < -5) {
- this.itemManager.removeItem(item.id);
- this.itemUpdateSinceLastEmit = true;
- }
- });
-
- if (this.gamemode) this.gamemode.onPeriodicCleanup();
- } catch (error) {
- console.error('⚠ error in periodicCleanup:', error);
- }
- }
-
- // Method to emit server info to all clients
- public emitServerInfo() {
- this.serverInfo.version = DataValidator.getServerVersion();
- this.serverInfo.currentPlayers = this.playerManager.getAllPlayers().length;
- this.io.emit('serverInfo', this.serverInfo);
- }
-
- public setGameMessage(player: Player, message: string, index: number, timeout?: number) {
- player.gameMsgs[index] = message;
- const extras = this.playerManager.getPlayerExtrasById(player.id);
- if (timeout && timeout > 0 && extras) {
- extras.gameMsgsTimeouts[index] = Date.now() / 1000 + timeout;
- }
- }
-
- private initGamemode() {
- try {
- switch (config.game.mode) {
- case 'ffa':
- this.gamemode = new FFAGamemode(this);
- break;
- default:
- console.log('⚠️ invalid gamemode supplied (check your config!)', config.game.mode);
- break;
- }
- } catch (error) {
- console.error('⚠ error initializing gamemode:', error);
- }
- }
+ private lastPlayerTickTimestamp: number = Date.now() / 1000;
+ private lastItemUpdateTimestamp: number = Date.now() / 1000;
+ public playerUpdateSinceLastEmit: boolean = false;
+ private itemUpdateSinceLastEmit: boolean = false;
+ private serverInfo: ServerInfo = new ServerInfo();
+ public gamemode: Gamemode | false = false;
+
+ constructor(
+ public playerManager: PlayerManager,
+ private itemManager: ItemManager,
+ public chatManager: ChatManager,
+ private damageSystem: DamageSystem,
+ private io: Server
+ ) {}
+
+ start() {
+ setInterval(() => this.serverTick(), 1000 / config.server.tickRate);
+ setInterval(() => this.periodicCleanup(), config.server.cleanupInterval);
+ setInterval(() => this.emitServerInfo(), config.server.cleanupInterval);
+ this.initGamemode();
+ }
+
+ private serverTick() {
+ try {
+ const currentTime = Date.now() / 1000;
+ this.playerManager.regenerateHealth();
+ this.itemManager.tick(currentTime);
+ if(this.gamemode) this.gamemode.tick();
+
+ // Emit player data if there are updates or enough time has passed
+ if (this.playerUpdateSinceLastEmit || currentTime - this.lastPlayerTickTimestamp > 1 / config.server.tickRate) {
+ try {
+ this.io.emit('remotePlayerData', this.playerManager.getAllPlayers());
+ this.playerUpdateSinceLastEmit = false;
+ this.lastPlayerTickTimestamp = currentTime;
+ } catch (err) {
+ console.error('⚠ error emitting player data:', err);
+ }
+ }
+
+ // Emit item data if there are updates
+ if (this.itemUpdateSinceLastEmit || this.itemManager.hasUpdates()) {
+ try {
+ this.io.emit('worldItemData', this.itemManager.getAllItems());
+ this.itemUpdateSinceLastEmit = false;
+ } catch (err) {
+ console.error('⚠ error emitting item data:', err);
+ }
+ }
+ } catch (error) {
+ console.error('⚠ error in serverTick:', error);
+ }
+ }
+
+ public periodicCleanup() {
+ // for(const player of this.playerManager.getAllPlayers())
+ // console.log(player.gameMsgs)
+
+ try {
+ const currentTime = Date.now() / 1000;
+ const players = this.playerManager.getAllPlayers();
+
+ players.forEach(player => {
+ if (player.position.y < -150) {
+ player.health = 0;
+ player.velocity = new Vector3(0, 0, 0);
+ this.chatManager.broadcastChat(`${player.name} fell off :'(`);
+ console.log(`💔 ${player.name}(${player.id}) fell off the map`);
+ }
+
+ if (player.health <= 0) {
+ if(this.gamemode) this.gamemode.onPlayerDeath(player); //gamemode now handles
+ else this.playerManager.respawnPlayer(player);
+ }
+
+ if ((player.updateTimestamp || 0) + config.player.disconnectTime < currentTime) {
+ if(this.gamemode) this.gamemode.onPlayerDisconnect(player);
+ console.log(`🟠 ${player.name}(${player.id}) left`);
+ this.chatManager.broadcastChat(`${player.name} left`);
+ this.playerManager.removePlayer(player.id);
+ }
+ });
+
+ const playerData = this.playerManager.getAllPlayerData();
+ playerData.forEach(playerData => {
+ for(let i = 0; i playerData.extras.gameMsgsTimeouts[i] && playerData.extras.gameMsgsTimeouts[i] !== -1){
+
+ playerData.player.gameMsgs[i] = '';
+ playerData.extras.gameMsgsTimeouts[i] = -1;
+ this.playerUpdateSinceLastEmit = true;
+
+ }
+ }
+ });
+
+
+
+ const items = this.itemManager.getAllItems();
+ items.forEach(item => {
+ if (item.vector.y < -5) {
+ this.itemManager.removeItem(item.id);
+ this.itemUpdateSinceLastEmit = true;
+ }
+ });
+
+ if(this.gamemode) this.gamemode.onPeriodicCleanup();
+
+ } catch (error) {
+ console.error('⚠ error in periodicCleanup:', error);
+ }
+ }
+
+ // Method to emit server info to all clients
+ public emitServerInfo() {
+ this.serverInfo.version = DataValidator.getServerVersion();
+ this.serverInfo.currentPlayers = this.playerManager.getAllPlayers().length;
+ this.io.emit('serverInfo', this.serverInfo);
+ }
+
+
+
+ public setGameMessage(player:Player, message:string, index:number, timeout?:number){
+ player.gameMsgs[index] = message;
+ const extras = this.playerManager.getPlayerExtrasById(player.id);
+ if(timeout && timeout > 0 && extras){
+ extras.gameMsgsTimeouts[index] = Date.now()/1000 + timeout;
+ }
+ }
+
+
+
+ private initGamemode(){
+ try {
+ switch(config.game.mode){
+ case 'ffa':
+ this.gamemode = new FFAGamemode(this);
+ break;
+ default:
+ console.log('⚠️ invalid gamemode supplied (check your config!)', config.game.mode);
+ break;
+ }
+ } catch (error) {
+ console.error('⚠ error initializing gamemode:', error);
+ }
+ }
}
diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts
index 674639e..f597bc4 100644
--- a/src/server/GameServer.ts
+++ b/src/server/GameServer.ts
@@ -1,7 +1,7 @@
-import { Application, Router, send } from '@oak/oak';
-import { Server, Socket } from 'https://deno.land/x/socket_io@0.2.0/mod.ts';
+import { Application, Router, send } from "@oak/oak";
+import { Server, Socket } from "https://deno.land/x/socket_io@0.2.0/mod.ts";
import config from './config.ts';
-import { serve } from 'https://deno.land/std@0.150.0/http/server.ts';
+import { serve } from "https://deno.land/std@0.150.0/http/server.ts";
import { GameEngine } from './GameEngine.ts';
import { PlayerManager } from './managers/PlayerManager.ts';
@@ -9,153 +9,154 @@ import { ItemManager } from './managers/ItemManager.ts';
import { ChatManager } from './managers/ChatManager.ts';
import { DamageSystem } from './managers/DamageSystem.ts';
import { MapData } from './models/MapData.ts';
-import { DataValidator } from './DataValidator.ts';
+import { DataValidator } from "./DataValidator.ts";
export class GameServer {
- router: Router = new Router();
- app: Application = new Application();
- io: Server = new Server();
-
- gameEngine: GameEngine;
- playerManager: PlayerManager;
- itemManager: ItemManager;
- chatManager: ChatManager;
- damageSystem: DamageSystem;
- mapData: MapData;
-
- constructor() {
- this.mapData = this.loadMapData();
- this.playerManager = new PlayerManager(this.mapData);
- this.chatManager = new ChatManager(this.io, this.playerManager);
- this.itemManager = new ItemManager(this.mapData, this.playerManager, this.chatManager);
- this.damageSystem = new DamageSystem(this.playerManager, this.chatManager);
-
- this.playerManager.setItemManager(this.itemManager);
-
- this.setupSocketIO();
- this.setupRoutes();
-
- this.gameEngine = new GameEngine(
- this.playerManager,
- this.itemManager,
- this.chatManager,
- this.damageSystem,
- this.io,
- );
- this.itemManager.setGamemode(this.gameEngine.gamemode);
- this.damageSystem.setGameEngine(this.gameEngine);
- this.gameEngine.start();
-
- DataValidator.updateServerVersion();
- this.start();
- }
-
- private setupSocketIO() {
- this.io.on('connection', (socket: Socket) => {
- if (socket.connected) {
- socket.on('error', (error) => {
- console.error(`Socket error for ${socket.id}:`, error);
- });
-
- // deno-lint-ignore require-await
- socket.on('playerData', async (data) => {
- try {
- const result = this.playerManager.addOrUpdatePlayer(data);
- if (result.isNew && result.player) {
- if (this.gameEngine.gamemode) this.gameEngine.gamemode.onPlayerConnect(result.player);
- this.chatManager.broadcastChat(`${result.player.name} joined`);
- console.log(`🟢 ${result.player.name}(${result.player.id}) joined`);
- this.gameEngine.emitServerInfo();
- }
- } catch (err) {
- console.error(`Error handling playerData:`, err);
- }
- });
-
- // deno-lint-ignore require-await
- socket.on('chatMsg', async (data) => {
- try {
- this.chatManager.handleChatMessage(data, socket);
- } catch (err) {
- console.error(`Error handling chat message:`, err);
- }
- });
-
- socket.on('applyDamage', (data) => {
- try {
- this.damageSystem.handleDamageRequest(data);
- } catch (err) {
- console.error(`Error handling damage request:`, err);
- }
- });
-
- socket.on('latencyTest', () => {
- try {
- socket.emit('latencyTest', 'response :)');
- } catch (err) {
- console.error(`Error handling latency test:`, err);
- }
- });
-
- socket.on('disconnect', () => {
- //console.log(`Socket disconnected: ${socket.id}, reason: ${reason}`); //reason is passed
- });
- }
- });
- }
-
- private setupRoutes() {
- this.router.get('/(.*)', async (context) => {
- try {
- await send(context, context.params[0], {
- root: `${Deno.cwd()}/dist`,
- index: 'index.html',
- });
- } catch {
- try {
- await send(context, 'index.html', {
- root: `${Deno.cwd()}/dist`,
- });
- } catch (err) {
- console.error('Error serving files:', err);
- context.response.status = 500;
- context.response.body = 'Internal Server Error';
- }
- }
- });
-
- this.app.use(this.router.routes());
- this.app.use(this.router.allowedMethods());
- }
-
- private async start() {
- try {
- const handler = this.io.handler(async (req: Request) => {
- try {
- return await this.app.handle(req) || new Response(null, { status: 404 });
- } catch (error) {
- console.error('Request handler error:', error);
- return new Response('Internal Server Error', { status: 500 });
- }
- });
-
- await serve(handler, {
- port: config.server.port,
- });
- } catch (error) {
- console.error('Failed to start server:', error);
- Deno.exit(1);
- }
- }
-
- private loadMapData(): MapData {
- try {
- const mapJson = Deno.readTextFileSync(`./dist/maps/${config.server.defaultMap}/map.json`);
- const mapObj = JSON.parse(mapJson);
- return MapData.fromJSON(mapObj);
- } catch (error) {
- console.error('Failed to load map data:', error);
- return new MapData('default_map', [], []);
- }
- }
+ router: Router = new Router();
+ app: Application = new Application();
+ io: Server = new Server();
+
+ gameEngine: GameEngine;
+ playerManager: PlayerManager;
+ itemManager: ItemManager;
+ chatManager: ChatManager;
+ damageSystem: DamageSystem;
+ mapData: MapData;
+
+ constructor() {
+ this.mapData = this.loadMapData();
+ this.playerManager = new PlayerManager(this.mapData);
+ this.chatManager = new ChatManager(this.io, this.playerManager);
+ this.itemManager = new ItemManager(this.mapData, this.playerManager, this.chatManager);
+ this.damageSystem = new DamageSystem(this.playerManager, this.chatManager);
+
+ this.playerManager.setItemManager(this.itemManager);
+
+ this.setupSocketIO();
+ this.setupRoutes();
+
+ this.gameEngine = new GameEngine(
+ this.playerManager,
+ this.itemManager,
+ this.chatManager,
+ this.damageSystem,
+ this.io
+ );
+ this.itemManager.setGamemode(this.gameEngine.gamemode);
+ this.damageSystem.setGameEngine(this.gameEngine);
+ this.gameEngine.start();
+
+ DataValidator.updateServerVersion();
+ this.start();
+ }
+
+ private setupSocketIO() {
+ this.io.on("connection", (socket: Socket) => {
+ if (socket.connected) {
+
+ socket.on("error", (error) => {
+ console.error(`Socket error for ${socket.id}:`, error);
+ });
+
+ // deno-lint-ignore require-await
+ socket.on("playerData", async (data) => {
+ try {
+ const result = this.playerManager.addOrUpdatePlayer(data);
+ if (result.isNew && result.player) {
+ if(this.gameEngine.gamemode) this.gameEngine.gamemode.onPlayerConnect(result.player);
+ this.chatManager.broadcastChat(`${result.player.name} joined`);
+ console.log(`🟢 ${result.player.name}(${result.player.id}) joined`);
+ this.gameEngine.emitServerInfo();
+ }
+ } catch (err) {
+ console.error(`Error handling playerData:`, err);
+ }
+ });
+
+ // deno-lint-ignore require-await
+ socket.on("chatMsg", async (data) => {
+ try {
+ this.chatManager.handleChatMessage(data, socket);
+ } catch (err) {
+ console.error(`Error handling chat message:`, err);
+ }
+ });
+
+ socket.on("applyDamage", (data) => {
+ try {
+ this.damageSystem.handleDamageRequest(data);
+ } catch (err) {
+ console.error(`Error handling damage request:`, err);
+ }
+ });
+
+ socket.on('latencyTest', () => {
+ try {
+ socket.emit('latencyTest', 'response :)');
+ } catch (err) {
+ console.error(`Error handling latency test:`, err);
+ }
+ });
+
+ socket.on("disconnect", () => {
+ //console.log(`Socket disconnected: ${socket.id}, reason: ${reason}`); //reason is passed
+ });
+ }
+ });
+ }
+
+ private setupRoutes() {
+ this.router.get("/(.*)", async (context) => {
+ try {
+ await send(context, context.params[0], {
+ root: `${Deno.cwd()}/dist`,
+ index: "index.html",
+ });
+ } catch {
+ try {
+ await send(context, "index.html", {
+ root: `${Deno.cwd()}/dist`,
+ });
+ } catch (err) {
+ console.error('Error serving files:', err);
+ context.response.status = 500;
+ context.response.body = "Internal Server Error";
+ }
+ }
+ });
+
+ this.app.use(this.router.routes());
+ this.app.use(this.router.allowedMethods());
+ }
+
+ private async start() {
+ try {
+ const handler = this.io.handler(async (req: Request) => {
+ try {
+ return await this.app.handle(req) || new Response(null, { status: 404 });
+ } catch (error) {
+ console.error('Request handler error:', error);
+ return new Response('Internal Server Error', { status: 500 });
+ }
+ });
+
+ await serve(handler, {
+ port: config.server.port
+ });
+ } catch (error) {
+ console.error('Failed to start server:', error);
+ Deno.exit(1);
+ }
+ }
+
+ private loadMapData(): MapData {
+ try {
+ const mapJson = Deno.readTextFileSync(`./dist/maps/${config.server.defaultMap}/map.json`);
+ const mapObj = JSON.parse(mapJson);
+ return MapData.fromJSON(mapObj);
+ } catch (error) {
+ console.error("Failed to load map data:", error);
+ return new MapData('default_map', [], []);
+ }
+ }
}
diff --git a/src/server/config.ts b/src/server/config.ts
index f040b7c..98a9d72 100644
--- a/src/server/config.ts
+++ b/src/server/config.ts
@@ -1,90 +1,91 @@
const defaults = {
- // Server settings
- SERVER_PORT: '3000',
- SERVER_NAME: 'my-server',
- SERVER_URL: 'https://example.com',
- SERVER_DEFAULT_MAP: 'crackhouse_1',
- SERVER_TICK_RATE: '15',
- SERVER_CLEANUP_INTERVAL: '2000',
+ // Server settings
+ SERVER_PORT: '3000',
+ SERVER_NAME: 'my-server',
+ SERVER_URL: 'https://example.com',
+ SERVER_DEFAULT_MAP: 'crackhouse_1',
+ SERVER_TICK_RATE: '15',
+ SERVER_CLEANUP_INTERVAL: '2000',
- // Player settings
- PLAYER_DISCONNECT_TIME: '8',
- PLAYER_AFK_KICK_TIME: '600',
- PLAYER_MAX_HEALTH: '100',
- PLAYER_BASE_INVENTORY: '[]',
+ // Player settings
+ PLAYER_DISCONNECT_TIME: '8',
+ PLAYER_AFK_KICK_TIME: '600',
+ PLAYER_MAX_HEALTH: '100',
+ PLAYER_BASE_INVENTORY: '[]',
- //Game settings
- GAME_MODE: 'ffa',
- GAME_MAX_PLAYERS: '20',
+ //Game settings
+ GAME_MODE: 'ffa',
+ GAME_MAX_PLAYERS: '20',
- // Health settings
- HEALTH_REGEN_DELAY: '5',
- HEALTH_REGEN_RATE: '3',
+ // Health settings
+ HEALTH_REGEN_DELAY: '5',
+ HEALTH_REGEN_RATE: '3',
- //Item settings
- MAX_ITEMS_IN_WORLD: '7',
- ITEM_RESPAWN_TIME: '10',
+ //Item settings
+ MAX_ITEMS_IN_WORLD: '7',
+ ITEM_RESPAWN_TIME: '10',
};
async function updateEnvFile(defaults: Record) {
- const envPath = '.env';
- const envExists = await Deno.stat(envPath).catch(() => false);
- let currentEnv: Record = {};
+ const envPath = '.env';
+ const envExists = await Deno.stat(envPath).catch(() => false);
+ let currentEnv: Record = {};
- if (envExists) {
- const content = await Deno.readTextFile(envPath);
- currentEnv = content.split('\n')
- .filter((line) => line && !line.startsWith('#'))
- .reduce((acc, line) => {
- const [key, value] = line.split('=').map((s) => s.trim());
- acc[key] = value.replace(/["']/g, '');
- return acc;
- }, {} as Record);
- }
+ if (envExists) {
+ const content = await Deno.readTextFile(envPath);
+ currentEnv = content.split('\n')
+ .filter(line => line && !line.startsWith('#'))
+ .reduce((acc, line) => {
+ const [key, value] = line.split('=').map(s => s.trim());
+ acc[key] = value.replace(/["']/g, '');
+ return acc;
+ }, {} as Record);
+ }
- const finalEnv = {
- ...defaults,
- ...currentEnv,
- };
+ const finalEnv = {
+ ...defaults,
+ ...currentEnv
+ };
- const envContent = Object.entries(finalEnv)
- .map(([key, value]) => `${key}=${value}`)
- .join('\n');
- await Deno.writeTextFile(envPath, envContent);
- return finalEnv;
+ const envContent = Object.entries(finalEnv)
+ .map(([key, value]) => `${key}=${value}`)
+ .join('\n');
+
+ await Deno.writeTextFile(envPath, envContent);
+ return finalEnv;
}
// Parse specific types from string values
function parseConfig(env: Record) {
- return {
- server: {
- port: parseInt(env.SERVER_PORT),
- name: env.SERVER_NAME,
- url: env.SERVER_URL,
- defaultMap: env.SERVER_DEFAULT_MAP,
- tickRate: parseInt(env.SERVER_TICK_RATE),
- cleanupInterval: parseInt(env.SERVER_CLEANUP_INTERVAL),
- },
- game: {
- mode: env.GAME_MODE,
- maxPlayers: parseInt(env.GAME_MAX_PLAYERS),
- },
- player: {
- disconnectTime: parseInt(env.PLAYER_DISCONNECT_TIME),
- afkKickTime: parseInt(env.PLAYER_AFK_KICK_TIME),
- maxHealth: parseInt(env.PLAYER_MAX_HEALTH),
- baseInventory: JSON.parse(env.PLAYER_BASE_INVENTORY) as number[],
- },
- health: {
- regenDelay: parseInt(env.HEALTH_REGEN_DELAY),
- regenRate: parseInt(env.HEALTH_REGEN_RATE),
- },
- items: {
- maxItemsInWorld: parseInt(env.MAX_ITEMS_IN_WORLD),
- respawnTime: parseInt(env.ITEM_RESPAWN_TIME),
- },
- };
+ return {
+ server: {
+ port: parseInt(env.SERVER_PORT),
+ name: env.SERVER_NAME,
+ url: env.SERVER_URL,
+ defaultMap: env.SERVER_DEFAULT_MAP,
+ tickRate: parseInt(env.SERVER_TICK_RATE),
+ cleanupInterval: parseInt(env.SERVER_CLEANUP_INTERVAL)
+ },
+ game: {
+ mode: env.GAME_MODE,
+ maxPlayers: parseInt(env.GAME_MAX_PLAYERS)
+ },
+ player: {
+ disconnectTime: parseInt(env.PLAYER_DISCONNECT_TIME),
+ afkKickTime: parseInt(env.PLAYER_AFK_KICK_TIME),
+ maxHealth: parseInt(env.PLAYER_MAX_HEALTH),
+ baseInventory: JSON.parse(env.PLAYER_BASE_INVENTORY) as number[]
+ },
+ health: {
+ regenDelay: parseInt(env.HEALTH_REGEN_DELAY),
+ regenRate: parseInt(env.HEALTH_REGEN_RATE)
+ },
+ items: {
+ maxItemsInWorld: parseInt(env.MAX_ITEMS_IN_WORLD),
+ respawnTime: parseInt(env.ITEM_RESPAWN_TIME)
+ }
+ };
}
const rawConfig = await updateEnvFile(defaults);
diff --git a/src/server/gamemodes/FFAGamemode.ts b/src/server/gamemodes/FFAGamemode.ts
index aff8ff6..70f6118 100644
--- a/src/server/gamemodes/FFAGamemode.ts
+++ b/src/server/gamemodes/FFAGamemode.ts
@@ -1,120 +1,124 @@
-import { GameEngine } from '../GameEngine.ts';
-import { Player } from '../models/Player.ts';
-import { Gamemode } from './Gamemode.ts';
-import config from '../config.ts';
+import {GameEngine} from "../GameEngine.ts";
+import {Player} from "../models/Player.ts";
+import {Gamemode} from "./Gamemode.ts";
+import config from "../config.ts";
+
export class FFAGamemode extends Gamemode {
- private spectateTimeouts: Map = new Map();
- constructor(gameEngine: GameEngine) {
- super(gameEngine);
- this.init();
- }
-
- init(): void {
- console.log('🐙 FFA Gamemode initialized');
- }
-
- tick(): void {
- const currentTime = Date.now() / 1000;
- for (const [player, timestamp] of this.spectateTimeouts) {
- if (currentTime - timestamp > 10) {
- this.gameEngine.playerManager.respawnPlayer(player);
- player.playerSpectating = -1;
- this.spectateTimeouts.delete(player);
- this.gameEngine.setGameMessage(player, '', 0);
- } else {
- this.gameEngine.setGameMessage(
- player,
- '&crespawn in ' + Math.floor(10 + timestamp - currentTime) + ' seconds',
- 1,
- 0.5,
- );
- }
- player.health = config.player.maxHealth;
- }
- }
-
- onPeriodicCleanup(): void {
- // send kill death stats to all players
- for (const player of this.gameEngine.playerManager.getAllPlayers()) {
- const extras = this.gameEngine.playerManager.getPlayerExtrasById(player.id);
- if (extras) player.gameMsgs2 = ['&7' + extras.kills + ' kills, ' + extras.deaths + ' deaths'];
- }
- }
-
- onPlayerConnect(_player: Player): void {
- }
-
- onPlayerDisconnect(_player: Player): void {
- }
-
- onPlayerDeath(player: Player): void {
- const extras = this.gameEngine.playerManager.getPlayerExtrasById(player.id);
- if (extras) {
- extras.deaths++;
- extras.killStreak = 0;
- }
-
- if (
- player.lastDamageTime && player.idLastDamagedBy &&
- Date.now() / 1000 - player.lastDamageTime < 5
- ) {
- const killer = this.gameEngine.playerManager.getPlayerById(player.idLastDamagedBy);
- if (killer) {
- // Redirect spectators of the dead player to the killer
- for (const otherPlayer of this.gameEngine.playerManager.getAllPlayers()) {
- if (otherPlayer.playerSpectating === player.id) {
- otherPlayer.playerSpectating = killer.id;
- this.gameEngine.setGameMessage(otherPlayer, '&cspectating ' + killer.name, 0, 10);
- }
- }
-
- // Set the dead player to spectate the killer
- player.playerSpectating = player.idLastDamagedBy;
- player.health = config.player.maxHealth;
- this.gameEngine.playerManager.dropAllItems(player);
-
- this.gameEngine.setGameMessage(player, '&cspectating ' + killer.name, 0, 10);
- this.gameEngine.setGameMessage(player, '&crespawn in 10 seconds', 1, 2);
- this.gameEngine.setGameMessage(killer, '&akilled ' + player.name, 0, 5);
-
- // Add the dead player to the spectate timeout list
- this.spectateTimeouts.set(player, Date.now() / 1000);
- this.gameEngine.playerUpdateSinceLastEmit = true;
-
- this.onPlayerKill(killer);
- } else {
- // Respawn the player if no killer is found
- this.gameEngine.playerManager.respawnPlayer(player);
- }
- } else {
- // Respawn the player if no valid killer is found
- this.gameEngine.playerManager.respawnPlayer(player);
- }
- }
-
- onPlayerKill(player: Player) {
- const extras = this.gameEngine.playerManager.getPlayerExtrasById(player.id);
- if (extras) {
- extras.kills++;
- extras.killStreak++;
-
- let colorCode = '&a';
- if (extras.killStreak >= 5) colorCode = '&b';
- if (extras.killStreak >= 10) colorCode = '&6';
- if (extras.killStreak >= 15) colorCode = '&g';
-
- if (extras.killStreak >= 3) {
- this.gameEngine.setGameMessage(player, colorCode + extras.killStreak + ' kill streak', 1, 5);
- }
- if (extras.killStreak >= 5) {
- this.gameEngine.chatManager.broadcastChat(
- colorCode + player.name + ' is on a ' + extras.killStreak + ' kill streak',
- );
- }
- }
- }
-
- onItemPickup(_player: Player): void {
- }
+
+ private spectateTimeouts: Map = new Map();
+ constructor(gameEngine: GameEngine) {
+ super(gameEngine);
+ this.init();
+ }
+
+ init(): void {
+ console.log('🐙 FFA Gamemode initialized');
+ }
+
+ tick(): void {
+ const currentTime = Date.now() / 1000;
+ for (const [player, timestamp] of this.spectateTimeouts) {
+ if (currentTime - timestamp > 10) {
+ this.gameEngine.playerManager.respawnPlayer(player);
+ player.playerSpectating = -1;
+ this.spectateTimeouts.delete(player);
+ this.gameEngine.setGameMessage(player, '', 0);
+ }else{
+ this.gameEngine.setGameMessage(player, '&crespawn in ' + Math.floor(10 + timestamp - currentTime)+ ' seconds', 1, 0.5);
+ }
+ player.health = config.player.maxHealth;
+
+
+ }
+ }
+
+ onPeriodicCleanup(): void {
+ // send kill death stats to all players
+ for (const player of this.gameEngine.playerManager.getAllPlayers()) {
+ const extras = this.gameEngine.playerManager.getPlayerExtrasById(player.id);
+ if(extras) player.gameMsgs2 = ['&7'+extras.kills + ' kills, ' + extras.deaths + ' deaths'];
+ }
+
+
+ }
+
+ onPlayerConnect(_player: Player): void {
+ }
+
+ onPlayerDisconnect(_player: Player): void {
+ }
+
+ onPlayerDeath(player: Player): void {
+ const extras = this.gameEngine.playerManager.getPlayerExtrasById(player.id);
+ if(extras) {extras.deaths++; extras.killStreak = 0;}
+
+ if (player.lastDamageTime && player.idLastDamagedBy &&
+ Date.now() / 1000 - player.lastDamageTime < 5) {
+ const killer = this.gameEngine.playerManager.getPlayerById(player.idLastDamagedBy);
+ if (killer) {
+ // Redirect spectators of the dead player to the killer
+ for (const otherPlayer of this.gameEngine.playerManager.getAllPlayers()) {
+ if (otherPlayer.playerSpectating === player.id) {
+ otherPlayer.playerSpectating = killer.id;
+ this.gameEngine.setGameMessage(otherPlayer, '&cspectating ' + killer.name, 0, 10);
+ }
+ }
+
+
+ // Set the dead player to spectate the killer
+ player.playerSpectating = player.idLastDamagedBy;
+ player.health = config.player.maxHealth;
+ this.gameEngine.playerManager.dropAllItems(player);
+
+ this.gameEngine.setGameMessage(player, '&cspectating ' + killer.name, 0, 10);
+ this.gameEngine.setGameMessage(player, '&crespawn in 10 seconds', 1, 2);
+ this.gameEngine.setGameMessage(killer, '&akilled ' + player.name, 0, 5);
+
+ // Add the dead player to the spectate timeout list
+ this.spectateTimeouts.set(player, Date.now() / 1000);
+ this.gameEngine.playerUpdateSinceLastEmit = true;
+
+ this.onPlayerKill(killer);
+
+
+ } else {
+ // Respawn the player if no killer is found
+ this.gameEngine.playerManager.respawnPlayer(player);
+ }
+ } else {
+ // Respawn the player if no valid killer is found
+ this.gameEngine.playerManager.respawnPlayer(player);
+ }
+
+
+ }
+
+ onPlayerKill(player:Player){
+ const extras = this.gameEngine.playerManager.getPlayerExtrasById(player.id);
+ if(extras) {
+ extras.kills++;
+ extras.killStreak++;
+
+ let colorCode = '&a';
+ if(extras.killStreak >= 5) colorCode = '&b';
+ if(extras.killStreak >= 10) colorCode = '&6';
+ if(extras.killStreak >= 15) colorCode = '&g';
+
+ if(extras.killStreak >= 3)
+ this.gameEngine.setGameMessage(player, colorCode+extras.killStreak+' kill streak', 1, 5);
+ if(extras.killStreak >= 5)
+ this.gameEngine.chatManager.broadcastChat(colorCode+player.name+' is on a '+extras.killStreak+' kill streak');
+
+
+ }
+ }
+
+
+
+
+ onItemPickup(_player: Player): void {
+ }
+
+
}
diff --git a/src/server/gamemodes/Gamemode.ts b/src/server/gamemodes/Gamemode.ts
index 1032652..9df17da 100644
--- a/src/server/gamemodes/Gamemode.ts
+++ b/src/server/gamemodes/Gamemode.ts
@@ -1,20 +1,24 @@
-import { GameEngine } from '../GameEngine.ts';
-import { Player } from '../models/Player.ts';
+import {GameEngine} from "../GameEngine.ts";
+import {Player} from "../models/Player.ts";
export abstract class Gamemode {
- constructor(protected gameEngine: GameEngine) {}
- abstract init(): void;
+ constructor(protected gameEngine: GameEngine) {}
- abstract tick(): void;
+ abstract init(): void;
- abstract onPeriodicCleanup(): void;
+ abstract tick(): void;
- abstract onPlayerConnect(player: Player): void;
+ abstract onPeriodicCleanup(): void;
- abstract onPlayerDisconnect(player: Player): void;
+ abstract onPlayerConnect(player:Player): void;
- abstract onPlayerDeath(player: Player): void;
+ abstract onPlayerDisconnect(player:Player): void;
- abstract onItemPickup(player: Player): void;
-}
+ abstract onPlayerDeath(player:Player): void;
+
+ abstract onItemPickup(player:Player): void;
+
+
+
+}
\ No newline at end of file
diff --git a/src/server/managers/ChatManager.ts b/src/server/managers/ChatManager.ts
index 825a80e..329cd14 100644
--- a/src/server/managers/ChatManager.ts
+++ b/src/server/managers/ChatManager.ts
@@ -1,87 +1,86 @@
-import { Server, Socket } from 'https://deno.land/x/socket_io@0.2.0/mod.ts';
-import { DataValidator } from '../DataValidator.ts';
+import { Server, Socket } from "https://deno.land/x/socket_io@0.2.0/mod.ts";
+import { DataValidator } from "../DataValidator.ts";
import { ChatMessage } from '../models/ChatMessage.ts';
-import { PlayerManager } from './PlayerManager.ts';
+import {PlayerManager} from "./PlayerManager.ts";
+
export class ChatManager {
- constructor(private io: Server, private playerManager: PlayerManager) {}
+ constructor(private io: Server, private playerManager:PlayerManager) {}
- handleChatMessage(data: ChatMessage, socket: Socket) {
- const { error } = DataValidator.validateChatMessage(data);
- if (error) {
- console.warn(`Invalid chat message: ${error.message}`);
- return;
- }
+ handleChatMessage(data: ChatMessage, socket: Socket) {
+ const { error } = DataValidator.validateChatMessage(data);
+ if (error) {
+ console.warn(`Invalid chat message: ${error.message}`);
+ return;
+ }
- const isCommand = this.parseCommand(data.message, socket, data.id);
- if (!isCommand) {
- if (data.message.startsWith('>')) data.message = '&2' + data.message;
- console.log(`💬 ${data.name}: ${data.message}`);
- this.io.emit('chatMsg', data);
- }
- }
+ const isCommand = this.parseCommand(data.message, socket, data.id);
+ if (!isCommand) {
+ if(data.message.startsWith('>')) data.message = '&2'+data.message;
+ console.log(`💬 ${data.name}: ${data.message}`);
+ this.io.emit('chatMsg', data);
+ }
+ }
- private parseCommand(message: string, socket: Socket, playerId: number): boolean {
- if (message.charAt(0) !== '/') return false;
+ private parseCommand(message: string, socket: Socket, playerId: number): boolean {
+ if (message.charAt(0) !== '/') return false;
- const args = message.slice(1).split(' ');
- const command = args.shift()?.toLowerCase();
+ const args = message.slice(1).split(' ');
+ const command = args.shift()?.toLowerCase();
- switch (command) {
- case 'help':
- this.whisperChatMessage(message + ` -> nah i'm good`, socket);
- break;
- case 'kill': {
- const player = this.playerManager.getPlayerById(playerId);
- if (player) {
- this.playerManager.respawnPlayer(player);
- }
- this.broadcastChat(`${this.playerManager.getPlayerById(playerId)?.name} killed himself`);
- break;
- }
- case 'thumbsup':
- this.broadcastChat(`${this.playerManager.getPlayerById(playerId)?.name}: 👍`);
- break;
- case 'thumbsdown':
- this.broadcastChat(`${this.playerManager.getPlayerById(playerId)?.name}: 👎`);
- break;
- case 'octopus':
- this.broadcastChat(`${this.playerManager.getPlayerById(playerId)?.name}: 🐙`);
- break;
- case 'ping':
- this.whisperChatMessage(message + ' -> pong!', socket);
- break;
- case 'version':
- this.whisperChatMessage(message + ` -> candiru ${DataValidator.getServerVersion()}`, socket);
- break;
- case 'clear':
- for (let i = 0; i < 25; i++) {
- this.whisperChatMessage(' ', socket);
- }
- this.whisperChatMessage(message + ' -> cleared chat', socket);
- break;
- default:
- this.whisperChatMessage(message + ' -> unknown command', socket);
- }
+ switch (command) {
+ case 'help':
+ this.whisperChatMessage(message + ` -> nah i'm good`, socket);
+ break;
+ case 'kill':{
+ const player = this.playerManager.getPlayerById(playerId);
+ if(player)
+ this.playerManager.respawnPlayer(player);
+ this.broadcastChat(`${this.playerManager.getPlayerById(playerId)?.name} killed himself`);
+ break;
+ }
+ case 'thumbsup':
+ this.broadcastChat(`${this.playerManager.getPlayerById(playerId)?.name}: 👍`);
+ break;
+ case 'thumbsdown':
+ this.broadcastChat(`${this.playerManager.getPlayerById(playerId)?.name}: 👎`);
+ break;
+ case 'octopus':
+ this.broadcastChat(`${this.playerManager.getPlayerById(playerId)?.name}: 🐙`);
+ break;
+ case 'ping':
+ this.whisperChatMessage(message + ' -> pong!', socket);
+ break;
+ case 'version':
+ this.whisperChatMessage(message + ` -> candiru ${DataValidator.getServerVersion()}`, socket);
+ break;
+ case 'clear':
+ for(let i = 0; i < 25; i++)
+ this.whisperChatMessage(' ', socket);
+ this.whisperChatMessage(message + ' -> cleared chat', socket);
+ break;
+ default:
+ this.whisperChatMessage(message +' -> unknown command', socket);
+ }
- return true;
- }
+ return true;
+ }
- broadcastChat(message: string) {
- const chatMessage: ChatMessage = {
- id: -1,
- name: '',
- message,
- };
- this.io.emit('chatMsg', chatMessage);
- }
+ broadcastChat(message: string) {
+ const chatMessage: ChatMessage = {
+ id: -1,
+ name: '',
+ message,
+ };
+ this.io.emit('chatMsg', chatMessage);
+ }
- whisperChatMessage(message: string, socket: Socket) {
- const chatMessage: ChatMessage = {
- id: -1,
- name: '',
- message,
- };
- socket.emit('chatMsg', chatMessage);
- }
-}
+ whisperChatMessage(message: string, socket: Socket) {
+ const chatMessage: ChatMessage = {
+ id: -1,
+ name: '',
+ message,
+ };
+ socket.emit('chatMsg', chatMessage);
+ }
+}
\ No newline at end of file
diff --git a/src/server/managers/DamageSystem.ts b/src/server/managers/DamageSystem.ts
index 4f77d0b..d079ac1 100644
--- a/src/server/managers/DamageSystem.ts
+++ b/src/server/managers/DamageSystem.ts
@@ -1,68 +1,68 @@
-import { PlayerManager } from './PlayerManager.ts';
-import { ChatManager } from './ChatManager.ts';
-import { DamageRequest } from '../models/DamageRequest.ts';
-import { DataValidator } from '../DataValidator.ts';
-import { Vector3 } from '../models/Vector3.ts';
-import { GameEngine } from '../GameEngine.ts';
+import { PlayerManager } from "./PlayerManager.ts";
+import { ChatManager } from "./ChatManager.ts";
+import { DamageRequest } from "../models/DamageRequest.ts";
+import { DataValidator } from "../DataValidator.ts";
+import { Vector3 } from "../models/Vector3.ts";
+import {GameEngine} from "../GameEngine.ts";
export class DamageSystem {
- constructor(
- private playerManager: PlayerManager,
- private chatManager: ChatManager,
- ) {}
- private gameEngine!: GameEngine;
- public setGameEngine(gameEngine: GameEngine) {
- this.gameEngine = gameEngine;
- }
+ constructor(
+ private playerManager: PlayerManager,
+ private chatManager: ChatManager,
+ ) {}
+ private gameEngine!:GameEngine;
+ public setGameEngine(gameEngine:GameEngine){
+ this.gameEngine = gameEngine;
+ }
- handleDamageRequest(data: DamageRequest) {
- const validationResult = DataValidator.validateDamageRequest(data);
- if (!validationResult.success) {
- console.warn(`Invalid damage request: ${validationResult.error?.message}`);
- return;
- }
+ handleDamageRequest(data: DamageRequest) {
+ const validationResult = DataValidator.validateDamageRequest(data);
+ if (!validationResult.success) {
+ console.warn(`Invalid damage request: ${validationResult.error?.message}`);
+ return;
+ }
- const targetPlayer = this.playerManager.getPlayerById(data.targetPlayer.id);
- const localPlayer = this.playerManager.getPlayerById(data.localPlayer.id);
+ const targetPlayer = this.playerManager.getPlayerById(data.targetPlayer.id);
+ const localPlayer = this.playerManager.getPlayerById(data.localPlayer.id);
- if (!targetPlayer || !localPlayer) {
- console.warn('Target or local player not found.');
- return;
- }
+ if (!targetPlayer || !localPlayer) {
+ console.warn('Target or local player not found.');
+ return;
+ }
- // Validate positions
- const localPlayerSentPosition = data.localPlayer.position;
- const localPlayerServerPosition = localPlayer.position;
- const localDistance = Vector3.distanceTo(localPlayerSentPosition, localPlayerServerPosition);
+ // Validate positions
+ const localPlayerSentPosition = data.localPlayer.position;
+ const localPlayerServerPosition = localPlayer.position;
+ const localDistance = Vector3.distanceTo(localPlayerSentPosition, localPlayerServerPosition);
- const targetPlayerSentPosition = data.targetPlayer.position;
- const targetPlayerServerPosition = targetPlayer.position;
- const targetDistance = Vector3.distanceTo(targetPlayerSentPosition, targetPlayerServerPosition);
+ const targetPlayerSentPosition = data.targetPlayer.position;
+ const targetPlayerServerPosition = targetPlayer.position;
+ const targetDistance = Vector3.distanceTo(targetPlayerSentPosition, targetPlayerServerPosition);
- const MAX_DESYNC_DISTANCE = 1; // Threshold for considering positions in sync
+ const MAX_DESYNC_DISTANCE = 1; // Threshold for considering positions in sync
- if (localDistance > MAX_DESYNC_DISTANCE || targetDistance > MAX_DESYNC_DISTANCE) {
- //console.warn(`⚠️ Client out of sync - localDistance: ${localDistance}, targetDistance: ${targetDistance}`);
- // Optionally, send a message back to the client
- // this.chatManager.whisperChatMessage('⚠️ Shot not registered (client out of sync)', localPlayer.socket);
- return;
- }
+ if (localDistance > MAX_DESYNC_DISTANCE || targetDistance > MAX_DESYNC_DISTANCE) {
+ //console.warn(`⚠️ Client out of sync - localDistance: ${localDistance}, targetDistance: ${targetDistance}`);
+ // Optionally, send a message back to the client
+ // this.chatManager.whisperChatMessage('⚠️ Shot not registered (client out of sync)', localPlayer.socket);
+ return;
+ }
- // Apply damage
- targetPlayer.health -= data.damage;
- targetPlayer.lastDamageTime = Date.now() / 1000;
- targetPlayer.idLastDamagedBy = localPlayer.id;
+ // Apply damage
+ targetPlayer.health -= data.damage;
+ targetPlayer.lastDamageTime = Date.now() / 1000;
+ targetPlayer.idLastDamagedBy = localPlayer.id;
- if (targetPlayer.health <= 0) {
- const killerName = localPlayer.name;
- const killedName = targetPlayer.name;
- //this.chatManager.broadcastChat(`${killerName} &fkilled ${killedName}`);
- console.log(`💔 ${killerName} killed ${killedName}`);
- //this.playerManager.respawnPlayer(targetPlayer);
- this.gameEngine.periodicCleanup();
- }
+ if (targetPlayer.health <= 0) {
+ const killerName = localPlayer.name;
+ const killedName = targetPlayer.name;
+ //this.chatManager.broadcastChat(`${killerName} &fkilled ${killedName}`);
+ console.log(`💔 ${killerName} killed ${killedName}`);
+ //this.playerManager.respawnPlayer(targetPlayer);
+ this.gameEngine.periodicCleanup();
+ }
- // Update player data
- this.playerManager.addOrUpdatePlayer(targetPlayer);
- }
-}
+ // Update player data
+ this.playerManager.addOrUpdatePlayer(targetPlayer);
+ }
+}
\ No newline at end of file
diff --git a/src/server/managers/GameMsgManager.ts b/src/server/managers/GameMsgManager.ts
index 7c8dd39..96ab7fa 100644
--- a/src/server/managers/GameMsgManager.ts
+++ b/src/server/managers/GameMsgManager.ts
@@ -1,4 +1,4 @@
export class GameMsgManager {
- constructor() {
- }
-}
+ constructor() {
+ }
+}
\ No newline at end of file
diff --git a/src/server/managers/ItemManager.ts b/src/server/managers/ItemManager.ts
index 78fcd2a..3b75d9b 100644
--- a/src/server/managers/ItemManager.ts
+++ b/src/server/managers/ItemManager.ts
@@ -1,122 +1,124 @@
import { WorldItem } from '../models/WorldItem.ts';
import { MapData } from '../models/MapData.ts';
import { Vector3 } from '../models/Vector3.ts';
-import config from '../config.ts';
-import { PlayerManager } from './PlayerManager.ts';
-import { ChatManager } from './ChatManager.ts';
-import { Gamemode } from '../gamemodes/Gamemode.ts';
+import config from "../config.ts";
+import {PlayerManager} from "./PlayerManager.ts";
+import {ChatManager} from "./ChatManager.ts";
+import {Gamemode} from "../gamemodes/Gamemode.ts";
+
export class ItemManager {
- private worldItems: WorldItem[] = [];
- private lastItemCreationTimestamp: number = Date.now() / 1000;
- private itemUpdateFlag: boolean = false;
- private gamemode: Gamemode | false = false;
-
- constructor(private mapData: MapData, public playerManager: PlayerManager, private chatManager: ChatManager) {}
-
- public setGamemode(gamemode: Gamemode | false) {
- this.gamemode = gamemode;
- }
-
- tick(currentTime: number) {
- this.checkForPickups();
- if (currentTime - this.lastItemCreationTimestamp > config.items.respawnTime) {
- this.createItem();
- this.lastItemCreationTimestamp = currentTime;
- }
- // Additional item-related logic can be added here
- }
-
- createItem() {
- if (!this.mapData) return;
-
- const randomIndex = Math.floor(Math.random() * this.mapData.itemRespawnPoints.length);
- const respawnPoint = this.mapData.itemRespawnPoints[randomIndex];
- const newItem = new WorldItem(
- new Vector3(respawnPoint.position.x, respawnPoint.position.y, respawnPoint.position.z),
- respawnPoint.itemId,
- );
-
- if (this.isItemCloseToPoint(newItem.vector, 1)) return; // Another item is too close
- if (this.worldItems.length >= config.items.maxItemsInWorld) return; // Max items reached
-
- this.worldItems.push(newItem);
- this.itemUpdateFlag = true;
- }
-
- pushItem(item: WorldItem) {
- this.worldItems.push(item);
- this.itemUpdateFlag = true;
- }
-
- checkForPickups() {
- const players = this.playerManager.getAllPlayers();
-
- for (const player of players) {
- if (player.playerSpectating !== -1) continue;
- if (player.health <= 0) continue;
- const itemIndex = this.worldItems.findIndex((item) => Vector3.distanceTo(player.position, item.vector) < 0.5);
-
- if (itemIndex === -1) continue;
-
- const item = this.worldItems[itemIndex];
- let shouldPickup = false;
-
- switch (item.itemType) {
- case 0: // Cube
- player.inventory.push(0);
- shouldPickup = true;
- this.chatManager.broadcastChat(`${player.name} picked up [Object]!`);
- console.log(`🍌 ${player.name} picked up cube!`);
- break;
-
- case 1: // Banana
- if (!player.inventory.includes(1)) {
- player.inventory.push(1);
- shouldPickup = true;
- console.log(`🍌 ${player.name} picked up banana!`);
- }
- break;
-
- case 2: // Fish
- if (!player.inventory.includes(2)) {
- player.inventory.push(2);
- shouldPickup = true;
- console.log(`🍌 ${player.name} picked up fish!`);
- }
- break;
- }
-
- if (shouldPickup) {
- if (this.gamemode) this.gamemode.onItemPickup(player);
- this.worldItems.splice(itemIndex, 1);
- this.itemUpdateFlag = true;
- }
- }
- }
-
- isItemCloseToPoint(vector: Vector3, distance: number): boolean {
- return this.worldItems.some((item) => Vector3.distanceTo(item.vector, vector) < distance);
- }
-
- getAllItems(): WorldItem[] {
- return this.worldItems;
- }
-
- removeItem(itemId: number) {
- this.worldItems = this.worldItems.filter((item) => item.id !== itemId);
- this.itemUpdateFlag = true;
- }
-
- hasUpdates(): boolean {
- if (this.itemUpdateFlag) {
- this.itemUpdateFlag = false;
- return true;
- }
- return false;
- }
-
- triggerUpdateFlag() {
- this.itemUpdateFlag = true;
- }
-}
+ private worldItems: WorldItem[] = [];
+ private lastItemCreationTimestamp: number = Date.now() / 1000;
+ private itemUpdateFlag: boolean = false;
+ private gamemode: Gamemode | false = false;
+
+ constructor(private mapData: MapData, public playerManager:PlayerManager, private chatManager:ChatManager) {}
+
+ public setGamemode(gamemode: Gamemode | false) {this.gamemode = gamemode;}
+
+ tick(currentTime: number) {
+ this.checkForPickups();
+ if (currentTime - this.lastItemCreationTimestamp > config.items.respawnTime) {
+ this.createItem();
+ this.lastItemCreationTimestamp = currentTime;
+ }
+ // Additional item-related logic can be added here
+ }
+
+ createItem() {
+ if (!this.mapData) return;
+
+ const randomIndex = Math.floor(Math.random() * this.mapData.itemRespawnPoints.length);
+ const respawnPoint = this.mapData.itemRespawnPoints[randomIndex];
+ const newItem = new WorldItem(
+ new Vector3(respawnPoint.position.x, respawnPoint.position.y, respawnPoint.position.z),
+ respawnPoint.itemId
+ );
+
+ if (this.isItemCloseToPoint(newItem.vector, 1)) return; // Another item is too close
+ if (this.worldItems.length >= config.items.maxItemsInWorld) return; // Max items reached
+
+ this.worldItems.push(newItem);
+ this.itemUpdateFlag = true;
+ }
+
+ pushItem(item: WorldItem) {
+ this.worldItems.push(item);
+ this.itemUpdateFlag = true;
+ }
+
+ checkForPickups() {
+ const players = this.playerManager.getAllPlayers();
+
+ for (const player of players) {
+ if(player.playerSpectating !== -1) continue;
+ if(player.health <= 0) continue;
+ const itemIndex = this.worldItems.findIndex(item =>
+ Vector3.distanceTo(player.position, item.vector) < 0.5
+ );
+
+ if (itemIndex === -1) continue;
+
+ const item = this.worldItems[itemIndex];
+ let shouldPickup = false;
+
+ switch (item.itemType) {
+ case 0: // Cube
+ player.inventory.push(0);
+ shouldPickup = true;
+ this.chatManager.broadcastChat(`${player.name} picked up [Object]!`);
+ console.log(`🍌 ${player.name} picked up cube!`);
+ break;
+
+ case 1: // Banana
+ if (!player.inventory.includes(1)) {
+ player.inventory.push(1);
+ shouldPickup = true;
+ console.log(`🍌 ${player.name} picked up banana!`);
+ }
+ break;
+
+ case 2: // Fish
+ if (!player.inventory.includes(2)) {
+ player.inventory.push(2);
+ shouldPickup = true;
+ console.log(`🍌 ${player.name} picked up fish!`);
+ }
+ break;
+ }
+
+ if (shouldPickup) {
+ if(this.gamemode) this.gamemode.onItemPickup(player);
+ this.worldItems.splice(itemIndex, 1);
+ this.itemUpdateFlag = true;
+ }
+ }
+ }
+
+
+ isItemCloseToPoint(vector: Vector3, distance: number): boolean {
+ return this.worldItems.some(item => Vector3.distanceTo(item.vector,vector) < distance);
+ }
+
+ getAllItems(): WorldItem[] {
+ return this.worldItems;
+ }
+
+ removeItem(itemId: number) {
+ this.worldItems = this.worldItems.filter(item => item.id !== itemId);
+ this.itemUpdateFlag = true;
+ }
+
+ hasUpdates(): boolean {
+ if (this.itemUpdateFlag) {
+ this.itemUpdateFlag = false;
+ return true;
+ }
+ return false;
+ }
+
+ triggerUpdateFlag(){
+ this.itemUpdateFlag = true;
+ }
+}
\ No newline at end of file
diff --git a/src/server/managers/PlayerManager.ts b/src/server/managers/PlayerManager.ts
index 86b4fa3..5e04151 100644
--- a/src/server/managers/PlayerManager.ts
+++ b/src/server/managers/PlayerManager.ts
@@ -1,171 +1,162 @@
-import { Player } from '../models/Player.ts';
+import { Player } from "../models/Player.ts";
import { Vector3 } from '../models/Vector3.ts';
import { Quaternion } from '../models/Quaternion.ts';
import { DataValidator } from '../DataValidator.ts';
-import { MapData } from '../models/MapData.ts';
-import config from '../config.ts';
-import { WorldItem } from '../models/WorldItem.ts';
-import { ItemManager } from './ItemManager.ts';
-import { PlayerExtras } from '../models/PlayerExtras.ts';
+import { MapData } from "../models/MapData.ts";
+import config from "../config.ts";
+import { WorldItem } from "../models/WorldItem.ts";
+import { ItemManager } from "./ItemManager.ts";
+import {PlayerExtras} from "../models/PlayerExtras.ts";
interface PlayerData {
- player: Player;
- extras: PlayerExtras;
+ player: Player;
+ extras: PlayerExtras;
}
export class PlayerManager {
- private players: Map = new Map();
- private mapData: MapData;
- private itemManager!: ItemManager;
-
- constructor(mapData: MapData) {
- this.mapData = mapData;
- }
-
- setItemManager(itemManager: ItemManager) {
- this.itemManager = itemManager;
- }
-
- addOrUpdatePlayer(data: Player): { isNew: boolean; player?: Player } {
- const { error } = DataValidator.validatePlayerData(data);
- if (error) {
- throw new Error(`⚠️ invalid player data `);
- }
-
- const existingPlayerData = this.players.get(data.id);
- if (data.name.length < 1) data.name = 'possum' + data.id.toString().substring(0, 3);
- if (data.chatMsg.startsWith('/admin ')) data.chatMsg = '/admin ' + data.chatMsg.substring(7).replace(/./g, '*');
- if (data.chatMsg.startsWith('>')) data.chatMsg = '&2' + data.chatMsg;
- if (!data.chatMsg.startsWith('&f')) data.chatMsg = '&f' + data.chatMsg;
-
- if (existingPlayerData) {
- // Handle forced acknowledgment
- if (existingPlayerData.player.forced && !data.forcedAcknowledged) {
- return { isNew: false };
- }
- if (existingPlayerData.player.forced && data.forcedAcknowledged) {
- existingPlayerData.player.forced = false;
- }
-
- // Update existing player, preserving certain fields
- data.health = existingPlayerData.player.health;
- data.inventory = existingPlayerData.player.inventory;
- data.lastDamageTime = existingPlayerData.player.lastDamageTime;
- data.gameMsgs = existingPlayerData.player.gameMsgs;
- data.gameMsgs2 = existingPlayerData.player.gameMsgs2;
- data.playerSpectating = existingPlayerData.player.playerSpectating;
- data.updateTimestamp = Date.now() / 1000;
-
- const updatedData: PlayerData = {
- player: data,
- extras: existingPlayerData.extras,
- };
- this.players.set(data.id, updatedData);
- return { isNew: false };
- } else {
- // New player
- data.inventory = [...config.player.baseInventory];
- const spawnPoint = this.getRandomSpawnPoint();
- data.position = spawnPoint.vec;
- data.health = config.player.maxHealth;
- data.gameMsgs = [];
- data.gameMsgs2 = [];
- data.playerSpectating = -1;
- data.lookQuaternion = [
- spawnPoint.quaternion.x,
- spawnPoint.quaternion.y,
- spawnPoint.quaternion.z,
- spawnPoint.quaternion.w,
- ];
- data.forced = true;
-
- const newPlayerData: PlayerData = {
- player: data,
- extras: new PlayerExtras(),
- };
- this.players.set(data.id, newPlayerData);
- this.itemManager.triggerUpdateFlag();
-
- return { isNew: true, player: data };
- }
- }
-
- removePlayer(playerId: number) {
- this.players.delete(playerId);
- }
-
- getAllPlayers(): Player[] {
- return Array.from(this.players.values()).map((playerData) => playerData.player);
- }
-
- getPlayerById(playerId: number): Player | undefined {
- const playerData = this.players.get(playerId);
- return playerData?.player;
- }
-
- getPlayerDataById(playerId: number): PlayerData | undefined {
- return this.players.get(playerId);
- }
-
- getPlayerExtrasById(playerId: number): PlayerExtras | undefined {
- const playerData = this.players.get(playerId);
- return playerData?.extras;
- }
-
- getAllPlayerData(): PlayerData[] {
- return Array.from(this.players.values());
- }
-
- public dropAllItems(player: Player) {
- for (let i = 0; i < player.inventory.length; i++) {
- this.itemManager.pushItem(new WorldItem(player.position, player.inventory[i]));
- }
- player.inventory = [];
- }
-
- respawnPlayer(player: Player) {
- const playerData = this.players.get(player.id);
- if (!playerData) return;
-
- const spawnPoint = this.getRandomSpawnPoint();
- player.position = spawnPoint.vec;
- player.lookQuaternion = [
- spawnPoint.quaternion.x,
- spawnPoint.quaternion.y,
- spawnPoint.quaternion.z,
- spawnPoint.quaternion.w,
- ];
- player.health = config.player.maxHealth;
- player.gravity = 0;
- player.velocity = new Vector3(0, 0, 0);
- player.forced = true;
-
- const updatedPlayerData: PlayerData = {
- player: player,
- extras: playerData.extras,
- };
- this.players.set(player.id, updatedPlayerData);
- }
-
- regenerateHealth() {
- const currentTime = Date.now() / 1000;
- for (const playerData of this.players.values()) {
- const player = playerData.player;
- const lastDamage = player.lastDamageTime ?? 0;
- if (player.health < config.player.maxHealth && (lastDamage + config.health.regenDelay < currentTime)) {
- player.health += config.health.regenRate / config.server.tickRate;
- if (player.health > config.player.maxHealth) player.health = config.player.maxHealth;
- }
- }
- }
-
- private getRandomSpawnPoint(): { vec: Vector3; quaternion: Quaternion } {
- if (!this.mapData) {
- return { vec: new Vector3(2, 1, 0), quaternion: new Quaternion(0, 0, 0, 1) };
- }
-
- const randomIndex = Math.floor(Math.random() * this.mapData.respawnPoints.length);
- const respawnPoint = this.mapData.respawnPoints[randomIndex];
- return { vec: respawnPoint.position, quaternion: respawnPoint.quaternion };
- }
+ private players: Map = new Map();
+ private mapData: MapData;
+ private itemManager!: ItemManager;
+
+ constructor(mapData: MapData) {
+ this.mapData = mapData;
+ }
+
+ setItemManager(itemManager: ItemManager) {
+ this.itemManager = itemManager
+ }
+
+ addOrUpdatePlayer(data: Player): { isNew: boolean; player?: Player } {
+ const { error } = DataValidator.validatePlayerData(data);
+ if (error) {
+ throw new Error(`⚠️ invalid player data `);
+ }
+
+ const existingPlayerData = this.players.get(data.id);
+ if (data.name.length < 1) data.name = 'possum' + data.id.toString().substring(0,3);
+ if(data.chatMsg.startsWith('/admin ')) data.chatMsg = '/admin ' + data.chatMsg.substring(7).replace(/./g, '*');
+ if(data.chatMsg.startsWith('>')) data.chatMsg = '&2'+data.chatMsg;
+ if(!data.chatMsg.startsWith('&f')) data.chatMsg = '&f'+data.chatMsg;
+
+ if (existingPlayerData) {
+ // Handle forced acknowledgment
+ if (existingPlayerData.player.forced && !data.forcedAcknowledged) {
+ return { isNew: false };
+ }
+ if (existingPlayerData.player.forced && data.forcedAcknowledged) {
+ existingPlayerData.player.forced = false;
+ }
+
+ // Update existing player, preserving certain fields
+ data.health = existingPlayerData.player.health;
+ data.inventory = existingPlayerData.player.inventory;
+ data.lastDamageTime = existingPlayerData.player.lastDamageTime;
+ data.gameMsgs = existingPlayerData.player.gameMsgs;
+ data.gameMsgs2 = existingPlayerData.player.gameMsgs2;
+ data.playerSpectating = existingPlayerData.player.playerSpectating;
+ data.updateTimestamp = Date.now() / 1000;
+
+ const updatedData: PlayerData = {
+ player: data,
+ extras: existingPlayerData.extras
+ };
+ this.players.set(data.id, updatedData);
+ return { isNew: false };
+ } else {
+ // New player
+ data.inventory = [...config.player.baseInventory];
+ const spawnPoint = this.getRandomSpawnPoint();
+ data.position = spawnPoint.vec;
+ data.health = config.player.maxHealth;
+ data.gameMsgs = [];
+ data.gameMsgs2 = [];
+ data.playerSpectating = -1;
+ data.lookQuaternion = [spawnPoint.quaternion.x, spawnPoint.quaternion.y, spawnPoint.quaternion.z, spawnPoint.quaternion.w];
+ data.forced = true;
+
+ const newPlayerData: PlayerData = {
+ player: data,
+ extras: new PlayerExtras()
+ };
+ this.players.set(data.id, newPlayerData);
+ this.itemManager.triggerUpdateFlag();
+
+ return { isNew: true, player: data };
+ }
+ }
+
+ removePlayer(playerId: number) {
+ this.players.delete(playerId);
+ }
+
+ getAllPlayers(): Player[] {
+ return Array.from(this.players.values()).map(playerData => playerData.player);
+ }
+
+ getPlayerById(playerId: number): Player | undefined {
+ const playerData = this.players.get(playerId);
+ return playerData?.player;
+ }
+
+ getPlayerDataById(playerId: number): PlayerData | undefined {
+ return this.players.get(playerId);
+ }
+
+ getPlayerExtrasById(playerId: number): PlayerExtras | undefined {
+ const playerData = this.players.get(playerId);
+ return playerData?.extras;
+ }
+
+ getAllPlayerData(): PlayerData[] {
+ return Array.from(this.players.values());
+ }
+
+ public dropAllItems(player:Player){
+ for(let i = 0; i < player.inventory.length; i++){
+ this.itemManager.pushItem(new WorldItem(player.position, player.inventory[i]));
+ }
+ player.inventory = [];
+
+ }
+
+ respawnPlayer(player: Player) {
+ const playerData = this.players.get(player.id);
+ if (!playerData) return;
+
+ const spawnPoint = this.getRandomSpawnPoint();
+ player.position = spawnPoint.vec;
+ player.lookQuaternion = [spawnPoint.quaternion.x, spawnPoint.quaternion.y, spawnPoint.quaternion.z, spawnPoint.quaternion.w];
+ player.health = config.player.maxHealth;
+ player.gravity = 0;
+ player.velocity = new Vector3(0, 0, 0);
+ player.forced = true;
+
+ const updatedPlayerData: PlayerData = {
+ player: player,
+ extras: playerData.extras
+ };
+ this.players.set(player.id, updatedPlayerData);
+ }
+
+ regenerateHealth() {
+ const currentTime = Date.now() / 1000;
+ for (const playerData of this.players.values()) {
+ const player = playerData.player;
+ const lastDamage = player.lastDamageTime ?? 0;
+ if (player.health < config.player.maxHealth && (lastDamage + config.health.regenDelay < currentTime)) {
+ player.health += config.health.regenRate / config.server.tickRate;
+ if (player.health > config.player.maxHealth) player.health = config.player.maxHealth;
+ }
+ }
+ }
+
+ private getRandomSpawnPoint(): { vec: Vector3; quaternion: Quaternion } {
+ if (!this.mapData) {
+ return { vec: new Vector3(2, 1, 0), quaternion: new Quaternion(0, 0, 0, 1) };
+ }
+
+ const randomIndex = Math.floor(Math.random() * this.mapData.respawnPoints.length);
+ const respawnPoint = this.mapData.respawnPoints[randomIndex];
+ return { vec: respawnPoint.position, quaternion: respawnPoint.quaternion };
+ }
}
diff --git a/src/server/models/ChatMessage.ts b/src/server/models/ChatMessage.ts
index f2fe937..c3e0ede 100644
--- a/src/server/models/ChatMessage.ts
+++ b/src/server/models/ChatMessage.ts
@@ -1,5 +1,5 @@
export interface ChatMessage {
- id: number;
- name: string;
- message: string;
-}
+ id: number;
+ name: string;
+ message: string;
+}
\ No newline at end of file
diff --git a/src/server/models/DamageRequest.ts b/src/server/models/DamageRequest.ts
index 421b991..40b94aa 100644
--- a/src/server/models/DamageRequest.ts
+++ b/src/server/models/DamageRequest.ts
@@ -1,7 +1,7 @@
-import { Player } from './Player.ts';
+import { Player } from "./Player.ts";
export interface DamageRequest {
- localPlayer: Player;
- targetPlayer: Player;
- damage: number;
-}
+ localPlayer: Player;
+ targetPlayer: Player;
+ damage: number;
+}
\ No newline at end of file
diff --git a/src/server/models/ItemRespawnPoint.ts b/src/server/models/ItemRespawnPoint.ts
index 811fc3c..d77b7c5 100644
--- a/src/server/models/ItemRespawnPoint.ts
+++ b/src/server/models/ItemRespawnPoint.ts
@@ -1,9 +1,9 @@
import { Vector3 } from './Vector3.ts';
export class ItemRespawnPoint {
- constructor(
- public position: Vector3,
- public itemId: number,
- public spawnChancePerTick: number,
- ) {}
-}
+ constructor(
+ public position: Vector3,
+ public itemId: number,
+ public spawnChancePerTick: number
+ ) {}
+}
\ No newline at end of file
diff --git a/src/server/models/MapData.ts b/src/server/models/MapData.ts
index ed2b665..5ea4bb8 100644
--- a/src/server/models/MapData.ts
+++ b/src/server/models/MapData.ts
@@ -1,35 +1,33 @@
import { RespawnPoint } from './RespawnPoint.ts';
import { ItemRespawnPoint } from './ItemRespawnPoint.ts';
-import { Vector3 } from './Vector3.ts';
-import { Quaternion } from './Quaternion.ts';
+import { Vector3 } from "./Vector3.ts";
+import { Quaternion } from "./Quaternion.ts";
export class MapData {
- constructor(
- public name: string,
- public respawnPoints: RespawnPoint[],
- public itemRespawnPoints: ItemRespawnPoint[],
- ) {}
+ constructor(
+ public name: string,
+ public respawnPoints: RespawnPoint[],
+ public itemRespawnPoints: ItemRespawnPoint[]
+ ) {}
- static fromJSON(
- json: { respawnPoints: RespawnPoint[]; itemRespawnPoints: ItemRespawnPoint[]; name: string },
- ): MapData {
- const respawnPoints = json.respawnPoints.map(
- (rp) =>
- new RespawnPoint(
- new Vector3(rp.position.x, rp.position.y, rp.position.z),
- new Quaternion(rp.quaternion.x, rp.quaternion.y, rp.quaternion.z, rp.quaternion.w),
- ),
- );
+ static fromJSON(json: { respawnPoints: RespawnPoint[]; itemRespawnPoints: ItemRespawnPoint[]; name: string; }): MapData {
+ const respawnPoints = json.respawnPoints.map(
+ (rp) =>
+ new RespawnPoint(
+ new Vector3(rp.position.x, rp.position.y, rp.position.z),
+ new Quaternion(rp.quaternion.x, rp.quaternion.y, rp.quaternion.z, rp.quaternion.w)
+ )
+ );
- const itemRespawnPoints = json.itemRespawnPoints.map(
- (irp) =>
- new ItemRespawnPoint(
- new Vector3(irp.position.x, irp.position.y, irp.position.z),
- irp.itemId,
- irp.spawnChancePerTick,
- ),
- );
+ const itemRespawnPoints = json.itemRespawnPoints.map(
+ (irp) =>
+ new ItemRespawnPoint(
+ new Vector3(irp.position.x, irp.position.y, irp.position.z),
+ irp.itemId,
+ irp.spawnChancePerTick
+ )
+ );
- return new MapData(json.name, respawnPoints, itemRespawnPoints);
- }
-}
+ return new MapData(json.name, respawnPoints, itemRespawnPoints);
+ }
+}
\ No newline at end of file
diff --git a/src/server/models/Player.ts b/src/server/models/Player.ts
index de4ab1f..3f63639 100644
--- a/src/server/models/Player.ts
+++ b/src/server/models/Player.ts
@@ -1,28 +1,29 @@
-import { Vector3 } from './Vector3.ts';
+import { Vector3 } from "./Vector3.ts";
export interface Player {
- id: number;
- speed: number;
- acceleration: number;
- name: string;
- gameVersion: string;
- position: Vector3;
- velocity: Vector3;
- inputVelocity: Vector3;
- gravity: number;
- lookQuaternion: number[];
- quaternion: number[];
- chatActive: boolean;
- chatMsg: string;
- latency: number;
- health: number;
- forced: boolean;
- forcedAcknowledged: boolean;
- updateTimestamp?: number;
- lastDamageTime?: number;
- inventory: number[];
- idLastDamagedBy?: number;
- playerSpectating: number;
- gameMsgs: string[];
- gameMsgs2: string[];
-}
+ id: number;
+ speed: number;
+ acceleration: number;
+ name: string;
+ gameVersion: string;
+ position: Vector3;
+ velocity: Vector3;
+ inputVelocity: Vector3;
+ gravity: number;
+ lookQuaternion: number[];
+ quaternion: number[];
+ chatActive: boolean;
+ chatMsg: string;
+ latency: number;
+ health: number;
+ forced: boolean;
+ forcedAcknowledged: boolean;
+ updateTimestamp?: number;
+ lastDamageTime?: number;
+ inventory: number[];
+ idLastDamagedBy?: number;
+ playerSpectating:number;
+ gameMsgs:string[];
+ gameMsgs2:string[];
+
+}
\ No newline at end of file
diff --git a/src/server/models/PlayerExtras.ts b/src/server/models/PlayerExtras.ts
index f16eff1..610cf4e 100644
--- a/src/server/models/PlayerExtras.ts
+++ b/src/server/models/PlayerExtras.ts
@@ -1,7 +1,8 @@
export class PlayerExtras {
- gameMsgsTimeouts: number[] = [];
- kills: number = 0;
- deaths: number = 0;
- killStreak: number = 0;
- points: number = 0;
-}
+gameMsgsTimeouts: number[] = [];
+kills: number = 0;
+deaths: number = 0;
+killStreak: number = 0;
+points: number = 0;
+
+}
\ No newline at end of file
diff --git a/src/server/models/Quaternion.ts b/src/server/models/Quaternion.ts
index dd75516..97216d3 100644
--- a/src/server/models/Quaternion.ts
+++ b/src/server/models/Quaternion.ts
@@ -1,3 +1,3 @@
export class Quaternion {
- constructor(public x: number, public y: number, public z: number, public w: number) {}
-}
+ constructor(public x: number, public y: number, public z: number, public w: number) {}
+}
\ No newline at end of file
diff --git a/src/server/models/RespawnPoint.ts b/src/server/models/RespawnPoint.ts
index c0ad5ea..e96e5e6 100644
--- a/src/server/models/RespawnPoint.ts
+++ b/src/server/models/RespawnPoint.ts
@@ -2,5 +2,5 @@ import { Vector3 } from './Vector3.ts';
import { Quaternion } from './Quaternion.ts';
export class RespawnPoint {
- constructor(public position: Vector3, public quaternion: Quaternion) {}
-}
+ constructor(public position: Vector3, public quaternion: Quaternion) {}
+}
\ No newline at end of file
diff --git a/src/server/models/ServerInfo.ts b/src/server/models/ServerInfo.ts
index 6dc1e90..a628942 100644
--- a/src/server/models/ServerInfo.ts
+++ b/src/server/models/ServerInfo.ts
@@ -1,22 +1,22 @@
-import config from '../config.ts';
+import config from "../config.ts";
export class ServerInfo {
- public name: string;
- public maxPlayers: number;
- public currentPlayers: number;
- public mapName: string;
- public tickRate: number;
- public version: string;
- public gameMode: string;
- public playerMaxHealth: number;
- constructor() {
- this.name = config.server.name;
- this.maxPlayers = config.game.maxPlayers;
- this.currentPlayers = 0;
- this.mapName = config.server.defaultMap;
- this.tickRate = config.server.tickRate;
- this.version = '';
- this.gameMode = config.game.mode;
- this.playerMaxHealth = config.player.maxHealth;
- }
-}
+ public name: string;
+ public maxPlayers: number;
+ public currentPlayers: number;
+ public mapName: string;
+ public tickRate: number;
+ public version: string;
+ public gameMode: string;
+ public playerMaxHealth: number;
+ constructor() {
+ this.name = config.server.name;
+ this.maxPlayers = config.game.maxPlayers;
+ this.currentPlayers = 0;
+ this.mapName = config.server.defaultMap;
+ this.tickRate = config.server.tickRate;
+ this.version = '';
+ this.gameMode = config.game.mode;
+ this.playerMaxHealth = config.player.maxHealth;
+ }
+}
\ No newline at end of file
diff --git a/src/server/models/Vector3.ts b/src/server/models/Vector3.ts
index 253af65..a8f44c1 100644
--- a/src/server/models/Vector3.ts
+++ b/src/server/models/Vector3.ts
@@ -1,18 +1,18 @@
export class Vector3 {
- constructor(public x: number, public y: number, public z: number) {}
- public distanceTo(other: Vector3): number {
- return Math.sqrt(
- Math.pow(this.x - other.x, 2) +
- Math.pow(this.y - other.y, 2) +
- Math.pow(this.z - other.z, 2),
- );
- }
+ constructor(public x: number, public y: number, public z: number) {}
+ public distanceTo(other: Vector3): number {
+ return Math.sqrt(
+ Math.pow(this.x - other.x, 2) +
+ Math.pow(this.y - other.y, 2) +
+ Math.pow(this.z - other.z, 2)
+ );
+ }
- public static distanceTo(v1: Vector3, v2: Vector3): number {
- return Math.sqrt(
- Math.pow(v1.x - v2.x, 2) +
- Math.pow(v1.y - v2.y, 2) +
- Math.pow(v1.z - v2.z, 2),
- );
- }
-}
+ public static distanceTo(v1: Vector3, v2: Vector3): number {
+ return Math.sqrt(
+ Math.pow(v1.x - v2.x, 2) +
+ Math.pow(v1.y - v2.y, 2) +
+ Math.pow(v1.z - v2.z, 2)
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/server/models/WorldItem.ts b/src/server/models/WorldItem.ts
index 43c3bf3..629e6b3 100644
--- a/src/server/models/WorldItem.ts
+++ b/src/server/models/WorldItem.ts
@@ -1,9 +1,9 @@
-import { Vector3 } from './Vector3.ts';
+import { Vector3 } from "./Vector3.ts";
export class WorldItem {
- public id: number;
+ public id: number;
- constructor(public vector: Vector3, public itemType: number) {
- this.id = Math.floor(Math.random() * 100000) + 1;
- }
-}
+ constructor(public vector: Vector3, public itemType: number) {
+ this.id = Math.floor(Math.random() * 100000) + 1;
+ }
+}
\ No newline at end of file
diff --git a/src/styles.css b/src/styles.css
index 6d69c38..3852c97 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -5,85 +5,85 @@
/* You can add global styles to this file, and also import other style files */
:root {
- font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
- font-size: 16px;
- line-height: 24px;
- font-weight: 400;
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- -webkit-text-size-adjust: 100%;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
}
a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
}
a:hover {
- color: #535bf2;
+ color: #535bf2;
}
body {
- margin: 0;
- /*display: flex;*/
- /*place-items: center;*/
- min-width: 320px;
- min-height: 100vh;
+ margin: 0;
+ /*display: flex;*/
+ /*place-items: center;*/
+ min-width: 320px;
+ min-height: 100vh;
}
h1 {
- font-size: 3.2em;
- line-height: 1.1;
+ font-size: 3.2em;
+ line-height: 1.1;
}
button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
}
button:hover {
- border-color: #646cff;
+ border-color: #646cff;
}
button:focus,
button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
+ outline: 4px auto -webkit-focus-ring-color;
}
.card {
- padding: 2em;
+ padding: 2em;
}
.logo {
- @apply box-content;
+ @apply box-content;
}
@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
- a:hover {
- color: #747bff;
- }
+ a:hover {
+ color: #747bff;
+ }
- button {
- background-color: #f9f9f9;
- }
+ button {
+ background-color: #f9f9f9;
+ }
}