diff --git a/client/css/style.css b/client/css/style.css index 4c2c04e..f83f529 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1362,6 +1362,24 @@ input:focus, select:focus { border-color: var(--accent); } box-shadow: 0 6px 18px rgba(0, 0, 0, 0.6); } +/* A resource chip's tooltip: production above consumption, each row + right-aligned. One grid holds both rows so the sign and figure columns line + up; the figure column reserves the width of the largest pool figure + (99 999 999 999), so the bubble never shifts as a stock moves. */ +.resource-tip { + display: grid; + grid-template-columns: auto auto auto; + justify-content: end; + column-gap: 0.35em; + row-gap: 2px; + font-variant-numeric: tabular-nums; +} +.resource-tip .rt-sign { text-align: right; font-weight: 700; } +.resource-tip .rt-figure { text-align: right; min-width: 14ch; } +.resource-tip .rt-tail { text-align: left; } +.resource-tip .rt-sign.produce, .resource-tip .rt-figure.produce { color: #6ee787; } +.resource-tip .rt-sign.consume, .resource-tip .rt-figure.consume { color: #f27272; } + /* Right-click menu for the units sharing a tile. */ .stack-menu { position: absolute; diff --git a/client/js/devlog_data.js b/client/js/devlog_data.js index 087b370..166b95d 100644 --- a/client/js/devlog_data.js +++ b/client/js/devlog_data.js @@ -1,6 +1,12 @@ // Generated by scripts/generate-devlog.js from `git log`; do not edit. // The pre-commit hook refreshes it so the main menu shows the latest commits. export const DEVLOG = [ + { + "hash": "0a1c2d0", + "date": "2026-09-25", + "subject": "Guaranteed the 45-second starter build even on an unpowered map", + "body": "" + }, { "hash": "80d2e22", "date": "2026-09-25", @@ -54,11 +60,5 @@ export const DEVLOG = [ "date": "2026-09-24", "subject": "Grouped the unit stack by type and let ranged units pick a target", "body": "" - }, - { - "hash": "1c6f0ae", - "date": "2026-09-24", - "subject": "Cut the per-broadcast and per-frame cost on a full-sized map", - "body": "The server sent the whole army on every snapshot and the client re-cloned\nthe whole icon layer whenever a unit moved, so a settled world with a\nstanding army drained the broadcast loop and the frame loop alike.\n\n- Ship only the units and city list that changed, keyed by a per-peer\n fingerprint and a serialised hash, and merge those deltas on the client.\n- Memoise the battlefield scan on a battle version bumped when a unit\n joins, leaves or changes side.\n- Skip the DOM writes for units whose snapshot object is unchanged, and\n cache the unit-view inner nodes.\n- Fold units, cities and tile markers onto the nearest world copy instead\n of mirroring the icon layers, which re-cloned thousands of views every\n frame; the small decoration layers still mirror, and only on a change.\n\nFrame work at full zoom-out with units moving fell from ~190ms to ~6.5ms." } ]; diff --git a/client/js/game_screen/panels.js b/client/js/game_screen/panels.js index 29b2dcc..b8dddcc 100644 --- a/client/js/game_screen/panels.js +++ b/client/js/game_screen/panels.js @@ -163,11 +163,13 @@ export const panelMethods = { }, // A small hover tooltip anchored above a stat, fed by its data-tooltip text. + // A top-panel resource chip is the exception: its tooltip is a two-line bill, + // production above consumption, both right-aligned. _showStatTooltip($stat) { - const text = $stat.attr("data-tooltip"); - if (!text) return; + const content = this._statTooltipContent($stat); + if (content === null) return; this._hoveredStat = $stat; - this.$statTooltip.text(text).removeClass("hidden"); + this.$statTooltip.empty().append(content).removeClass("hidden"); const offset = $stat.offset() || { left: 0, top: 0 }; const width = this.$statTooltip.outerWidth() || 0; const height = this.$statTooltip.outerHeight() || 0; @@ -177,6 +179,50 @@ export const panelMethods = { }); }, + // The body of a stat tooltip: the resource bill for a pool chip, or the plain + // text another stat carries. Null means the stat has no tooltip. + _statTooltipContent($stat) { + if ($stat.hasClass("top-resource")) { + const proto = resourceById($stat.attr("data-resource")); + const unit = proto ? proto.unit : ""; + return this._resourceTooltip( + Number($stat.attr("data-produced")) || 0, + Number($stat.attr("data-consumed")) || 0, + unit + ); + } + const text = $stat.attr("data-tooltip"); + return text ? document.createTextNode(text) : null; + }, + + // A resource's daily flows as two right-aligned rows: production first with a + // plus, consumption second with a minus. One grid holds both rows, so the sign + // and figure columns line up; the figure column reserves room for the largest + // pool figure (99 999 999 999), so the bubble never shifts width as a stock + // moves. + _resourceTooltip(produced, consumed, unit) { + const $tip = $("
"); + for (const row of [ + { cls: "produce", sign: "+", amount: produced }, + { cls: "consume", sign: "−", amount: consumed }, + ]) { + $tip.append( + $("").addClass(row.cls).text(row.sign), + $("").addClass(row.cls).text(groupDigits(row.amount)), + $("").addClass(row.cls).text(`${unit}/day`) + ); + } + return $tip; + }, + + // Rebuilds the open stat tooltip in place, so a snapshot that changes the + // figures under the pointer does not leave a stale bubble. + _refreshStatTooltip() { + if (!this._hoveredStat || this.$statTooltip.hasClass("hidden")) return; + const content = this._statTooltipContent(this._hoveredStat); + if (content !== null) this.$statTooltip.empty().append(content); + }, + _hideStatTooltip() { this._hoveredStat = null; this.$statTooltip.addClass("hidden"); @@ -917,11 +963,9 @@ export const panelMethods = { if (Math.abs(net) <= 0.5) $arrow.text("=").addClass("equal"); else $arrow.text(net > 0 ? "▲" : "▼").addClass(net > 0 ? "up" : "down"); $value.after($arrow); - $chip.attr( - "data-tooltip", - `Produced ${formatResourceAmount(id, made)}/day · ` + - `Consumed ${formatResourceAmount(id, used)}/day` - ); + // The hover tooltip pairs the two flows; the raw figures ride on the chip + // so an open tooltip can be rebuilt in step with the next snapshot. + $chip.attr("data-produced", made).attr("data-consumed", used); } }, @@ -976,10 +1020,7 @@ export const panelMethods = { 0.005 ); // Keep an open stat tooltip in step with the figures it describes. - if (this._hoveredStat && !this.$statTooltip.hasClass("hidden")) { - const text = this._hoveredStat.attr("data-tooltip"); - if (text) this.$statTooltip.text(text); - } + this._refreshStatTooltip(); }, // Appends the month-on-month change marker inside a stat's value: a bold @@ -1366,8 +1407,7 @@ export const panelMethods = { .attr("data-tooltip", parts.join(", ")); // Keep an open tooltip in step with the unit it describes across snapshots. if (this._hoveredStat && !this.$statTooltip.hasClass("hidden")) { - const text = this._hoveredStat.attr("data-tooltip"); - if (text) this.$statTooltip.text(text); + this._refreshStatTooltip(); } else { this._hideStatTooltip(); } diff --git a/tests/simple_ui_test.js b/tests/simple_ui_test.js index b269619..b3c963f 100644 --- a/tests/simple_ui_test.js +++ b/tests/simple_ui_test.js @@ -253,7 +253,14 @@ export class SimpleUiTest extends TestCase { async test_the_top_panel_shows_the_pool_in_simple_mode() { const env = await setupDom(); try { - const screen = { economyModel: "simple", _updateTopResources: panelMethods._updateTopResources }; + const screen = { + economyModel: "simple", + _updateTopResources: panelMethods._updateTopResources, + _showStatTooltip: panelMethods._showStatTooltip, + _statTooltipContent: panelMethods._statTooltipContent, + _resourceTooltip: panelMethods._resourceTooltip, + $statTooltip: env.$("#stat-tooltip"), + }; screen._updateTopResources({ resources: { stock: { steel: 1_000_000, food: 500_000, luxury: 500_000, hightech: 20_000 }, @@ -276,15 +283,22 @@ export class SimpleUiTest extends TestCase { // less than it eats, so it is draining. this.assertTrue(chips.eq(0).find(".stat-arrow").hasClass("up"), "a growing pool arrow up"); this.assertTrue(chips.eq(1).find(".stat-arrow").hasClass("down"), "a draining pool arrow down"); - const tooltip = chips.eq(0).attr("data-tooltip"); - this.assertTrue( - tooltip.includes(`Produced ${formatResourceAmount("steel", 1_500)}/day`), - "the tooltip names production" - ); - this.assertTrue( - tooltip.includes(`Consumed ${formatResourceAmount("steel", 1_000)}/day`), - "the tooltip names consumption" - ); + // The chip's tooltip is a two-line bill: production with a plus, then + // consumption with a minus, each with its figure and unit. + screen._showStatTooltip(chips.eq(0)); + const tip = env.$("#stat-tooltip .resource-tip"); + const signs = tip.find(".rt-sign"); + const figures = tip.find(".rt-figure"); + const tails = tip.find(".rt-tail"); + this.assertSize(signs, 2, "production and consumption"); + this.assertTrue(signs.eq(0).hasClass("produce"), "production leads"); + this.assertEqual(signs.eq(0).text(), "+"); + this.assertEqual(figures.eq(0).text(), groupDigits(1_500)); + this.assertEqual(tails.eq(0).text(), "t/day"); + this.assertTrue(signs.eq(1).hasClass("consume"), "consumption follows"); + this.assertEqual(signs.eq(1).text(), "−"); + this.assertEqual(figures.eq(1).text(), groupDigits(1_000)); + this.assertEqual(tails.eq(1).text(), "t/day"); } finally { teardownDom(env); }