castle-web-cli 0.4.74 → 0.4.75

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.
@@ -1 +1 @@
1
- export declare const COMMON_INSTRUCTIONS = "## Touch controls (every deck)\n\n- **Build for touch from the start.** Every deck must be fully playable on a touchscreen with no physical keyboard \u2014 design the touch controls first, not as an afterthought. Keyboard input is still fine on top, for shortcuts or typing. A touch control that only mirrors keys (e.g. a d-pad over the arrow keys) may hide itself when a hardware keyboard is present. Use good judgement.\n";
1
+ export declare const COMMON_INSTRUCTIONS = "## Touch controls (every deck)\n\n- **Playable on a touchscreen, with only the controls the game actually needs.** Castle decks are played on phones, so whatever input a game does use must work by touch \u2014 direct tap/drag on the game itself wherever possible, and on-screen buttons only where the mechanics genuinely call for them. Do NOT add controls a game doesn't need: never drop in a generic d-pad or movement overlay by default. Prefer touching the game directly over an overlay that just mirrors keyboard keys. Keyboard input is fine to support on top for desktop play. Match the controls to the actual mechanics \u2014 a game with no directional movement should have no movement controls at all.\n";
@@ -4,5 +4,5 @@
4
4
  // (e.g. Space being reserved for play/stop) live in each kit's own CLAUDE.md.
5
5
  export const COMMON_INSTRUCTIONS = `## Touch controls (every deck)
6
6
 
7
- - **Build for touch from the start.** Every deck must be fully playable on a touchscreen with no physical keyboarddesign the touch controls first, not as an afterthought. Keyboard input is still fine on top, for shortcuts or typing. A touch control that only mirrors keys (e.g. a d-pad over the arrow keys) may hide itself when a hardware keyboard is present. Use good judgement.
7
+ - **Playable on a touchscreen, with only the controls the game actually needs.** Castle decks are played on phones, so whatever input a game does use must work by touch direct tap/drag on the game itself wherever possible, and on-screen buttons only where the mechanics genuinely call for them. Do NOT add controls a game doesn't need: never drop in a generic d-pad or movement overlay by default. Prefer touching the game directly over an overlay that just mirrors keyboard keys. Keyboard input is fine to support on top for desktop play. Match the controls to the actual mechanics a game with no directional movement should have no movement controls at all.
8
8
  `;
@@ -144,7 +144,7 @@ if (scene.keys.has('KeyX')) /* launch ball */ ;
144
144
 
145
145
  **Space is reserved** — the editor binds it to the play/stop toggle, so don't bind Space to a gameplay action (jump / shoot / launch / ...). Use arrows, WASD, letter keys, or on-screen buttons instead.
146
146
 
147
- For HUD text use a behavior's `ui` hook (returns React); for in-world text or shapes, draw with `ctx` from `draw`. The deck has TouchControls overlay support out of the box for mobile play (arrow keys + a button); no setup needed.
147
+ For HUD text use a behavior's `ui` hook (returns React); for in-world text or shapes, draw with `ctx` from `draw`.
148
148
 
149
149
  ## Common breakout-shaped recipe (sketch)
150
150
 
@@ -1,7 +1,6 @@
1
1
  import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
2
  import { renderSpriteFrame } from '../engine/pxart';
3
3
  import { basename, formatJson, parseJsonFile } from '../engine/files';
4
- import { TouchControls } from '../engine/TouchControls';
5
4
  import {
6
5
  addActor,
7
6
  arrangeActors,
@@ -90,7 +89,6 @@ export function SceneEditor({
90
89
  const showMulti = selectedActorIds.length > 1 || multiSelectMode;
91
90
  const inspectorSheet = useSelectionInspectorSheet(true);
92
91
  const { value: sceneData, error } = parseJsonFile(path, text);
93
- const getRuntimeKeys = useCallback(() => runtimeRef.current?.keys ?? null, []);
94
92
  const getRuntime = useCallback(() => runtimeRef.current, []);
95
93
  useScenePlayLoop({
96
94
  sceneData,
@@ -288,7 +286,6 @@ export function SceneEditor({
288
286
  </aside>
289
287
  </div>
290
288
  </EditorBody>
291
- <TouchControls getKeys={getRuntimeKeys} visible={isPlaying} />
292
289
  </>
293
290
  );
294
291
  }
@@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
  import { configureSceneCanvas, makeScene } from './scene';
4
4
  import { SceneUI } from './SceneUI';
5
- import { TouchControls } from './TouchControls';
6
5
  import { cx, Icon, styles } from './ui';
7
6
  import { usePlayLogs } from './playConsole';
8
7
  // Engine-level scene player: mount a `SceneRuntime` against a canvas, wire
@@ -15,7 +14,6 @@ export function ScenePlayer({ sceneData, sprites, behaviorClasses, onFirstFrame
15
14
  // Bumping this re-runs the mount effect below, which tears down the running
16
15
  // loop/runtime and rebuilds a fresh scene from the same data -- a clean restart.
17
16
  const [runToken, setRunToken] = useState(0);
18
- const getKeys = useCallback(() => runtimeRef.current?.keys ?? null, []);
19
17
  const getRuntime = useCallback(() => runtimeRef.current, []);
20
18
  const restart = useCallback(() => setRunToken((token) => token + 1), []);
21
19
  useEffect(() => {
@@ -87,7 +85,6 @@ export function ScenePlayer({ sceneData, sprites, behaviorClasses, onFirstFrame
87
85
  style={{ width: '100%', height: '100%', display: 'block', outline: 'none' }}
88
86
  />
89
87
  <SceneUI getRuntime={getRuntime} />
90
- <TouchControls getKeys={getKeys} />
91
88
  </div>
92
89
  {/* Portal to <body> so the console drawer escapes the SDK's card-sized,
93
90
  transformed `#root > *` wrapper and pins to the panel, outside the
@@ -0,0 +1,3 @@
1
+ allowBuilds:
2
+ '@fortawesome/fontawesome-common-types': set this to true or false
3
+ '@fortawesome/free-solid-svg-icons': set this to true or false
@@ -142,7 +142,7 @@ if (scene.keys.has('KeyJ')) /* jump */ ;
142
142
 
143
143
  **Space is reserved** — the editor binds it to the play/stop toggle, so don't bind Space to a gameplay action (jump / shoot / launch / ...). Use arrows, WASD, letter keys, or on-screen buttons instead.
144
144
 
145
- For HUD text use a behavior's `ui` hook (returns React, 500x700 design space); for in-world visuals, use Mesh/Model components or a custom `sync`. The deck has TouchControls overlay support out of the box for mobile play (arrow keys d-pad); no setup needed.
145
+ For HUD text use a behavior's `ui` hook (returns React, 500x700 design space); for in-world visuals, use Mesh/Model components or a custom `sync`.
146
146
 
147
147
  ## Common exploration-shaped recipe (sketch)
148
148
 
@@ -1,7 +1,6 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import * as THREE from 'three';
3
3
  import { basename, formatJson, parseJsonFile } from '../engine/files';
4
- import { TouchControls } from '../engine/TouchControls';
5
4
  import {
6
5
  addActor,
7
6
  defaultCameraSpec,
@@ -83,10 +82,6 @@ export function SceneEditor({
83
82
  applyScene,
84
83
  recordSnapshot: history.recordSnapshot,
85
84
  });
86
- const getRuntimeKeys = useCallback(
87
- () => viewport.playRuntimeRef.current?.keys ?? null,
88
- [viewport.playRuntimeRef]
89
- );
90
85
  const getPlayRuntime = useCallback(
91
86
  () => viewport.playRuntimeRef.current,
92
87
  [viewport.playRuntimeRef]
@@ -240,7 +235,6 @@ export function SceneEditor({
240
235
  </aside>
241
236
  </div>
242
237
  </EditorBody>
243
- <TouchControls getKeys={getRuntimeKeys} visible={isPlaying} />
244
238
  </>
245
239
  );
246
240
  }
@@ -4,7 +4,6 @@ import { makeScene, defaultCameraSpec } from './scene';
4
4
  import { applyCameraSpec, fitRendererToCanvas } from './threeUtil';
5
5
  import { attachSceneKeys, makePlayPointerHandlers, ThreeCanvas } from './SceneViewport';
6
6
  import { SceneUI } from './SceneUI';
7
- import { TouchControls } from './TouchControls';
8
7
  // Engine-level scene player: mount a `SceneRuntime` against a WebGL canvas,
9
8
  // wire keyboard / pointer input, run the update+sync+render loop, and render
10
9
  // the behavior-driven UI overlay. No game logic lives here -- behaviors and
@@ -13,7 +12,6 @@ export function ScenePlayer({ sceneData, models, behaviorClasses, onFirstFrame }
13
12
  const runtimeRef = useRef(null);
14
13
  const cameraRef = useRef(null);
15
14
  const firstFrameRef = useRef(false);
16
- const getKeys = useCallback(() => runtimeRef.current?.keys ?? null, []);
17
15
  const getRuntime = useCallback(() => runtimeRef.current, []);
18
16
  const pointerHandlers = useMemo(() => makePlayPointerHandlers(getRuntime), [getRuntime]);
19
17
  useEffect(() => {
@@ -55,7 +53,6 @@ export function ScenePlayer({ sceneData, models, behaviorClasses, onFirstFrame }
55
53
  {...pointerHandlers}
56
54
  />
57
55
  <SceneUI getRuntime={getRuntime} />
58
- <TouchControls getKeys={getKeys} />
59
56
  </div>
60
57
  );
61
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.74",
3
+ "version": "0.4.75",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"
@@ -1,136 +0,0 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
2
- // On-screen movement controls: a 4-way d-pad feeds arrow keys onto
3
- // `scene.keys` -- the exact same keyboard keys behaviors consume, no separate
4
- // input path.
5
- const KEYS = {
6
- up: 'ArrowUp',
7
- down: 'ArrowDown',
8
- left: 'ArrowLeft',
9
- right: 'ArrowRight',
10
- };
11
- export function TouchControls({ getKeys, visible = true }) {
12
- const [isTouch, setIsTouch] = useState(false);
13
- const heldRef = useRef(new Set());
14
- useEffect(() => {
15
- const params = new URLSearchParams(window.location.search);
16
- const override = params.get('touch');
17
- if (override === '1' || override === 'true') {
18
- setIsTouch(true);
19
- return undefined;
20
- }
21
- if (override === '0' || override === 'false') {
22
- setIsTouch(false);
23
- return undefined;
24
- }
25
- const mq = window.matchMedia('(pointer: coarse)');
26
- const update = () => {
27
- setIsTouch(mq.matches || 'ontouchstart' in window || navigator.maxTouchPoints > 0);
28
- };
29
- update();
30
- mq.addEventListener?.('change', update);
31
- return () => mq.removeEventListener?.('change', update);
32
- }, []);
33
- const release = useCallback(
34
- (id) => {
35
- if (!heldRef.current.has(id)) return;
36
- heldRef.current.delete(id);
37
- getKeys()?.delete(KEYS[id]);
38
- },
39
- [getKeys]
40
- );
41
- useEffect(() => {
42
- if (!visible || !isTouch) {
43
- // Drop any keys we were holding when controls hide.
44
- const keys = getKeys();
45
- if (keys) for (const id of heldRef.current) keys.delete(KEYS[id]);
46
- heldRef.current.clear();
47
- }
48
- }, [visible, isTouch, getKeys]);
49
- if (!visible || !isTouch) return null;
50
- const press = (id) => {
51
- heldRef.current.add(id);
52
- getKeys()?.add(KEYS[id]);
53
- };
54
- const button = (id, label, extra) => (
55
- <button
56
- key={id}
57
- type="button"
58
- tabIndex={-1}
59
- aria-label={id}
60
- onTouchStart={(event) => {
61
- event.preventDefault();
62
- press(id);
63
- }}
64
- onTouchEnd={(event) => {
65
- event.preventDefault();
66
- release(id);
67
- }}
68
- onTouchCancel={(event) => {
69
- event.preventDefault();
70
- release(id);
71
- }}
72
- onMouseDown={(event) => {
73
- event.preventDefault();
74
- press(id);
75
- }}
76
- onMouseUp={() => release(id)}
77
- onMouseLeave={() => release(id)}
78
- onContextMenu={(event) => event.preventDefault()}
79
- style={{ ...buttonStyle, ...extra }}>
80
- {label}
81
- </button>
82
- );
83
- return (
84
- <div style={containerStyle}>
85
- <div style={dpadStyle}>
86
- {button('up', '▲', dpadUpStyle)}
87
- {button('left', '◀', dpadLeftStyle)}
88
- {button('right', '▶', dpadRightStyle)}
89
- {button('down', '▼', dpadDownStyle)}
90
- </div>
91
- </div>
92
- );
93
- }
94
- const containerStyle = {
95
- position: 'fixed',
96
- left: 0,
97
- right: 0,
98
- bottom: 0,
99
- display: 'flex',
100
- justifyContent: 'flex-start',
101
- alignItems: 'flex-end',
102
- padding: '12px 16px calc(env(safe-area-inset-bottom, 0px) + 12px)',
103
- pointerEvents: 'none',
104
- zIndex: 50,
105
- };
106
- // 3x3 grid -- the d-pad arms occupy the cross cells, corners stay empty.
107
- const dpadStyle = {
108
- display: 'grid',
109
- gridTemplateColumns: 'repeat(3, 64px)',
110
- gridTemplateRows: 'repeat(3, 64px)',
111
- gap: 6,
112
- pointerEvents: 'auto',
113
- };
114
- const dpadUpStyle = { gridColumn: 2, gridRow: 1 };
115
- const dpadLeftStyle = { gridColumn: 1, gridRow: 2 };
116
- const dpadRightStyle = { gridColumn: 3, gridRow: 2 };
117
- const dpadDownStyle = { gridColumn: 2, gridRow: 3 };
118
- const buttonStyle = {
119
- width: 64,
120
- height: 64,
121
- borderRadius: 12,
122
- border: '1px solid rgba(255, 255, 255, 0.35)',
123
- background: 'rgba(0, 0, 0, 0.35)',
124
- color: 'rgba(255, 255, 255, 0.92)',
125
- fontSize: 24,
126
- fontWeight: 600,
127
- display: 'flex',
128
- alignItems: 'center',
129
- justifyContent: 'center',
130
- userSelect: 'none',
131
- WebkitUserSelect: 'none',
132
- WebkitTouchCallout: 'none',
133
- WebkitTapHighlightColor: 'transparent',
134
- touchAction: 'none',
135
- cursor: 'pointer',
136
- };
@@ -1,136 +0,0 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
2
- // On-screen movement controls: a 4-way d-pad feeds arrow keys onto
3
- // `scene.keys` -- the exact same keyboard keys behaviors consume, no separate
4
- // input path.
5
- const KEYS = {
6
- up: 'ArrowUp',
7
- down: 'ArrowDown',
8
- left: 'ArrowLeft',
9
- right: 'ArrowRight',
10
- };
11
- export function TouchControls({ getKeys, visible = true }) {
12
- const [isTouch, setIsTouch] = useState(false);
13
- const heldRef = useRef(new Set());
14
- useEffect(() => {
15
- const params = new URLSearchParams(window.location.search);
16
- const override = params.get('touch');
17
- if (override === '1' || override === 'true') {
18
- setIsTouch(true);
19
- return undefined;
20
- }
21
- if (override === '0' || override === 'false') {
22
- setIsTouch(false);
23
- return undefined;
24
- }
25
- const mq = window.matchMedia('(pointer: coarse)');
26
- const update = () => {
27
- setIsTouch(mq.matches || 'ontouchstart' in window || navigator.maxTouchPoints > 0);
28
- };
29
- update();
30
- mq.addEventListener?.('change', update);
31
- return () => mq.removeEventListener?.('change', update);
32
- }, []);
33
- const release = useCallback(
34
- (id) => {
35
- if (!heldRef.current.has(id)) return;
36
- heldRef.current.delete(id);
37
- getKeys()?.delete(KEYS[id]);
38
- },
39
- [getKeys]
40
- );
41
- useEffect(() => {
42
- if (!visible || !isTouch) {
43
- // Drop any keys we were holding when controls hide.
44
- const keys = getKeys();
45
- if (keys) for (const id of heldRef.current) keys.delete(KEYS[id]);
46
- heldRef.current.clear();
47
- }
48
- }, [visible, isTouch, getKeys]);
49
- if (!visible || !isTouch) return null;
50
- const press = (id) => {
51
- heldRef.current.add(id);
52
- getKeys()?.add(KEYS[id]);
53
- };
54
- const button = (id, label, extra) => (
55
- <button
56
- key={id}
57
- type="button"
58
- tabIndex={-1}
59
- aria-label={id}
60
- onTouchStart={(event) => {
61
- event.preventDefault();
62
- press(id);
63
- }}
64
- onTouchEnd={(event) => {
65
- event.preventDefault();
66
- release(id);
67
- }}
68
- onTouchCancel={(event) => {
69
- event.preventDefault();
70
- release(id);
71
- }}
72
- onMouseDown={(event) => {
73
- event.preventDefault();
74
- press(id);
75
- }}
76
- onMouseUp={() => release(id)}
77
- onMouseLeave={() => release(id)}
78
- onContextMenu={(event) => event.preventDefault()}
79
- style={{ ...buttonStyle, ...extra }}>
80
- {label}
81
- </button>
82
- );
83
- return (
84
- <div style={containerStyle}>
85
- <div style={dpadStyle}>
86
- {button('up', '▲', dpadUpStyle)}
87
- {button('left', '◀', dpadLeftStyle)}
88
- {button('right', '▶', dpadRightStyle)}
89
- {button('down', '▼', dpadDownStyle)}
90
- </div>
91
- </div>
92
- );
93
- }
94
- const containerStyle = {
95
- position: 'fixed',
96
- left: 0,
97
- right: 0,
98
- bottom: 0,
99
- display: 'flex',
100
- justifyContent: 'flex-start',
101
- alignItems: 'flex-end',
102
- padding: '12px 16px calc(env(safe-area-inset-bottom, 0px) + 12px)',
103
- pointerEvents: 'none',
104
- zIndex: 50,
105
- };
106
- // 3x3 grid -- the d-pad arms occupy the cross cells, corners stay empty.
107
- const dpadStyle = {
108
- display: 'grid',
109
- gridTemplateColumns: 'repeat(3, 64px)',
110
- gridTemplateRows: 'repeat(3, 64px)',
111
- gap: 6,
112
- pointerEvents: 'auto',
113
- };
114
- const dpadUpStyle = { gridColumn: 2, gridRow: 1 };
115
- const dpadLeftStyle = { gridColumn: 1, gridRow: 2 };
116
- const dpadRightStyle = { gridColumn: 3, gridRow: 2 };
117
- const dpadDownStyle = { gridColumn: 2, gridRow: 3 };
118
- const buttonStyle = {
119
- width: 64,
120
- height: 64,
121
- borderRadius: 12,
122
- border: '1px solid rgba(255, 255, 255, 0.35)',
123
- background: 'rgba(0, 0, 0, 0.35)',
124
- color: 'rgba(255, 255, 255, 0.92)',
125
- fontSize: 24,
126
- fontWeight: 600,
127
- display: 'flex',
128
- alignItems: 'center',
129
- justifyContent: 'center',
130
- userSelect: 'none',
131
- WebkitUserSelect: 'none',
132
- WebkitTouchCallout: 'none',
133
- WebkitTapHighlightColor: 'transparent',
134
- touchAction: 'none',
135
- cursor: 'pointer',
136
- };