touch-coop 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,13 +23,15 @@ TouchCoop is powered by [PeerJS](https://peerjs.com/), which provides a simple A
23
23
 
24
24
  ```ts
25
25
  // Example: Using a custom PeerJS server
26
+ import { Match, Player } from "touch-coop";
26
27
  const customPeerConfig = {
27
- host: 'your-peerjs-server.com',
28
- port: 9000,
29
- path: '/peerjs'
28
+ host: 'your-peerjs-server.com',
29
+ port: 9000,
30
+ path: '/peerjs'
30
31
  };
31
32
 
32
- const match = new Match(gamePadURL, handlePlayerEvent, customPeerConfig);
33
+ const match = new Match();
34
+ await match.createLobby(gamePadURL, handlePlayerEvent, customPeerConfig);
33
35
  const player = new Player(customPeerConfig);
34
36
  ```
35
37
 
@@ -48,54 +50,56 @@ The library expects your game to use the `Match` and `Player` classes. Your game
48
50
  The main game page hosts the game and creates a `Match` instance. This page uses `createLobby()` to generate a QR code that players can scan to join the game.
49
51
 
50
52
  ```ts
51
- import { Match, PlayerEvent } from "touch-coop";
52
- import { useCallback, useState } from "react";
53
+ import { Match, type PlayerEvent } from "touch-coop";
54
+
55
+ // PlayerEvent type:
56
+ // type PlayerEvent =
57
+ // | { action: "JOIN" | "LEAVE"; playerId: string; playerName: string; timestamp: number; }
58
+ // | { action: "MOVE"; playerId: string; playerName: string; button: string; timestamp: number; }
53
59
 
54
60
  const gamePadURL = "http://localhost:8080/demos/gamepad";
55
61
 
56
62
  function handlePlayerEvent(event: PlayerEvent) {
57
63
  switch (event.action) {
58
64
  case "JOIN":
59
- console.log(
60
- `Player ${event.playerId} ${event.playerName} joined the game.`
61
- );
65
+ console.log(`Player ${event.playerId} ${event.playerName} joined the game.`);
62
66
  break;
63
67
  case "LEAVE":
64
- console.log(
65
- `Player ${event.playerId} ${event.playerName} left the game.`
66
- );
68
+ console.log(`Player ${event.playerId} ${event.playerName} left the game.`);
67
69
  break;
68
70
  case "MOVE":
69
- console.log(
70
- `Player ${event.playerId} ${event.playerName} pressed ${event.button}`
71
- );
71
+ console.log(`Player ${event.playerId} ${event.playerName} pressed ${event.button}`);
72
72
  break;
73
- }
73
+ }
74
74
  }
75
75
 
76
76
  export function Lobby() {
77
- const [dataUrl, setDataUrl] = React.useState<string | null>(null);
78
- const createLobby = useCallback(() => {
77
+ const [lobby, setLobby] = React.useState<{
78
+ dataUrl: string;
79
+ shareURL: string;
80
+ } | null>(null);
81
+
82
+ React.useEffect(() => {
79
83
  (async () => {
80
- const match = new Match(
81
- gamePadURL,
82
- handlePlayerEvent
83
- );
84
- const { dataUrl } = await match.createLobby();
85
- setDataUrl(dataUrl);
84
+ if (lobby === null) {
85
+ const match = new Match();
86
+ const { dataUrl, shareURL } = await match.createLobby(
87
+ gamePadURL, handlePlayerEvent
88
+ );
89
+ setLobby({ dataUrl, shareURL });
90
+ }
86
91
  })();
87
- }, []);
92
+ }, [lobby]);
88
93
 
89
- return dataUrl === null ? (
94
+ return lobby === null ? (
90
95
  <div>
91
- <button onClick={createLobby}>
92
- Create Lobby
93
- </button>
96
+ <button onClick={() => setLobby(null)}>Create Lobby</button>
94
97
  </div>
95
- ) :(
98
+ ) : (
96
99
  <div>
97
100
  <p>Scan the QR code below to join the game:</p>
98
- <img src={dataUrl} alt="GamePad QR Code" />
101
+ <img src={lobby.dataUrl} alt="GamePad QR Code" />
102
+ <a href={lobby.shareURL} target="_blank" rel="noopener noreferrer">Join</a>
99
103
  </div>
100
104
  );
101
105
  }
@@ -115,54 +119,82 @@ export default function GamePad() {
115
119
  const [loading, setLoading] = React.useState(true);
116
120
 
117
121
  React.useEffect(() => {
118
- (async () => {
119
- const playerName = prompt("Enter your player name:");
120
- await player.joinMatch(playerName);
121
- setLoading(false);
122
- })();
123
- }, []);
124
-
125
- if (loading) {
126
- return <div>Loading…</div>;
127
- }
128
-
129
- return (
130
- <div>
131
- <button onClick={() => player.sendMove("up")}>
132
- Up
133
- </button>
134
- <button onClick={() => player.sendMove("down")}>
135
- Down
136
- </button>
137
- <button onClick={() => player.sendMove("left")}>
138
- Left
139
- </button>
140
- <button onClick={() => player.sendMove("right")}>
141
- Right
142
- </button>
143
- <button onClick={() => player.sendMove("A")}>
144
- A
145
- </button>
146
- <button onClick={() => player.sendMove("B")}>
147
- B
148
- </button>
149
- <button onClick={() => player.sendMove("X")}>
150
- X
151
- </button>
152
- <button onClick={() => player.sendMove("Y")}>
153
- Y
154
- </button>
155
- </div>
156
- );
157
- }
158
- ```
159
-
160
- ## Live Demo
122
+ ```ts
123
+ import React from "react";
124
+ import { Player } from "touch-coop";
125
+
126
+ const player = new Player();
127
+
128
+ export default function GamePad() {
129
+ const [loading, setLoading] = React.useState(true);
130
+
131
+ React.useEffect(() => {
132
+ (async () => {
133
+ const playerName = prompt("Enter your player name:") || "Player";
134
+ await player.joinMatch(playerName);
135
+ setLoading(false);
136
+ })();
137
+ }, []);
138
+
139
+ if (loading) {
140
+ return <div>Loading…</div>;
141
+ }
142
+
143
+ return (
144
+ <div>
145
+ <button onClick={() => player.sendMove("up")}>Up</button>
146
+ <button onClick={() => player.sendMove("down")}>Down</button>
147
+ <button onClick={() => player.sendMove("left")}>Left</button>
148
+ <button onClick={() => player.sendMove("right")}>Right</button>
149
+ <button onClick={() => player.sendMove("A")}>A</button>
150
+ <button onClick={() => player.sendMove("B")}>B</button>
151
+ <button onClick={() => player.sendMove("X")}>X</button>
152
+ <button onClick={() => player.sendMove("Y")}>Y</button>
153
+ </div>
154
+ );
155
+ }
156
+ ```
161
157
 
162
158
  You can try a live demo of TouchCoop at [https://SlaneyEE.github.io/touch-coop/demos/match.html](https://SlaneyEE.github.io/touch-coop/demos/match.html).
163
159
 
164
- The demo contains a simple game where players can join by scaning a QR Code and use their mobile devices as controllers. Each player can use the on-screen buttons to send input events to the game.
160
+ The demo contains a simple game where players can join by scanning a QR Code and use their mobile devices as controllers. Each player can use the on-screen buttons to send input events to the game.
165
161
 
166
162
  The game page is [./demos/match.html](./demos/match.html). The QR code redirects players to [./demos/gamepad/index.html](./demos/gamepad/index.html).
167
163
 
168
164
  You need to run a local server to host the demo files. You can use a simple HTTP server like `http-server` or `live-server` to serve the files from the `root` directory and then access `http://localhost:8080/demos/match.html` in your browser.
165
+
166
+ ---
167
+
168
+ ### Match PUBLIC API (v3)
169
+
170
+ #### PlayerEvent type
171
+
172
+ ```ts
173
+ type PlayerEvent =
174
+ | { action: "JOIN" | "LEAVE"; playerId: string; playerName: string; timestamp: number; }
175
+ | { action: "MOVE"; playerId: string; playerName: string; button: string; timestamp: number; };
176
+ ```
177
+
178
+ #### Match constructor
179
+
180
+ ```ts
181
+ new Match(gamepadUiUrl: string, onPlayerEvent: (event: PlayerEvent) => void)
182
+ ```
183
+
184
+ #### createLobby
185
+
186
+ ```ts
187
+ async createLobby(peerConfig?: Peer.PeerOptions): Promise<{ dataUrl: string; shareURL: string; }>
188
+ ```
189
+
190
+ #### getInvitationStatus
191
+
192
+ ```ts
193
+ getInvitationStatus(playerId: string): boolean | undefined
194
+ ```
195
+
196
+ #### destroy
197
+
198
+ ```ts
199
+ destroy(): void
200
+ ```
package/demos/match.html CHANGED
@@ -22,21 +22,7 @@
22
22
  <div id="players-container"></div>
23
23
  <script src="../dist/index.global.js"></script>
24
24
  <script>
25
- const players = {}; // playerId -> { div, textarea }
26
-
27
- function createLobby() {
28
- const lobbyDiv = document.getElementById('lobby-qr');
29
- lobbyDiv.style.display = 'block';
30
- lobbyDiv.innerHTML = 'Loading...';
31
- match.createLobby().then(({ dataUrl, shareURL }) => {
32
- lobbyDiv.innerHTML = `
33
- <img class='qr' src='${dataUrl}' alt='QR Code' /><br>
34
- <a href='${shareURL}' target='_blank' style='color: white;'>Connect</a>
35
- `;
36
- }).catch(err => {
37
- lobbyDiv.innerHTML = `<span style='color:red;'>Error: ${err.message}</span>`;
38
- });
39
- }
25
+ const players = {};
40
26
 
41
27
  function handlePlayerEvent(event) {
42
28
  const playerId = event.playerId;
@@ -90,10 +76,23 @@
90
76
  }
91
77
  }
92
78
 
93
- const match = new TouchCoop.Match(
94
- "http://localhost:8080/demos/gamepad",
95
- handlePlayerEvent
96
- );
79
+ function createLobby() {
80
+ const match = new TouchCoop.Match();
81
+ const lobbyDiv = document.getElementById('lobby-qr');
82
+ lobbyDiv.style.display = 'block';
83
+ lobbyDiv.innerHTML = 'Loading...';
84
+ match.createLobby(
85
+ "http://localhost:8080/demos/gamepad",
86
+ handlePlayerEvent
87
+ ).then(({ dataUrl, shareURL }) => {
88
+ lobbyDiv.innerHTML = `
89
+ <img class='qr' src='${dataUrl}' alt='QR Code' /><br>
90
+ <a href='${shareURL}' target='_blank' style='color: white;'>Connect</a>
91
+ `;
92
+ }).catch(err => {
93
+ lobbyDiv.innerHTML = `<span style='color:red;'>Error: ${err.message}</span>`;
94
+ });
95
+ }
97
96
  </script>
98
97
  </body>
99
98
  </html>
package/dist/index.cjs CHANGED
@@ -45,4 +45,4 @@ Make sure your charset is UTF-8`);s=(s>>>8&255)*192+(s&255),t.put(s,13)}},je=n,j
45
45
  The chosen QR Code version cannot contain this amount of data.
46
46
  Minimum version required to store current data is: `+C+`.
47
47
  `);const T=E(S,v,P),b=r.getSymbolSize(S),x=new t(b);return h(x,S),g(x),_(x,S),k(x,v,0),S>=7&&M(x,S),I(x,T),isNaN(m)&&(m=o.getBestMask(x,k.bind(null,x,v))),o.applyMask(m,x),k(x,v,m),{modules:x,version:S,errorCorrectionLevel:v,maskPattern:m,segments:P}}return Te.create=function(S,v){if(typeof S>"u"||S==="")throw new Error("No input text");let m=e.M,P,C;return typeof v<"u"&&(m=e.from(v.errorCorrectionLevel,e.M),P=f.from(v.version),C=o.from(v.maskPattern),v.toSJISFunc&&r.setToSJISFunction(v.toSJISFunc)),A(S,P,m,C)},Te}var we={},Ve={},jt;function Nn(){return jt||(jt=1,(function(r){function e(n){if(typeof n=="number"&&(n=n.toString()),typeof n!="string")throw new Error("Color should be defined as hex string");let t=n.slice().replace("#","").split("");if(t.length<3||t.length===5||t.length>8)throw new Error("Invalid hex color: "+n);(t.length===3||t.length===4)&&(t=Array.prototype.concat.apply([],t.map(function(s){return[s,s]}))),t.length===6&&t.push("F","F");const i=parseInt(t.join(""),16);return{r:i>>24&255,g:i>>16&255,b:i>>8&255,a:i&255,hex:"#"+t.slice(0,6).join("")}}r.getOptions=function(t){t||(t={}),t.color||(t.color={});const i=typeof t.margin>"u"||t.margin===null||t.margin<0?4:t.margin,s=t.width&&t.width>=21?t.width:void 0,o=t.scale||4;return{width:s,scale:s?4:o,margin:i,color:{dark:e(t.color.dark||"#000000ff"),light:e(t.color.light||"#ffffffff")},type:t.type,rendererOpts:t.rendererOpts||{}}},r.getScale=function(t,i){return i.width&&i.width>=t+i.margin*2?i.width/(t+i.margin*2):i.scale},r.getImageWidth=function(t,i){const s=r.getScale(t,i);return Math.floor((t+i.margin*2)*s)},r.qrToImageData=function(t,i,s){const o=i.modules.size,a=i.modules.data,c=r.getScale(o,s),f=Math.floor((o+s.margin*2)*c),u=s.margin*c,l=[s.color.light,s.color.dark];for(let d=0;d<f;d++)for(let h=0;h<f;h++){let g=(d*f+h)*4,_=s.color.light;if(d>=u&&h>=u&&d<f-u&&h<f-u){const M=Math.floor((d-u)/c),k=Math.floor((h-u)/c);_=l[a[M*o+k]?1:0]}t[g++]=_.r,t[g++]=_.g,t[g++]=_.b,t[g]=_.a}}})(Ve)),Ve}var zt;function Dr(){return zt||(zt=1,(function(r){const e=Nn();function n(i,s,o){i.clearRect(0,0,s.width,s.height),s.style||(s.style={}),s.height=o,s.width=o,s.style.height=o+"px",s.style.width=o+"px"}function t(){try{return document.createElement("canvas")}catch{throw new Error("You need to specify a canvas element")}}r.render=function(s,o,a){let c=a,f=o;typeof c>"u"&&(!o||!o.getContext)&&(c=o,o=void 0),o||(f=t()),c=e.getOptions(c);const u=e.getImageWidth(s.modules.size,c),l=f.getContext("2d"),d=l.createImageData(u,u);return e.qrToImageData(d.data,s,c),n(l,f,u),l.putImageData(d,0,0),f},r.renderToDataURL=function(s,o,a){let c=a;typeof c>"u"&&(!o||!o.getContext)&&(c=o,o=void 0),c||(c={});const f=r.render(s,o,c),u=c.type||"image/png",l=c.rendererOpts||{};return f.toDataURL(u,l.quality)}})(we)),we}var qe={},wt;function Mr(){if(wt)return qe;wt=1;const r=Nn();function e(i,s){const o=i.a/255,a=s+'="'+i.hex+'"';return o<1?a+" "+s+'-opacity="'+o.toFixed(2).slice(1)+'"':a}function n(i,s,o){let a=i+s;return typeof o<"u"&&(a+=" "+o),a}function t(i,s,o){let a="",c=0,f=!1,u=0;for(let l=0;l<i.length;l++){const d=Math.floor(l%s),h=Math.floor(l/s);!d&&!f&&(f=!0),i[l]?(u++,l>0&&d>0&&i[l-1]||(a+=f?n("M",d+o,.5+h+o):n("m",c,0),c=0,f=!1),d+1<s&&i[l+1]||(a+=n("h",u),u=0)):c++}return a}return qe.render=function(s,o,a){const c=r.getOptions(o),f=s.modules.size,u=s.modules.data,l=f+c.margin*2,d=c.color.light.a?"<path "+e(c.color.light,"fill")+' d="M0 0h'+l+"v"+l+'H0z"/>':"",h="<path "+e(c.color.dark,"stroke")+' d="'+t(u,f,c.margin)+'"/>',g='viewBox="0 0 '+l+" "+l+'"',M='<svg xmlns="http://www.w3.org/2000/svg" '+(c.width?'width="'+c.width+'" height="'+c.width+'" ':"")+g+' shape-rendering="crispEdges">'+d+h+`</svg>
48
- `;return typeof a=="function"&&a(null,M),M},qe}var Vt;function Ar(){if(Vt)return Q;Vt=1;const r=dr(),e=Ir(),n=Dr(),t=Mr();function i(s,o,a,c,f){const u=[].slice.call(arguments,1),l=u.length,d=typeof u[l-1]=="function";if(!d&&!r())throw new Error("Callback required as last argument");if(d){if(l<2)throw new Error("Too few arguments provided");l===2?(f=a,a=o,o=c=void 0):l===3&&(o.getContext&&typeof f>"u"?(f=c,c=void 0):(f=c,c=a,a=o,o=void 0))}else{if(l<1)throw new Error("Too few arguments provided");return l===1?(a=o,o=c=void 0):l===2&&!o.getContext&&(c=a,a=o,o=void 0),new Promise(function(h,g){try{const _=e.create(a,c);h(s(_,o,c))}catch(_){g(_)}})}try{const h=e.create(a,c);f(null,s(h,o,c))}catch(h){f(h)}}return Q.create=e.create,Q.toCanvas=i.bind(null,n.render),Q.toDataURL=i.bind(null,n.renderToDataURL),Q.toString=i.bind(null,function(s,o,a){return t.render(s,a)}),Q}var Lr=Ar();const Br=Rn(Lr);class Or{constructor(e,n,t){this._playerConnections=new Map,this._invitationAccepted=new Map,this._connectionToPlayerId=new Map,this._playerNames=new Map,this._onPlayerEvent=null,this._onPlayerEvent=n,this._gamepadUiUrl=e,this._peer=t?new he(t):new he,this._peer.on("open",i=>{console.log(`Host PeerJS ID: ${i}`)}),this._peer.on("connection",i=>{i.on("open",()=>{console.log(`Connection open for PeerJS ID: ${i.peer}`)}),i.on("data",s=>{if(this._onPlayerEvent){let o;if(typeof s=="string")try{o=JSON.parse(s)}catch{console.warn("Invalid JSON from player",i.peer,s);return}else if(typeof s=="object"&&s!==null)o=s;else{console.warn("Unexpected data type from player",i.peer,s);return}let a;if("playerId"in o&&typeof o.playerId=="string"&&(a=o.playerId),a===void 0){console.warn("Malformed event from player",i.peer,o);return}o.playerId=a,"action"in o&&"timestamp"in o?(o.action==="JOIN"&&(this._playerConnections.set(o.playerId,i),this._invitationAccepted.set(o.playerId,!0),this._connectionToPlayerId.set(i,o.playerId),this._playerNames.set(o.playerId,o.playerName)),o.action==="MOVE"&&(o.playerName=this._playerNames.get(o.playerId)||""),this._onPlayerEvent(o)):console.warn("Malformed event from player",i.peer,o)}}),i.on("close",()=>{const s=this._connectionToPlayerId.get(i);if(s!==void 0){this._playerConnections.delete(s),this._invitationAccepted.delete(s),this._connectionToPlayerId.delete(i);const o=this._playerNames.get(s)||"";this._playerNames.delete(s),this._onPlayerEvent&&this._onPlayerEvent({playerId:s,action:"LEAVE",playerName:o,timestamp:Date.now()}),console.log(`Player ${s} disconnected`)}else console.log(`Connection closed for unknown player (PeerJS ID: ${i.peer})`)}),i.on("error",s=>{const o=this._connectionToPlayerId.get(i);o!==void 0?(console.error(`Connection error for player ${o}:`,s),this._playerConnections.delete(o),this._invitationAccepted.delete(o),this._connectionToPlayerId.delete(i),this._playerNames.delete(o)):console.error(`Connection error for unknown player (PeerJS ID: ${i.peer}):`,s)})}),this._peer.on("error",i=>{console.error("PeerJS error:",i)})}async createLobby(){return new Promise((e,n)=>{if(this._peer.destroyed||!this._peer.open)return n(new Error("Host Peer is not open or has been destroyed"));const t=this._peer.id;if(!t)return n(new Error("Host Peer ID not yet assigned"));const i=`${this._gamepadUiUrl}?hostPeerId=${encodeURIComponent(t)}`;Br.toDataURL(i,{errorCorrectionLevel:"M"},(s,o)=>{if(s)return n(s);e({dataUrl:o,shareURL:i})})})}getInvitationStatus(e){return this._invitationAccepted.get(e)}destroy(){for(const e of this._playerConnections.keys())this._invitationAccepted.delete(e);this._playerConnections.clear(),this._invitationAccepted.clear(),this._connectionToPlayerId.clear(),this._playerNames.clear(),this._peer.destroy()}}exports.Match=Or;exports.Player=lr;
48
+ `;return typeof a=="function"&&a(null,M),M},qe}var Vt;function Ar(){if(Vt)return Q;Vt=1;const r=dr(),e=Ir(),n=Dr(),t=Mr();function i(s,o,a,c,f){const u=[].slice.call(arguments,1),l=u.length,d=typeof u[l-1]=="function";if(!d&&!r())throw new Error("Callback required as last argument");if(d){if(l<2)throw new Error("Too few arguments provided");l===2?(f=a,a=o,o=c=void 0):l===3&&(o.getContext&&typeof f>"u"?(f=c,c=void 0):(f=c,c=a,a=o,o=void 0))}else{if(l<1)throw new Error("Too few arguments provided");return l===1?(a=o,o=c=void 0):l===2&&!o.getContext&&(c=a,a=o,o=void 0),new Promise(function(h,g){try{const _=e.create(a,c);h(s(_,o,c))}catch(_){g(_)}})}try{const h=e.create(a,c);f(null,s(h,o,c))}catch(h){f(h)}}return Q.create=e.create,Q.toCanvas=i.bind(null,n.render),Q.toDataURL=i.bind(null,n.renderToDataURL),Q.toString=i.bind(null,function(s,o,a){return t.render(s,a)}),Q}var Lr=Ar();const Br=Rn(Lr);class Or{constructor(){this._peer=null,this._playerConnections=new Map,this._invitationAccepted=new Map,this._connectionToPlayerId=new Map,this._playerNames=new Map,this._onPlayerEvent=null,this._gamepadUiUrl=null}async createLobby(e,n,t){return new Promise((i,s)=>{this._onPlayerEvent=n,this._gamepadUiUrl=e,this._peer=t?new he(t):new he,this._peer.on("open",o=>{if(console.log(`Host PeerJS ID: ${o}`),!o)return s(new Error("Host Peer ID not yet assigned"));const a=`${this._gamepadUiUrl}?hostPeerId=${encodeURIComponent(o)}`;Br.toDataURL(a,{errorCorrectionLevel:"M"},(c,f)=>{if(c)return s(c);i({dataUrl:f,shareURL:a})})}),this._peer.on("connection",o=>{o.on("open",()=>{console.log(`Connection open for PeerJS ID: ${o.peer}`)}),o.on("data",a=>{if(this._onPlayerEvent){let c;if(typeof a=="string")try{c=JSON.parse(a)}catch{console.warn("Invalid JSON from player",o.peer,a);return}else if(typeof a=="object"&&a!==null)c=a;else{console.warn("Unexpected data type from player",o.peer,a);return}let f;if("playerId"in c&&typeof c.playerId=="string"&&(f=c.playerId),f===void 0){console.warn("Malformed event from player",o.peer,c);return}c.playerId=f,"action"in c&&"timestamp"in c?(c.action==="JOIN"&&(this._playerConnections.set(c.playerId,o),this._invitationAccepted.set(c.playerId,!0),this._connectionToPlayerId.set(o,c.playerId),this._playerNames.set(c.playerId,c.playerName)),c.action==="MOVE"&&(c.playerName=this._playerNames.get(c.playerId)||""),this._onPlayerEvent(c)):console.warn("Malformed event from player",o.peer,c)}}),o.on("close",()=>{const a=this._connectionToPlayerId.get(o);if(a!==void 0){this._playerConnections.delete(a),this._invitationAccepted.delete(a),this._connectionToPlayerId.delete(o);const c=this._playerNames.get(a)||"";this._playerNames.delete(a),this._onPlayerEvent&&this._onPlayerEvent({playerId:a,action:"LEAVE",playerName:c,timestamp:Date.now()}),console.log(`Player ${a} disconnected`)}else console.log(`Connection closed for unknown player (PeerJS ID: ${o.peer})`)}),o.on("error",a=>{const c=this._connectionToPlayerId.get(o);c!==void 0?(console.error(`Connection error for player ${c}:`,a),this._playerConnections.delete(c),this._invitationAccepted.delete(c),this._connectionToPlayerId.delete(o),this._playerNames.delete(c)):console.error(`Connection error for unknown player (PeerJS ID: ${o.peer}):`,a)})}),this._peer.on("error",o=>{console.error("PeerJS error:",o)})})}getInvitationStatus(e){return this._invitationAccepted.get(e)}destroy(){for(const e of this._playerConnections.keys())this._invitationAccepted.delete(e);this._playerConnections.clear(),this._invitationAccepted.clear(),this._connectionToPlayerId.clear(),this._playerNames.clear(),this._peer&&(this._peer.destroy(),this._peer=null)}}exports.Match=Or;exports.Player=lr;
@@ -45,4 +45,4 @@ Make sure your charset is UTF-8`);s=(s>>>8&255)*192+(s&255),t.put(s,13)}},Qe=n,Q
45
45
  The chosen QR Code version cannot contain this amount of data.
46
46
  Minimum version required to store current data is: `+C+`.
47
47
  `);const T=E(S,v,P),b=r.getSymbolSize(S),x=new t(b);return h(x,S),g(x),_(x,S),k(x,v,0),S>=7&&M(x,S),I(x,T),isNaN(m)&&(m=o.getBestMask(x,k.bind(null,x,v))),o.applyMask(m,x),k(x,v,m),{modules:x,version:S,errorCorrectionLevel:v,maskPattern:m,segments:P}}return Ae.create=function(S,v){if(typeof S>"u"||S==="")throw new Error("No input text");let m=e.M,P,C;return typeof v<"u"&&(m=e.from(v.errorCorrectionLevel,e.M),P=f.from(v.version),C=o.from(v.maskPattern),v.toSJISFunc&&r.setToSJISFunction(v.toSJISFunc)),A(S,P,m,C)},Ae}var Xe={},Ze={},Dn;function Mn(){return Dn||(Dn=1,(function(r){function e(n){if(typeof n=="number"&&(n=n.toString()),typeof n!="string")throw new Error("Color should be defined as hex string");let t=n.slice().replace("#","").split("");if(t.length<3||t.length===5||t.length>8)throw new Error("Invalid hex color: "+n);(t.length===3||t.length===4)&&(t=Array.prototype.concat.apply([],t.map(function(s){return[s,s]}))),t.length===6&&t.push("F","F");const i=parseInt(t.join(""),16);return{r:i>>24&255,g:i>>16&255,b:i>>8&255,a:i&255,hex:"#"+t.slice(0,6).join("")}}r.getOptions=function(t){t||(t={}),t.color||(t.color={});const i=typeof t.margin>"u"||t.margin===null||t.margin<0?4:t.margin,s=t.width&&t.width>=21?t.width:void 0,o=t.scale||4;return{width:s,scale:s?4:o,margin:i,color:{dark:e(t.color.dark||"#000000ff"),light:e(t.color.light||"#ffffffff")},type:t.type,rendererOpts:t.rendererOpts||{}}},r.getScale=function(t,i){return i.width&&i.width>=t+i.margin*2?i.width/(t+i.margin*2):i.scale},r.getImageWidth=function(t,i){const s=r.getScale(t,i);return Math.floor((t+i.margin*2)*s)},r.qrToImageData=function(t,i,s){const o=i.modules.size,a=i.modules.data,c=r.getScale(o,s),f=Math.floor((o+s.margin*2)*c),u=s.margin*c,l=[s.color.light,s.color.dark];for(let d=0;d<f;d++)for(let h=0;h<f;h++){let g=(d*f+h)*4,_=s.color.light;if(d>=u&&h>=u&&d<f-u&&h<f-u){const M=Math.floor((d-u)/c),k=Math.floor((h-u)/c);_=l[a[M*o+k]?1:0]}t[g++]=_.r,t[g++]=_.g,t[g++]=_.b,t[g]=_.a}}})(Ze)),Ze}var An;function xr(){return An||(An=1,(function(r){const e=Mn();function n(i,s,o){i.clearRect(0,0,s.width,s.height),s.style||(s.style={}),s.height=o,s.width=o,s.style.height=o+"px",s.style.width=o+"px"}function t(){try{return document.createElement("canvas")}catch{throw new Error("You need to specify a canvas element")}}r.render=function(s,o,a){let c=a,f=o;typeof c>"u"&&(!o||!o.getContext)&&(c=o,o=void 0),o||(f=t()),c=e.getOptions(c);const u=e.getImageWidth(s.modules.size,c),l=f.getContext("2d"),d=l.createImageData(u,u);return e.qrToImageData(d.data,s,c),n(l,f,u),l.putImageData(d,0,0),f},r.renderToDataURL=function(s,o,a){let c=a;typeof c>"u"&&(!o||!o.getContext)&&(c=o,o=void 0),c||(c={});const f=r.render(s,o,c),u=c.type||"image/png",l=c.rendererOpts||{};return f.toDataURL(u,l.quality)}})(Xe)),Xe}var et={},Ln;function Ir(){if(Ln)return et;Ln=1;const r=Mn();function e(i,s){const o=i.a/255,a=s+'="'+i.hex+'"';return o<1?a+" "+s+'-opacity="'+o.toFixed(2).slice(1)+'"':a}function n(i,s,o){let a=i+s;return typeof o<"u"&&(a+=" "+o),a}function t(i,s,o){let a="",c=0,f=!1,u=0;for(let l=0;l<i.length;l++){const d=Math.floor(l%s),h=Math.floor(l/s);!d&&!f&&(f=!0),i[l]?(u++,l>0&&d>0&&i[l-1]||(a+=f?n("M",d+o,.5+h+o):n("m",c,0),c=0,f=!1),d+1<s&&i[l+1]||(a+=n("h",u),u=0)):c++}return a}return et.render=function(s,o,a){const c=r.getOptions(o),f=s.modules.size,u=s.modules.data,l=f+c.margin*2,d=c.color.light.a?"<path "+e(c.color.light,"fill")+' d="M0 0h'+l+"v"+l+'H0z"/>':"",h="<path "+e(c.color.dark,"stroke")+' d="'+t(u,f,c.margin)+'"/>',g='viewBox="0 0 '+l+" "+l+'"',M='<svg xmlns="http://www.w3.org/2000/svg" '+(c.width?'width="'+c.width+'" height="'+c.width+'" ':"")+g+' shape-rendering="crispEdges">'+d+h+`</svg>
48
- `;return typeof a=="function"&&a(null,M),M},et}var Bn;function Dr(){if(Bn)return Q;Bn=1;const r=ur(),e=Er(),n=xr(),t=Ir();function i(s,o,a,c,f){const u=[].slice.call(arguments,1),l=u.length,d=typeof u[l-1]=="function";if(!d&&!r())throw new Error("Callback required as last argument");if(d){if(l<2)throw new Error("Too few arguments provided");l===2?(f=a,a=o,o=c=void 0):l===3&&(o.getContext&&typeof f>"u"?(f=c,c=void 0):(f=c,c=a,a=o,o=void 0))}else{if(l<1)throw new Error("Too few arguments provided");return l===1?(a=o,o=c=void 0):l===2&&!o.getContext&&(c=a,a=o,o=void 0),new Promise(function(h,g){try{const _=e.create(a,c);h(s(_,o,c))}catch(_){g(_)}})}try{const h=e.create(a,c);f(null,s(h,o,c))}catch(h){f(h)}}return Q.create=e.create,Q.toCanvas=i.bind(null,n.render),Q.toDataURL=i.bind(null,n.renderToDataURL),Q.toString=i.bind(null,function(s,o,a){return t.render(s,a)}),Q}var Mr=Dr();const Ar=qt(Mr);class Lr{constructor(e,n,t){this._playerConnections=new Map,this._invitationAccepted=new Map,this._connectionToPlayerId=new Map,this._playerNames=new Map,this._onPlayerEvent=null,this._onPlayerEvent=n,this._gamepadUiUrl=e,this._peer=t?new he(t):new he,this._peer.on("open",i=>{console.log(`Host PeerJS ID: ${i}`)}),this._peer.on("connection",i=>{i.on("open",()=>{console.log(`Connection open for PeerJS ID: ${i.peer}`)}),i.on("data",s=>{if(this._onPlayerEvent){let o;if(typeof s=="string")try{o=JSON.parse(s)}catch{console.warn("Invalid JSON from player",i.peer,s);return}else if(typeof s=="object"&&s!==null)o=s;else{console.warn("Unexpected data type from player",i.peer,s);return}let a;if("playerId"in o&&typeof o.playerId=="string"&&(a=o.playerId),a===void 0){console.warn("Malformed event from player",i.peer,o);return}o.playerId=a,"action"in o&&"timestamp"in o?(o.action==="JOIN"&&(this._playerConnections.set(o.playerId,i),this._invitationAccepted.set(o.playerId,!0),this._connectionToPlayerId.set(i,o.playerId),this._playerNames.set(o.playerId,o.playerName)),o.action==="MOVE"&&(o.playerName=this._playerNames.get(o.playerId)||""),this._onPlayerEvent(o)):console.warn("Malformed event from player",i.peer,o)}}),i.on("close",()=>{const s=this._connectionToPlayerId.get(i);if(s!==void 0){this._playerConnections.delete(s),this._invitationAccepted.delete(s),this._connectionToPlayerId.delete(i);const o=this._playerNames.get(s)||"";this._playerNames.delete(s),this._onPlayerEvent&&this._onPlayerEvent({playerId:s,action:"LEAVE",playerName:o,timestamp:Date.now()}),console.log(`Player ${s} disconnected`)}else console.log(`Connection closed for unknown player (PeerJS ID: ${i.peer})`)}),i.on("error",s=>{const o=this._connectionToPlayerId.get(i);o!==void 0?(console.error(`Connection error for player ${o}:`,s),this._playerConnections.delete(o),this._invitationAccepted.delete(o),this._connectionToPlayerId.delete(i),this._playerNames.delete(o)):console.error(`Connection error for unknown player (PeerJS ID: ${i.peer}):`,s)})}),this._peer.on("error",i=>{console.error("PeerJS error:",i)})}async createLobby(){return new Promise((e,n)=>{if(this._peer.destroyed||!this._peer.open)return n(new Error("Host Peer is not open or has been destroyed"));const t=this._peer.id;if(!t)return n(new Error("Host Peer ID not yet assigned"));const i=`${this._gamepadUiUrl}?hostPeerId=${encodeURIComponent(t)}`;Ar.toDataURL(i,{errorCorrectionLevel:"M"},(s,o)=>{if(s)return n(s);e({dataUrl:o,shareURL:i})})})}getInvitationStatus(e){return this._invitationAccepted.get(e)}destroy(){for(const e of this._playerConnections.keys())this._invitationAccepted.delete(e);this._playerConnections.clear(),this._invitationAccepted.clear(),this._connectionToPlayerId.clear(),this._playerNames.clear(),this._peer.destroy()}}return re.Match=Lr,re.Player=fr,Object.defineProperty(re,Symbol.toStringTag,{value:"Module"}),re})({});
48
+ `;return typeof a=="function"&&a(null,M),M},et}var Bn;function Dr(){if(Bn)return Q;Bn=1;const r=ur(),e=Er(),n=xr(),t=Ir();function i(s,o,a,c,f){const u=[].slice.call(arguments,1),l=u.length,d=typeof u[l-1]=="function";if(!d&&!r())throw new Error("Callback required as last argument");if(d){if(l<2)throw new Error("Too few arguments provided");l===2?(f=a,a=o,o=c=void 0):l===3&&(o.getContext&&typeof f>"u"?(f=c,c=void 0):(f=c,c=a,a=o,o=void 0))}else{if(l<1)throw new Error("Too few arguments provided");return l===1?(a=o,o=c=void 0):l===2&&!o.getContext&&(c=a,a=o,o=void 0),new Promise(function(h,g){try{const _=e.create(a,c);h(s(_,o,c))}catch(_){g(_)}})}try{const h=e.create(a,c);f(null,s(h,o,c))}catch(h){f(h)}}return Q.create=e.create,Q.toCanvas=i.bind(null,n.render),Q.toDataURL=i.bind(null,n.renderToDataURL),Q.toString=i.bind(null,function(s,o,a){return t.render(s,a)}),Q}var Mr=Dr();const Ar=qt(Mr);class Lr{constructor(){this._peer=null,this._playerConnections=new Map,this._invitationAccepted=new Map,this._connectionToPlayerId=new Map,this._playerNames=new Map,this._onPlayerEvent=null,this._gamepadUiUrl=null}async createLobby(e,n,t){return new Promise((i,s)=>{this._onPlayerEvent=n,this._gamepadUiUrl=e,this._peer=t?new he(t):new he,this._peer.on("open",o=>{if(console.log(`Host PeerJS ID: ${o}`),!o)return s(new Error("Host Peer ID not yet assigned"));const a=`${this._gamepadUiUrl}?hostPeerId=${encodeURIComponent(o)}`;Ar.toDataURL(a,{errorCorrectionLevel:"M"},(c,f)=>{if(c)return s(c);i({dataUrl:f,shareURL:a})})}),this._peer.on("connection",o=>{o.on("open",()=>{console.log(`Connection open for PeerJS ID: ${o.peer}`)}),o.on("data",a=>{if(this._onPlayerEvent){let c;if(typeof a=="string")try{c=JSON.parse(a)}catch{console.warn("Invalid JSON from player",o.peer,a);return}else if(typeof a=="object"&&a!==null)c=a;else{console.warn("Unexpected data type from player",o.peer,a);return}let f;if("playerId"in c&&typeof c.playerId=="string"&&(f=c.playerId),f===void 0){console.warn("Malformed event from player",o.peer,c);return}c.playerId=f,"action"in c&&"timestamp"in c?(c.action==="JOIN"&&(this._playerConnections.set(c.playerId,o),this._invitationAccepted.set(c.playerId,!0),this._connectionToPlayerId.set(o,c.playerId),this._playerNames.set(c.playerId,c.playerName)),c.action==="MOVE"&&(c.playerName=this._playerNames.get(c.playerId)||""),this._onPlayerEvent(c)):console.warn("Malformed event from player",o.peer,c)}}),o.on("close",()=>{const a=this._connectionToPlayerId.get(o);if(a!==void 0){this._playerConnections.delete(a),this._invitationAccepted.delete(a),this._connectionToPlayerId.delete(o);const c=this._playerNames.get(a)||"";this._playerNames.delete(a),this._onPlayerEvent&&this._onPlayerEvent({playerId:a,action:"LEAVE",playerName:c,timestamp:Date.now()}),console.log(`Player ${a} disconnected`)}else console.log(`Connection closed for unknown player (PeerJS ID: ${o.peer})`)}),o.on("error",a=>{const c=this._connectionToPlayerId.get(o);c!==void 0?(console.error(`Connection error for player ${c}:`,a),this._playerConnections.delete(c),this._invitationAccepted.delete(c),this._connectionToPlayerId.delete(o),this._playerNames.delete(c)):console.error(`Connection error for unknown player (PeerJS ID: ${o.peer}):`,a)})}),this._peer.on("error",o=>{console.error("PeerJS error:",o)})})}getInvitationStatus(e){return this._invitationAccepted.get(e)}destroy(){for(const e of this._playerConnections.keys())this._invitationAccepted.delete(e);this._playerConnections.clear(),this._invitationAccepted.clear(),this._connectionToPlayerId.clear(),this._playerNames.clear(),this._peer&&(this._peer.destroy(),this._peer=null)}}return re.Match=Lr,re.Player=fr,Object.defineProperty(re,Symbol.toStringTag,{value:"Module"}),re})({});
package/dist/index.mjs CHANGED
@@ -4755,84 +4755,81 @@ function Mr() {
4755
4755
  var Ar = Mr();
4756
4756
  const Lr = /* @__PURE__ */ Rn(Ar);
4757
4757
  class Or {
4758
- constructor(e, n, t) {
4759
- this._playerConnections = /* @__PURE__ */ new Map(), this._invitationAccepted = /* @__PURE__ */ new Map(), this._connectionToPlayerId = /* @__PURE__ */ new Map(), this._playerNames = /* @__PURE__ */ new Map(), this._onPlayerEvent = null, this._onPlayerEvent = n, this._gamepadUiUrl = e, this._peer = t ? new he(t) : new he(), this._peer.on("open", (i) => {
4760
- console.log(`Host PeerJS ID: ${i}`);
4761
- }), this._peer.on("connection", (i) => {
4762
- i.on("open", () => {
4763
- console.log(`Connection open for PeerJS ID: ${i.peer}`);
4764
- }), i.on("data", (s) => {
4765
- if (this._onPlayerEvent) {
4766
- let o;
4767
- if (typeof s == "string")
4768
- try {
4769
- o = JSON.parse(s);
4770
- } catch {
4771
- console.warn("Invalid JSON from player", i.peer, s);
4758
+ constructor() {
4759
+ this._peer = null, this._playerConnections = /* @__PURE__ */ new Map(), this._invitationAccepted = /* @__PURE__ */ new Map(), this._connectionToPlayerId = /* @__PURE__ */ new Map(), this._playerNames = /* @__PURE__ */ new Map(), this._onPlayerEvent = null, this._gamepadUiUrl = null;
4760
+ }
4761
+ async createLobby(e, n, t) {
4762
+ return new Promise((i, s) => {
4763
+ this._onPlayerEvent = n, this._gamepadUiUrl = e, this._peer = t ? new he(t) : new he(), this._peer.on("open", (o) => {
4764
+ if (console.log(`Host PeerJS ID: ${o}`), !o)
4765
+ return s(new Error("Host Peer ID not yet assigned"));
4766
+ const a = `${this._gamepadUiUrl}?hostPeerId=${encodeURIComponent(o)}`;
4767
+ Lr.toDataURL(
4768
+ a,
4769
+ { errorCorrectionLevel: "M" },
4770
+ (c, f) => {
4771
+ if (c) return s(c);
4772
+ i({
4773
+ dataUrl: f,
4774
+ shareURL: a
4775
+ });
4776
+ }
4777
+ );
4778
+ }), this._peer.on("connection", (o) => {
4779
+ o.on("open", () => {
4780
+ console.log(`Connection open for PeerJS ID: ${o.peer}`);
4781
+ }), o.on("data", (a) => {
4782
+ if (this._onPlayerEvent) {
4783
+ let c;
4784
+ if (typeof a == "string")
4785
+ try {
4786
+ c = JSON.parse(a);
4787
+ } catch {
4788
+ console.warn("Invalid JSON from player", o.peer, a);
4789
+ return;
4790
+ }
4791
+ else if (typeof a == "object" && a !== null)
4792
+ c = a;
4793
+ else {
4794
+ console.warn(
4795
+ "Unexpected data type from player",
4796
+ o.peer,
4797
+ a
4798
+ );
4772
4799
  return;
4773
4800
  }
4774
- else if (typeof s == "object" && s !== null)
4775
- o = s;
4776
- else {
4777
- console.warn(
4778
- "Unexpected data type from player",
4779
- i.peer,
4780
- s
4781
- );
4782
- return;
4783
- }
4784
- let a;
4785
- if ("playerId" in o && typeof o.playerId == "string" && (a = o.playerId), a === void 0) {
4786
- console.warn("Malformed event from player", i.peer, o);
4787
- return;
4801
+ let f;
4802
+ if ("playerId" in c && typeof c.playerId == "string" && (f = c.playerId), f === void 0) {
4803
+ console.warn("Malformed event from player", o.peer, c);
4804
+ return;
4805
+ }
4806
+ c.playerId = f, "action" in c && "timestamp" in c ? (c.action === "JOIN" && (this._playerConnections.set(c.playerId, o), this._invitationAccepted.set(c.playerId, !0), this._connectionToPlayerId.set(o, c.playerId), this._playerNames.set(c.playerId, c.playerName)), c.action === "MOVE" && (c.playerName = this._playerNames.get(c.playerId) || ""), this._onPlayerEvent(c)) : console.warn("Malformed event from player", o.peer, c);
4788
4807
  }
4789
- o.playerId = a, "action" in o && "timestamp" in o ? (o.action === "JOIN" && (this._playerConnections.set(o.playerId, i), this._invitationAccepted.set(o.playerId, !0), this._connectionToPlayerId.set(i, o.playerId), this._playerNames.set(o.playerId, o.playerName)), o.action === "MOVE" && (o.playerName = this._playerNames.get(o.playerId) || ""), this._onPlayerEvent(o)) : console.warn("Malformed event from player", i.peer, o);
4790
- }
4791
- }), i.on("close", () => {
4792
- const s = this._connectionToPlayerId.get(i);
4793
- if (s !== void 0) {
4794
- this._playerConnections.delete(s), this._invitationAccepted.delete(s), this._connectionToPlayerId.delete(i);
4795
- const o = this._playerNames.get(s) || "";
4796
- this._playerNames.delete(s), this._onPlayerEvent && this._onPlayerEvent({
4797
- playerId: s,
4798
- action: "LEAVE",
4799
- playerName: o,
4800
- timestamp: Date.now()
4801
- }), console.log(`Player ${s} disconnected`);
4802
- } else
4803
- console.log(
4804
- `Connection closed for unknown player (PeerJS ID: ${i.peer})`
4808
+ }), o.on("close", () => {
4809
+ const a = this._connectionToPlayerId.get(o);
4810
+ if (a !== void 0) {
4811
+ this._playerConnections.delete(a), this._invitationAccepted.delete(a), this._connectionToPlayerId.delete(o);
4812
+ const c = this._playerNames.get(a) || "";
4813
+ this._playerNames.delete(a), this._onPlayerEvent && this._onPlayerEvent({
4814
+ playerId: a,
4815
+ action: "LEAVE",
4816
+ playerName: c,
4817
+ timestamp: Date.now()
4818
+ }), console.log(`Player ${a} disconnected`);
4819
+ } else
4820
+ console.log(
4821
+ `Connection closed for unknown player (PeerJS ID: ${o.peer})`
4822
+ );
4823
+ }), o.on("error", (a) => {
4824
+ const c = this._connectionToPlayerId.get(o);
4825
+ c !== void 0 ? (console.error(`Connection error for player ${c}:`, a), this._playerConnections.delete(c), this._invitationAccepted.delete(c), this._connectionToPlayerId.delete(o), this._playerNames.delete(c)) : console.error(
4826
+ `Connection error for unknown player (PeerJS ID: ${o.peer}):`,
4827
+ a
4805
4828
  );
4806
- }), i.on("error", (s) => {
4807
- const o = this._connectionToPlayerId.get(i);
4808
- o !== void 0 ? (console.error(`Connection error for player ${o}:`, s), this._playerConnections.delete(o), this._invitationAccepted.delete(o), this._connectionToPlayerId.delete(i), this._playerNames.delete(o)) : console.error(
4809
- `Connection error for unknown player (PeerJS ID: ${i.peer}):`,
4810
- s
4811
- );
4829
+ });
4830
+ }), this._peer.on("error", (o) => {
4831
+ console.error("PeerJS error:", o);
4812
4832
  });
4813
- }), this._peer.on("error", (i) => {
4814
- console.error("PeerJS error:", i);
4815
- });
4816
- }
4817
- async createLobby() {
4818
- return new Promise((e, n) => {
4819
- if (this._peer.destroyed || !this._peer.open)
4820
- return n(new Error("Host Peer is not open or has been destroyed"));
4821
- const t = this._peer.id;
4822
- if (!t)
4823
- return n(new Error("Host Peer ID not yet assigned"));
4824
- const i = `${this._gamepadUiUrl}?hostPeerId=${encodeURIComponent(t)}`;
4825
- Lr.toDataURL(
4826
- i,
4827
- { errorCorrectionLevel: "M" },
4828
- (s, o) => {
4829
- if (s) return n(s);
4830
- e({
4831
- dataUrl: o,
4832
- shareURL: i
4833
- });
4834
- }
4835
- );
4836
4833
  });
4837
4834
  }
4838
4835
  getInvitationStatus(e) {
@@ -4841,7 +4838,7 @@ class Or {
4841
4838
  destroy() {
4842
4839
  for (const e of this._playerConnections.keys())
4843
4840
  this._invitationAccepted.delete(e);
4844
- this._playerConnections.clear(), this._invitationAccepted.clear(), this._connectionToPlayerId.clear(), this._playerNames.clear(), this._peer.destroy();
4841
+ this._playerConnections.clear(), this._invitationAccepted.clear(), this._connectionToPlayerId.clear(), this._playerNames.clear(), this._peer && (this._peer.destroy(), this._peer = null);
4845
4842
  }
4846
4843
  }
4847
4844
  export {
package/dist/match.d.ts CHANGED
@@ -21,8 +21,7 @@ export declare class Match {
21
21
  private _playerNames;
22
22
  private _onPlayerEvent;
23
23
  private _gamepadUiUrl;
24
- constructor(gamepadUiUrl: string, onPlayerEvent: OnPlayerEventHandler, peerConfig?: PeerOptions);
25
- createLobby(): Promise<{
24
+ createLobby(gamepadUiUrl: string, onPlayerEvent: OnPlayerEventHandler, peerConfig?: PeerOptions): Promise<{
26
25
  dataUrl: string;
27
26
  shareURL: string;
28
27
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../src/match.ts"],"names":[],"mappings":"AAAA,OAAa,EAAuB,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAC;AAGrE,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,cAAc,GAAG,SAAS,CAAC,GAAG,eAAe,CAAC;AAEzE,KAAK,oBAAoB,GAAG,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;AAExD,qBAAa,KAAK;IAChB,OAAO,CAAC,KAAK,CAAO;IACpB,OAAO,CAAC,kBAAkB,CAA0C;IACpE,OAAO,CAAC,mBAAmB,CAAmC;IAC9D,OAAO,CAAC,qBAAqB,CAA0C;IACvE,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,cAAc,CAAqC;IAC3D,OAAO,CAAC,aAAa,CAAS;gBAG5B,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,oBAAoB,EACnC,UAAU,CAAC,EAAE,WAAW;IAkHpB,WAAW,IAAI,OAAO,CAAC;QAC3B,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IA4BF,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAI1D,OAAO;CAUR"}
1
+ {"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../src/match.ts"],"names":[],"mappings":"AAAA,OAAa,EAAuB,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAC;AAGrE,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,cAAc,GAAG,SAAS,CAAC,GAAG,eAAe,CAAC;AAEzE,KAAK,oBAAoB,GAAG,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;AAExD,qBAAa,KAAK;IAChB,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,kBAAkB,CAA0C;IACpE,OAAO,CAAC,mBAAmB,CAAmC;IAC9D,OAAO,CAAC,qBAAqB,CAA0C;IACvE,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,cAAc,CAAqC;IAC3D,OAAO,CAAC,aAAa,CAAuB;IAEtC,WAAW,CACf,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,oBAAoB,EACnC,UAAU,CAAC,EAAE,WAAW,GACvB,OAAO,CAAC;QACT,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IAoIF,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAI1D,OAAO;CAaR"}
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "touch-coop",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
+ "license": "MIT",
4
5
  "description": "Allow players to play couch co-op in your game using their mobile devices as controllers.",
5
6
  "keywords": [
6
7
  "couch-coop",
@@ -25,7 +26,8 @@
25
26
  "start": "http-server",
26
27
  "lint": "npx @biomejs/biome check",
27
28
  "lint-and-format": "npx @biomejs/biome check --write --unsafe",
28
- "build": "vite build && tsc --emitDeclarationOnly"
29
+ "build": "vite build && tsc --emitDeclarationOnly",
30
+ "migrate": "biome migrate --write"
29
31
  },
30
32
  "devDependencies": {
31
33
  "@biomejs/biome": "^1.9.4",
package/src/match.ts CHANGED
@@ -21,160 +21,151 @@ export type PlayerEvent = (JoinLeaveEvent | MoveEvent) & BasePlayerEvent;
21
21
  type OnPlayerEventHandler = (data: PlayerEvent) => void;
22
22
 
23
23
  export class Match {
24
- private _peer: Peer;
24
+ private _peer: Peer | null = null;
25
25
  private _playerConnections: Map<string, DataConnection> = new Map();
26
26
  private _invitationAccepted: Map<string, boolean> = new Map();
27
27
  private _connectionToPlayerId: Map<DataConnection, string> = new Map();
28
28
  private _playerNames: Map<string, string> = new Map();
29
29
  private _onPlayerEvent: OnPlayerEventHandler | null = null;
30
- private _gamepadUiUrl: string;
30
+ private _gamepadUiUrl: string | null = null;
31
31
 
32
- constructor(
32
+ async createLobby(
33
33
  gamepadUiUrl: string,
34
34
  onPlayerEvent: OnPlayerEventHandler,
35
35
  peerConfig?: PeerOptions,
36
- ) {
37
- this._onPlayerEvent = onPlayerEvent;
38
- this._gamepadUiUrl = gamepadUiUrl;
36
+ ): Promise<{
37
+ dataUrl: string;
38
+ shareURL: string;
39
+ }> {
40
+ return new Promise((resolve, reject) => {
41
+ this._onPlayerEvent = onPlayerEvent;
42
+ this._gamepadUiUrl = gamepadUiUrl;
43
+ this._peer = peerConfig ? new Peer(peerConfig) : new Peer();
44
+ this._peer.on("open", (hostId) => {
45
+ console.log(`Host PeerJS ID: ${hostId}`);
46
+
47
+ if (!hostId) {
48
+ return reject(new Error("Host Peer ID not yet assigned"));
49
+ }
39
50
 
40
- this._peer = peerConfig ? new Peer(peerConfig) : new Peer();
51
+ const shareURL = `${this._gamepadUiUrl}?hostPeerId=${encodeURIComponent(hostId)}`;
41
52
 
42
- this._peer.on("open", (hostId) => {
43
- console.log(`Host PeerJS ID: ${hostId}`);
44
- });
53
+ QRCode.toDataURL(
54
+ shareURL,
55
+ { errorCorrectionLevel: "M" },
56
+ (err, dataUrl) => {
57
+ if (err) return reject(err);
45
58
 
46
- this._peer.on("connection", (conn: DataConnection) => {
47
- conn.on("open", () => {
48
- console.log(`Connection open for PeerJS ID: ${conn.peer}`);
59
+ resolve({
60
+ dataUrl,
61
+ shareURL,
62
+ });
63
+ },
64
+ );
49
65
  });
50
66
 
51
- conn.on("data", (rawData: unknown) => {
52
- if (this._onPlayerEvent) {
53
- let eventData: PlayerEvent;
54
- if (typeof rawData === "string") {
55
- try {
56
- eventData = JSON.parse(rawData);
57
- } catch {
58
- console.warn("Invalid JSON from player", conn.peer, rawData);
67
+ this._peer.on("connection", (conn: DataConnection) => {
68
+ conn.on("open", () => {
69
+ console.log(`Connection open for PeerJS ID: ${conn.peer}`);
70
+ });
71
+
72
+ conn.on("data", (rawData: unknown) => {
73
+ if (this._onPlayerEvent) {
74
+ let eventData: PlayerEvent;
75
+ if (typeof rawData === "string") {
76
+ try {
77
+ eventData = JSON.parse(rawData);
78
+ } catch {
79
+ console.warn("Invalid JSON from player", conn.peer, rawData);
80
+ return;
81
+ }
82
+ } else if (typeof rawData === "object" && rawData !== null) {
83
+ eventData = rawData as PlayerEvent;
84
+ } else {
85
+ console.warn(
86
+ "Unexpected data type from player",
87
+ conn.peer,
88
+ rawData,
89
+ );
59
90
  return;
60
91
  }
61
- } else if (typeof rawData === "object" && rawData !== null) {
62
- eventData = rawData as PlayerEvent;
63
- } else {
64
- console.warn(
65
- "Unexpected data type from player",
66
- conn.peer,
67
- rawData,
68
- );
69
- return;
70
- }
71
92
 
72
- let playerId: string | undefined;
73
- if (
74
- "playerId" in eventData &&
75
- typeof eventData.playerId === "string"
76
- ) {
77
- playerId = eventData.playerId;
78
- }
79
- if (playerId === undefined) {
80
- console.warn("Malformed event from player", conn.peer, eventData);
81
- return;
82
- }
83
- eventData.playerId = playerId;
84
-
85
- if ("action" in eventData && "timestamp" in eventData) {
86
- if (eventData.action === "JOIN") {
87
- this._playerConnections.set(eventData.playerId, conn);
88
- this._invitationAccepted.set(eventData.playerId, true);
89
- this._connectionToPlayerId.set(conn, eventData.playerId);
90
- this._playerNames.set(eventData.playerId, eventData.playerName);
93
+ let playerId: string | undefined;
94
+ if (
95
+ "playerId" in eventData &&
96
+ typeof eventData.playerId === "string"
97
+ ) {
98
+ playerId = eventData.playerId;
91
99
  }
92
- if (eventData.action === "MOVE") {
93
- eventData.playerName =
94
- this._playerNames.get(eventData.playerId) || "";
100
+ if (playerId === undefined) {
101
+ console.warn("Malformed event from player", conn.peer, eventData);
102
+ return;
95
103
  }
96
- this._onPlayerEvent(eventData);
104
+ eventData.playerId = playerId;
105
+
106
+ if ("action" in eventData && "timestamp" in eventData) {
107
+ if (eventData.action === "JOIN") {
108
+ this._playerConnections.set(eventData.playerId, conn);
109
+ this._invitationAccepted.set(eventData.playerId, true);
110
+ this._connectionToPlayerId.set(conn, eventData.playerId);
111
+ this._playerNames.set(eventData.playerId, eventData.playerName);
112
+ }
113
+ if (eventData.action === "MOVE") {
114
+ eventData.playerName =
115
+ this._playerNames.get(eventData.playerId) || "";
116
+ }
117
+ this._onPlayerEvent(eventData);
118
+ } else {
119
+ console.warn("Malformed event from player", conn.peer, eventData);
120
+ }
121
+ }
122
+ });
123
+
124
+ conn.on("close", () => {
125
+ const playerId = this._connectionToPlayerId.get(conn);
126
+ if (playerId !== undefined) {
127
+ this._playerConnections.delete(playerId);
128
+ this._invitationAccepted.delete(playerId);
129
+ this._connectionToPlayerId.delete(conn);
130
+ const playerName = this._playerNames.get(playerId) || "";
131
+ this._playerNames.delete(playerId);
132
+ if (this._onPlayerEvent) {
133
+ this._onPlayerEvent({
134
+ playerId,
135
+ action: "LEAVE",
136
+ playerName,
137
+ timestamp: Date.now(),
138
+ });
139
+ }
140
+ console.log(`Player ${playerId} disconnected`);
97
141
  } else {
98
- console.warn("Malformed event from player", conn.peer, eventData);
142
+ console.log(
143
+ `Connection closed for unknown player (PeerJS ID: ${conn.peer})`,
144
+ );
99
145
  }
100
- }
101
- });
102
-
103
- conn.on("close", () => {
104
- const playerId = this._connectionToPlayerId.get(conn);
105
- if (playerId !== undefined) {
106
- this._playerConnections.delete(playerId);
107
- this._invitationAccepted.delete(playerId);
108
- this._connectionToPlayerId.delete(conn);
109
- const playerName = this._playerNames.get(playerId) || "";
110
- this._playerNames.delete(playerId);
111
- if (this._onPlayerEvent) {
112
- this._onPlayerEvent({
113
- playerId,
114
- action: "LEAVE",
115
- playerName,
116
- timestamp: Date.now(),
117
- });
146
+ });
147
+
148
+ conn.on("error", (err) => {
149
+ const playerId = this._connectionToPlayerId.get(conn);
150
+ if (playerId !== undefined) {
151
+ console.error(`Connection error for player ${playerId}:`, err);
152
+ this._playerConnections.delete(playerId);
153
+ this._invitationAccepted.delete(playerId);
154
+ this._connectionToPlayerId.delete(conn);
155
+ this._playerNames.delete(playerId);
156
+ } else {
157
+ console.error(
158
+ `Connection error for unknown player (PeerJS ID: ${conn.peer}):`,
159
+ err,
160
+ );
118
161
  }
119
- console.log(`Player ${playerId} disconnected`);
120
- } else {
121
- console.log(
122
- `Connection closed for unknown player (PeerJS ID: ${conn.peer})`,
123
- );
124
- }
162
+ });
125
163
  });
126
164
 
127
- conn.on("error", (err) => {
128
- const playerId = this._connectionToPlayerId.get(conn);
129
- if (playerId !== undefined) {
130
- console.error(`Connection error for player ${playerId}:`, err);
131
- this._playerConnections.delete(playerId);
132
- this._invitationAccepted.delete(playerId);
133
- this._connectionToPlayerId.delete(conn);
134
- this._playerNames.delete(playerId);
135
- } else {
136
- console.error(
137
- `Connection error for unknown player (PeerJS ID: ${conn.peer}):`,
138
- err,
139
- );
140
- }
165
+ this._peer.on("error", (err) => {
166
+ console.error("PeerJS error:", err);
141
167
  });
142
168
  });
143
-
144
- this._peer.on("error", (err) => {
145
- console.error("PeerJS error:", err);
146
- });
147
- }
148
-
149
- async createLobby(): Promise<{
150
- dataUrl: string;
151
- shareURL: string;
152
- }> {
153
- return new Promise((resolve, reject) => {
154
- if (this._peer.destroyed || !this._peer.open) {
155
- return reject(new Error("Host Peer is not open or has been destroyed"));
156
- }
157
-
158
- const hostId = this._peer.id;
159
- if (!hostId) {
160
- return reject(new Error("Host Peer ID not yet assigned"));
161
- }
162
-
163
- const shareURL = `${this._gamepadUiUrl}?hostPeerId=${encodeURIComponent(hostId)}`;
164
-
165
- QRCode.toDataURL(
166
- shareURL,
167
- { errorCorrectionLevel: "M" },
168
- (err, dataUrl) => {
169
- if (err) return reject(err);
170
-
171
- resolve({
172
- dataUrl,
173
- shareURL,
174
- });
175
- },
176
- );
177
- });
178
169
  }
179
170
 
180
171
  getInvitationStatus(playerId: string): boolean | undefined {
@@ -189,6 +180,9 @@ export class Match {
189
180
  this._invitationAccepted.clear();
190
181
  this._connectionToPlayerId.clear();
191
182
  this._playerNames.clear();
192
- this._peer.destroy();
183
+ if (this._peer) {
184
+ this._peer.destroy();
185
+ this._peer = null;
186
+ }
193
187
  }
194
188
  }