@jjlmoya/utils-tabletop 1.7.0 → 1.9.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.
- package/package.json +1 -1
- package/src/entries.ts +14 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/decision-wheel/drawing.ts +1 -1
- package/src/tool/investigation-board/interactDrag.ts +2 -1
- package/src/tool/lunar-tide-tracker/bibliography.astro +16 -0
- package/src/tool/lunar-tide-tracker/bibliography.ts +8 -0
- package/src/tool/lunar-tide-tracker/client.ts +264 -0
- package/src/tool/lunar-tide-tracker/component.astro +103 -0
- package/src/tool/lunar-tide-tracker/entry.ts +26 -0
- package/src/tool/lunar-tide-tracker/i18n/de.ts +156 -0
- package/src/tool/lunar-tide-tracker/i18n/en.ts +214 -0
- package/src/tool/lunar-tide-tracker/i18n/es.ts +157 -0
- package/src/tool/lunar-tide-tracker/i18n/fr.ts +156 -0
- package/src/tool/lunar-tide-tracker/i18n/id.ts +156 -0
- package/src/tool/lunar-tide-tracker/i18n/it.ts +156 -0
- package/src/tool/lunar-tide-tracker/i18n/ja.ts +156 -0
- package/src/tool/lunar-tide-tracker/i18n/ko.ts +214 -0
- package/src/tool/lunar-tide-tracker/i18n/nl.ts +214 -0
- package/src/tool/lunar-tide-tracker/i18n/pl.ts +214 -0
- package/src/tool/lunar-tide-tracker/i18n/pt.ts +214 -0
- package/src/tool/lunar-tide-tracker/i18n/ru.ts +214 -0
- package/src/tool/lunar-tide-tracker/i18n/sv.ts +214 -0
- package/src/tool/lunar-tide-tracker/i18n/tr.ts +214 -0
- package/src/tool/lunar-tide-tracker/i18n/zh.ts +214 -0
- package/src/tool/lunar-tide-tracker/index.ts +9 -0
- package/src/tool/lunar-tide-tracker/logic.test.ts +56 -0
- package/src/tool/lunar-tide-tracker/logic.ts +76 -0
- package/src/tool/lunar-tide-tracker/lunar-tide-tracker.css +494 -0
- package/src/tool/lunar-tide-tracker/seo.astro +16 -0
- package/src/tool/lunar-tide-tracker/types.ts +70 -0
- package/src/tools.ts +2 -0
package/package.json
CHANGED
package/src/entries.ts
CHANGED
|
@@ -12,6 +12,8 @@ export { fantasyRunesTranslator } from './tool/fantasy-runes-translator/entry';
|
|
|
12
12
|
export type { FantasyRunesTranslatorUI, FantasyRunesTranslatorLocaleContent } from './tool/fantasy-runes-translator/entry';
|
|
13
13
|
export { investigationBoard } from './tool/investigation-board/entry';
|
|
14
14
|
export type { InvestigationBoardUI, InvestigationBoardLocaleContent } from './tool/investigation-board/entry';
|
|
15
|
+
export { lunarTideTracker } from './tool/lunar-tide-tracker/entry';
|
|
16
|
+
export type { LunarTideUI, LunarTideLocaleContent } from './tool/lunar-tide-tracker/entry';
|
|
15
17
|
export { tabletopCategory } from './category';
|
|
16
18
|
|
|
17
19
|
import { diceRollerSimulator } from './tool/dice-roller-simulator/entry';
|
|
@@ -22,6 +24,17 @@ import { initiativeTracker } from './tool/rpg-initiative-tracker/entry';
|
|
|
22
24
|
import { fantasyRunesTranslator } from './tool/fantasy-runes-translator/entry';
|
|
23
25
|
import { decisionWheel } from './tool/decision-wheel/entry';
|
|
24
26
|
import { investigationBoard } from './tool/investigation-board/entry';
|
|
27
|
+
import { lunarTideTracker } from './tool/lunar-tide-tracker/entry';
|
|
25
28
|
|
|
26
|
-
export const ALL_ENTRIES = [
|
|
29
|
+
export const ALL_ENTRIES = [
|
|
30
|
+
diceRollerSimulator,
|
|
31
|
+
boardGameTimer,
|
|
32
|
+
firstPlayerSelector,
|
|
33
|
+
scoreTracker,
|
|
34
|
+
initiativeTracker,
|
|
35
|
+
fantasyRunesTranslator,
|
|
36
|
+
decisionWheel,
|
|
37
|
+
investigationBoard,
|
|
38
|
+
lunarTideTracker,
|
|
39
|
+
];
|
|
27
40
|
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { diceRollerSimulator, DICE_ROLLER_SIMULATOR_TOOL } from './tool/dice-rol
|
|
|
5
5
|
export { boardGameTimer, BOARD_GAME_TIMER_TOOL } from './tool/board-game-timer';
|
|
6
6
|
export { fantasyRunesTranslator, FANTASY_RUNES_TRANSLATOR_TOOL } from './tool/fantasy-runes-translator';
|
|
7
7
|
export { investigationBoard, INVESTIGATION_BOARD_TOOL } from './tool/investigation-board';
|
|
8
|
+
export { lunarTideTracker, LUNAR_TIDE_TRACKER_TOOL } from './tool/lunar-tide-tracker';
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
export type {
|
|
@@ -48,7 +48,7 @@ function drawSegments(c: CanvasCtx, segments: WheelSegment[], currentAngle: numb
|
|
|
48
48
|
const seg = segments[i];
|
|
49
49
|
const weight = seg.weight || 1;
|
|
50
50
|
const sliceAngle = (weight / totalWeight) * 2 * Math.PI;
|
|
51
|
-
const startAngle = accumulatedAngle + currentAngle - Math.PI / 2;
|
|
51
|
+
const startAngle = accumulatedAngle + (currentAngle * Math.PI) / 180 - Math.PI / 2;
|
|
52
52
|
const endAngle = startAngle + sliceAngle;
|
|
53
53
|
|
|
54
54
|
ctx.beginPath();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as interactModule from 'interactjs';
|
|
2
|
+
const interact = (interactModule.default || interactModule) as unknown as typeof interactModule.default;
|
|
2
3
|
import { state } from './dom';
|
|
3
4
|
import { getHighlightPath } from './logic';
|
|
4
5
|
import { drawConnections } from './renderer';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { lunarTideTracker } 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 = lunarTideTracker.i18n[locale] || lunarTideTracker.i18n.en;
|
|
12
|
+
const content = await loader?.();
|
|
13
|
+
if (!content) return null;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import type { Moon, MoonPhaseType } from './types';
|
|
2
|
+
import { calculateMoonPhase, calculateCombinedTide, generateForecast } from './logic';
|
|
3
|
+
|
|
4
|
+
const tideLabelMap: Record<string, string> = {
|
|
5
|
+
neap: 'Neap Tide', low: 'Low Tide', normal: 'Normal Tide', high: 'High Tide', spring: 'Spring Tide',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
let moons: Moon[] = [{ id: '1', name: 'Solaria', period: 28, offset: 0, color: '#f59e0b' }];
|
|
9
|
+
let currentDay = 0;
|
|
10
|
+
|
|
11
|
+
const phaseUiKeys: Record<MoonPhaseType, string> = {
|
|
12
|
+
new: 'newMoon', waxing_crescent: 'waxingCrescent', first_quarter: 'firstQuarter',
|
|
13
|
+
waxing_gibbous: 'waxingGibbous', full: 'fullMoon', waning_gibbous: 'waningGibbous',
|
|
14
|
+
last_quarter: 'lastQuarter', waning_crescent: 'waningCrescent',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const phaseDefault: Record<MoonPhaseType, string> = {
|
|
18
|
+
new: 'New Moon', waxing_crescent: 'Waxing Crescent', first_quarter: 'First Quarter',
|
|
19
|
+
waxing_gibbous: 'Waxing Gibbous', full: 'Full Moon', waning_gibbous: 'Waning Gibbous',
|
|
20
|
+
last_quarter: 'Last Quarter', waning_crescent: 'Waning Crescent',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const presets: Record<string, () => Moon[]> = {
|
|
24
|
+
'lunar-preset-single': () => [{ id: '1', name: 'Solaria', period: 28, offset: 0, color: '#f59e0b' }],
|
|
25
|
+
'lunar-preset-double': () => [
|
|
26
|
+
{ id: '1', name: 'Solaria', period: 20, offset: 0, color: '#a78bfa' },
|
|
27
|
+
{ id: '2', name: 'Lunaria', period: 30, offset: 10, color: '#3b82f6' },
|
|
28
|
+
],
|
|
29
|
+
'lunar-preset-triple': () => [
|
|
30
|
+
{ id: '1', name: 'Solaria', period: 15, offset: 0, color: '#f87171' },
|
|
31
|
+
{ id: '2', name: 'Lunaria', period: 25, offset: 5, color: '#3b82f6' },
|
|
32
|
+
{ id: '3', name: 'Astraria', period: 40, offset: 15, color: '#34d399' },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function getCssVar(name: string, fallback: string): string {
|
|
37
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getUI(): Record<string, string> {
|
|
41
|
+
const el = document.querySelector('.ltt-root');
|
|
42
|
+
if (!el) return {};
|
|
43
|
+
try { return JSON.parse(el.getAttribute('data-ui') || '{}'); } catch { return {}; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getPhaseLabel(phase: MoonPhaseType, ui: Record<string, string>): string {
|
|
47
|
+
return ui[phaseUiKeys[phase]] || phaseDefault[phase];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function spawnParticle(x: number, y: number, text: string): void {
|
|
51
|
+
const p = document.createElement('div');
|
|
52
|
+
p.style.cssText = `position:fixed;left:${x}px;top:${y}px;transform:translate(-50%,-50%);pointer-events:none;color:#a78bfa;font-weight:700;font-size:0.9rem;text-shadow:0 0 8px rgba(139,92,246,0.7);animation:ltt-particle 0.8s ease-out forwards;z-index:9999`;
|
|
53
|
+
p.innerText = text;
|
|
54
|
+
document.body.appendChild(p);
|
|
55
|
+
setTimeout(() => p.remove(), 800);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getSweep(isOuter: boolean, isWaxing: boolean): number {
|
|
59
|
+
if (isOuter && !isWaxing) return 1;
|
|
60
|
+
if (!isOuter && isWaxing) return 1;
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildPhasePath(pct: number, cx: number, cy: number, r: number): string {
|
|
65
|
+
if (pct <= 0.01 || pct >= 0.99) return '';
|
|
66
|
+
const isWaxing = pct < 0.5;
|
|
67
|
+
const base = isWaxing ? pct : pct - 0.5;
|
|
68
|
+
const rx = r * Math.abs(1 - 4 * base);
|
|
69
|
+
const isOuter = pct < 0.25 || pct >= 0.75;
|
|
70
|
+
const sweep = getSweep(isOuter, isWaxing);
|
|
71
|
+
return `M ${cx},${cy - r} A ${r},${r} 0 0,1 ${cx},${cy + r} A ${rx},${r} 0 0,${sweep} ${cx},${cy - r}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderMoonOrbs(): void {
|
|
75
|
+
const el = document.getElementById('ltt-moon-orbs');
|
|
76
|
+
if (!el) return;
|
|
77
|
+
const ui = getUI();
|
|
78
|
+
const orbRadius = 100;
|
|
79
|
+
const moonR = 8;
|
|
80
|
+
const angleStep = (2 * Math.PI) / moons.length;
|
|
81
|
+
const moonBg = getCssVar('--ltt-moon-bg', '#0f172a');
|
|
82
|
+
const moonStroke = getCssVar('--ltt-moon-stroke', 'rgba(255,255,255,0.1)');
|
|
83
|
+
el.innerHTML = moons.map((moon, i) => {
|
|
84
|
+
const angle = i * angleStep - Math.PI / 2;
|
|
85
|
+
const mx = 120 + orbRadius * Math.cos(angle);
|
|
86
|
+
const my = 120 + orbRadius * Math.sin(angle);
|
|
87
|
+
const info = calculateMoonPhase(currentDay, moon.period, moon.offset);
|
|
88
|
+
const phasePath = buildPhasePath(info.percentage, mx, my, moonR);
|
|
89
|
+
const pathSvg = phasePath ? `<path d="${phasePath}" fill="${moon.color}" style="filter:drop-shadow(0 0 3px ${moon.color})" />` : '';
|
|
90
|
+
return `<g><circle cx="${mx}" cy="${my}" r="${moonR}" fill="${moonBg}" stroke="${moonStroke}" stroke-width="0.5" />${pathSvg}<circle cx="${mx}" cy="${my}" r="${moonR}" fill="none" stroke="${moon.color}" stroke-width="0.5" opacity="0.4" /><title>${moon.name}: ${getPhaseLabel(info.phase, ui)}</title></g>`;
|
|
91
|
+
}).join('');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function updateCoreGlow(intensity: number): void {
|
|
95
|
+
const core = document.getElementById('ltt-core');
|
|
96
|
+
if (!core) return;
|
|
97
|
+
let glow = '2';
|
|
98
|
+
if (intensity > 75) glow = '8';
|
|
99
|
+
else if (intensity > 40) glow = '4';
|
|
100
|
+
core.style.filter = `drop-shadow(0 0 ${glow}px rgba(139,92,246,${intensity / 100}))`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function updateCorePhase(): void {
|
|
104
|
+
const pathEl = document.getElementById('ltt-phase-path');
|
|
105
|
+
if (!pathEl) return;
|
|
106
|
+
if (moons.length === 1) {
|
|
107
|
+
const info = calculateMoonPhase(currentDay, moons[0].period, moons[0].offset);
|
|
108
|
+
const d = buildPhasePath(info.percentage, 120, 120, 60);
|
|
109
|
+
pathEl.setAttribute('d', d || '');
|
|
110
|
+
pathEl.setAttribute('fill', d ? moons[0].color : 'transparent');
|
|
111
|
+
if (d) pathEl.setAttribute('style', `filter:drop-shadow(0 0 8px ${moons[0].color})`);
|
|
112
|
+
} else {
|
|
113
|
+
pathEl.setAttribute('d', '');
|
|
114
|
+
pathEl.setAttribute('fill', 'transparent');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function renderPortalCore(): void {
|
|
119
|
+
const tide = calculateCombinedTide(currentDay, moons);
|
|
120
|
+
updateCoreGlow(tide.intensity);
|
|
121
|
+
updateCorePhase();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function renderTideStatus(): void {
|
|
125
|
+
const tide = calculateCombinedTide(currentDay, moons);
|
|
126
|
+
const bar = document.getElementById('ltt-tide-bar');
|
|
127
|
+
const pct = document.getElementById('ltt-tide-pct');
|
|
128
|
+
const lbl = document.getElementById('ltt-tide-label');
|
|
129
|
+
if (bar) bar.style.width = `${tide.intensity}%`;
|
|
130
|
+
if (pct) pct.innerText = `${tide.intensity}%`;
|
|
131
|
+
if (lbl) lbl.innerText = tideLabelMap[tide.labelKey] || tide.labelKey;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderMoonsList(): void {
|
|
135
|
+
const container = document.getElementById('ltt-moons-list');
|
|
136
|
+
if (!container) return;
|
|
137
|
+
const ui = getUI();
|
|
138
|
+
const moonBg = getComputedStyle(document.documentElement).getPropertyValue('--ltt-moon-bg').trim() || '#0f172a';
|
|
139
|
+
container.innerHTML = moons.map((moon) => {
|
|
140
|
+
const info = calculateMoonPhase(currentDay, moon.period, moon.offset);
|
|
141
|
+
const label = getPhaseLabel(info.phase, ui);
|
|
142
|
+
const path = buildPhasePath(info.percentage, 12, 12, 11);
|
|
143
|
+
return `
|
|
144
|
+
<div class="ltt-moon-row" data-id="${moon.id}">
|
|
145
|
+
<svg class="ltt-moon-vis-mini" viewBox="0 0 24 24">
|
|
146
|
+
<circle cx="12" cy="12" r="11" fill="${moonBg}" stroke="${getCssVar('--ltt-moon-stroke', 'rgba(255,255,255,0.1)')}" stroke-width="0.5"/>
|
|
147
|
+
${path ? `<path d="${path}" fill="${moon.color}" style="filter:drop-shadow(0 0 2px ${moon.color})"/>` : ''}
|
|
148
|
+
</svg>
|
|
149
|
+
<input type="text" class="ltt-moon-input ltt-moon-name" value="${moon.name}" placeholder="${ui.moonName || 'Name'}" />
|
|
150
|
+
<input type="number" class="ltt-moon-num ltt-moon-period" value="${moon.period}" min="1" title="${ui.orbitalPeriod || 'Period'}" />
|
|
151
|
+
<input type="number" class="ltt-moon-num ltt-moon-offset" value="${moon.offset}" title="${ui.startingOffset || 'Offset'}" />
|
|
152
|
+
<input type="color" class="ltt-color-swatch" value="${moon.color}" />
|
|
153
|
+
<button type="button" class="ltt-remove-btn" data-id="${moon.id}" title="${ui.removeMoon || 'Remove'}">x</button>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="ltt-phase-tag"><span class="ltt-phase-dot" style="background:${moon.color};"></span>${moon.name}: ${label} (${Math.round(info.percentage * 100)}%)</div>
|
|
156
|
+
`;
|
|
157
|
+
}).join('');
|
|
158
|
+
bindMoonRowEvents(container);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function bindMoonRowEvents(container: HTMLElement): void {
|
|
162
|
+
container.querySelectorAll('.ltt-remove-btn').forEach((btn) => {
|
|
163
|
+
btn.addEventListener('click', (e) => {
|
|
164
|
+
const id = (e.currentTarget as HTMLElement).getAttribute('data-id');
|
|
165
|
+
if (moons.length > 1) { moons = moons.filter((m) => m.id !== id); updateAll(); }
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
container.querySelectorAll('.ltt-moon-row').forEach((row) => {
|
|
169
|
+
const id = row.getAttribute('data-id');
|
|
170
|
+
const moon = moons.find((m) => m.id === id);
|
|
171
|
+
if (!moon) return;
|
|
172
|
+
bindSingleMoon(row as HTMLElement, moon);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function bindSingleMoon(row: HTMLElement, moon: Moon): void {
|
|
177
|
+
row.querySelector('.ltt-moon-name')?.addEventListener('input', (e) => {
|
|
178
|
+
moon.name = (e.target as HTMLInputElement).value;
|
|
179
|
+
renderMoonsList(); renderMoonOrbs();
|
|
180
|
+
});
|
|
181
|
+
row.querySelector('.ltt-moon-period')?.addEventListener('input', (e) => {
|
|
182
|
+
const v = parseInt((e.target as HTMLInputElement).value);
|
|
183
|
+
moon.period = isNaN(v) || v < 1 ? 1 : v; renderAll();
|
|
184
|
+
});
|
|
185
|
+
row.querySelector('.ltt-moon-offset')?.addEventListener('input', (e) => {
|
|
186
|
+
const v = parseInt((e.target as HTMLInputElement).value);
|
|
187
|
+
moon.offset = isNaN(v) ? 0 : v; renderAll();
|
|
188
|
+
});
|
|
189
|
+
row.querySelector('.ltt-color-swatch')?.addEventListener('input', (e) => {
|
|
190
|
+
moon.color = (e.target as HTMLInputElement).value; renderAll();
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function renderForecast(): void {
|
|
195
|
+
const container = document.getElementById('ltt-forecast');
|
|
196
|
+
if (!container) return;
|
|
197
|
+
const ui = getUI();
|
|
198
|
+
const forecast = generateForecast(currentDay, 10, moons);
|
|
199
|
+
container.innerHTML = forecast.map((item) => {
|
|
200
|
+
const isPeak = item.tideLevel >= 80;
|
|
201
|
+
const dots = item.moons.map((m) => {
|
|
202
|
+
const src = moons.find((mn) => mn.id === m.moonId);
|
|
203
|
+
const col = src ? src.color : '#fff';
|
|
204
|
+
const op = m.phase === 'new' ? '0.15' : '1';
|
|
205
|
+
return `<div class="ltt-fc-dot" style="background:${col};opacity:${op};" title="${src?.name}: ${m.phase}"></div>`;
|
|
206
|
+
}).join('');
|
|
207
|
+
return `<div class="ltt-fc-cell${isPeak ? ' ltt-fc-peak' : ''}"><div class="ltt-fc-day">${ui.dayLabel || 'D'}${item.day}</div><div class="ltt-fc-dots">${dots}</div><div class="ltt-fc-pct">${item.tideLevel}%</div></div>`;
|
|
208
|
+
}).join('');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function renderAll(): void {
|
|
212
|
+
renderPortalCore();
|
|
213
|
+
renderMoonOrbs();
|
|
214
|
+
renderTideStatus();
|
|
215
|
+
renderMoonsList();
|
|
216
|
+
renderForecast();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function updateAll(): void {
|
|
220
|
+
const dayVal = document.getElementById('ltt-day-val');
|
|
221
|
+
const slider = document.getElementById('ltt-slider') as HTMLInputElement;
|
|
222
|
+
if (dayVal) dayVal.innerText = currentDay.toString();
|
|
223
|
+
if (slider) slider.value = currentDay.toString();
|
|
224
|
+
renderAll();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function bindStepButtons(): void {
|
|
228
|
+
const steps = [
|
|
229
|
+
{ id: 'lunar-btn-prev7', delta: -7 }, { id: 'lunar-btn-prev1', delta: -1 },
|
|
230
|
+
{ id: 'lunar-btn-next1', delta: 1 }, { id: 'lunar-btn-next7', delta: 7 },
|
|
231
|
+
];
|
|
232
|
+
steps.forEach(({ id, delta }) => {
|
|
233
|
+
document.getElementById(id)?.addEventListener('click', (e) => {
|
|
234
|
+
currentDay = Math.max(0, currentDay + delta);
|
|
235
|
+
spawnParticle(e.clientX, e.clientY, `${delta > 0 ? '+' : ''}${delta}`);
|
|
236
|
+
updateAll();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
document.getElementById('ltt-slider')?.addEventListener('input', (e) => {
|
|
240
|
+
currentDay = parseInt((e.target as HTMLInputElement).value) || 0;
|
|
241
|
+
updateAll();
|
|
242
|
+
});
|
|
243
|
+
document.getElementById('lunar-btn-reset')?.addEventListener('click', () => { currentDay = 0; updateAll(); });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function bindPresetsAndMoon(): void {
|
|
247
|
+
Object.entries(presets).forEach(([id, fn]) => {
|
|
248
|
+
const btn = document.getElementById(id);
|
|
249
|
+
btn?.addEventListener('click', () => {
|
|
250
|
+
moons = fn();
|
|
251
|
+
document.querySelectorAll('.ltt-preset-btn').forEach((b) => b.classList.remove('active'));
|
|
252
|
+
btn.classList.add('active');
|
|
253
|
+
updateAll();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
document.getElementById('ltt-add-moon')?.addEventListener('click', () => {
|
|
257
|
+
if (moons.length >= 3) return;
|
|
258
|
+
const colors = ['#a78bfa', '#60a5fa', '#34d399'];
|
|
259
|
+
moons.push({ id: Date.now().toString(), name: `Moon ${moons.length + 1}`, period: 30, offset: 0, color: colors[moons.length % 3] });
|
|
260
|
+
updateAll();
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
document.addEventListener('DOMContentLoaded', () => { bindStepButtons(); bindPresetsAndMoon(); updateAll(); });
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
import './lunar-tide-tracker.css';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div class="ltt-root" data-ui={JSON.stringify(ui)}>
|
|
12
|
+
|
|
13
|
+
<div class="ltt-presets">
|
|
14
|
+
<button type="button" id="lunar-preset-single" class="ltt-preset-btn" data-key="0">{ui.presetSingle || 'Classic Moon'}</button>
|
|
15
|
+
<button type="button" id="lunar-preset-double" class="ltt-preset-btn" data-key="1">{ui.presetDouble || 'Twin Moons'}</button>
|
|
16
|
+
<button type="button" id="lunar-preset-triple" class="ltt-preset-btn" data-key="2">{ui.presetTriple || 'Triple Eclipse'}</button>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="ltt-body">
|
|
20
|
+
|
|
21
|
+
<div class="ltt-portal-col">
|
|
22
|
+
<div class="ltt-portal-wrap">
|
|
23
|
+
<svg class="ltt-portal-svg" viewBox="0 0 240 240" aria-hidden="true">
|
|
24
|
+
<defs>
|
|
25
|
+
<radialGradient id="ltt-core-grad" cx="50%" cy="50%" r="50%">
|
|
26
|
+
<stop offset="0%" stop-color="#f0e6ff" />
|
|
27
|
+
<stop offset="40%" stop-color="#a78bfa" />
|
|
28
|
+
<stop offset="100%" stop-color="#1e0a3c" />
|
|
29
|
+
</radialGradient>
|
|
30
|
+
<radialGradient id="ltt-halo-grad" cx="50%" cy="50%" r="50%">
|
|
31
|
+
<stop offset="60%" stop-color="transparent" />
|
|
32
|
+
<stop offset="100%" stop-color="rgba(139,92,246,0.25)" />
|
|
33
|
+
</radialGradient>
|
|
34
|
+
<filter id="ltt-glow">
|
|
35
|
+
<feGaussianBlur stdDeviation="4" result="blur" />
|
|
36
|
+
<feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
|
|
37
|
+
</filter>
|
|
38
|
+
</defs>
|
|
39
|
+
<circle cx="120" cy="120" r="118" fill="url(#ltt-halo-grad)" />
|
|
40
|
+
<circle cx="120" cy="120" r="108" fill="none" stroke="rgba(139,92,246,0.12)" stroke-width="1" stroke-dasharray="3 6" class="ltt-ring ltt-ring-a" />
|
|
41
|
+
<circle cx="120" cy="120" r="90" fill="none" stroke="rgba(167,139,250,0.2)" stroke-width="1" class="ltt-ring ltt-ring-b" />
|
|
42
|
+
<circle cx="120" cy="120" r="72" fill="none" stroke="rgba(139,92,246,0.35)" stroke-width="1.5" class="ltt-ring ltt-ring-c" />
|
|
43
|
+
<circle id="ltt-core" cx="120" cy="120" r="60" fill="url(#ltt-core-grad)" filter="url(#ltt-glow)" />
|
|
44
|
+
<path id="ltt-phase-path" fill="transparent" />
|
|
45
|
+
<g id="ltt-moon-orbs"></g>
|
|
46
|
+
</svg>
|
|
47
|
+
<div class="ltt-tide-badge">
|
|
48
|
+
<div class="ltt-tide-label" id="ltt-tide-label">Full Tide</div>
|
|
49
|
+
<div class="ltt-tide-bar-wrap"><div class="ltt-tide-bar" id="ltt-tide-bar"></div></div>
|
|
50
|
+
<div class="ltt-tide-pct" id="ltt-tide-pct">50%</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="ltt-controls-col">
|
|
56
|
+
|
|
57
|
+
<div class="ltt-day-block">
|
|
58
|
+
<div class="ltt-day-row">
|
|
59
|
+
<span class="ltt-label">{ui.currentDay || 'Campaign Day'}</span>
|
|
60
|
+
<span class="ltt-day-val" id="ltt-day-val">0</span>
|
|
61
|
+
</div>
|
|
62
|
+
<input type="range" id="ltt-slider" class="ltt-slider" min="0" max="365" value="0" />
|
|
63
|
+
<div class="ltt-step-row">
|
|
64
|
+
<button type="button" id="lunar-btn-prev7" class="ltt-step-btn">-7</button>
|
|
65
|
+
<button type="button" id="lunar-btn-prev1" class="ltt-step-btn">-1</button>
|
|
66
|
+
<button type="button" id="lunar-btn-next1" class="ltt-step-btn ltt-step-pos">+1</button>
|
|
67
|
+
<button type="button" id="lunar-btn-next7" class="ltt-step-btn ltt-step-pos">+7</button>
|
|
68
|
+
<button type="button" id="lunar-btn-reset" class="ltt-reset-btn">{ui.resetButton || 'Day 0'}</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="ltt-moons-block">
|
|
73
|
+
<div class="ltt-moons-header">
|
|
74
|
+
<span class="ltt-label">{ui.moonsTitle || 'Moons'}</span>
|
|
75
|
+
<button type="button" id="ltt-add-moon" class="ltt-add-btn">+ {ui.addMoon || 'Add'}</button>
|
|
76
|
+
</div>
|
|
77
|
+
<div id="ltt-moons-list" class="ltt-moons-list"></div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div class="ltt-forecast-wrap">
|
|
84
|
+
<div class="ltt-forecast-label">{ui.forecastTitle || 'Forecast'}</div>
|
|
85
|
+
<div id="ltt-forecast" class="ltt-forecast"></div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<script src="./client.ts"></script>
|
|
91
|
+
|
|
92
|
+
<style>
|
|
93
|
+
@keyframes ltt-particle {
|
|
94
|
+
0% {
|
|
95
|
+
opacity: 1;
|
|
96
|
+
transform: translate(-50%, -50%) translateY(0) scale(1);
|
|
97
|
+
}
|
|
98
|
+
100% {
|
|
99
|
+
opacity: 0;
|
|
100
|
+
transform: translate(-50%, -50%) translateY(-36px) scale(0.5);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { TabletopToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { LunarTideUI } from './types';
|
|
3
|
+
|
|
4
|
+
export type LunarTideLocaleContent = ToolLocaleContent<LunarTideUI>;
|
|
5
|
+
|
|
6
|
+
export const lunarTideTracker: TabletopToolEntry<LunarTideUI> = {
|
|
7
|
+
id: 'lunar-tide-tracker',
|
|
8
|
+
icons: { bg: 'mdi:weather-night', fg: 'mdi:moon-waning-crescent' },
|
|
9
|
+
i18n: {
|
|
10
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
11
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
12
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
13
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
14
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
15
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
16
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
17
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
18
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
19
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
20
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
21
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
22
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
23
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
24
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
25
|
+
},
|
|
26
|
+
};
|