diff --git a/ai_animation/CLAUDE.md b/ai_animation/CLAUDE.md index eaff51e..c71d4ee 100644 --- a/ai_animation/CLAUDE.md +++ b/ai_animation/CLAUDE.md @@ -31,6 +31,14 @@ - When animations complete, show phase summary (if available) - Advance to next phase and repeat +## Power Name Display System +The application now includes a dynamic power name display system: + +1. **Model Names**: If a `moments.json` file exists with a `power_models` key, the UI will display AI model names instead of generic power names (e.g., "o3" instead of "FRANCE") +2. **Fallback**: If no model names are available, the system falls back to standard power names (AUSTRIA, ENGLAND, etc.) +3. **Utility Function**: `getPowerDisplayName(power)` resolves the appropriate display name for any power +4. **Game-Aware**: The system automatically adapts based on the currently loaded game's data + ## Agent State Display The game now includes agent state data that can be visualized: @@ -70,6 +78,44 @@ The game now includes agent state data that can be visualized: - The schema automatically converts object-format orders to array format for use in the code - When debugging order issues, check the format in the original JSON +## Debug Tools +The application includes a debug menu system (enabled when `config.isDebugMode` is true): + +### Debug Menu Structure +- Located in `src/debug/` directory +- `DebugMenu` class (`debugMenu.ts`) manages the collapsible menu system +- Individual debug tools are implemented as separate modules and registered with the menu +- Menu does not close when clicking outside (for better UX during debugging) + +### Available Debug Tools + +#### Province Highlighting (`provinceHighlight.ts`) +- Allows highlighting specific provinces on the map by name +- Input validation with visual feedback for invalid province names +- Supports Enter key and button click to trigger highlighting + +#### Next Moment Display (`nextMoment.ts`) +- Shows the next chronological moment that will occur in the game +- Displays current phase, next phase, and next moment information +- Uses phase name parsing to determine chronological order +- Finds the next moment across all phases, not just the immediate next phase +- Shows moment category, phase name, and interest score +- Color-coded by importance (red for high scores ≥9, blue for others) + +### Phase Name Format +Phase names follow the format: `[Season][Year][Phase]` +- Seasons: Spring (S) → Fall (F) → Winter (W) +- Phases within each season: Move (M) → Retreat (R) → Adjustment (A) +- Example: `W1901R` = Winter 1901 Retreat phase +- Phase parsing logic is implemented in `types/moments.ts` with Zod schemas + +### Adding New Debug Tools +1. Create a new file in `src/debug/` directory +2. Implement an init function that takes the DebugMenu instance +3. Use `debugMenu.addDebugTool(title, htmlContent, beforeSection?)` to add to menu +4. Register the tool in the DebugMenu's `initTools()` method +5. Add any update functions to `updateTools()` method if needed + ## Code Style Preferences - Use descriptive function and variable names - Add JSDoc comments for all exported functions diff --git a/ai_animation/package.json b/ai_animation/package.json index 4fe6e32..dd10e31 100644 --- a/ai_animation/package.json +++ b/ai_animation/package.json @@ -9,7 +9,8 @@ "build": "tsc && vite build", "preview": "vite preview --host 0.0.0.0", "test": "vitest", - "test:ui": "vitest --ui" + "test:ui": "vitest --ui", + "lint": "tsc --noEmit" }, "devDependencies": { "@vitest/ui": "^3.1.4", diff --git a/ai_animation/src/components/rotatingDisplay.ts b/ai_animation/src/components/rotatingDisplay.ts index d1f8849..26fe018 100644 --- a/ai_animation/src/components/rotatingDisplay.ts +++ b/ai_animation/src/components/rotatingDisplay.ts @@ -2,6 +2,7 @@ import { gameState } from "../gameState"; import { PowerENUM } from "../types/map"; import { GameSchemaType } from "../types/gameState"; import { Chart } from "chart.js"; +import { getPowerDisplayName } from "../utils/powerNames"; // Enum for the different display types export enum DisplayType { @@ -12,14 +13,16 @@ export enum DisplayType { // Configuration const ROTATION_INTERVAL_MS = 15000; // 15 seconds between rotations -const POWER_COLORS = { - AUSTRIA: "#ff1a1a", // Bright red - ENGLAND: "#1a1aff", // Bright blue - FRANCE: "#00bfff", // Sky blue - GERMANY: "#808080", // Gray - ITALY: "#00cc00", // Bright green - RUSSIA: "#e0e0e0", // Light gray/white - TURKEY: "#ffcc00" // Gold/yellow +const POWER_COLORS: Record = { + [PowerENUM.AUSTRIA]: "#ff1a1a", // Bright red + [PowerENUM.ENGLAND]: "#1a1aff", // Bright blue + [PowerENUM.FRANCE]: "#00bfff", // Sky blue + [PowerENUM.GERMANY]: "#808080", // Gray + [PowerENUM.ITALY]: "#00cc00", // Bright green + [PowerENUM.RUSSIA]: "#e0e0e0", // Light gray/white + [PowerENUM.TURKEY]: "#ffcc00", // Gold/yellow + [PowerENUM.GLOBAL]: "#ffffff", // White for global + [PowerENUM.EUROPE]: "#ffffff" // White for europe }; // Relationship value mapping @@ -226,7 +229,7 @@ function renderCurrentStandingsView( const units = unitCounts[power] || 0; html += `
- ${power} + ${getPowerDisplayName(power as PowerENUM)} ${centers} SCs, ${units} units
`; }); diff --git a/ai_animation/src/components/twoPowerConversation.ts b/ai_animation/src/components/twoPowerConversation.ts index 4bc90d0..18d9e9f 100644 --- a/ai_animation/src/components/twoPowerConversation.ts +++ b/ai_animation/src/components/twoPowerConversation.ts @@ -1,5 +1,7 @@ import { gameState } from '../gameState'; import { config } from '../config'; +import { getPowerDisplayName } from '../utils/powerNames'; +import { PowerENUM } from '../types/map'; interface ConversationMessage { sender: string; @@ -150,7 +152,7 @@ function createDialogueContainer(power1: string, power2: string, title?: string) // Add title const titleElement = document.createElement('h2'); - titleElement.textContent = title || `Conversation: ${power1} & ${power2}`; + titleElement.textContent = title || `Conversation: ${getPowerDisplayName(power1 as PowerENUM)} & ${getPowerDisplayName(power2 as PowerENUM)}`; titleElement.style.cssText = ` margin: 0 0 20px 0; text-align: center; @@ -190,7 +192,7 @@ function createConversationArea(): HTMLElement { */ function createCloseButton(): HTMLElement { const button = document.createElement('button'); - button.textContent = '×'; + button.textContent = '�'; button.className = 'close-button'; button.style.cssText = ` position: absolute; @@ -303,7 +305,7 @@ function createMessageElement(message: ConversationMessage, power1: string, powe // Sender label const senderLabel = document.createElement('div'); - senderLabel.textContent = message.sender; + senderLabel.textContent = getPowerDisplayName(message.sender as PowerENUM); senderLabel.className = `power-${message.sender.toLowerCase()}`; senderLabel.style.cssText = ` font-size: 12px; diff --git a/ai_animation/src/domElements/chatWindows.ts b/ai_animation/src/domElements/chatWindows.ts index 111d87c..ec91488 100644 --- a/ai_animation/src/domElements/chatWindows.ts +++ b/ai_animation/src/domElements/chatWindows.ts @@ -2,6 +2,8 @@ import * as THREE from "three"; import { gameState } from "../gameState"; import { config } from "../config"; import { advanceToNextPhase } from "../phase"; +import { getPowerDisplayName, getAllPowerDisplayNames } from '../utils/powerNames'; +import { PowerENUM } from '../types/map'; let faceIconCache = {}; // Cache for generated face icons @@ -19,17 +21,17 @@ export function createChatWindows() { chatWindows = {}; // Create a chat window for each power (except the current power) - const powers = ['AUSTRIA', 'ENGLAND', 'FRANCE', 'GERMANY', 'ITALY', 'RUSSIA', 'TURKEY']; + const powers = [PowerENUM.AUSTRIA, PowerENUM.ENGLAND, PowerENUM.FRANCE, PowerENUM.GERMANY, PowerENUM.ITALY, PowerENUM.RUSSIA, PowerENUM.TURKEY]; // Filter out the current power for chat windows const otherPowers = powers.filter(power => power !== gameState.currentPower); // Add a GLOBAL chat window first - createChatWindow('GLOBAL', true); + createChatWindow(PowerENUM.GLOBAL, true); // Create chat windows for each power except the current one otherPowers.forEach(power => { - createChatWindow(power); + createChatWindow(getPowerDisplayName(power)); }); } // Modified to use 3D face icons properly @@ -56,10 +58,10 @@ function createChatWindow(power, isGlobal = false) { const titleElement = document.createElement('span'); if (isGlobal) { titleElement.style.color = '#ffffff'; - titleElement.textContent = 'GLOBAL'; + titleElement.textContent = getPowerDisplayName(PowerENUM.GLOBAL); } else { titleElement.className = `power-${power.toLowerCase()}`; - titleElement.textContent = power; + titleElement.textContent = getPowerDisplayName(power as PowerENUM); } titleElement.style.fontWeight = 'bold'; // Make text more prominent titleElement.style.textShadow = '1px 1px 2px rgba(0,0,0,0.7)'; // Add text shadow for better readability @@ -325,7 +327,7 @@ function addMessageToChat(msg, phaseName, animateWords = false, onComplete = nul const headerSpan = document.createElement('span'); headerSpan.style.fontWeight = 'bold'; headerSpan.className = `power-${senderColor}`; - headerSpan.textContent = `${msg.sender}: `; + headerSpan.textContent = `${getPowerDisplayName(msg.sender as PowerENUM)}: `; messageElement.appendChild(headerSpan); // Create a span for the message content that will be filled word by word @@ -550,17 +552,18 @@ async function generateFaceIcon(power) { offCamera.position.set(0, 0, 50); // Power-specific colors with higher contrast/saturation - const colorMap = { - 'GLOBAL': 0xf5f5f5, // Brighter white - 'AUSTRIA': 0xff0000, // Brighter red - 'ENGLAND': 0x0000ff, // Brighter blue - 'FRANCE': 0x00bfff, // Brighter cyan - 'GERMANY': 0x1a1a1a, // Darker gray for better contrast - 'ITALY': 0x00cc00, // Brighter green - 'RUSSIA': 0xe0e0e0, // Brighter gray - 'TURKEY': 0xffcc00 // Brighter yellow + const colorMap: Record = { + [PowerENUM.GLOBAL]: 0xf5f5f5, // Brighter white + [PowerENUM.AUSTRIA]: 0xff0000, // Brighter red + [PowerENUM.ENGLAND]: 0x0000ff, // Brighter blue + [PowerENUM.FRANCE]: 0x00bfff, // Brighter cyan + [PowerENUM.GERMANY]: 0x1a1a1a, // Darker gray for better contrast + [PowerENUM.ITALY]: 0x00cc00, // Brighter green + [PowerENUM.RUSSIA]: 0xe0e0e0, // Brighter gray + [PowerENUM.TURKEY]: 0xffcc00, // Brighter yellow + [PowerENUM.EUROPE]: 0xf5f5f5, // Same as global }; - const headColor = colorMap[power] || 0x808080; + const headColor = colorMap[power as PowerENUM] || 0x808080; // Larger head geometry const headGeom = new THREE.BoxGeometry(20, 20, 20); // Increased from 16x16x16 diff --git a/ai_animation/src/domElements/relationshipPopup.ts b/ai_animation/src/domElements/relationshipPopup.ts index 7f4db0a..b23f977 100644 --- a/ai_animation/src/domElements/relationshipPopup.ts +++ b/ai_animation/src/domElements/relationshipPopup.ts @@ -3,6 +3,7 @@ import { gameState } from '../gameState'; import { PowerENUM } from '../types/map'; import { GameSchemaType } from '../types/gameState'; import { renderRelationshipHistoryChartView, DisplayType } from '../components/rotatingDisplay'; +import { getPowerDisplayName } from '../utils/powerNames'; // DOM element references let relationshipPopupContainer: HTMLElement | null = null; @@ -150,7 +151,7 @@ function renderRelationshipChart(): void { const powerHeader = document.createElement('h3'); powerHeader.className = `power-${power.toLowerCase()}`; - powerHeader.textContent = power; + powerHeader.textContent = getPowerDisplayName(power as PowerENUM); powerContainer.appendChild(powerHeader); const chartContainer = document.createElement('div'); @@ -199,7 +200,7 @@ function renderRelationshipChart(): void { const powerHeader = document.createElement('h3'); powerHeader.className = `power-${power.toLowerCase()}`; - powerHeader.textContent = power; + powerHeader.textContent = getPowerDisplayName(power as PowerENUM); powerContainer.appendChild(powerHeader); const chartContainer = document.createElement('div'); diff --git a/ai_animation/src/domElements/standingsBoard.ts b/ai_animation/src/domElements/standingsBoard.ts index ee0e105..d5da89c 100644 --- a/ai_animation/src/domElements/standingsBoard.ts +++ b/ai_animation/src/domElements/standingsBoard.ts @@ -2,6 +2,8 @@ import { StandingsData, StandingsEntry, SortBy, SortDirection, SortOptions } fro import { gameState } from '../gameState'; import { logger } from '../logger'; import { standingsBtn } from '../domElements'; +import { getPowerDisplayName } from '../utils/powerNames'; +import { PowerENUM } from '../types/map'; // DOM element references let standingsBoardContainer: HTMLElement | null = null; @@ -170,7 +172,7 @@ function renderStandingsTable(): void { // Power column headers standingsData.powers.forEach(power => { const th = document.createElement('th'); - th.textContent = power; + th.textContent = getPowerDisplayName(power as PowerENUM); th.className = `power-header power-${power.toLowerCase()}`; th.addEventListener('click', () => sortTable(`power_${power}`)); headerRow.appendChild(th); diff --git a/ai_animation/src/logger.ts b/ai_animation/src/logger.ts index 748e498..d35e868 100644 --- a/ai_animation/src/logger.ts +++ b/ai_animation/src/logger.ts @@ -1,4 +1,6 @@ import { gameState } from "./gameState"; +import { getPowerDisplayName } from './utils/powerNames'; +import { PowerENUM } from './types/map'; class Logger { get infoPanel() { @@ -34,18 +36,18 @@ class Logger { const scCounts = this.getSupplyCenterCounts(); this.infoPanel.innerHTML = ` -
Power: ${gameState.currentPower}
+
Power: ${getPowerDisplayName(gameState.currentPower)}
Current Phase: ${phaseName}

Supply Center Counts

`; diff --git a/ai_animation/src/utils/powerNames.ts b/ai_animation/src/utils/powerNames.ts new file mode 100644 index 0000000..e8a89c6 --- /dev/null +++ b/ai_animation/src/utils/powerNames.ts @@ -0,0 +1,67 @@ +import { PowerENUM } from '../types/map'; +import { gameState } from '../gameState'; + +/** + * Resolves a power name for display purposes. + * If model names are available in moments data for the current game, uses those. + * Otherwise falls back to the PowerENUM value. + * + * @param power - The PowerENUM value to resolve + * @returns The display name for the power + */ +export function getPowerDisplayName(power: PowerENUM | string): string { + // Convert string to PowerENUM if needed + const powerEnum = typeof power === 'string' ? power.toUpperCase() as PowerENUM : power; + + // Special handling for GLOBAL and EUROPE which aren't models + if (powerEnum === PowerENUM.GLOBAL || powerEnum === PowerENUM.EUROPE) { + return powerEnum; + } + + // Check if we have moments data with power_models for the current game + if (gameState.momentsData?.power_models && powerEnum in gameState.momentsData.power_models) { + const modelName = gameState.momentsData.power_models[powerEnum]; + if (modelName) { + return modelName; + } + } + + // Fall back to the PowerENUM value + return powerEnum; +} + +/** + * Gets all power display names as an array. + * Useful for creating UI elements that need to iterate over all powers. + * + * @returns Array of power display names in the standard order + */ +export function getAllPowerDisplayNames(): string[] { + const standardPowers = [ + PowerENUM.AUSTRIA, + PowerENUM.ENGLAND, + PowerENUM.FRANCE, + PowerENUM.GERMANY, + PowerENUM.ITALY, + PowerENUM.RUSSIA, + PowerENUM.TURKEY + ]; + + return standardPowers.map(power => getPowerDisplayName(power)); +} + +/** + * Gets a mapping from PowerENUM to display names. + * Useful for configurations that need both the enum value and display name. + * + * @returns Object mapping PowerENUM values to display names + */ +export function getPowerDisplayNameMapping(): Record { + const mapping: Record = {} as Record; + + Object.values(PowerENUM).forEach(power => { + mapping[power] = getPowerDisplayName(power); + }); + + return mapping; +} \ No newline at end of file