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
-
+
@@ -147,6 +147,20 @@
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
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 {