@wavegrid/simulator 0.1.1 → 0.2.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.
- package/LICENSE +18 -5
- package/animations.d.ts +4 -0
- package/animations.js +105 -0
- package/esm/animations.js +101 -0
- package/esm/grid.js +27 -9
- package/esm/index.js +2 -1
- package/esm/server.js +46 -8
- package/esm/ui.js +251 -20
- package/grid.d.ts +8 -2
- package/grid.js +27 -9
- package/index.d.ts +4 -2
- package/index.js +9 -6
- package/package.json +2 -2
- package/server.d.ts +2 -3
- package/server.js +44 -6
- package/ui.js +251 -20
package/LICENSE
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
The San Francisco License (SF License)
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Interweb, Inc.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Permission is granted, free of charge, to any person or organization
|
|
6
|
+
obtaining a copy of this software and associated documentation files
|
|
7
|
+
(the "Software"), to use, copy, modify, merge, publish, distribute,
|
|
8
|
+
sublicense, and/or sell copies of the Software, and to permit others
|
|
9
|
+
to whom the Software is furnished to do the same.
|
|
10
|
+
|
|
11
|
+
The only requirement is that this license notice and copyright notice
|
|
12
|
+
shall be included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
18
|
+
CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
19
|
+
TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE
|
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/animations.d.ts
ADDED
package/animations.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.animations = void 0;
|
|
4
|
+
exports.getAnimationNames = getAnimationNames;
|
|
5
|
+
const grid_1 = require("./grid");
|
|
6
|
+
exports.animations = {
|
|
7
|
+
wave: (grid, tick, attack) => {
|
|
8
|
+
for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
|
|
9
|
+
const col = i % grid_1.GRID_SIZE;
|
|
10
|
+
const hue = (tick * 2 + col * 40) % 360;
|
|
11
|
+
const bright = 60 + Math.sin(tick * 0.05 + col * 0.8) * 20;
|
|
12
|
+
(0, grid_1.setCannonTarget)(grid, i, hue, 85, bright, attack);
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
breathe: (grid, tick, attack) => {
|
|
16
|
+
const brightness = 40 + Math.sin(tick * 0.03) * 35;
|
|
17
|
+
for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
|
|
18
|
+
(0, grid_1.setCannonTarget)(grid, i, 220, 80, brightness, attack);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
rainbow: (grid, tick, attack) => {
|
|
22
|
+
for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
|
|
23
|
+
const row = Math.floor(i / grid_1.GRID_SIZE);
|
|
24
|
+
const col = i % grid_1.GRID_SIZE;
|
|
25
|
+
const hue = (tick * 1.5 + (row + col) * 25) % 360;
|
|
26
|
+
(0, grid_1.setCannonTarget)(grid, i, hue, 90, 80, attack);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
pacman: (grid, tick, attack) => {
|
|
30
|
+
// Bright dot chasing around the perimeter
|
|
31
|
+
const perimeter = getPerimeterIndices();
|
|
32
|
+
const pos = Math.floor(tick * 0.3) % perimeter.length;
|
|
33
|
+
for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
|
|
34
|
+
(0, grid_1.setCannonTarget)(grid, i, 220, 60, 15, attack);
|
|
35
|
+
}
|
|
36
|
+
// Pac-man (bright yellow)
|
|
37
|
+
const pacIdx = perimeter[pos];
|
|
38
|
+
(0, grid_1.setCannonTarget)(grid, pacIdx, 55, 95, 95, 1.0);
|
|
39
|
+
// Trail (fading)
|
|
40
|
+
for (let t = 1; t <= 3; t++) {
|
|
41
|
+
const trailPos = (pos - t + perimeter.length) % perimeter.length;
|
|
42
|
+
const trailIdx = perimeter[trailPos];
|
|
43
|
+
(0, grid_1.setCannonTarget)(grid, trailIdx, 55, 80, 70 - t * 18, 1.0);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
spiral: (grid, tick, attack) => {
|
|
47
|
+
for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
|
|
48
|
+
const row = Math.floor(i / grid_1.GRID_SIZE);
|
|
49
|
+
const col = i % grid_1.GRID_SIZE;
|
|
50
|
+
const cx = col - 3, cy = row - 3;
|
|
51
|
+
const angle = Math.atan2(cy, cx);
|
|
52
|
+
const dist = Math.sqrt(cx * cx + cy * cy);
|
|
53
|
+
const hue = (angle * 57.3 + dist * 40 + tick * 3) % 360;
|
|
54
|
+
(0, grid_1.setCannonTarget)(grid, i, (hue + 360) % 360, 85, 75, attack);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
rain: (grid, tick, attack) => {
|
|
58
|
+
for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
|
|
59
|
+
const row = Math.floor(i / grid_1.GRID_SIZE);
|
|
60
|
+
const col = i % grid_1.GRID_SIZE;
|
|
61
|
+
// Each column has a different phase offset (pseudo-random via prime)
|
|
62
|
+
const phase = (tick * 0.15 + col * 2.3 + col * col * 0.7) % grid_1.GRID_SIZE;
|
|
63
|
+
const dist = Math.abs(row - phase);
|
|
64
|
+
const bright = dist < 1.5 ? 90 - dist * 30 : 10;
|
|
65
|
+
(0, grid_1.setCannonTarget)(grid, i, 200 + col * 8, 70, bright, attack);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
heartbeat: (grid, tick, attack) => {
|
|
69
|
+
// Double-pulse then rest (period ~120 ticks at 60fps = 2s)
|
|
70
|
+
const phase = tick % 120;
|
|
71
|
+
let brightness;
|
|
72
|
+
if (phase < 10)
|
|
73
|
+
brightness = 40 + phase * 5; // first pulse up
|
|
74
|
+
else if (phase < 20)
|
|
75
|
+
brightness = 90 - (phase - 10) * 5; // first pulse down
|
|
76
|
+
else if (phase < 30)
|
|
77
|
+
brightness = 40 + (phase - 20) * 4; // second pulse up
|
|
78
|
+
else if (phase < 40)
|
|
79
|
+
brightness = 80 - (phase - 30) * 4; // second pulse down
|
|
80
|
+
else
|
|
81
|
+
brightness = 40; // rest
|
|
82
|
+
for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
|
|
83
|
+
(0, grid_1.setCannonTarget)(grid, i, 0, 90, brightness, attack);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function getPerimeterIndices() {
|
|
88
|
+
const indices = [];
|
|
89
|
+
// Top row
|
|
90
|
+
for (let c = 0; c < grid_1.GRID_SIZE; c++)
|
|
91
|
+
indices.push(c);
|
|
92
|
+
// Right column (skip top-right corner)
|
|
93
|
+
for (let r = 1; r < grid_1.GRID_SIZE; r++)
|
|
94
|
+
indices.push(r * grid_1.GRID_SIZE + (grid_1.GRID_SIZE - 1));
|
|
95
|
+
// Bottom row reversed (skip bottom-right corner)
|
|
96
|
+
for (let c = grid_1.GRID_SIZE - 2; c >= 0; c--)
|
|
97
|
+
indices.push((grid_1.GRID_SIZE - 1) * grid_1.GRID_SIZE + c);
|
|
98
|
+
// Left column reversed (skip corners)
|
|
99
|
+
for (let r = grid_1.GRID_SIZE - 2; r >= 1; r--)
|
|
100
|
+
indices.push(r * grid_1.GRID_SIZE);
|
|
101
|
+
return indices;
|
|
102
|
+
}
|
|
103
|
+
function getAnimationNames() {
|
|
104
|
+
return Object.keys(exports.animations);
|
|
105
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { GRID_SIZE, NUM_CANNONS, setCannonTarget } from './grid';
|
|
2
|
+
export const animations = {
|
|
3
|
+
wave: (grid, tick, attack) => {
|
|
4
|
+
for (let i = 0; i < NUM_CANNONS; i++) {
|
|
5
|
+
const col = i % GRID_SIZE;
|
|
6
|
+
const hue = (tick * 2 + col * 40) % 360;
|
|
7
|
+
const bright = 60 + Math.sin(tick * 0.05 + col * 0.8) * 20;
|
|
8
|
+
setCannonTarget(grid, i, hue, 85, bright, attack);
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
breathe: (grid, tick, attack) => {
|
|
12
|
+
const brightness = 40 + Math.sin(tick * 0.03) * 35;
|
|
13
|
+
for (let i = 0; i < NUM_CANNONS; i++) {
|
|
14
|
+
setCannonTarget(grid, i, 220, 80, brightness, attack);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
rainbow: (grid, tick, attack) => {
|
|
18
|
+
for (let i = 0; i < NUM_CANNONS; i++) {
|
|
19
|
+
const row = Math.floor(i / GRID_SIZE);
|
|
20
|
+
const col = i % GRID_SIZE;
|
|
21
|
+
const hue = (tick * 1.5 + (row + col) * 25) % 360;
|
|
22
|
+
setCannonTarget(grid, i, hue, 90, 80, attack);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
pacman: (grid, tick, attack) => {
|
|
26
|
+
// Bright dot chasing around the perimeter
|
|
27
|
+
const perimeter = getPerimeterIndices();
|
|
28
|
+
const pos = Math.floor(tick * 0.3) % perimeter.length;
|
|
29
|
+
for (let i = 0; i < NUM_CANNONS; i++) {
|
|
30
|
+
setCannonTarget(grid, i, 220, 60, 15, attack);
|
|
31
|
+
}
|
|
32
|
+
// Pac-man (bright yellow)
|
|
33
|
+
const pacIdx = perimeter[pos];
|
|
34
|
+
setCannonTarget(grid, pacIdx, 55, 95, 95, 1.0);
|
|
35
|
+
// Trail (fading)
|
|
36
|
+
for (let t = 1; t <= 3; t++) {
|
|
37
|
+
const trailPos = (pos - t + perimeter.length) % perimeter.length;
|
|
38
|
+
const trailIdx = perimeter[trailPos];
|
|
39
|
+
setCannonTarget(grid, trailIdx, 55, 80, 70 - t * 18, 1.0);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
spiral: (grid, tick, attack) => {
|
|
43
|
+
for (let i = 0; i < NUM_CANNONS; i++) {
|
|
44
|
+
const row = Math.floor(i / GRID_SIZE);
|
|
45
|
+
const col = i % GRID_SIZE;
|
|
46
|
+
const cx = col - 3, cy = row - 3;
|
|
47
|
+
const angle = Math.atan2(cy, cx);
|
|
48
|
+
const dist = Math.sqrt(cx * cx + cy * cy);
|
|
49
|
+
const hue = (angle * 57.3 + dist * 40 + tick * 3) % 360;
|
|
50
|
+
setCannonTarget(grid, i, (hue + 360) % 360, 85, 75, attack);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
rain: (grid, tick, attack) => {
|
|
54
|
+
for (let i = 0; i < NUM_CANNONS; i++) {
|
|
55
|
+
const row = Math.floor(i / GRID_SIZE);
|
|
56
|
+
const col = i % GRID_SIZE;
|
|
57
|
+
// Each column has a different phase offset (pseudo-random via prime)
|
|
58
|
+
const phase = (tick * 0.15 + col * 2.3 + col * col * 0.7) % GRID_SIZE;
|
|
59
|
+
const dist = Math.abs(row - phase);
|
|
60
|
+
const bright = dist < 1.5 ? 90 - dist * 30 : 10;
|
|
61
|
+
setCannonTarget(grid, i, 200 + col * 8, 70, bright, attack);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
heartbeat: (grid, tick, attack) => {
|
|
65
|
+
// Double-pulse then rest (period ~120 ticks at 60fps = 2s)
|
|
66
|
+
const phase = tick % 120;
|
|
67
|
+
let brightness;
|
|
68
|
+
if (phase < 10)
|
|
69
|
+
brightness = 40 + phase * 5; // first pulse up
|
|
70
|
+
else if (phase < 20)
|
|
71
|
+
brightness = 90 - (phase - 10) * 5; // first pulse down
|
|
72
|
+
else if (phase < 30)
|
|
73
|
+
brightness = 40 + (phase - 20) * 4; // second pulse up
|
|
74
|
+
else if (phase < 40)
|
|
75
|
+
brightness = 80 - (phase - 30) * 4; // second pulse down
|
|
76
|
+
else
|
|
77
|
+
brightness = 40; // rest
|
|
78
|
+
for (let i = 0; i < NUM_CANNONS; i++) {
|
|
79
|
+
setCannonTarget(grid, i, 0, 90, brightness, attack);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
function getPerimeterIndices() {
|
|
84
|
+
const indices = [];
|
|
85
|
+
// Top row
|
|
86
|
+
for (let c = 0; c < GRID_SIZE; c++)
|
|
87
|
+
indices.push(c);
|
|
88
|
+
// Right column (skip top-right corner)
|
|
89
|
+
for (let r = 1; r < GRID_SIZE; r++)
|
|
90
|
+
indices.push(r * GRID_SIZE + (GRID_SIZE - 1));
|
|
91
|
+
// Bottom row reversed (skip bottom-right corner)
|
|
92
|
+
for (let c = GRID_SIZE - 2; c >= 0; c--)
|
|
93
|
+
indices.push((GRID_SIZE - 1) * GRID_SIZE + c);
|
|
94
|
+
// Left column reversed (skip corners)
|
|
95
|
+
for (let r = GRID_SIZE - 2; r >= 1; r--)
|
|
96
|
+
indices.push(r * GRID_SIZE);
|
|
97
|
+
return indices;
|
|
98
|
+
}
|
|
99
|
+
export function getAnimationNames() {
|
|
100
|
+
return Object.keys(animations);
|
|
101
|
+
}
|
package/esm/grid.js
CHANGED
|
@@ -48,17 +48,35 @@ function angleDelta(from, to) {
|
|
|
48
48
|
let d = ((to - from + 540) % 360) - 180;
|
|
49
49
|
return d;
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Set target for a single cannon.
|
|
53
|
+
* attack (0–1): how much of the new value to apply.
|
|
54
|
+
* 1.0 = full (instant snap to new target)
|
|
55
|
+
* 0.1 = soft (target blends 10% toward new value)
|
|
56
|
+
*/
|
|
57
|
+
export function setCannonTarget(grid, index, h, s, b, attack = 1.0) {
|
|
52
58
|
const c = grid[index];
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
if (attack >= 1.0) {
|
|
60
|
+
if (h !== undefined)
|
|
61
|
+
c.targetH = h;
|
|
62
|
+
if (s !== undefined)
|
|
63
|
+
c.targetS = s;
|
|
64
|
+
if (b !== undefined)
|
|
65
|
+
c.targetB = b;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
if (h !== undefined) {
|
|
69
|
+
const dh = angleDelta(c.targetH, h);
|
|
70
|
+
c.targetH = (c.targetH + dh * attack + 360) % 360;
|
|
71
|
+
}
|
|
72
|
+
if (s !== undefined)
|
|
73
|
+
c.targetS = c.targetS + (s - c.targetS) * attack;
|
|
74
|
+
if (b !== undefined)
|
|
75
|
+
c.targetB = c.targetB + (b - c.targetB) * attack;
|
|
76
|
+
}
|
|
59
77
|
}
|
|
60
|
-
export function setAllTargets(grid, h, s, b) {
|
|
78
|
+
export function setAllTargets(grid, h, s, b, attack = 1.0) {
|
|
61
79
|
for (let i = 0; i < grid.length; i++) {
|
|
62
|
-
setCannonTarget(grid, i, h, s, b);
|
|
80
|
+
setCannonTarget(grid, i, h, s, b, attack);
|
|
63
81
|
}
|
|
64
82
|
}
|
package/esm/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { animations, getAnimationNames } from './animations';
|
|
2
|
+
export { createGrid, DEFAULT_ALPHA, GRID_SIZE, NUM_CANNONS, setAllTargets, setCannonTarget, tickGrid } from './grid';
|
|
2
3
|
export { applyScene, scenes } from './scenes';
|
package/esm/server.js
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
3
|
+
import { animations } from './animations';
|
|
4
|
+
import { createGrid, DEFAULT_ALPHA, setAllTargets, setCannonTarget, tickGrid } from './grid';
|
|
4
5
|
import { applyScene, scenes } from './scenes';
|
|
5
6
|
import { getHTML } from './ui';
|
|
6
7
|
const PORT = parseInt(process.env.PORT || '3000', 10);
|
|
7
8
|
const TICK_MS = 1000 / 60; // 60fps interpolation
|
|
8
9
|
const grid = createGrid();
|
|
9
|
-
|
|
10
|
+
let currentAlpha = DEFAULT_ALPHA;
|
|
11
|
+
let currentAttack = 1.0;
|
|
12
|
+
let currentAnimation = null;
|
|
13
|
+
let animationTick = 0;
|
|
14
|
+
// constructive.io brand mark — served as the favicon
|
|
15
|
+
const FAVICON_SVG = `<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
16
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3315 21.7046V28.9348L26.2354 27.2232L29.8206 25.1157L36.9909 29.3307V37.761L30.1657 41.7731V41.785L22.9955 46L19.4102 43.8925L15.8343 41.7848V41.7749L12.5759 39.8595L9 37.7518V21.2924L12.5759 19.1847L15.8343 17.2694V9.25722L19.4102 7.14956L22.6685 5.23418V5.21521L26.2445 3.10755L29.8297 1L37 5.21499V13.6453L30.1657 17.6628V17.6873L23.3315 21.7046ZM16.16 17.8789L12.9168 19.7854L10.0443 21.4784L16.0542 25.0113L22.2948 21.4903L19.4101 19.7945L16.16 17.8789ZM23.6598 5.43249L29.7813 9.0309L35.955 5.40169L33.0743 3.70829L29.8297 1.80095L26.5853 3.70818L23.6598 5.43249ZM22.5139 38.2327L16.8333 41.5721L19.7511 43.2918L22.5185 44.9187L22.5196 38.2427L22.5139 38.2327ZM29.0399 33.6349L29.0153 33.5916L29.1105 33.5357L26.24 31.8482L23.3405 30.1438V33.546V36.9854L29.0399 33.6349ZM29.0998 9.43154L26.24 7.75041L22.9953 5.84307L19.7509 7.7503L16.8486 9.461L22.97 13.0595L29.0348 9.49437L29.0244 9.47595L29.0998 9.43154ZM16.5153 10.0661V13.4722V17.2854L22.5224 20.8167L22.5236 13.598L16.5153 10.0661ZM35.9458 29.5176L33.0651 27.8242L29.8205 25.9168L26.5761 27.8241L23.6705 29.5367L29.7919 33.1352L35.9458 29.5176ZM15.794 33.7218L12.5758 31.8299L9.68105 30.1237V33.5369V37.3517L12.9167 39.2589L15.7928 40.9496L15.794 33.7218ZM15.7954 25.7332L9.68116 22.1389V25.5074V29.3222L12.9168 31.2293L15.7943 32.9208L15.7954 25.7332Z" fill="#01A1FF"/>
|
|
17
|
+
</svg>`;
|
|
18
|
+
const server = http.createServer((req, res) => {
|
|
19
|
+
if (req.url === '/favicon.svg' || req.url === '/favicon.ico') {
|
|
20
|
+
res.writeHead(200, { 'Content-Type': 'image/svg+xml; charset=utf-8' });
|
|
21
|
+
res.end(FAVICON_SVG);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
10
24
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
11
25
|
res.end(getHTML());
|
|
12
26
|
});
|
|
@@ -41,30 +55,54 @@ wss.on('connection', (ws) => {
|
|
|
41
55
|
function handleMessage(msg) {
|
|
42
56
|
switch (msg.type) {
|
|
43
57
|
case 'cannon':
|
|
44
|
-
setCannonTarget(grid, msg.index, msg.h ?? undefined, msg.s ?? undefined, msg.b ?? undefined);
|
|
58
|
+
setCannonTarget(grid, msg.index, msg.h ?? undefined, msg.s ?? undefined, msg.b ?? undefined, currentAttack);
|
|
45
59
|
break;
|
|
46
60
|
case 'master_brightness':
|
|
47
|
-
setAllTargets(grid, undefined, undefined, msg.value * 100);
|
|
61
|
+
setAllTargets(grid, undefined, undefined, msg.value * 100, currentAttack);
|
|
48
62
|
break;
|
|
49
63
|
case 'scene':
|
|
50
64
|
if (msg.name && scenes[msg.name]) {
|
|
65
|
+
currentAnimation = null;
|
|
51
66
|
applyScene(grid, msg.name);
|
|
52
67
|
}
|
|
53
68
|
break;
|
|
69
|
+
case 'animation':
|
|
70
|
+
if (msg.name && animations[msg.name]) {
|
|
71
|
+
currentAnimation = msg.name;
|
|
72
|
+
animationTick = 0;
|
|
73
|
+
}
|
|
74
|
+
else if (msg.name === 'stop') {
|
|
75
|
+
currentAnimation = null;
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
54
78
|
case 'selection':
|
|
55
79
|
if (Array.isArray(msg.indices)) {
|
|
56
80
|
for (const idx of msg.indices) {
|
|
57
81
|
if (idx >= 0 && idx < grid.length) {
|
|
58
|
-
setCannonTarget(grid, idx, msg.h ?? undefined, msg.s ?? undefined, msg.b ?? undefined);
|
|
82
|
+
setCannonTarget(grid, idx, msg.h ?? undefined, msg.s ?? undefined, msg.b ?? undefined, currentAttack);
|
|
59
83
|
}
|
|
60
84
|
}
|
|
61
85
|
}
|
|
62
86
|
break;
|
|
87
|
+
case 'smoothness':
|
|
88
|
+
if (typeof msg.value === 'number') {
|
|
89
|
+
currentAlpha = msg.value;
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case 'attack':
|
|
93
|
+
if (typeof msg.value === 'number') {
|
|
94
|
+
currentAttack = msg.value;
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
63
97
|
}
|
|
64
98
|
}
|
|
65
99
|
// Animation loop: tick interpolation and broadcast
|
|
66
100
|
setInterval(() => {
|
|
67
|
-
|
|
101
|
+
if (currentAnimation && animations[currentAnimation]) {
|
|
102
|
+
animations[currentAnimation](grid, animationTick, currentAttack);
|
|
103
|
+
animationTick++;
|
|
104
|
+
}
|
|
105
|
+
const changed = tickGrid(grid, currentAlpha);
|
|
68
106
|
if (changed) {
|
|
69
107
|
broadcastState();
|
|
70
108
|
}
|
|
@@ -79,4 +117,4 @@ server.listen(PORT, '0.0.0.0', () => {
|
|
|
79
117
|
console.log(` → http://localhost:${PORT}`);
|
|
80
118
|
console.log('');
|
|
81
119
|
});
|
|
82
|
-
export {
|
|
120
|
+
export { grid, server };
|