From 8d0299af5d0f4c03266356c9eaaa858acde5b918 Mon Sep 17 00:00:00 2001 From: Booga Date: Wed, 27 May 2026 21:06:58 -0500 Subject: [PATCH 1/7] fix(KHT-118): hide module settings from non-GM players --- src/settings.ts | 23 ++++++++++++++++++++--- tests/settings.test.ts | 24 ++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/settings.ts b/src/settings.ts index 5ab258e..6254de9 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -2,12 +2,29 @@ import { MODULE_ID } from './constants'; export const ENABLE_MONTE_CARLO_SETTING = 'enableMonteCarlo'; +/** + * True only when the current user is the GM. Used to gate the module's + * config UI so players see no Grim Arithmetic options (KHT-118). Returns + * false when game/user are unavailable (test env, pre-init), so settings + * default to hidden rather than leaking to players. + */ +export function isSettingsConfigVisible(): boolean { + if (typeof game === 'undefined') return false; + try { + return (game as { user?: { isGM?: boolean } }).user?.isGM === true; + } catch { + return false; + } +} + export function registerSettings(): void { + const config = isSettingsConfigVisible(); + game.settings.register(MODULE_ID, 'defaultStrikes', { name: 'GrimArithmetic.Settings.DefaultStrikes.Name', hint: 'GrimArithmetic.Settings.DefaultStrikes.Hint', scope: 'world', - config: true, + config, type: Number, default: 2, choices: { @@ -21,7 +38,7 @@ export function registerSettings(): void { name: 'GrimArithmetic.Settings.DebugLogging.Name', hint: 'GrimArithmetic.Settings.DebugLogging.Hint', scope: 'client', - config: true, + config, type: Boolean, default: false }); @@ -30,7 +47,7 @@ export function registerSettings(): void { name: 'GrimArithmetic.Settings.EnableMonteCarlo.Name', hint: 'GrimArithmetic.Settings.EnableMonteCarlo.Hint', scope: 'client', - config: true, + config, type: Boolean, default: true }); diff --git a/tests/settings.test.ts b/tests/settings.test.ts index 9895036..c4d3659 100644 --- a/tests/settings.test.ts +++ b/tests/settings.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'vitest'; -import { ENABLE_MONTE_CARLO_SETTING, isMonteCarloEnabled } from '../src/settings'; +import { afterEach, describe, expect, it } from 'vitest'; +import { ENABLE_MONTE_CARLO_SETTING, isMonteCarloEnabled, isSettingsConfigVisible } from '../src/settings'; describe('settings: enableMonteCarlo', () => { it('exposes the setting key as a stable constant', () => { @@ -12,3 +12,23 @@ describe('settings: enableMonteCarlo', () => { expect(isMonteCarloEnabled()).toBe(true); }); }); + +describe('settings: isSettingsConfigVisible (KHT-118)', () => { + afterEach(() => { + delete (globalThis as { game?: unknown }).game; + }); + + it('defaults to hidden outside Foundry (test env, no game)', () => { + expect(isSettingsConfigVisible()).toBe(false); + }); + + it('is hidden for a non-GM user', () => { + (globalThis as { game?: unknown }).game = { user: { isGM: false } }; + expect(isSettingsConfigVisible()).toBe(false); + }); + + it('is visible for a GM user', () => { + (globalThis as { game?: unknown }).game = { user: { isGM: true } }; + expect(isSettingsConfigVisible()).toBe(true); + }); +}); From a97972e3b392c1c9f1c2324e4be469e56e4f81f2 Mon Sep 17 00:00:00 2001 From: Booga Date: Wed, 27 May 2026 21:09:32 -0500 Subject: [PATCH 2/7] fix(KHT-118): clarify GM setting hints --- lang/en.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lang/en.json b/lang/en.json index ea28ed3..7466fc3 100644 --- a/lang/en.json +++ b/lang/en.json @@ -3,7 +3,7 @@ "Settings": { "DefaultStrikes": { "Name": "Default enemy Strike count", - "Hint": "Default number of Strikes used for immediate-threat estimates.", + "Hint": "How many Strikes each enemy is assumed to make per round when estimating immediate knock-out threat on the Encounter Danger Board.", "Choices": { "1": "1 Strike", "2": "2 Strikes", @@ -12,11 +12,11 @@ }, "DebugLogging": { "Name": "Debug logging", - "Hint": "Log Grim Arithmetic debug information to the browser console." + "Hint": "Write Grim Arithmetic diagnostic messages to the browser console (F12). Leave off unless troubleshooting." }, "EnableMonteCarlo": { "Name": "Enable Monte Carlo encounter simulation", - "Hint": "Disable on low-end machines if simulation runs are too slow. The Encounter Danger Board still works either way." + "Hint": "Turn on the Encounter Forecast's full-fight simulation. Disable on low-end machines if runs are too slow — the Encounter Danger Board still works either way." } }, "Window": { From b1bb0288a45fc3d545ac08c69ec415aa89a6c878 Mon Sep 17 00:00:00 2001 From: Booga Date: Wed, 27 May 2026 21:12:30 -0500 Subject: [PATCH 3/7] feat(KHT-120): lengthen tooltip hover delay to 750ms --- src/main.ts | 3 +++ src/ui/tooltip-delay.ts | 20 ++++++++++++++++++++ tests/tooltip-delay.test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 src/ui/tooltip-delay.ts create mode 100644 tests/tooltip-delay.test.ts diff --git a/src/main.ts b/src/main.ts index 8a05ca0..12292e0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { registerSettings } from './settings'; import { DangerBoardPanel } from './ui/danger-board-panel'; import { ForecastPanel } from './ui/forecast-panel'; import { PairDetailPanel } from './ui/pair-detail-panel'; +import { applyTooltipDelay } from './ui/tooltip-delay'; import { registerTokenControls } from './ui/token-controls'; Hooks.once('init', () => { @@ -22,6 +23,8 @@ function registerHandlebarsHelpers(): void { } Hooks.once('ready', () => { + applyTooltipDelay(750); + if (!game.user?.isGM) return; const grimArithmeticModule = game.modules.get(MODULE_ID); diff --git a/src/ui/tooltip-delay.ts b/src/ui/tooltip-delay.ts new file mode 100644 index 0000000..4b88cc9 --- /dev/null +++ b/src/ui/tooltip-delay.ts @@ -0,0 +1,20 @@ +/** + * Lengthen the delay before Foundry's native `data-tooltip` help appears, + * so tooltips don't fire continuously while scanning down a list of values + * (KHT-120). Foundry reads `TOOLTIP_ACTIVATION_MS` off the tooltip manager + * class at hover time, so overriding it affects all subsequent hovers. + * + * `game.tooltip.constructor` is the live class regardless of v13+ namespace + * relocation. No-ops safely when the manager is unavailable (pre-ready, or + * the test environment where `game` is undefined). + */ +export function applyTooltipDelay(ms: number): void { + if (typeof game === 'undefined') return; + try { + const manager = (game as { tooltip?: { constructor?: { TOOLTIP_ACTIVATION_MS?: number } } }).tooltip; + const managerClass = manager?.constructor; + if (managerClass) managerClass.TOOLTIP_ACTIVATION_MS = ms; + } catch { + /* tooltip manager unavailable; leave the default delay in place */ + } +} diff --git a/tests/tooltip-delay.test.ts b/tests/tooltip-delay.test.ts new file mode 100644 index 0000000..c89683b --- /dev/null +++ b/tests/tooltip-delay.test.ts @@ -0,0 +1,24 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { applyTooltipDelay } from '../src/ui/tooltip-delay'; + +afterEach(() => { + delete (globalThis as { game?: unknown }).game; +}); + +describe('applyTooltipDelay (KHT-120)', () => { + it('sets TOOLTIP_ACTIVATION_MS on the live tooltip manager class', () => { + const fakeClass: { TOOLTIP_ACTIVATION_MS?: number } = {}; + (globalThis as { game?: unknown }).game = { tooltip: { constructor: fakeClass } }; + applyTooltipDelay(750); + expect(fakeClass.TOOLTIP_ACTIVATION_MS).toBe(750); + }); + + it('does nothing and does not throw when no tooltip manager exists', () => { + expect(() => applyTooltipDelay(750)).not.toThrow(); + }); + + it('does nothing when game exists but tooltip manager is absent', () => { + (globalThis as { game?: unknown }).game = {}; + expect(() => applyTooltipDelay(750)).not.toThrow(); + }); +}); From ce2e75dd2b83abe28649f8713c16a20c8d22726b Mon Sep 17 00:00:00 2001 From: Booga Date: Wed, 27 May 2026 22:03:07 -0500 Subject: [PATCH 4/7] chore(release): v0.7.2-rc1 --- CHANGELOG.md | 8 ++++++++ module.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b88d6d..b3790cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to Grim Arithmetic are documented here. +## v0.7.2-rc1 - Player-safe options and calmer tooltips + +Release candidate. Two usability fixes on top of v0.7.1; no changes to the underlying mortality math. + +### Usability +- The Grim Arithmetic settings section is now GM-only — regular players no longer see any module options (KHT-118). The GM-facing options carry clearer descriptions of what each one does. +- Hover help across all three windows now waits a beat (750ms) before appearing, so tooltips no longer flicker continuously while scanning down a list of values (KHT-120). + ## v0.7.1 - Localization, tooltips, and tactics clarity Builds on v0.7.0's ApplicationV2 migration with internationalization groundwork, on-hover help across every window, clearer tactics options, and a real CI/release pipeline. No changes to the underlying mortality math. diff --git a/module.json b/module.json index 3a2e5b9..73c5d4a 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "id": "grim-arithmetic", "title": "Grim Arithmetic", "description": "GM-facing PF2e mortality and encounter-risk analysis for Foundry VTT.", - "version": "0.7.1", + "version": "0.7.2-rc1", "authors": [ { "name": "Kyle Travis", @@ -36,5 +36,5 @@ ], "url": "https://github.com/kyletravis/grim-arithmetic", "manifest": "https://github.com/kyletravis/grim-arithmetic/releases/latest/download/module.json", - "download": "https://github.com/kyletravis/grim-arithmetic/releases/download/v0.7.1/grim-arithmetic-v0.7.1.zip" + "download": "https://github.com/kyletravis/grim-arithmetic/releases/download/v0.7.2-rc1/grim-arithmetic-v0.7.2-rc1.zip" } diff --git a/package.json b/package.json index 796d8e1..b78ec9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grim-arithmetic", - "version": "0.7.1", + "version": "0.7.2-rc1", "description": "Foundry VTT module for GM-facing PF2e mortality and encounter-risk analysis.", "type": "module", "private": true, From 6368023e47f8c34bcb4eccb2c392e13de45cf345 Mon Sep 17 00:00:00 2001 From: Booga Date: Wed, 27 May 2026 22:45:33 -0500 Subject: [PATCH 5/7] fix(KHT-118): register settings config:true, hide from players at ready --- src/main.ts | 7 +++++- src/settings.ts | 51 +++++++++++++++++++++++++----------------- tests/settings.test.ts | 45 ++++++++++++++++++++++++++----------- 3 files changed, 69 insertions(+), 34 deletions(-) diff --git a/src/main.ts b/src/main.ts index 12292e0..29c777f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { MODULE_ID, MODULE_TITLE } from './constants'; import { logDebugCapture } from './debug-capture'; -import { registerSettings } from './settings'; +import { registerSettings, applyPlayerSettingsVisibility, type SettingsRegistry } from './settings'; import { DangerBoardPanel } from './ui/danger-board-panel'; import { ForecastPanel } from './ui/forecast-panel'; import { PairDetailPanel } from './ui/pair-detail-panel'; @@ -24,6 +24,11 @@ function registerHandlebarsHelpers(): void { Hooks.once('ready', () => { applyTooltipDelay(750); + // Runs before the GM guard below: players must reach this to have their settings hidden. + applyPlayerSettingsVisibility( + (game.settings as { settings?: SettingsRegistry } | undefined)?.settings, + game.user?.isGM === true + ); if (!game.user?.isGM) return; diff --git a/src/settings.ts b/src/settings.ts index 6254de9..74629a0 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -2,29 +2,12 @@ import { MODULE_ID } from './constants'; export const ENABLE_MONTE_CARLO_SETTING = 'enableMonteCarlo'; -/** - * True only when the current user is the GM. Used to gate the module's - * config UI so players see no Grim Arithmetic options (KHT-118). Returns - * false when game/user are unavailable (test env, pre-init), so settings - * default to hidden rather than leaking to players. - */ -export function isSettingsConfigVisible(): boolean { - if (typeof game === 'undefined') return false; - try { - return (game as { user?: { isGM?: boolean } }).user?.isGM === true; - } catch { - return false; - } -} - export function registerSettings(): void { - const config = isSettingsConfigVisible(); - game.settings.register(MODULE_ID, 'defaultStrikes', { name: 'GrimArithmetic.Settings.DefaultStrikes.Name', hint: 'GrimArithmetic.Settings.DefaultStrikes.Hint', scope: 'world', - config, + config: true, type: Number, default: 2, choices: { @@ -38,7 +21,7 @@ export function registerSettings(): void { name: 'GrimArithmetic.Settings.DebugLogging.Name', hint: 'GrimArithmetic.Settings.DebugLogging.Hint', scope: 'client', - config, + config: true, type: Boolean, default: false }); @@ -47,12 +30,40 @@ export function registerSettings(): void { name: 'GrimArithmetic.Settings.EnableMonteCarlo.Name', hint: 'GrimArithmetic.Settings.EnableMonteCarlo.Hint', scope: 'client', - config, + config: true, type: Boolean, default: true }); } +const GRIM_SETTING_KEYS = ['defaultStrikes', 'debugLogging', ENABLE_MONTE_CARLO_SETTING] as const; + +export interface SettingsRegistry { + get(key: string): { config?: boolean } | undefined; +} + +/** + * Hide every Grim Arithmetic setting from non-GM players (KHT-118). + * + * Settings must be registered during `init`, but `game.user` is not populated + * until later — so registration can't know who the GM is. We register with + * `config: true` and, once `game.user.isGM` is known (the `ready` hook), flip + * the `config` flag to false for non-GMs. Foundry's settings menu reads each + * registration's `config` flag at render time and omits a module's heading + * when it has no visible settings, so this removes the entire Grim Arithmetic + * section for players — heading included. No-op for the GM. + */ +export function applyPlayerSettingsVisibility( + registry: SettingsRegistry | undefined | null, + isGM: boolean +): void { + if (isGM || !registry) return; + for (const key of GRIM_SETTING_KEYS) { + const registration = registry.get(`${MODULE_ID}.${key}`); + if (registration) registration.config = false; + } +} + /** * True when the Monte Carlo simulation feature is enabled for this client. * diff --git a/tests/settings.test.ts b/tests/settings.test.ts index c4d3659..5cae49a 100644 --- a/tests/settings.test.ts +++ b/tests/settings.test.ts @@ -1,5 +1,5 @@ -import { afterEach, describe, expect, it } from 'vitest'; -import { ENABLE_MONTE_CARLO_SETTING, isMonteCarloEnabled, isSettingsConfigVisible } from '../src/settings'; +import { describe, expect, it } from 'vitest'; +import { ENABLE_MONTE_CARLO_SETTING, isMonteCarloEnabled, applyPlayerSettingsVisibility } from '../src/settings'; describe('settings: enableMonteCarlo', () => { it('exposes the setting key as a stable constant', () => { @@ -13,22 +13,41 @@ describe('settings: enableMonteCarlo', () => { }); }); -describe('settings: isSettingsConfigVisible (KHT-118)', () => { - afterEach(() => { - delete (globalThis as { game?: unknown }).game; +describe('settings: applyPlayerSettingsVisibility (KHT-118)', () => { + const KEYS = ['grim-arithmetic.defaultStrikes', 'grim-arithmetic.debugLogging', 'grim-arithmetic.enableMonteCarlo']; + + function makeRegistry(): Map { + return new Map([ + ['grim-arithmetic.defaultStrikes', { config: true }], + ['grim-arithmetic.debugLogging', { config: true }], + ['grim-arithmetic.enableMonteCarlo', { config: true }], + ['core.someCoreSetting', { config: true }] + ]); + } + + it('leaves every setting visible for the GM', () => { + const registry = makeRegistry(); + applyPlayerSettingsVisibility(registry, true); + for (const key of KEYS) expect(registry.get(key)!.config).toBe(true); + }); + + it('hides all three Grim Arithmetic settings from non-GM players', () => { + const registry = makeRegistry(); + applyPlayerSettingsVisibility(registry, false); + for (const key of KEYS) expect(registry.get(key)!.config).toBe(false); }); - it('defaults to hidden outside Foundry (test env, no game)', () => { - expect(isSettingsConfigVisible()).toBe(false); + it("does not touch other modules' settings", () => { + const registry = makeRegistry(); + applyPlayerSettingsVisibility(registry, false); + expect(registry.get('core.someCoreSetting')!.config).toBe(true); }); - it('is hidden for a non-GM user', () => { - (globalThis as { game?: unknown }).game = { user: { isGM: false } }; - expect(isSettingsConfigVisible()).toBe(false); + it('is a no-op when the registry is missing', () => { + expect(() => applyPlayerSettingsVisibility(undefined, false)).not.toThrow(); }); - it('is visible for a GM user', () => { - (globalThis as { game?: unknown }).game = { user: { isGM: true } }; - expect(isSettingsConfigVisible()).toBe(true); + it('is a no-op when the registry is null', () => { + expect(() => applyPlayerSettingsVisibility(null, false)).not.toThrow(); }); }); From 50854ec7f7bd92a3557a2dec88535410acc7cbd6 Mon Sep 17 00:00:00 2001 From: Booga Date: Wed, 27 May 2026 22:57:28 -0500 Subject: [PATCH 6/7] =?UTF-8?q?feat(KHT-121):=20expand=20risk=20pill=20too?= =?UTF-8?q?ltip=20to=20list=20Low=E2=80=93Grim=20ladder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lang/en.json | 3 ++- templates/danger-board-panel.hbs | 4 ++-- templates/forecast-panel.hbs | 2 +- tests/risk-tooltip.test.ts | 24 ++++++++++++++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 tests/risk-tooltip.test.ts diff --git a/lang/en.json b/lang/en.json index 7466fc3..f002c07 100644 --- a/lang/en.json +++ b/lang/en.json @@ -28,7 +28,8 @@ "Detail": "Detail", "Vs": "vs", "DownTooltip": "Chance this PC is knocked out in one round by this attack.", - "RiskPillTooltip": "Risk rating from chance of being downed: Low → Grim.", + "RiskPillTooltip": "Risk of being downed this round, lowest to highest: Low, Guarded, Dangerous, Severe, Grim.", + "RiskPillTooltipHtml": "Risk of being downed this round:
Low — below 5%
Guarded — 5–15%
Dangerous — 15–35%
Severe — 35–60%
Grim — 60% or more", "CiTooltip": "95% confidence interval — the range the true value likely falls in given sampling variance." }, "DangerBoard": { diff --git a/templates/danger-board-panel.hbs b/templates/danger-board-panel.hbs index 319f66e..3dc09cb 100644 --- a/templates/danger-board-panel.hbs +++ b/templates/danger-board-panel.hbs @@ -35,7 +35,7 @@ {{enemyName}} {{attackName}} {{downPercent}}% - {{riskLabel}} + {{riskLabel}}