touch-coop 1.0.0 → 2.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
@@ -1,4 +1,4 @@
1
- # TouchCoop
1
+ # Touch-Coop
2
2
 
3
3
  A TypeScript library that enables couch co-op gaming on the web, using mobile devices as controllers.
4
4
 
@@ -12,7 +12,26 @@ TouchCoop is intended for playing games on a TV or monitor, while players use th
12
12
 
13
13
  TouchCoop is ideal for casual multiplayer games, such as platformers, puzzle games, or party games. TouchCoop is not intended for games that require low-latency input, such as first-person shooters.
14
14
 
15
- TouchCoop does not require servers or user accounts. All communication is done using WebRTC, which allows for peer-to-peer connections between the players' devices.
15
+ TouchCoop does not require servers or user accounts for gameplay. However, it requires the PeerJS server (or your own signaling server) for initial connection setup. All communication is done using WebRTC, which allows for peer-to-peer connections between the players' devices.
16
+
17
+ ## Powered by PeerJS
18
+
19
+ TouchCoop is powered by [PeerJS](https://peerjs.com/), which provides a simple API for WebRTC peer-to-peer connections. By default, TouchCoop uses the public PeerJS servers for signaling and STUN/TURN services.
20
+
21
+ - **Status Page**: Check the status of PeerJS public servers at [https://status.peerjs.com/](https://status.peerjs.com/).
22
+ - **Custom Servers**: If you need more control or reliability, you can deploy your own PeerJS server. See the [PeerJS documentation](https://peerjs.com/docs/#start) for instructions on setting up your own server. You can pass custom PeerJS options to the `Match` and `Player` constructors to connect to your own server.
23
+
24
+ ```ts
25
+ // Example: Using a custom PeerJS server
26
+ const customPeerConfig = {
27
+ host: 'your-peerjs-server.com',
28
+ port: 9000,
29
+ path: '/peerjs'
30
+ };
31
+
32
+ const match = new Match(gamePadURL, handlePlayerEvent, customPeerConfig);
33
+ const player = new Player(customPeerConfig);
34
+ ```
16
35
 
17
36
  ## Installation
18
37
 
@@ -13,6 +13,8 @@
13
13
  </head>
14
14
  <body>
15
15
  <h1>Gamepad Controller</h1>
16
+ <button id="joinBtn">Join Game</button>
17
+ <div id="gamepad" style="display: none;">
16
18
  <div style="display: flex; flex-direction: row; gap: 80px; margin-top: 32px;">
17
19
  <div class="gamepad" style="grid-template-columns: 60px 60px 60px;">
18
20
  <div></div>
@@ -31,13 +33,16 @@
31
33
  <button id="a">A</button>
32
34
  <button id="b">B</button>
33
35
  </div>
36
+ </div>
34
37
  </div>
35
38
  <script src="../../dist/index.global.js"></script>
36
39
  <script>
37
40
  const player = new TouchCoop.Player();
38
41
  window.player = player;
39
42
 
40
- (async () => {
43
+ document.getElementById("joinBtn").onclick = async () => {
44
+ document.getElementById("joinBtn").style.display = "none";
45
+ document.getElementById("gamepad").style.display = "block";
41
46
  await player.joinMatch();
42
47
  function sendButtonEvent(btn) {
43
48
  player.sendMove(btn);
@@ -51,7 +56,7 @@
51
56
  document.getElementById("b").onclick = () => sendButtonEvent("B");
52
57
  document.getElementById("x").onclick = () => sendButtonEvent("X");
53
58
  document.getElementById("y").onclick = () => sendButtonEvent("Y");
54
- })();
59
+ };
55
60
  </script>
56
61
  </body>
57
62
  </html>
package/demos/match.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <head>
5
5
  <meta charset="UTF-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Touch Coop Game Demo</title>
7
+ <title>Touch Coop Demo</title>
8
8
  <style>
9
9
  body { background-color: black; color: white; }
10
10
  table { border-collapse: collapse; width: 100%; }
@@ -15,8 +15,8 @@
15
15
  </style>
16
16
  </head>
17
17
  <body>
18
- <image src="../media/logo.png" alt="Touch Coop Logo" style="width:200px; display:block; margin:auto;" />
19
- <h1>Touch Coop Game Demo</h1>
18
+ <image src="https://github.com/SlaneyEE/touch-coop/raw/main/media/logo.png" alt="Touch Coop Logo" style="width:200px; display:block; margin:auto; margin-top: 2rem;" />
19
+ <h1>TouchCoop Demo</h1>
20
20
  <table id="players-table">
21
21
  <tr>
22
22
  <th>Player 1</th>
@@ -64,7 +64,7 @@
64
64
 
65
65
  function updatePlayerIdDisplay(idx, playerId) {
66
66
  const playerIdDiv = document.getElementById(`player-id-${idx}`);
67
- if (playerId) {
67
+ if (playerId !== undefined) {
68
68
  playerIdDiv.textContent = `Player ID: ${playerId}`;
69
69
  } else {
70
70
  playerIdDiv.textContent = '';
@@ -72,7 +72,6 @@
72
72
  }
73
73
 
74
74
  function handlePlayerEvent(event) {
75
- // Find the player slot idx
76
75
  let idx = -1;
77
76
  for (let i = 0; i < 4; i++) {
78
77
  const div = document.getElementById(`qr-${i}`);
@@ -81,7 +80,6 @@
81
80
  break;
82
81
  }
83
82
  }
84
- // If not found and this is a JOIN, assign to first available
85
83
  if (idx === -1 && event.action === "JOIN") {
86
84
  for (let i = 0; i < 4; i++) {
87
85
  const div = document.getElementById(`qr-${i}`);
@@ -104,7 +102,7 @@
104
102
  const qrImg = qrDiv.querySelector('img.qr');
105
103
  if (qrImg) qrImg.remove();
106
104
  areaDiv.style.display = '';
107
- area.value += 'Player joined!\n';
105
+ area.value = 'Player joined!\n';
108
106
  updatePlayerIdDisplay(idx, event.playerId);
109
107
  break;
110
108
  }
@@ -113,7 +111,7 @@
113
111
  if (joinBtn2) joinBtn2.style.display = '';
114
112
  areaDiv.style.display = 'none';
115
113
  area.value += 'Player left!\n';
116
- updatePlayerIdDisplay(idx, '');
114
+ updatePlayerIdDisplay(idx, undefined);
117
115
  delete qrDiv.dataset.playerId;
118
116
  qrDiv.innerHTML = '<button onclick="joinPlayer(' + idx + ')">Join</button>';
119
117
  break;
@@ -139,13 +137,11 @@
139
137
  qrDiv.innerHTML = 'Loading...';
140
138
  playerIdDiv.textContent = '';
141
139
  try {
142
- const { dataUrl, playerId, shareURL } = await match.requestNewPlayerToJoin();
143
- qrDiv.dataset.playerId = playerId || '';
140
+ const { dataUrl, shareURL } = await match.requestNewPlayerToJoin();
144
141
  qrDiv.innerHTML = `
145
142
  <img class='qr' src='${dataUrl}' alt='QR Code' /></br>
146
143
  <a href='${shareURL}' target='_blank'>Connect</a>
147
144
  `;
148
- updatePlayerIdDisplay(idx, playerId);
149
145
  } catch (err) {
150
146
  qrDiv.innerHTML = `<span style='color:red;'>Error</span>`;
151
147
  }
package/media/demo.png CHANGED
Binary file
package/media/logo.png CHANGED
Binary file
package/package.json CHANGED
@@ -1,7 +1,17 @@
1
1
  {
2
2
  "name": "touch-coop",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Allow players to play couch co-op in your game using their mobile devices as controllers.",
5
+ "keywords": [
6
+ "couch-coop",
7
+ "mobile-controller",
8
+ "gaming",
9
+ "multiplayer",
10
+ "peerjs",
11
+ "webrtc",
12
+ "typescript",
13
+ "qrcode"
14
+ ],
5
15
  "main": "dist/index.cjs",
6
16
  "typings": "dist/index.d.ts",
7
17
  "exports": {
@@ -12,7 +22,7 @@
12
22
  }
13
23
  },
14
24
  "scripts": {
15
- "start": "vite",
25
+ "start": "http-server",
16
26
  "lint": "npx @biomejs/biome check",
17
27
  "lint-and-format": "npx @biomejs/biome check --write --unsafe",
18
28
  "build": "vite build && tsc --emitDeclarationOnly"
@@ -24,6 +34,7 @@
24
34
  "vite": "^7.3.1"
25
35
  },
26
36
  "dependencies": {
37
+ "peerjs": "^1.5.5",
27
38
  "qrcode": "^1.5.4"
28
39
  }
29
40
  }
package/src/match.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import Peer, { type DataConnection, type PeerOptions } from "peerjs";
1
2
  import QRCode from "qrcode";
2
- import { compressOfferData } from "./encoding";
3
3
 
4
4
  export interface BasePlayerEvent {
5
5
  playerId: string;
@@ -20,99 +20,164 @@ export type PlayerEvent = (JoinLeaveEvent | MoveEvent) & BasePlayerEvent;
20
20
  type OnPlayerEventHandler = (data: PlayerEvent) => void;
21
21
 
22
22
  export class Match {
23
- private _playerConnections: Map<string, RTCPeerConnection> = new Map();
24
- private _playerChannels: Map<string, RTCDataChannel> = new Map();
23
+ private _peer: Peer;
24
+ private _playerConnections: Map<string, DataConnection> = new Map();
25
25
  private _invitationAccepted: Map<string, boolean> = new Map();
26
+ private _connectionToPlayerId: Map<DataConnection, string> = new Map();
26
27
  private _onPlayerEvent: OnPlayerEventHandler | null = null;
27
28
  private _gamepadUiUrl: string;
28
- constructor(gamepadUiUrl: string, onPlayerEvent: OnPlayerEventHandler) {
29
+
30
+ constructor(
31
+ gamepadUiUrl: string,
32
+ onPlayerEvent: OnPlayerEventHandler,
33
+ peerConfig?: PeerOptions,
34
+ ) {
29
35
  this._onPlayerEvent = onPlayerEvent;
30
36
  this._gamepadUiUrl = gamepadUiUrl;
31
- const channel = new BroadcastChannel("touch-coop-signaling");
32
- channel.onmessage = async (event) => {
33
- const data = event.data;
34
- if (
35
- data &&
36
- data.type === "answer" &&
37
- data.playerId &&
38
- data.base64Answer
39
- ) {
40
- try {
41
- const pc = this._playerConnections.get(data.playerId);
42
- if (!pc) {
43
- throw new Error(`No peer connection for player: ${data.playerId}`);
37
+
38
+ this._peer = peerConfig ? new Peer(peerConfig) : new Peer();
39
+
40
+ this._peer.on("open", (hostId) => {
41
+ console.log(`Host PeerJS ID: ${hostId}`);
42
+ });
43
+
44
+ this._peer.on("connection", (conn: DataConnection) => {
45
+ conn.on("open", () => {
46
+ console.log(`Connection open for PeerJS ID: ${conn.peer}`);
47
+ });
48
+
49
+ conn.on("data", (rawData: unknown) => {
50
+ if (this._onPlayerEvent) {
51
+ let eventData: PlayerEvent;
52
+ if (typeof rawData === "string") {
53
+ try {
54
+ eventData = JSON.parse(rawData);
55
+ } catch {
56
+ console.warn("Invalid JSON from player", conn.peer, rawData);
57
+ return;
58
+ }
59
+ } else if (typeof rawData === "object" && rawData !== null) {
60
+ eventData = rawData as PlayerEvent;
61
+ } else {
62
+ console.warn(
63
+ "Unexpected data type from player",
64
+ conn.peer,
65
+ rawData,
66
+ );
67
+ return;
44
68
  }
45
- const answerObject = JSON.parse(atob(data.base64Answer));
46
- await pc.setRemoteDescription(answerObject.sdp);
47
- // Mark invitation as accepted
48
- this._invitationAccepted.set(data.playerId, true);
49
- } catch (err) {}
50
- }
51
- };
52
- }
53
- private _UUIID() {
54
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
55
- const r = (Math.random() * 16) | 0;
56
- const v = c === "x" ? r : (r & 0x3) | 0x8;
57
- return v.toString(16);
69
+
70
+ let playerId: string | undefined;
71
+ if (
72
+ "playerId" in eventData &&
73
+ typeof eventData.playerId === "string"
74
+ ) {
75
+ playerId = eventData.playerId;
76
+ }
77
+ if (playerId === undefined) {
78
+ console.warn("Malformed event from player", conn.peer, eventData);
79
+ return;
80
+ }
81
+ eventData.playerId = playerId;
82
+
83
+ if ("action" in eventData && "timestamp" in eventData) {
84
+ if (eventData.action === "JOIN") {
85
+ this._playerConnections.set(eventData.playerId, conn);
86
+ this._invitationAccepted.set(eventData.playerId, true);
87
+ this._connectionToPlayerId.set(conn, eventData.playerId);
88
+ console.log(`Player ${eventData.playerId} joined`);
89
+ }
90
+ this._onPlayerEvent(eventData);
91
+ } else {
92
+ console.warn("Malformed event from player", conn.peer, eventData);
93
+ }
94
+ }
95
+ });
96
+
97
+ conn.on("close", () => {
98
+ const playerId = this._connectionToPlayerId.get(conn);
99
+ if (playerId !== undefined) {
100
+ this._playerConnections.delete(playerId);
101
+ this._invitationAccepted.delete(playerId);
102
+ this._connectionToPlayerId.delete(conn);
103
+ if (this._onPlayerEvent) {
104
+ this._onPlayerEvent({
105
+ playerId,
106
+ action: "LEAVE",
107
+ timestamp: Date.now(),
108
+ });
109
+ }
110
+ console.log(`Player ${playerId} disconnected`);
111
+ } else {
112
+ console.log(
113
+ `Connection closed for unknown player (PeerJS ID: ${conn.peer})`,
114
+ );
115
+ }
116
+ });
117
+
118
+ conn.on("error", (err) => {
119
+ const playerId = this._connectionToPlayerId.get(conn);
120
+ if (playerId !== undefined) {
121
+ console.error(`Connection error for player ${playerId}:`, err);
122
+ this._playerConnections.delete(playerId);
123
+ this._invitationAccepted.delete(playerId);
124
+ this._connectionToPlayerId.delete(conn);
125
+ } else {
126
+ console.error(
127
+ `Connection error for unknown player (PeerJS ID: ${conn.peer}):`,
128
+ err,
129
+ );
130
+ }
131
+ });
132
+ });
133
+
134
+ this._peer.on("error", (err) => {
135
+ console.error("PeerJS error:", err);
58
136
  });
59
137
  }
60
- async requestNewPlayerToJoin() {
61
- return new Promise<{ dataUrl: string; playerId: string; shareURL: string }>(
62
- (resolve, reject) => {
63
- (async () => {
64
- const newPlayer = this._UUIID();
65
- const pc = new RTCPeerConnection();
66
- const dataChannel = pc.createDataChannel("player");
67
- this._playerConnections.set(newPlayer, pc);
68
- this._playerChannels.set(newPlayer, dataChannel);
69
- this._invitationAccepted.set(newPlayer, false);
70
- dataChannel.onmessage = (msg) => {
71
- if (this._onPlayerEvent) {
72
- let eventData = msg.data;
73
- eventData = JSON.parse(msg.data);
74
- this._onPlayerEvent(eventData);
75
- }
76
- };
77
- const offer = await pc.createOffer();
78
- await pc.setLocalDescription(offer);
79
- await new Promise((resolveIce) => {
80
- pc.onicecandidate = (event) => {
81
- if (!event.candidate) {
82
- resolveIce(void 0);
83
- }
84
- };
85
- });
86
- const offerObject = {
87
- playerId: newPlayer,
88
- sdp: pc.localDescription,
89
- };
90
- const offerJSON = JSON.stringify(offerObject);
91
- const compressedBase64OfferObject =
92
- await compressOfferData(offerJSON);
93
- const shareURL = `${this._gamepadUiUrl}?remoteSDP=${compressedBase64OfferObject}`;
94
- QRCode.toDataURL(
138
+
139
+ async requestNewPlayerToJoin(): Promise<{
140
+ dataUrl: string;
141
+ shareURL: string;
142
+ }> {
143
+ return new Promise((resolve, reject) => {
144
+ if (this._peer.destroyed || !this._peer.open) {
145
+ return reject(new Error("Host Peer is not open or has been destroyed"));
146
+ }
147
+
148
+ const hostId = this._peer.id;
149
+ if (!hostId) {
150
+ return reject(new Error("Host Peer ID not yet assigned"));
151
+ }
152
+
153
+ const shareURL = `${this._gamepadUiUrl}?hostPeerId=${encodeURIComponent(hostId)}`;
154
+
155
+ QRCode.toDataURL(
156
+ shareURL,
157
+ { errorCorrectionLevel: "M" },
158
+ (err, dataUrl) => {
159
+ if (err) return reject(err);
160
+
161
+ resolve({
162
+ dataUrl,
95
163
  shareURL,
96
- { errorCorrectionLevel: "M" },
97
- (err, dataUrl) => {
98
- if (err) {
99
- return reject(err);
100
- }
101
- resolve({
102
- dataUrl: dataUrl,
103
- shareURL: shareURL,
104
- playerId: newPlayer,
105
- });
106
- },
107
- );
108
- })();
109
- },
110
- );
164
+ });
165
+ },
166
+ );
167
+ });
111
168
  }
112
169
 
113
- // Returns true if invitation was accepted, false if still pending, undefined if not found
114
170
  getInvitationStatus(playerId: string): boolean | undefined {
115
171
  return this._invitationAccepted.get(playerId);
116
172
  }
117
- async acceptPlayerAnswer(playerId: string, base64Answer: string) {}
173
+
174
+ destroy() {
175
+ for (const playerId of this._playerConnections.keys()) {
176
+ this._invitationAccepted.delete(playerId);
177
+ }
178
+ this._playerConnections.clear();
179
+ this._invitationAccepted.clear();
180
+ this._connectionToPlayerId.clear();
181
+ this._peer.destroy();
182
+ }
118
183
  }
package/src/player.ts CHANGED
@@ -1,83 +1,118 @@
1
- import { decompressOfferData } from "./encoding";
1
+ import Peer, { type DataConnection, type PeerOptions } from "peerjs";
2
2
  import type { PlayerEvent } from "./match";
3
3
 
4
4
  export class Player {
5
- private _pc: RTCPeerConnection;
5
+ private _peer: Peer;
6
6
  private _playerId: string | null = null;
7
- private _dataChannel: RTCDataChannel | null = null;
8
- constructor() {
9
- this._pc = new RTCPeerConnection();
10
- this._pc.ondatachannel = (event) => {
11
- this._dataChannel = event.channel;
12
- this._dataChannel.onopen = () => {
13
- if (this._playerId && this._dataChannel) {
14
- const joinEvent = {
15
- playerId: this._playerId,
16
- action: "JOIN",
17
- timestamp: Date.now(),
18
- };
19
- this._dataChannel.send(JSON.stringify(joinEvent));
20
- }
21
- };
22
- };
7
+ private _dataConnection: DataConnection | null = null;
8
+ private _ownIdPromise: Promise<string>;
9
+
10
+ constructor(peerConfig?: PeerOptions) {
11
+ this._peer = peerConfig ? new Peer(peerConfig) : new Peer();
12
+
13
+ this._ownIdPromise = new Promise<string>((resolve, reject) => {
14
+ const timeout = setTimeout(
15
+ () => reject(new Error("Peer ID not assigned in time")),
16
+ 10000,
17
+ );
18
+ this._peer.on("open", (id) => {
19
+ clearTimeout(timeout);
20
+ this._playerId = id;
21
+ resolve(id);
22
+ });
23
+ this._peer.on("error", (err) => {
24
+ clearTimeout(timeout);
25
+ reject(err);
26
+ });
27
+ });
28
+
29
+ this._peer.on("error", (err) => {
30
+ console.error("PeerJS error:", err.type, err.message);
31
+ });
32
+
23
33
  window.addEventListener("beforeunload", () => {
24
- if (
25
- this._dataChannel &&
26
- this._dataChannel.readyState === "open" &&
27
- this._playerId
28
- ) {
29
- const leaveEvent = {
34
+ if (this._dataConnection?.open && this._playerId !== null) {
35
+ const leaveEvent: PlayerEvent = {
30
36
  playerId: this._playerId,
31
37
  action: "LEAVE",
32
- button: "",
33
38
  timestamp: Date.now(),
34
39
  };
35
40
  try {
36
- this._dataChannel.send(JSON.stringify(leaveEvent));
37
- } catch {}
41
+ this._dataConnection.send(leaveEvent);
42
+ } catch (err) {
43
+ console.warn("Failed to send LEAVE:", err);
44
+ }
38
45
  }
46
+ this._dataConnection?.close();
47
+ this._peer.destroy();
39
48
  });
40
49
  }
50
+
41
51
  async joinMatch() {
42
52
  const params = new URLSearchParams(window.location.search);
43
- const remoteBase64 = params.get("remoteSDP");
44
- if (!remoteBase64) {
45
- throw new Error("No remoteSDP found in URL parameters.");
53
+ const hostPeerId = params.get("hostPeerId");
54
+ if (!hostPeerId) {
55
+ throw new Error("No hostPeerId found in URL parameters.");
46
56
  }
47
- const decodedURL = await decompressOfferData(remoteBase64);
48
- const remoteObject = JSON.parse(decodedURL);
49
- this._playerId = remoteObject.playerId;
50
- const remoteSDP = remoteObject.sdp;
51
- await this._pc.setRemoteDescription(remoteSDP);
52
- if (remoteSDP.type === "offer") {
53
- const answer = await this._pc.createAnswer();
54
- await this._pc.setLocalDescription(answer);
55
- await new Promise((resolve) => {
56
- this._pc.onicecandidate = (event) => {
57
- if (!event.candidate) {
58
- resolve(void 0);
59
- }
60
- };
57
+
58
+ const conn = this._peer.connect(hostPeerId, {
59
+ reliable: false,
60
+ });
61
+
62
+ this._dataConnection = conn;
63
+
64
+ await new Promise<void>((resolve, reject) => {
65
+ const timeout = setTimeout(() => {
66
+ conn.close();
67
+ reject(new Error("Connection timeout"));
68
+ }, 15000);
69
+
70
+ conn.on("open", () => {
71
+ clearTimeout(timeout);
72
+ if (this._playerId) {
73
+ const joinEvent: PlayerEvent = {
74
+ playerId: this._playerId,
75
+ action: "JOIN",
76
+ timestamp: Date.now(),
77
+ };
78
+ conn.send(joinEvent);
79
+ console.log("JOIN sent, data connection open");
80
+ resolve();
81
+ } else {
82
+ this._ownIdPromise
83
+ .then((playerId) => {
84
+ const joinEvent: PlayerEvent = {
85
+ playerId,
86
+ action: "JOIN",
87
+ timestamp: Date.now(),
88
+ };
89
+ conn.send(joinEvent);
90
+ console.log("JOIN sent, data connection open");
91
+ resolve();
92
+ })
93
+ .catch(reject);
94
+ }
61
95
  });
62
- const answerObject = {
63
- playerId: this._playerId,
64
- sdp: this._pc.localDescription,
65
- };
66
- const answerJSON = JSON.stringify(answerObject);
67
- const base64AnswerObject = btoa(answerJSON);
68
- const channel = new BroadcastChannel("touch-coop-signaling");
69
- const msg = {
70
- type: "answer",
71
- playerId: this._playerId,
72
- base64Answer: base64AnswerObject,
73
- };
74
- channel.postMessage(msg);
75
- channel.close();
76
- }
96
+
97
+ conn.on("error", (err) => {
98
+ clearTimeout(timeout);
99
+ reject(err);
100
+ });
101
+ });
102
+
103
+ conn.on("data", (data: unknown) => {
104
+ console.log("Received from host:", data);
105
+ });
106
+
107
+ conn.on("close", () => {
108
+ console.log("Data connection closed by host");
109
+ this._dataConnection = null;
110
+ });
77
111
  }
112
+
78
113
  async sendMove(button: string) {
79
- if (this._dataChannel && this._dataChannel.readyState === "open") {
80
- if (!this._playerId) {
114
+ if (this._dataConnection?.open) {
115
+ if (this._playerId === null) {
81
116
  throw new Error("Player ID is not set. Cannot send data.");
82
117
  }
83
118
  const playerEvent: PlayerEvent = {
@@ -86,10 +121,18 @@ export class Player {
86
121
  button: button,
87
122
  timestamp: Date.now(),
88
123
  };
89
- const playerEventJSON = JSON.stringify(playerEvent);
90
- this._dataChannel.send(playerEventJSON);
124
+ this._dataConnection.send(playerEvent);
91
125
  } else {
92
- console.warn("Data channel is not open. Cannot send data.");
126
+ console.warn("Data connection is not open. Cannot send move.");
93
127
  }
94
128
  }
129
+
130
+ get isConnected(): boolean {
131
+ return !!this._dataConnection?.open;
132
+ }
133
+
134
+ destroy() {
135
+ this._dataConnection?.close();
136
+ this._peer.destroy();
137
+ }
95
138
  }
package/vite.config.ts CHANGED
@@ -18,18 +18,14 @@ export default defineConfig({
18
18
  emptyOutDir: true,
19
19
  rollupOptions: {
20
20
  external: (id) => {
21
- // For IIFE (browser), bundle all dependencies (including qrcode)
22
21
  if (/qrcode/.test(id)) {
23
- // Only externalize for es/cjs
24
22
  const format = process.env.BUILD_FORMAT;
25
23
  return format === "es" || format === "cjs";
26
24
  }
27
25
  return false;
28
26
  },
29
27
  output: {
30
- globals: {
31
- // No external globals needed for IIFE
32
- },
28
+ globals: {},
33
29
  },
34
30
  },
35
31
  },
@@ -1,3 +0,0 @@
1
- export declare function compressOfferData(originalText: string): Promise<string>;
2
- export declare function decompressOfferData(base64Input: string): Promise<string>;
3
- //# sourceMappingURL=encoding.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"encoding.d.ts","sourceRoot":"","sources":["../src/encoding.ts"],"names":[],"mappings":"AAAA,wBAAsB,iBAAiB,CAAC,YAAY,EAAE,MAAM,mBA0B3D;AAED,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,CAiBjB"}