diff --git a/assets/clean-pony2fix.glb b/assets/clean-pony2fix.glb index a9ff5d6..2cf27a8 100644 Binary files a/assets/clean-pony2fix.glb and b/assets/clean-pony2fix.glb differ diff --git a/src/client/game.ts b/src/client/game.ts index 5365a3a..d508ec3 100644 --- a/src/client/game.ts +++ b/src/client/game.ts @@ -1,7 +1,8 @@ import e from 'express'; import { Socket } from 'socket.io-client'; +import { Color } from 'three'; import { isMobileOrTablet } from '../common/helper'; -import { CompositePacket } from '../common/types/packet'; +import { CharacterPacket, CompositePacket } from '../common/types/packet'; import { IcyNetUser } from '../common/types/user'; import { ThirdPersonCamera } from './object/camera'; import { Chat } from './object/chat'; @@ -19,6 +20,7 @@ export class Game { public thirdPersonCamera!: ThirdPersonCamera; public joystick!: Joystick; public chat!: Chat; + private character: CharacterPacket = {}; private party: string[] = []; public renderer = new Renderer(); @@ -43,6 +45,11 @@ export class Game { this.party = (localStorage.getItem('party')?.split('|') || []).filter( (item) => item, ); + + const colorGet = localStorage.getItem('color'); + if (colorGet) { + this.character.color = colorGet; + } // end of this.chat.registerSendFunction((message) => { @@ -89,6 +96,7 @@ export class Game { this.me = user; const player = Player.fromUser(user, this.renderer.scene); + player.setCharacter(this.character); this.players.push(player); this.player = player; this.thirdPersonCamera = new ThirdPersonCamera( @@ -104,6 +112,8 @@ export class Game { if (isMobileOrTablet()) { this.joystick.show(); } + + this.socket.emit('character', this.character); }); this.socket.on('playerjoin', (user) => { @@ -119,6 +129,7 @@ export class Game { const findPlayer = this.players.find((item) => item.user.id === user.id); if (findPlayer) { this.renderer.scene.remove(findPlayer.container); + findPlayer.dispose(); this.players.splice(this.players.indexOf(findPlayer), 1); } }); @@ -150,6 +161,17 @@ export class Game { }); }); + this.socket.on('playercharacter', (data) => { + const player = this.players.find((player) => player.user.id === data.id); + if ( + player && + player instanceof PlayerEntity && + player.user.id !== this.me.id + ) { + player.setCharacter(data); + } + }); + this.socket.on('chat', (event) => { const player = this.players.find( (item) => item.user.id === event.sender.id, @@ -167,32 +189,51 @@ export class Game { } private experimentalPlayerCmd(message: string, sender: IcyNetUser) { - if (message.startsWith('!party') && this.me.id === sender.id) { - const array = message.split(' '); - const name = array.slice(2).join(' '); + if (this.me.id === sender.id) { + if (message.startsWith('!color')) { + const [cmd, color] = message.split(' '); + try { + const colorr = new Color(color); + if (!colorr) { + throw 'invalid'; + } + } catch (e: any) { + this.chat.addMessage('Invalid color.'); + return; + } - if (array[1] === 'join') { - this.party.push(name); - this.chat.addMessage(`Joined party of user "${name}".`); + this.player.setColor(color); + this.socket.emit('character', { color }); + localStorage.setItem('color', color); } - if (array[1] === 'leave') { - this.party.splice(this.party.indexOf(name), 1); - this.chat.addMessage(`Left party of user "${name}".`); - } + if (message.startsWith('!party')) { + const array = message.split(' '); + const name = array.slice(2).join(' '); - if (array[1] === 'clear') { - this.party.length = 0; - this.chat.addMessage('Cleared party list.'); - } + if (array[1] === 'join') { + this.party.push(name); + this.chat.addMessage(`Joined party of user "${name}".`); + } - if (array[1] === 'list') { - this.chat.addMessage( - `You have joined the watch party of: ${this.party.join(', ')}`, - ); - } + if (array[1] === 'leave') { + this.party.splice(this.party.indexOf(name), 1); + this.chat.addMessage(`Left party of user "${name}".`); + } - localStorage.setItem('party', this.party.join('|')); + if (array[1] === 'clear') { + this.party.length = 0; + this.chat.addMessage('Cleared party list.'); + } + + if (array[1] === 'list') { + this.chat.addMessage( + `You have joined the watch party of: ${this.party.join(', ')}`, + ); + } + + localStorage.setItem('party', this.party.join('|')); + } } if ( diff --git a/src/client/object/player-entity.ts b/src/client/object/player-entity.ts index e02ddc6..905191a 100644 --- a/src/client/object/player-entity.ts +++ b/src/client/object/player-entity.ts @@ -2,6 +2,7 @@ import { IcyNetUser } from '../../common/types/user'; import * as THREE from 'three'; import { PonyEntity } from './pony'; import { + CharacterPacket, FullStatePacket, PositionUpdatePacket, } from '../../common/types/packet'; @@ -101,13 +102,17 @@ export class PlayerEntity extends PonyEntity { ); } - public addUncommittedChanges(packet: PositionUpdatePacket) { + public addUncommittedChanges(packet: FullStatePacket) { const appendix = { ...packet, time: 0.1 }; this._updateQueue.push(appendix); if (!this._targetFrame) { this.setFromPacket(packet); this._targetFrame = appendix; } + + if (packet.character) { + this.setCharacter(packet.character); + } } public update(dt: number) { diff --git a/src/client/object/pony-loader.ts b/src/client/object/pony-loader.ts index b2d166f..82f7555 100644 --- a/src/client/object/pony-loader.ts +++ b/src/client/object/pony-loader.ts @@ -1,5 +1,6 @@ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; +import { Color, Mesh, MeshStandardMaterial } from 'three'; // Instantiate a loader const loader = new GLTFLoader(); @@ -26,6 +27,11 @@ class PonyModel { // temp: disable frustum culling for eyes this.ponyModel.children[0].children[2].frustumCulled = false; + // ( + // (this.ponyModel.children[0].children[1] as Mesh) + // .material as MeshStandardMaterial + // ).color = new Color(0xffffff); + resolve(gltf.scene); // gltf.animations; // Array diff --git a/src/client/object/pony.ts b/src/client/object/pony.ts index 3d54538..0018091 100644 --- a/src/client/object/pony.ts +++ b/src/client/object/pony.ts @@ -1,9 +1,10 @@ import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils'; import * as THREE from 'three'; import modelLoaderInstance from './pony-loader'; -import { FullStatePacket } from '../../common/types/packet'; +import { CharacterPacket, FullStatePacket } from '../../common/types/packet'; import { NameTag } from './nametag'; import { CanvasUtils } from './canvas-utils'; +import { Mesh, MeshStandardMaterial, Color } from 'three'; const nameTagBuilder = new CanvasUtils({ fill: false, @@ -19,6 +20,7 @@ export class PonyEntity { public mixer!: THREE.AnimationMixer; public container!: THREE.Object3D; public model!: THREE.Object3D; + public material!: THREE.MeshStandardMaterial; public walkAnimationState = 0; public idleAction: THREE.AnimationAction; public walkAction: THREE.AnimationAction; @@ -27,6 +29,11 @@ export class PonyEntity { initialize() { this.model = (SkeletonUtils as any).clone(modelLoaderInstance.ponyModel); + this.material = ( + (this.model.children[0].children[1] as Mesh) + .material as MeshStandardMaterial + ).clone(); + (this.model.children[0].children[1] as Mesh).material = this.material; this.mixer = new THREE.AnimationMixer(this.model); this.idleAction = this.mixer.clipAction(modelLoaderInstance.animations[0]); this.walkAction = this.mixer.clipAction(modelLoaderInstance.animations[2]); @@ -47,6 +54,12 @@ export class PonyEntity { this.mixer.update(dt); } + dispose() { + this.model = null; + this.material.dispose(); + this.nameTag?.dispose(); + } + public addNameTag(name: string) { this.nameTag = new NameTag(nameTagBuilder, name); this.nameTag.tag.position.set(0, 1.8, 0.5); @@ -68,4 +81,14 @@ export class PonyEntity { this.walkAction.crossFadeTo(this.idleAction.reset(), 0.5, false).play(); } } + + public setColor(color: number | string) { + this.material.color = new Color(color); + } + + public setCharacter(packet: CharacterPacket) { + if (packet.color) { + this.setColor(packet.color); + } + } } diff --git a/src/common/types/packet.ts b/src/common/types/packet.ts index dcf8605..1f5e52b 100644 --- a/src/common/types/packet.ts +++ b/src/common/types/packet.ts @@ -14,6 +14,11 @@ export interface FullStatePacket { position?: number[]; rotation?: (number | string)[]; animState?: number; + character?: CharacterPacket; +} + +export interface CharacterPacket { + color?: number | string; } export interface CompositePacket extends IcyNetUser, FullStatePacket {} diff --git a/src/server/object/game.ts b/src/server/object/game.ts index 3a44478..5c5878d 100644 --- a/src/server/object/game.ts +++ b/src/server/object/game.ts @@ -1,7 +1,10 @@ import { Server, Socket } from 'socket.io'; import { RequestHandler } from 'express'; import { IcyNetUser } from '../../common/types/user'; -import { PositionUpdatePacket } from '../../common/types/packet'; +import { + CharacterPacket, + PositionUpdatePacket, +} from '../../common/types/packet'; const PLACEHOLDER_USER = (socket: Socket): IcyNetUser => { const randomName = `player-${socket.id.substring(0, 8)}`; @@ -55,6 +58,7 @@ export class Game { position: [0, 0, 0], rotation: [0, 0, 0, 'XYZ'], animState: 0, + character: {}, }; socket.emit( @@ -78,6 +82,18 @@ export class Game { } }); + socket.on('character', (info: CharacterPacket) => { + socket.data.playerinfo.character = { + ...socket.data.playerinfo.character, + ...info, + }; + + this.io.emit('playercharacter', { + id: socket.data.user.id, + ...socket.data.playerinfo.character, + }); + }); + socket.on('chat-send', (raw) => { const message = raw.trim().substring(0, 260); this.io.emit('chat', { sender: publicUserInfo, message });