UNPKG

gamepeer-js

Version:

Peer-to-peer gaming JS library with a ton of capabilities

2 lines (1 loc) 29.7 kB
class e{constructor({game:e,connectionManager:t,playerId:i,debug:s=!1,...o}){this.debug=s,this.log=s?console.log.bind(console,"[KeyboardController]"):()=>{},this.customBindings=new Map,this.eventHandlers={up:[],down:[],left:[],right:[],space:[],enter:[],keydown:[],keyup:[],error:[]},this.game=e,this.connectionManager=t,this.playerId=i,this.standardKeys={ArrowUp:"up",ArrowDown:"down",ArrowLeft:"left",ArrowRight:"right"," ":"space",Enter:"enter"},o.keybindings&&o.keybindings.forEach((([e,t])=>{this.customBindings.set(t,e)})),this._handleKeyDown=this._handleKeyDown.bind(this),this._handleKeyUp=this._handleKeyUp.bind(this),this._handleIncomingData=this._handleIncomingData.bind(this),this.log(`Initializing with playerId: ${this.playerId}`),this._setupEventListeners()}on(e,t){return this.eventHandlers[e]||(this.eventHandlers[e]=[]),this.eventHandlers[e].push(t),this}_setupEventListeners(){try{window.addEventListener("keydown",this._handleKeyDown,!0),window.addEventListener("keyup",this._handleKeyUp,!0),this.connectionManager&&this.connectionManager.on("data",this._handleIncomingData),this.log("Event listeners registered successfully.")}catch(e){this._triggerError("Failed to setup event listeners",e)}}_handleKeyDown(e){this.log(`Key Down: ${e.code}`);const t=this._getActionForKey(e.code);if(!t)return;["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.code)&&e.preventDefault();const i={action:"down",key:e.code,keyName:t,altKey:e.altKey,ctrlKey:e.ctrlKey,shiftKey:e.shiftKey};this.log(`Triggering action: ${t}`,i)}_handleKeyUp(e){this.log(`Key Up: ${e.code}`);const t=this._getActionForKey(e.code);if(!t)return;const i={action:"up",key:e.code,keyName:t,altKey:e.altKey,ctrlKey:e.ctrlKey,shiftKey:e.shiftKey,playerId:this.playerId};this.log(`Releasing action: ${t}`,i),this._triggerEvent(t,i)}_handleIncomingData({data:e}){if("keyboardEvent"===e.type){if(this.log("Received keyboard event from peer:",e),e.data.playerId===this.playerId)return void this.log(`Ignoring self-broadcasted event: ${e.event}`);this._triggerEvent(e.event,e.data)}}_getActionForKey(e){return this.customBindings.get(e)||this.standardKeys[e]}_broadcast(e,t){if(this.log(`Broadcasting: ${e}`,t),this.connectionManager&&this.connectionManager.connections.size>0){const i={type:"keyboardEvent",event:e,data:{...t,playerId:this.playerId,timestamp:Date.now()}};try{this.connectionManager.broadcast(i),this.log("Broadcast successful.")}catch(e){this._triggerError("Broadcast failed",e)}}else this.log("No peers to broadcast to.")}_triggerEvent(e,t){this.log(`Dispatching event: ${e}`,t),this.eventHandlers[e]&&this.eventHandlers[e].forEach((e=>e(t))),t.playerId===this.game.localPlayerId&&(this.log(`Ignoring self-broadcasted event: ${t.event}`),this._broadcast(e,t))}_triggerError(e,t){console.error(`[KeyboardController] Error: ${e}`,t),this.eventHandlers.error&&this.eventHandlers.error.forEach((i=>i({message:e,error:t})))}destroy(){try{window.removeEventListener("keydown",this._handleKeyDown,!0),window.removeEventListener("keyup",this._handleKeyUp,!0),this.connectionManager&&this.connectionManager.off("data",this._handleIncomingData),this.log("Destroyed successfully.")}catch(e){console.error("[KeyboardController] Error during destroy:",e)}}}class t{constructor({debug:e=!1,...t}={}){this.debug=e,this.log=e?console.log.bind(console,"[MouseController]"):()=>{},this.connectionManager=t.connectionManager,this.x=0,this.y=0,this.isDown=!1,this.defaultEvents=["mousemove","mousedown","mouseup","click","rightclick"],this.customBindings=new Map,this.eventHandlers={...Object.fromEntries(this.defaultEvents.map((e=>[e,[]]))),error:[]}}setup(e){try{this.element=e,e.addEventListener("mousemove",(e=>this._handleMove(e))),e.addEventListener("mousedown",(e=>this._handleDown(e))),e.addEventListener("mouseup",(e=>this._handleUp(e))),e.addEventListener("click",(e=>this._handleClick(e))),e.addEventListener("contextmenu",(e=>{e.preventDefault(),this._handleRightClick(e)}))}catch(e){this._triggerError("Failed to setup mouse controller",e)}}addBinding(e,t){this.customBindings.set(t,e)}_handleMove(e){try{const t=e.target.getBoundingClientRect();this.x=e.clientX-t.left,this.y=e.clientY-t.top,this._triggerEvent("mousemove",{x:this.x,y:this.y,event:e}),this._broadcast("mousemove",{x:this.x,y:this.y,event:e})}catch(e){this._triggerError("Mouse move error",e)}}_handleDown(e){try{this.isDown=!0;const t=this.customBindings.get(e.button)||"mousedown";this._triggerEvent(t,{x:this.x,y:this.y,event:e}),this._broadcast(t,{x:this.x,y:this.y,event:e})}catch(e){this._triggerError("Mouse down error",e)}}_handleUp(e){try{this.isDown=!1;const t=this.customBindings.get(e.button)||"mouseup";this._triggerEvent(t,{x:this.x,y:this.y,event:e}),this._broadcast(t,{x:this.x,y:this.y,event:e})}catch(e){this._triggerError("Mouse up error",e)}}_handleClick(e){try{const t=this.customBindings.get(e.button)||"click";this._triggerEvent(t,{x:this.x,y:this.y,event:e}),this._broadcast(t,{x:this.x,y:this.y,event:e})}catch(e){this._triggerError("Mouse click error",e)}}_handleRightClick(e){try{this._triggerEvent("rightclick",{x:this.x,y:this.y,event:e}),this._broadcast("rightclick",{x:this.x,y:this.y,event:e})}catch(e){this._triggerError("Right click error",e)}}_broadcast(e,t){try{this.connectionManager&&this.connectionManager.broadcast({type:"mouseEvent",event:e,data:t})}catch(e){this._triggerError("Broadcast error",e)}}on(e,t){this.eventHandlers[e]?this.eventHandlers[e].push(t):this._triggerError(`Invalid event: ${e}`)}_triggerEvent(e,t){this.eventHandlers[e]?.forEach((e=>e(t)))}_triggerError(e,t){this.log(`Error: ${e}`,t),this.eventHandlers.error?.forEach((i=>i({message:e,error:t})))}destroy(){this.element&&(this.element.removeEventListener("mousemove",this._handleMove),this.element.removeEventListener("mousedown",this._handleDown),this.element.removeEventListener("mouseup",this._handleUp),this.element.removeEventListener("click",this._handleClick),this.element.removeEventListener("contextmenu",this._handleRightClick))}}class i{constructor(e={}){this.peer=null,this.connections=new Map,this.eventHandlers={connection:[],disconnection:[],data:[],error:[]},this.peerOptions=e}async createPeer(e){return this.peer=new Peer(e||void 0,this.peerOptions),new Promise(((e,t)=>{this.peer.on("open",(t=>{e(t)})),this.peer.on("error",t)}))}async connect(e,t={}){const i=this.peer.connect(e,t);return new Promise(((e,t)=>{i.on("open",(()=>{this._setupConnection(i),e(i)})),i.on("error",t)}))}onConnection(e){this.peer.on("connection",(t=>{this._setupConnection(t),e(t)}))}_setupConnection(e){this.connections.set(e.peer,e),e.on("data",(t=>{this._triggerEvent("data",{conn:e,data:t})})),e.on("close",(()=>{this.connections.delete(e.peer),this._triggerEvent("disconnection",{peerId:e.peer})}))}send(e,t){const i=this.connections.get(e);i&&i.send(t)}broadcast(e){this.connections.forEach((t=>t.send(e)))}on(e,t){this.eventHandlers[e]??=[],this.eventHandlers[e].push(t)}_triggerEvent(e,t){this.eventHandlers[e]?.forEach((e=>e(t)))}destroy(){this.peer?.destroy()}}class s{constructor({game:e,connectionManager:t,playerId:s,debug:o=!1,...r}){this.options={heartbeatInterval:3e4,...r},this.debug=o,this.log=o?console.log.bind(console,"[MatchmakingService]"):()=>{},this.scores=[0],this.players=1,this.log("options match: ",r),this.gameName=r.gameName||"Untitled Game",this.gameMode=r.gameMode||"standard",this.isPrivate=r.isPrivate||!1,this.hasPassword=r.hasPassword||!1,this.region=r.region||this._detectRegion(),this.maxPlayers=r.maxPlayers,this.peerManager=new i,this.availableRooms=new Map,this.ownRoom=null,this.heartbeatTimer=null,this.clientId=s,this.id=null,this.registerRoom=this.registerRoom,this.updateRoom=this.updateRoom,this.connectionManager=t,this._handleIncomingData=this._handleIncomingData.bind(this),this.eventHandlers={roomsUpdated:[],error:[]},this.peerManager.on("data",(({data:e})=>{"roomUpdate"===e.type&&this._handleRoomUpdate(e.room)})),this._init()}async _init(){try{return this.log("[_init] listening peers: ",this.clientId),await this.connectionManager.createPeer(this.clientId),this.addPlayer(),this.connectionManager&&this.connectionManager.on("data",this._handleIncomingData),this._startHeartbeat(),!0}catch(e){return this._triggerEvent("error",{message:"Failed to initialize matchmaking service",error:e}),!1}}async registerRoom(e){if(!this.clientId)throw new Error("Matchmaking service not initialized");this.scores=[0],this.players=1;const t={id:e,host:this.clientId,createdAt:(new Date).toISOString(),players:this.players,maxPlayers:this.maxPlayers,gameName:this.gameName,gameMode:this.gameMode,isPrivate:this.isPrivate,hasPassword:this.hasPassword,region:this.region,scores:this.scores};return this.log("initializing match: ",t),this.password&&(t.password=this.password),this.ownRoom=t,this.availableRooms.set(e,t),this._triggerEvent("roomsUpdated",{rooms:Array.from(this.availableRooms.values())}),!0}async updateRoom(e={}){if(!this.ownRoom)throw new Error("No room registered");return this.ownRoom={...this.ownRoom,...e},this.availableRooms.set(this.ownRoom.id,this.ownRoom),this.peerManager.broadcast({type:"roomUpdate",room:this.ownRoom}),this._triggerEvent("roomsUpdated",{rooms:Array.from(this.availableRooms.values())}),!0}async unregisterRoom(){return!this.ownRoom||(this.availableRooms.delete(this.ownRoom.id),this.peerManager.broadcast({type:"roomRemoval",roomId:this.ownRoom.id}),this.ownRoom=null,this._triggerEvent("roomsUpdated",{rooms:Array.from(this.availableRooms.values())}),!0)}async refreshRooms(){return Array.from(this.availableRooms.values())}findRooms(e={}){return Array.from(this.availableRooms.values()).filter((t=>{for(const[i,s]of Object.entries(e))if(t[i]!==s)return!1;return!0}))}async joinRoom(e,t=null){const i=this.availableRooms.get(e);if(!i)throw new Error("Room not found");if(i.hasPassword&&!t)throw new Error("Password required");return{id:i.id,host:i.host,password:t}}_handleIncomingData({data:e}){if("keyboardEvent"===e.type){if(this.log("Received keyboard event from peer:",e),e.data.playerId===this.playerId)return void this.log(`Ignoring self-broadcasted event: ${e.event}`);this._triggerEvent(e.event,e.data)}}_handleRoomUpdate(e){e.host!==this.clientId&&(this.availableRooms.set(e.id,e),this._triggerEvent("roomsUpdated",{rooms:Array.from(this.availableRooms.values())}))}_handleRoomRemoval(e){this.ownRoom?.id!==e&&(this.availableRooms.delete(e),this._triggerEvent("roomsUpdated",{rooms:Array.from(this.availableRooms.values())}))}on(e,t){return this.eventHandlers[e]||(this.eventHandlers[e]=[]),this.eventHandlers[e].push(t),this}off(e,t){return this.eventHandlers[e]&&(this.eventHandlers[e]=this.eventHandlers[e].filter((e=>e!==t))),this}destroy(){this.heartbeatTimer&&clearInterval(this.heartbeatTimer),this.ownRoom&&this.unregisterRoom().catch(console.error),this.connectionManager&&this.connectionManager.off("data",this._handleIncomingData),this.peerManager.destroy()}_startHeartbeat(){this.heartbeatTimer&&clearInterval(this.heartbeatTimer),this.heartbeatTimer=setInterval((()=>{const e=Date.now();for(const[t,i]of this.availableRooms){e-new Date(i.createdAt).getTime()>3e5&&i.host!==this.clientId&&this.availableRooms.delete(t)}this._triggerEvent("roomsUpdated",{rooms:Array.from(this.availableRooms.values())})}),this.options.heartbeatInterval)}_triggerEvent(e,t){this.eventHandlers[e]&&this.eventHandlers[e].forEach((i=>{try{i(t),this._broadcast(e,t)}catch(e){this.log("Error in event handler:",e)}}))}_broadcast(e,t){if(this.log(`Broadcasting: ${e}`,t),this.log("Broadcasting connectionManager: ",this.connectionManager),this.connectionManager&&this.connectionManager.connections.size>0){const i={type:"roomsUpdated",event:e,data:{...t,playerId:this.playerId,timestamp:Date.now()}};try{this.connectionManager.broadcast(i),this.log("Broadcast successful.")}catch(e){this._triggerError("Broadcast failed",e)}}else this.log("No peers to broadcast to.")}addPlayer(){return this.players++,this.scores.push(0),this.log("[addPlayer] players:",this.players,"scores:",this.scores),this.updateRoom({players:this.players,scores:this.scores}),this.players-1}updateScore(e,t=1){if(this.log("[updateScore] start:",e,t,"current scores:",this.scores),(!this.scores||this.scores.length<this.players)&&(this.scores=Array(this.players).fill(0)),e>=0&&e<this.scores.length){const i=[...this.scores];return i[e]+=t,this.scores=i,this.updateRoom({scores:this.scores,lastUpdate:Date.now()}),this.log("[updateScore] success:",this.scores),!0}return this.log("[updateScore] invalid playerIndex:",e),!1}getScores(){return this.scores}_detectRegion(){const e=(new Date).getTimezoneOffset();return e>=240&&e<=300?"na-east":e>300&&e<=480?"na-west":e>=-60&&e<=60?"eu":e>=-660&&e<=-480?"asia-pacific":"global"}}class o{constructor({debug:e=!1,...t}={}){this.debug=e,this.log=e?console.log.bind(console,"[VoiceChatManager]"):()=>{},this.options={enableVideo:!1,autoConnect:!1,muted:!1,maxBitrate:128,echoCancellation:!0,noiseSuppression:!0,...t},this.peer=null,this.localStream=null,this.connections=new Map,this.remoteStreams=new Map,this.audioElements=new Map,this.videoElements=new Map,this.eventHandlers={streamConnected:[],streamDisconnected:[],localStreamReady:[],error:[]},this.audioContext=null,this.audioNodes=new Map}async init(e){return this.peer=e,this.peer.on("call",(e=>{this._handleIncomingCall(e)})),this.options.autoConnect&&await this._getLocalStream(),!0}async startBroadcasting(){try{return await this._getLocalStream(),!0}catch(e){return this._triggerEvent("error",{message:"Failed to start broadcasting",error:e}),!1}}stopBroadcasting(){this.localStream&&(this.localStream.getTracks().forEach((e=>e.stop())),this.localStream=null),this.connections.forEach((e=>{e.close()})),this.connections.clear()}async callPeer(e){if(this.localStream||await this._getLocalStream(),!this.peer||!this.localStream)throw new Error("Voice chat not properly initialized");if(this.connections.has(e))this.log(`Already connected to ${e}`);else try{const t=this.peer.call(e,this.localStream);return this._setupCallEvents(t),this.connections.set(e,t),!0}catch(t){return this._triggerEvent("error",{message:`Failed to call peer ${e}`,error:t}),!1}}hangUp(e){const t=this.connections.get(e);return!!t&&(t.close(),this.connections.delete(e),this._removeMediaElements(e),!0)}hangUpAll(){this.connections.forEach(((e,t)=>{e.close(),this._removeMediaElements(t)})),this.connections.clear()}setMuted(e){if(this.localStream){return this.localStream.getAudioTracks().forEach((t=>{t.enabled=!e})),this.options.muted=e,!0}return!1}setVideoEnabled(e){if(this.localStream){return this.localStream.getVideoTracks().forEach((t=>{t.enabled=e})),this.options.enableVideo=e,!0}return!1}getLocalVideoElement(){const e=document.createElement("video");return e.muted=!0,e.autoplay=!0,this.localStream&&(e.srcObject=this.localStream),e}createMediaElements(e){this.audioElements.forEach((e=>{e.parentNode&&e.parentNode.removeChild(e)})),this.videoElements.forEach((e=>{e.parentNode&&e.parentNode.removeChild(e)})),this.audioElements.clear(),this.videoElements.clear(),this.remoteStreams.forEach(((t,i)=>{this._createMediaElementsForPeer(i,t,e)}))}applyAudioEffect(e,t){this.audioContext||(this.audioContext=new(window.AudioContext||window.webkitAudioContext));const i=this.remoteStreams.get(e);if(!i)return!1;const s=this.audioNodes.get(e);s&&s.forEach((e=>{try{e.disconnect()}catch(e){console.error("Error disconnecting audio node:",e)}}));const o=this.audioContext.createMediaStreamSource(i),r=this.audioContext.createMediaStreamDestination(),n=[o];switch(t){case"pitch-up":const h=this.audioContext.createBiquadFilter();h.type="highshelf",h.frequency.value=1e3,h.gain.value=15,o.connect(h),h.connect(r),n.push(h);break;case"pitch-down":const l=this.audioContext.createBiquadFilter();l.type="lowshelf",l.frequency.value=1e3,l.gain.value=15,o.connect(l),l.connect(r),n.push(l);break;case"robot":const c=this.audioContext.createWaveShaper();function d(e){const t=e,i=44100,s=new Float32Array(i);for(let e=0;e<i;++e){const o=2*e/i-1;s[e]=(Math.PI+t)*o/(Math.PI+t*Math.abs(o))}return s}c.curve=d(400),c.oversample="4x",o.connect(c),c.connect(r),n.push(c);break;default:o.connect(r)}this.audioNodes.set(e,n);const a=this.audioElements.get(e);return a&&(a.srcObject=r.stream),!0}on(e,t){return this.eventHandlers[e]&&this.eventHandlers[e].push(t),this}off(e,t){return this.eventHandlers[e]&&(this.eventHandlers[e]=this.eventHandlers[e].filter((e=>e!==t))),this}destroy(){this.stopBroadcasting(),this.audioElements.forEach((e=>{e.parentNode&&e.parentNode.removeChild(e)})),this.videoElements.forEach((e=>{e.parentNode&&e.parentNode.removeChild(e)})),this.audioElements.clear(),this.videoElements.clear(),this.audioContext&&"closed"!==this.audioContext.state&&this.audioContext.close().catch(console.error)}async _getLocalStream(){try{const e={audio:{echoCancellation:this.options.echoCancellation,noiseSuppression:this.options.noiseSuppression},video:this.options.enableVideo};if(this.localStream=await navigator.mediaDevices.getUserMedia(e),this.options.muted){this.localStream.getAudioTracks().forEach((e=>{e.enabled=!1}))}return this._triggerEvent("localStreamReady",{stream:this.localStream}),!0}catch(e){throw this._triggerEvent("error",{message:"Failed to access microphone/camera",error:e}),e}}_handleIncomingCall(e){this.log(`Incoming call from ${e.peer}`);(async()=>{if(!this.localStream)try{await this._getLocalStream()}catch(e){return void console.error("Failed to get local stream for answering call",e)}e.answer(this.localStream),this._setupCallEvents(e),this.connections.set(e.peer,e)})()}_setupCallEvents(e){e.on("stream",(t=>{this.log(`Received stream from ${e.peer}`),this.remoteStreams.set(e.peer,t),this._createMediaElementsForPeer(e.peer,t),this._triggerEvent("streamConnected",{peerId:e.peer,stream:t})})),e.on("close",(()=>{this.log(`Call with ${e.peer} ended`),this.connections.delete(e.peer),this.remoteStreams.delete(e.peer),this._removeMediaElements(e.peer),this._triggerEvent("streamDisconnected",{peerId:e.peer})})),e.on("error",(t=>{console.error(`Call error with ${e.peer}:`,t),this._triggerEvent("error",{message:`Call error with ${e.peer}`,peerId:e.peer,error:t})}))}_createMediaElementsForPeer(e,t,i=document.body){const s=document.createElement("audio");s.autoplay=!0,s.srcObject=t,s.id=`audio-${e}`,s.style.display="none",i.appendChild(s),this.audioElements.set(e,s);if(t.getVideoTracks().length>0){const s=document.createElement("video");s.autoplay=!0,s.srcObject=t,s.id=`video-${e}`,s.className="peer-video",s.style.width="160px",s.style.height="120px",s.style.objectFit="cover",s.style.margin="5px",s.style.borderRadius="8px",i.appendChild(s),this.videoElements.set(e,s)}}_removeMediaElements(e){const t=this.audioElements.get(e);t&&t.parentNode&&(t.srcObject=null,t.parentNode.removeChild(t)),this.audioElements.delete(e);const i=this.videoElements.get(e);i&&i.parentNode&&(i.srcObject=null,i.parentNode.removeChild(i)),this.videoElements.delete(e);const s=this.audioNodes.get(e);s&&(s.forEach((e=>{try{e.disconnect()}catch(e){}})),this.audioNodes.delete(e))}_triggerEvent(e,t){this.eventHandlers[e]&&this.eventHandlers[e].forEach((e=>{try{e(t)}catch(e){console.error("Error in event handler:",e)}}))}}class r{constructor(e){if(!e)throw new Error("Game instance required");this.game=e,this.debug=e.options.debug,this.log=this.debug?console.log.bind(console,"[ServicesInitializer]"):()=>{},this.game.services=this,this._initServices()}_initServices(){this.log("initializing services: ",this.game),this.game.options.useKeyboardController&&(this.keyboardController=new e({game:this.game,connectionManager:this.game.connectionManager,playerId:this.game.localPlayerId,debug:this.game.options.debug,...this.game.options.keyboardOptions}),this.log("Keyboard controller initialized: ",this.keyboardController),this.game.keyboardController=this.keyboardController),this.game.options.useMouseController&&(this.mouseController=new t({connectionManager:this.game.connectionManager,debug:this.game.options.debug,...this.game.options.mouseOptions}),this.game.mouseController=this.mouseController,this.log("Mouse controller initialized: ",this.mouseController)),this.game.options.useMatchmaking&&(this.matchmaking=new s({connectionManager:this.game.connectionManager,...this.game.options.matchmakingOptions,game:this.game,playerId:this.game.localPlayerId,debug:this.game.options.debug}),this.matchmaking.on("roomsUpdated",(e=>{this.game._triggerEvent("roomsUpdated",e)})),this.game.matchmaking=this.matchmaking,this.log("Matchmaking service initialized: ",this.matchmaking)),this.game.options.useVoiceChat&&(this.voiceChat=new o({...this.game.options.voiceChatOptions,debug:this.game.options.debug}),this.voiceChat.on("connected",(e=>{this.game._triggerEvent("voiceChatConnected",{peerId:e})})),this.voiceChat.on("disconnected",(e=>{this.game._triggerEvent("voiceChatDisconnected",{peerId:e})})),this.game.voiceChat=this.voiceChat,this.log("Voice chat service initialized: ",this.voiceChat))}getKeyboardController(){if(this.game.options.useKeyboardController){if(!this.game.clientId)throw new Error("KeyboardController requires active game connection");return this.keyboardController}this.log("Keyboard controller not enabled in options")}getMouseController(){if(this.game.options.useMouseController){if(!this.game.clientId)throw new Error("MouseController requires active game connection");return this.mouseController}this.log("Mouse controller not enabled in options")}getMatchmakingService(){if(this.game.options.useMatchmaking){if(!this.game.clientId)throw new Error("Matchmaking requires active game connection");return this.matchmaking}this.log("Matchmaking service not enabled in options")}getVoiceChatManager(){if(this.game.options.useVoiceChat){if(!this.game.clientId)throw new Error("VoiceChat requires active game connection");return this.voiceChat}this.log("VoiceChat not enabled in options")}destroy(){this.keyboardController&&this.keyboardController.destroy(),this.mouseController&&this.mouseController.destroy(),this.voiceChatController&&this.voiceChatController.destroy(),this.matchmakingController&&this.matchmakingController.destroy(),this.keyboardController=null,this.mouseController=null,this.matchmakingController=null,this.voiceChatController=null}}class n{constructor(){this.state={}}set(e,t){this.state[e]=t}get(e){return this.state[e]}getFullState(){return this.state}updateState(e){Object.assign(this.state,e)}}class a{constructor(e,t={}){if(!e)throw new Error("GamePeer instance is required");this.gamePeer=e,this.options={defaultPlayerName:"Player",defaultX:0,defaultY:0,colorPalette:["#FF5733","#33FF57","#3357FF","#F3FF33","#FF33F3","#33FFF3","#FF33A8","#8A33FF","#33FF8A","#FF8A33"],...t},this.players={},this.initialized=!1,this.localPlayerId=null}async initialize(){if(!this.initialized){if(!this.gamePeer.connectionManager.peer)throw new Error("Cannot initialize player - connection not established");if(!this.gamePeer.clientId)throw new Error("Cannot initialize player - missing client it");this.localPlayerId=`player_${this.gamePeer.clientId}`,this.initialized=!0}}async createLocalPlayer(){if(!this.initialized)throw new Error("PlayerInitializer not initialized - call initialize() first");if(!this.localPlayerId)throw new Error("Missing localPlayerId");if(!this.gamePeer.connectionManager.peer.open)throw new Error("Cannot create player - connection not ready");const e={name:`${this.options.defaultPlayerName} ${this.gamePeer.clientId.substr(0,5)}`,x:this._getRandomPosition(this.options.defaultX),y:this._getRandomPosition(this.options.defaultY),color:this._getRandomColor(),id:this.localPlayerId};if(!this._validatePlayerData(e))throw new Error("Invalid player data generated");return this.players[this.localPlayerId]=e,e}getLocalPlayer(){return this.players[this.localPlayerId]||null}_validatePlayerData(e){return e&&"object"==typeof e&&"string"==typeof e.name&&"number"==typeof e.x&&"number"==typeof e.y&&"string"==typeof e.color&&"string"==typeof e.id}_getRandomPosition(e){return e+Math.floor(200*Math.random()-100)}_getRandomColor(){return this.options.colorPalette[Math.floor(Math.random()*this.options.colorPalette.length)]}destroy(){this.players={},this.initialized=!1,this.localPlayerId=null}}function h(){return new Promise((e=>{if(window.Peer)return e();const t=document.createElement("script");t.src="https://cdnjs.cloudflare.com/ajax/libs/peerjs/1.4.7/peerjs.min.js",t.onload=e,document.head.appendChild(t)}))}class l{constructor(e={}){this.options={debug:!1,tickRate:20,peerOptions:{},useMatchmaking:!1,matchmakingOptions:{},useVoiceChat:!1,voiceChatOptions:{},useKeyboardController:!1,keyboardOptions:{keybindings:[]},useMouseController:!1,mouseOptions:{},...e},this._setupLogger=()=>{this.log=this.options.debug?console.log.bind(console,"[GamePeerJS]"):()=>{}},this.connectionManager=new i(this.options.peerOptions),this.gameState=new n,this.playerInitializer=new a(this),this.isHost=!1,this.clientId=null,this.lastUpdateTime=0,this.tickInterval=null,this.players={},this.gameObjects={},this.localPlayerId=null,this.eventHandlers={connection:[],disconnection:[],stateUpdate:[],error:[],roomsUpdated:[],voiceChatConnected:[],voiceChatDisconnected:[]},this._setupLogger()}_initServiceInstances(){this.services=new r(this)}getKeyboardController(){return this.services?.getKeyboardController()}getMouseController(){return this.services?.getMouseController()}getMatchmakingService(){return this.services?.getMatchmakingService()}getVoiceChatManager(){return this.services?.getVoiceChatManager()}async hostGame(e=null,t={}){await h(),this.isHost=!0;const i=e||this._generateRoomId();try{this.clientId=await this.connectionManager.createPeer(i),this.localPlayerId=`player_${this.clientId}`,this.log(`Initialized as host with ID: ${this.clientId}`),this.connectionManager.onConnection((e=>{this._handleNewConnection(e)})),await this.playerInitializer.initialize();const e=await this.playerInitializer.createLocalPlayer();return this.players[this.localPlayerId]=e,this.syncGameObject(this.localPlayerId,{...e,syncAll:!0}),this._initServiceInstances(),this._startGameLoop(),this.clientId}catch(e){throw this._triggerEvent("error",e),e}}async joinGame(e){await h();try{if(this.clientId=await this.connectionManager.createPeer(),this.localPlayerId=`player_${this.clientId}`,this.log(`Initialized as client with ID: ${this.clientId}`),this.voiceChat)try{await this.voiceChat.init(this.connectionManager.peer)}catch(e){this.log("Warning: Failed to initialize voice chat",e)}const t=await this.connectionManager.connect(e,{reliable:!0});this.log(`Connected to host: ${e}`),this._setupDataHandling(t),this._triggerEvent("connection",{peerId:e}),this.voiceChat&&this.voiceChat.callPeer(e).catch((e=>{this.log("Warning: Failed to connect voice chat",e)})),await this.playerInitializer.initialize();const i=await this.playerInitializer.createLocalPlayer();return this.players[this.localPlayerId]=i,this.syncGameObject(this.localPlayerId,{...i,syncAll:!0}),this._initServiceInstances(),this._requestFullState(),e}catch(e){throw this._triggerEvent("error",e),e}}createGameObject(e,t={}){const i=`obj_${Date.now()}_${Math.random().toString(36).substr(2,5)}`,s={type:e,ownerId:this.localPlayerId,...t};return this.gameObjects[i]=s,this.syncGameObject(i,s),i}syncGameObject(e,t){this.connectionManager.peer&&(e.startsWith("player_")?this.players[e]?Object.assign(this.players[e],t):this.players[e]=t:this.gameObjects[e]?Object.assign(this.gameObjects[e],t):this.gameObjects[e]=t,this.connectionManager.broadcast({type:"stateUpdate",objectId:e,data:t}))}movePlayer(e,t){if(!this.localPlayerId)return;const i=this.players[this.localPlayerId];i&&(i.x=e,i.y=t,this.syncGameObject(this.localPlayerId,{x:e,y:t}))}destroy(){this.tickInterval&&clearInterval(this.tickInterval),this.connectionManager.destroy(),this.services.destroy()}_generateRoomId(){return Math.random().toString(36).substr(2,8)}_handleNewConnection(e){this._setupDataHandling(),this._triggerEvent("connection",{peerId:e}),console.log("[_handleNewConnection] broadcasting new connection: ",e),this.broadcastEvent("playerJoined",{playerId:`player_${e.peer}`,timestamp:Date.now()})}on(e,t){return this.eventHandlers[e]||(this.eventHandlers[e]=[]),this.eventHandlers[e].push(t),this}off(e,t){return this.eventHandlers[e]&&(this.eventHandlers[e]=this.eventHandlers[e].filter((e=>e!==t))),this}_triggerEvent(e,t){this.eventHandlers[e]&&this.eventHandlers[e].forEach((e=>e(t)))}broadcastEvent(e,t){this._triggerEvent(e,t),this.connectionManager.peer&&this.connectionManager.broadcast({type:"customEvent",eventName:e,data:t})}_setupDataHandling(){this.connectionManager.on("data",(({data:e})=>{if("customEvent"===e?.type)this._triggerEvent(e.eventName,e.data);else if("stateUpdate"===e?.type&&e?.objectId){if(e.objectId.startsWith("player_"))if(this.players[e.objectId]){const t=this.players[e.objectId].color;this.players[e.objectId]={...this.players[e.objectId],...e.data,color:e.data.color||t}}else this.players[e.objectId]={name:`Player ${e.objectId.substr(7,5)}`,x:0,y:0,color:this.playerInitializer._getRandomColor(),...e.data};else this.gameObjects[e.objectId]?Object.assign(this.gameObjects[e.objectId],e.data):this.gameObjects[e.objectId]=e.data;this._triggerEvent("stateUpdate",e)}})),this.connectionManager.on("disconnection",(({peerId:e})=>{this._triggerEvent("disconnection",{peerId:e})}))}_startGameLoop(){this.tickInterval=setInterval((()=>{this.isHost&&this.connectionManager.broadcast({type:"stateUpdate",data:this.gameState.getFullState()})}),1e3/this.options.tickRate)}_requestFullState(){this.connectionManager.broadcast({type:"fullStateRequest"})}}export{l as default};