dignity.js 0.4.0 → 0.5.1

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 (38) hide show
  1. package/README.md +83 -2
  2. package/dist/dignity.cjs.js +542 -21
  3. package/dist/dignity.cjs.js.map +4 -4
  4. package/dist/dignity.esm.js +542 -21
  5. package/dist/dignity.esm.js.map +3 -3
  6. package/dist/dignity.min.js +18 -18
  7. package/docs/assets/dignity.esm.js +11205 -0
  8. package/docs/assets/favicon.svg +8 -0
  9. package/docs/chess/assets/chess-app.js +58022 -0
  10. package/docs/chess/assets/chess-app.js.map +7 -0
  11. package/docs/chess/assets/chess.css +584 -0
  12. package/docs/chess/favicon.ico +0 -0
  13. package/docs/chess/index.html +16 -0
  14. package/docs/chess/src/App.jsx +128 -0
  15. package/docs/chess/src/components/Board3D.jsx +364 -0
  16. package/docs/chess/src/components/GameView.jsx +847 -0
  17. package/docs/chess/src/components/JoinGate.jsx +68 -0
  18. package/docs/chess/src/components/LinkPanel.jsx +132 -0
  19. package/docs/chess/src/components/Lobby.jsx +154 -0
  20. package/docs/chess/src/components/MovePanel.jsx +123 -0
  21. package/docs/chess/src/lib/audio.js +50 -0
  22. package/docs/chess/src/lib/dignitySetup.js +42 -0
  23. package/docs/chess/src/lib/links.js +124 -0
  24. package/docs/chess/src/lib/localGames.js +160 -0
  25. package/docs/chess/src/lib/p2pDebug.js +192 -0
  26. package/docs/chess/src/main.jsx +5 -0
  27. package/docs/favicon.ico +0 -0
  28. package/docs/index.html +7 -3
  29. package/docs/openapi-like.json +35 -6
  30. package/examples/decentralized-chess-lite.js +52 -30
  31. package/package.json +12 -4
  32. package/src/core/dignity-p2p.js +388 -16
  33. package/src/index.js +6 -0
  34. package/src/network/peerjs-network.js +234 -0
  35. package/src/persistence/indexeddb-persistence.js +2 -0
  36. package/src/react/index.js +143 -1
  37. package/src/signaling/parse-peerjs-url.js +24 -0
  38. package/src/signaling/peerjs-signaling-provider.js +2 -8
@@ -0,0 +1,364 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import * as THREE from 'three';
3
+ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
4
+
5
+ const LIGHT_WOOD = 0xc9a66b;
6
+ const DARK_WOOD = 0x6b4423;
7
+ const FRAME_WOOD = 0x4a2f16;
8
+ const WHITE_PIECE = 0xf5f0e6;
9
+ const BLACK_PIECE = 0x1a1410;
10
+
11
+ function squareName(file, rank) {
12
+ return `${String.fromCharCode(97 + file)}${rank + 1}`;
13
+ }
14
+
15
+ function parseSquare(name) {
16
+ return {
17
+ file: name.charCodeAt(0) - 97,
18
+ rank: Number(name[1]) - 1
19
+ };
20
+ }
21
+
22
+ function boardPosition(file, rank) {
23
+ return {
24
+ x: file - 3.5,
25
+ y: 0.35,
26
+ z: rank - 3.5
27
+ };
28
+ }
29
+
30
+ function createPieceMesh(type, color, materialBase) {
31
+ const group = new THREE.Group();
32
+ const material = materialBase.clone();
33
+ material.color.setHex(color);
34
+
35
+ if (type === 'p') {
36
+ const base = new THREE.Mesh(new THREE.CylinderGeometry(0.34, 0.42, 0.18, 24), material);
37
+ const body = new THREE.Mesh(new THREE.CylinderGeometry(0.22, 0.28, 0.55, 24), material);
38
+ const head = new THREE.Mesh(new THREE.SphereGeometry(0.18, 24, 24), material);
39
+ base.position.y = 0.09;
40
+ body.position.y = 0.45;
41
+ head.position.y = 0.82;
42
+ group.add(base, body, head);
43
+ } else if (type === 'r') {
44
+ const base = new THREE.Mesh(new THREE.CylinderGeometry(0.38, 0.44, 0.22, 24), material);
45
+ const tower = new THREE.Mesh(new THREE.BoxGeometry(0.52, 0.55, 0.52), material);
46
+ const top = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.12, 0.62), material);
47
+ base.position.y = 0.11;
48
+ tower.position.y = 0.5;
49
+ top.position.y = 0.86;
50
+ group.add(base, tower, top);
51
+ } else if (type === 'n') {
52
+ const base = new THREE.Mesh(new THREE.CylinderGeometry(0.36, 0.44, 0.2, 24), material);
53
+ const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.16, 0.24, 0.55, 16), material);
54
+ const head = new THREE.Mesh(new THREE.SphereGeometry(0.2, 20, 20), material);
55
+ neck.rotation.z = 0.45;
56
+ neck.position.set(0.08, 0.58, 0);
57
+ head.position.set(0.22, 0.86, 0);
58
+ base.position.y = 0.1;
59
+ group.add(base, neck, head);
60
+ } else if (type === 'b') {
61
+ const base = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.42, 0.2, 24), material);
62
+ const body = new THREE.Mesh(new THREE.CylinderGeometry(0.14, 0.28, 0.72, 24), material);
63
+ const head = new THREE.Mesh(new THREE.SphereGeometry(0.18, 20, 20), material);
64
+ base.position.y = 0.1;
65
+ body.position.y = 0.56;
66
+ head.position.y = 0.98;
67
+ group.add(base, body, head);
68
+ } else if (type === 'q') {
69
+ const base = new THREE.Mesh(new THREE.CylinderGeometry(0.38, 0.46, 0.22, 24), material);
70
+ const body = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.3, 0.72, 24), material);
71
+ const crown = new THREE.Mesh(new THREE.TorusGeometry(0.22, 0.05, 12, 24), material);
72
+ base.position.y = 0.11;
73
+ body.position.y = 0.58;
74
+ crown.rotation.x = Math.PI / 2;
75
+ crown.position.y = 1.02;
76
+ group.add(base, body, crown);
77
+ } else {
78
+ const base = new THREE.Mesh(new THREE.CylinderGeometry(0.42, 0.48, 0.24, 24), material);
79
+ const body = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.32, 0.78, 24), material);
80
+ const crossBar = new THREE.Mesh(new THREE.BoxGeometry(0.52, 0.08, 0.12), material);
81
+ const crossVert = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.28, 0.12), material);
82
+ base.position.y = 0.12;
83
+ body.position.y = 0.62;
84
+ crossBar.position.y = 1.02;
85
+ crossVert.position.y = 1.12;
86
+ group.add(base, body, crossBar, crossVert);
87
+ }
88
+
89
+ group.traverse((child) => {
90
+ if (child.isMesh) {
91
+ child.castShadow = true;
92
+ child.receiveShadow = true;
93
+ }
94
+ });
95
+
96
+ return group;
97
+ }
98
+
99
+ function buildBoard(scene) {
100
+ const boardGroup = new THREE.Group();
101
+
102
+ for (let rank = 0; rank < 8; rank += 1) {
103
+ for (let file = 0; file < 8; file += 1) {
104
+ const isLight = (file + rank) % 2 === 0;
105
+ const square = new THREE.Mesh(
106
+ new THREE.BoxGeometry(0.98, 0.16, 0.98),
107
+ new THREE.MeshStandardMaterial({
108
+ color: isLight ? LIGHT_WOOD : DARK_WOOD,
109
+ roughness: 0.78,
110
+ metalness: 0.04
111
+ })
112
+ );
113
+ square.position.set(file - 3.5, 0.08, rank - 3.5);
114
+ square.receiveShadow = true;
115
+ square.userData.square = squareName(file, rank);
116
+ boardGroup.add(square);
117
+ }
118
+ }
119
+
120
+ const frame = new THREE.Mesh(
121
+ new THREE.BoxGeometry(9.4, 0.28, 9.4),
122
+ new THREE.MeshStandardMaterial({ color: FRAME_WOOD, roughness: 0.85, metalness: 0.02 })
123
+ );
124
+ frame.position.y = -0.08;
125
+ frame.receiveShadow = true;
126
+ boardGroup.add(frame);
127
+
128
+ scene.add(boardGroup);
129
+ return boardGroup;
130
+ }
131
+
132
+ function syncPieces(pieceGroup, fen, selectedSquare, highlightSquares, pieceMeshesRef) {
133
+ while (pieceGroup.children.length) {
134
+ pieceGroup.remove(pieceGroup.children[0]);
135
+ }
136
+ pieceMeshesRef.current = [];
137
+
138
+ const [piecePlacement] = fen.split(' ');
139
+ const rows = piecePlacement.split('/');
140
+ const pieceMaterial = new THREE.MeshStandardMaterial({
141
+ roughness: 0.42,
142
+ metalness: 0.18
143
+ });
144
+
145
+ rows.forEach((row, rowIndex) => {
146
+ let file = 0;
147
+ for (const char of row) {
148
+ if (Number.isInteger(Number(char))) {
149
+ file += Number(char);
150
+ } else {
151
+ const rank = 7 - rowIndex;
152
+ const mesh = createPieceMesh(
153
+ char.toLowerCase(),
154
+ char === char.toUpperCase() ? WHITE_PIECE : BLACK_PIECE,
155
+ pieceMaterial
156
+ );
157
+ const pos = boardPosition(file, rank);
158
+ mesh.position.set(pos.x, pos.y, pos.z);
159
+ mesh.userData.square = squareName(file, rank);
160
+ pieceGroup.add(mesh);
161
+ pieceMeshesRef.current.push(mesh);
162
+ file += 1;
163
+ }
164
+ }
165
+ });
166
+
167
+ const markerMaterial = new THREE.MeshBasicMaterial({
168
+ color: 0x5b7fff,
169
+ transparent: true,
170
+ opacity: 0.35
171
+ });
172
+ const targetMaterial = new THREE.MeshBasicMaterial({
173
+ color: 0x7ee787,
174
+ transparent: true,
175
+ opacity: 0.38
176
+ });
177
+
178
+ if (selectedSquare) {
179
+ const { file, rank } = parseSquare(selectedSquare);
180
+ const marker = new THREE.Mesh(new THREE.CylinderGeometry(0.38, 0.38, 0.05, 32), markerMaterial);
181
+ const pos = boardPosition(file, rank);
182
+ marker.position.set(pos.x, 0.22, pos.z);
183
+ pieceGroup.add(marker);
184
+ }
185
+
186
+ highlightSquares.forEach((square) => {
187
+ const { file, rank } = parseSquare(square);
188
+ const marker = new THREE.Mesh(new THREE.CylinderGeometry(0.32, 0.32, 0.05, 32), targetMaterial);
189
+ const pos = boardPosition(file, rank);
190
+ marker.position.set(pos.x, 0.21, pos.z);
191
+ pieceGroup.add(marker);
192
+ });
193
+ }
194
+
195
+ function squareFromHit(object) {
196
+ let current = object;
197
+ while (current) {
198
+ if (current.userData?.square) {
199
+ return current.userData.square;
200
+ }
201
+ current = current.parent;
202
+ }
203
+ return null;
204
+ }
205
+
206
+ export default function Board3D({
207
+ fen,
208
+ selectedSquare,
209
+ legalTargets,
210
+ onSquareClick,
211
+ orientation = 'w',
212
+ interactive = false
213
+ }) {
214
+ const mountRef = useRef(null);
215
+ const sceneRef = useRef(null);
216
+ const pieceGroupRef = useRef(null);
217
+ const raycasterRef = useRef(new THREE.Raycaster());
218
+ const pointerRef = useRef(new THREE.Vector2());
219
+ const squaresRef = useRef([]);
220
+ const pieceMeshesRef = useRef([]);
221
+ const clickHandlerRef = useRef(onSquareClick);
222
+ const interactiveRef = useRef(interactive);
223
+
224
+ useEffect(() => {
225
+ clickHandlerRef.current = onSquareClick;
226
+ }, [onSquareClick]);
227
+
228
+ useEffect(() => {
229
+ interactiveRef.current = interactive;
230
+ }, [interactive]);
231
+
232
+ useEffect(() => {
233
+ const mount = mountRef.current;
234
+ const scene = new THREE.Scene();
235
+ scene.background = new THREE.Color(0x120d0a);
236
+ scene.fog = new THREE.Fog(0x120d0a, 14, 28);
237
+
238
+ const camera = new THREE.PerspectiveCamera(42, mount.clientWidth / mount.clientHeight, 0.1, 100);
239
+ camera.position.set(orientation === 'w' ? 0 : 0, 8.5, orientation === 'w' ? 8.5 : -8.5);
240
+
241
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
242
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
243
+ renderer.setSize(mount.clientWidth, mount.clientHeight);
244
+ renderer.shadowMap.enabled = true;
245
+ mount.appendChild(renderer.domElement);
246
+
247
+ const controls = new OrbitControls(camera, renderer.domElement);
248
+ controls.enablePan = false;
249
+ controls.minDistance = 6;
250
+ controls.maxDistance = 16;
251
+ controls.maxPolarAngle = Math.PI / 2.1;
252
+ controls.target.set(0, 0.2, 0);
253
+ controls.enableRotate = true;
254
+
255
+ scene.add(new THREE.AmbientLight(0xfff1dd, 0.45));
256
+ const keyLight = new THREE.DirectionalLight(0xffe7c4, 1.15);
257
+ keyLight.position.set(6, 12, 8);
258
+ keyLight.castShadow = true;
259
+ scene.add(keyLight);
260
+ const fill = new THREE.PointLight(0x8ec5ff, 0.35, 30);
261
+ fill.position.set(-5, 6, -4);
262
+ scene.add(fill);
263
+
264
+ const boardGroup = buildBoard(scene);
265
+ squaresRef.current = boardGroup.children.filter((child) => child.userData.square);
266
+
267
+ const pieceGroup = new THREE.Group();
268
+ scene.add(pieceGroup);
269
+ pieceGroupRef.current = pieceGroup;
270
+ sceneRef.current = scene;
271
+
272
+ let frameId;
273
+ const animate = () => {
274
+ frameId = requestAnimationFrame(animate);
275
+ controls.update();
276
+ renderer.render(scene, camera);
277
+ };
278
+ animate();
279
+
280
+ const handleResize = () => {
281
+ if (!mount.clientWidth || !mount.clientHeight) {
282
+ return;
283
+ }
284
+ camera.aspect = mount.clientWidth / mount.clientHeight;
285
+ camera.updateProjectionMatrix();
286
+ renderer.setSize(mount.clientWidth, mount.clientHeight);
287
+ };
288
+ window.addEventListener('resize', handleResize);
289
+
290
+ const resizeObserver = typeof ResizeObserver !== 'undefined'
291
+ ? new ResizeObserver(handleResize)
292
+ : null;
293
+ resizeObserver?.observe(mount);
294
+
295
+ let pointerDown = null;
296
+
297
+ const pickSquare = (clientX, clientY) => {
298
+ const rect = renderer.domElement.getBoundingClientRect();
299
+ pointerRef.current.x = ((clientX - rect.left) / rect.width) * 2 - 1;
300
+ pointerRef.current.y = -((clientY - rect.top) / rect.height) * 2 + 1;
301
+ raycasterRef.current.setFromCamera(pointerRef.current, camera);
302
+
303
+ const targets = [...squaresRef.current, ...pieceMeshesRef.current];
304
+ const hits = raycasterRef.current.intersectObjects(targets, true);
305
+ for (const hit of hits) {
306
+ const square = squareFromHit(hit.object);
307
+ if (square) {
308
+ return square;
309
+ }
310
+ }
311
+ return null;
312
+ };
313
+
314
+ const handlePointerDown = (event) => {
315
+ pointerDown = { x: event.clientX, y: event.clientY };
316
+ };
317
+
318
+ const handlePointerUp = (event) => {
319
+ if (!interactiveRef.current || !pointerDown) {
320
+ pointerDown = null;
321
+ return;
322
+ }
323
+
324
+ const dx = event.clientX - pointerDown.x;
325
+ const dy = event.clientY - pointerDown.y;
326
+ pointerDown = null;
327
+
328
+ if ((dx * dx) + (dy * dy) > 36) {
329
+ return;
330
+ }
331
+
332
+ const square = pickSquare(event.clientX, event.clientY);
333
+ if (square) {
334
+ clickHandlerRef.current?.(square);
335
+ }
336
+ };
337
+
338
+ renderer.domElement.addEventListener('pointerdown', handlePointerDown);
339
+ renderer.domElement.addEventListener('pointerup', handlePointerUp);
340
+
341
+ return () => {
342
+ cancelAnimationFrame(frameId);
343
+ window.removeEventListener('resize', handleResize);
344
+ resizeObserver?.disconnect();
345
+ renderer.domElement.removeEventListener('pointerdown', handlePointerDown);
346
+ renderer.domElement.removeEventListener('pointerup', handlePointerUp);
347
+ controls.dispose();
348
+ renderer.dispose();
349
+ mount.innerHTML = '';
350
+ };
351
+ }, [orientation]);
352
+
353
+ useEffect(() => {
354
+ if (pieceGroupRef.current) {
355
+ syncPieces(pieceGroupRef.current, fen, selectedSquare, legalTargets, pieceMeshesRef);
356
+ }
357
+ }, [fen, selectedSquare, legalTargets]);
358
+
359
+ return (
360
+ <div className={`board-3d${interactive ? ' board-3d--interactive' : ''}`} ref={mountRef}>
361
+ {interactive ? <p className="board-3d__hint">Click pieces or squares to move</p> : null}
362
+ </div>
363
+ );
364
+ }