From 0fe9dd02d663442b2d73b93ab6457d54a6711a5c Mon Sep 17 00:00:00 2001 From: Octoturge Date: Mon, 17 Aug 2026 21:01:56 +0200 Subject: [PATCH] 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). --- lang/en.json | 31 +++++ lang/pl.json | 31 +++++ package.json | 2 +- src/sheets/character-sheet.ts | 97 +------------- src/sheets/money-converter-dialog.ts | 185 +++++++++++++++++++++++++++ src/sheets/npc-sheet.ts | 110 +--------------- system.json | 4 +- 7 files changed, 254 insertions(+), 206 deletions(-) create mode 100644 src/sheets/money-converter-dialog.ts diff --git a/lang/en.json b/lang/en.json index a30ce3d..53642ae 100644 --- a/lang/en.json +++ b/lang/en.json @@ -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" + } } } } \ No newline at end of file diff --git a/lang/pl.json b/lang/pl.json index 8a80eae..641ca1d 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -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" + } } } } \ No newline at end of file diff --git a/package.json b/package.json index 9ae336c..9ec6059 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/sheets/character-sheet.ts b/src/sheets/character-sheet.ts index 58f10e9..a82776f 100644 --- a/src/sheets/character-sheet.ts +++ b/src/sheets/character-sheet.ts @@ -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 = { - 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 => - `` - ).join(''); - const ratesRows = currKeys.map(k => { - const c = CURRENCIES[k]; - const fromPln = k === 'PLN' ? '1.0000' : (1 / c.toPln).toPrecision(4); - return `${c.label}${c.toPln === 1 ? '1.0000' : c.toPln.toFixed(4)}${fromPln}${c.note}`; - }).join(''); - const ratesJson = JSON.stringify(Object.fromEntries(currKeys.map(k => [k, CURRENCIES[k].toPln]))); - - const content = ` -
-

- 📡 ${isLive ? `Kursy z NBP (${rateDate})` : 'Domyślne kursy (brak połączenia z NBP)'} -

-
- Gotówka aktora: - ${pln} PLN - (Rok kampanii: ${currentYear}) -
-
- Kalkulator -
- - - - - -
-
-
- ▸ Tabela kursów wymiany - - - - - ${ratesRows} -
WalutaKurs (PLN)1 PLN =Uwaga
-
-
- `; - - await (foundry.applications.api as any).DialogV2.prompt({ - window: { title: game.i18n.localize('HBM.ui.moneyConverterTitle') }, - content, - rejectClose: false, - }); + return showMoneyConverter(actor); } } diff --git a/src/sheets/money-converter-dialog.ts b/src/sheets/money-converter-dialog.ts new file mode 100644 index 0000000..ad083e1 --- /dev/null +++ b/src/sheets/money-converter-dialog.ts @@ -0,0 +1,185 @@ +const { DialogV2 } = foundry.applications.api as unknown as { + DialogV2: any; +}; + +class MoneyConverterDialog extends DialogV2 { + rates: Record; + + constructor(options: any, rates: Record) { + 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 { + 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 }> = { + 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 ``; + }).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 ` + ${label} + ${c.toPln === 1 ? '1.0000' : c.toPln.toFixed(4)} + ${fromPln} + ${note} + `; + }).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 = ` +
+

+ ${liveText} +

+
+ ${game.i18n.localize('HBM.moneyConverter.actorCash')} + ${pln} PLN + (${game.i18n.format('HBM.moneyConverter.campaignYear', { year: currentYear })}) +
+
+ ${game.i18n.localize('HBM.moneyConverter.calculator')} +
+ + + + + +
+
+
+ ▸ ${game.i18n.localize('HBM.moneyConverter.exchangeRatesTable')} + + + + + + + + ${ratesRows} +
${game.i18n.localize('HBM.moneyConverter.currency')}${game.i18n.localize('HBM.moneyConverter.ratePln')}${game.i18n.localize('HBM.moneyConverter.onePlnEquals')}${game.i18n.localize('HBM.moneyConverter.note')}
+
+
+ `; + + 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); +} diff --git a/src/sheets/npc-sheet.ts b/src/sheets/npc-sheet.ts index 31d17c2..a3a26f9 100644 --- a/src/sheets/npc-sheet.ts +++ b/src/sheets/npc-sheet.ts @@ -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 = ` -
-

Bieżąca gotówka: ${pln} PLN (Rok kampanii: ${currentYear})

-

- Kursy: ${rateSource}
- (1 EUR = ${eurRate.toFixed(4)} PLN, 1 USD = ${usdRate.toFixed(4)} PLN) -

-
-

Waluty Ziemskie i Międzyświatowe

-
    -
  • FC (Kredyt Federacji Sol-3): ${fc} Credits (Waluta Federacji Sol-3, stały kurs 2 PLN = 1 Credit)
  • -
  • EUR (Euro): ${eur} €
  • -
  • USD (Dolar): ${usd} $
  • -
  • Thrakka (TH): ${th} TH (krasnoludzka waluta rozliczeniowa, 1:1 z EUR)
  • -
-
-

Krasnoludzkie Monety Klanowe (System Dwunastkowy)

-
    -
  • ST (Srebrny Talent): ${st} ST (1 ST = 240 PLN / 55 TH)
  • -
  • ZK (Złota Korona): ${zk} ZK (1 ZK = 2880 PLN / 660 TH)
  • -
  • PL (Platynowy Lingot): ${pl} PL (1 PL = 34560 PLN / 7920 TH)
  • -
-
- `; - - await (foundry.applications.api as any).DialogV2.prompt({ - window: { title: game.i18n.localize('HBM.ui.moneyConverterTitle') }, - content: content, - rejectClose: false, - }); + return showMoneyConverter(actor); } } diff --git a/system.json b/system.json index 722acee..cb773c6 100644 --- a/system.json +++ b/system.json @@ -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"