Skip to content

Accessibility, Visual Customization, Player Experience, Mobile Support and CLI Configuration#40

Merged
dmccoystephenson merged 22 commits intomainfrom
copilot/improve-accessibility-player-experience
Apr 7, 2026
Merged

Accessibility, Visual Customization, Player Experience, Mobile Support and CLI Configuration#40
dmccoystephenson merged 22 commits intomainfrom
copilot/improve-accessibility-player-experience

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 29, 2026

Replace all in-game blocking alert() calls and modal notifications with a non-blocking toast system across both clients. Add canvas hover tooltips, selection feedback, colorblind modes, theme switching, font scaling, a mobile-friendly responsive layout, and CLI server configuration for the desktop client.

Web Client (game.html + style.css)

  • Toast systemshowToast(message, severity) with 4 levels (INFO/SUCCESS/WARNING/DANGER), auto-dismiss 4s, max 4 stacked, critical events persist. Severity is normalized (lowercase + allowlist validation, defaults unknown values to info). All in-game alert() calls replaced. Toast container includes role="status" and aria-live="polite" for screen reader accessibility. Individual toasts are keyboard-focusable (tabindex="0") with aria-label and Enter/Space keydown handlers so keyboard-only users can dismiss persistent critical toasts. A @media (prefers-reduced-motion: reduce) rule disables slide-in, pulse, and fade-out animations for users who request reduced motion.
  • Canvas interaction — Gold selection ring (#FFD700) on selected army, right-click deselect, mousemove tooltip DOM div showing tile type/ownership/army stats/capture progress (throttled via requestAnimationFrame). On touch devices, a touchstart handler shows the same tooltip on tap with 3-second auto-dismiss, positioned below the finger to avoid obscuring content.
  • Canvas coordinate scalingeventToGrid() helper computes a scale ratio between the canvas pixel buffer (canvas.width) and CSS display size (getBoundingClientRect().width) so click/tap and tooltip handlers map to the correct grid cell even when the canvas is CSS-scaled on mobile.
  • Auto Play — Merged two buttons into single toggle with amber active state
  • HUD improvements — Color-coded stats (green ≥90, amber 70–89, red <70), policy cooldown progress bar, game-over banner overlay on canvas
  • Inline split validationsetCustomValidity() replaces alert-based validation
  • Visual customizationPALETTE object swapped by colorblind mode, CSS custom properties consumed via var() in body, container, headings, and info-section rules for live theme/font-size switching, settings persisted to localStorage key barony_settings
  • Mobile-friendly responsive layout — Viewport meta tag and @media (max-width: 768px) breakpoint that stacks HUD elements vertically, wraps policy controls and settings rows for touch usability, enforces 44×44px minimum tap targets on buttons/selects, positions toasts full-width at the bottom with no clipping. Tooltip is restyled for compact display on small screens (max-width: 240px, font-size: 12px). All mobile rules use existing CSS custom properties — no hard-coded color/font values introduced.
  • Dynamic canvas sizingresizeCanvas() function dynamically computes canvas pixel dimensions on mobile based on the game grid's aspect ratio and available viewport space, capping height at 50% viewport height so the board doesn't dominate the screen. Cells remain square regardless of grid dimensions (e.g., a 15×13 board no longer stretches into a fixed 800×600 rectangle). On desktop the canvas stays at 800×600. A debounced (150ms) window.resize listener triggers redraw on orientation change or resize.
  • Mobile-first layout reorder — On mobile, a .game-layout flex container with CSS order properties places the game board and turn controls at the top of the page so the canvas is the first thing visible. Info panels (Game Status & Stats, Change Policy, Settings, Armies) are positioned below the board.
  • Collapsible menu sections — All info panels are wrapped in native <details>/<summary> elements with styled toggle indicators (▸ arrow). On mobile, "Game Status & Stats" and "Armies" default open while "Change Policy" and "Settings" default collapsed to minimize scrolling. On desktop, all sections are auto-expanded via JavaScript. Sections use semantic class names (section-status, section-policy, section-settings, section-armies) for maintainable CSS ordering.

Web Client (login.html)

  • Mobile-friendly login screen — Viewport meta tag added, @media (max-width: 768px) breakpoint centers the login form vertically, constrains max-width to 90vw, increases input/button tap targets to 44px minimum height, and uses responsive font sizing. The login page is usable at 375px viewport width with no overflow or clipped elements.

Desktop Frontend (Java)

  • NotificationManager — Singleton FIFO queue, 4 severity levels, auto-dismiss, history buffer, null-safe show() (skips null messages, defaults null severity to INFO). Toast notifications are scoped to important game events only (turn advancement, game over, policy changes) — routine game log entries are not duplicated into toasts.
  • ToastOverlay / NotificationLogPanel — OpenGL overlay renderers; toast renders at highest Z-order (after settings/log panels) with newest-first layout (closest to bottom edge) and enlarged readable text (2.5× scale with wider toast boxes); log panel accessible via F10 with UP/DOWN scroll
  • ThemeManager — Centralized color lookups with colorblind palettes (deuteranopia/protanopia/tritanopia), 3 themes (dark/classic/high-contrast), BASE_FONT_SIZE scaling, persisted to ~/.barony/settings.json with clamped fontScale on load and sanitized values against null/invalid JSON. Public setters setTheme()/setColorblindMode() are null-safe — null/blank inputs normalize to "dark"/"none" defaults, and theme names are validated against the known set.
  • SettingsPanel — F9 keybind, arrow-key navigation, live preview, normalized theme name matching (handles hyphen/space variants), saves only on panel close (dirty flag debounce)
  • FrontendApplication — Wired overlays into render loop, army colors driven by ThemeManager; GL line width properly restored after overlay border draws. Supports --server <url> command-line argument to configure the backend server address (defaults to http://localhost:8080).
# Launch with custom server (Linux/macOS)
./mvnw -pl frontend exec:java -Dexec.args="--server http://192.168.1.100:8080"

# Launch with custom server (Windows)
mvnw.cmd -pl frontend exec:java -Dexec.args="--server http://192.168.1.100:8080"
// Usage in any frontend code
NotificationManager.getInstance().warning("Policy cooldown — 8 turns remaining");

// Theme-aware army colors
float[] p1Color = ThemeManager.getInstance().getPlayer1Color();

Tests

  • 8 tests for NotificationManager (flush, dismiss, severity, critical persistence)
  • 9 tests for ThemeManager (colorblind palette swaps, theme switching, font scale clamping)

Docs

  • README.md — F9/F10 keybinds, --server CLI flag with platform-specific commands (Linux/macOS + Windows), web client feature list, accessibility features
  • PLAYER_GUIDE.md — New "Accessibility & Visual Settings" section, updated controls table with F9/F10, --server usage with platform-specific commands

💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.

Copilot AI and others added 4 commits March 29, 2026 06:10
…olorblind & settings CSS

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
… auto-play toggle, colorblind modes, settings panel, game-over banner, stat color-coding, and policy progress bar

Agent-Logs-Url: https://github.com/Preponderous-Software/barony/sessions/875a2b7b-2e63-4ea1-bdb0-98125ff3d0d6

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
…, SettingsPanel, ThemeManager with colorblind modes and tests

Agent-Logs-Url: https://github.com/Preponderous-Software/barony/sessions/875a2b7b-2e63-4ea1-bdb0-98125ff3d0d6

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
…orblind modes, and new keybindings

Agent-Logs-Url: https://github.com/Preponderous-Software/barony/sessions/875a2b7b-2e63-4ea1-bdb0-98125ff3d0d6

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copilot AI changed the title [WIP] Improve accessibility features and player experience across clients Accessibility, Visual Customization & Player Experience Mar 29, 2026
Copilot AI requested a review from dmccoystephenson March 29, 2026 06:25
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR modernizes player-facing feedback and accessibility controls across the web and desktop clients by replacing blocking modal notifications with toast-style messaging and introducing theme/colorblind/font scaling options plus richer hover/selection UI.

Changes:

  • Web: adds toast notifications, canvas hover tooltips + selection ring, autoplay toggle, policy cooldown progress UI, and a settings panel persisted to localStorage.
  • Desktop: introduces NotificationManager + OpenGL overlays (toast, tooltip, settings, notification log) and a ThemeManager persisted to ~/.barony/settings.json.
  • Adds JUnit coverage for the new desktop notification/theme managers and updates user docs.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
web-client/src/main/resources/templates/game.html Adds web toasts, settings persistence, canvas tooltips/selection, autoplay toggle, and HUD improvements.
web-client/src/main/resources/static/css/style.css Styles for toasts/tooltips/banners/settings; introduces theme/colorblind/font CSS custom properties.
frontend/src/test/java/com/barony/frontend/ui/NotificationManagerTest.java Unit tests for NotificationManager behavior (capacity, severity, critical persistence).
frontend/src/test/java/com/barony/frontend/rendering/ThemeManagerTest.java Unit tests for ThemeManager palette/theme switching and font scale clamping.
frontend/src/main/resources/settings/default-settings.json Adds a default settings JSON resource.
frontend/src/main/java/com/barony/frontend/ui/TooltipOverlay.java New OpenGL tooltip renderer (cursor-adjacent, wrapped text).
frontend/src/main/java/com/barony/frontend/ui/ToastOverlay.java New OpenGL toast renderer reading from NotificationManager.
frontend/src/main/java/com/barony/frontend/ui/SettingsPanel.java New in-game settings panel (F9) that applies ThemeManager settings and saves them.
frontend/src/main/java/com/barony/frontend/ui/NotificationManager.java New singleton manager for toast lifecycle + history buffer.
frontend/src/main/java/com/barony/frontend/ui/NotificationLogPanel.java New history panel renderer for past notifications.
frontend/src/main/java/com/barony/frontend/rendering/ThemeManager.java New centralized theme/colorblind/font scale manager with JSON persistence.
frontend/src/main/java/com/barony/frontend/FrontendApplication.java Wires in ThemeManager-based army colors and renders overlays; adds F9 settings handling.
README.md Documents new settings keybind/features and web client enhancements.
PLAYER_GUIDE.md Adds accessibility/settings section and updates controls/help text.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to +67
glColor4f(0.54f, 0.45f, 0.33f, 1.0f);
glLineWidth(2.0f);
glBegin(GL_LINE_LOOP);
glVertex2f(PANEL_X, PANEL_Y);
glVertex2f(PANEL_X + PANEL_WIDTH, PANEL_Y);
glVertex2f(PANEL_X + PANEL_WIDTH, PANEL_Y + PANEL_HEIGHT);
glVertex2f(PANEL_X, PANEL_Y + PANEL_HEIGHT);
glEnd();
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NotificationLogPanel changes global OpenGL line width (glLineWidth(2.0f)) but does not restore it before returning. Since GL state persists across frames, this can unintentionally affect other rendering. Consider resetting line width to 1.0f before glPopAttrib() or pushing/restoring GL_LINE_BIT as well.

Copilot uses AI. Check for mistakes.
Comment on lines +117 to +124
glColor4f(0.79f, 0.66f, 0.43f, 1.0f);
glLineWidth(2.0f);
glBegin(GL_LINE_LOOP);
glVertex2f(PANEL_X, PANEL_Y);
glVertex2f(PANEL_X + PANEL_WIDTH, PANEL_Y);
glVertex2f(PANEL_X + PANEL_WIDTH, PANEL_Y + PANEL_HEIGHT);
glVertex2f(PANEL_X, PANEL_Y + PANEL_HEIGHT);
glEnd();
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SettingsPanel changes global OpenGL line width (glLineWidth(2.0f)) but does not restore it before returning. Since GL state persists across frames, this can unintentionally affect other rendering. Consider resetting line width to 1.0f before glPopAttrib() or pushing/restoring GL_LINE_BIT as well.

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +85
// Overlay UI components
private ToastOverlay toastOverlay = new ToastOverlay();
private TooltipOverlay tooltipOverlay = new TooltipOverlay();
private NotificationLogPanel notificationLogPanel = new NotificationLogPanel();
private SettingsPanel settingsPanel = new SettingsPanel();
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FrontendApplication declares a TooltipOverlay instance but it is never set/updated or rendered, so desktop tooltips won’t appear even though the overlay exists. Either wire it into hover handling (set/hide on hover changes) and render it with the other overlays, or remove the unused field/import to avoid confusion.

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +173
if (settingsPanel.isVisible()) {
settingsPanel.hide();
} else if (notificationLogPanel.isVisible()) {
notificationLogPanel.hide();
} else if (splitModeActive) {
splitModeActive = false;
splitModeArmyId = -1;
splitModeTotalSoldiers = 0;
System.out.println("Split mode cancelled");
NotificationManager.getInstance().info("Split mode cancelled");
} else {
glfwSetWindowShouldClose(window, true);
}
}

// F9 toggles settings panel
if (key == GLFW_KEY_F9 && action == GLFW_RELEASE) {
settingsPanel.toggle();
return;
}

// Settings panel input handling
if (settingsPanel.isVisible() && action == GLFW_RELEASE) {
if (key == GLFW_KEY_UP) settingsPanel.navigateUp();
else if (key == GLFW_KEY_DOWN) settingsPanel.navigateDown();
else if (key == GLFW_KEY_LEFT) settingsPanel.navigateLeft();
else if (key == GLFW_KEY_RIGHT) settingsPanel.navigateRight();
return;
}
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NotificationLogPanel is rendered and can be closed via ESC, but there is no keybind/menu action to ever open it (no calls to notificationLogPanel.toggle()). As a result, the log panel is effectively unreachable. Consider adding a dedicated keybind (and optional scroll key handling while visible) or removing the panel integration if it’s not meant to be user-accessible yet.

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +5
"colorblindMode": "none",
"theme": "dark",
"fontScale": 1.0
}
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This default settings JSON file doesn't appear to be referenced/loaded anywhere in the codebase, so it will be dead weight in the build artifacts. Either wire it into ThemeManager (e.g., use it when ~/.barony/settings.json is missing) or remove it to avoid confusion about which defaults are authoritative.

Suggested change
"colorblindMode": "none",
"theme": "dark",
"fontScale": 1.0
}
"_comment": "Deprecated: this file is not used by ThemeManager or the runtime. Defaults are defined in code and/or user settings."
}

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +153
### Web Client Features
- Toast notifications replace all blocking alerts
- Canvas hover tooltips (tile info, army stats, castle capture progress)
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description/README says all blocking alert() calls were replaced in the web client, but web-client/src/main/resources/templates/login.html still contains alert('Please enter a username') and alert('Login failed: ...'). Either migrate login.html to the toast/non-blocking pattern as well, or narrow the claim/docs to only the in-game page.

Copilot uses AI. Check for mistakes.
themeName = (String) settings.get("theme");
}
if (settings.containsKey("fontScale")) {
fontScale = ((Number) settings.get("fontScale")).floatValue();
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ThemeManager.load() assigns fontScale directly from disk without clamping. A corrupted/hand-edited settings.json could set an invalid scale and break layout. Consider routing the loaded value through setFontScale() (or applying the same clamp logic) before storing it.

Suggested change
fontScale = ((Number) settings.get("fontScale")).floatValue();
setFontScale(((Number) settings.get("fontScale")).floatValue());

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +51
private void loadFromThemeManager() {
ThemeManager tm = ThemeManager.getInstance();
String cb = tm.getColorblindMode();
for (int i = 0; i < COLORBLIND_OPTIONS.length; i++) {
if (COLORBLIND_OPTIONS[i].equalsIgnoreCase(cb)) { colorblindIndex = i; break; }
}
String theme = tm.getThemeName();
for (int i = 0; i < THEME_OPTIONS.length; i++) {
if (THEME_OPTIONS[i].equalsIgnoreCase(theme)) { themeIndex = i; break; }
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SettingsPanel.loadFromThemeManager() matches theme names via equalsIgnoreCase against "High Contrast". If ThemeManager ever persists/returns "high-contrast" (hyphen) instead of "high contrast" (space), this will fail to match and the panel will show the wrong selected theme. Consider normalizing both strings (e.g., replace '-' with ' ' before comparison) or using a canonical enum/value map.

Suggested change
private void loadFromThemeManager() {
ThemeManager tm = ThemeManager.getInstance();
String cb = tm.getColorblindMode();
for (int i = 0; i < COLORBLIND_OPTIONS.length; i++) {
if (COLORBLIND_OPTIONS[i].equalsIgnoreCase(cb)) { colorblindIndex = i; break; }
}
String theme = tm.getThemeName();
for (int i = 0; i < THEME_OPTIONS.length; i++) {
if (THEME_OPTIONS[i].equalsIgnoreCase(theme)) { themeIndex = i; break; }
/**
* Normalize a theme name for comparison.
* Currently replaces hyphens with spaces and trims whitespace.
*/
private String normalizeThemeName(String name) {
if (name == null) {
return "";
}
return name.replace('-', ' ').trim();
}
private void loadFromThemeManager() {
ThemeManager tm = ThemeManager.getInstance();
String cb = tm.getColorblindMode();
for (int i = 0; i < COLORBLIND_OPTIONS.length; i++) {
if (COLORBLIND_OPTIONS[i].equalsIgnoreCase(cb)) { colorblindIndex = i; break; }
}
String theme = normalizeThemeName(tm.getThemeName());
for (int i = 0; i < THEME_OPTIONS.length; i++) {
String option = normalizeThemeName(THEME_OPTIONS[i]);
if (option.equalsIgnoreCase(theme)) { themeIndex = i; break; }

Copilot uses AI. Check for mistakes.
float bottomStart = -1.0f + TOAST_BOTTOM_MARGIN;

for (int i = 0; i < notifications.length; i++) {
Notification n = notifications[i];
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToastOverlay renders notifications in the order returned by NotificationManager (oldest→newest), which places the newest toast furthest from the bottom edge. This is the opposite of the web client (newest closest to bottom). Consider iterating in reverse (or reversing the snapshot) so newly created notifications appear at the bottom of the stack.

Suggested change
Notification n = notifications[i];
// Render newest notifications closest to the bottom edge
Notification n = notifications[notifications.length - 1 - i];

Copilot uses AI. Check for mistakes.
@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot address comments

…, add aria attrs, wire CSS vars, clamp fontScale on load, add F10 keybind for log panel, remove unused default-settings.json

Agent-Logs-Url: https://github.com/Preponderous-Software/barony/sessions/897c4d45-112c-4485-8cdc-43a77c9884dc

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 29, 2026

@copilot address comments

All 12 review comments addressed in 0c984cb:

  1. GL line width restoredglLineWidth(1.0f) added after border drawing in both NotificationLogPanel and SettingsPanel
  2. Unused TooltipOverlay field removed — The existing renderTooltip() method already handles desktop tooltips; removed the unconnected field and import
  3. NotificationLogPanel now accessible — Added F10 keybind to toggle the log panel, with UP/DOWN scroll keys while visible
  4. Unused default-settings.json deleted — Defaults are defined in ThemeManager code
  5. Log panel shows newest first — History now iterates from the end of the array so recent entries appear at top
  6. Toast container has ARIA attributes — Added role="status", aria-live="polite", aria-atomic="true" for screen reader announcements
  7. README narrowed — Changed "replace all blocking alerts" → "replace all in-game blocking alerts" since login.html is out of scope
  8. CSS custom properties wiredbody, .container, h1, h3, .info-section now use var(--bg-primary), var(--text-primary), var(--accent), var(--font-size-base) etc., and :root defines all defaults
  9. fontScale clamped on loadload() now routes through setFontScale() which applies the 0.5–2.0 clamp
  10. Theme name matching normalized — Added normalizeThemeName() that replaces hyphens with spaces before comparison
  11. Toast rendering order fixed — Newest notifications now render closest to bottom edge, matching web client convention

Copilot AI changed the title Accessibility, Visual Customization & Player Experience Accessibility, Visual Customization &amp; Player Experience Mar 29, 2026
@dmccoystephenson dmccoystephenson marked this pull request as ready for review April 4, 2026 19:15
@dmccoystephenson dmccoystephenson requested a review from Copilot April 4, 2026 21:04
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +912 to +935
canvas.addEventListener('mousemove', function(e) {
if (!lastGameState) return;

const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const cellWidth = canvas.width / lastGameState.width;
const cellHeight = canvas.height / lastGameState.height;
const gridX = Math.floor(x / cellWidth);
const gridY = Math.floor(y / cellHeight);

const text = getTooltipText(gridX, gridY, lastGameState);
if (text) {
tooltip.textContent = text;
tooltip.style.display = 'block';
tooltip.style.left = (e.clientX - rect.left + 12) + 'px';
tooltip.style.top = (e.clientY - rect.top - 8) + 'px';
} else {
tooltip.style.display = 'none';
}
});

canvas.addEventListener('mouseleave', function() {
tooltip.style.display = 'none';
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mousemove handler calls getTooltipText() on every mousemove, and getTooltipText() does a linear search over gameState.armies via .find(). This can become a hot path and cause jank as army counts grow. Consider throttling the handler (e.g., requestAnimationFrame) and/or maintaining a position->army lookup map when updating lastGameState.

Suggested change
canvas.addEventListener('mousemove', function(e) {
if (!lastGameState) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const cellWidth = canvas.width / lastGameState.width;
const cellHeight = canvas.height / lastGameState.height;
const gridX = Math.floor(x / cellWidth);
const gridY = Math.floor(y / cellHeight);
const text = getTooltipText(gridX, gridY, lastGameState);
if (text) {
tooltip.textContent = text;
tooltip.style.display = 'block';
tooltip.style.left = (e.clientX - rect.left + 12) + 'px';
tooltip.style.top = (e.clientY - rect.top - 8) + 'px';
} else {
tooltip.style.display = 'none';
}
});
canvas.addEventListener('mouseleave', function() {
tooltip.style.display = 'none';
let latestTooltipMousePosition = null;
let tooltipFramePending = false;
function hideTooltip() {
tooltip.style.display = 'none';
}
function scheduleTooltipUpdate() {
if (tooltipFramePending) return;
tooltipFramePending = true;
requestAnimationFrame(function() {
tooltipFramePending = false;
if (!lastGameState || !latestTooltipMousePosition) {
hideTooltip();
return;
}
const rect = canvas.getBoundingClientRect();
const x = latestTooltipMousePosition.clientX - rect.left;
const y = latestTooltipMousePosition.clientY - rect.top;
const cellWidth = canvas.width / lastGameState.width;
const cellHeight = canvas.height / lastGameState.height;
const gridX = Math.floor(x / cellWidth);
const gridY = Math.floor(y / cellHeight);
const text = getTooltipText(gridX, gridY, lastGameState);
if (text) {
tooltip.textContent = text;
tooltip.style.display = 'block';
tooltip.style.left = (x + 12) + 'px';
tooltip.style.top = (y - 8) + 'px';
} else {
hideTooltip();
}
});
}
canvas.addEventListener('mousemove', function(e) {
latestTooltipMousePosition = {
clientX: e.clientX,
clientY: e.clientY
};
scheduleTooltipUpdate();
});
canvas.addEventListener('mouseleave', function() {
latestTooltipMousePosition = null;
hideTooltip();

Copilot uses AI. Check for mistakes.
Comment on lines +602 to +632
.settings-panel {
background: #352f28;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #c9a96e;
}

.settings-panel h3 {
margin-top: 0;
}

.settings-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}

.settings-row label {
min-width: 120px;
font-weight: bold;
}

.settings-row select {
padding: 6px 10px;
border: 1px solid #5a4e3c;
border-radius: 4px;
background: #2b2520;
color: #d4cfc8;
font-size: 14px;
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Settings panel is both .info-section and .settings-panel, but .settings-panel overrides background/border colors with hard-coded values, which prevents the new theme switching (CSS custom properties) from affecting the settings UI. Use the existing CSS variables (e.g., --bg-secondary/--accent/--text-primary) for these rules so the settings panel live-preview matches the selected theme.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +62
public void show(String message, Severity severity) {
boolean isCritical = severity == Severity.DANGER &&
(message.toLowerCase().contains("game over") ||
message.toLowerCase().contains("castle") ||
message.toLowerCase().contains("destroyed"));

Notification n = new Notification(message, severity, !isCritical);
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NotificationManager.show() assumes message is non-null and calls message.toLowerCase(), which will throw if any code logs a null message. Also, severity can be passed as null (e.g., from future call sites), which would later break switch statements in renderers. Add null-handling (default severity to INFO, default/skip null messages) or enforce non-null inputs with a clear exception.

Copilot uses AI. Check for mistakes.
Comment on lines +154 to +173
public void load() {
try {
Path path = getSettingsPath();
if (Files.exists(path)) {
String json = Files.readString(path);
Gson gson = new Gson();
@SuppressWarnings("unchecked")
Map<String, Object> settings = gson.fromJson(json, Map.class);
if (settings.containsKey("colorblindMode")) {
colorblindMode = (String) settings.get("colorblindMode");
}
if (settings.containsKey("theme")) {
themeName = (String) settings.get("theme");
}
if (settings.containsKey("fontScale")) {
setFontScale(((Number) settings.get("fontScale")).floatValue());
}
applyTheme();
applyColorblindPalette();
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ThemeManager.load() assigns themeName/colorblindMode directly from JSON. If the settings file contains nulls or unexpected types, applyTheme()/applyColorblindPalette() will NPE on themeName.toLowerCase() / colorblindMode.toLowerCase(). Sanitize loaded values (default to "dark"/"none" when missing or invalid) before calling applyTheme/applyColorblindPalette.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +103
private void applySettings() {
ThemeManager tm = ThemeManager.getInstance();
tm.setColorblindMode(COLORBLIND_OPTIONS[colorblindIndex].toLowerCase());
tm.setTheme(THEME_OPTIONS[themeIndex].toLowerCase());

float scale;
switch (fontSizeIndex) {
case 0: scale = 0.8f; break;
case 2: scale = 1.2f; break;
default: scale = 1.0f; break;
}
tm.setFontScale(scale);
tm.save();
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SettingsPanel.applySettings() calls ThemeManager.save() on every left/right adjustment. This can generate frequent disk writes (especially if a key repeats) and may cause stutter on slower disks. Consider saving only when closing the panel, or debounce saves (e.g., save after N ms of inactivity).

Copilot uses AI. Check for mistakes.
@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot address comments

…ng, throttle mousemove, theme CSS vars in settings panel, save-on-close

Agent-Logs-Url: https://github.com/Preponderous-Software/barony/sessions/1d95564b-bd27-4c20-96f6-9497eb2013db

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 4, 2026

@copilot address comments

All 7 review comments addressed in aca5400:

  1. Tooltip positioning fixed — Computes offset relative to #gameCanvas container (canvasRect.left - containerRect.left) so the tooltip tracks the cursor correctly regardless of canvas centering.
  2. mousemove throttledrequestAnimationFrame gate prevents redundant tooltip calculations per frame.
  3. showToast() null-safeString(message || '') coercion before .toLowerCase().
  4. Settings panel CSS themed.settings-panel, h3, label, and select now use var(--bg-secondary), var(--accent), var(--text-primary), var(--font-size-base) with fallbacks.
  5. NotificationManager.show() null-safe — Returns early on null message; defaults null severity to INFO.
  6. ThemeManager.load() sanitized — Uses instanceof String/instanceof Number checks to guard against null or wrong-type values from malformed JSON.
  7. SettingsPanel save debounced — Introduced dirty flag; applySettings() sets dirty = true without saving; save() only runs on hide()/toggle() close.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +178 to +195
String json = Files.readString(path);
Gson gson = new Gson();
@SuppressWarnings("unchecked")
Map<String, Object> settings = gson.fromJson(json, Map.class);
Object cbValue = settings.get("colorblindMode");
if (cbValue instanceof String && !((String) cbValue).isEmpty()) {
colorblindMode = (String) cbValue;
}
Object themeValue = settings.get("theme");
if (themeValue instanceof String && !((String) themeValue).isEmpty()) {
themeName = (String) themeValue;
}
Object scaleValue = settings.get("fontScale");
if (scaleValue instanceof Number) {
setFontScale(((Number) scaleValue).floatValue());
}
applyTheme();
applyColorblindPalette();
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load() assigns themeName/colorblindMode directly from JSON and then dereferences settings without a null-check. gson.fromJson() can return null (e.g., empty file), causing an NPE that is swallowed by the broad catch; also, direct assignment bypasses the validation/normalization in setTheme()/setColorblindMode(). Consider guarding settings == null and applying loaded values via the setters so invalid/unknown values can’t leak into getThemeName()/getColorblindMode().

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +86
private float[] getBackgroundColor(Severity severity) {
ThemeManager theme = ThemeManager.getInstance();
switch (severity) {
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getBackgroundColor() creates a ThemeManager theme = ThemeManager.getInstance(); local that is never used. This is dead code and may trigger warnings; removing it will keep the method minimal and clearer.

Copilot uses AI. Check for mistakes.
Comment on lines +607 to +613
// Color based on player (uses ThemeManager for colorblind support)
float[] p1Color = ThemeManager.getInstance().getPlayer1Color();
float[] p2Color = ThemeManager.getInstance().getPlayer2Color();
if (army.getPlayerId() == 1) {
glColor3f(0.0f, 0.0f, 1.0f); // Blue
glColor3f(p1Color[0], p1Color[1], p1Color[2]);
} else {
glColor3f(1.0f, 0.0f, 0.0f); // Red
glColor3f(p2Color[0], p2Color[1], p2Color[2]);
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p1Color/p2Color are retrieved from ThemeManager.getInstance() for every army render. Since these colors are constant per frame, fetch them once before iterating armies to avoid repeated singleton lookups/array reads in the inner loop.

Copilot uses AI. Check for mistakes.
@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot address comments

…dation, dead code, color hoist

Agent-Logs-Url: https://github.com/Preponderous-Software/barony/sessions/d4577203-1af4-477e-baf9-ba1f2f0b45c7

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 6, 2026

@copilot address comments

All 4 review comments addressed in 00dc018:

  1. Toast aria-label updated — Now says "press Enter or Space to dismiss" to match both supported keys.
  2. ThemeManager.load() uses setters — Loaded values now go through setTheme()/setColorblindMode() for null/validation normalization, with a settings == null guard for empty files.
  3. Dead code removed — Unused ThemeManager theme local in ToastOverlay.getBackgroundColor() deleted.
  4. Army colors hoistedp1Color/p2Color fetched once before the army loop instead of per-army.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if (e.key === ' ') e.preventDefault(); // prevent scroll on Space regardless
if (e.key === 'Enter' || e.key === ' ') dismissToast(toast);
});
container.appendChild(toast);
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#toast-container is styled with flex-direction: column-reverse (style.css:387), but new toasts are added via appendChild(). With column-reverse, appending causes newer toasts to render above older ones (oldest stays closest to the bottom), which is the opposite of the intended “newest closest to bottom” stacking. Consider either switching the container to flex-direction: column or inserting new toasts with prepend()/insertBefore() so the visual order matches the intended stacking behavior.

Suggested change
container.appendChild(toast);
container.prepend(toast);

Copilot uses AI. Check for mistakes.
@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot address comments

…st allowlists

Agent-Logs-Url: https://github.com/Preponderous-Software/barony/sessions/e04f4702-4dbf-43c6-89d1-7f16a8e4bc54

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 6, 2026

@copilot address comments

Both review comments addressed in bef5c06:

  1. Toast stacking fixedcontainer.prepend(toast) replaces appendChild() so newest toasts render closest to the bottom edge (matching flex-direction: column-reverse).
  2. loadSettings() validated — Stored values are checked against allowlists (validColorblind, validTheme, validFontSize) before applying to selects. Malformed or outdated values are silently ignored and defaults remain.

@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot The popup notifications in the bottom right of the desktop application can't be read, the text is too small. Also it appears to be duplication the game log. Finally, it should be possible to configure the target backend server address when runninng the desktop application from the Windows command line.

Screenshot:
image

… flag

Agent-Logs-Url: https://github.com/Preponderous-Software/barony/sessions/7766f524-3ac3-48fd-b988-a9d5cf8083ab

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 7, 2026

@copilot The popup notifications in the bottom right of the desktop application can't be read, the text is too small. Also it appears to be duplication the game log. Finally, it should be possible to ...

All 3 issues addressed in 7fb4368:

  1. Toast text enlarged — Text scale increased 2.5× (0.003f from 0.0012f), toast boxes widened to 55% viewport width and 9% height (from 45%/6%), with 60-char truncation limit.
  2. Log duplication removedaddLogMessage() no longer auto-forwards every entry to NotificationManager. Toasts and the game log are now independent — only explicit NotificationManager.getInstance().info/warning/etc. calls produce toast popups.
  3. --server CLI flag added — Run with --server http://192.168.1.100:8080 to connect to a remote backend. Defaults to http://localhost:8080 when omitted. Documented in README.md and PLAYER_GUIDE.md.

Copilot AI changed the title Accessibility, Visual Customization, Player Experience &amp; Mobile Support Accessibility, Visual Customization, Player Experience, Mobile Support &amp; CLI Configuration Apr 7, 2026
README.md Outdated
Comment on lines +119 to +120
# Or via the start script with arguments
java -jar frontend.jar --server http://myserver:8080
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work, I run into the following:

no main manifest attribute, in .\frontend-1.0.0.jar

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 363043a. The project doesn't produce an executable jar — it runs via Maven's exec:java. Removed the misleading java -jar example and replaced with the correct mvnw compile exec:java -Dexec.args="--server ..." command for both Linux/macOS and Windows.


```bash
# Connect to a custom backend server
cd frontend && ./mvnw compile exec:java -Dexec.args="--server http://192.168.1.100:8080"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document Windows equivalent here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 363043a. Both README.md and PLAYER_GUIDE.md now show the Windows equivalent:

cd frontend && mvnw.cmd compile exec:java -Dexec.args="--server http://192.168.1.100:8080"

…d equivalent

Agent-Logs-Url: https://github.com/Preponderous-Software/barony/sessions/572f4344-5204-402c-9cc1-3413de8fbae7

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copilot AI requested a review from dmccoystephenson April 7, 2026 02:26
@dmccoystephenson dmccoystephenson changed the title Accessibility, Visual Customization, Player Experience, Mobile Support &amp; CLI Configuration Accessibility, Visual Customization, Player Experience, Mobile Support and CLI Configuration Apr 7, 2026
@dmccoystephenson dmccoystephenson merged commit fa4c27a into main Apr 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Accessibility, Visual Customization & Player Experience

3 participants