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)",
|
"zakazana-wiedza-dziedzina": "Forbidden Knowledge (Discipline)",
|
||||||
"zawisanie-w-powietrzu": "Hovering",
|
"zawisanie-w-powietrzu": "Hovering",
|
||||||
"naturalny-lot": "Natural Flight"
|
"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)",
|
"zakazana-wiedza-dziedzina": "Zakazana wiedza (Dziedzina)",
|
||||||
"zawisanie-w-powietrzu": "Zawisanie w powietrzu",
|
"zawisanie-w-powietrzu": "Zawisanie w powietrzu",
|
||||||
"naturalny-lot": "Naturalny lot"
|
"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",
|
"name": "hbm-rpg-v3-foundry",
|
||||||
"version": "1.3.30",
|
"version": "1.3.36",
|
||||||
"description": "Foundry VTT system for Homebrew Magic: Role Playing Game v3",
|
"description": "Foundry VTT system for Homebrew Magic: Role Playing Game v3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { askApplyDamage } from '../dice/damage-dialog';
|
|||||||
import { applyDamage } from '../logic/damage';
|
import { applyDamage } from '../logic/damage';
|
||||||
import { rest } from '../logic/rest';
|
import { rest } from '../logic/rest';
|
||||||
import { ATTRIBUTES, AttributeKey, SKILL_KEYS, getMagicPowerEntry } from '../constants';
|
import { ATTRIBUTES, AttributeKey, SKILL_KEYS, getMagicPowerEntry } from '../constants';
|
||||||
|
import { showMoneyConverter } from './money-converter-dialog';
|
||||||
|
|
||||||
const { ActorSheetV2 } = foundry.applications.sheets as unknown as {
|
const { ActorSheetV2 } = foundry.applications.sheets as unknown as {
|
||||||
ActorSheetV2: typeof foundry.applications.sheets.ActorSheetV2;
|
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) {
|
static async _onRecalculateMoney(this: CharacterSheet, _event: PointerEvent, _target: HTMLElement) {
|
||||||
const actor = (this as unknown as { actor: any }).actor;
|
const actor = (this as unknown as { actor: any }).actor;
|
||||||
const pln = actor.system.details?.money ?? 0;
|
return showMoneyConverter(actor);
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { castSpell } from '../logic/spell-cast';
|
||||||
import { HbmTSRoll } from '../dice/ts-roll';
|
import { HbmTSRoll } from '../dice/ts-roll';
|
||||||
import { ATTRIBUTES, AttributeKey, SKILL_KEYS, TS_DEFAULT_REQUIRED, TS_DEFAULT_THRESHOLD, getMagicPowerEntry } from '../constants';
|
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 {
|
const { ActorSheetV2 } = foundry.applications.sheets as unknown as {
|
||||||
ActorSheetV2: typeof foundry.applications.sheets.ActorSheetV2;
|
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) {
|
static async _onRecalculateMoney(this: NpcSheet, event: PointerEvent, target: HTMLElement) {
|
||||||
const actor = (this as unknown as { actor: any }).actor;
|
const actor = (this as unknown as { actor: any }).actor;
|
||||||
const pln = actor.system.details?.money ?? 0;
|
return showMoneyConverter(actor);
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -2,9 +2,9 @@
|
|||||||
"id": "hbm-rpg-v3",
|
"id": "hbm-rpg-v3",
|
||||||
"title": "Homebrew Magic: 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).",
|
"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",
|
"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": {
|
"compatibility": {
|
||||||
"minimum": "13",
|
"minimum": "13",
|
||||||
"verified": "14"
|
"verified": "14"
|
||||||
|
|||||||
Reference in New Issue
Block a user