diff --git a/newtab.html b/newtab.html index c6bc824..48f05d2 100644 --- a/newtab.html +++ b/newtab.html @@ -511,6 +511,7 @@ + diff --git a/src/css/main.css b/src/css/main.css index 4233370..8fd074f 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -1534,6 +1534,47 @@ body.show-desc .bm-desc { display: block; } gap: 4px; } +/* Calculator Stationeers specifics */ +.calc-game-hint { + font-size: 10px; + color: var(--text-muted); + font-style: italic; + margin-top: -4px; + text-align: right; +} +.calc-game-details { + border-top: 1px solid var(--border); + padding-top: 6px; + margin-top: 4px; +} +.calc-game-details summary { + font-size: 10px; + color: var(--text-muted); + cursor: pointer; + text-transform: uppercase; + letter-spacing: 0.5px; +} +.calc-game-table { + width: 100%; + font-size: 11px; + border-collapse: collapse; + margin-top: 4px; +} +.calc-game-table th { + text-align: left; + color: var(--text-muted); + font-weight: 600; + padding: 2px 6px; + border-bottom: 1px solid var(--border); +} +.calc-game-table td { + padding: 2px 6px; + color: var(--text-secondary); +} +.calc-game-table tr:nth-child(even) td { + background: rgba(0,0,0,0.1); +} + /* ============================================ TIMER WIDGET ============================================ */ diff --git a/src/js/calc-stationeers.js b/src/js/calc-stationeers.js new file mode 100644 index 0000000..842b1f5 --- /dev/null +++ b/src/js/calc-stationeers.js @@ -0,0 +1,361 @@ +/* ============================================= + HELLION NEWTAB — calc-stationeers.js + Stationeers Calculator Modus + ============================================= */ + +(function() { + 'use strict'; + + const R = 8314.46261815324; + const COMBUSTION_ENERGY = 563452; + const HEAT_CAP_PURE_FUEL = 61.9; + const HEAT_CAP_DELTA = 172.615; + const BATTERY_CAPACITY = 50000; + + const HEAT_CAPS = [ + { gas: 'O\u2082', cp: 21.1 }, + { gas: 'H\u2082', cp: 20.4 }, + { gas: 'CO\u2082', cp: 28.2 }, + { gas: 'N\u2082', cp: 20.6 }, + { gas: 'H\u2082O', cp: 72.0 }, + { gas: 'N\u2082O', cp: 23.0 }, + { gas: 'Pollutant', cp: 24.8 } + ]; + + const GAS_VARS = ['P', 'V', 'n', 'T']; + const SUB_MODES = ['gas', 'furnace', 'solar', 'atmo']; + let _activeSubMode = 'gas'; + + function createField(labelKey, defaultVal, opts) { + opts = opts || {}; + const row = document.createElement('div'); + row.className = 'calc-game-field'; + const label = document.createElement('label'); + label.textContent = t(labelKey); + const input = document.createElement('input'); + input.type = 'number'; + input.className = 'calc-game-input'; + input.value = defaultVal; + if (opts.step) input.step = opts.step; + if (opts.min !== undefined) input.min = opts.min; + if (opts.max !== undefined) input.max = opts.max; + if (opts.disabled) input.disabled = true; + row.append(label, input); + return { row, input }; + } + + function createOutput(labelKey) { + const row = document.createElement('div'); + row.className = 'calc-game-output'; + const label = document.createElement('span'); + label.textContent = t(labelKey); + const value = document.createElement('span'); + value.className = 'calc-game-value'; + row.append(label, value); + return { row, value }; + } + + function renderGas(container) { + const solveRow = document.createElement('div'); + solveRow.className = 'calc-game-field'; + const solveLabel = document.createElement('label'); + solveLabel.textContent = t('calculator.sta.solve_for'); + const solveSelect = document.createElement('select'); + solveSelect.className = 'calc-game-input'; + + GAS_VARS.forEach(v => { + const opt = document.createElement('option'); + opt.value = v; + opt.textContent = t('calculator.sta.var.' + v); + solveSelect.appendChild(opt); + }); + + solveRow.append(solveLabel, solveSelect); + container.appendChild(solveRow); + + const fields = {}; + const defaults = { P: 101.325, V: 1000, n: 1, T: 293.15 }; + + GAS_VARS.forEach(v => { + const f = createField( + 'calculator.sta.var.' + v + '_label', + defaults[v], + { step: 'any' } + ); + fields[v] = f; + container.appendChild(f.row); + }); + + const tempHelper = document.createElement('div'); + tempHelper.className = 'calc-game-hint'; + container.appendChild(tempHelper); + + const resultOutput = createOutput('calculator.sta.result'); + container.appendChild(resultOutput.row); + + function calc() { + const solveFor = solveSelect.value; + + GAS_VARS.forEach(v => { + fields[v].input.disabled = (v === solveFor); + fields[v].input.style.opacity = (v === solveFor) ? '0.5' : '1'; + }); + + const P_kPa = parseFloat(fields.P.input.value) || 0; + const P = P_kPa * 1000; + const V = parseFloat(fields.V.input.value) || 0; + const n = parseFloat(fields.n.input.value) || 0; + const T = parseFloat(fields.T.input.value) || 0; + + let result = null; + let unit = ''; + + switch (solveFor) { + case 'P': + if (V > 0) { result = (n * R * T) / V; result /= 1000; unit = 'kPa'; } + break; + case 'V': + if (P > 0) { result = (n * R * T) / P; unit = 'L'; } + break; + case 'n': + if (R * T > 0) { result = (P * V) / (R * T); unit = 'mol'; } + break; + case 'T': + if (n * R > 0) { result = (P * V) / (n * R); unit = 'K'; } + break; + } + + if (result !== null && isFinite(result)) { + fields[solveFor].input.value = Calculator._formatResult(result); + resultOutput.value.textContent = Calculator._formatResult(result) + ' ' + unit; + } else { + resultOutput.value.textContent = '-'; + } + + const tempVal = parseFloat(fields.T.input.value) || 0; + tempHelper.textContent = Calculator._formatResult(tempVal - 273.15) + ' \u00B0C'; + } + + GAS_VARS.forEach(v => { + fields[v].input.addEventListener('input', calc); + }); + solveSelect.addEventListener('change', calc); + calc(); + } + + function renderFurnace(container) { + const fuelField = createField('calculator.sta.fuel_ratio', 0.5, { step: 0.01, min: 0, max: 1 }); + const tempField = createField('calculator.sta.start_temp', 293.15, { step: 1, min: 0 }); + const pressField = createField('calculator.sta.start_pressure', 101.325, { step: 0.1, min: 0 }); + + const tempOutput = createOutput('calculator.sta.temp_after'); + const pressOutput = createOutput('calculator.sta.pressure_after'); + const warningEl = document.createElement('div'); + warningEl.className = 'calc-game-warning'; + + function calc() { + const fuel = parseFloat(fuelField.input.value) || 0; + const T_vor = parseFloat(tempField.input.value) || 293.15; + const P_vor = parseFloat(pressField.input.value) || 101.325; + + warningEl.textContent = ''; + if (fuel < 0.05) { + warningEl.textContent = t('calculator.sta.warn_low_fuel'); + } + if (P_vor < 10) { + warningEl.textContent += (warningEl.textContent ? ' ' : '') + t('calculator.sta.warn_low_pressure'); + } + + const specificHeat = HEAT_CAP_PURE_FUEL; + const T_nach = (T_vor * specificHeat + fuel * COMBUSTION_ENERGY) / (specificHeat + fuel * HEAT_CAP_DELTA); + const P_nach = P_vor * T_nach * (1 + 5.7 * fuel) / T_vor; + + tempOutput.value.textContent = Calculator._formatResult(T_nach) + ' K (' + Calculator._formatResult(T_nach - 273.15) + ' \u00B0C)'; + pressOutput.value.textContent = Calculator._formatResult(P_nach) + ' kPa'; + } + + [fuelField, tempField, pressField].forEach(f => f.input.addEventListener('input', calc)); + + container.append(fuelField.row, tempField.row, pressField.row, warningEl, tempOutput.row, pressOutput.row); + calc(); + } + + function renderSolar(container) { + const panelField = createField('calculator.sta.panels', 12, { step: 1, min: 1 }); + const wattField = createField('calculator.sta.watts_per_panel', 500, { step: 10, min: 1 }); + const dayField = createField('calculator.sta.day_length', 600, { step: 1, min: 1 }); + const nightField = createField('calculator.sta.night_length', 600, { step: 1, min: 1 }); + const consumeField = createField('calculator.sta.consumption', 2000, { step: 10, min: 0 }); + + const genOutput = createOutput('calculator.sta.generation'); + const surplusOutput = createOutput('calculator.sta.surplus'); + const nightOutput = createOutput('calculator.sta.night_energy'); + const battOutput = createOutput('calculator.sta.batteries_needed'); + + function calc() { + const panels = parseFloat(panelField.input.value) || 0; + const wpp = parseFloat(wattField.input.value) || 0; + const nightLen = parseFloat(nightField.input.value) || 0; + const consume = parseFloat(consumeField.input.value) || 0; + + const generation = panels * wpp; + const surplus = generation - consume; + const nightEnergy = consume * nightLen; + const batteries = nightEnergy > 0 ? Math.ceil(nightEnergy / BATTERY_CAPACITY) : 0; + + genOutput.value.textContent = Calculator._formatResult(generation) + ' W'; + + surplusOutput.value.textContent = Calculator._formatResult(surplus) + ' W'; + if (surplus < 0) { + surplusOutput.value.style.color = 'var(--danger)'; + } else { + surplusOutput.value.style.color = ''; + } + + nightOutput.value.textContent = Calculator._formatResult(nightEnergy) + ' Ws'; + battOutput.value.textContent = batteries; + } + + [panelField, wattField, dayField, nightField, consumeField].forEach(f => f.input.addEventListener('input', calc)); + + container.append(panelField.row, wattField.row, dayField.row, nightField.row, consumeField.row, + genOutput.row, surplusOutput.row, nightOutput.row, battOutput.row); + calc(); + } + + function renderAtmo(container) { + const targetField = createField('calculator.sta.target_temp', 293.15, { step: 1 }); + const gas1Field = createField('calculator.sta.gas1_temp', 200, { step: 1 }); + const gas2Field = createField('calculator.sta.gas2_temp', 400, { step: 1 }); + + const m1Output = createOutput('calculator.sta.mixer_input1'); + const m2Output = createOutput('calculator.sta.mixer_input2'); + + function calc() { + const T0 = parseFloat(targetField.input.value) || 0; + const T1 = parseFloat(gas1Field.input.value) || 0; + const T2 = parseFloat(gas2Field.input.value) || 0; + + const denom = Math.abs(T1 - T0) + Math.abs(T2 - T0); + if (denom === 0) { + m1Output.value.textContent = '50%'; + m2Output.value.textContent = '50%'; + return; + } + + const M1 = Math.abs(T2 - T0) / denom; + const M2 = 1 - M1; + + m1Output.value.textContent = Calculator._formatResult(M1 * 100) + '%'; + m2Output.value.textContent = Calculator._formatResult(M2 * 100) + '%'; + } + + [targetField, gas1Field, gas2Field].forEach(f => f.input.addEventListener('input', calc)); + + container.append(targetField.row, gas1Field.row, gas2Field.row, m1Output.row, m2Output.row); + calc(); + + const details = document.createElement('details'); + details.className = 'calc-game-details'; + + const summary = document.createElement('summary'); + summary.textContent = t('calculator.sta.heat_cap_ref'); + details.appendChild(summary); + + const table = document.createElement('table'); + table.className = 'calc-game-table'; + + const thead = document.createElement('thead'); + const headerRow = document.createElement('tr'); + const thGas = document.createElement('th'); + thGas.textContent = t('calculator.sta.gas'); + const thCp = document.createElement('th'); + thCp.textContent = 'Cp (J/mol\u00B7K)'; + headerRow.append(thGas, thCp); + thead.appendChild(headerRow); + + const tbody = document.createElement('tbody'); + HEAT_CAPS.forEach(entry => { + const tr = document.createElement('tr'); + const tdGas = document.createElement('td'); + tdGas.textContent = entry.gas; + const tdCp = document.createElement('td'); + tdCp.textContent = entry.cp; + tr.append(tdGas, tdCp); + tbody.appendChild(tr); + }); + + table.append(thead, tbody); + details.appendChild(table); + container.appendChild(details); + } + + async function loadState() { + const data = await Store.get(Calculator.STORAGE_KEY); + if (data && data.calculator && data.calculator.stationeers) { + const s = data.calculator.stationeers; + if (s.lastSubMode && SUB_MODES.includes(s.lastSubMode)) _activeSubMode = s.lastSubMode; + } + } + + async function saveState() { + const data = await Store.get(Calculator.STORAGE_KEY) || {}; + if (!data.calculator) data.calculator = {}; + data.calculator.stationeers = { lastSubMode: _activeSubMode }; + await Store.set(Calculator.STORAGE_KEY, data); + } + + function renderSubMode(container) { + container.textContent = ''; + switch (_activeSubMode) { + case 'gas': renderGas(container); break; + case 'furnace': renderFurnace(container); break; + case 'solar': renderSolar(container); break; + case 'atmo': renderAtmo(container); break; + } + } + + Calculator.registerMode('stationeers', { + label: '\uD83D\uDE80', + shortName: 'STA', + titleKey: 'calculator.tab.stationeers', + + render(bodyEl) { + bodyEl.style.padding = '8px'; + bodyEl.style.display = 'flex'; + bodyEl.style.flexDirection = 'column'; + bodyEl.style.gap = '8px'; + + loadState().then(() => { + const subContent = document.createElement('div'); + subContent.className = 'calc-game-content'; + + const bar = document.createElement('div'); + bar.className = 'calc-game-subtabs'; + + SUB_MODES.forEach(mode => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'calc-game-subtab' + (mode === _activeSubMode ? ' active' : ''); + btn.textContent = t('calculator.sta.tab.' + mode); + btn.dataset.mode = mode; + btn.addEventListener('click', () => { + bar.querySelectorAll('.calc-game-subtab').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + _activeSubMode = mode; + renderSubMode(subContent); + saveState(); + }); + bar.appendChild(btn); + }); + + bodyEl.append(bar, subContent); + renderSubMode(subContent); + }); + }, + + destroy() { + saveState(); + } + }); +})(); diff --git a/src/js/i18n.js b/src/js/i18n.js index ca0722d..6a213a0 100644 --- a/src/js/i18n.js +++ b/src/js/i18n.js @@ -148,6 +148,46 @@ const STRINGS = { 'calculator.fac.belt_needed': 'Belt benötigt', 'calculator.fac.exceeds_belt': 'Übersteigt max. Belt', + // Stationeers Calculator + 'calculator.tab.stationeers': 'Stationeers', + 'calculator.sta.tab.gas': 'Gas', + 'calculator.sta.tab.furnace': 'Ofen', + 'calculator.sta.tab.solar': 'Solar', + 'calculator.sta.tab.atmo': 'Atmo', + 'calculator.sta.solve_for': 'Gesucht', + 'calculator.sta.var.P': 'Druck (P)', + 'calculator.sta.var.V': 'Volumen (V)', + 'calculator.sta.var.n': 'Stoffmenge (n)', + 'calculator.sta.var.T': 'Temperatur (T)', + 'calculator.sta.var.P_label': 'Druck (kPa)', + 'calculator.sta.var.V_label': 'Volumen (L)', + 'calculator.sta.var.n_label': 'Stoffmenge (mol)', + 'calculator.sta.var.T_label': 'Temperatur (K)', + 'calculator.sta.result': 'Ergebnis', + 'calculator.sta.fuel_ratio': 'Fuel-Anteil (0-1)', + 'calculator.sta.start_temp': 'Start-Temperatur (K)', + 'calculator.sta.start_pressure': 'Start-Druck (kPa)', + 'calculator.sta.temp_after': 'T nach Zündung', + 'calculator.sta.pressure_after': 'P nach Zündung', + 'calculator.sta.warn_low_fuel': '\u26A0 Fuel unter 5%', + 'calculator.sta.warn_low_pressure': '\u26A0 Druck unter 10 kPa', + 'calculator.sta.panels': 'Anzahl Panels', + 'calculator.sta.watts_per_panel': 'Watt/Panel', + 'calculator.sta.day_length': 'Taglänge (s)', + 'calculator.sta.night_length': 'Nachtlänge (s)', + 'calculator.sta.consumption': 'Verbrauch (W)', + 'calculator.sta.generation': 'Erzeugung', + 'calculator.sta.surplus': 'Überschuss', + 'calculator.sta.night_energy': 'Nacht-Energie', + 'calculator.sta.batteries_needed': 'Batterien benötigt', + 'calculator.sta.target_temp': 'Ziel-Temperatur (K)', + 'calculator.sta.gas1_temp': 'Gas 1 Temperatur (K)', + 'calculator.sta.gas2_temp': 'Gas 2 Temperatur (K)', + 'calculator.sta.mixer_input1': 'Mixer Input 1', + 'calculator.sta.mixer_input2': 'Mixer Input 2', + 'calculator.sta.heat_cap_ref': 'Wärmekapazitäten (Referenz)', + 'calculator.sta.gas': 'Gas', + // Timer 'timer.title': 'Timer', 'timer.start': 'Start', @@ -520,6 +560,46 @@ const STRINGS = { 'calculator.fac.belt_needed': 'Belt needed', 'calculator.fac.exceeds_belt': 'Exceeds max belt', + // Stationeers Calculator + 'calculator.tab.stationeers': 'Stationeers', + 'calculator.sta.tab.gas': 'Gas', + 'calculator.sta.tab.furnace': 'Furnace', + 'calculator.sta.tab.solar': 'Solar', + 'calculator.sta.tab.atmo': 'Atmo', + 'calculator.sta.solve_for': 'Solve for', + 'calculator.sta.var.P': 'Pressure (P)', + 'calculator.sta.var.V': 'Volume (V)', + 'calculator.sta.var.n': 'Amount (n)', + 'calculator.sta.var.T': 'Temperature (T)', + 'calculator.sta.var.P_label': 'Pressure (kPa)', + 'calculator.sta.var.V_label': 'Volume (L)', + 'calculator.sta.var.n_label': 'Amount (mol)', + 'calculator.sta.var.T_label': 'Temperature (K)', + 'calculator.sta.result': 'Result', + 'calculator.sta.fuel_ratio': 'Fuel Ratio (0-1)', + 'calculator.sta.start_temp': 'Start Temperature (K)', + 'calculator.sta.start_pressure': 'Start Pressure (kPa)', + 'calculator.sta.temp_after': 'T after ignition', + 'calculator.sta.pressure_after': 'P after ignition', + 'calculator.sta.warn_low_fuel': '\u26A0 Fuel below 5%', + 'calculator.sta.warn_low_pressure': '\u26A0 Pressure below 10 kPa', + 'calculator.sta.panels': 'Panel Count', + 'calculator.sta.watts_per_panel': 'Watts/Panel', + 'calculator.sta.day_length': 'Day Length (s)', + 'calculator.sta.night_length': 'Night Length (s)', + 'calculator.sta.consumption': 'Consumption (W)', + 'calculator.sta.generation': 'Generation', + 'calculator.sta.surplus': 'Surplus', + 'calculator.sta.night_energy': 'Night Energy', + 'calculator.sta.batteries_needed': 'Batteries needed', + 'calculator.sta.target_temp': 'Target Temperature (K)', + 'calculator.sta.gas1_temp': 'Gas 1 Temperature (K)', + 'calculator.sta.gas2_temp': 'Gas 2 Temperature (K)', + 'calculator.sta.mixer_input1': 'Mixer Input 1', + 'calculator.sta.mixer_input2': 'Mixer Input 2', + 'calculator.sta.heat_cap_ref': 'Heat Capacities (Reference)', + 'calculator.sta.gas': 'Gas', + // Timer 'timer.title': 'Timer', 'timer.start': 'Start',