import { Socket } from 'socket.io-client'; import { Color, DoubleSide, MeshBasicMaterial, Vector3 } from 'three'; import { clamp, isMobileOrTablet } from '../common/helper'; import { CharacterPacket, CompositePacket } from '../common/types/packet'; import { IcyNetUser } from '../common/types/user'; import { ThirdPersonCamera } from './object/camera'; import { Chat } from './object/chat'; import { Joystick } from './object/joystick'; import { PonyModelLoader } from './object/resource/pony-loader'; import { PonyEyes } from './object/model/eyes'; import { CubeMap } from './object/resource/cubemap'; import { VideoPlayer } from './object/other/video-player'; import { Player } from './object/player'; import { PlayerEntity } from './object/player-entity'; import { Renderer } from './renderer'; import { Grass } from './object/model/grass'; import { BaseTexture } from './object/resource/texture'; import { ClientWorld } from './object/world/client-world'; import { ClientWorldManifest } from './object/world/client-world-manifest'; import { ClientWorldLoader } from './object/world/client-world-loader'; import { LoadingManagerWrapper } from './object/resource/loading-manager'; export class Game { public players: (Player | PlayerEntity)[] = []; public player!: Player; public me!: IcyNetUser; public thirdPersonCamera!: ThirdPersonCamera; public joystick!: Joystick; public chat!: Chat; private _loading = LoadingManagerWrapper.getInstance(); private character: CharacterPacket = {}; private party: string[] = []; private _locked = false; public world!: ClientWorld; public renderer = new Renderer(); private videoTest = new VideoPlayer(24, 12); constructor(public socket: Socket) {} async initialize(): Promise { this._loading.initialize(); const worldManifest = await ClientWorldManifest.loadManifest(); this.world = new ClientWorld(new ClientWorldLoader(), worldManifest); const cube = await CubeMap.load('/assets/skybox/default'); const grasstex = await BaseTexture.load( '/assets/terrain/decoration/grass01.png', ); const flowertex = await BaseTexture.load( '/assets/terrain/decoration/flowers02.png', ); const flowertex2 = await BaseTexture.load( '/assets/terrain/decoration/flowers01.png', ); await PonyModelLoader.getInstance().initialize(); await PonyEyes.getInstance().initialize(); await this.world.initialize(); this.renderer.initialize(); this.bindSocket(); this.chat = new Chat(); this.chat.initialize(); this.socket.connect(); // experimental this.videoTest.initialize(); this.videoTest.mesh.position.set(0, 22, 20); this.videoTest.mesh.rotateY(Math.PI / 2); this.renderer.scene.add(this.videoTest.mesh); this.party = (localStorage.getItem('party')?.split('|') || []).filter( (item) => item, ); // end of this.chat.registerSendFunction((message) => { this.socket.emit('chat-send', message); }); this.renderer.registerUpdateFunction((dt: number) => { this.update(dt); }); this.renderer.scene.add(this.world.world); this.renderer.scene.background = cube.texture; // test const grassfield = Grass.getInstance().createGrassPatch( new Vector3(10, 0, 10), 8, 0.5, 8, this.world.getHeight.bind(this.world), ); const flowerfield = Grass.getInstance().createGrassPatch( new Vector3(10, 0, 10), 8, 4, 3, this.world.getHeight.bind(this.world), ); const flowerfield2 = Grass.getInstance().createGrassPatch( new Vector3(8, 0, 8), 4, 4, 3, this.world.getHeight.bind(this.world), ); const grass = Grass.getInstance().createInstance( grassfield, new MeshBasicMaterial({ side: DoubleSide, map: grasstex.texture, alphaTest: 0.7, }), ); const flowers = Grass.getInstance().createInstance( flowerfield, new MeshBasicMaterial({ side: DoubleSide, map: flowertex.texture, alphaTest: 0.7, }), ); const flowers2 = Grass.getInstance().createInstance( flowerfield2, new MeshBasicMaterial({ side: DoubleSide, map: flowertex2.texture, alphaTest: 0.7, }), ); this.renderer.scene.add(grass); this.renderer.scene.add(flowers); this.renderer.scene.add(flowers2); this._loading.isConnecting(); window.addEventListener('keyup', (ev) => { if (ev.key === 'Shift') { this.toggleCamLock(); } }); } public toggleCamLock(): boolean { this._locked = !this._locked; this.thirdPersonCamera?.setLock(this._locked); this.player?.setCameraLock(this._locked); return this._locked; } public dispose() { this.players.forEach((player) => { player.dispose(); this.renderer.scene.remove(player.container); }); this.thirdPersonCamera?.dispose(); this.joystick?.dispose(); this.players.length = 0; } public update(dt: number) { this.players.forEach((player) => player.update(dt)); this.player?.createPacket(this.socket); this.thirdPersonCamera?.update(dt); this.joystick?.update(dt); this.player && this.world?.update(this.player.container.position); } bindSocket() { this.socket.on('connect', () => { this.dispose(); this._loading.connected(); console.log('connected'); }); this.socket.on('error.duplicate', () => { this._loading.showError( 'Error: You are already connected on another device!', ); }); this.socket.on('me', (user) => { console.log(user); if (!user) { this._loading.showError('Error: You need to log in!'); window.location.href = '/login'; return; } this.me = user; this.chat.addMessage( `Welcome to Icy3D World Experiment, ${user.display_name}!`, null, { color: '#fbff4e', }, ); const player = Player.fromUser(user, this.renderer.scene); user.character && player.setCharacter(user.character); user.position && player.container.position.fromArray(user.position); user.rotation && player.container.rotation.fromArray(user.rotation); player.setHeightSource(this.world); this.players.push(player); this.player = player; this.thirdPersonCamera = new ThirdPersonCamera( this.renderer.camera, this.player.container, this.renderer.canvas, ); this.thirdPersonCamera.initialize(); this.thirdPersonCamera.registerAltMoveFunction((x, y) => { this.player.angularVelocity.set( 0, clamp(x * 0.5, -Math.PI, Math.PI), 0, ); }); this.joystick = new Joystick(player); this.joystick.initialize(); this.joystick.addButton(-60, -20, 'LOCK', () => this.toggleCamLock()); this.joystick.addButton(135, -20, 'JUMP', () => this.player.jump()); if (isMobileOrTablet()) { this.joystick.show(); } this.socket.emit('character', this.character); }); this.socket.on('playerjoin', (user) => { if (user.id === this.me.id) { return; } const newplayer = PlayerEntity.fromUser(user, this.renderer.scene); newplayer.setHeightSource(this.world); this.chat.addMessage(`${user.display_name} has joined the game.`, null, { color: '#fbff4e', }); this.players.push(newplayer); }); this.socket.on('playerleave', (user) => { const findPlayer = this.players.find((item) => item.user.id === user.id); if (findPlayer) { this.chat.addMessage(`${user.display_name} has left the game.`, null, { color: '#fbff4e', }); this.renderer.scene.remove(findPlayer.container); findPlayer.dispose(); this.players.splice(this.players.indexOf(findPlayer), 1); } }); this.socket.on('players', (list: CompositePacket[]) => { list.forEach((player) => { if (player.id === this.me.id) { return; } const newplayer = PlayerEntity.fromUser(player, this.renderer.scene); newplayer.setHeightSource(this.world); newplayer.addUncommittedChanges(player); this.players.push(newplayer); }); this.chat.addMessage( `List of players: ${list.map((user) => user.display_name).join(', ')}`, null, { color: '#fbff4e', }, ); }); this.socket.on('playerupdate', (data) => { data.forEach((item) => { const player = this.players.find( (player) => player.user.id === item.id, ); if ( player && player instanceof PlayerEntity && player.user.id !== this.me.id ) { player.addUncommittedChanges(item); } }); }); 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, ); if (player && player.user.id !== this.me.id) { (player as PlayerEntity).addChat(event.message); } this.chat.addMessage(event.message, event.sender.display_name); // experimental stuff this.experimentalPlayerCmd(event.message, event.sender); }); this.socket.on('disconnect', () => { this.chat.addMessage( `Disconnected from the server, reconnecting..`, null, { color: '#ff0000', }, ); }); } private experimentalPlayerCmd(message: string, sender: IcyNetUser) { 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; } this.character = { ...this.character, color }; this.player.setColor(color); this.socket.emit('character', this.character); } if (message.startsWith('!eyecolor')) { const [cmd, eyeColor] = message.split(' '); try { const colorr = new Color(eyeColor); if (!colorr) { throw 'invalid'; } } catch (e: any) { this.chat.addMessage('Invalid color.'); return; } this.character = { ...this.character, eyeColor }; this.player.setEyeColor(eyeColor); this.socket.emit('character', this.character); localStorage.setItem('character', JSON.stringify(this.character)); } if (message.startsWith('!party')) { const array = message.split(' '); const name = array.slice(2).join(' '); if (array[1] === 'join') { this.party.push(name); this.chat.addMessage(`Joined party of user "${name}".`); } if (array[1] === 'leave') { this.party.splice(this.party.indexOf(name), 1); this.chat.addMessage(`Left party of user "${name}".`); } 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 ( !( sender.display_name === this.me.display_name || this.party.includes(sender.display_name) ) ) { return; } if (message.startsWith('!play')) { const [cmd, src] = message.split(' '); if (src) { this.videoTest.setSource(src, true); } else { this.videoTest.play(); } return; } if (message.startsWith('!stop') || message.startsWith('!pause')) { this.videoTest.stop(); return; } if (message.startsWith('!volume')) { const [cmd, vol] = message.split(' '); if (!vol) { this.chat.addMessage( `Current volume: ${Math.floor(this.videoTest.video.volume * 100)}`, ); return; } this.videoTest.setVolume(parseInt(vol.replace('%', ''), 10)); } } }