Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to Grim Arithmetic are documented here.

## v0.7.2-rc1 - Player-safe options and calmer tooltips

Release candidate. Three 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).
- The risk-rating tooltip now spells out the full ladder — Low, Guarded, Dangerous, Severe, Grim, with the chance-of-being-downed band for each — instead of just "Low → Grim" (KHT-121).

## 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.
Expand Down
9 changes: 5 additions & 4 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": {
Expand All @@ -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:<br>Low — below 5%<br>Guarded — 5–15%<br>Dangerous — 15–35%<br>Severe — 35–60%<br>Grim — 60% or more",
"CiTooltip": "95% confidence interval — the range the true value likely falls in given sampling variance."
},
"DangerBoard": {
Expand Down
4 changes: 2 additions & 2 deletions module.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
10 changes: 9 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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';
import { applyTooltipDelay } from './ui/tooltip-delay';
import { registerTokenControls } from './ui/token-controls';

Hooks.once('init', () => {
Expand All @@ -22,6 +23,13 @@ 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;

const grimArithmeticModule = game.modules.get(MODULE_ID);
Expand Down
28 changes: 28 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,34 @@ export function registerSettings(): void {
});
}

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.
*
Expand Down
20 changes: 20 additions & 0 deletions src/ui/tooltip-delay.ts
Original file line number Diff line number Diff line change
@@ -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 */
}
}
4 changes: 2 additions & 2 deletions templates/danger-board-panel.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
{{enemyName}} <em>{{attackName}}</em>
</span>
<span class="grim-arithmetic-panel__danger-row-percent" data-tooltip="{{localize "GrimArithmetic.Common.DownTooltip"}}">{{downPercent}}%</span>
<span class="grim-arithmetic-panel__risk-pill grim-arithmetic-panel__risk-pill--{{riskClass}}" data-tooltip="{{localize "GrimArithmetic.Common.RiskPillTooltip"}}">{{riskLabel}}</span>
<span class="grim-arithmetic-panel__risk-pill grim-arithmetic-panel__risk-pill--{{riskClass}}" data-tooltip="{{localize "GrimArithmetic.Common.RiskPillTooltip"}}" data-tooltip-html="{{localize "GrimArithmetic.Common.RiskPillTooltipHtml"}}">{{riskLabel}}</span>
<button type="button"
class="grim-arithmetic-panel__danger-row-action"
data-action="openDetailPair"
Expand All @@ -57,7 +57,7 @@
{{pcName}}
</span>
<span class="grim-arithmetic-panel__danger-row-percent" data-tooltip="{{localize "GrimArithmetic.Common.DownTooltip"}}">{{downPercent}}%</span>
<span class="grim-arithmetic-panel__risk-pill grim-arithmetic-panel__risk-pill--{{riskClass}}" data-tooltip="{{localize "GrimArithmetic.Common.RiskPillTooltip"}}">{{riskLabel}}</span>
<span class="grim-arithmetic-panel__risk-pill grim-arithmetic-panel__risk-pill--{{riskClass}}" data-tooltip="{{localize "GrimArithmetic.Common.RiskPillTooltip"}}" data-tooltip-html="{{localize "GrimArithmetic.Common.RiskPillTooltipHtml"}}">{{riskLabel}}</span>
<button type="button"
class="grim-arithmetic-panel__danger-row-action"
data-action="openDetailPair"
Expand Down
2 changes: 1 addition & 1 deletion templates/forecast-panel.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
<td>{{deathPercent}}%{{#if deathCi}} <span class="grim-arithmetic-forecast__ci" data-tooltip="{{localize "GrimArithmetic.Common.CiTooltip"}}">[{{deathCi}}]</span>{{/if}}</td>
<td>{{meanEndingHp}}</td>
<td>{{topContributingEnemyName}}</td>
<td><span class="grim-arithmetic-panel__risk-pill grim-arithmetic-panel__risk-pill--{{riskClass}}" data-tooltip="{{localize "GrimArithmetic.Common.RiskPillTooltip"}}">{{riskLabel}}</span></td>
<td><span class="grim-arithmetic-panel__risk-pill grim-arithmetic-panel__risk-pill--{{riskClass}}" data-tooltip="{{localize "GrimArithmetic.Common.RiskPillTooltip"}}" data-tooltip-html="{{localize "GrimArithmetic.Common.RiskPillTooltipHtml"}}">{{riskLabel}}</span></td>
</tr>
{{/each}}
</tbody>
Expand Down
24 changes: 24 additions & 0 deletions tests/risk-tooltip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

const lang = JSON.parse(readFileSync(join(process.cwd(), 'lang/en.json'), 'utf8')) as {
GrimArithmetic: { Common: Record<string, string> };
};
const common = lang.GrimArithmetic.Common;
const TIERS = ['Low', 'Guarded', 'Dangerous', 'Severe', 'Grim'];

describe('KHT-121: risk pill tooltip lists the full ladder', () => {
it('RiskPillTooltipHtml is multi-line HTML listing all five tiers', () => {
const html = common.RiskPillTooltipHtml;
expect(html).toBeDefined();
expect(html).toContain('<br>');
for (const tier of TIERS) expect(html).toContain(tier);
expect(html).not.toMatch(/<(?!br>)/); // no stray tags besides <br>
});

it('RiskPillTooltip plain fallback lists all five tiers', () => {
const text = common.RiskPillTooltip;
for (const tier of TIERS) expect(text).toContain(tier);
});
});
41 changes: 40 additions & 1 deletion tests/settings.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { ENABLE_MONTE_CARLO_SETTING, isMonteCarloEnabled } from '../src/settings';
import { ENABLE_MONTE_CARLO_SETTING, isMonteCarloEnabled, applyPlayerSettingsVisibility } from '../src/settings';

describe('settings: enableMonteCarlo', () => {
it('exposes the setting key as a stable constant', () => {
Expand All @@ -12,3 +12,42 @@ describe('settings: enableMonteCarlo', () => {
expect(isMonteCarloEnabled()).toBe(true);
});
});

describe('settings: applyPlayerSettingsVisibility (KHT-118)', () => {
const KEYS = ['grim-arithmetic.defaultStrikes', 'grim-arithmetic.debugLogging', 'grim-arithmetic.enableMonteCarlo'];

function makeRegistry(): Map<string, { config: boolean }> {
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("does not touch other modules' settings", () => {
const registry = makeRegistry();
applyPlayerSettingsVisibility(registry, false);
expect(registry.get('core.someCoreSetting')!.config).toBe(true);
});

it('is a no-op when the registry is missing', () => {
expect(() => applyPlayerSettingsVisibility(undefined, false)).not.toThrow();
});

it('is a no-op when the registry is null', () => {
expect(() => applyPlayerSettingsVisibility(null, false)).not.toThrow();
});
});
24 changes: 24 additions & 0 deletions tests/tooltip-delay.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});