diff --git a/src/client/game.ts b/src/client/game.ts index 0e5a829..3ae2138 100644 --- a/src/client/game.ts +++ b/src/client/game.ts @@ -129,6 +129,14 @@ export class Game { }); this.socket.on('chat', (event) => { + const player = this.players.find( + (item) => item.user.id === event.sender.id, + ); + + if (player && player.user.id !== this.me.id) { + // (player as PlayerEntity).addChat(event.message); + } + this.chat.addMessage(event.sender.display_name, event.message); }); } diff --git a/src/client/object/canvas-utils.ts b/src/client/object/canvas-utils.ts index a9e5566..5520b92 100644 --- a/src/client/object/canvas-utils.ts +++ b/src/client/object/canvas-utils.ts @@ -2,7 +2,7 @@ import { CanvasTexture, LinearFilter, ClampToEdgeWrapping } from 'three'; export class CanvasUtils { public createTextCanvas( - text: string, + text: string | string[], bold = true, fontSize = 16, padding = 4, @@ -10,12 +10,23 @@ export class CanvasUtils { const ctx = document.createElement('canvas').getContext('2d'); const font = `${fontSize}px${bold ? ' bold' : ''} sans`; + const lines = Array.isArray(text) ? text : [text]; + const lineWidths = []; + let longestLine = 0; + // Measure the text bounds ctx.font = font; - const measure = ctx.measureText(text); + lines.forEach((line) => { + const lineWidth = ctx.measureText(line).width; + if (longestLine < lineWidth) { + longestLine = lineWidth; + } + lineWidths.push(lineWidth); + }); - const width = measure.width + padding * 2; - const height = fontSize + padding * 2; + const width = longestLine + padding * 2; + const textHeight = fontSize * lines.length; + const height = textHeight + padding * 2; // Resize canvas ctx.canvas.width = width; @@ -23,7 +34,6 @@ export class CanvasUtils { // Set text parameters ctx.font = font; - ctx.textBaseline = 'middle'; ctx.textAlign = 'center'; // Draw background @@ -31,11 +41,13 @@ export class CanvasUtils { ctx.fillRect(0, 0, width, height); // Scale the text to fit within the canvas - const scaleFactor = Math.min(1, width / measure.width); - ctx.translate(width / 2 - padding, height / 2 - padding); + const scaleFactor = Math.min(1, width / longestLine); + ctx.translate(width / 2 - padding, padding + fontSize / 2); ctx.scale(scaleFactor, 1); ctx.fillStyle = '#000'; - ctx.fillText(text, padding, padding); + lines.forEach((line, i) => { + ctx.fillText(line, padding, i * fontSize + padding); + }); // Create texture with appropriate flags const texture = new CanvasTexture(ctx.canvas); @@ -43,6 +55,6 @@ export class CanvasUtils { texture.wrapS = ClampToEdgeWrapping; texture.wrapT = ClampToEdgeWrapping; - return { texture, width: width, height: height }; + return { texture, width, height }; } } diff --git a/src/client/object/nametag.ts b/src/client/object/nametag.ts index 6587491..f99bc80 100644 --- a/src/client/object/nametag.ts +++ b/src/client/object/nametag.ts @@ -4,6 +4,7 @@ import { CanvasUtils } from './canvas-utils'; export class NameTag { public tag!: Sprite; public width!: number; + public height!: number; private texture!: CanvasTexture; private material!: SpriteMaterial; @@ -16,6 +17,7 @@ export class NameTag { this.texture = texture; this.width = width; + this.height = height; this.material = new SpriteMaterial({ map: texture, diff --git a/src/client/object/player-entity.ts b/src/client/object/player-entity.ts index 193283b..4fe477f 100644 --- a/src/client/object/player-entity.ts +++ b/src/client/object/player-entity.ts @@ -2,9 +2,15 @@ import { IcyNetUser } from '../../common/types/user'; import * as THREE from 'three'; import { PonyEntity } from './pony'; import { Packet } from '../../common/types/packet'; +import { CanvasUtils } from './canvas-utils'; +import { ChatBubble } from './chat-bubble'; + +const chatBuilder = new CanvasUtils(); export class PlayerEntity extends PonyEntity { private uncommittedPacket: Packet = {}; + private _chats: ChatBubble[] = []; + constructor(public user: IcyNetUser) { super(); } @@ -29,6 +35,47 @@ export class PlayerEntity extends PonyEntity { this.container.position.copy(pos); } + public addChat(message: string): void { + const lines = []; + let truncated = message; + + while (truncated.length > 80) { + lines.push(truncated.substring(0, 80)); + truncated = truncated.substring(80); + } + + if (truncated) { + lines.push(truncated); + } + + const newChat = new ChatBubble(chatBuilder, lines); + + this._chats.forEach((item) => { + item.tag.position.add(new THREE.Vector3(0, item.height * 0.01, 0)); + }); + + this._chats.unshift(newChat); + newChat.tag.position.set(0, 1.8, 0.5); + this.container.add(newChat.tag); + + if (this._chats.length > 3) { + this._chats.splice(3, this._chats.length - 1).forEach((item) => { + this.container.remove(item.tag); + item.dispose(); + }); + } + + setTimeout(() => { + const i = this._chats.indexOf(newChat); + + if (i !== -1) { + this.container.remove(newChat.tag); + this._chats.splice(i, 1); + newChat.dispose(); + } + }, 5000); + } + public setRotation(rot: THREE.Vector3 | THREE.Euler) { this.container.rotation.copy( rot instanceof THREE.Euler diff --git a/src/client/object/player.ts b/src/client/object/player.ts index 7ae23d5..87a9da2 100644 --- a/src/client/object/player.ts +++ b/src/client/object/player.ts @@ -86,7 +86,7 @@ export class Player extends PonyEntity { } if (vector.x !== 0) { - this.angularVelocity.set(0, Math.PI * dt * vector.x, 0); + this.angularVelocity.set(0, Math.PI * vector.x, 0); this.changes.angular = this.angularVelocity.toArray(); this._wasTurning = true; } else if (this._wasTurning && !wasExternalForce) { @@ -97,9 +97,7 @@ export class Player extends PonyEntity { } if (vector.y !== 0) { - this.velocity.copy( - this._lookVector.clone().multiplyScalar(vector.y * dt), - ); + this.velocity.copy(this._lookVector.clone().multiplyScalar(vector.y)); this.changes.velocity = this.velocity.toArray(); this._wasMoving = true; } else if (this._wasMoving && !wasExternalForce) { diff --git a/src/client/object/pony.ts b/src/client/object/pony.ts index ed35e9f..d392ce2 100644 --- a/src/client/object/pony.ts +++ b/src/client/object/pony.ts @@ -30,13 +30,13 @@ export class PonyEntity { } update(dt: number) { - this.container.position.add(this.velocity); + this.container.position.add(this.velocity.clone().multiplyScalar(dt)); this.container.rotation.setFromVector3( new THREE.Vector3( this.container.rotation.x, this.container.rotation.y, this.container.rotation.z, - ).add(this.angularVelocity), + ).add(this.angularVelocity.clone().multiplyScalar(dt)), ); this.mixer.update(dt); diff --git a/src/client/scss/index.scss b/src/client/scss/index.scss index 79e7eb5..3dc03c9 100644 --- a/src/client/scss/index.scss +++ b/src/client/scss/index.scss @@ -152,7 +152,7 @@ body { pointer-events: none; } - button { + button, input { pointer-events: all; } } diff --git a/src/server/object/game.ts b/src/server/object/game.ts index 0a571fc..ffc0389 100644 --- a/src/server/object/game.ts +++ b/src/server/object/game.ts @@ -41,47 +41,54 @@ export class Game { const publicUserInfo = user ? this.mapPlayer(user) : null; this._connections.push(socket); - socket.data.user = user; - socket.data.playerinfo = { - velocity: [0, 0, 0], - angular: [0, 0, 0], - position: [0, 0, 0], - rotation: [0, 0, 0], - animState: 0, - }; socket.emit('me', publicUserInfo); - socket.emit( - 'players', - this._connections - .filter((player) => player.data.user) - .map((conn) => ({ - ...this.mapPlayer(conn.data.user), - ...conn.data.playerinfo, - })), - ); - socket.broadcast.emit('playerjoin', publicUserInfo); + if (user) { + socket.broadcast.emit('playerjoin', publicUserInfo); + + socket.data.user = user; + socket.data.playerinfo = { + velocity: [0, 0, 0], + angular: [0, 0, 0], + position: [0, 0, 0], + rotation: [0, 0, 0], + animState: 0, + }; + + socket.emit( + 'players', + this._connections + .filter((player) => player.data.user) + .map((conn) => ({ + ...this.mapPlayer(conn.data.user), + ...conn.data.playerinfo, + })), + ); + + socket.on('move', (packet) => { + socket.data.playerinfo = { + ...socket.data.playerinfo, + ...packet, + }; + socket.broadcast.emit('playerupdate', { + id: user.id, + ...packet, + }); + }); + + socket.on('chat-send', (raw) => { + const message = raw.trim().substring(0, 260); + this.io.emit('chat', { sender: publicUserInfo, message }); + }); + } socket.on('disconnect', () => { this._connections.splice(this._connections.indexOf(socket), 1); - this.io.emit('playerleave', publicUserInfo); - }); - socket.on('move', (packet) => { - socket.data.playerinfo = { - ...socket.data.playerinfo, - ...packet, - }; - socket.broadcast.emit('playerupdate', { - id: user.id, - ...packet, - }); - }); - - socket.on('chat-send', (raw) => { - const message = raw.trim().substring(0, 260); - this.io.emit('chat', { sender: publicUserInfo, message }); + if (user) { + this.io.emit('playerleave', publicUserInfo); + } }); }); }