@jjlmoya/utils-tabletop 1.7.0 → 1.8.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 (32) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +14 -1
  3. package/src/index.ts +1 -0
  4. package/src/tests/locale_completeness.test.ts +1 -1
  5. package/src/tests/tool_validation.test.ts +1 -1
  6. package/src/tool/lunar-tide-tracker/bibliography.astro +16 -0
  7. package/src/tool/lunar-tide-tracker/bibliography.ts +8 -0
  8. package/src/tool/lunar-tide-tracker/client.ts +264 -0
  9. package/src/tool/lunar-tide-tracker/component.astro +103 -0
  10. package/src/tool/lunar-tide-tracker/entry.ts +26 -0
  11. package/src/tool/lunar-tide-tracker/i18n/de.ts +156 -0
  12. package/src/tool/lunar-tide-tracker/i18n/en.ts +214 -0
  13. package/src/tool/lunar-tide-tracker/i18n/es.ts +157 -0
  14. package/src/tool/lunar-tide-tracker/i18n/fr.ts +156 -0
  15. package/src/tool/lunar-tide-tracker/i18n/id.ts +156 -0
  16. package/src/tool/lunar-tide-tracker/i18n/it.ts +156 -0
  17. package/src/tool/lunar-tide-tracker/i18n/ja.ts +156 -0
  18. package/src/tool/lunar-tide-tracker/i18n/ko.ts +214 -0
  19. package/src/tool/lunar-tide-tracker/i18n/nl.ts +214 -0
  20. package/src/tool/lunar-tide-tracker/i18n/pl.ts +214 -0
  21. package/src/tool/lunar-tide-tracker/i18n/pt.ts +214 -0
  22. package/src/tool/lunar-tide-tracker/i18n/ru.ts +214 -0
  23. package/src/tool/lunar-tide-tracker/i18n/sv.ts +214 -0
  24. package/src/tool/lunar-tide-tracker/i18n/tr.ts +214 -0
  25. package/src/tool/lunar-tide-tracker/i18n/zh.ts +214 -0
  26. package/src/tool/lunar-tide-tracker/index.ts +9 -0
  27. package/src/tool/lunar-tide-tracker/logic.test.ts +56 -0
  28. package/src/tool/lunar-tide-tracker/logic.ts +76 -0
  29. package/src/tool/lunar-tide-tracker/lunar-tide-tracker.css +494 -0
  30. package/src/tool/lunar-tide-tracker/seo.astro +16 -0
  31. package/src/tool/lunar-tide-tracker/types.ts +70 -0
  32. 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.7.0",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
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 = [diceRollerSimulator, boardGameTimer, firstPlayerSelector, scoreTracker, initiativeTracker, fantasyRunesTranslator, decisionWheel, investigationBoard];
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 {
@@ -22,7 +22,7 @@ describe('Locale Completeness Validation', () => {
22
22
  });
23
23
 
24
24
  it('all tools registered', () => {
25
- expect(ALL_TOOLS.length).toBe(8);
25
+ expect(ALL_TOOLS.length).toBe(9);
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(8);
8
+ expect(ALL_TOOLS.length).toBe(9);
9
9
  });
10
10
 
11
11
 
@@ -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,8 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Astronomical Moon Phase Calculations',
6
+ url: 'https://en.wikipedia.org/wiki/Lunar_phase',
7
+ },
8
+ ];
@@ -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
+ };
@@ -0,0 +1,156 @@
1
+ import { bibliography } from '../bibliography';
2
+ import type { LunarTideLocaleContent } from '../entry';
3
+
4
+ export const content: LunarTideLocaleContent = {
5
+ slug: 'mondphasen-gezeiten-rechner',
6
+ title: 'Mondphasenrechner für Tabletop RPG: Eigene Fantasiemonde und Gezeitenzyklen berechnen',
7
+ description: 'Mondphasen für jede Fantasywelt mit eigenen Umlaufzeiten berechnen. Bis zu drei Monde gleichzeitig verfolgen, 10-Tage-Prognosen abrufen und einen Gezeitenstand für Magie- und Wettersysteme in D&D, Pathfinder oder anderen Rollenspielen ermitteln.',
8
+ ui: {
9
+ title: 'Mondphasen und Gezeitenrechner',
10
+ moonsTitle: 'Mondkonfiguration',
11
+ addMoon: 'Mond hinzufügen',
12
+ removeMoon: 'Mond entfernen',
13
+ moonName: 'Mondname',
14
+ orbitalPeriod: 'Umlaufzeit (Tage)',
15
+ startingOffset: 'Startversatz (Tage)',
16
+ timelineTitle: 'Zeitsteuerung',
17
+ currentDay: 'Aktueller Kampagnentag',
18
+ tideTitle: 'Mystischer Gezeitenstand',
19
+ tideLevel: 'Gezeitenenergie',
20
+ magicModifier: 'Magischer Gezeiteneffekt',
21
+ forecastTitle: 'Mondphasen- und Gezeitenprognose',
22
+ dayLabel: 'Tag',
23
+ resetButton: 'Auf Tag 0 zurücksetzen',
24
+ presetTitle: 'Kampagnen-Voreinstellungen',
25
+ presetSingle: 'Klassischer Mond',
26
+ presetDouble: 'Zwillingsmonde der Magie',
27
+ presetTriple: 'Dreifaches Finsternisreich',
28
+ newMoon: 'Neumond',
29
+ waxingCrescent: 'Zunehmende Sichel',
30
+ firstQuarter: 'Erstes Viertel',
31
+ waxingGibbous: 'Zunehmender Gibbous',
32
+ fullMoon: 'Vollmond',
33
+ waningGibbous: 'Abnehmender Gibbous',
34
+ lastQuarter: 'Letztes Viertel',
35
+ waningCrescent: 'Abnehmende Sichel',
36
+ },
37
+ seo: [
38
+ { type: 'title', text: 'Mondphasen in Tabletop-Rollenspielen verfolgen', level: 2 },
39
+ { type: 'paragraph', html: 'Die meisten Fantasywelten haben Monde mit anderen Umlaufzeiten als der reale Mond mit seinen 29,5 Tagen. Das Feld <strong>Umlaufzeit</strong> legt fest, wie viele Spieltage ein Mond für einen vollständigen Zyklus benötigt. Die Eberron-Welt verwendet 28 Tage, Mittelerde liegt bei ca. 30. Stellen Sie den Wert so ein, wie es das Lore Ihrer Welt vorgibt, oder wählen Sie einen Wert zwischen 5 und 60 Tagen.' },
40
+ {
41
+ type: 'stats',
42
+ items: [
43
+ { value: '3', label: 'Gleichzeitige Monde' },
44
+ { value: '10', label: 'Tage Prognose' },
45
+ { value: '5', label: 'Gezeitenstufen' }
46
+ ],
47
+ columns: 3
48
+ },
49
+ { type: 'title', text: 'Was der Startversatz bewirkt', level: 2 },
50
+ { type: 'paragraph', html: 'Der <strong>Startversatz</strong> verschiebt die Ausgangsphase eines Mondes am Kampagnentag 0. Ein Versatz von 0 beginnt beim Neumond. Ein Versatz von der Hälfte der Umlaufzeit startet beim Vollmond. Nutzen Sie dies, um die in Ihrer Einstiegsszene beschriebene Phase zu reproduzieren, oder damit zwei Monde nicht immer gleichzeitig stehen.' },
51
+ { type: 'title', text: 'Was der Gezeitenstand im Spiel bedeutet', level: 2 },
52
+ { type: 'paragraph', html: 'Der Gezeitenstand kombiniert die Anziehungskraft aller aktiven Monde anhand ihrer aktuellen Phasen. Eine <strong>Springflut</strong> (über 75%) entsteht, wenn Monde nahezu ausgerichtet sind. Eine <strong>Nipptide</strong> (unter 30%) tritt auf, wenn Monde entgegengesetzte Phasen haben. Binden Sie dies an Spielmechaniken: Rettungswurf-Boni, Werwolf-Verwandlungen oder Sturmschwere auf Küstenkarten.' },
53
+ {
54
+ type: 'comparative',
55
+ items: [
56
+ {
57
+ title: 'Einzelner Mond',
58
+ description: 'Ideal für Welten ähnlich der realen Welt oder mit einem dominanten Mond. Einfacher 8-Phasen-Zyklus.',
59
+ icon: 'mdi:moon-full',
60
+ highlight: false,
61
+ points: [
62
+ 'D&D Vergessene Reiche (Selune, 30 Tage)',
63
+ 'Werwolf- und Lykanthropie-Kampagnen',
64
+ 'Horror-Sitzungen beim Vollmond'
65
+ ]
66
+ },
67
+ {
68
+ title: 'Zwillingsmonde',
69
+ description: 'Häufig in High-Fantasy-Settings. Erzeugt interessante Teilausrichtungen und abwechslungsreiche Gezeiten.',
70
+ icon: 'mdi:moon-waxing-crescent',
71
+ highlight: true,
72
+ points: [
73
+ 'Eberron (Rhaan 28T + Aryth 12T)',
74
+ 'Vorhersagbare Finsterniszeiten',
75
+ 'Gut für druidische Magiesysteme'
76
+ ]
77
+ },
78
+ {
79
+ title: 'Drei Monde',
80
+ description: 'Seltene Dreifachausrichtungen erzeugen unvorhersehbare Energieschübe. Dramatisch nützlich für wilde Magieereignisse.',
81
+ icon: 'mdi:moon-waning-crescent',
82
+ highlight: false,
83
+ points: [
84
+ 'Wilde-Magie-Tabellen bei Ausrichtung',
85
+ 'Pathfinder Golarion-Komplexität',
86
+ 'Langperiodische Zyklen für epische Kampagnen'
87
+ ]
88
+ }
89
+ ],
90
+ columns: 3
91
+ },
92
+ { type: 'title', text: 'Häufige Umlaufzeiten für Fantasywelten', level: 3 },
93
+ {
94
+ type: 'glossary',
95
+ items: [
96
+ { term: 'Selune (Vergessene Reiche)', definition: '30-tägige Umlaufzeit, einzelner Mond. Versatz 15 für Vollmond am ersten Spielabend.' },
97
+ { term: 'Eberron-Monde', definition: 'Eberron hat 12 Monde mit Umlaufzeiten von 9 bis 28 Tagen. Die zwei handlungsrelevantesten als separate Mond-Instanzen verwenden.' },
98
+ { term: 'Golarion (Pathfinder)', definition: 'Golarion hat einen Mond, Somal, mit einem 29-Tage-Zyklus. Zweiter Mond mit kurzem Zyklus modelliert Finstere Tapisserie-Ereignisse.' },
99
+ { term: 'Eigene Welt', definition: 'Ein 15-Tage-Mond erzeugt 24 Vollmonde pro Spieljahr. Ein 45-Tage-Mond nur 8. Kürzere Zyklen bedeuten häufigere Ereignisse, aber weniger Dramatik.' }
100
+ ]
101
+ },
102
+ {
103
+ type: 'diagnostic',
104
+ variant: 'info',
105
+ title: 'Tipp: Bestimmte Phase für Sitzung 1 einrichten',
106
+ icon: 'mdi:information-outline',
107
+ badge: 'SETUP-TIPP',
108
+ html: 'Für eine Sichel am Spielbeginn: Versatz auf etwa 20 % der Umlaufzeit setzen. Für Vollmond: 50 % der Umlaufzeit als Versatz verwenden. Beispiel: 28-Tage-Mond, Vollmond-Start: Versatz 14. Tageszähler auf den aktuellen Kampagnentag stellen und die Prognose zeigt automatisch die nächsten 10 Tage.'
109
+ }
110
+ ],
111
+ faq: [
112
+ { question: 'Welche Umlaufzeit sollte ich für meine Fantasywelt verwenden?', answer: 'Falls Ihr Setting nichts vorgibt, funktionieren 28 bis 30 Tage gut für einen einzelnen Mond. Für mehrere Monde wählen Sie Zeiten, die keine einfachen Vielfachen voneinander sind, damit Ausrichtungen nicht zu häufig vorkommen.' },
113
+ { question: 'Wie bringe ich zwei Monde an einem bestimmten Kampagnenmoment zur Ausrichtung?', answer: 'Setzen Sie beide Monde auf denselben Versatzwert. Sie richten sich bei jedem gemeinsamen Vielfachen ihrer Umlaufzeiten neu aus. Für versetzten Start gibt der zweite Mond einen Versatz von der Hälfte seiner Umlaufzeit.' },
114
+ { question: 'Was bedeutet Springflut in Spielmechaniken?', answer: 'Springflut bedeutet, dass alle aktiven Monde maximale Gezeitenenergie erzeugen. Nutzen Sie dies für wilde Magieauslöser, verstärkte Heilzauber, schwerere Stürme oder als heilige Nacht für religiöse Fraktionen.' },
115
+ { question: 'Was bedeutet Nipptide in Spielmechaniken?', answer: 'Nipptide tritt auf, wenn Monde sich gegenseitig abschwächen. Gut für Antimagie-Effekte, Totenstille auf See oder für Antagonisten, die verlässliche Magieunterdrückung für ihre Pläne benötigen.' },
116
+ { question: 'Kann ich dies für Cthulhu- oder Horror-Kampagnen verwenden?', answer: 'Ja. Einen 29-Tage-Mond verwenden und Versatz so setzen, dass der Vollmond auf den Sitzungshöhepunkt fällt. Der Gezeitenstand kann Mythos-Einfluss oder Höhepunkte der psychischen Empfindlichkeit darstellen.' },
117
+ { question: 'Wie funktioniert die 10-Tage-Prognose?', answer: 'Die Prognose zeigt die Mondphase jedes aktiven Mondes als farbigen Punkt für jeden der nächsten 10 Kampagnentage. Der Prozentsatz ist der kombinierte Gezeitenstand. Hohe Werte eignen sich für dramatische Begegnungen.' },
118
+ { question: 'Kann ich einen Mond mit sehr kurzem Zyklus, z. B. 7 Tagen, verfolgen?', answer: 'Ja. Umlaufzeit auf 7 setzen. Der Mond durchläuft alle 8 Phasen in einer Spielwoche. Nützlich für magische Systeme, die wöchentlich zurückgesetzt werden.' },
119
+ { question: 'Wie gleiche ich die Mondphase aus einem veröffentlichten Abenteuermodul ab?', answer: 'Bestimmen Sie, welcher Phase im Abenteuer der beschriebene Zustand entspricht, und teilen Sie diesen Tageszähler durch die Umlaufzeit. Beispiel: Halbmond am Tag 0 eines 28-Tage-Mondes bedeutet Versatz 7.' }
120
+ ],
121
+ bibliography,
122
+ howTo: [
123
+ { name: 'Umlaufzeit festlegen', text: 'Anzahl der Spieltage eingeben, die der Mond für einen vollständigen Zyklus benötigt.' },
124
+ { name: 'Startversatz festlegen', text: 'Startphase durch einen Tagesversatz verschieben. Hälfte der Umlaufzeit entspricht Vollmond-Start.' },
125
+ { name: 'Tageszähler vorrücken', text: 'Schaltflächen oder Schieberegler auf den aktuellen Kampagnentag stellen und Phase sowie Gezeitenstand ablesen.' }
126
+ ],
127
+ schemas: [
128
+ {
129
+ '@context': 'https://schema.org',
130
+ '@type': 'SoftwareApplication',
131
+ 'name': 'Mondphasenrechner für Tabletop RPG: Eigene Fantasiemonde und Gezeitenzyklen berechnen',
132
+ 'operatingSystem': 'All',
133
+ 'applicationCategory': 'UtilitiesApplication',
134
+ 'browserRequirements': 'Requires JavaScript.'
135
+ },
136
+ {
137
+ '@context': 'https://schema.org',
138
+ '@type': 'FAQPage',
139
+ 'mainEntity': [
140
+ { '@type': 'Question', 'name': 'Welche Umlaufzeit sollte ich für meine Fantasywelt verwenden?', 'acceptedAnswer': { '@type': 'Answer', 'text': 'Falls Ihr Setting nichts vorgibt, funktionieren 28 bis 30 Tage gut für einen einzelnen Mond. Für mehrere Monde wählen Sie Zeiten, die keine einfachen Vielfachen voneinander sind.' } },
141
+ { '@type': 'Question', 'name': 'Was bedeutet Springflut in Spielmechaniken?', 'acceptedAnswer': { '@type': 'Answer', 'text': 'Springflut bedeutet maximale Gezeitenenergie. Nutzen Sie dies für wilde Magieauslöser, verstärkte Heilzauber oder als heilige Nacht für religiöse Fraktionen.' } }
142
+ ]
143
+ },
144
+ {
145
+ '@context': 'https://schema.org',
146
+ '@type': 'HowTo',
147
+ 'name': 'Mondphasen in einer Tabletop-RPG-Kampagne verfolgen',
148
+ 'step': [
149
+ { '@type': 'HowToStep', 'name': 'Umlaufzeit festlegen', 'text': 'Anzahl der Spieltage eingeben, die der Mond für einen vollständigen Zyklus benötigt.' },
150
+ { '@type': 'HowToStep', 'name': 'Startversatz festlegen', 'text': 'Startphase durch einen Tagesversatz verschieben.' },
151
+ { '@type': 'HowToStep', 'name': 'Tageszähler vorrücken', 'text': 'Schieberegler auf den aktuellen Kampagnentag stellen und Phase ablesen.' }
152
+ ]
153
+ }
154
+ ]
155
+ };
156
+ export default content;