@jjlmoya/utils-tabletop 1.11.0 → 1.13.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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +4 -0
  3. package/src/tests/locale_completeness.test.ts +1 -1
  4. package/src/tests/tool_validation.test.ts +1 -1
  5. package/src/tool/dice-roller-simulator/component.astro +116 -26
  6. package/src/tool/scatter-direction-selector/animation.ts +278 -0
  7. package/src/tool/scatter-direction-selector/bibliography.astro +16 -0
  8. package/src/tool/scatter-direction-selector/bibliography.ts +12 -0
  9. package/src/tool/scatter-direction-selector/client.ts +293 -0
  10. package/src/tool/scatter-direction-selector/component.astro +183 -0
  11. package/src/tool/scatter-direction-selector/entry.ts +59 -0
  12. package/src/tool/scatter-direction-selector/history.ts +71 -0
  13. package/src/tool/scatter-direction-selector/i18n/de.ts +143 -0
  14. package/src/tool/scatter-direction-selector/i18n/en.ts +231 -0
  15. package/src/tool/scatter-direction-selector/i18n/es.ts +143 -0
  16. package/src/tool/scatter-direction-selector/i18n/fr.ts +143 -0
  17. package/src/tool/scatter-direction-selector/i18n/id.ts +143 -0
  18. package/src/tool/scatter-direction-selector/i18n/it.ts +143 -0
  19. package/src/tool/scatter-direction-selector/i18n/ja.ts +143 -0
  20. package/src/tool/scatter-direction-selector/i18n/ko.ts +143 -0
  21. package/src/tool/scatter-direction-selector/i18n/nl.ts +143 -0
  22. package/src/tool/scatter-direction-selector/i18n/pl.ts +143 -0
  23. package/src/tool/scatter-direction-selector/i18n/pt.ts +143 -0
  24. package/src/tool/scatter-direction-selector/i18n/ru.ts +143 -0
  25. package/src/tool/scatter-direction-selector/i18n/sv.ts +143 -0
  26. package/src/tool/scatter-direction-selector/i18n/tr.ts +143 -0
  27. package/src/tool/scatter-direction-selector/i18n/zh.ts +143 -0
  28. package/src/tool/scatter-direction-selector/index.ts +10 -0
  29. package/src/tool/scatter-direction-selector/logic.test.ts +62 -0
  30. package/src/tool/scatter-direction-selector/logic.ts +52 -0
  31. package/src/tool/scatter-direction-selector/scatter-direction-selector.css +809 -0
  32. package/src/tool/scatter-direction-selector/seo.astro +16 -0
  33. package/src/tool/scatter-direction-selector/settings.ts +81 -0
  34. package/src/tool/scatter-direction-selector/sound.ts +22 -0
  35. package/src/tools.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-tabletop",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
package/src/entries.ts CHANGED
@@ -16,6 +16,8 @@ export { lunarTideTracker } from './tool/lunar-tide-tracker/entry';
16
16
  export type { LunarTideUI, LunarTideLocaleContent } from './tool/lunar-tide-tracker/entry';
17
17
  export { hiddenRoleDealer } from './tool/hidden-role-dealer/entry';
18
18
  export type { HiddenRoleDealerUI, HiddenRoleDealerLocaleContent } from './tool/hidden-role-dealer/entry';
19
+ export { scatterDirectionSelector } from './tool/scatter-direction-selector/entry';
20
+ export type { ScatterSelectorUI, ScatterSelectorLocaleContent } from './tool/scatter-direction-selector/entry';
19
21
  export { tabletopCategory } from './category';
20
22
 
21
23
  import { diceRollerSimulator } from './tool/dice-roller-simulator/entry';
@@ -28,6 +30,7 @@ import { decisionWheel } from './tool/decision-wheel/entry';
28
30
  import { investigationBoard } from './tool/investigation-board/entry';
29
31
  import { lunarTideTracker } from './tool/lunar-tide-tracker/entry';
30
32
  import { hiddenRoleDealer } from './tool/hidden-role-dealer/entry';
33
+ import { scatterDirectionSelector } from './tool/scatter-direction-selector/entry';
31
34
 
32
35
  export const ALL_ENTRIES = [
33
36
  diceRollerSimulator,
@@ -40,5 +43,6 @@ export const ALL_ENTRIES = [
40
43
  investigationBoard,
41
44
  lunarTideTracker,
42
45
  hiddenRoleDealer,
46
+ scatterDirectionSelector,
43
47
  ];
44
48
 
@@ -22,7 +22,7 @@ describe('Locale Completeness Validation', () => {
22
22
  });
23
23
 
24
24
  it('all tools registered', () => {
25
- expect(ALL_TOOLS.length).toBe(10);
25
+ expect(ALL_TOOLS.length).toBe(11);
26
26
  });
27
27
 
28
28
 
@@ -5,7 +5,7 @@ import { tabletopCategory } from '../data';
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
7
  it('should have tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(10);
8
+ expect(ALL_TOOLS.length).toBe(11);
9
9
  });
10
10
 
11
11
 
@@ -1,8 +1,8 @@
1
1
  ---
2
- import DiceGrid from './components/DiceGrid.astro';
3
- import RollHistorySection from './components/RollHistorySection.astro';
4
- import StatsPanel from './components/StatsPanel.astro';
5
- import RollResultBanner from './components/RollResultBanner.astro';
2
+ import DiceGrid from "./components/DiceGrid.astro";
3
+ import RollHistorySection from "./components/RollHistorySection.astro";
4
+ import StatsPanel from "./components/StatsPanel.astro";
5
+ import RollResultBanner from "./components/RollResultBanner.astro";
6
6
 
7
7
  interface Props {
8
8
  ui: Record<string, string>;
@@ -15,33 +15,117 @@ const { ui } = Astro.props;
15
15
  <div class="tabletop-gaming-dashboard">
16
16
  <div class="forge-and-tray-grid">
17
17
  <DiceGrid labels={ui} />
18
-
18
+
19
19
  <div class="dashboard-center">
20
20
  <div class="rolling-tray-container">
21
21
  <div class="tray-rim">
22
22
  <div class="tray-felt" id="roll-arena">
23
23
  <div class="felt-placeholder" id="arena-placeholder">
24
- <svg viewBox="0 0 100 100" class="felt-crest-svg" aria-hidden="true">
25
- <circle cx="50" cy="50" r="45" fill="none" stroke="currentColor" stroke-opacity="0.15" stroke-width="1" />
26
- <circle cx="50" cy="50" r="40" fill="none" stroke="currentColor" stroke-opacity="0.08" stroke-width="1" stroke-dasharray="3,3" />
27
-
28
- <polygon points="50,15 80,32 80,68 50,85 20,68 20,32" fill="currentColor" fill-opacity="0.03" stroke="currentColor" stroke-width="2" />
29
-
30
- <polygon points="50,38 72,50 50,75 28,50" fill="none" stroke="currentColor" stroke-width="1.5" />
31
- <line x1="50" y1="15" x2="50" y2="38" stroke="currentColor" stroke-width="1.5" />
32
- <line x1="20" y1="32" x2="28" y2="50" stroke="currentColor" stroke-width="1.5" />
33
- <line x1="80" y1="32" x2="72" y2="50" stroke="currentColor" stroke-width="1.5" />
34
- <line x1="20" y1="68" x2="28" y2="50" stroke="currentColor" stroke-width="1.5" />
35
- <line x1="80" y1="68" x2="72" y2="50" stroke="currentColor" stroke-width="1.5" />
36
- <line x1="50" y1="85" x2="50" y2="75" stroke="currentColor" stroke-width="1.5" />
37
- <line x1="28" y1="50" x2="50" y2="75" stroke="currentColor" stroke-width="1.5" />
38
- <line x1="72" y1="50" x2="50" y2="75" stroke="currentColor" stroke-width="1.5" />
39
-
40
- <text x="50" y="58" font-size="12" font-weight="900" text-anchor="middle" fill="currentColor" font-family="'Outfit', sans-serif">20</text>
24
+ <svg
25
+ viewBox="0 0 100 100"
26
+ class="felt-crest-svg"
27
+ aria-hidden="true"
28
+ >
29
+ <circle
30
+ cx="50"
31
+ cy="50"
32
+ r="45"
33
+ fill="none"
34
+ stroke="currentColor"
35
+ stroke-opacity="0.15"
36
+ stroke-width="1"></circle>
37
+ <circle
38
+ cx="50"
39
+ cy="50"
40
+ r="40"
41
+ fill="none"
42
+ stroke="currentColor"
43
+ stroke-opacity="0.08"
44
+ stroke-width="1"
45
+ stroke-dasharray="3,3"></circle>
46
+
47
+ <polygon
48
+ points="50,15 80,32 80,68 50,85 20,68 20,32"
49
+ fill="currentColor"
50
+ fill-opacity="0.03"
51
+ stroke="currentColor"
52
+ stroke-width="2"></polygon>
53
+
54
+ <polygon
55
+ points="50,38 72,50 50,75 28,50"
56
+ fill="none"
57
+ stroke="currentColor"
58
+ stroke-width="1.5"></polygon>
59
+ <line
60
+ x1="50"
61
+ y1="15"
62
+ x2="50"
63
+ y2="38"
64
+ stroke="currentColor"
65
+ stroke-width="1.5"></line>
66
+ <line
67
+ x1="20"
68
+ y1="32"
69
+ x2="28"
70
+ y2="50"
71
+ stroke="currentColor"
72
+ stroke-width="1.5"></line>
73
+ <line
74
+ x1="80"
75
+ y1="32"
76
+ x2="72"
77
+ y2="50"
78
+ stroke="currentColor"
79
+ stroke-width="1.5"></line>
80
+ <line
81
+ x1="20"
82
+ y1="68"
83
+ x2="28"
84
+ y2="50"
85
+ stroke="currentColor"
86
+ stroke-width="1.5"></line>
87
+ <line
88
+ x1="80"
89
+ y1="68"
90
+ x2="72"
91
+ y2="50"
92
+ stroke="currentColor"
93
+ stroke-width="1.5"></line>
94
+ <line
95
+ x1="50"
96
+ y1="85"
97
+ x2="50"
98
+ y2="75"
99
+ stroke="currentColor"
100
+ stroke-width="1.5"></line>
101
+ <line
102
+ x1="28"
103
+ y1="50"
104
+ x2="50"
105
+ y2="75"
106
+ stroke="currentColor"
107
+ stroke-width="1.5"></line>
108
+ <line
109
+ x1="72"
110
+ y1="50"
111
+ x2="50"
112
+ y2="75"
113
+ stroke="currentColor"
114
+ stroke-width="1.5"></line>
115
+
116
+ <text
117
+ x="50"
118
+ y="58"
119
+ font-size="12"
120
+ font-weight="900"
121
+ text-anchor="middle"
122
+ fill="currentColor">20</text
123
+ >
41
124
  </svg>
42
125
  <p class="felt-hint-text">Add dice to roll</p>
43
126
  </div>
44
- <div class="felt-dice-container" id="dice-results-container"></div>
127
+ <div class="felt-dice-container" id="dice-results-container">
128
+ </div>
45
129
  </div>
46
130
  </div>
47
131
  </div>
@@ -56,10 +140,16 @@ const { ui } = Astro.props;
56
140
 
57
141
  <div class="tabletop-extra-tabs">
58
142
  <div class="tabs-nav">
59
- <button type="button" class="extra-tab-btn active" data-extra-tab="logs-panel">Roll Logs</button>
60
- <button type="button" class="extra-tab-btn" data-extra-tab="stats-panel">Statistics</button>
143
+ <button
144
+ type="button"
145
+ class="extra-tab-btn active"
146
+ data-extra-tab="logs-panel">Roll Logs</button
147
+ >
148
+ <button type="button" class="extra-tab-btn" data-extra-tab="stats-panel"
149
+ >Statistics</button
150
+ >
61
151
  </div>
62
-
152
+
63
153
  <div class="extra-tabs-content">
64
154
  <div class="extra-tab-pane active" id="logs-panel">
65
155
  <RollHistorySection labels={ui} />
@@ -0,0 +1,278 @@
1
+ export class Particle {
2
+ x: number = 0;
3
+ y: number = 0;
4
+ vx: number = 0;
5
+ vy: number = 0;
6
+ size: number = 0;
7
+ alpha: number = 1;
8
+ color: string = '';
9
+
10
+ constructor(w: number, h: number, angleRad: number, burst: boolean) {
11
+ if (burst) {
12
+ this.x = w / 2;
13
+ this.y = h / 2;
14
+ const speed = Math.random() * 6 + 4;
15
+ this.vx = Math.cos(angleRad) * speed + (Math.random() - 0.5) * 2;
16
+ this.vy = Math.sin(angleRad) * speed + (Math.random() - 0.5) * 2;
17
+ this.size = Math.random() * 3 + 2;
18
+ this.color = '56, 189, 248';
19
+ } else {
20
+ this.x = Math.random() * w;
21
+ this.y = Math.random() * h;
22
+ this.vx = Math.cos(angleRad) * 0.8 + (Math.random() - 0.5) * 0.2;
23
+ this.vy = Math.sin(angleRad) * 0.8 + (Math.random() - 0.5) * 0.2;
24
+ this.size = Math.random() * 1.5 + 0.5;
25
+ this.color = '125, 211, 252';
26
+ }
27
+ }
28
+
29
+ update(): void {
30
+ this.x += this.vx;
31
+ this.y += this.vy;
32
+ this.alpha -= 0.012;
33
+ }
34
+ }
35
+
36
+ interface FrameCtx {
37
+ ctx: CanvasRenderingContext2D;
38
+ cx: number;
39
+ cy: number;
40
+ progress: number;
41
+ tgtX: number;
42
+ tgtY: number;
43
+ }
44
+
45
+ export class Animator {
46
+ canvas: HTMLCanvasElement;
47
+ ctx: CanvasRenderingContext2D;
48
+ active: boolean = false;
49
+ private startTime: number = 0;
50
+ private isHit: boolean = false;
51
+ private distance: number = 0;
52
+ private targetX: number = 100;
53
+ private targetY: number = 100;
54
+ private particles: Particle[] = [];
55
+
56
+ constructor(canvas: HTMLCanvasElement) {
57
+ this.canvas = canvas;
58
+ this.ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
59
+ this.canvas.width = 300;
60
+ this.canvas.height = 300;
61
+ }
62
+
63
+ start(opts: { hit: boolean; dist: number; tgtX: number; tgtY: number }): void {
64
+ this.active = true;
65
+ this.startTime = performance.now();
66
+ this.isHit = opts.hit;
67
+ this.distance = opts.dist;
68
+ this.targetX = opts.tgtX;
69
+ this.targetY = opts.tgtY;
70
+ this.particles = [];
71
+ }
72
+
73
+ drawFrame(): boolean {
74
+ const elapsed = performance.now() - this.startTime;
75
+ const duration = this.isHit ? 1800 : 2200;
76
+ const progress = Math.min(elapsed / duration, 1);
77
+ const w = this.canvas.width;
78
+ const h = this.canvas.height;
79
+ const cx = w / 2;
80
+ const cy = h / 2;
81
+ const s = w / 200;
82
+ const tgtX = cx + (this.targetX - 100) * s;
83
+ const tgtY = cy + (this.targetY - 100) * s;
84
+ const f: FrameCtx = { ctx: this.ctx, cx, cy, progress, tgtX, tgtY };
85
+
86
+ this.ctx.clearRect(0, 0, w, h);
87
+
88
+ if (this.isHit) {
89
+ this.drawHitPulse(f);
90
+ this.drawHitRings(f);
91
+ this.drawHitText(f);
92
+ } else {
93
+ this.drawScatterTravel(f);
94
+ this.drawScatterImpact(f);
95
+ }
96
+
97
+ this.drawParticles();
98
+ return progress >= 1;
99
+ }
100
+
101
+ reset(): void {
102
+ this.active = false;
103
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
104
+ this.particles = [];
105
+ }
106
+
107
+ getResult(): { isHit: boolean; distance: number } {
108
+ return { isHit: this.isHit, distance: this.distance };
109
+ }
110
+
111
+ private drawHitPulse(f: FrameCtx): void {
112
+ if (f.progress >= 0.35) return;
113
+ const t = f.progress / 0.35;
114
+ const pulse = Math.sin(t * Math.PI * 3) * 0.5 + 0.5;
115
+ const r = 5 + pulse * 18;
116
+ const g = f.ctx.createRadialGradient(f.cx, f.cy, 0, f.cx, f.cy, r);
117
+ g.addColorStop(0, `rgba(52, 211, 153, ${0.7 * (1 - pulse * 0.3)})`);
118
+ g.addColorStop(1, 'rgba(52, 211, 153, 0)');
119
+ f.ctx.fillStyle = g;
120
+ f.ctx.beginPath();
121
+ f.ctx.arc(f.cx, f.cy, r, 0, Math.PI * 2);
122
+ f.ctx.fill();
123
+ }
124
+
125
+ private drawHitRings(f: FrameCtx): void {
126
+ if (f.progress <= 0.3 || f.progress >= 0.85) return;
127
+ const t = (f.progress - 0.3) / 0.55;
128
+ const rr = 5 + t * 75;
129
+ const a = 1 - t;
130
+ f.ctx.strokeStyle = `rgba(52, 211, 153, ${a})`;
131
+ f.ctx.lineWidth = 2;
132
+ f.ctx.beginPath();
133
+ f.ctx.arc(f.cx, f.cy, rr, 0, Math.PI * 2);
134
+ f.ctx.stroke();
135
+ f.ctx.strokeStyle = `rgba(52, 211, 153, ${a * 0.5})`;
136
+ f.ctx.lineWidth = 1.5;
137
+ f.ctx.beginPath();
138
+ f.ctx.arc(f.cx, f.cy, rr * 0.6, 0, Math.PI * 2);
139
+ f.ctx.stroke();
140
+ }
141
+
142
+ private drawHitText(f: FrameCtx): void {
143
+ if (f.progress <= 0.6) return;
144
+ const t = Math.min((f.progress - 0.6) / 0.4, 1);
145
+ f.ctx.save();
146
+ f.ctx.translate(f.cx, f.cy + 5);
147
+ f.ctx.scale(0.5 + t * 0.5, 0.5 + t * 0.5);
148
+ f.ctx.font = 'bold 16px Orbitron, system-ui, sans-serif';
149
+ f.ctx.textAlign = 'center';
150
+ f.ctx.textBaseline = 'middle';
151
+ f.ctx.shadowColor = 'rgba(52, 211, 153, 0.6)';
152
+ f.ctx.shadowBlur = 12;
153
+ f.ctx.fillStyle = `rgba(52, 211, 153, ${Math.min(t * 2, 1)})`;
154
+ f.ctx.fillText('DIRECT HIT', 0, 0);
155
+ f.ctx.restore();
156
+ }
157
+
158
+ private drawScatterTravel(f: FrameCtx): void {
159
+ if (f.progress >= 0.6) return;
160
+ const t = f.progress / 0.6;
161
+ const eased = 1 - (1 - t) * (1 - t);
162
+ const curX = f.cx + (f.tgtX - f.cx) * eased;
163
+ const curY = f.cy + (f.tgtY - f.cy) * eased;
164
+
165
+ f.ctx.beginPath();
166
+ f.ctx.moveTo(f.cx, f.cy);
167
+ f.ctx.lineTo(curX, curY);
168
+ f.ctx.strokeStyle = 'rgba(248, 113, 113, 0.5)';
169
+ f.ctx.lineWidth = 2;
170
+ f.ctx.setLineDash([4, 6]);
171
+ f.ctx.stroke();
172
+ f.ctx.setLineDash([]);
173
+
174
+ this.drawProjectile(f.ctx, curX, curY);
175
+
176
+ if (Math.random() < 0.5) {
177
+ const along = eased - 0.15 * Math.random();
178
+ this.spawnTrailParticle(f.cx + (f.tgtX - f.cx) * Math.max(along, 0), f.cy + (f.tgtY - f.cy) * Math.max(along, 0));
179
+ }
180
+ }
181
+
182
+ private drawScatterImpact(f: FrameCtx): void {
183
+ if (f.progress < 0.6) return;
184
+ const t = (f.progress - 0.6) / 0.4;
185
+ const impactT = Math.min(t * 2, 1);
186
+
187
+ this.drawImpactGlow(f.ctx, f.tgtX, f.tgtY, impactT);
188
+ this.drawImpactLabel(f);
189
+ this.spawnImpactParticles(f.ctx, f.tgtX, f.tgtY, t);
190
+ }
191
+
192
+ private drawProjectile(ctx: CanvasRenderingContext2D, x: number, y: number): void {
193
+ const g = ctx.createRadialGradient(x, y, 0, x, y, 16);
194
+ g.addColorStop(0, 'rgba(248, 113, 113, 0.7)');
195
+ g.addColorStop(1, 'rgba(248, 113, 113, 0)');
196
+ ctx.fillStyle = g;
197
+ ctx.beginPath();
198
+ ctx.arc(x, y, 16, 0, Math.PI * 2);
199
+ ctx.fill();
200
+
201
+ ctx.fillStyle = '#f87171';
202
+ ctx.beginPath();
203
+ ctx.arc(x, y, 4, 0, Math.PI * 2);
204
+ ctx.fill();
205
+ }
206
+
207
+ private drawImpactGlow(ctx: CanvasRenderingContext2D, x: number, y: number, t: number): void {
208
+ const r = 5 + t * 28;
209
+ const a = 0.6 * (1 - t * 0.5);
210
+ const g = ctx.createRadialGradient(x, y, 0, x, y, r);
211
+ g.addColorStop(0, `rgba(248, 113, 113, ${a})`);
212
+ g.addColorStop(1, 'rgba(248, 113, 113, 0)');
213
+ ctx.fillStyle = g;
214
+ ctx.beginPath();
215
+ ctx.arc(x, y, r, 0, Math.PI * 2);
216
+ ctx.fill();
217
+
218
+ ctx.fillStyle = '#f87171';
219
+ ctx.beginPath();
220
+ ctx.arc(x, y, 3 + t * 2, 0, Math.PI * 2);
221
+ ctx.fill();
222
+ }
223
+
224
+ private drawImpactLabel(f: FrameCtx): void {
225
+ const t = (f.progress - 0.6) / 0.4;
226
+ if (t <= 0.2) return;
227
+ const textT = Math.min((t - 0.2) / 0.3, 1);
228
+ const dx = f.tgtX - f.cx;
229
+ const dy = f.tgtY - f.cy;
230
+ const len = Math.sqrt(dx * dx + dy * dy) || 1;
231
+ f.ctx.save();
232
+ f.ctx.font = 'bold 12px Orbitron, system-ui, sans-serif';
233
+ f.ctx.textAlign = 'center';
234
+ f.ctx.textBaseline = 'middle';
235
+ f.ctx.shadowColor = 'rgba(0, 0, 0, 0.8)';
236
+ f.ctx.shadowBlur = 4;
237
+ f.ctx.fillStyle = `rgba(248, 113, 113, ${textT})`;
238
+ f.ctx.fillText(`${this.distance}"`, f.tgtX + (dx / len) * 22, f.tgtY + (dy / len) * 18);
239
+ f.ctx.restore();
240
+ }
241
+
242
+ private spawnTrailParticle(x: number, y: number): void {
243
+ const p = new Particle(this.canvas.width, this.canvas.height, 0, false);
244
+ p.x = x;
245
+ p.y = y;
246
+ p.color = '248, 113, 113';
247
+ p.size = Math.random() * 2 + 1;
248
+ this.particles.push(p);
249
+ }
250
+
251
+ private spawnImpactParticles(ctx: CanvasRenderingContext2D, x: number, y: number, t: number): void {
252
+ if (t >= 0.5 || Math.random() >= 0.7) return;
253
+ const a = Math.random() * Math.PI * 2;
254
+ const spd = Math.random() * 3 + 1;
255
+ const p = new Particle(this.canvas.width, this.canvas.height, a, false);
256
+ p.x = x;
257
+ p.y = y;
258
+ p.vx = Math.cos(a) * spd;
259
+ p.vy = Math.sin(a) * spd;
260
+ p.color = '248, 113, 113';
261
+ p.size = Math.random() * 2.5 + 1;
262
+ p.alpha = 1;
263
+ this.particles.push(p);
264
+ }
265
+
266
+ private drawParticles(): void {
267
+ const ctx = this.ctx;
268
+ this.particles = this.particles.filter(p => {
269
+ p.update();
270
+ if (p.alpha <= 0) return false;
271
+ ctx.fillStyle = `rgba(${p.color}, ${p.alpha})`;
272
+ ctx.beginPath();
273
+ ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
274
+ ctx.fill();
275
+ return true;
276
+ });
277
+ }
278
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { scatterDirectionSelector } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props as Props;
11
+ const loader = scatterDirectionSelector.i18n[locale] || scatterDirectionSelector.i18n.en;
12
+ const content = await loader?.();
13
+ if (!content) return null;
14
+ ---
15
+
16
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Wargaming Scatter Mechanics',
6
+ url: 'https://en.wikipedia.org/wiki/Miniature_wargaming',
7
+ },
8
+ {
9
+ name: 'Compass Rose & Wind Directions',
10
+ url: 'https://en.wikipedia.org/wiki/Points_of_the_compass',
11
+ },
12
+ ];