diff --git a/client/css/style.css b/client/css/style.css index 26a4a45..0e8e00d 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -505,12 +505,12 @@ input:focus, select:focus { border-color: var(--accent); } text-shadow: 0 1px 2px #000; } -#bottom-panel { - left: 50%; +/* The inspected tile / city panel is anchored to the bottom right. */ +#tile-panel { + right: 14px; bottom: 14px; - transform: translateX(-50%); - min-width: 420px; - max-width: 720px; + min-width: 320px; + max-width: 560px; padding: 12px 18px; } .info-section.hidden { display: none !important; } @@ -522,7 +522,7 @@ input:focus, select:focus { border-color: var(--accent); } .tile-gdp-caret { margin-left: 6px; color: var(--muted); font-size: 11px; } .tile-gdp-mods { margin-top: 2px; } .tile-gdp-mod { font-size: 12px; color: var(--muted); padding-left: 12px; font-variant-numeric: tabular-nums; } -#bottom-panel .row { justify-content: flex-start; margin-top: 10px; } +#tile-panel .row, #unit-panel .row { justify-content: flex-start; margin-top: 10px; } .health-row { display: flex; align-items: center; gap: 8px; } .health-bar { width: 220px; height: 12px; background: rgba(30, 0, 0, 0.8); border-radius: 6px; overflow: hidden; margin-top: 6px; } #unit-health-fill { height: 100%; background: linear-gradient(180deg, #6fe08a, #2fa04d); } @@ -537,9 +537,9 @@ input:focus, select:focus { border-color: var(--accent); } } .unit-stat:hover { color: var(--text); } -/* Chat-like feed of short-lived news in the lower-left corner, with the News - button anchored beneath it. */ -.game-corner { +/* Lower-left dock: the news feed sits above the selected-unit panel, so both + stay clear of the bottom-right tile panel. */ +#left-dock { position: absolute; left: 14px; bottom: 14px; @@ -547,10 +547,26 @@ input:focus, select:focus { border-color: var(--accent); } max-width: calc(50vw - 30px); display: flex; flex-direction: column; + align-items: stretch; + gap: 10px; + pointer-events: none; + z-index: 40; +} +#unit-panel { + position: relative; + width: 100%; + padding: 12px 18px; + pointer-events: auto; +} +/* Chat-like feed of short-lived news, with the News button beneath it. */ +.game-corner { + position: relative; + width: 100%; + display: flex; + flex-direction: column; align-items: flex-start; gap: 8px; pointer-events: none; - z-index: 40; } .game-feed { width: 100%; diff --git a/client/index.html b/client/index.html index 22bf422..c5c5f82 100644 --- a/client/index.html +++ b/client/index.html @@ -133,7 +133,7 @@
60 FPS
- + + + + + + + +
+
+
+ +
+ - -
- - - - - -
-
-
diff --git a/client/js/game_screen.js b/client/js/game_screen.js index 2188fe6..a04e7cd 100644 --- a/client/js/game_screen.js +++ b/client/js/game_screen.js @@ -336,12 +336,17 @@ export class GameScreen { this.map.camera.zoom = Math.max(0.2, this.map.camera.zoom / 1.1); this.map._applyCamera(); break; - case "Escape": + case "Escape": { + // ESC closes an open modal first; over the map it clears the selection + // and the inspected tile, dismissing both bottom panels. + const modalOpen = $(".modal").not(".hidden").length > 0; this.cityModal.close(); this.nationModal.close(); this.confirmModal.close(); this.newsModal.close(); + if (!modalOpen) this.map.clearSelection(); break; + } default: return; } diff --git a/client/js/game_screen/panels.js b/client/js/game_screen/panels.js index 2b1eb6d..c386563 100644 --- a/client/js/game_screen/panels.js +++ b/client/js/game_screen/panels.js @@ -118,7 +118,13 @@ export const panelMethods = { } this.selectedUnitIds = units.map((unit) => unit.id); this.selectedCityId = 0; - if (units.length > 0) this._inspectedTile = null; + if (units.length === 0) { + // Clearing the selection also drops the inspected tile, so the tile panel + // closes with it. Selecting units leaves the inspected tile alone, which + // is what lets the unit panel and the tile panel show together. + this._inspectedTile = null; + $("#tile-info, #city-info").addClass("hidden"); + } this.map.setSelectedUnits(this.selectedUnitIds); this._hideStackMenu(); this._showUnits(units); @@ -479,7 +485,7 @@ export const panelMethods = { if (units.length === 0) { $("#unit-info").addClass("hidden"); this._hideStatTooltip(); - this._refreshBottomVisibility(); + this._refreshPanelVisibility(); return; } if (units.length === 1) { @@ -493,16 +499,15 @@ export const panelMethods = { $("#unit-health-fill").css("width", `${maxHp > 0 ? Math.min(100, (hp / maxHp) * 100) : 0}%`); $("#unit-health-label").text(`${Math.round(hp)} / ${Math.round(maxHp)} HP`); this._updatePillageButton(units); - $("#tile-info, #city-info").addClass("hidden"); $("#unit-info").removeClass("hidden"); - this._refreshBottomVisibility(); + this._refreshPanelVisibility(); }, _showUnit(unit) { if (!unit) { $("#unit-info").addClass("hidden"); this._hideStatTooltip(); - this._refreshBottomVisibility(); + this._refreshPanelVisibility(); return; } const proto = this.protoUnits[unit.proto]; @@ -513,9 +518,8 @@ export const panelMethods = { $("#unit-health-fill").css("width", `${Math.max(0, Math.min(100, ratio))}%`); $("#unit-health-label").text(`${Math.round(unit.hp)} / ${Math.round(unit.maxHp)} HP`); this._updatePillageButton([unit]); - $("#tile-info, #city-info").addClass("hidden"); $("#unit-info").removeClass("hidden"); - this._refreshBottomVisibility(); + this._refreshPanelVisibility(); }, // The unit's name with its current air activity, so the panel explains why a @@ -573,7 +577,7 @@ export const panelMethods = { if (!city) { $("#city-info").addClass("hidden"); $("#tile-info").addClass("hidden"); - this._refreshBottomVisibility(); + this._refreshPanelVisibility(); return; } this._inspectedTile = null; @@ -581,7 +585,7 @@ export const panelMethods = { $("#city-name").text(city.name); $("#unit-info").addClass("hidden"); $("#tile-info, #city-info").removeClass("hidden"); - this._refreshBottomVisibility(); + this._refreshPanelVisibility(); this._updateTileHighlight(); }, @@ -596,7 +600,7 @@ export const panelMethods = { this._renderTileInfo(coords); $("#city-info, #unit-info").addClass("hidden"); $("#tile-info").removeClass("hidden"); - this._refreshBottomVisibility(); + this._refreshPanelVisibility(); this._updateTileHighlight(); }, @@ -809,12 +813,15 @@ export const panelMethods = { } }, - _refreshBottomVisibility() { - const visible = + // The tile/city panel and the unit panel are independent docks, so either + // can be open without the other. + _refreshPanelVisibility() { + const tileVisible = !$("#tile-info").hasClass("hidden") || - !$("#city-info").hasClass("hidden") || - !$("#unit-info").hasClass("hidden"); - $("#bottom-panel").toggleClass("hidden", !visible); + !$("#city-info").hasClass("hidden"); + const unitVisible = !$("#unit-info").hasClass("hidden"); + $("#tile-panel").toggleClass("hidden", !tileVisible); + $("#unit-panel").toggleClass("hidden", !unitVisible); }, _unit(id) { diff --git a/tests/game_screen_test.js b/tests/game_screen_test.js index 81dbe36..208873b 100644 --- a/tests/game_screen_test.js +++ b/tests/game_screen_test.js @@ -592,6 +592,44 @@ export class GameScreenTest extends TestCase { } } + async test_tile_and_unit_panels_dock_independently() { + const env = await setupDom(); + try { + const screen = new GameScreen(stubNetwork(), makeConfig()); + screen.enter(); + const state = smallState(); + screen.onState(state.snapshot(0)); + const capital = state.cities.find((c) => c.civ === 0 && c.isCapital); + + // Inspecting a tile opens the bottom-right tile panel on its own. + screen.map.onTileRequested(capital.coords); + this.assertFalse(env.$("#tile-panel").hasClass("hidden"), "the tile panel opens"); + this.assertTrue(env.$("#unit-panel").hasClass("hidden"), "the unit panel stays shut"); + + // Selecting a unit opens the bottom-left unit panel and leaves the tile + // panel showing the inspected tile. + const unit = state.units.find((u) => u.civ === 0); + screen.map.onUnitSelected(unit.id); + this.assertFalse(env.$("#unit-panel").hasClass("hidden"), "the unit panel opens"); + this.assertFalse(env.$("#tile-panel").hasClass("hidden"), "the tile panel stays open"); + + // Clearing the selection (an empty left-click) dismisses both. + screen.map.clearSelection(); + this.assertTrue(env.$("#tile-panel").hasClass("hidden"), "the click clears the tile panel"); + this.assertTrue(env.$("#unit-panel").hasClass("hidden"), "the click clears the unit panel"); + + // ESC over the map does the same. + screen.map.onTileRequested(capital.coords); + screen.map.onUnitSelected(unit.id); + env.$(env.window).trigger(env.$.Event("keydown", { key: "Escape" })); + this.assertTrue(env.$("#tile-panel").hasClass("hidden"), "ESC clears the tile panel"); + this.assertTrue(env.$("#unit-panel").hasClass("hidden"), "ESC clears the unit panel"); + screen.leave(); + } finally { + teardownDom(env); + } + } + async test_keyboard_and_render_loop() { const env = await setupDom(); try {