@makefully/platypus 4.1.20 → 4.1.22

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": "@makefully/platypus",
3
- "version": "4.1.20",
3
+ "version": "4.1.22",
4
4
  "description": "2D tile-based HTML5 game engine with a component-based entity model",
5
5
  "license": "MIT",
6
6
  "contributors": [
package/src/Game.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /* global document, platypus, window */
2
2
  import {Application} from 'springroll';
3
- import {CullerPlugin, Application as PixiApplication, Ticker, extensions} from 'pixi.js';
3
+ import {CullerPlugin, Application as PixiApplication, Rectangle, Ticker, extensions} from 'pixi.js';
4
4
  import {arrayCache, greenSlice, greenSplice, union} from './utils/array.js';
5
5
  import Data from './Data.js';
6
6
  import Entity from './Entity.js';
@@ -224,6 +224,9 @@ class Game extends Messenger {
224
224
  this.ticker.remove(this.tickInstance);
225
225
  this.paused = true;
226
226
  sound.pauseAll();
227
+ // Release held keys so pause does not leave sticky input,
228
+ // and so HandlerController can pass keyboard through to HTML.
229
+ this.triggerOnChildren('blur-control');
227
230
  }
228
231
  } else {
229
232
  if (this.paused) {
@@ -820,6 +823,82 @@ class Game extends Messenger {
820
823
  }
821
824
  return entities;
822
825
  }
826
+
827
+ /**
828
+ * Captures a screenshot of the currently visible game viewport (the canvas /
829
+ * camera view), not the full stage or world bounds.
830
+ *
831
+ * Prefer this over `renderer.extract.*(stage)` without a frame — extracting
832
+ * the bare stage uses content bounds and can include off-screen world area.
833
+ *
834
+ * @param {Object} [options]
835
+ * @param {string} [options.format='png'] Image format for data URLs (`png`, `jpg`/`jpeg`, `webp`).
836
+ * @param {number} [options.quality] Encoder quality for lossy formats (0–1).
837
+ * @param {boolean} [options.render=true] Render the stage before capturing.
838
+ * @param {string} [options.as='dataURL'] Return type: `dataURL` or `canvas`.
839
+ * @return {Promise<string|HTMLCanvasElement|null>} Screenshot, or null if unavailable.
840
+ **/
841
+ async captureViewport (options = {}) {
842
+ const
843
+ {
844
+ format = 'png',
845
+ quality,
846
+ render = true,
847
+ as = 'dataURL'
848
+ } = options,
849
+ {renderer, stage} = this;
850
+
851
+ if (!renderer || !stage) {
852
+ return null;
853
+ }
854
+
855
+ if (render) {
856
+ renderer.render(stage);
857
+ }
858
+
859
+ const
860
+ {resolution, screen} = renderer,
861
+ frame = new Rectangle(0, 0, screen.width, screen.height),
862
+ extractOptions = {
863
+ target: stage,
864
+ frame,
865
+ resolution
866
+ };
867
+
868
+ try {
869
+ if (as === 'canvas') {
870
+ return renderer.extract.canvas(extractOptions);
871
+ }
872
+
873
+ return await renderer.extract.base64({
874
+ ...extractOptions,
875
+ format,
876
+ quality
877
+ });
878
+ } catch (err) {
879
+ platypus.debug.warn('Game.captureViewport failed.', err);
880
+
881
+ // Fallback: canvas buffer after the render above (viewport-sized).
882
+ try {
883
+ if (as === 'canvas') {
884
+ return this.canvas;
885
+ }
886
+
887
+ const mime = format === 'jpg' || format === 'jpeg'
888
+ ? 'image/jpeg'
889
+ : format === 'webp'
890
+ ? 'image/webp'
891
+ : 'image/png';
892
+
893
+ return typeof quality === 'number'
894
+ ? this.canvas.toDataURL(mime, quality)
895
+ : this.canvas.toDataURL(mime);
896
+ } catch (fallbackErr) {
897
+ platypus.debug.warn('Game.captureViewport canvas fallback failed.', fallbackErr);
898
+ return null;
899
+ }
900
+ }
901
+ }
823
902
 
824
903
  /**
825
904
  * This method destroys the game.
@@ -4,6 +4,43 @@ import Gamepad from '../Gamepad.js';
4
4
  import createComponentClass from '../factory.js';
5
5
 
6
6
  const
7
+ /**
8
+ * Whether the event target is (or is inside) an editable field. Used so
9
+ * capture-phase game key handlers do not call preventDefault / emit game
10
+ * input while the user types in HTML overlays (forms, Notefully, etc.).
11
+ *
12
+ * @param {Event} event
13
+ * @return {Boolean}
14
+ */
15
+ isEditableKeyboardTarget = function (event) {
16
+ const target = event?.target;
17
+
18
+ if (!target || typeof target.closest !== 'function') {
19
+ return false;
20
+ }
21
+
22
+ if (target.isContentEditable) {
23
+ return true;
24
+ }
25
+
26
+ const tag = target.tagName;
27
+
28
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
29
+ return true;
30
+ }
31
+
32
+ return !!target.closest('input, textarea, select, [contenteditable=""], [contenteditable="true"]');
33
+ },
34
+ /**
35
+ * When the game is paused or focus is in an editable field, let the browser
36
+ * handle keys (no game events, no preventDefault).
37
+ *
38
+ * @param {Event} event
39
+ * @return {Boolean}
40
+ */
41
+ shouldPassThroughKeyboard = function (event) {
42
+ return !!(platypus.game?.paused || isEditableKeyboardTarget(event));
43
+ },
7
44
  broadcastHandleController = function (tick) {
8
45
  if (hasGamepads) {
9
46
  const
@@ -170,25 +207,33 @@ export default createComponentClass(/** @lends platypus.components.HandlerContro
170
207
  */
171
208
  initialize: function () {
172
209
  const
173
- {enableDefaultEvents} = this;
210
+ {enableDefaultEvents} = this,
211
+ wrapKeyboard = (handler) => (event) => {
212
+ if (shouldPassThroughKeyboard(event)) {
213
+ return;
214
+ }
215
+
216
+ handler(event);
217
+ };
174
218
 
175
219
  this.upsToBlur = [];
176
220
 
177
221
  if (platypus.game.settings.debug || enableDefaultEvents === 'all') { // If this is a test build, leave in the browser key combinations so debug tools can be opened as expected.
178
- this.callbackKeyDown = onDown.bind(this, 'keyboard');
179
- this.callbackKeyUp = onUp.bind(this, 'keyboard');
222
+ this.callbackKeyDown = wrapKeyboard(onDown.bind(this, 'keyboard'));
223
+ this.callbackKeyUp = wrapKeyboard(onUp.bind(this, 'keyboard'));
180
224
  } else { // Otherwise remove default browser behavior for key inputs so that they do not interfere with game-play.
181
225
  const
182
226
  preventDefault = Array.isArray(enableDefaultEvents) ? (event) => enableDefaultEvents.indexOf(event.code) === -1 ? event.preventDefault() : null : (event) => event.preventDefault();
183
227
 
184
- this.callbackKeyDown = (event) => {
228
+ this.callbackKeyDown = wrapKeyboard((event) => {
185
229
  onDown.call(this, 'keyboard', event);
186
- preventDefault(event); // default 'none' may be too aggressive - if problems arise, we may need to limit this to certain key combos that get in the way of game-play. Example: (event.metaKey && event.keyCode == 37) causes an accidental cmd key press to send the browser back a page while playing and hitting the left arrow button.
187
- };
188
- this.callbackKeyUp = (event) => {
230
+ // Still aggressive for gameplay keys, but not while paused or typing in HTML.
231
+ preventDefault(event);
232
+ });
233
+ this.callbackKeyUp = wrapKeyboard((event) => {
189
234
  onUp.call(this, 'keyboard', event);
190
235
  preventDefault(event);
191
- };
236
+ });
192
237
  }
193
238
 
194
239
  window.addEventListener('keydown', this.callbackKeyDown, true);
@@ -256,8 +301,8 @@ export default createComponentClass(/** @lends platypus.components.HandlerContro
256
301
  },
257
302
  methods: {
258
303
  destroy: function () {
259
- window.removeEventListener('keydown', this.callbackKeyDown);
260
- window.removeEventListener('keyup', this.callbackKeyUp);
304
+ window.removeEventListener('keydown', this.callbackKeyDown, true);
305
+ window.removeEventListener('keyup', this.callbackKeyUp, true);
261
306
  for (let i = 0; i < this.gamepads.length; i++) {
262
307
  if (this.gamepads[i]) {
263
308
  this.gamepads[i].recycle();