@caperjs/core 0.6.1 → 0.7.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.
Files changed (53) hide show
  1. package/build/index.d.mts +6 -0
  2. package/build/index.mjs +7 -0
  3. package/build/plugins/solid.mjs +30 -0
  4. package/cli/doctor.mjs +128 -0
  5. package/cli/doctor.test.mjs +154 -0
  6. package/extras/llms.txt +192 -4
  7. package/extras/skills/caper/SKILL.md +51 -3
  8. package/lib/{CaptionsPlugin-BtVJiQs_.js → CaptionsPlugin-Boj-gm3A.js} +5 -5
  9. package/lib/{CaptionsPlugin-BtVJiQs_.js.map → CaptionsPlugin-Boj-gm3A.js.map} +1 -1
  10. package/lib/{DataAdapter-BtNz4yi2.js → DataAdapter-Cra9dJ6W.js} +9 -9
  11. package/lib/{DataAdapter-BtNz4yi2.js.map → DataAdapter-Cra9dJ6W.js.map} +1 -1
  12. package/lib/{DebugRenderer-DK8FAED1.js → DebugRenderer-19S58S4h.js} +2 -2
  13. package/lib/{DebugRenderer-DK8FAED1.js.map → DebugRenderer-19S58S4h.js.map} +1 -1
  14. package/lib/{DevToolsPlugin-BS9idGRi.js → DevToolsPlugin-BTSkRsdS.js} +2 -2
  15. package/lib/{DevToolsPlugin-BS9idGRi.js.map → DevToolsPlugin-BTSkRsdS.js.map} +1 -1
  16. package/lib/{GSAPPlugin-DVDbDVP8.js → GSAPPlugin-BcL7vjyw.js} +5 -5
  17. package/lib/{GSAPPlugin-DVDbDVP8.js.map → GSAPPlugin-BcL7vjyw.js.map} +1 -1
  18. package/lib/{LayoutPlugin-qQJ5qJud.js → LayoutPlugin-BDhSngz_.js} +4 -4
  19. package/lib/{LayoutPlugin-qQJ5qJud.js.map → LayoutPlugin-BDhSngz_.js.map} +1 -1
  20. package/lib/ScreenDebugPlugin-C6pxs_3O.js +103 -0
  21. package/lib/ScreenDebugPlugin-C6pxs_3O.js.map +1 -0
  22. package/lib/{SpinePlugin-CPNRM2J1.js → SpinePlugin-CrQhaK6r.js} +2 -2
  23. package/lib/{SpinePlugin-CPNRM2J1.js.map → SpinePlugin-CrQhaK6r.js.map} +1 -1
  24. package/lib/{StatsPlugin-BHihicBl.js → StatsPlugin-Cu625dmk.js} +2 -2
  25. package/lib/{StatsPlugin-BHihicBl.js.map → StatsPlugin-Cu625dmk.js.map} +1 -1
  26. package/lib/{VoiceOverPlugin-BdTHxgku.js → VoiceOverPlugin-D4rfHFyJ.js} +5 -5
  27. package/lib/{VoiceOverPlugin-BdTHxgku.js.map → VoiceOverPlugin-D4rfHFyJ.js.map} +1 -1
  28. package/lib/caper.mjs +98 -97
  29. package/lib/caper.mjs.map +1 -1
  30. package/lib/{const-BQItSP39.js → const-BQ2iMCtc.js} +180 -180
  31. package/lib/{const-BQItSP39.js.map → const-BQ2iMCtc.js.map} +1 -1
  32. package/lib/core/Application.d.ts.map +1 -1
  33. package/lib/plugins/ScreenDebugPlugin.d.ts +22 -0
  34. package/lib/plugins/ScreenDebugPlugin.d.ts.map +1 -0
  35. package/lib/plugins/ScreenDebugPlugin.test.d.ts +2 -0
  36. package/lib/plugins/ScreenDebugPlugin.test.d.ts.map +1 -0
  37. package/lib/plugins/index.d.ts +2 -0
  38. package/lib/plugins/index.d.ts.map +1 -1
  39. package/lib/plugins/screenDebugFlag.d.ts +2 -0
  40. package/lib/plugins/screenDebugFlag.d.ts.map +1 -0
  41. package/lib/plugins/screenDebugFlag.test.d.ts +2 -0
  42. package/lib/plugins/screenDebugFlag.test.d.ts.map +1 -0
  43. package/lib/{registries-DlqHRO90.js → registries-BmDw_GV1.js} +237 -221
  44. package/lib/registries-BmDw_GV1.js.map +1 -0
  45. package/package.json +1 -1
  46. package/src/core/Application.ts +8 -0
  47. package/src/plugins/ScreenDebugPlugin.test.ts +61 -0
  48. package/src/plugins/ScreenDebugPlugin.ts +174 -0
  49. package/src/plugins/index.ts +2 -0
  50. package/src/plugins/screenDebugFlag.test.ts +45 -0
  51. package/src/plugins/screenDebugFlag.ts +26 -0
  52. package/src/version.ts +1 -1
  53. package/lib/registries-DlqHRO90.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caperjs/core",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "title": "Caper",
5
5
  "description": "An opinionated HTML game framework built on PixiJS v8",
6
6
  "bin": {
@@ -66,6 +66,7 @@ import { IFullScreenPlugin } from '../plugins/FullScreenPlugin';
66
66
  import { type IGSAPPlugin } from '../plugins/GSAPPlugin';
67
67
  import { ILookupPlugin } from '../plugins/LookupPlugin';
68
68
  import { ITimerPlugin } from '../plugins/TimerPlugin';
69
+ import { shouldEnableScreenDebug } from '../plugins/screenDebugFlag';
69
70
  import { Signal } from '../signals';
70
71
 
71
72
  type App = AppTypeOverrides['App'];
@@ -1014,6 +1015,13 @@ export class Application extends PIXIPApplication implements IApplication {
1014
1015
  namedExport: 'StatsPlugin',
1015
1016
  });
1016
1017
  }
1018
+ if (shouldEnableScreenDebug()) {
1019
+ await this.loadPlugin({
1020
+ id: 'screenDebug',
1021
+ module: () => import('../plugins/ScreenDebugPlugin'),
1022
+ namedExport: 'ScreenDebugPlugin',
1023
+ });
1024
+ }
1017
1025
  if (this.config.useVoiceover) {
1018
1026
  await this.loadPlugin({
1019
1027
  id: 'voiceover',
@@ -0,0 +1,61 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('../core', () => ({ coreFunctionRegistry: {}, coreSignalRegistry: {} }));
4
+ vi.mock('../core/Application', () => ({
5
+ Application: {
6
+ getInstance: () => ({
7
+ renderer: { canvas: null, width: 320, height: 480, resolution: 1 },
8
+ size: { width: 320, height: 480 },
9
+ resizer: { scale: 1, safeArea: { top: 0, right: 0, bottom: 0, left: 0 } },
10
+ }),
11
+ },
12
+ }));
13
+
14
+ import { ScreenDebugPlugin } from './ScreenDebugPlugin';
15
+
16
+ describe('ScreenDebugPlugin', () => {
17
+ let plugin: ScreenDebugPlugin;
18
+
19
+ beforeEach(() => {
20
+ vi.useFakeTimers();
21
+ plugin = new ScreenDebugPlugin();
22
+ });
23
+
24
+ afterEach(() => {
25
+ plugin.destroy();
26
+ vi.useRealTimers();
27
+ document.querySelectorAll('[data-caper-screen-debug]').forEach((node) => node.remove());
28
+ });
29
+
30
+ it('appends the overlay and populates metrics on a tick', () => {
31
+ plugin.initialize();
32
+ expect(document.querySelectorAll('[data-caper-screen-debug]')).toHaveLength(3);
33
+
34
+ vi.advanceTimersByTime(500);
35
+
36
+ const panel = document.querySelector('[data-caper-screen-debug="panel"]');
37
+ expect(panel?.textContent).toContain('inner ');
38
+ expect(panel?.textContent).toContain('standalone ');
39
+ });
40
+
41
+ it('repopulates the panel from the interval tick, not just the initial call', () => {
42
+ plugin.initialize();
43
+ const panel = document.querySelector('[data-caper-screen-debug="panel"]')!;
44
+ panel.textContent = '';
45
+
46
+ vi.advanceTimersByTime(500);
47
+
48
+ expect(panel.textContent).toContain('inner ');
49
+ });
50
+
51
+ it('removes every overlay node and clears its interval on destroy', () => {
52
+ const clearInterval = vi.spyOn(window, 'clearInterval');
53
+ plugin.initialize();
54
+
55
+ plugin.destroy();
56
+
57
+ expect(document.querySelectorAll('[data-caper-screen-debug]')).toHaveLength(0);
58
+ expect(clearInterval).toHaveBeenCalledOnce();
59
+ expect(vi.getTimerCount()).toBe(0);
60
+ });
61
+ });
@@ -0,0 +1,174 @@
1
+ import { Plugin } from './Plugin';
2
+
3
+ type TimelineEntry = { t: number; type: string; innerH: number; vvH: number };
4
+
5
+ export class ScreenDebugPlugin extends Plugin {
6
+ public readonly id = 'screenDebug';
7
+
8
+ private _panel: HTMLDivElement | null = null;
9
+ private _visibleBottom: HTMLDivElement | null = null;
10
+ private _canvasBottom: HTMLDivElement | null = null;
11
+ private _safeAreaProbe: HTMLDivElement | null = null;
12
+ private _startedAt = 0;
13
+ private _timeline: TimelineEntry[] = [];
14
+ private _initialVvH = 0;
15
+
16
+ public initialize(): void {
17
+ if (typeof document === 'undefined') {
18
+ return;
19
+ }
20
+
21
+ this._startedAt = performance.now();
22
+ this._initialVvH = window.visualViewport?.height ?? window.innerHeight;
23
+
24
+ this._panel = document.createElement('div');
25
+ this._panel.dataset.caperScreenDebug = 'panel';
26
+ this._panel.style.cssText =
27
+ 'position:fixed;top:8px;left:8px;z-index:999999;pointer-events:none;font:10px/1.4 monospace;' +
28
+ 'color:#9f9;background:rgba(0,0,0,.75);padding:6px 8px;border-radius:4px;max-width:86vw;white-space:pre;';
29
+ this._visibleBottom = this._makeMarker('red', 'visible-bottom');
30
+ this._canvasBottom = this._makeMarker('cyan', 'canvas-bottom');
31
+ document.body.append(this._panel, this._visibleBottom, this._canvasBottom);
32
+
33
+ this.listen(window, 'resize', () => this._onViewportEvent('resize'));
34
+ this.listen(window, 'orientationchange', () => this._onViewportEvent('orientationchange'));
35
+ if (window.visualViewport) {
36
+ this.listen(window.visualViewport, 'resize', () => this._onViewportEvent('vv.resize'));
37
+ }
38
+
39
+ const interval = window.setInterval(() => this._update(), 500);
40
+ this.addDisposer(
41
+ () => window.clearInterval(interval),
42
+ () => {
43
+ this._panel?.remove();
44
+ this._visibleBottom?.remove();
45
+ this._canvasBottom?.remove();
46
+ this._safeAreaProbe?.remove();
47
+ this._panel = this._visibleBottom = this._canvasBottom = this._safeAreaProbe = null;
48
+ },
49
+ );
50
+ this._update();
51
+ }
52
+
53
+ private _makeMarker(color: string, name: string): HTMLDivElement {
54
+ const marker = document.createElement('div');
55
+ marker.dataset.caperScreenDebug = name;
56
+ marker.style.cssText =
57
+ `position:fixed;left:0;width:100%;height:2px;background:${color};` +
58
+ 'z-index:999999;pointer-events:none;';
59
+ return marker;
60
+ }
61
+
62
+ private _onViewportEvent(type: string): void {
63
+ const innerH = window.innerHeight;
64
+ const vvH = window.visualViewport?.height ?? innerH;
65
+ this._timeline.push({ t: performance.now() - this._startedAt, type, innerH, vvH });
66
+ if (this._timeline.length > 20) {
67
+ this._timeline.shift();
68
+ }
69
+ this._update();
70
+ }
71
+
72
+ private _update(): void {
73
+ if (!this._panel || !this._visibleBottom || !this._canvasBottom) {
74
+ return;
75
+ }
76
+
77
+ const vv = window.visualViewport;
78
+ const canvas = this._getCanvas();
79
+ const canvasRect = canvas?.getBoundingClientRect();
80
+ const safeArea = this._measureSafeAreaCssPx();
81
+ const renderer = this._getRenderer();
82
+ const appSize = this._getAppSize();
83
+ const resizer = this._getResizer();
84
+ const r = (n: number) => Math.round(n * 10) / 10;
85
+ const viewportUnits = ['dvh', 'svh', 'lvh', 'vh'].map((unit) => `${unit} ${r(this._measureViewportUnit(unit))}`);
86
+ const events = this._timeline.map((event, index) => {
87
+ const prior = this._timeline[index - 1];
88
+ return `+${Math.round(event.t)}ms ${event.type} ${r(prior?.vvH ?? this._initialVvH)}→${r(event.vvH)}`;
89
+ });
90
+
91
+ this._panel.textContent = [
92
+ `inner ${window.innerWidth}x${window.innerHeight}`,
93
+ `client ${document.documentElement.clientHeight}`,
94
+ `vv ${vv ? `${r(vv.width)}x${r(vv.height)} scale ${vv.scale} offsetTop ${r(vv.offsetTop)}` : 'n/a'}`,
95
+ ...viewportUnits,
96
+ `screen ${window.screen.width}x${window.screen.height}`,
97
+ `dpr ${window.devicePixelRatio}`,
98
+ `standalone ${window.matchMedia('(display-mode: standalone)').matches}`,
99
+ `env ${safeArea.top},${safeArea.right},${safeArea.bottom},${safeArea.left}`,
100
+ `canvas ${canvasRect ? `top ${r(canvasRect.top)} bottom ${r(canvasRect.bottom)} height ${r(canvasRect.height)}` : 'n/a'} style ${canvas?.style.height || 'n/a'}`,
101
+ `renderer ${renderer ? `${renderer.width}x${renderer.height} resolution ${renderer.resolution}` : 'n/a'}`,
102
+ `app.size ${appSize ? `${appSize.width}x${appSize.height}` : 'n/a'}`,
103
+ `resizer ${resizer ? `scale ${Math.round(resizer.scale * 1000) / 1000} safeArea ${JSON.stringify(resizer.safeArea)}` : 'n/a'}`,
104
+ ...events,
105
+ ].join('\n');
106
+
107
+ this._visibleBottom.style.top = `${(vv ? vv.offsetTop + vv.height : window.innerHeight) - 2}px`;
108
+ if (canvasRect) {
109
+ this._canvasBottom.style.top = `${canvasRect.bottom - 2}px`;
110
+ this._canvasBottom.style.display = 'block';
111
+ } else {
112
+ this._canvasBottom.style.display = 'none';
113
+ }
114
+ }
115
+
116
+ private _measureViewportUnit(unit: string): number {
117
+ const probe = document.createElement('div');
118
+ probe.style.cssText = `position:fixed;height:100${unit};visibility:hidden;pointer-events:none;`;
119
+ document.body.appendChild(probe);
120
+ const height = probe.getBoundingClientRect().height;
121
+ probe.remove();
122
+ return height;
123
+ }
124
+
125
+ private _measureSafeAreaCssPx(): { top: number; right: number; bottom: number; left: number } {
126
+ if (!this._safeAreaProbe) {
127
+ this._safeAreaProbe = document.createElement('div');
128
+ this._safeAreaProbe.style.cssText =
129
+ 'position:fixed;top:0;left:0;width:0;height:0;visibility:hidden;pointer-events:none;' +
130
+ 'padding-top:env(safe-area-inset-top, 0px);padding-right:env(safe-area-inset-right, 0px);' +
131
+ 'padding-bottom:env(safe-area-inset-bottom, 0px);padding-left:env(safe-area-inset-left, 0px);';
132
+ document.body.appendChild(this._safeAreaProbe);
133
+ }
134
+ const style = getComputedStyle(this._safeAreaProbe);
135
+ return {
136
+ top: parseFloat(style.paddingTop) || 0,
137
+ right: parseFloat(style.paddingRight) || 0,
138
+ bottom: parseFloat(style.paddingBottom) || 0,
139
+ left: parseFloat(style.paddingLeft) || 0,
140
+ };
141
+ }
142
+
143
+ private _getCanvas(): HTMLCanvasElement | null {
144
+ try {
145
+ return (this.app.renderer?.canvas as HTMLCanvasElement | undefined) ?? null;
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ private _getRenderer(): { width: number; height: number; resolution: number } | null {
152
+ try {
153
+ return this.app.renderer ?? null;
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
158
+
159
+ private _getAppSize(): { width: number; height: number } | null {
160
+ try {
161
+ return this.app.size ?? null;
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ private _getResizer(): { scale: number; safeArea: unknown } | null {
168
+ try {
169
+ return this.app.resizer ?? null;
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+ }
@@ -13,6 +13,8 @@ export * from './LookupPlugin';
13
13
  export * from './Plugin';
14
14
  export * from './PopupManagerPlugin';
15
15
  export * from './ResizerPlugin';
16
+ export * from './ScreenDebugPlugin';
17
+ export * from './screenDebugFlag';
16
18
  export * from './SceneManagerPlugin';
17
19
  export * from './TimerPlugin';
18
20
  export * from './WebEventsPlugin';
@@ -0,0 +1,45 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { shouldEnableScreenDebug } from './screenDebugFlag';
3
+
4
+ describe('shouldEnableScreenDebug', () => {
5
+ beforeEach(() => {
6
+ window.history.replaceState({}, '', '/');
7
+ window.localStorage.clear();
8
+ });
9
+
10
+ afterEach(() => {
11
+ vi.restoreAllMocks();
12
+ window.history.replaceState({}, '', '/');
13
+ window.localStorage.clear();
14
+ });
15
+
16
+ it('enables and persists from the query string', () => {
17
+ window.history.replaceState({}, '', '/?caper-screen-debug=1');
18
+ expect(shouldEnableScreenDebug()).toBe(true);
19
+ expect(window.localStorage.getItem('caper:screen-debug')).toBe('1');
20
+ });
21
+
22
+ it('disables and clears persistence from the query string', () => {
23
+ window.localStorage.setItem('caper:screen-debug', '1');
24
+ window.history.replaceState({}, '', '/?caper-screen-debug=0');
25
+ expect(shouldEnableScreenDebug()).toBe(false);
26
+ expect(window.localStorage.getItem('caper:screen-debug')).toBeNull();
27
+ });
28
+
29
+ it('enables from a persisted key without a query value', () => {
30
+ window.localStorage.setItem('caper:screen-debug', '1');
31
+ expect(shouldEnableScreenDebug()).toBe(true);
32
+ });
33
+
34
+ it('stays disabled without a query value or persisted key', () => {
35
+ expect(shouldEnableScreenDebug()).toBe(false);
36
+ });
37
+
38
+ it('falls back to the query value when localStorage throws', () => {
39
+ vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
40
+ throw new Error('unavailable');
41
+ });
42
+ window.history.replaceState({}, '', '/?caper-screen-debug=true');
43
+ expect(shouldEnableScreenDebug()).toBe(true);
44
+ });
45
+ });
@@ -0,0 +1,26 @@
1
+ const queryKey = 'caper-screen-debug';
2
+ const storageKey = 'caper:screen-debug';
3
+
4
+ export function shouldEnableScreenDebug(): boolean {
5
+ if (typeof window === 'undefined') {
6
+ return false;
7
+ }
8
+
9
+ const value = new URLSearchParams(window.location.search).get(queryKey);
10
+ const enabledByQuery = value === '1' || value === 'true';
11
+ const disabledByQuery = value === '0' || value === 'false';
12
+
13
+ try {
14
+ if (enabledByQuery) {
15
+ window.localStorage.setItem(storageKey, '1');
16
+ return true;
17
+ }
18
+ if (disabledByQuery) {
19
+ window.localStorage.removeItem(storageKey);
20
+ return false;
21
+ }
22
+ return window.localStorage.getItem(storageKey) === '1';
23
+ } catch {
24
+ return enabledByQuery;
25
+ }
26
+ }
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // This file is auto-generated during the build process.
2
- export const version: string = '0.6.1';
2
+ export const version: string = '0.7.0';
3
3
  export const pixiVersion: string = '8.20.0';