homegames-client 0.1.1 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homegames-client",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Embeddable browser client for Homegames",
5
5
  "main": "src/index.js",
6
6
  "browser": "dist/homegames-client.js",
@@ -13,8 +13,8 @@
13
13
  "license": "GPL-3.0",
14
14
  "dependencies": {
15
15
  "buffer": "^6.0.3",
16
- "homegames-common": "1.7.1",
17
- "homepad": "0.2.0",
16
+ "homegames-common": "1.7.2",
17
+ "homepad": "0.2.1",
18
18
  "qrcode-generator": "^2.0.4",
19
19
  "squish-142": "npm:squishjs@1.4.2",
20
20
  "squish-143": "npm:squishjs@1.4.3",
@@ -57,6 +57,15 @@ const loadGameClass = ({ files, entryPoint }) => {
57
57
  moduleCache[path] = module;
58
58
 
59
59
  const source = normalized[path];
60
+ if (path.endsWith('.json')) {
61
+ try {
62
+ module.exports = JSON.parse(source);
63
+ return module.exports;
64
+ } catch (err) {
65
+ delete moduleCache[path];
66
+ throw new Error('Invalid JSON module ' + path + ': ' + err.message);
67
+ }
68
+ }
60
69
  const fn = new Function('module', 'exports', 'require', '__filename', '__dirname', source);
61
70
  try {
62
71
  fn(module, module.exports, requireShim, path, dir);
package/src/game-view.js CHANGED
@@ -11,7 +11,13 @@ class GameView {
11
11
  container.innerHTML = '';
12
12
  Object.assign(container.style, { position: 'relative', overflow: 'hidden', background: '#000' });
13
13
  this.canvas = document.createElement('canvas');
14
- Object.assign(this.canvas.style, { display: 'block', margin: '0 auto', cursor: 'default' });
14
+ Object.assign(this.canvas.style, {
15
+ display: 'block', margin: '0 auto', cursor: 'default',
16
+ // Keep iOS tap highlights and long-press selection off the game surface.
17
+ webkitTapHighlightColor: 'transparent',
18
+ webkitTouchCallout: 'none',
19
+ webkitUserSelect: 'none', userSelect: 'none',
20
+ });
15
21
  this.cursor = null;
16
22
  this.cursorOverride = null;
17
23
  this._cursorRaster = null;
package/src/index.js CHANGED
@@ -111,7 +111,7 @@ class HomegamesClient {
111
111
  onVisibility: this._shareOptions.room ? visibility => this._getRtcHost().setVisibility(visibility) : null,
112
112
  onOpen: () => this._localSession?.inputHandler?.detach(),
113
113
  onClose: () => this._localSession?.inputHandler?.attach(),
114
- } : null, { onExports: () => this.viewExports(),
114
+ } : null, { author: game.author, onExports: () => this.viewExports(),
115
115
  onOpen: () => this.setInputEnabled(false), onClose: () => this.setInputEnabled(true) });
116
116
  this._configureToolbar(chrome.toolbar);
117
117
  return chrome;
package/src/input.js CHANGED
@@ -106,6 +106,7 @@ class InputHandler {
106
106
  this._pressClickSent = false;
107
107
  this.mousePos = this._hasPointerLock() ? this._canvasCenter() : [e.clientX, e.clientY];
108
108
  this._lastClientPos = this.mousePos.slice();
109
+ this._pressOrigin = this.mousePos.slice();
109
110
  this.assetManager.unlockAudio();
110
111
  const captureGesture = this._canCapturePointer() && !this.spectating &&
111
112
  e.target === this.canvas && e.button === 0 && !e.sourceCapabilities?.firesTouchEvents &&
@@ -178,6 +179,11 @@ class InputHandler {
178
179
  return;
179
180
  }
180
181
  e.preventDefault();
182
+ // Claiming a canvas touch suppresses the synthesized mouse click
183
+ // that would normally focus an embedded game. Treat this direct
184
+ // gesture as input focus so the first tap can reach the game too.
185
+ // Page-control touches return above; blur still releases input.
186
+ this._focused = true;
181
187
  this._touchActive = true;
182
188
  clearTimeout(this.clickStopper); this.clickStopper = null;
183
189
  this.assetManager.liveMedia?.hold('pointer');
@@ -185,6 +191,7 @@ class InputHandler {
185
191
  this._pressClickSent = false;
186
192
  this.mousePos = [e.touches[0].clientX + window.scrollX, e.touches[0].clientY + window.scrollY];
187
193
  this._lastClientPos = [e.touches[0].clientX, e.touches[0].clientY];
194
+ this._pressOrigin = this.mousePos.slice();
188
195
  this.assetManager.unlockAudio();
189
196
  };
190
197
 
@@ -229,7 +236,7 @@ class InputHandler {
229
236
  // report the release at the last known position so the server can
230
237
  // close out the press (drag-end / offClick).
231
238
  if (this.mouseDown && this._lastClientPos) {
232
- this._sendMouseUp(this._lastClientPos[0], this._lastClientPos[1]);
239
+ this._sendMouseUp(this._lastClientPos[0], this._lastClientPos[1], true);
233
240
  }
234
241
  this.mouseDown = false;
235
242
  };
@@ -408,8 +415,7 @@ class InputHandler {
408
415
  // click contract those games' interactions were built on.
409
416
  if (this.mouseDown && !this.clickStopper) {
410
417
  if (!this._pressClickSent) {
411
- this._pressClickSent = true;
412
- this._handleClick(clickInfo, true);
418
+ this._sendPendingClick(clickInfo);
413
419
  } else if (this._hasPointerLock()) {
414
420
  // Captured presses fire once; relative look is sent separately.
415
421
  } else if (this._usesSingleClickContract()) {
@@ -476,7 +482,7 @@ class InputHandler {
476
482
  _releasePointerPress() {
477
483
  this._pointerLockOnRelease = false;
478
484
  this.assetManager.liveMedia?.release('pointer');
479
- if (this.mouseDown && this._lastClientPos) this._sendMouseUp(...this._lastClientPos);
485
+ if (this.mouseDown && this._lastClientPos) this._sendMouseUp(...this._lastClientPos, true);
480
486
  this.mouseDown = false;
481
487
  this._pressClickSent = false;
482
488
  this.mousePos = null;
@@ -524,7 +530,7 @@ class InputHandler {
524
530
  event.key === 'Escape' || event.key === 'Shift';
525
531
  }
526
532
 
527
- _sendMouseUp(clientX, clientY) {
533
+ _sendMouseUp(clientX, clientY, cancelled = false) {
528
534
  const rect = this.canvas.getBoundingClientRect();
529
535
  const clickX = (clientX - rect.left) / rect.width * 100;
530
536
  const clickY = (clientY - rect.top) / rect.height * 100;
@@ -533,7 +539,16 @@ class InputHandler {
533
539
  // (drag-end / offClick), even ones that finish outside the canvas.
534
540
  const x = Math.min(99.99, Math.max(0, clickX));
535
541
  const y = Math.min(99.99, Math.max(0, clickY));
536
- this.send(JSON.stringify({ type: 'mouseup', data: { x, y } }));
542
+ // Preserve the last movement even when the whole drag happened between
543
+ // animation frames or inside the 30ms repeat throttle.
544
+ const origin = this._pressOrigin;
545
+ if (!cancelled && origin && this._pressClickSent && !this._hasPointerLock() &&
546
+ (Math.abs(clientX - origin[0]) > .5 || Math.abs(clientY - origin[1]) > .5)) {
547
+ this._sendMouseMove(clientX, clientY);
548
+ }
549
+ this._pressOrigin = null;
550
+ // End the press on blur/touchcancel without committing a drag-and-drop.
551
+ this.send(JSON.stringify({ type: 'mouseup', data: { x, y, ...(cancelled ? { cancelled: true } : {}) } }));
537
552
  }
538
553
 
539
554
  // Whether the game's squish version uses the single-click contract
@@ -572,7 +587,15 @@ class InputHandler {
572
587
  _sendPendingClick(clickInfo) {
573
588
  if (!this.mouseDown || this._pressClickSent || this.spectating || !this._focused) return;
574
589
  this._pressClickSent = true;
590
+ // A pointer can move before the first animation frame. Its press still
591
+ // belongs to the original object, not whichever object it moved over.
592
+ const latest = this.mousePos;
593
+ if (this._pressOrigin) {
594
+ this.mousePos = this._pressOrigin.slice();
595
+ clickInfo = this._canClick(this.mousePos[0], this.mousePos[1]);
596
+ }
575
597
  this._handleClick(clickInfo, true);
598
+ if (this._attached) this.mousePos = latest;
576
599
  }
577
600
 
578
601
  _promptTextInput(nodeId) {
@@ -7,7 +7,7 @@ const createLocalChrome = (containerId, title, onHome, share = null, options = {
7
7
  container.innerHTML = '';
8
8
  const releaseStyles = useFrameStyles();
9
9
  const frame = document.createElement('div'); frame.className = 'hg-session-frame';
10
- const toolbar = createPlayerToolbar(window, { title, home: onHome, status: options.status || 'Playing locally',
10
+ const toolbar = createPlayerToolbar(window, { title, author: options.author, home: onHome, status: options.status || 'Playing locally',
11
11
  onOpen: options.onOpen || share?.onOpen, onClose: options.onClose || share?.onClose });
12
12
  toolbar.bar.classList.add('hg-session-bar');
13
13
  if (options.onExports) toolbar.setAction('exports', { label: 'View exports', run: options.onExports });
@@ -15,6 +15,7 @@ const loadLocalGame = async ({ manifestUrl, gameKey, versionId, baseUrl } = {},
15
15
  }
16
16
  return {
17
17
  name: manifest.name,
18
+ author: manifest.author || manifest.createdBy,
18
19
  multiplayer: manifest.multiplayer === true,
19
20
  files: manifest.files,
20
21
  entryPoint: manifest.entryPoint,
@@ -9,6 +9,8 @@ const saves = require('homegames-common/portable-saves');
9
9
  const portableClient = require('homegames-common/portable-client');
10
10
 
11
11
  module.exports = {
12
+ ...require('homegames-common/studio-debug'),
13
+ ...require('homegames-common/studio-handoff'),
12
14
  create: files => project.createProject(files, options),
13
15
  importArchive: bytes => project.importArchive(bytes, options),
14
16
  exportArchive: project.exportArchive,
@@ -36,5 +38,6 @@ module.exports = {
36
38
  readHtmlArchive: html.readHtmlArchive,
37
39
  htmlBytesLimit: html.htmlBytesLimit,
38
40
  checkSyntax: project.checkSyntax,
41
+ thumbnailPath: project.thumbnailPath,
39
42
  prepareTrustedLocalGame: (value, settings) => prepareTrustedLocalGame(value, squishMap, settings),
40
43
  };