diff --git a/.env.example b/.env.example index 17b8657..f3c2e71 100644 --- a/.env.example +++ b/.env.example @@ -86,3 +86,5 @@ tlEditorCmd='auto' # Hotkeys (event.key names, e.g. F9, F10, Control) tlHotkey='F9' forgeHotkey='F10' +# UI scale for in-game overlays: auto (match game width) or a number like 1.5 / 2 +playtestUiScale='auto' diff --git a/gui/workflow_tab.py b/gui/workflow_tab.py index c3d03cc..53a6a83 100644 --- a/gui/workflow_tab.py +++ b/gui/workflow_tab.py @@ -2551,6 +2551,32 @@ class WorkflowTab(QWidget): hotkey_row.addStretch(1) settings_inner.addLayout(hotkey_row) + scale_row = QHBoxLayout() + scale_row.setSpacing(8) + scale_lbl = QLabel("UI scale:") + scale_lbl.setFixedWidth(_PT_LABEL_W) + scale_lbl.setStyleSheet(_PT_LBL_STYLE) + scale_lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + scale_row.addWidget(scale_lbl) + + self._pt_ui_scale_combo = QComboBox() + self._pt_ui_scale_combo.setToolTip( + "In-game overlay size. Auto scales from game resolution and display DPI." + ) + for label, value in ( + ("Auto (match game width)", "auto"), + ("100%", "1"), + ("125%", "1.25"), + ("150%", "1.5"), + ("175%", "1.75"), + ("200%", "2"), + ("225%", "2.25"), + ("250%", "2.5"), + ): + self._pt_ui_scale_combo.addItem(label, value) + scale_row.addWidget(self._pt_ui_scale_combo, 1) + settings_inner.addLayout(scale_row) + editor_title = QLabel("Editor settings") editor_title.setStyleSheet(_PT_SECTION_STYLE + "padding-top:2px;") settings_inner.addWidget(editor_title) @@ -2753,11 +2779,19 @@ class WorkflowTab(QWidget): cfg = { "hotkey": "F9", "forgeHotkey": "F10", + "uiScale": "auto", "editorCmd": "auto", } self._pt_hotkey_edit.setText(cfg.get("hotkey", "F9")) self._pt_forge_hotkey_edit.setText(cfg.get("forgeHotkey", "F10")) + want_scale = str(cfg.get("uiScale", "auto")) + scale_idx = self._pt_ui_scale_combo.findData(want_scale) + if scale_idx >= 0: + self._pt_ui_scale_combo.setCurrentIndex(scale_idx) + else: + custom_idx = self._pt_ui_scale_combo.findData("auto") + self._pt_ui_scale_combo.setCurrentIndex(custom_idx if custom_idx >= 0 else 0) self._populate_tli_editor_combo(select=cfg.get("editorCmd", "auto")) def _resolve_playtest_config(self) -> dict: @@ -2773,6 +2807,7 @@ class WorkflowTab(QWidget): return { "hotkey": self._pt_hotkey_edit.text().strip() or "F9", "forgeHotkey": self._pt_forge_hotkey_edit.text().strip() or "F10", + "uiScale": str(self._pt_ui_scale_combo.currentData() or "auto"), "editorCmd": editor, "workspaceFolder": "auto", } @@ -2785,7 +2820,7 @@ class WorkflowTab(QWidget): self._log( "✅ Playtest settings saved — " f"inspector={cfg['hotkey']}, forge={cfg['forgeHotkey']}, " - f"editor={cfg['editorCmd']}" + f"scale={cfg['uiScale']}, editor={cfg['editorCmd']}" ) except Exception as exc: self._log(f"❌ Could not save playtest settings: {exc}") diff --git a/util/forge/Forge_MZ.js b/util/forge/Forge_MZ.js index e73684e..62398b2 100644 --- a/util/forge/Forge_MZ.js +++ b/util/forge/Forge_MZ.js @@ -32,6 +32,12 @@ * @min 0 * @default 0 * + * @param uiScale + * @text UI Scale + * @desc Overlay size. Use auto to match game width, or a number like 1.5 or 2. + * @type string + * @default auto + * * @command open * @text Open / Toggle Panel * @desc Toggle the Forge overlay. @@ -416,6 +422,18 @@ var LS = 'forge:'; function store(k, v) { try { localStorage.setItem(LS + k, JSON.stringify(v)); } catch (e) {} } function load(k, d) { try { var s = localStorage.getItem(LS + k); return s == null ? d : JSON.parse(s); } catch (e) { return d; } } + function resolveUiScale(v) { + if (v !== 'auto' && v != null && String(v).trim() !== '') { + var n = parseFloat(v); + if (!isNaN(n) && n > 0) return Math.max(0.75, Math.min(3, n)); + } + var base = 816; + var w = (typeof Graphics !== 'undefined' && Graphics.width) ? Graphics.width : (window.innerWidth || base); + var scale = w / base; + var dpr = window.devicePixelRatio || 1; + if (dpr > 1.15) scale *= Math.min(2, 0.75 + dpr * 0.35); + return Math.max(1, Math.min(2.75, scale)); + } // persisted speed settings (UI-controlled; override core/param defaults if the user changed them) API._speedKey = load('speedKey', API._speedKey || 'Control'); API._speedUseKey = load('speedUseKey', true); @@ -524,6 +542,8 @@ input::placeholder{color:var(--fg-text-faint)}\ var shadow = host.attachShadow({ mode: 'open' }); var rootEl = el('div', { id: 'forge-root' }); rootEl.setAttribute('data-theme', load('theme', 'dark')); + var uiScale = resolveUiScale(API._uiScale || 'auto'); + if (uiScale !== 1) rootEl.style.zoom = String(uiScale); shadow.appendChild(el('style', { text: CSS })); shadow.appendChild(rootEl); @@ -1080,6 +1100,7 @@ input::placeholder{color:var(--fg-text-faint)}\ if (window.Forge) { window.Forge._hotkey = (P.hotkey || 'F10').trim(); window.Forge._itemMaxOverride = Number(P.itemMaxOverride) || 0; + window.Forge._uiScale = (P.uiScale || 'auto').trim(); try { if (!localStorage.getItem('forge:speedKey')) window.Forge._speedKey = (P.speedKey || 'Control').trim(); } catch (e) { window.Forge._speedKey = (P.speedKey || 'Control').trim(); } } if (typeof PluginManager !== 'undefined' && PluginManager.registerCommand) { diff --git a/util/forge/config.py b/util/forge/config.py index 880ad02..cec067c 100644 --- a/util/forge/config.py +++ b/util/forge/config.py @@ -22,13 +22,14 @@ def _js_literal(value) -> str: return json.dumps(str(value)) -def plugin_entry(hotkey: str) -> str: +def plugin_entry(hotkey: str, ui_scale: str = "auto") -> str: hk = _js_literal(hotkey.strip() or "F10") + scale = _js_literal(ui_scale.strip() or "auto") return ( f' {{ "name": "Forge_MZ", "status": true, ' f'"description": "Forge — in-game cheat & editor overlay", ' f'"parameters": {{ "hotkey": {hk}, "speedKey": "Control", ' - f'"startOpen": "false", "itemMaxOverride": "0" }} }}' + f'"startOpen": "false", "itemMaxOverride": "0", "uiScale": {scale} }} }}' ) @@ -48,7 +49,18 @@ def _patch_forge_hotkey(forge_text: str, hotkey: str) -> str: def prepare_forge_mz_js(source: Path | None = None, cfg: dict | None = None) -> str: - """Build Forge_MZ.js with the configured toggle hotkey.""" + """Build Forge_MZ.js with the configured toggle hotkey and UI scale.""" src = source or BUNDLED_PLUGIN effective = {**load_config(), **(cfg or {})} - return _patch_forge_hotkey(src.read_text(encoding="utf-8"), effective["forgeHotkey"]) + text = _patch_forge_hotkey(src.read_text(encoding="utf-8"), effective["forgeHotkey"]) + scale = effective.get("uiScale", "auto") + text, n = re.subn( + r"(\* @param uiScale\s*\n(?:\s*\*[^\n]*\n)*?\s*\* @default )auto", + rf"\g<1>{scale}", + text, + count=1, + ) + if n == 0: + raise ValueError("Could not patch @default uiScale in Forge_MZ.js") + text = re.sub(r"\(P\.uiScale \|\| 'auto'\)", f"(P.uiScale || '{scale}')", text) + return text diff --git a/util/forge/installer.py b/util/forge/installer.py index f27b7a1..e89e1a0 100644 --- a/util/forge/installer.py +++ b/util/forge/installer.py @@ -90,7 +90,8 @@ def install(game_root: Path, source_js: Path | None = None, cfg: dict | None = N target.write_text(prepare_forge_mz_js(source_js, cfg), encoding="utf-8") hotkey = (cfg or {}).get("forgeHotkey", "F10") - entry = plugin_entry(hotkey) + ui_scale = (cfg or {}).get("uiScale", "auto") + entry = plugin_entry(hotkey, ui_scale) if not _is_declared(content): idx = content.rfind("];") if idx < 0: diff --git a/util/playtest/config.py b/util/playtest/config.py index a426e2e..4570ddf 100644 --- a/util/playtest/config.py +++ b/util/playtest/config.py @@ -9,11 +9,13 @@ from dotenv import dotenv_values ENV_TL_HOTKEY = "tlHotkey" ENV_FORGE_HOTKEY = "forgeHotkey" +ENV_UI_SCALE = "playtestUiScale" ENV_EDITOR = "tlEditorCmd" DEFAULTS = { "hotkey": "F9", "forgeHotkey": "F10", + "uiScale": "auto", "editorCmd": "auto", "workspaceFolder": "auto", } @@ -28,6 +30,7 @@ def load_config(env_path: Path | None = None) -> dict: for key, env_key in ( ("hotkey", ENV_TL_HOTKEY), ("forgeHotkey", ENV_FORGE_HOTKEY), + ("uiScale", ENV_UI_SCALE), ("editorCmd", ENV_EDITOR), ): val = (env.get(env_key) or "").strip() @@ -42,6 +45,7 @@ def save_config(cfg: dict, env_path: Path | None = None) -> None: updates = { ENV_TL_HOTKEY: cfg.get("hotkey", DEFAULTS["hotkey"]), ENV_FORGE_HOTKEY: cfg.get("forgeHotkey", DEFAULTS["forgeHotkey"]), + ENV_UI_SCALE: cfg.get("uiScale", DEFAULTS["uiScale"]), ENV_EDITOR: cfg.get("editorCmd", DEFAULTS["editorCmd"]), } for key, val in updates.items(): diff --git a/util/tl_inspector/TLInspector.js b/util/tl_inspector/TLInspector.js index 9e562b7..8c7b2b0 100644 --- a/util/tl_inspector/TLInspector.js +++ b/util/tl_inspector/TLInspector.js @@ -72,11 +72,44 @@ historySize: 80, // how many past text lines to keep captureUiText: true, // capture plugin / menu / UI text drawn via Bitmap.drawText // (so e.g. status-window strings from plugins.js are locatable) - dataDirOverride: null // set an absolute path to force the data dir + dataDirOverride: null, // set an absolute path to force the data dir + uiScale: 'auto' // overlay scale: 'auto' from game width, or a number (1.5, 2, …) }; if (!CFG.enabled) { return; } + function resolveUiScale(v) { + if (v !== 'auto' && v != null && String(v).trim() !== '') { + var n = parseFloat(v); + if (!isNaN(n) && n > 0) { return Math.max(0.75, Math.min(3, n)); } + } + var base = 816; + var w = (typeof Graphics !== 'undefined' && Graphics.width) ? Graphics.width : + (window.innerWidth || base); + var scale = w / base; + var dpr = window.devicePixelRatio || 1; + if (dpr > 1.15) { scale *= Math.min(2, 0.75 + dpr * 0.35); } + return Math.max(1, Math.min(2.75, scale)); + } + + function currentUiScale() { + return resolveUiScale(CFG.uiScale); + } + + function applyOverlayScale() { + var s = currentUiScale(); + var z = (s === 1) ? '' : String(s); + if (ui.root) { + ui.root.style.zoom = z; + ui.root.style.transformOrigin = (ui.side === 'left') ? 'top left' : 'top right'; + } + if (editor.root) { + editor.root.style.zoom = z; + editor.root.style.transformOrigin = 'top center'; + } + if (ui.pickLabel) { ui.pickLabel.style.zoom = z; } + } + //========================================================================= // Node / environment bootstrap (NW.js exposes require) //========================================================================= @@ -1339,6 +1372,7 @@ editor.tabsEl = tabs; editor.findEl = find; editor.statusEl = foot.querySelector('.tl-ed-status'); + applyOverlayScale(); } //--- documents / tabs ---------------------------------------------------- @@ -1781,6 +1815,7 @@ ui.root.style.left = left ? '0' : 'auto'; ui.root.style.right = left ? 'auto' : '0'; ui.root.style.boxShadow = (left ? '2px' : '-2px') + ' 0 12px rgba(0,0,0,0.6)'; + applyOverlayScale(); } function flipSide() { @@ -1941,6 +1976,7 @@ hlz.style.cssText = 'position:fixed;left:0;top:0;z-index:2147483645;pointer-events:none;display:none'; document.body.appendChild(hlz); ui.hlLayer = hlz; + applyOverlayScale(); } function setTabStyles() { diff --git a/util/tl_inspector/config.py b/util/tl_inspector/config.py index b026764..48d60f0 100644 --- a/util/tl_inspector/config.py +++ b/util/tl_inspector/config.py @@ -12,12 +12,13 @@ from util.playtest.config import load_config as _load_playtest_config, save_conf ENV_EDITOR = "tlEditorCmd" -CFG_KEYS = ("editorCmd", "workspaceFolder", "hotkey") +CFG_KEYS = ("editorCmd", "workspaceFolder", "hotkey", "uiScale") DEFAULTS = { "editorCmd": "auto", "workspaceFolder": "auto", "hotkey": "F9", + "uiScale": "auto", } _PKG_ROOT = Path(__file__).resolve().parent