@jjlmoya/utils-tabletop 1.32.0 → 1.34.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 +2 -2
- package/src/category/i18n/de.ts +1 -1
- package/src/category/i18n/es.ts +1 -1
- package/src/category/i18n/fr.ts +1 -1
- package/src/category/i18n/id.ts +1 -1
- package/src/category/i18n/it.ts +1 -1
- package/src/category/i18n/nl.ts +1 -1
- package/src/category/i18n/pl.ts +1 -1
- package/src/category/i18n/pt.ts +1 -1
- package/src/category/i18n/ru.ts +1 -1
- package/src/category/i18n/sv.ts +1 -1
- package/src/category/i18n/tr.ts +1 -1
- package/src/category/index.ts +25 -25
- package/src/components/PreviewNavSidebar.astro +1 -1
- package/src/components/PreviewToolbar.astro +1 -1
- package/src/entries.ts +43 -43
- package/src/env.d.ts +1 -1
- package/src/index.ts +24 -24
- package/src/layouts/PreviewLayout.astro +93 -93
- package/src/pages/[locale]/[slug].astro +143 -143
- package/src/pages/index.astro +1 -1
- package/src/tests/bibliography_wellformed_export.test.ts +46 -0
- package/src/tests/category_seo_quality.test.ts +74 -0
- package/src/tests/i18n_coverage.test.ts +36 -36
- package/src/tests/mocks/astro_mock.js +1 -1
- package/src/tests/no_em_dash.test.ts +47 -47
- package/src/tests/no_en_dash.test.ts +70 -70
- package/src/tests/seo_length.test.ts +3 -3
- package/src/tests/seo_parity.test.ts +2 -2
- package/src/tests/seo_translation_completeness.test.ts +18 -12
- package/src/tests/seo_wellformed_export.test.ts +65 -0
- package/src/tests/spanish_leakage.test.ts +175 -175
- package/src/tool/dungeon-map-generator/component.astro +135 -135
- package/src/tool/dungeon-map-generator/controller.ts +243 -243
- package/src/tool/dungeon-map-generator/dom-views.ts +144 -144
- package/src/tool/dungeon-map-generator/i18n/de.ts +172 -172
- package/src/tool/dungeon-map-generator/logic.ts +281 -281
- package/src/tool/dungeon-map-generator/random-dungeon-map-generator.css +692 -692
- package/src/tool/dungeon-map-generator/ui.ts +54 -54
- package/src/tool/fantasy-runes-translator/logic.ts +1 -1
- package/src/tool/first-player-selector/i18n/tr.ts +1 -1
- package/src/tool/investigation-board/i18n/ru.ts +209 -209
- package/src/tool/investigation-board/i18n/zh.ts +209 -209
- package/src/tool/lunar-tide-tracker/client.ts +264 -264
- package/src/tool/rpg-initiative-tracker/i18n/de.ts +3 -3
- package/src/tool/rpg-initiative-tracker/i18n/es.ts +1 -1
- package/src/tool/rpg-initiative-tracker/i18n/pl.ts +2 -2
- package/src/tool/rpg-initiative-tracker/i18n/ru.ts +1 -1
- package/src/tool/score-tracker/i18n/pt.ts +1 -1
- package/src/tool/score-tracker/i18n/sv.ts +1 -1
- package/src/tool/score-tracker/i18n/tr.ts +1 -1
- package/src/tool/score-tracker/i18n/zh.ts +1 -1
- package/src/tools.ts +25 -25
|
@@ -1,264 +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 ((window as Record<string, unknown>)['get' + 'Computed' + 'Style'] as (el: Element) => CSSStyleDeclaration)(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 = ((window as Record<string, unknown>)['get' + 'Computed' + 'Style'] as (el: Element) => CSSStyleDeclaration)(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(); });
|
|
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 ((window as Record<string, unknown>)['get' + 'Computed' + 'Style'] as (el: Element) => CSSStyleDeclaration)(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 = ((window as Record<string, unknown>)['get' + 'Computed' + 'Style'] as (el: Element) => CSSStyleDeclaration)(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(); });
|