7be391de99
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3069 lines
94 KiB
Markdown
3069 lines
94 KiB
Markdown
# Calculator Upgrade v2.1.0 — Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Extend the Calculator widget with 5 new modes — Scientific, Unit Converter, Satisfactory, Factorio, Stationeers — using a tab-based architecture with mode registration pattern.
|
||
|
||
**Architecture:** Each mode lives in its own IIFE file that calls `Calculator.registerMode()`. The core `calculator.js` gains a `_modes` Map, tab-bar rendering, and `switchMode()` logic. The Shunting-Yard parser is extended with `^` (power) and `sqrt()` (function). All new files load between `calculator.js` and `timer.js` in newtab.html.
|
||
|
||
**Tech Stack:** Vanilla JS ES2020, CSS Custom Properties, chrome.storage.local via Store, i18n via t() helper
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-04-16-calculator-upgrade-design.md`
|
||
|
||
---
|
||
|
||
### Task 1: Tab-System im Calculator Core
|
||
|
||
**Files:**
|
||
- Modify: `src/js/calculator.js` (全 Datei — Tab-System, registerMode, switchMode, Standard-Mode-Extraktion)
|
||
- Modify: `src/css/main.css:1284` (neue CSS-Klassen nach bestehenden Calculator-Styles)
|
||
|
||
**Kontext:** Aktuell rendert `Calculator.renderBody()` direkt das Standard-UI (Display + Buttons + History). Wir refactoren das zu einem Tab-System: `renderBody()` baut Tab-Bar + Mode-Container, dann delegiert an den aktiven Modus. Der Standard-Modus wird als interner Modus registriert. Externe Modi registrieren sich per `Calculator.registerMode()`.
|
||
|
||
- [ ] **Step 1: Neue Properties und registerMode() hinzufügen**
|
||
|
||
In `calculator.js`, nach `_keydownHandler: null,` (Zeile 19) folgende Properties ergänzen:
|
||
|
||
```javascript
|
||
_modes: new Map(),
|
||
_activeMode: 'standard',
|
||
_tabBarEl: null,
|
||
```
|
||
|
||
Neue Methode nach `_keydownHandler` Block:
|
||
|
||
```javascript
|
||
/**
|
||
* Modus registrieren (wird von externen Mode-Dateien aufgerufen)
|
||
* @param {string} name - Eindeutiger Modus-Name
|
||
* @param {Object} config - { label, shortName, titleKey, render(bodyEl), destroy() }
|
||
*/
|
||
registerMode(name, config) {
|
||
this._modes.set(name, config);
|
||
// Tab-Bar aktualisieren falls Widget bereits offen
|
||
if (this._tabBarEl) this._renderTabBar();
|
||
},
|
||
```
|
||
|
||
- [ ] **Step 2: Standard-Modus als internen Modus registrieren**
|
||
|
||
Am Ende der `init()` Methode (vor dem Schließen des Objekts), nach den Event-Listener-Registrierungen, den Standard-Modus registrieren:
|
||
|
||
```javascript
|
||
// Standard-Modus intern registrieren
|
||
this._modes.set('standard', {
|
||
label: '🔢',
|
||
shortName: 'Std',
|
||
titleKey: 'calculator.tab.standard',
|
||
render: (bodyEl) => this._renderStandardMode(bodyEl),
|
||
destroy: () => {
|
||
// Standard-Modus hat keinen speziellen Cleanup
|
||
this._displayExprEl = null;
|
||
this._displayResultEl = null;
|
||
}
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 3: renderBody() refactoren — Tab-Bar + Mode-Container**
|
||
|
||
Die bestehende `renderBody()` Methode komplett ersetzen:
|
||
|
||
```javascript
|
||
/**
|
||
* Calculator-Body rendern: Tab-Bar + aktiver Modus
|
||
* @param {HTMLElement} bodyEl
|
||
*/
|
||
renderBody(bodyEl) {
|
||
bodyEl.textContent = '';
|
||
bodyEl.style.padding = '0';
|
||
bodyEl.style.display = 'flex';
|
||
bodyEl.style.flexDirection = 'column';
|
||
bodyEl.style.height = '100%';
|
||
|
||
// Tab-Bar
|
||
const tabBar = document.createElement('div');
|
||
tabBar.className = 'calc-tab-bar';
|
||
this._tabBarEl = tabBar;
|
||
this._renderTabBar();
|
||
|
||
// Mode-Body Container
|
||
const modeBody = document.createElement('div');
|
||
modeBody.className = 'calc-mode-body';
|
||
|
||
bodyEl.append(tabBar, modeBody);
|
||
|
||
// Aktiven Modus rendern
|
||
const mode = this._modes.get(this._activeMode);
|
||
if (mode) {
|
||
mode.render(modeBody);
|
||
}
|
||
},
|
||
```
|
||
|
||
- [ ] **Step 4: _renderTabBar() und _updateTabBar() erstellen**
|
||
|
||
```javascript
|
||
/**
|
||
* Tab-Bar mit Buttons aus _modes Map befüllen
|
||
*/
|
||
_renderTabBar() {
|
||
if (!this._tabBarEl) return;
|
||
// Alle bisherigen Tabs entfernen
|
||
while (this._tabBarEl.firstChild) {
|
||
this._tabBarEl.removeChild(this._tabBarEl.firstChild);
|
||
}
|
||
|
||
this._modes.forEach((config, name) => {
|
||
const tab = document.createElement('button');
|
||
tab.type = 'button';
|
||
tab.className = 'calc-tab' + (name === this._activeMode ? ' active' : '');
|
||
tab.dataset.mode = name;
|
||
|
||
const icon = document.createElement('span');
|
||
icon.className = 'calc-tab-icon';
|
||
icon.textContent = config.label;
|
||
|
||
const label = document.createElement('span');
|
||
label.className = 'calc-tab-label';
|
||
label.textContent = config.shortName;
|
||
|
||
tab.append(icon, label);
|
||
tab.addEventListener('click', () => this.switchMode(name));
|
||
this._tabBarEl.appendChild(tab);
|
||
});
|
||
},
|
||
|
||
/**
|
||
* Aktiven Tab visuell markieren (ohne Neuaufbau)
|
||
*/
|
||
_updateTabBar() {
|
||
if (!this._tabBarEl) return;
|
||
const tabs = this._tabBarEl.querySelectorAll('.calc-tab');
|
||
tabs.forEach(tab => {
|
||
tab.classList.toggle('active', tab.dataset.mode === this._activeMode);
|
||
});
|
||
},
|
||
```
|
||
|
||
- [ ] **Step 5: switchMode() erstellen**
|
||
|
||
```javascript
|
||
/**
|
||
* Modus wechseln
|
||
* @param {string} name - Ziel-Modus
|
||
*/
|
||
async switchMode(name) {
|
||
if (name === this._activeMode) return;
|
||
const mode = this._modes.get(name);
|
||
if (!mode) return;
|
||
|
||
// Alten Modus aufräumen
|
||
const oldMode = this._modes.get(this._activeMode);
|
||
if (oldMode && oldMode.destroy) oldMode.destroy();
|
||
|
||
this._activeMode = name;
|
||
|
||
// Mode-Body leeren und neu rendern
|
||
const entry = WidgetManager._widgets.get(this.WIDGET_ID);
|
||
if (!entry) return;
|
||
const modeBody = entry.el.querySelector('.calc-mode-body');
|
||
if (!modeBody) return;
|
||
modeBody.textContent = '';
|
||
mode.render(modeBody);
|
||
|
||
// Tab-UI aktualisieren
|
||
this._updateTabBar();
|
||
|
||
// Auto-Resize für komplexe Modi
|
||
const isComplex = name !== 'standard';
|
||
if (isComplex) {
|
||
const state = WidgetManager.getState(this.WIDGET_ID);
|
||
if (state) {
|
||
const newW = Math.max(state.width, 320);
|
||
const newH = Math.max(state.height, 480);
|
||
if (newW !== state.width || newH !== state.height) {
|
||
WidgetManager.resize(this.WIDGET_ID, newW, newH);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Keyboard neu binden
|
||
this._unbindKeyboard();
|
||
if (name === 'standard' || name === 'scientific') {
|
||
if (entry) this._bindKeyboard(entry.el);
|
||
}
|
||
|
||
await this.save();
|
||
},
|
||
```
|
||
|
||
- [ ] **Step 6: _renderStandardMode() extrahieren**
|
||
|
||
Neue Methode, die den bisherigen `renderBody()` Inhalt enthält (Display + Buttons + History):
|
||
|
||
```javascript
|
||
/**
|
||
* Standard-Modus UI rendern
|
||
* @param {HTMLElement} bodyEl
|
||
*/
|
||
_renderStandardMode(bodyEl) {
|
||
bodyEl.style.padding = '8px';
|
||
bodyEl.style.display = 'flex';
|
||
bodyEl.style.flexDirection = 'column';
|
||
bodyEl.style.flex = '1';
|
||
bodyEl.style.overflow = 'hidden';
|
||
|
||
// Display
|
||
const display = document.createElement('div');
|
||
display.className = 'calc-display';
|
||
|
||
const exprEl = document.createElement('div');
|
||
exprEl.className = 'calc-expression';
|
||
this._displayExprEl = exprEl;
|
||
|
||
const resultEl = document.createElement('div');
|
||
resultEl.className = 'calc-result';
|
||
resultEl.textContent = '0';
|
||
this._displayResultEl = resultEl;
|
||
|
||
display.append(exprEl, resultEl);
|
||
|
||
// Buttons
|
||
const buttonsEl = this._createButtons();
|
||
|
||
// History
|
||
const historyEl = this._createHistoryPanel();
|
||
|
||
bodyEl.append(display, buttonsEl, historyEl);
|
||
|
||
// Aktuellen State anzeigen
|
||
this._updateDisplay();
|
||
},
|
||
```
|
||
|
||
- [ ] **Step 7: save() und load() um activeMode erweitern**
|
||
|
||
In `save()`, `calcData` um `activeMode` erweitern:
|
||
|
||
```javascript
|
||
const calcData = {
|
||
x: widgetState ? widgetState.x : 400,
|
||
y: widgetState ? widgetState.y : 120,
|
||
width: widgetState ? widgetState.width : 280,
|
||
height: widgetState ? widgetState.height : 400,
|
||
open: this._isOpen,
|
||
activeMode: this._activeMode,
|
||
history: this._history.slice(0, this.MAX_HISTORY)
|
||
};
|
||
```
|
||
|
||
In `load()`, activeMode wiederherstellen:
|
||
|
||
```javascript
|
||
async load() {
|
||
const data = await Store.get(this.STORAGE_KEY);
|
||
if (data && data.calculator) {
|
||
this._history = Array.isArray(data.calculator.history) ? data.calculator.history : [];
|
||
if (data.calculator.activeMode) {
|
||
this._activeMode = data.calculator.activeMode;
|
||
}
|
||
}
|
||
},
|
||
```
|
||
|
||
In `onClose()`, alten Modus aufräumen:
|
||
|
||
```javascript
|
||
async onClose() {
|
||
// Aktiven Modus aufräumen
|
||
const mode = this._modes.get(this._activeMode);
|
||
if (mode && mode.destroy) mode.destroy();
|
||
|
||
this._isOpen = false;
|
||
this._unbindKeyboard();
|
||
this._tabBarEl = null;
|
||
this._displayExprEl = null;
|
||
this._displayResultEl = null;
|
||
await this.save();
|
||
},
|
||
```
|
||
|
||
- [ ] **Step 8: CSS für Tab-Bar und Mode-Body**
|
||
|
||
In `src/css/main.css`, nach dem `.calc-history-item .calc-h-result` Block (Zeile 1283), vor den Timer-Styles einfügen:
|
||
|
||
```css
|
||
/* Calculator Tab System */
|
||
.calc-tab-bar {
|
||
display: flex;
|
||
background: rgba(0,0,0,0.2);
|
||
border-bottom: 1px solid var(--border);
|
||
overflow-x: auto;
|
||
scrollbar-width: none;
|
||
flex-shrink: 0;
|
||
}
|
||
.calc-tab-bar::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
.calc-tab {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 3px;
|
||
padding: 6px 8px;
|
||
border: none;
|
||
border-bottom: 2px solid transparent;
|
||
background: none;
|
||
color: var(--text-muted);
|
||
font-size: 11px;
|
||
font-family: 'Rajdhani', sans-serif;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: color 0.15s, border-color 0.15s;
|
||
flex-shrink: 0;
|
||
}
|
||
.calc-tab:hover {
|
||
color: var(--text-secondary);
|
||
}
|
||
.calc-tab.active {
|
||
color: var(--accent);
|
||
border-bottom-color: var(--accent);
|
||
font-weight: 600;
|
||
}
|
||
.calc-tab-icon {
|
||
font-size: 12px;
|
||
}
|
||
.calc-tab-label {
|
||
font-size: 10px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
.calc-mode-body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 9: i18n-Key für Standard-Tab**
|
||
|
||
In `src/js/i18n.js`, nach `'calculator.error'` in beiden Sprachen:
|
||
|
||
DE:
|
||
```javascript
|
||
'calculator.tab.standard': 'Standard',
|
||
```
|
||
|
||
EN:
|
||
```javascript
|
||
'calculator.tab.standard': 'Standard',
|
||
```
|
||
|
||
- [ ] **Step 10: Testen**
|
||
|
||
1. Extension in Chrome laden
|
||
2. Calculator öffnen → Tab-Bar mit einem Tab "🔢 Std" sichtbar
|
||
3. Calculator schließen und wieder öffnen → State bleibt erhalten
|
||
4. Standard-Rechner funktioniert wie vorher (Buttons, History, Keyboard)
|
||
|
||
- [ ] **Step 11: Commit**
|
||
|
||
```bash
|
||
git add src/js/calculator.js src/css/main.css src/js/i18n.js
|
||
git commit -m "feat(calculator): Tab-System mit registerMode() und switchMode()"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: Parser-Erweiterung — ^ und sqrt
|
||
|
||
**Files:**
|
||
- Modify: `src/js/calculator.js` — `_evaluate()`, `_tokenize()`, `_parseExpression()` innere Funktionen
|
||
|
||
**Kontext:** Der Shunting-Yard-Parser kennt aktuell nur `+`, `-`, `*`, `/`, `%`. Wir fügen `^` (Potenz, rechts-assoziativ) und `sqrt` (unäre Funktion) hinzu. Die neue Hierarchie: `parseExpr(+,-)` → `parseTerm(*,/,%)` → `parsePower(^)` → `parseFactor(number | parens | func)`.
|
||
|
||
- [ ] **Step 1: Sanitizer erweitern**
|
||
|
||
In `_evaluate()`, die Sanitizer-Regex erweitern um `^` und Buchstaben (für `sqrt`):
|
||
|
||
```javascript
|
||
_evaluate(expr) {
|
||
try {
|
||
// Nur erlaubte Zeichen (inkl. ^ und sqrt-Buchstaben)
|
||
const sanitized = expr.replace(/[^0-9+\-*/.%()^a-z]/g, '');
|
||
if (!sanitized) return null;
|
||
|
||
const tokens = this._tokenize(sanitized);
|
||
if (!tokens) return null;
|
||
|
||
return this._parseExpression(tokens);
|
||
} catch {
|
||
return null;
|
||
}
|
||
},
|
||
```
|
||
|
||
- [ ] **Step 2: Tokenizer um ^ und sqrt erweitern**
|
||
|
||
In `_tokenize()`, nach dem Operator-Block (`if (/[+\-*/%]/.test(ch))`) und vor dem Klammern-Block, `^` als Operator erkennen. Vor dem Zahlen-Block `sqrt` als Funktion erkennen:
|
||
|
||
```javascript
|
||
_tokenize(expr) {
|
||
const tokens = [];
|
||
let i = 0;
|
||
|
||
while (i < expr.length) {
|
||
const ch = expr[i];
|
||
|
||
// Funktion: sqrt
|
||
if (expr.substring(i, i + 4) === 'sqrt') {
|
||
tokens.push({ type: 'func', value: 'sqrt' });
|
||
i += 4;
|
||
continue;
|
||
}
|
||
|
||
// Zahl (inkl. Dezimal)
|
||
if (/[0-9.]/.test(ch)) {
|
||
let num = '';
|
||
while (i < expr.length && /[0-9.]/.test(expr[i])) {
|
||
num += expr[i];
|
||
i++;
|
||
}
|
||
const parsed = parseFloat(num);
|
||
if (isNaN(parsed)) return null;
|
||
tokens.push({ type: 'number', value: parsed });
|
||
continue;
|
||
}
|
||
|
||
// Potenz-Operator
|
||
if (ch === '^') {
|
||
tokens.push({ type: 'op', value: '^' });
|
||
i++;
|
||
continue;
|
||
}
|
||
|
||
// Operator
|
||
if (/[+\-*/%]/.test(ch)) {
|
||
// Negativer Vorzeichen-Check: am Anfang oder nach Operator/oeffnender Klammer
|
||
if (ch === '-') {
|
||
const prev = tokens[tokens.length - 1];
|
||
if (!prev || prev.type === 'op' || (prev.type === 'paren' && prev.value === '(')) {
|
||
let num = '-';
|
||
i++;
|
||
while (i < expr.length && /[0-9.]/.test(expr[i])) {
|
||
num += expr[i];
|
||
i++;
|
||
}
|
||
if (num === '-') return null;
|
||
const parsed = parseFloat(num);
|
||
if (isNaN(parsed)) return null;
|
||
tokens.push({ type: 'number', value: parsed });
|
||
continue;
|
||
}
|
||
}
|
||
tokens.push({ type: 'op', value: ch });
|
||
i++;
|
||
continue;
|
||
}
|
||
|
||
// Klammern
|
||
if (ch === '(' || ch === ')') {
|
||
tokens.push({ type: 'paren', value: ch });
|
||
i++;
|
||
continue;
|
||
}
|
||
|
||
// Unbekannte Buchstaben überspringen (können Reste von Funktionsnamen sein)
|
||
if (/[a-z]/.test(ch)) {
|
||
return null;
|
||
}
|
||
|
||
// Unbekanntes Zeichen
|
||
return null;
|
||
}
|
||
|
||
return tokens;
|
||
},
|
||
```
|
||
|
||
- [ ] **Step 3: Parser um parsePower() und Funktions-Support erweitern**
|
||
|
||
Die `_parseExpression()` Methode komplett ersetzen. `parseTerm()` ruft jetzt `parsePower()` statt `parseFactor()` auf. `parsePower()` ist rechts-assoziativ. `parseFactor()` prüft auf Funktionen:
|
||
|
||
```javascript
|
||
_parseExpression(tokens) {
|
||
let pos = 0;
|
||
|
||
function peek() { return tokens[pos]; }
|
||
function consume() { return tokens[pos++]; }
|
||
|
||
// Expression: Term (('+' | '-') Term)*
|
||
function parseExpr() {
|
||
let left = parseTerm();
|
||
if (left === null) return null;
|
||
|
||
while (pos < tokens.length) {
|
||
const tk = peek();
|
||
if (!tk || tk.type !== 'op' || (tk.value !== '+' && tk.value !== '-')) break;
|
||
consume();
|
||
const right = parseTerm();
|
||
if (right === null) return null;
|
||
left = tk.value === '+' ? left + right : left - right;
|
||
}
|
||
return left;
|
||
}
|
||
|
||
// Term: Power (('*' | '/' | '%') Power)*
|
||
function parseTerm() {
|
||
let left = parsePower();
|
||
if (left === null) return null;
|
||
|
||
while (pos < tokens.length) {
|
||
const tk = peek();
|
||
if (!tk || tk.type !== 'op' || (tk.value !== '*' && tk.value !== '/' && tk.value !== '%')) break;
|
||
consume();
|
||
const right = parsePower();
|
||
if (right === null) return null;
|
||
if (tk.value === '*') {
|
||
left = left * right;
|
||
} else if (tk.value === '/') {
|
||
if (right === 0) return null;
|
||
left = left / right;
|
||
} else {
|
||
left = left % right;
|
||
}
|
||
}
|
||
return left;
|
||
}
|
||
|
||
// Power: Factor ('^' Power)? — rechts-assoziativ durch Rekursion
|
||
function parsePower() {
|
||
let base = parseFactor();
|
||
if (base === null) return null;
|
||
|
||
const tk = peek();
|
||
if (tk && tk.type === 'op' && tk.value === '^') {
|
||
consume();
|
||
const exp = parsePower(); // Rechts-assoziativ!
|
||
if (exp === null) return null;
|
||
return Math.pow(base, exp);
|
||
}
|
||
return base;
|
||
}
|
||
|
||
// Factor: func '(' Expression ')' | Number | '(' Expression ')'
|
||
function parseFactor() {
|
||
const tk = peek();
|
||
if (!tk) return null;
|
||
|
||
// Funktion: sqrt(...)
|
||
if (tk.type === 'func') {
|
||
const funcName = tk.value;
|
||
consume();
|
||
// Erwarte öffnende Klammer
|
||
const open = peek();
|
||
if (!open || open.type !== 'paren' || open.value !== '(') return null;
|
||
consume();
|
||
const val = parseExpr();
|
||
if (val === null) return null;
|
||
const close = peek();
|
||
if (close && close.type === 'paren' && close.value === ')') {
|
||
consume();
|
||
}
|
||
if (funcName === 'sqrt') {
|
||
if (val < 0) return null; // Keine negativen Wurzeln
|
||
return Math.sqrt(val);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
if (tk.type === 'number') {
|
||
consume();
|
||
return tk.value;
|
||
}
|
||
|
||
if (tk.type === 'paren' && tk.value === '(') {
|
||
consume();
|
||
const val = parseExpr();
|
||
if (val === null) return null;
|
||
const closing = peek();
|
||
if (closing && closing.type === 'paren' && closing.value === ')') {
|
||
consume();
|
||
}
|
||
return val;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
const result = parseExpr();
|
||
|
||
// Alle Tokens muessen verbraucht sein
|
||
if (pos < tokens.length) return null;
|
||
|
||
if (result === null || !isFinite(result)) return null;
|
||
return result;
|
||
},
|
||
```
|
||
|
||
- [ ] **Step 4: _handleKey() um ^ erweitern**
|
||
|
||
Im `default`-case der `_handleKey()` Methode, nach dem Ziffern-Check, `^` als Operator behandeln. In den Operator-Case (die bestehende `case '%': case '+': ...` Kette), `'^'` hinzufügen:
|
||
|
||
```javascript
|
||
case '%':
|
||
case '+':
|
||
case '-':
|
||
case '*':
|
||
case '/':
|
||
case '^': {
|
||
// Wenn gerade ein Ergebnis angezeigt wird, damit weiterrechnen
|
||
if (this._lastResult && this._currentExpr === '') {
|
||
this._currentExpr = this._lastResult;
|
||
this._lastResult = '';
|
||
}
|
||
// Doppelte Operatoren verhindern (letzten ersetzen)
|
||
const last = this._currentExpr.slice(-1);
|
||
if (/[+\-*/%^]/.test(last)) {
|
||
this._currentExpr = this._currentExpr.slice(0, -1) + key;
|
||
} else {
|
||
this._currentExpr += key;
|
||
}
|
||
break;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: _formatExpression() um ^ erweitern**
|
||
|
||
```javascript
|
||
_formatExpression(expr) {
|
||
return expr
|
||
.replace(/\*/g, '\u00D7')
|
||
.replace(/\//g, '\u00F7')
|
||
.replace(/sqrt\(/g, '√(');
|
||
},
|
||
```
|
||
|
||
- [ ] **Step 6: Testen**
|
||
|
||
1. Calculator öffnen
|
||
2. `2^10` eingeben → Ergebnis: 1024
|
||
3. `2^3^2` eingeben → Ergebnis: 512 (rechts-assoziativ)
|
||
4. In Browser-Console: Calculator._evaluate('sqrt(144)') → 12
|
||
5. `3+sqrt(16)` → 7
|
||
6. `sqrt(-4)` → Fehler
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add src/js/calculator.js
|
||
git commit -m "feat(calculator): Parser um ^ (Potenz) und sqrt() erweitern"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: Scientific-Modus
|
||
|
||
**Files:**
|
||
- Create: `src/js/calc-scientific.js`
|
||
- Modify: `newtab.html:509` (Script-Tag nach calculator.js einfügen)
|
||
- Modify: `src/js/i18n.js` (neue Keys)
|
||
- Modify: `src/css/main.css` (Scientific-spezifische Styles)
|
||
|
||
**Kontext:** Der Scientific-Modus erweitert den Standard-Rechner um 6 wissenschaftliche Buttons (√, x², xⁿ, π, e, ±) und einen Formel-Helfer mit 6 vorgefertigten Formeln. Er nutzt den gleichen `_handleKey()`/`_calculate()` Flow des Cores.
|
||
|
||
- [ ] **Step 1: calc-scientific.js erstellen**
|
||
|
||
```javascript
|
||
/* =============================================
|
||
HELLION NEWTAB — calc-scientific.js
|
||
Scientific-Modus für Calculator Widget
|
||
============================================= */
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
/** Formel-Definitionen */
|
||
const FORMULAS = [
|
||
{
|
||
key: 'circle_area',
|
||
fields: [{ key: 'radius', default: '' }],
|
||
calc: (vals) => Math.PI * vals.radius * vals.radius
|
||
},
|
||
{
|
||
key: 'circle_circumference',
|
||
fields: [{ key: 'radius', default: '' }],
|
||
calc: (vals) => 2 * Math.PI * vals.radius
|
||
},
|
||
{
|
||
key: 'celsius_to_fahrenheit',
|
||
fields: [{ key: 'temp', default: '' }],
|
||
calc: (vals) => (vals.temp * 9 / 5) + 32
|
||
},
|
||
{
|
||
key: 'fahrenheit_to_celsius',
|
||
fields: [{ key: 'temp', default: '' }],
|
||
calc: (vals) => (vals.temp - 32) * 5 / 9
|
||
},
|
||
{
|
||
key: 'pythagoras',
|
||
fields: [{ key: 'a', default: '' }, { key: 'b', default: '' }],
|
||
calc: (vals) => Math.sqrt(vals.a * vals.a + vals.b * vals.b)
|
||
},
|
||
{
|
||
key: 'percentage',
|
||
fields: [{ key: 'value', default: '' }, { key: 'percent', default: '' }],
|
||
calc: (vals) => vals.value * vals.percent / 100
|
||
}
|
||
];
|
||
|
||
/** Keyboard-Handler Referenz für Cleanup */
|
||
let _keyboardExtHandler = null;
|
||
|
||
/**
|
||
* Scientific-Buttons rendern
|
||
* @param {HTMLElement} container
|
||
*/
|
||
function renderSciButtons(container) {
|
||
const grid = document.createElement('div');
|
||
grid.className = 'calc-buttons calc-sci-buttons';
|
||
|
||
const buttons = [
|
||
['√', 'sqrt', 'operator'],
|
||
['x²', 'square', 'operator'],
|
||
['xⁿ', 'power', 'operator'],
|
||
['π', 'pi', 'operator'],
|
||
['e', 'euler', 'operator'],
|
||
['±', 'negate', 'operator']
|
||
];
|
||
|
||
buttons.forEach(([label, value, cls]) => {
|
||
const btn = document.createElement('button');
|
||
btn.className = 'calc-btn' + (cls ? ' ' + cls : '');
|
||
btn.textContent = label;
|
||
btn.type = 'button';
|
||
btn.addEventListener('click', () => handleSciKey(value));
|
||
grid.appendChild(btn);
|
||
});
|
||
|
||
container.appendChild(grid);
|
||
}
|
||
|
||
/**
|
||
* Scientific-Taste verarbeiten
|
||
* @param {string} key
|
||
*/
|
||
function handleSciKey(key) {
|
||
switch (key) {
|
||
case 'sqrt':
|
||
Calculator._currentExpr += 'sqrt(';
|
||
Calculator._updateDisplay();
|
||
break;
|
||
case 'square':
|
||
Calculator._currentExpr += '^2';
|
||
Calculator._updateDisplay();
|
||
break;
|
||
case 'power':
|
||
Calculator._handleKey('^');
|
||
break;
|
||
case 'pi':
|
||
Calculator._currentExpr += '3.14159265359';
|
||
Calculator._updateDisplay();
|
||
break;
|
||
case 'euler':
|
||
Calculator._currentExpr += '2.71828182846';
|
||
Calculator._updateDisplay();
|
||
break;
|
||
case 'negate':
|
||
handleNegate();
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Vorzeichen-Wechsel: letzten numerischen Wert negieren
|
||
*/
|
||
function handleNegate() {
|
||
const expr = Calculator._currentExpr;
|
||
if (!expr && Calculator._lastResult) {
|
||
// Ergebnis negieren
|
||
const num = parseFloat(Calculator._lastResult);
|
||
if (!isNaN(num)) {
|
||
Calculator._currentExpr = String(-num);
|
||
Calculator._lastResult = '';
|
||
Calculator._updateDisplay();
|
||
}
|
||
return;
|
||
}
|
||
// Letzte Zahl in der Expression finden und negieren
|
||
const match = expr.match(/(-?\d+\.?\d*)$/);
|
||
if (match) {
|
||
const num = parseFloat(match[1]);
|
||
const negated = String(-num);
|
||
Calculator._currentExpr = expr.slice(0, expr.length - match[1].length) + negated;
|
||
Calculator._updateDisplay();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Formel-Helfer UI rendern
|
||
* @param {HTMLElement} container
|
||
*/
|
||
function renderFormulaHelper(container) {
|
||
const wrapper = document.createElement('div');
|
||
wrapper.className = 'calc-formula-helper';
|
||
|
||
const label = document.createElement('div');
|
||
label.className = 'calc-formula-label';
|
||
label.textContent = t('calculator.sci.formulas');
|
||
|
||
const select = document.createElement('select');
|
||
select.className = 'calc-formula-select';
|
||
|
||
// Leer-Option
|
||
const emptyOpt = document.createElement('option');
|
||
emptyOpt.value = '';
|
||
emptyOpt.textContent = t('calculator.sci.select_formula');
|
||
select.appendChild(emptyOpt);
|
||
|
||
FORMULAS.forEach((f, i) => {
|
||
const opt = document.createElement('option');
|
||
opt.value = String(i);
|
||
opt.textContent = t('calculator.sci.formula.' + f.key);
|
||
select.appendChild(opt);
|
||
});
|
||
|
||
const inputsContainer = document.createElement('div');
|
||
inputsContainer.className = 'calc-formula-inputs';
|
||
|
||
const resultContainer = document.createElement('div');
|
||
resultContainer.className = 'calc-formula-result';
|
||
|
||
select.addEventListener('change', () => {
|
||
// Inputs leeren
|
||
while (inputsContainer.firstChild) {
|
||
inputsContainer.removeChild(inputsContainer.firstChild);
|
||
}
|
||
resultContainer.textContent = '';
|
||
|
||
const idx = parseInt(select.value, 10);
|
||
if (isNaN(idx)) return;
|
||
|
||
const formula = FORMULAS[idx];
|
||
renderFormulaInputs(formula, inputsContainer, resultContainer);
|
||
});
|
||
|
||
wrapper.append(label, select, inputsContainer, resultContainer);
|
||
container.appendChild(wrapper);
|
||
}
|
||
|
||
/**
|
||
* Formel-Eingabefelder und Live-Ergebnis rendern
|
||
* @param {Object} formula
|
||
* @param {HTMLElement} inputsEl
|
||
* @param {HTMLElement} resultEl
|
||
*/
|
||
function renderFormulaInputs(formula, inputsEl, resultEl) {
|
||
const inputs = {};
|
||
|
||
formula.fields.forEach(field => {
|
||
const row = document.createElement('div');
|
||
row.className = 'calc-formula-row';
|
||
|
||
const lbl = document.createElement('label');
|
||
lbl.textContent = t('calculator.sci.field.' + field.key);
|
||
|
||
const inp = document.createElement('input');
|
||
inp.type = 'number';
|
||
inp.className = 'calc-formula-input';
|
||
inp.placeholder = '0';
|
||
inp.step = 'any';
|
||
inputs[field.key] = inp;
|
||
|
||
inp.addEventListener('input', () => {
|
||
recalcFormula(formula, inputs, resultEl);
|
||
});
|
||
|
||
row.append(lbl, inp);
|
||
inputsEl.appendChild(row);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Formel-Ergebnis live berechnen
|
||
* @param {Object} formula
|
||
* @param {Object} inputs - { key: HTMLInputElement }
|
||
* @param {HTMLElement} resultEl
|
||
*/
|
||
function recalcFormula(formula, inputs, resultEl) {
|
||
const vals = {};
|
||
let allValid = true;
|
||
|
||
for (const field of formula.fields) {
|
||
const v = parseFloat(inputs[field.key].value);
|
||
if (isNaN(v)) { allValid = false; break; }
|
||
vals[field.key] = v;
|
||
}
|
||
|
||
if (!allValid) {
|
||
resultEl.textContent = '';
|
||
return;
|
||
}
|
||
|
||
const result = formula.calc(vals);
|
||
if (result === null || !isFinite(result)) {
|
||
resultEl.textContent = t('calculator.error');
|
||
return;
|
||
}
|
||
|
||
resultEl.textContent = '= ' + Calculator._formatResult(result);
|
||
}
|
||
|
||
/**
|
||
* Keyboard-Erweiterung für Scientific-Modus
|
||
* @param {HTMLElement} widgetEl
|
||
*/
|
||
function bindSciKeyboard(widgetEl) {
|
||
_keyboardExtHandler = (e) => {
|
||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||
if (e.target.contentEditable === 'true') return;
|
||
|
||
if (e.key === 'p') {
|
||
handleSciKey('pi');
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
} else if (e.key === '^') {
|
||
handleSciKey('power');
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
}
|
||
};
|
||
widgetEl.addEventListener('keydown', _keyboardExtHandler);
|
||
}
|
||
|
||
// Modus registrieren
|
||
Calculator.registerMode('scientific', {
|
||
label: '📐',
|
||
shortName: 'Sci',
|
||
titleKey: 'calculator.tab.scientific',
|
||
|
||
render(bodyEl) {
|
||
bodyEl.style.padding = '8px';
|
||
bodyEl.style.display = 'flex';
|
||
bodyEl.style.flexDirection = 'column';
|
||
bodyEl.style.flex = '1';
|
||
bodyEl.style.overflow = 'hidden';
|
||
|
||
// Display (gemeinsam mit Standard)
|
||
const display = document.createElement('div');
|
||
display.className = 'calc-display';
|
||
|
||
const exprEl = document.createElement('div');
|
||
exprEl.className = 'calc-expression';
|
||
Calculator._displayExprEl = exprEl;
|
||
|
||
const resultEl = document.createElement('div');
|
||
resultEl.className = 'calc-result';
|
||
resultEl.textContent = Calculator._lastResult || '0';
|
||
Calculator._displayResultEl = resultEl;
|
||
|
||
display.append(exprEl, resultEl);
|
||
|
||
// Scientific-Buttons (2x3 Grid)
|
||
const sciSection = document.createElement('div');
|
||
renderSciButtons(sciSection);
|
||
|
||
// Standard-Buttons (4x5 Grid)
|
||
const stdButtons = Calculator._createButtons();
|
||
|
||
// History
|
||
const historyEl = Calculator._createHistoryPanel();
|
||
|
||
// Formel-Helfer
|
||
const formulaSection = document.createElement('div');
|
||
renderFormulaHelper(formulaSection);
|
||
|
||
bodyEl.append(display, sciSection, stdButtons, historyEl, formulaSection);
|
||
Calculator._updateDisplay();
|
||
|
||
// Keyboard erweitern
|
||
const entry = WidgetManager._widgets.get(Calculator.WIDGET_ID);
|
||
if (entry) bindSciKeyboard(entry.el);
|
||
},
|
||
|
||
destroy() {
|
||
// Keyboard-Extension entfernen
|
||
if (_keyboardExtHandler) {
|
||
const entry = WidgetManager._widgets.get(Calculator.WIDGET_ID);
|
||
if (entry) {
|
||
entry.el.removeEventListener('keydown', _keyboardExtHandler);
|
||
}
|
||
_keyboardExtHandler = null;
|
||
}
|
||
Calculator._displayExprEl = null;
|
||
Calculator._displayResultEl = null;
|
||
}
|
||
});
|
||
})();
|
||
```
|
||
|
||
- [ ] **Step 2: Script-Tag in newtab.html einfügen**
|
||
|
||
Nach Zeile 509 (`<script src="src/js/calculator.js"></script>`):
|
||
|
||
```html
|
||
<script src="src/js/calc-scientific.js"></script>
|
||
```
|
||
|
||
- [ ] **Step 3: CSS für Scientific-Modus**
|
||
|
||
In `src/css/main.css`, nach den Tab-System Styles:
|
||
|
||
```css
|
||
/* Calculator Scientific Mode */
|
||
.calc-sci-buttons {
|
||
grid-template-columns: repeat(3, 1fr);
|
||
margin-bottom: 4px;
|
||
}
|
||
.calc-formula-helper {
|
||
border-top: 1px solid var(--border);
|
||
padding-top: 8px;
|
||
margin-top: 4px;
|
||
}
|
||
.calc-formula-label {
|
||
font-size: 9px;
|
||
color: var(--text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
margin-bottom: 4px;
|
||
}
|
||
.calc-formula-select {
|
||
width: 100%;
|
||
padding: 4px 6px;
|
||
background: rgba(0,0,0,0.3);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
color: var(--text-primary);
|
||
font-size: 12px;
|
||
font-family: 'Rajdhani', sans-serif;
|
||
margin-bottom: 6px;
|
||
}
|
||
.calc-formula-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 4px;
|
||
}
|
||
.calc-formula-row label {
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
min-width: 50px;
|
||
}
|
||
.calc-formula-input {
|
||
flex: 1;
|
||
padding: 4px 6px;
|
||
background: rgba(0,0,0,0.3);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
color: var(--text-primary);
|
||
font-size: 12px;
|
||
font-family: 'Rajdhani', sans-serif;
|
||
}
|
||
.calc-formula-result {
|
||
font-size: 14px;
|
||
color: var(--accent);
|
||
font-weight: 600;
|
||
font-family: 'Rajdhani', monospace;
|
||
text-align: right;
|
||
min-height: 20px;
|
||
padding: 2px 0;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: i18n-Keys für Scientific**
|
||
|
||
In `src/js/i18n.js`, DE-Block nach `'calculator.tab.standard'`:
|
||
|
||
```javascript
|
||
'calculator.tab.scientific': 'Wissenschaftlich',
|
||
'calculator.sci.formulas': 'Formel-Helfer',
|
||
'calculator.sci.select_formula': 'Formel wählen…',
|
||
'calculator.sci.formula.circle_area': 'Kreisfläche (π×r²)',
|
||
'calculator.sci.formula.circle_circumference':'Kreisumfang (2πr)',
|
||
'calculator.sci.formula.celsius_to_fahrenheit':'°C → °F',
|
||
'calculator.sci.formula.fahrenheit_to_celsius':'°F → °C',
|
||
'calculator.sci.formula.pythagoras': 'Pythagoras (√(a²+b²))',
|
||
'calculator.sci.formula.percentage': 'Prozentwert',
|
||
'calculator.sci.field.radius': 'Radius',
|
||
'calculator.sci.field.temp': 'Temperatur',
|
||
'calculator.sci.field.a': 'Seite a',
|
||
'calculator.sci.field.b': 'Seite b',
|
||
'calculator.sci.field.value': 'Wert',
|
||
'calculator.sci.field.percent': 'Prozent',
|
||
```
|
||
|
||
EN-Block nach `'calculator.tab.standard'`:
|
||
|
||
```javascript
|
||
'calculator.tab.scientific': 'Scientific',
|
||
'calculator.sci.formulas': 'Formula Helper',
|
||
'calculator.sci.select_formula': 'Choose formula…',
|
||
'calculator.sci.formula.circle_area': 'Circle Area (π×r²)',
|
||
'calculator.sci.formula.circle_circumference':'Circle Circumference (2πr)',
|
||
'calculator.sci.formula.celsius_to_fahrenheit':'°C → °F',
|
||
'calculator.sci.formula.fahrenheit_to_celsius':'°F → °C',
|
||
'calculator.sci.formula.pythagoras': 'Pythagoras (√(a²+b²))',
|
||
'calculator.sci.formula.percentage': 'Percentage',
|
||
'calculator.sci.field.radius': 'Radius',
|
||
'calculator.sci.field.temp': 'Temperature',
|
||
'calculator.sci.field.a': 'Side a',
|
||
'calculator.sci.field.b': 'Side b',
|
||
'calculator.sci.field.value': 'Value',
|
||
'calculator.sci.field.percent': 'Percent',
|
||
```
|
||
|
||
- [ ] **Step 5: Testen**
|
||
|
||
1. Calculator öffnen → 2 Tabs sichtbar (Std, Sci)
|
||
2. Sci-Tab klicken → 6 Scientific-Buttons + Standard-Grid + Formel-Helfer
|
||
3. √ klicken → `sqrt(` im Display, 9 eingeben, `)` eingeben, = → 3
|
||
4. π klicken → 3.14159265359 im Display
|
||
5. Formel-Dropdown "Kreisfläche" wählen → Radius-Feld, r=5 → = 78.5398...
|
||
6. `p`-Taste → Pi einfügen
|
||
7. `^`-Taste → Potenz-Operator
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add src/js/calc-scientific.js newtab.html src/css/main.css src/js/i18n.js
|
||
git commit -m "feat(calculator): Scientific-Modus mit Formel-Helfer"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: Unit-Converter
|
||
|
||
**Files:**
|
||
- Create: `src/js/calc-converter.js`
|
||
- Modify: `newtab.html` (Script-Tag nach calc-scientific.js)
|
||
- Modify: `src/js/i18n.js` (neue Keys)
|
||
- Modify: `src/css/main.css` (Converter-spezifische Styles)
|
||
|
||
**Kontext:** 6 Kategorien (Länge, Gewicht, Temperatur, Volumen, Geschwindigkeit, Fläche) mit toBase/fromBase Pattern. Temperatur als Spezialfall. Live-Update bei Eingabe, Swap-Button, Schnellreferenz.
|
||
|
||
- [ ] **Step 1: calc-converter.js erstellen**
|
||
|
||
```javascript
|
||
/* =============================================
|
||
HELLION NEWTAB — calc-converter.js
|
||
Unit-Converter Modus für Calculator Widget
|
||
============================================= */
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
// ---- UNIT DEFINITIONS ----
|
||
|
||
const CATEGORIES = {
|
||
length: {
|
||
titleKey: 'calculator.conv.cat.length',
|
||
baseUnit: 'm',
|
||
units: {
|
||
mm: { toBase: v => v / 1000, fromBase: v => v * 1000 },
|
||
cm: { toBase: v => v / 100, fromBase: v => v * 100 },
|
||
m: { toBase: v => v, fromBase: v => v },
|
||
km: { toBase: v => v * 1000, fromBase: v => v / 1000 },
|
||
in: { toBase: v => v * 0.0254, fromBase: v => v / 0.0254 },
|
||
ft: { toBase: v => v * 0.3048, fromBase: v => v / 0.3048 },
|
||
yd: { toBase: v => v * 0.9144, fromBase: v => v / 0.9144 },
|
||
mi: { toBase: v => v * 1609.344, fromBase: v => v / 1609.344 }
|
||
}
|
||
},
|
||
weight: {
|
||
titleKey: 'calculator.conv.cat.weight',
|
||
baseUnit: 'g',
|
||
units: {
|
||
mg: { toBase: v => v / 1000, fromBase: v => v * 1000 },
|
||
g: { toBase: v => v, fromBase: v => v },
|
||
kg: { toBase: v => v * 1000, fromBase: v => v / 1000 },
|
||
t: { toBase: v => v * 1000000, fromBase: v => v / 1000000 },
|
||
oz: { toBase: v => v * 28.3495, fromBase: v => v / 28.3495 },
|
||
lb: { toBase: v => v * 453.592, fromBase: v => v / 453.592 }
|
||
}
|
||
},
|
||
temperature: {
|
||
titleKey: 'calculator.conv.cat.temperature',
|
||
baseUnit: null, // Spezialfall
|
||
units: { '\u00B0C': null, '\u00B0F': null, 'K': null },
|
||
convert(value, from, to) {
|
||
if (from === to) return value;
|
||
const key = from + '_' + to;
|
||
const conversions = {
|
||
'\u00B0C_\u00B0F': v => (v * 9 / 5) + 32,
|
||
'\u00B0C_K': v => v + 273.15,
|
||
'\u00B0F_\u00B0C': v => (v - 32) * 5 / 9,
|
||
'\u00B0F_K': v => (v - 32) * 5 / 9 + 273.15,
|
||
'K_\u00B0C': v => v - 273.15,
|
||
'K_\u00B0F': v => (v - 273.15) * 9 / 5 + 32
|
||
};
|
||
const fn = conversions[key];
|
||
return fn ? fn(value) : null;
|
||
}
|
||
},
|
||
volume: {
|
||
titleKey: 'calculator.conv.cat.volume',
|
||
baseUnit: 'ml',
|
||
units: {
|
||
ml: { toBase: v => v, fromBase: v => v },
|
||
L: { toBase: v => v * 1000, fromBase: v => v / 1000 },
|
||
'm\u00B3':{ toBase: v => v * 1000000, fromBase: v => v / 1000000 },
|
||
'gal(US)':{ toBase: v => v * 3785.41, fromBase: v => v / 3785.41 },
|
||
'gal(UK)':{ toBase: v => v * 4546.09, fromBase: v => v / 4546.09 },
|
||
'ft\u00B3':{ toBase: v => v * 28316.8, fromBase: v => v / 28316.8 }
|
||
}
|
||
},
|
||
speed: {
|
||
titleKey: 'calculator.conv.cat.speed',
|
||
baseUnit: 'm/s',
|
||
units: {
|
||
'm/s': { toBase: v => v, fromBase: v => v },
|
||
'km/h': { toBase: v => v / 3.6, fromBase: v => v * 3.6 },
|
||
'mph': { toBase: v => v * 0.44704, fromBase: v => v / 0.44704 },
|
||
'kn': { toBase: v => v * 0.514444, fromBase: v => v / 0.514444 }
|
||
}
|
||
},
|
||
area: {
|
||
titleKey: 'calculator.conv.cat.area',
|
||
baseUnit: 'm\u00B2',
|
||
units: {
|
||
'mm\u00B2': { toBase: v => v / 1000000, fromBase: v => v * 1000000 },
|
||
'cm\u00B2': { toBase: v => v / 10000, fromBase: v => v * 10000 },
|
||
'm\u00B2': { toBase: v => v, fromBase: v => v },
|
||
'km\u00B2': { toBase: v => v * 1000000, fromBase: v => v / 1000000 },
|
||
'ha': { toBase: v => v * 10000, fromBase: v => v / 10000 },
|
||
'acre': { toBase: v => v * 4046.86, fromBase: v => v / 4046.86 },
|
||
'ft\u00B2': { toBase: v => v * 0.092903, fromBase: v => v / 0.092903 },
|
||
'in\u00B2': { toBase: v => v * 0.00064516, fromBase: v => v / 0.00064516 }
|
||
}
|
||
}
|
||
};
|
||
|
||
const CATEGORY_ORDER = ['length', 'weight', 'temperature', 'volume', 'speed', 'area'];
|
||
|
||
// ---- STATE ----
|
||
|
||
let _currentCategory = 'length';
|
||
let _fromUnit = 'cm';
|
||
let _toUnit = 'in';
|
||
let _fromInput = null;
|
||
let _toInput = null;
|
||
let _refEl = null;
|
||
|
||
// ---- CONVERSION ----
|
||
|
||
/**
|
||
* Wert konvertieren
|
||
* @param {number} value
|
||
* @param {string} from - Quelleinheit
|
||
* @param {string} to - Zieleinheit
|
||
* @returns {number|null}
|
||
*/
|
||
function convert(value, from, to) {
|
||
const cat = CATEGORIES[_currentCategory];
|
||
if (!cat) return null;
|
||
|
||
if (cat.convert) {
|
||
return cat.convert(value, from, to);
|
||
}
|
||
|
||
const fromDef = cat.units[from];
|
||
const toDef = cat.units[to];
|
||
if (!fromDef || !toDef) return null;
|
||
|
||
const base = fromDef.toBase(value);
|
||
return toDef.fromBase(base);
|
||
}
|
||
|
||
/**
|
||
* Live-Berechnung auslösen
|
||
*/
|
||
function recalc() {
|
||
if (!_fromInput || !_toInput) return;
|
||
const val = parseFloat(_fromInput.value);
|
||
if (isNaN(val)) {
|
||
_toInput.value = '';
|
||
updateReference();
|
||
return;
|
||
}
|
||
const result = convert(val, _fromUnit, _toUnit);
|
||
if (result === null) {
|
||
_toInput.value = '';
|
||
} else {
|
||
_toInput.value = Calculator._formatResult(result);
|
||
}
|
||
updateReference();
|
||
}
|
||
|
||
/**
|
||
* Schnellreferenz aktualisieren
|
||
*/
|
||
function updateReference() {
|
||
if (!_refEl) return;
|
||
_refEl.textContent = '';
|
||
|
||
const r1 = convert(1, _fromUnit, _toUnit);
|
||
const r2 = convert(1, _toUnit, _fromUnit);
|
||
|
||
if (r1 !== null) {
|
||
const line1 = document.createElement('div');
|
||
line1.textContent = '1 ' + _fromUnit + ' = ' + Calculator._formatResult(r1) + ' ' + _toUnit;
|
||
_refEl.appendChild(line1);
|
||
}
|
||
if (r2 !== null) {
|
||
const line2 = document.createElement('div');
|
||
line2.textContent = '1 ' + _toUnit + ' = ' + Calculator._formatResult(r2) + ' ' + _fromUnit;
|
||
_refEl.appendChild(line2);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Unit-Selects für aktuelle Kategorie befüllen
|
||
* @param {HTMLSelectElement} selectEl
|
||
* @param {string} selectedUnit
|
||
*/
|
||
function populateUnitSelect(selectEl, selectedUnit) {
|
||
while (selectEl.firstChild) {
|
||
selectEl.removeChild(selectEl.firstChild);
|
||
}
|
||
const cat = CATEGORIES[_currentCategory];
|
||
if (!cat) return;
|
||
|
||
const units = Object.keys(cat.units);
|
||
units.forEach(unit => {
|
||
const opt = document.createElement('option');
|
||
opt.value = unit;
|
||
opt.textContent = unit;
|
||
if (unit === selectedUnit) opt.selected = true;
|
||
selectEl.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Kategorie-spezifische Defaults für Units
|
||
* @param {string} catKey
|
||
* @returns {{ from: string, to: string }}
|
||
*/
|
||
function getDefaultUnits(catKey) {
|
||
const defaults = {
|
||
length: { from: 'cm', to: 'in' },
|
||
weight: { from: 'kg', to: 'lb' },
|
||
temperature: { from: '\u00B0C', to: '\u00B0F' },
|
||
volume: { from: 'L', to: 'gal(US)' },
|
||
speed: { from: 'km/h', to: 'mph' },
|
||
area: { from: 'm\u00B2', to: 'ft\u00B2' }
|
||
};
|
||
return defaults[catKey] || { from: Object.keys(CATEGORIES[catKey].units)[0], to: Object.keys(CATEGORIES[catKey].units)[1] };
|
||
}
|
||
|
||
/**
|
||
* State aus Storage laden
|
||
*/
|
||
async function loadState() {
|
||
const data = await Store.get(Calculator.STORAGE_KEY);
|
||
if (data && data.calculator && data.calculator.converter) {
|
||
const s = data.calculator.converter;
|
||
if (s.lastCategory && CATEGORIES[s.lastCategory]) _currentCategory = s.lastCategory;
|
||
if (s.fromUnit) _fromUnit = s.fromUnit;
|
||
if (s.toUnit) _toUnit = s.toUnit;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* State in Storage speichern
|
||
*/
|
||
async function saveState() {
|
||
const data = await Store.get(Calculator.STORAGE_KEY) || {};
|
||
if (!data.calculator) data.calculator = {};
|
||
data.calculator.converter = {
|
||
lastCategory: _currentCategory,
|
||
fromUnit: _fromUnit,
|
||
toUnit: _toUnit
|
||
};
|
||
await Store.set(Calculator.STORAGE_KEY, data);
|
||
}
|
||
|
||
// ---- REGISTRATION ----
|
||
|
||
Calculator.registerMode('converter', {
|
||
label: '⚖️',
|
||
shortName: 'Unit',
|
||
titleKey: 'calculator.tab.converter',
|
||
|
||
render(bodyEl) {
|
||
bodyEl.style.padding = '8px';
|
||
bodyEl.style.display = 'flex';
|
||
bodyEl.style.flexDirection = 'column';
|
||
bodyEl.style.gap = '8px';
|
||
|
||
loadState().then(() => {
|
||
buildUI(bodyEl);
|
||
});
|
||
},
|
||
|
||
destroy() {
|
||
_fromInput = null;
|
||
_toInput = null;
|
||
_refEl = null;
|
||
saveState();
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Converter-UI aufbauen
|
||
* @param {HTMLElement} bodyEl
|
||
*/
|
||
function buildUI(bodyEl) {
|
||
// Kategorie-Dropdown
|
||
const catSelect = document.createElement('select');
|
||
catSelect.className = 'calc-conv-select';
|
||
|
||
CATEGORY_ORDER.forEach(catKey => {
|
||
const opt = document.createElement('option');
|
||
opt.value = catKey;
|
||
opt.textContent = t(CATEGORIES[catKey].titleKey);
|
||
if (catKey === _currentCategory) opt.selected = true;
|
||
catSelect.appendChild(opt);
|
||
});
|
||
|
||
// From-Zeile
|
||
const fromRow = document.createElement('div');
|
||
fromRow.className = 'calc-conv-row';
|
||
|
||
_fromInput = document.createElement('input');
|
||
_fromInput.type = 'number';
|
||
_fromInput.className = 'calc-conv-input';
|
||
_fromInput.placeholder = '0';
|
||
_fromInput.step = 'any';
|
||
|
||
const fromSelect = document.createElement('select');
|
||
fromSelect.className = 'calc-conv-unit';
|
||
populateUnitSelect(fromSelect, _fromUnit);
|
||
|
||
fromRow.append(_fromInput, fromSelect);
|
||
|
||
// Swap-Button
|
||
const swapBtn = document.createElement('button');
|
||
swapBtn.type = 'button';
|
||
swapBtn.className = 'calc-conv-swap';
|
||
swapBtn.textContent = '\u21C5';
|
||
swapBtn.title = t('calculator.conv.swap');
|
||
|
||
// To-Zeile
|
||
const toRow = document.createElement('div');
|
||
toRow.className = 'calc-conv-row';
|
||
|
||
_toInput = document.createElement('input');
|
||
_toInput.type = 'text';
|
||
_toInput.className = 'calc-conv-input';
|
||
_toInput.readOnly = true;
|
||
_toInput.placeholder = '0';
|
||
|
||
const toSelect = document.createElement('select');
|
||
toSelect.className = 'calc-conv-unit';
|
||
populateUnitSelect(toSelect, _toUnit);
|
||
|
||
toRow.append(_toInput, toSelect);
|
||
|
||
// Schnellreferenz
|
||
_refEl = document.createElement('div');
|
||
_refEl.className = 'calc-conv-ref';
|
||
|
||
// Event Listeners
|
||
_fromInput.addEventListener('input', () => recalc());
|
||
|
||
fromSelect.addEventListener('change', () => {
|
||
_fromUnit = fromSelect.value;
|
||
recalc();
|
||
saveState();
|
||
});
|
||
|
||
toSelect.addEventListener('change', () => {
|
||
_toUnit = toSelect.value;
|
||
recalc();
|
||
saveState();
|
||
});
|
||
|
||
swapBtn.addEventListener('click', () => {
|
||
const tmpUnit = _fromUnit;
|
||
_fromUnit = _toUnit;
|
||
_toUnit = tmpUnit;
|
||
populateUnitSelect(fromSelect, _fromUnit);
|
||
populateUnitSelect(toSelect, _toUnit);
|
||
// Swap-Wert übernehmen
|
||
const currentVal = _toInput.value;
|
||
if (currentVal) {
|
||
_fromInput.value = currentVal;
|
||
}
|
||
recalc();
|
||
saveState();
|
||
});
|
||
|
||
catSelect.addEventListener('change', () => {
|
||
_currentCategory = catSelect.value;
|
||
const defaults = getDefaultUnits(_currentCategory);
|
||
_fromUnit = defaults.from;
|
||
_toUnit = defaults.to;
|
||
populateUnitSelect(fromSelect, _fromUnit);
|
||
populateUnitSelect(toSelect, _toUnit);
|
||
_fromInput.value = '';
|
||
_toInput.value = '';
|
||
updateReference();
|
||
saveState();
|
||
});
|
||
|
||
bodyEl.append(catSelect, fromRow, swapBtn, toRow, _refEl);
|
||
|
||
// Initiale Referenz anzeigen
|
||
updateReference();
|
||
}
|
||
})();
|
||
```
|
||
|
||
- [ ] **Step 2: Script-Tag in newtab.html einfügen**
|
||
|
||
Nach dem `calc-scientific.js` Script-Tag:
|
||
|
||
```html
|
||
<script src="src/js/calc-converter.js"></script>
|
||
```
|
||
|
||
- [ ] **Step 3: CSS für Converter**
|
||
|
||
In `src/css/main.css`, nach den Scientific-Styles:
|
||
|
||
```css
|
||
/* Calculator Converter Mode */
|
||
.calc-conv-select {
|
||
width: 100%;
|
||
padding: 6px 8px;
|
||
background: rgba(0,0,0,0.3);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
color: var(--text-primary);
|
||
font-size: 13px;
|
||
font-family: 'Rajdhani', sans-serif;
|
||
}
|
||
.calc-conv-row {
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
.calc-conv-input {
|
||
flex: 1;
|
||
padding: 8px;
|
||
background: rgba(0,0,0,0.3);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
color: var(--text-primary);
|
||
font-size: 16px;
|
||
font-family: 'Rajdhani', monospace;
|
||
}
|
||
.calc-conv-input:read-only {
|
||
color: var(--accent);
|
||
font-weight: 600;
|
||
}
|
||
.calc-conv-unit {
|
||
width: 80px;
|
||
padding: 4px 6px;
|
||
background: rgba(0,0,0,0.3);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
color: var(--text-primary);
|
||
font-size: 12px;
|
||
font-family: 'Rajdhani', sans-serif;
|
||
}
|
||
.calc-conv-swap {
|
||
align-self: center;
|
||
width: 36px;
|
||
height: 28px;
|
||
background: rgba(255,255,255,0.04);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
color: var(--accent);
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
}
|
||
.calc-conv-swap:hover {
|
||
background: var(--accent-dim);
|
||
}
|
||
.calc-conv-ref {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
padding: 4px 0;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: i18n-Keys für Converter**
|
||
|
||
DE-Block:
|
||
|
||
```javascript
|
||
'calculator.tab.converter': 'Umrechner',
|
||
'calculator.conv.swap': 'Einheiten tauschen',
|
||
'calculator.conv.cat.length': 'Länge',
|
||
'calculator.conv.cat.weight': 'Gewicht',
|
||
'calculator.conv.cat.temperature': 'Temperatur',
|
||
'calculator.conv.cat.volume': 'Volumen',
|
||
'calculator.conv.cat.speed': 'Geschwindigkeit',
|
||
'calculator.conv.cat.area': 'Fläche',
|
||
```
|
||
|
||
EN-Block:
|
||
|
||
```javascript
|
||
'calculator.tab.converter': 'Converter',
|
||
'calculator.conv.swap': 'Swap units',
|
||
'calculator.conv.cat.length': 'Length',
|
||
'calculator.conv.cat.weight': 'Weight',
|
||
'calculator.conv.cat.temperature': 'Temperature',
|
||
'calculator.conv.cat.volume': 'Volume',
|
||
'calculator.conv.cat.speed': 'Speed',
|
||
'calculator.conv.cat.area': 'Area',
|
||
```
|
||
|
||
- [ ] **Step 5: Testen**
|
||
|
||
1. Calculator öffnen → 3 Tabs sichtbar (Std, Sci, Unit)
|
||
2. Unit-Tab klicken → Kategorie-Dropdown, Eingabe-Feld, Ergebnis
|
||
3. 100 cm eingeben → 39.3701 in
|
||
4. Swap-Button → cm und in tauschen, Wert übernehmen
|
||
5. Kategorie zu Temperatur wechseln → °C und °F als Units
|
||
6. 100 °C → 212 °F
|
||
7. 0 °F → -17.7778 °C
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add src/js/calc-converter.js newtab.html src/css/main.css src/js/i18n.js
|
||
git commit -m "feat(calculator): Unit-Converter mit 6 Kategorien"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: Satisfactory Calculator
|
||
|
||
**Files:**
|
||
- Create: `src/js/calc-satisfactory.js`
|
||
- Modify: `newtab.html` (Script-Tag nach calc-converter.js)
|
||
- Modify: `src/js/i18n.js` (neue Keys)
|
||
- Modify: `src/css/main.css` (Game-Calculator shared Styles)
|
||
|
||
**Kontext:** 3 Sub-Modi: Items/Min, Overclock Power, Machines. Power-Exponent: 1.321928 (Update 8 Wert). Shared CSS-Klassen `.calc-game-*` werden hier definiert und von Factorio/Stationeers wiederverwendet.
|
||
|
||
- [ ] **Step 1: calc-satisfactory.js erstellen**
|
||
|
||
```javascript
|
||
/* =============================================
|
||
HELLION NEWTAB — calc-satisfactory.js
|
||
Satisfactory Calculator Modus
|
||
============================================= */
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
const POWER_EXPONENT = 1.321928;
|
||
|
||
const SUB_MODES = ['itemsPerMin', 'power', 'machines'];
|
||
let _activeSubMode = 'itemsPerMin';
|
||
|
||
// ---- SHARED HELPERS ----
|
||
|
||
/**
|
||
* Sub-Tab-Leiste erstellen
|
||
* @param {HTMLElement} container
|
||
* @param {Array<string>} modes
|
||
* @param {string} activeMode
|
||
* @param {string} i18nPrefix
|
||
* @param {Function} onSwitch
|
||
*/
|
||
function createSubTabs(container, modes, activeMode, i18nPrefix, onSwitch) {
|
||
const bar = document.createElement('div');
|
||
bar.className = 'calc-game-subtabs';
|
||
|
||
modes.forEach(mode => {
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'calc-game-subtab' + (mode === activeMode ? ' active' : '');
|
||
btn.textContent = t(i18nPrefix + mode);
|
||
btn.dataset.mode = mode;
|
||
btn.addEventListener('click', () => {
|
||
bar.querySelectorAll('.calc-game-subtab').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
onSwitch(mode);
|
||
});
|
||
bar.appendChild(btn);
|
||
});
|
||
|
||
container.appendChild(bar);
|
||
}
|
||
|
||
/**
|
||
* Eingabefeld mit Label erstellen
|
||
* @param {string} labelKey - i18n-Key
|
||
* @param {number} defaultVal
|
||
* @param {Object} opts - { step, min, max }
|
||
* @returns {{ row: HTMLElement, input: HTMLInputElement }}
|
||
*/
|
||
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;
|
||
|
||
row.append(label, input);
|
||
return { row, input };
|
||
}
|
||
|
||
/**
|
||
* Ergebnis-Zeile erstellen
|
||
* @param {string} labelKey
|
||
* @returns {{ row: HTMLElement, value: HTMLElement }}
|
||
*/
|
||
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 };
|
||
}
|
||
|
||
// ---- SUB-MODE RENDERERS ----
|
||
|
||
function renderItemsPerMin(container) {
|
||
const itemsField = createField('calculator.sat.items_per_craft', 1, { step: 1, min: 1 });
|
||
const timeField = createField('calculator.sat.craft_time', 4, { step: 0.1, min: 0.1 });
|
||
const clockField = createField('calculator.sat.clock_speed', 100, { step: 1, min: 1, max: 250 });
|
||
|
||
const output = createOutput('calculator.sat.output_per_min');
|
||
|
||
function calc() {
|
||
const items = parseFloat(itemsField.input.value) || 0;
|
||
const time = parseFloat(timeField.input.value) || 1;
|
||
const clock = parseFloat(clockField.input.value) || 100;
|
||
const result = (items * 60) / time * (clock / 100);
|
||
output.value.textContent = Calculator._formatResult(result) + ' items/min';
|
||
}
|
||
|
||
[itemsField, timeField, clockField].forEach(f => {
|
||
f.input.addEventListener('input', calc);
|
||
});
|
||
|
||
container.append(itemsField.row, timeField.row, clockField.row, output.row);
|
||
calc();
|
||
}
|
||
|
||
function renderPower(container) {
|
||
const basePowerField = createField('calculator.sat.base_power', 30, { step: 1, min: 0.1 });
|
||
const clockField = createField('calculator.sat.clock_speed', 100, { step: 1, min: 1, max: 250 });
|
||
|
||
const powerOutput = createOutput('calculator.sat.power_usage');
|
||
const effOutput = createOutput('calculator.sat.efficiency');
|
||
|
||
function calc() {
|
||
const basePower = parseFloat(basePowerField.input.value) || 0;
|
||
const clock = parseFloat(clockField.input.value) || 100;
|
||
const ratio = clock / 100;
|
||
const power = basePower * Math.pow(ratio, POWER_EXPONENT);
|
||
const effPerItem = Math.pow(ratio, POWER_EXPONENT - 1);
|
||
|
||
powerOutput.value.textContent = Calculator._formatResult(power) + ' MW';
|
||
|
||
if (clock > 100) {
|
||
const overhead = (effPerItem - 1) * 100;
|
||
effOutput.value.textContent = '+' + Calculator._formatResult(overhead) + '% ' + t('calculator.sat.per_item');
|
||
effOutput.row.style.display = '';
|
||
} else {
|
||
effOutput.row.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
[basePowerField, clockField].forEach(f => {
|
||
f.input.addEventListener('input', calc);
|
||
});
|
||
|
||
container.append(basePowerField.row, clockField.row, powerOutput.row, effOutput.row);
|
||
calc();
|
||
}
|
||
|
||
function renderMachines(container) {
|
||
const targetField = createField('calculator.sat.target_output', 60, { step: 1, min: 1 });
|
||
const itemsField = createField('calculator.sat.items_per_craft', 1, { step: 1, min: 1 });
|
||
const timeField = createField('calculator.sat.craft_time', 4, { step: 0.1, min: 0.1 });
|
||
const clockField = createField('calculator.sat.clock_speed', 100, { step: 1, min: 1, max: 250 });
|
||
const basePowerField = createField('calculator.sat.base_power', 30, { step: 1, min: 0.1 });
|
||
|
||
const machinesOutput = createOutput('calculator.sat.machines_needed');
|
||
const totalPowerOutput = createOutput('calculator.sat.total_power');
|
||
|
||
function calc() {
|
||
const target = parseFloat(targetField.input.value) || 0;
|
||
const items = parseFloat(itemsField.input.value) || 1;
|
||
const time = parseFloat(timeField.input.value) || 1;
|
||
const clock = parseFloat(clockField.input.value) || 100;
|
||
const basePower = parseFloat(basePowerField.input.value) || 0;
|
||
|
||
const ratio = clock / 100;
|
||
const itemsPerMin = (items * 60) / time * ratio;
|
||
const machines = itemsPerMin > 0 ? Math.ceil(target / itemsPerMin) : 0;
|
||
const totalPower = machines * basePower * Math.pow(ratio, POWER_EXPONENT);
|
||
|
||
machinesOutput.value.textContent = machines;
|
||
totalPowerOutput.value.textContent = Calculator._formatResult(totalPower) + ' MW';
|
||
}
|
||
|
||
[targetField, itemsField, timeField, clockField, basePowerField].forEach(f => {
|
||
f.input.addEventListener('input', calc);
|
||
});
|
||
|
||
container.append(targetField.row, itemsField.row, timeField.row, clockField.row, basePowerField.row, machinesOutput.row, totalPowerOutput.row);
|
||
calc();
|
||
}
|
||
|
||
// ---- STATE ----
|
||
|
||
async function loadState() {
|
||
const data = await Store.get(Calculator.STORAGE_KEY);
|
||
if (data && data.calculator && data.calculator.satisfactory) {
|
||
const s = data.calculator.satisfactory;
|
||
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.satisfactory = { lastSubMode: _activeSubMode };
|
||
await Store.set(Calculator.STORAGE_KEY, data);
|
||
}
|
||
|
||
// ---- RENDER ----
|
||
|
||
function renderSubMode(container) {
|
||
container.textContent = '';
|
||
switch (_activeSubMode) {
|
||
case 'itemsPerMin': renderItemsPerMin(container); break;
|
||
case 'power': renderPower(container); break;
|
||
case 'machines': renderMachines(container); break;
|
||
}
|
||
}
|
||
|
||
// ---- REGISTRATION ----
|
||
|
||
Calculator.registerMode('satisfactory', {
|
||
label: '⚙️',
|
||
shortName: 'SAT',
|
||
titleKey: 'calculator.tab.satisfactory',
|
||
|
||
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';
|
||
|
||
createSubTabs(bodyEl, SUB_MODES, _activeSubMode, 'calculator.sat.tab.', (mode) => {
|
||
_activeSubMode = mode;
|
||
renderSubMode(subContent);
|
||
saveState();
|
||
});
|
||
|
||
bodyEl.appendChild(subContent);
|
||
renderSubMode(subContent);
|
||
});
|
||
},
|
||
|
||
destroy() {
|
||
saveState();
|
||
}
|
||
});
|
||
})();
|
||
```
|
||
|
||
- [ ] **Step 2: Script-Tag in newtab.html**
|
||
|
||
Nach dem `calc-converter.js` Script-Tag:
|
||
|
||
```html
|
||
<script src="src/js/calc-satisfactory.js"></script>
|
||
```
|
||
|
||
- [ ] **Step 3: Shared Game-Calculator CSS**
|
||
|
||
In `src/css/main.css`, nach den Converter-Styles:
|
||
|
||
```css
|
||
/* Calculator Game Modes (shared) */
|
||
.calc-game-subtabs {
|
||
display: flex;
|
||
gap: 4px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.calc-game-subtab {
|
||
flex: 1;
|
||
padding: 5px 4px;
|
||
background: rgba(255,255,255,0.04);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
color: var(--text-muted);
|
||
font-size: 10px;
|
||
font-family: 'Rajdhani', sans-serif;
|
||
cursor: pointer;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
transition: all 0.15s;
|
||
}
|
||
.calc-game-subtab:hover {
|
||
color: var(--text-secondary);
|
||
}
|
||
.calc-game-subtab.active {
|
||
background: var(--accent-dim);
|
||
border-color: var(--accent);
|
||
color: var(--accent);
|
||
font-weight: 600;
|
||
}
|
||
.calc-game-field {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 4px;
|
||
}
|
||
.calc-game-field label {
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
min-width: 90px;
|
||
flex-shrink: 0;
|
||
}
|
||
.calc-game-input {
|
||
flex: 1;
|
||
padding: 5px 8px;
|
||
background: rgba(0,0,0,0.3);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
color: var(--text-primary);
|
||
font-size: 13px;
|
||
font-family: 'Rajdhani', monospace;
|
||
}
|
||
.calc-game-output {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 6px 8px;
|
||
background: rgba(0,0,0,0.2);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
margin-top: 4px;
|
||
}
|
||
.calc-game-output span:first-child {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
}
|
||
.calc-game-value {
|
||
font-size: 14px;
|
||
color: var(--accent);
|
||
font-weight: 600;
|
||
font-family: 'Rajdhani', monospace;
|
||
}
|
||
.calc-game-warning {
|
||
font-size: 10px;
|
||
color: var(--danger);
|
||
padding: 2px 0;
|
||
}
|
||
.calc-game-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: i18n-Keys für Satisfactory**
|
||
|
||
DE-Block:
|
||
|
||
```javascript
|
||
'calculator.tab.satisfactory': 'Satisfactory',
|
||
'calculator.sat.tab.itemsPerMin': 'Items/Min',
|
||
'calculator.sat.tab.power': 'Strom',
|
||
'calculator.sat.tab.machines': 'Maschinen',
|
||
'calculator.sat.items_per_craft': 'Items/Craft',
|
||
'calculator.sat.craft_time': 'Craftzeit (s)',
|
||
'calculator.sat.clock_speed': 'Taktrate (%)',
|
||
'calculator.sat.base_power': 'Grundleistung (MW)',
|
||
'calculator.sat.target_output': 'Ziel Output/Min',
|
||
'calculator.sat.output_per_min': 'Output',
|
||
'calculator.sat.power_usage': 'Stromverbrauch',
|
||
'calculator.sat.efficiency': 'Effizienz',
|
||
'calculator.sat.per_item': 'pro Item',
|
||
'calculator.sat.machines_needed': 'Maschinen benötigt',
|
||
'calculator.sat.total_power': 'Gesamtleistung',
|
||
```
|
||
|
||
EN-Block:
|
||
|
||
```javascript
|
||
'calculator.tab.satisfactory': 'Satisfactory',
|
||
'calculator.sat.tab.itemsPerMin': 'Items/Min',
|
||
'calculator.sat.tab.power': 'Power',
|
||
'calculator.sat.tab.machines': 'Machines',
|
||
'calculator.sat.items_per_craft': 'Items/Craft',
|
||
'calculator.sat.craft_time': 'Craft Time (s)',
|
||
'calculator.sat.clock_speed': 'Clock Speed (%)',
|
||
'calculator.sat.base_power': 'Base Power (MW)',
|
||
'calculator.sat.target_output': 'Target Output/Min',
|
||
'calculator.sat.output_per_min': 'Output',
|
||
'calculator.sat.power_usage': 'Power Usage',
|
||
'calculator.sat.efficiency': 'Efficiency',
|
||
'calculator.sat.per_item': 'per item',
|
||
'calculator.sat.machines_needed': 'Machines needed',
|
||
'calculator.sat.total_power': 'Total Power',
|
||
```
|
||
|
||
- [ ] **Step 5: Testen**
|
||
|
||
1. SAT-Tab klicken → 3 Sub-Tabs (Items/Min, Strom, Maschinen)
|
||
2. Items/Min: 1 Item, 4s Craft, 100% → 15 items/min
|
||
3. Power: 30 MW Base, 250% Clock → ~115 MW (wegen Exponent 1.321928)
|
||
4. Machines: 60 Target, 1 Item, 4s, 100%, 30 MW → 4 Maschinen, 120 MW
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add src/js/calc-satisfactory.js newtab.html src/css/main.css src/js/i18n.js
|
||
git commit -m "feat(calculator): Satisfactory Calculator mit Overclock-Power"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: Factorio Calculator
|
||
|
||
**Files:**
|
||
- Create: `src/js/calc-factorio.js`
|
||
- Modify: `newtab.html` (Script-Tag nach calc-satisfactory.js)
|
||
- Modify: `src/js/i18n.js` (neue Keys)
|
||
|
||
**Kontext:** 3 Sub-Modi: Ratio, Belt, Machines. Assembler-Speeds: Asm1=0.5, Asm2=0.75, Asm3=1.25. Belt-Throughput: Yellow=15/s, Red=30/s, Blue=45/s. Nutzt die shared `.calc-game-*` CSS-Klassen aus Task 5.
|
||
|
||
- [ ] **Step 1: calc-factorio.js erstellen**
|
||
|
||
```javascript
|
||
/* =============================================
|
||
HELLION NEWTAB — calc-factorio.js
|
||
Factorio Calculator Modus
|
||
============================================= */
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
const ASSEMBLERS = [
|
||
{ key: 'asm1', speed: 0.5 },
|
||
{ key: 'asm2', speed: 0.75 },
|
||
{ key: 'asm3', speed: 1.25 }
|
||
];
|
||
|
||
const BELTS = [
|
||
{ key: 'yellow', throughput: 15, perSide: 7.5 },
|
||
{ key: 'red', throughput: 30, perSide: 15 },
|
||
{ key: 'blue', throughput: 45, perSide: 22.5 }
|
||
];
|
||
|
||
const SUB_MODES = ['ratio', 'belt', 'machines'];
|
||
let _activeSubMode = 'ratio';
|
||
|
||
/**
|
||
* Assembler-Dropdown erstellen
|
||
* @param {string} selectedKey
|
||
* @returns {{ row: HTMLElement, select: HTMLSelectElement }}
|
||
*/
|
||
function createAssemblerSelect(selectedKey) {
|
||
const row = document.createElement('div');
|
||
row.className = 'calc-game-field';
|
||
|
||
const label = document.createElement('label');
|
||
label.textContent = t('calculator.fac.assembler');
|
||
|
||
const select = document.createElement('select');
|
||
select.className = 'calc-game-input';
|
||
|
||
ASSEMBLERS.forEach(asm => {
|
||
const opt = document.createElement('option');
|
||
opt.value = asm.key;
|
||
opt.textContent = t('calculator.fac.asm.' + asm.key) + ' (' + asm.speed + 'x)';
|
||
if (asm.key === selectedKey) opt.selected = true;
|
||
select.appendChild(opt);
|
||
});
|
||
|
||
row.append(label, select);
|
||
return { row, select };
|
||
}
|
||
|
||
/**
|
||
* Belt-Dropdown erstellen
|
||
* @param {string} selectedKey
|
||
* @returns {{ row: HTMLElement, select: HTMLSelectElement }}
|
||
*/
|
||
function createBeltSelect(selectedKey) {
|
||
const row = document.createElement('div');
|
||
row.className = 'calc-game-field';
|
||
|
||
const label = document.createElement('label');
|
||
label.textContent = t('calculator.fac.belt');
|
||
|
||
const select = document.createElement('select');
|
||
select.className = 'calc-game-input';
|
||
|
||
BELTS.forEach(belt => {
|
||
const opt = document.createElement('option');
|
||
opt.value = belt.key;
|
||
opt.textContent = t('calculator.fac.belt.' + belt.key) + ' (' + belt.throughput + '/s)';
|
||
if (belt.key === selectedKey) opt.selected = true;
|
||
select.appendChild(opt);
|
||
});
|
||
|
||
row.append(label, select);
|
||
return { row, select };
|
||
}
|
||
|
||
function getAssemblerSpeed(key) {
|
||
const asm = ASSEMBLERS.find(a => a.key === key);
|
||
return asm ? asm.speed : 1;
|
||
}
|
||
|
||
function getBelt(key) {
|
||
return BELTS.find(b => b.key === key) || BELTS[0];
|
||
}
|
||
|
||
/**
|
||
* Findet den kleinsten Belt der den Throughput schafft
|
||
* @param {number} throughput
|
||
* @returns {Object|null}
|
||
*/
|
||
function findSmallestBelt(throughput) {
|
||
for (const belt of BELTS) {
|
||
if (belt.throughput >= throughput) return belt;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ---- Field/Output helpers (reuse pattern from Satisfactory) ----
|
||
|
||
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;
|
||
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 };
|
||
}
|
||
|
||
// ---- SUB-MODE RENDERERS ----
|
||
|
||
function renderRatio(container) {
|
||
const asmSelect = createAssemblerSelect('asm3');
|
||
const outputField = createField('calculator.fac.recipe_output', 1, { step: 1, min: 1 });
|
||
const timeField = createField('calculator.fac.recipe_time', 1, { step: 0.1, min: 0.1 });
|
||
|
||
const perSecOutput = createOutput('calculator.fac.items_per_sec');
|
||
const perMinOutput = createOutput('calculator.fac.items_per_min');
|
||
|
||
function calc() {
|
||
const speed = getAssemblerSpeed(asmSelect.select.value);
|
||
const output = parseFloat(outputField.input.value) || 0;
|
||
const time = parseFloat(timeField.input.value) || 1;
|
||
const perSec = output * speed / time;
|
||
const perMin = perSec * 60;
|
||
perSecOutput.value.textContent = Calculator._formatResult(perSec) + ' /s';
|
||
perMinOutput.value.textContent = Calculator._formatResult(perMin) + ' /min';
|
||
}
|
||
|
||
[outputField, timeField].forEach(f => f.input.addEventListener('input', calc));
|
||
asmSelect.select.addEventListener('change', calc);
|
||
|
||
container.append(asmSelect.row, outputField.row, timeField.row, perSecOutput.row, perMinOutput.row);
|
||
calc();
|
||
}
|
||
|
||
function renderBelt(container) {
|
||
const beltSelect = createBeltSelect('yellow');
|
||
const consumeField = createField('calculator.fac.consume_per_sec', 1, { step: 0.1, min: 0.1 });
|
||
|
||
const machinesOutput = createOutput('calculator.fac.machines_per_belt');
|
||
const utilOutput = createOutput('calculator.fac.belt_utilization');
|
||
|
||
function calc() {
|
||
const belt = getBelt(beltSelect.select.value);
|
||
const consume = parseFloat(consumeField.input.value) || 1;
|
||
const machines = Math.floor(belt.throughput / consume);
|
||
const util = (consume * machines) / belt.throughput * 100;
|
||
machinesOutput.value.textContent = machines;
|
||
utilOutput.value.textContent = Calculator._formatResult(util) + '%';
|
||
}
|
||
|
||
consumeField.input.addEventListener('input', calc);
|
||
beltSelect.select.addEventListener('change', calc);
|
||
|
||
container.append(beltSelect.row, consumeField.row, machinesOutput.row, utilOutput.row);
|
||
calc();
|
||
}
|
||
|
||
function renderMachines(container) {
|
||
const asmSelect = createAssemblerSelect('asm3');
|
||
const targetField = createField('calculator.fac.target_output_sec', 10, { step: 0.1, min: 0.1 });
|
||
const outputField = createField('calculator.fac.recipe_output', 1, { step: 1, min: 1 });
|
||
const timeField = createField('calculator.fac.recipe_time', 1, { step: 0.1, min: 0.1 });
|
||
|
||
const machinesOutput = createOutput('calculator.fac.machines_needed');
|
||
const beltOutput = createOutput('calculator.fac.belt_needed');
|
||
|
||
function calc() {
|
||
const speed = getAssemblerSpeed(asmSelect.select.value);
|
||
const target = parseFloat(targetField.input.value) || 0;
|
||
const output = parseFloat(outputField.input.value) || 1;
|
||
const time = parseFloat(timeField.input.value) || 1;
|
||
|
||
const perMachine = output * speed / time;
|
||
const machines = perMachine > 0 ? Math.ceil(target / perMachine) : 0;
|
||
const totalThroughput = machines * perMachine;
|
||
const belt = findSmallestBelt(totalThroughput);
|
||
|
||
machinesOutput.value.textContent = machines;
|
||
if (belt) {
|
||
const util = (totalThroughput / belt.throughput) * 100;
|
||
beltOutput.value.textContent = t('calculator.fac.belt.' + belt.key) + ' (' + Calculator._formatResult(util) + '%)';
|
||
} else {
|
||
beltOutput.value.textContent = t('calculator.fac.exceeds_belt');
|
||
}
|
||
}
|
||
|
||
[targetField, outputField, timeField].forEach(f => f.input.addEventListener('input', calc));
|
||
asmSelect.select.addEventListener('change', calc);
|
||
|
||
container.append(asmSelect.row, targetField.row, outputField.row, timeField.row, machinesOutput.row, beltOutput.row);
|
||
calc();
|
||
}
|
||
|
||
// ---- STATE ----
|
||
|
||
async function loadState() {
|
||
const data = await Store.get(Calculator.STORAGE_KEY);
|
||
if (data && data.calculator && data.calculator.factorio) {
|
||
const s = data.calculator.factorio;
|
||
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.factorio = { lastSubMode: _activeSubMode };
|
||
await Store.set(Calculator.STORAGE_KEY, data);
|
||
}
|
||
|
||
function renderSubMode(container) {
|
||
container.textContent = '';
|
||
switch (_activeSubMode) {
|
||
case 'ratio': renderRatio(container); break;
|
||
case 'belt': renderBelt(container); break;
|
||
case 'machines': renderMachines(container); break;
|
||
}
|
||
}
|
||
|
||
// ---- REGISTRATION ----
|
||
|
||
Calculator.registerMode('factorio', {
|
||
label: '🏭',
|
||
shortName: 'FAC',
|
||
titleKey: 'calculator.tab.factorio',
|
||
|
||
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';
|
||
|
||
// Sub-Tab helper aus Satisfactory ist nicht verfügbar (IIFE-Scope),
|
||
// daher lokale Implementierung
|
||
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.fac.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();
|
||
}
|
||
});
|
||
})();
|
||
```
|
||
|
||
- [ ] **Step 2: Script-Tag in newtab.html**
|
||
|
||
Nach dem `calc-satisfactory.js` Script-Tag:
|
||
|
||
```html
|
||
<script src="src/js/calc-factorio.js"></script>
|
||
```
|
||
|
||
- [ ] **Step 3: i18n-Keys für Factorio**
|
||
|
||
DE-Block:
|
||
|
||
```javascript
|
||
'calculator.tab.factorio': 'Factorio',
|
||
'calculator.fac.tab.ratio': 'Ratio',
|
||
'calculator.fac.tab.belt': 'Belt',
|
||
'calculator.fac.tab.machines': 'Maschinen',
|
||
'calculator.fac.assembler': 'Assembler',
|
||
'calculator.fac.asm.asm1': 'Assembler 1',
|
||
'calculator.fac.asm.asm2': 'Assembler 2',
|
||
'calculator.fac.asm.asm3': 'Assembler 3',
|
||
'calculator.fac.belt': 'Belt-Typ',
|
||
'calculator.fac.belt.yellow': 'Gelb',
|
||
'calculator.fac.belt.red': 'Rot',
|
||
'calculator.fac.belt.blue': 'Blau',
|
||
'calculator.fac.recipe_output': 'Rezept-Output',
|
||
'calculator.fac.recipe_time': 'Rezeptzeit (s)',
|
||
'calculator.fac.consume_per_sec': 'Verbrauch/s',
|
||
'calculator.fac.target_output_sec': 'Ziel Output/s',
|
||
'calculator.fac.items_per_sec': 'Items/s',
|
||
'calculator.fac.items_per_min': 'Items/min',
|
||
'calculator.fac.machines_per_belt': 'Maschinen/Belt',
|
||
'calculator.fac.belt_utilization': 'Belt-Auslastung',
|
||
'calculator.fac.machines_needed': 'Maschinen benötigt',
|
||
'calculator.fac.belt_needed': 'Belt benötigt',
|
||
'calculator.fac.exceeds_belt': 'Übersteigt max. Belt',
|
||
```
|
||
|
||
EN-Block:
|
||
|
||
```javascript
|
||
'calculator.tab.factorio': 'Factorio',
|
||
'calculator.fac.tab.ratio': 'Ratio',
|
||
'calculator.fac.tab.belt': 'Belt',
|
||
'calculator.fac.tab.machines': 'Machines',
|
||
'calculator.fac.assembler': 'Assembler',
|
||
'calculator.fac.asm.asm1': 'Assembler 1',
|
||
'calculator.fac.asm.asm2': 'Assembler 2',
|
||
'calculator.fac.asm.asm3': 'Assembler 3',
|
||
'calculator.fac.belt': 'Belt Type',
|
||
'calculator.fac.belt.yellow': 'Yellow',
|
||
'calculator.fac.belt.red': 'Red',
|
||
'calculator.fac.belt.blue': 'Blue',
|
||
'calculator.fac.recipe_output': 'Recipe Output',
|
||
'calculator.fac.recipe_time': 'Recipe Time (s)',
|
||
'calculator.fac.consume_per_sec': 'Consume/s',
|
||
'calculator.fac.target_output_sec': 'Target Output/s',
|
||
'calculator.fac.items_per_sec': 'Items/s',
|
||
'calculator.fac.items_per_min': 'Items/min',
|
||
'calculator.fac.machines_per_belt': 'Machines/Belt',
|
||
'calculator.fac.belt_utilization': 'Belt Utilization',
|
||
'calculator.fac.machines_needed': 'Machines needed',
|
||
'calculator.fac.belt_needed': 'Belt needed',
|
||
'calculator.fac.exceeds_belt': 'Exceeds max belt',
|
||
```
|
||
|
||
- [ ] **Step 4: Testen**
|
||
|
||
1. FAC-Tab klicken → 3 Sub-Tabs (Ratio, Belt, Maschinen)
|
||
2. Ratio: Asm3 (1.25x), 1 Output, 1s → 1.25/s, 75/min
|
||
3. Belt: Yellow (15/s), 1/s consume → 15 Machines, 100%
|
||
4. Machines: Asm3, 10/s target, 1 output, 1s → 8 machines, Blue (22.2%)
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/js/calc-factorio.js newtab.html src/js/i18n.js
|
||
git commit -m "feat(calculator): Factorio Calculator mit Assembler-Ratios"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: Stationeers Calculator
|
||
|
||
**Files:**
|
||
- Create: `src/js/calc-stationeers.js`
|
||
- Modify: `newtab.html` (Script-Tag nach calc-factorio.js)
|
||
- Modify: `src/js/i18n.js` (neue Keys)
|
||
- Modify: `src/css/main.css` (Stationeers-spezifische Styles)
|
||
|
||
**Kontext:** 4 Sub-Modi: Gas (PV=nRT), Furnace, Solar/Battery, Atmosphere. R=8314.46261815324, Combustion Energy=563452 J/mol. Komplexester Modus. Nutzt shared `.calc-game-*` CSS.
|
||
|
||
- [ ] **Step 1: calc-stationeers.js erstellen**
|
||
|
||
```javascript
|
||
/* =============================================
|
||
HELLION NEWTAB — calc-stationeers.js
|
||
Stationeers Calculator Modus
|
||
============================================= */
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
// ---- CONSTANTS ----
|
||
const R = 8314.46261815324; // L·Pa / (mol·K)
|
||
const COMBUSTION_ENERGY = 563452; // J/mol bei 95% Effizienz
|
||
const HEAT_CAP_PURE_FUEL = 61.9; // J/(mol·K) für 1:2 O₂:H₂
|
||
const HEAT_CAP_DELTA = 172.615; // 0.95 × (243.6 − 61.9)
|
||
const BATTERY_CAPACITY = 50000; // Ws (Station Battery)
|
||
|
||
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';
|
||
|
||
// ---- Field/Output helpers ----
|
||
|
||
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 };
|
||
}
|
||
|
||
// ---- GAS (PV=nRT) ----
|
||
|
||
function renderGas(container) {
|
||
// Solve-for Dropdown
|
||
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);
|
||
|
||
// Eingabefelder für alle 4 Variablen
|
||
const fields = {};
|
||
const units = { P: 'kPa', V: 'L', n: 'mol', T: 'K' };
|
||
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);
|
||
});
|
||
|
||
// Hilfstext für Temperatur
|
||
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;
|
||
|
||
// Felder aktivieren/deaktivieren
|
||
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; // kPa → Pa
|
||
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 = '-';
|
||
}
|
||
|
||
// Temperatur-Hilfstext
|
||
const tempVal = parseFloat(fields.T.input.value) || 0;
|
||
tempHelper.textContent = Calculator._formatResult(tempVal - 273.15) + ' \u00B0C';
|
||
}
|
||
|
||
// Events
|
||
GAS_VARS.forEach(v => {
|
||
fields[v].input.addEventListener('input', calc);
|
||
});
|
||
solveSelect.addEventListener('change', calc);
|
||
calc();
|
||
}
|
||
|
||
// ---- FURNACE ----
|
||
|
||
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();
|
||
}
|
||
|
||
// ---- SOLAR / BATTERY ----
|
||
|
||
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();
|
||
}
|
||
|
||
// ---- ATMOSPHERE / MIXER ----
|
||
|
||
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();
|
||
|
||
// Aufklappbare Wärmekapazität-Referenz
|
||
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);
|
||
}
|
||
|
||
// ---- STATE ----
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
// ---- REGISTRATION ----
|
||
|
||
Calculator.registerMode('stationeers', {
|
||
label: '🚀',
|
||
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();
|
||
}
|
||
});
|
||
})();
|
||
```
|
||
|
||
- [ ] **Step 2: Script-Tag in newtab.html**
|
||
|
||
Nach dem `calc-factorio.js` Script-Tag:
|
||
|
||
```html
|
||
<script src="src/js/calc-stationeers.js"></script>
|
||
```
|
||
|
||
- [ ] **Step 3: CSS für Stationeers-spezifische Elemente**
|
||
|
||
In `src/css/main.css`, nach den Game-Calculator shared Styles:
|
||
|
||
```css
|
||
/* 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);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: i18n-Keys für Stationeers**
|
||
|
||
DE-Block:
|
||
|
||
```javascript
|
||
'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': '⚠ Fuel unter 5%',
|
||
'calculator.sta.warn_low_pressure': '⚠ 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',
|
||
```
|
||
|
||
EN-Block:
|
||
|
||
```javascript
|
||
'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': '⚠ Fuel below 5%',
|
||
'calculator.sta.warn_low_pressure': '⚠ 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',
|
||
```
|
||
|
||
- [ ] **Step 5: Testen**
|
||
|
||
1. STA-Tab klicken → 4 Sub-Tabs (Gas, Ofen, Solar, Atmo)
|
||
2. Gas: P gesucht, V=1000, n=1, T=293.15 → P=2437.66 kPa (Stationeers-R ist groß!)
|
||
3. Furnace: Fuel=0.5, T=293K, P=101kPa → hohe Temperatur nach Zündung
|
||
4. Solar: 12 Panels, 500W, 600s Tag/Nacht, 2000W → 6000W Gen, 4000W Surplus, 24 Batteries
|
||
5. Atmo: Target=300K, Gas1=200K, Gas2=400K → M1=50%, M2=50%
|
||
6. Wärmekapazität-Tabelle aufklappbar
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add src/js/calc-stationeers.js newtab.html src/css/main.css src/js/i18n.js
|
||
git commit -m "feat(calculator): Stationeers Calculator mit Gas/Furnace/Solar/Atmo"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 8: Version Bump und CHANGELOG
|
||
|
||
**Files:**
|
||
- Modify: `manifest.json` — Version → 2.1.0
|
||
- Modify: `manifest.firefox.json` — Version → 2.1.0
|
||
- Modify: `manifest.opera.json` — Version → 2.1.0
|
||
- Modify: `newtab.html` — Version-String → 2.1.0
|
||
- Modify: `src/js/data.js` — Export-Version → 2.1.0
|
||
- Modify: `src/js/app.js` — Backup-Version → 2.1.0
|
||
- Modify: `CHANGELOG.md` — v2.1.0 Eintrag
|
||
|
||
- [ ] **Step 1: Manifests aktualisieren**
|
||
|
||
In allen drei Manifest-Dateien:
|
||
```json
|
||
"version": "2.1.0"
|
||
```
|
||
|
||
- [ ] **Step 2: newtab.html Version-String**
|
||
|
||
Den bestehenden Version-String von "2.0.1" auf "2.1.0" ändern.
|
||
|
||
- [ ] **Step 3: data.js Export-Version**
|
||
|
||
```javascript
|
||
version: '2.1.0',
|
||
```
|
||
|
||
- [ ] **Step 4: app.js Backup-Version**
|
||
|
||
```javascript
|
||
version: '2.1.0',
|
||
```
|
||
|
||
- [ ] **Step 5: CHANGELOG.md Eintrag**
|
||
|
||
Am Anfang der Datei (nach dem Header):
|
||
|
||
```markdown
|
||
## [2.1.0] — 2026-04-16
|
||
|
||
### Added
|
||
- **Calculator Tab-System:** 6 Modi über Tab-Leiste erreichbar (Standard, Scientific, Unit, SAT, FAC, STA)
|
||
- **Scientific-Modus:** Wurzel, Potenz, Pi, Euler, Vorzeichen-Wechsel + Formel-Helfer (Kreis, Pythagoras, Prozent, Temperatur)
|
||
- **Unit-Converter:** 6 Kategorien (Länge, Gewicht, Temperatur, Volumen, Geschwindigkeit, Fläche) mit Live-Konvertierung und Swap
|
||
- **Satisfactory Calculator:** Items/Min, Overclock-Power (Exponent 1.321928), Maschinen-Rechner
|
||
- **Factorio Calculator:** Assembler-Ratios, Belt-Throughput, Maschinen-Rechner mit Belt-Empfehlung
|
||
- **Stationeers Calculator:** Idealgas (PV=nRT), Furnace/Verbrennung, Solar/Batterie-Dimensionierung, Atmosphären-Mixer
|
||
|
||
### Changed
|
||
- Parser um `^` (Potenz, rechts-assoziativ) und `sqrt()` erweitert
|
||
- Calculator-Widget Auto-Resize auf 320×480 für komplexe Modi
|
||
- ~110 neue i18n-Keys (DE + EN)
|
||
```
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add manifest.json manifest.firefox.json manifest.opera.json newtab.html src/js/data.js src/js/app.js CHANGELOG.md
|
||
git commit -m "feat(release): Version auf v2.1.0 bumpen — Calculator Upgrade"
|
||
```
|