Split the bottom HUD into tile and unit panels
This commit is contained in:
+26
-10
@@ -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%;
|
||||
|
||||
+15
-11
@@ -133,7 +133,7 @@
|
||||
</div>
|
||||
<div id="fps-label">60 FPS</div>
|
||||
|
||||
<div id="bottom-panel" class="panel hidden">
|
||||
<div id="tile-panel" class="panel hidden">
|
||||
<div id="tile-info" class="info-section hidden">
|
||||
<div id="tile-desc" class="info-title"></div>
|
||||
<div id="tile-coords" class="tile-coords"></div>
|
||||
@@ -147,6 +147,20 @@
|
||||
<button id="city-manage" class="aero">City</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stat-tooltip" class="tooltip hidden"></div>
|
||||
</div>
|
||||
|
||||
<!-- Right-click menu listing the units sharing a tile -->
|
||||
<div id="stack-menu" class="stack-menu hidden"></div>
|
||||
|
||||
<!-- Lower-left dock: the news feed sits above the selected-unit panel. -->
|
||||
<div id="left-dock">
|
||||
<div class="game-corner">
|
||||
<div id="game-feed" class="game-feed"></div>
|
||||
<button id="news-button" class="aero news-button" title="News">News</button>
|
||||
</div>
|
||||
<div id="unit-panel" class="panel hidden">
|
||||
<div id="unit-info" class="info-section hidden">
|
||||
<div id="unit-name" class="info-title"></div>
|
||||
<div class="unit-stats">
|
||||
@@ -162,16 +176,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stat-tooltip" class="tooltip hidden"></div>
|
||||
</div>
|
||||
|
||||
<!-- Right-click menu listing the units sharing a tile -->
|
||||
<div id="stack-menu" class="stack-menu hidden"></div>
|
||||
|
||||
<!-- Chat-like feed of short-lived news, pinned to the lower left -->
|
||||
<div class="game-corner">
|
||||
<div id="game-feed" class="game-feed"></div>
|
||||
<button id="news-button" class="aero news-button" title="News">News</button>
|
||||
</div>
|
||||
|
||||
<!-- City modal: train, buildings and the regional budget tabs -->
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user