feat: add money converter dialog, bump version to 1.3.36
Extract currency conversion out of character/NPC sheets into a dedicated dialog with live NBP exchange rates, covering both real-world (PLN/EUR/USD) and in-world currencies (Thrakka, Sol-3 Federation Credit, Silver Talent, Gold Crown, Platinum Lingot).
This commit is contained in:
@@ -573,6 +573,37 @@
|
||||
"zakazana-wiedza-dziedzina": "Forbidden Knowledge (Discipline)",
|
||||
"zawisanie-w-powietrzu": "Hovering",
|
||||
"naturalny-lot": "Natural Flight"
|
||||
},
|
||||
"moneyConverter": {
|
||||
"liveRates": "📡 Rates from NBP ({date})",
|
||||
"defaultRates": "📡 Default rates (no connection to NBP)",
|
||||
"actorCash": "Actor cash:",
|
||||
"campaignYear": "Campaign year: {year}",
|
||||
"calculator": "Calculator",
|
||||
"resetToCash": "Reset to cash",
|
||||
"exchangeRatesTable": "Exchange rates table",
|
||||
"currency": "Currency",
|
||||
"ratePln": "Rate (PLN)",
|
||||
"onePlnEquals": "1 PLN =",
|
||||
"note": "Note",
|
||||
"currencies": {
|
||||
"PLN": "PLN (Zloty)",
|
||||
"EUR": "EUR (Euro)",
|
||||
"USD": "USD (US Dollar)",
|
||||
"TH": "TH (Thrakka)",
|
||||
"FC": "FC (Sol-3 Federation Credit)",
|
||||
"ST": "ST (Silver Talent)",
|
||||
"ZK": "ZK (Gold Crown)",
|
||||
"PL": "PL (Platinum Lingot)"
|
||||
},
|
||||
"notes": {
|
||||
"nbp": "NBP {date}",
|
||||
"th": "1:1 with EUR",
|
||||
"fc": "fixed rate 2 PLN = 1 FC",
|
||||
"st": "1 ST = 240 PLN",
|
||||
"zk": "1 ZK = 12 ST",
|
||||
"pl": "1 PL = 12 ZK"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -576,6 +576,37 @@
|
||||
"zakazana-wiedza-dziedzina": "Zakazana wiedza (Dziedzina)",
|
||||
"zawisanie-w-powietrzu": "Zawisanie w powietrzu",
|
||||
"naturalny-lot": "Naturalny lot"
|
||||
},
|
||||
"moneyConverter": {
|
||||
"liveRates": "📡 Kursy z NBP ({date})",
|
||||
"defaultRates": "📡 Domyślne kursy (brak połączenia z NBP)",
|
||||
"actorCash": "Gotówka aktora:",
|
||||
"campaignYear": "Rok kampanii: {year}",
|
||||
"calculator": "Kalkulator",
|
||||
"resetToCash": "Przywróć gotówkę",
|
||||
"exchangeRatesTable": "Tabela kursów wymiany",
|
||||
"currency": "Waluta",
|
||||
"ratePln": "Kurs (PLN)",
|
||||
"onePlnEquals": "1 PLN =",
|
||||
"note": "Uwaga",
|
||||
"currencies": {
|
||||
"PLN": "PLN (Złoty)",
|
||||
"EUR": "EUR (Euro)",
|
||||
"USD": "USD (Dolar)",
|
||||
"TH": "TH (Thrakka)",
|
||||
"FC": "FC (Kredyt Federacji Sol-3)",
|
||||
"ST": "ST (Srebrny Talent)",
|
||||
"ZK": "ZK (Złota Korona)",
|
||||
"PL": "PL (Platynowy Lingot)"
|
||||
},
|
||||
"notes": {
|
||||
"nbp": "NBP {date}",
|
||||
"th": "1:1 z EUR",
|
||||
"fc": "stały kurs 2 PLN = 1 FC",
|
||||
"st": "1 ST = 240 PLN",
|
||||
"zk": "1 ZK = 12 ST",
|
||||
"pl": "1 PL = 12 ZK"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hbm-rpg-v3-foundry",
|
||||
"version": "1.3.30",
|
||||
"version": "1.3.36",
|
||||
"description": "Foundry VTT system for Homebrew Magic: Role Playing Game v3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { askApplyDamage } from '../dice/damage-dialog';
|
||||
import { applyDamage } from '../logic/damage';
|
||||
import { rest } from '../logic/rest';
|
||||
import { ATTRIBUTES, AttributeKey, SKILL_KEYS, getMagicPowerEntry } from '../constants';
|
||||
import { showMoneyConverter } from './money-converter-dialog';
|
||||
|
||||
const { ActorSheetV2 } = foundry.applications.sheets as unknown as {
|
||||
ActorSheetV2: typeof foundry.applications.sheets.ActorSheetV2;
|
||||
@@ -482,101 +483,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
|
||||
static async _onRecalculateMoney(this: CharacterSheet, _event: PointerEvent, _target: HTMLElement) {
|
||||
const actor = (this as unknown as { actor: any }).actor;
|
||||
const pln = actor.system.details?.money ?? 0;
|
||||
const currentYear = actor.system.details?.currentYear ?? 2026;
|
||||
const currentRealYear = new Date().getFullYear();
|
||||
|
||||
const fetchNBP = async (currency: string, year: number): Promise<{ rate: number; date: string } | null> => {
|
||||
if (year >= 2002 && year <= currentRealYear) {
|
||||
for (let day = 1; day <= 7; day++) {
|
||||
const d = `${year}-06-${String(day).padStart(2, '0')}`;
|
||||
try {
|
||||
const res = await fetch(`https://api.nbp.pl/api/exchangerates/rates/a/${currency}/${d}/?format=json`);
|
||||
if (res.ok) { const data = await res.json(); const r = data?.rates?.[0]?.mid; if (typeof r === 'number') return { rate: r, date: d }; }
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`https://api.nbp.pl/api/exchangerates/rates/a/${currency}/?format=json`);
|
||||
if (res.ok) { const data = await res.json(); const r = data?.rates?.[0]?.mid; const dt = data?.rates?.[0]?.effectiveDate || ''; if (typeof r === 'number') return { rate: r, date: dt }; }
|
||||
} catch (_) { /* ignore */ }
|
||||
return null;
|
||||
};
|
||||
|
||||
const [eurData, usdData] = await Promise.all([fetchNBP('eur', currentYear), fetchNBP('usd', currentYear)]);
|
||||
const eurRate = eurData?.rate ?? 4.35;
|
||||
const usdRate = usdData?.rate ?? 4.00;
|
||||
const rateDate = eurData?.date ?? usdData?.date ?? 'default';
|
||||
const isLive = !!(eurData || usdData);
|
||||
|
||||
const CURRENCIES: Record<string, { label: string; toPln: number; note: string }> = {
|
||||
PLN: { label: 'PLN (Złoty)', toPln: 1, note: '' },
|
||||
EUR: { label: 'EUR (Euro)', toPln: eurRate, note: `NBP ${rateDate}` },
|
||||
USD: { label: 'USD (Dolar)', toPln: usdRate, note: `NBP ${rateDate}` },
|
||||
TH: { label: 'TH (Thrakka)', toPln: eurRate, note: '1:1 z EUR' },
|
||||
FC: { label: 'FC (Kredyt Federacji Sol-3)', toPln: 2.0, note: 'stały kurs 2 PLN = 1 FC' },
|
||||
ST: { label: 'ST (Srebrny Talent)', toPln: 240, note: '1 ST = 240 PLN' },
|
||||
ZK: { label: 'ZK (Złota Korona)', toPln: 2880, note: '1 ZK = 12 ST' },
|
||||
PL: { label: 'PL (Platynowy Lingot)', toPln: 34560, note: '1 PL = 12 ZK' },
|
||||
};
|
||||
const currKeys = Object.keys(CURRENCIES);
|
||||
const optHtml = (sel: string) => currKeys.map(k =>
|
||||
`<option value="${k}"${k === sel ? ' selected' : ''}>${CURRENCIES[k].label}</option>`
|
||||
).join('');
|
||||
const ratesRows = currKeys.map(k => {
|
||||
const c = CURRENCIES[k];
|
||||
const fromPln = k === 'PLN' ? '1.0000' : (1 / c.toPln).toPrecision(4);
|
||||
return `<tr><td>${c.label}</td><td style="text-align:right;font-family:monospace;">${c.toPln === 1 ? '1.0000' : c.toPln.toFixed(4)}</td><td style="text-align:right;font-family:monospace;">${fromPln}</td><td style="color:#777;font-size:0.75rem;">${c.note}</td></tr>`;
|
||||
}).join('');
|
||||
const ratesJson = JSON.stringify(Object.fromEntries(currKeys.map(k => [k, CURRENCIES[k].toPln])));
|
||||
|
||||
const content = `
|
||||
<div class="hbm money-converter" style="padding:10px;font-family:'Signika',sans-serif;display:flex;flex-direction:column;gap:12px;">
|
||||
<p style="margin:0;font-size:0.78rem;color:${isLive ? '#2a7a3e' : '#888'};">
|
||||
<em>📡 ${isLive ? `Kursy z NBP (${rateDate})` : 'Domyślne kursy (brak połączenia z NBP)'}</em>
|
||||
</p>
|
||||
<div style="background:var(--hbm-card-bg,#1a1a2e);border:1px solid var(--hbm-border,#333);border-radius:6px;padding:8px 12px;">
|
||||
<span style="font-weight:bold;">Gotówka aktora:</span>
|
||||
<span style="font-family:monospace;font-size:1.1rem;margin-left:6px;">${pln} PLN</span>
|
||||
<small style="color:#888;margin-left:6px;">(Rok kampanii: ${currentYear})</small>
|
||||
</div>
|
||||
<div style="background:var(--hbm-card-bg,#1a1a2e);border:1px solid var(--hbm-accent,#6060c0);border-radius:6px;padding:10px 12px;display:flex;flex-direction:column;gap:8px;">
|
||||
<strong style="font-size:0.9rem;">Kalkulator</strong>
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
|
||||
<input id="hbm-conv-left" type="number" value="1" min="0" step="any"
|
||||
style="width:6.5rem;font-size:1rem;text-align:right;padding:3px 6px;background:transparent;color:var(--hbm-fg,#eee);border:1px solid var(--hbm-border,#555);border-radius:4px;"
|
||||
oninput="(function(v){var r=window._hbmRates;var lk=document.getElementById('hbm-sf').value;var rk=document.getElementById('hbm-st').value;var res=v*r[lk]/r[rk];document.getElementById('hbm-conv-right').value=isFinite(res)?res.toFixed(4):'';})(this.value)" />
|
||||
<select id="hbm-sf" style="flex:1;min-width:8rem;"
|
||||
onchange="(function(){var v=parseFloat(document.getElementById('hbm-conv-left').value)||0;var r=window._hbmRates;var lk=this.value;var rk=document.getElementById('hbm-st').value;var res=v*r[lk]/r[rk];document.getElementById('hbm-conv-right').value=isFinite(res)?res.toFixed(4):'';}).call(this)">
|
||||
${optHtml('PLN')}
|
||||
</select>
|
||||
<span style="font-size:1.2rem;color:var(--hbm-accent,#8080ff);">⇄</span>
|
||||
<input id="hbm-conv-right" type="number" value="" min="0" step="any"
|
||||
style="width:6.5rem;font-size:1rem;text-align:right;padding:3px 6px;background:transparent;color:var(--hbm-fg,#eee);border:1px solid var(--hbm-border,#555);border-radius:4px;"
|
||||
oninput="(function(v){var r=window._hbmRates;var lk=document.getElementById('hbm-sf').value;var rk=document.getElementById('hbm-st').value;var res=v*r[rk]/r[lk];document.getElementById('hbm-conv-left').value=isFinite(res)?res.toFixed(4):'';}).call(this)" />
|
||||
<select id="hbm-st" style="flex:1;min-width:8rem;"
|
||||
onchange="(function(){var v=parseFloat(document.getElementById('hbm-conv-right').value)||0;var r=window._hbmRates;var lk=document.getElementById('hbm-sf').value;var rk=this.value;var res=v*r[rk]/r[lk];document.getElementById('hbm-conv-left').value=isFinite(res)?res.toFixed(4):'';}).call(this)">
|
||||
${optHtml('EUR')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<details>
|
||||
<summary style="cursor:pointer;font-size:0.85rem;color:var(--hbm-accent-dim,#999);">▸ Tabela kursów wymiany</summary>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:0.8rem;margin-top:8px;">
|
||||
<thead><tr style="border-bottom:1px solid var(--hbm-border,#555);">
|
||||
<th style="text-align:left;">Waluta</th><th style="text-align:right;">Kurs (PLN)</th><th style="text-align:right;">1 PLN =</th><th>Uwaga</th>
|
||||
</tr></thead>
|
||||
<tbody>${ratesRows}</tbody>
|
||||
</table>
|
||||
</details>
|
||||
</div>
|
||||
<script>window._hbmRates=${ratesJson};</script>`;
|
||||
|
||||
await (foundry.applications.api as any).DialogV2.prompt({
|
||||
window: { title: game.i18n.localize('HBM.ui.moneyConverterTitle') },
|
||||
content,
|
||||
rejectClose: false,
|
||||
});
|
||||
return showMoneyConverter(actor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
const { DialogV2 } = foundry.applications.api as unknown as {
|
||||
DialogV2: any;
|
||||
};
|
||||
|
||||
class MoneyConverterDialog extends DialogV2 {
|
||||
rates: Record<string, number>;
|
||||
|
||||
constructor(options: any, rates: Record<string, number>) {
|
||||
super(options);
|
||||
this.rates = rates;
|
||||
}
|
||||
|
||||
protected override _onRender(context: any, options: any) {
|
||||
super._onRender(context, options);
|
||||
const html = this.element;
|
||||
|
||||
const leftInput = html.querySelector('#hbm-conv-left') as HTMLInputElement;
|
||||
const rightInput = html.querySelector('#hbm-conv-right') as HTMLInputElement;
|
||||
const leftSelect = html.querySelector('#hbm-sf') as HTMLSelectElement;
|
||||
const rightSelect = html.querySelector('#hbm-st') as HTMLSelectElement;
|
||||
|
||||
if (!leftInput || !rightInput || !leftSelect || !rightSelect) return;
|
||||
|
||||
const calcRight = () => {
|
||||
const v = parseFloat(leftInput.value);
|
||||
if (Number.isNaN(v)) {
|
||||
rightInput.value = '';
|
||||
return;
|
||||
}
|
||||
const lk = leftSelect.value;
|
||||
const rk = rightSelect.value;
|
||||
const res = v * this.rates[lk] / this.rates[rk];
|
||||
rightInput.value = isFinite(res) ? res.toFixed(4) : '';
|
||||
};
|
||||
|
||||
const calcLeft = () => {
|
||||
const v = parseFloat(rightInput.value);
|
||||
if (Number.isNaN(v)) {
|
||||
leftInput.value = '';
|
||||
return;
|
||||
}
|
||||
const lk = leftSelect.value;
|
||||
const rk = rightSelect.value;
|
||||
const res = v * this.rates[rk] / this.rates[lk];
|
||||
leftInput.value = isFinite(res) ? res.toFixed(4) : '';
|
||||
};
|
||||
|
||||
leftInput.addEventListener('input', calcRight);
|
||||
leftSelect.addEventListener('change', calcRight);
|
||||
rightInput.addEventListener('input', calcLeft);
|
||||
rightSelect.addEventListener('change', calcRight);
|
||||
}
|
||||
}
|
||||
|
||||
export async function showMoneyConverter(actor: any): Promise<void> {
|
||||
const pln = actor.system.details?.money ?? 0;
|
||||
const currentYear = actor.system.details?.currentYear ?? 2026;
|
||||
const currentRealYear = new Date().getFullYear();
|
||||
|
||||
const fetchNBP = async (currency: string, year: number): Promise<{ rate: number; date: string } | null> => {
|
||||
if (year >= 2002 && year <= currentRealYear) {
|
||||
for (let day = 1; day <= 7; day++) {
|
||||
const d = `${year}-06-${String(day).padStart(2, '0')}`;
|
||||
try {
|
||||
const res = await fetch(`https://api.nbp.pl/api/exchangerates/rates/a/${currency}/${d}/?format=json`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const r = data?.rates?.[0]?.mid;
|
||||
if (typeof r === 'number') return { rate: r, date: d };
|
||||
}
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`https://api.nbp.pl/api/exchangerates/rates/a/${currency}/?format=json`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const r = data?.rates?.[0]?.mid;
|
||||
const dt = data?.rates?.[0]?.effectiveDate || '';
|
||||
if (typeof r === 'number') return { rate: r, date: dt };
|
||||
}
|
||||
} catch (_) { /* ignore */ }
|
||||
return null;
|
||||
};
|
||||
|
||||
const [eurData, usdData] = await Promise.all([fetchNBP('eur', currentYear), fetchNBP('usd', currentYear)]);
|
||||
const eurRate = eurData?.rate ?? 4.35;
|
||||
const usdRate = usdData?.rate ?? 4.00;
|
||||
const rateDate = eurData?.date ?? usdData?.date ?? 'default';
|
||||
const isLive = !!(eurData || usdData);
|
||||
|
||||
const CURRENCIES: Record<string, { labelKey: string; toPln: number; noteKey: string; noteParam?: Record<string, any> }> = {
|
||||
PLN: { labelKey: 'HBM.moneyConverter.currencies.PLN', toPln: 1, noteKey: '' },
|
||||
EUR: { labelKey: 'HBM.moneyConverter.currencies.EUR', toPln: eurRate, noteKey: 'HBM.moneyConverter.notes.nbp', noteParam: { date: rateDate } },
|
||||
USD: { labelKey: 'HBM.moneyConverter.currencies.USD', toPln: usdRate, noteKey: 'HBM.moneyConverter.notes.nbp', noteParam: { date: rateDate } },
|
||||
TH: { labelKey: 'HBM.moneyConverter.currencies.TH', toPln: eurRate, noteKey: 'HBM.moneyConverter.notes.th' },
|
||||
FC: { labelKey: 'HBM.moneyConverter.currencies.FC', toPln: 2.0, noteKey: 'HBM.moneyConverter.notes.fc' },
|
||||
ST: { labelKey: 'HBM.moneyConverter.currencies.ST', toPln: 240, noteKey: 'HBM.moneyConverter.notes.st' },
|
||||
ZK: { labelKey: 'HBM.moneyConverter.currencies.ZK', toPln: 2880, noteKey: 'HBM.moneyConverter.notes.zk' },
|
||||
PL: { labelKey: 'HBM.moneyConverter.currencies.PL', toPln: 34560, noteKey: 'HBM.moneyConverter.notes.pl' },
|
||||
};
|
||||
|
||||
const currKeys = Object.keys(CURRENCIES);
|
||||
const optHtml = (sel: string) => currKeys.map(k => {
|
||||
const label = game.i18n.localize(CURRENCIES[k].labelKey);
|
||||
return `<option value="${k}"${k === sel ? ' selected' : ''}>${label}</option>`;
|
||||
}).join('');
|
||||
|
||||
const ratesRows = currKeys.map(k => {
|
||||
const c = CURRENCIES[k];
|
||||
const label = game.i18n.localize(c.labelKey);
|
||||
const note = c.noteKey ? game.i18n.format(c.noteKey, c.noteParam || {}) : '';
|
||||
const fromPln = k === 'PLN' ? '1.0000' : (1 / c.toPln).toPrecision(4);
|
||||
return `<tr>
|
||||
<td>${label}</td>
|
||||
<td style="text-align:right;font-family:monospace;">${c.toPln === 1 ? '1.0000' : c.toPln.toFixed(4)}</td>
|
||||
<td style="text-align:right;font-family:monospace;">${fromPln}</td>
|
||||
<td style="color:#777;font-size:0.75rem;">${note}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
const rates = Object.fromEntries(currKeys.map(k => [k, CURRENCIES[k].toPln]));
|
||||
|
||||
const liveText = isLive
|
||||
? game.i18n.format('HBM.moneyConverter.liveRates', { date: rateDate })
|
||||
: game.i18n.localize('HBM.moneyConverter.defaultRates');
|
||||
|
||||
const initialEur = isFinite(pln / eurRate) ? (pln / eurRate).toFixed(4) : '';
|
||||
|
||||
const content = `
|
||||
<div class="hbm money-converter" style="padding:10px;font-family:'Signika',sans-serif;display:flex;flex-direction:column;gap:12px;">
|
||||
<p style="margin:0;font-size:0.78rem;color:${isLive ? '#2a7a3e' : '#888'};">
|
||||
<em>${liveText}</em>
|
||||
</p>
|
||||
<div style="background:var(--hbm-card-bg,#1a1a2e);border:1px solid var(--hbm-border,#333);border-radius:6px;padding:8px 12px;">
|
||||
<span style="font-weight:bold;">${game.i18n.localize('HBM.moneyConverter.actorCash')}</span>
|
||||
<span style="font-family:monospace;font-size:1.1rem;margin-left:6px;">${pln} PLN</span>
|
||||
<small style="color:#888;margin-left:6px;">(${game.i18n.format('HBM.moneyConverter.campaignYear', { year: currentYear })})</small>
|
||||
</div>
|
||||
<div style="background:var(--hbm-card-bg,#1a1a2e);border:1px solid var(--hbm-accent,#6060c0);border-radius:6px;padding:10px 12px;display:flex;flex-direction:column;gap:8px;">
|
||||
<strong style="font-size:0.9rem;">${game.i18n.localize('HBM.moneyConverter.calculator')}</strong>
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
|
||||
<input id="hbm-conv-left" type="number" value="${pln}" min="0" step="any"
|
||||
style="width:6.5rem;font-size:1rem;text-align:right;padding:3px 6px;background:transparent;color:var(--hbm-fg,#eee);border:1px solid var(--hbm-border,#555);border-radius:4px;" />
|
||||
<select id="hbm-sf" style="flex:1;min-width:8rem;">
|
||||
${optHtml('PLN')}
|
||||
</select>
|
||||
<span style="font-size:1.2rem;color:var(--hbm-accent,#8080ff);">⇄</span>
|
||||
<input id="hbm-conv-right" type="number" value="${initialEur}" min="0" step="any"
|
||||
style="width:6.5rem;font-size:1rem;text-align:right;padding:3px 6px;background:transparent;color:var(--hbm-fg,#eee);border:1px solid var(--hbm-border,#555);border-radius:4px;" />
|
||||
<select id="hbm-st" style="flex:1;min-width:8rem;">
|
||||
${optHtml('EUR')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<details>
|
||||
<summary style="cursor:pointer;font-size:0.85rem;color:var(--hbm-accent-dim,#999);">▸ ${game.i18n.localize('HBM.moneyConverter.exchangeRatesTable')}</summary>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:0.8rem;margin-top:8px;">
|
||||
<thead><tr style="border-bottom:1px solid var(--hbm-border,#555);">
|
||||
<th style="text-align:left;">${game.i18n.localize('HBM.moneyConverter.currency')}</th>
|
||||
<th style="text-align:right;">${game.i18n.localize('HBM.moneyConverter.ratePln')}</th>
|
||||
<th style="text-align:right;">${game.i18n.localize('HBM.moneyConverter.onePlnEquals')}</th>
|
||||
<th>${game.i18n.localize('HBM.moneyConverter.note')}</th>
|
||||
</tr></thead>
|
||||
<tbody>${ratesRows}</tbody>
|
||||
</table>
|
||||
</details>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const dialog = new MoneyConverterDialog({
|
||||
window: { title: game.i18n.localize('HBM.ui.moneyConverterTitle') },
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: 'ok',
|
||||
label: game.i18n.localize('HBM.ui.confirm') || 'Ok',
|
||||
default: true
|
||||
}
|
||||
],
|
||||
rejectClose: false
|
||||
}, rates);
|
||||
|
||||
dialog.render(true);
|
||||
}
|
||||
+2
-108
@@ -6,6 +6,7 @@ import { applyDamage } from '../logic/damage';
|
||||
import { castSpell } from '../logic/spell-cast';
|
||||
import { HbmTSRoll } from '../dice/ts-roll';
|
||||
import { ATTRIBUTES, AttributeKey, SKILL_KEYS, TS_DEFAULT_REQUIRED, TS_DEFAULT_THRESHOLD, getMagicPowerEntry } from '../constants';
|
||||
import { showMoneyConverter } from './money-converter-dialog';
|
||||
|
||||
const { ActorSheetV2 } = foundry.applications.sheets as unknown as {
|
||||
ActorSheetV2: typeof foundry.applications.sheets.ActorSheetV2;
|
||||
@@ -338,113 +339,6 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
|
||||
static async _onRecalculateMoney(this: NpcSheet, event: PointerEvent, target: HTMLElement) {
|
||||
const actor = (this as unknown as { actor: any }).actor;
|
||||
const pln = actor.system.details?.money ?? 0;
|
||||
const currentYear = actor.system.details?.currentYear ?? 2026;
|
||||
|
||||
let eurRate = 4.35;
|
||||
let usdRate = 4.00;
|
||||
let rateSource = "Domyślne przeliczniki (brak połączenia lub rok poza zakresem API NBP)";
|
||||
let isLive = false;
|
||||
|
||||
const currentRealYear = new Date().getFullYear();
|
||||
|
||||
// Helper function to fetch rate from NBP
|
||||
const fetchNBP = async (currency: string, year: number): Promise<{ rate: number; date: string } | null> => {
|
||||
if (year >= 2002 && year <= currentRealYear) {
|
||||
// Try first 7 days of June to find a working business day (NBP doesn't publish on weekends)
|
||||
for (let day = 1; day <= 7; day++) {
|
||||
const dateString = `${year}-06-${String(day).padStart(2, '0')}`;
|
||||
try {
|
||||
const response = await fetch(`https://api.nbp.pl/api/exchangerates/rates/a/${currency}/${dateString}/?format=json`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const rate = data?.rates?.[0]?.mid;
|
||||
if (typeof rate === 'number') {
|
||||
return { rate, date: dateString };
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore and try next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to latest rate
|
||||
try {
|
||||
const response = await fetch(`https://api.nbp.pl/api/exchangerates/rates/a/${currency}/?format=json`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const rate = data?.rates?.[0]?.mid;
|
||||
const date = data?.rates?.[0]?.effectiveDate || "";
|
||||
if (typeof rate === 'number') {
|
||||
return { rate, date };
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Fetch rates in parallel
|
||||
const [eurData, usdData] = await Promise.all([
|
||||
fetchNBP("eur", currentYear),
|
||||
fetchNBP("usd", currentYear)
|
||||
]);
|
||||
|
||||
if (eurData && usdData) {
|
||||
eurRate = eurData.rate;
|
||||
usdRate = usdData.rate;
|
||||
rateSource = `Pobrane z NBP (EUR z ${eurData.date}, USD z ${usdData.date})`;
|
||||
isLive = true;
|
||||
} else if (eurData) {
|
||||
eurRate = eurData.rate;
|
||||
rateSource = `Częściowo pobrane z NBP (EUR z ${eurData.date})`;
|
||||
isLive = true;
|
||||
} else if (usdData) {
|
||||
usdRate = usdData.rate;
|
||||
rateSource = `Częściowo pobrane z NBP (USD z ${usdData.date})`;
|
||||
isLive = true;
|
||||
}
|
||||
|
||||
// Conversion rates
|
||||
const eur = (pln / eurRate).toFixed(2);
|
||||
const th = (pln / eurRate).toFixed(2); // 1:1 with EUR
|
||||
const usd = (pln / usdRate).toFixed(2);
|
||||
const fc = (pln / 2.0).toFixed(2); // 2 PLN = 1 Credit
|
||||
const st = (pln / 240).toFixed(2);
|
||||
const zk = (pln / 2880).toFixed(2);
|
||||
const pl = (pln / 34560).toFixed(2);
|
||||
|
||||
const content = `
|
||||
<div class="hbm money-converter" style="padding: 10px; font-family: 'Signika', sans-serif;">
|
||||
<p><strong>Bieżąca gotówka:</strong> ${pln} PLN (Rok kampanii: ${currentYear})</p>
|
||||
<p style="font-size: 0.8rem; color: ${isLive ? '#2a7a3e' : '#888'}; margin-top: -5px;">
|
||||
<em>Kursy: ${rateSource}</em><br/>
|
||||
(1 EUR = ${eurRate.toFixed(4)} PLN, 1 USD = ${usdRate.toFixed(4)} PLN)
|
||||
</p>
|
||||
<hr style="border-top: 1px dashed var(--hbm-border, #ccc); margin: 10px 0;" />
|
||||
<h4 style="margin: 5px 0;">Waluty Ziemskie i Międzyświatowe</h4>
|
||||
<ul style="list-style: none; padding: 0; margin: 5px 0; display: flex; flex-direction: column; gap: 4px;">
|
||||
<li><strong>FC (Kredyt Federacji Sol-3):</strong> ${fc} Credits <small style="color: #666;">(Waluta Federacji Sol-3, stały kurs 2 PLN = 1 Credit)</small></li>
|
||||
<li><strong>EUR (Euro):</strong> ${eur} €</li>
|
||||
<li><strong>USD (Dolar):</strong> ${usd} $</li>
|
||||
<li><strong>Thrakka (TH):</strong> ${th} TH <small style="color: #666;">(krasnoludzka waluta rozliczeniowa, 1:1 z EUR)</small></li>
|
||||
</ul>
|
||||
<hr style="border-top: 1px dashed var(--hbm-border, #ccc); margin: 10px 0;" />
|
||||
<h4 style="margin: 5px 0;">Krasnoludzkie Monety Klanowe (System Dwunastkowy)</h4>
|
||||
<ul style="list-style: none; padding: 0; margin: 5px 0; display: flex; flex-direction: column; gap: 4px;">
|
||||
<li><strong>ST (Srebrny Talent):</strong> ${st} ST <small style="color: #666;">(1 ST = 240 PLN / 55 TH)</small></li>
|
||||
<li><strong>ZK (Złota Korona):</strong> ${zk} ZK <small style="color: #666;">(1 ZK = 2880 PLN / 660 TH)</small></li>
|
||||
<li><strong>PL (Platynowy Lingot):</strong> ${pl} PL <small style="color: #666;">(1 PL = 34560 PLN / 7920 TH)</small></li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
await (foundry.applications.api as any).DialogV2.prompt({
|
||||
window: { title: game.i18n.localize('HBM.ui.moneyConverterTitle') },
|
||||
content: content,
|
||||
rejectClose: false,
|
||||
});
|
||||
return showMoneyConverter(actor);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,9 +2,9 @@
|
||||
"id": "hbm-rpg-v3",
|
||||
"title": "Homebrew Magic: RPG v3",
|
||||
"description": "System Foundry VTT dla Homebrew Magic: Role Playing Game v3 - autorska gra fabularna z mechaniką puli kości d6 (TS - Trudność:Sukcesy).",
|
||||
"version": "1.3.30",
|
||||
"version": "1.3.36",
|
||||
"manifest": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3.json",
|
||||
"download": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3-v1.3.30.zip",
|
||||
"download": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3-v1.3.36.zip",
|
||||
"compatibility": {
|
||||
"minimum": "13",
|
||||
"verified": "14"
|
||||
|
||||
Reference in New Issue
Block a user