diff --git a/ai_animation/index.html b/ai_animation/index.html index 7fe3e57..72e1cba 100644 --- a/ai_animation/index.html +++ b/ai_animation/index.html @@ -39,6 +39,11 @@
Diplomatic actions unfolding...
+ + diff --git a/ai_animation/src/debug/provinceHighlight.ts b/ai_animation/src/debug/provinceHighlight.ts new file mode 100644 index 0000000..3dfa5b3 --- /dev/null +++ b/ai_animation/src/debug/provinceHighlight.ts @@ -0,0 +1,121 @@ +import { gameState } from "../gameState"; +import { ProvinceENUM } from "../types/map"; +import { MeshBasicMaterial } from "three"; + +interface FlashAnimation { + mesh: THREE.Mesh; + originalColor: number; + startTime: number; + duration: number; +} + +let currentFlashAnimation: FlashAnimation | null = null; + +/** + * Highlights a province on the map with a flashing animation + * @param provinceName - The name of the province to highlight (e.g., "PAR", "LON", "BER") + */ +export function highlightProvince(provinceName: string): void { + // Stop any existing animation + if (currentFlashAnimation) { + stopCurrentFlash(); + } + + // Normalize the province name to uppercase + const normalizedName = provinceName.toUpperCase().trim(); + + // Check if it's a valid province + if (!Object.values(ProvinceENUM).includes(normalizedName as ProvinceENUM)) { + console.warn(`Province "${normalizedName}" not found. Valid provinces are:`, Object.values(ProvinceENUM)); + return; + } + + // Find the province in the board state + const province = gameState.boardState.provinces[normalizedName]; + if (!province || !province.mesh) { + console.warn(`Province "${normalizedName}" mesh not found on the map`); + return; + } + + // Get the mesh material + const material = province.mesh.material as MeshBasicMaterial; + const originalColor = material.color.getHex(); + + // Start the flash animation + currentFlashAnimation = { + mesh: province.mesh, + originalColor, + startTime: Date.now(), + duration: 2000 // 2 seconds + }; + + console.log(`Highlighting province: ${normalizedName}`); + + // Start the animation loop + animateFlash(); +} + +/** + * Animates the flashing effect + */ +function animateFlash(): void { + if (!currentFlashAnimation) return; + + const elapsed = Date.now() - currentFlashAnimation.startTime; + const progress = elapsed / currentFlashAnimation.duration; + + if (progress >= 1) { + // Animation complete, restore original color + stopCurrentFlash(); + return; + } + + // Calculate flash intensity using sine wave for smooth pulsing + const flashIntensity = Math.sin(elapsed * 0.01) * 0.5 + 0.5; // 0 to 1 + + // Interpolate between original color and bright yellow + const material = currentFlashAnimation.mesh.material as MeshBasicMaterial; + const originalColor = currentFlashAnimation.originalColor; + + // Extract RGB components from original color + const originalR = (originalColor >> 16) & 255; + const originalG = (originalColor >> 8) & 255; + const originalB = originalColor & 255; + + // Flash to a bright yellow (255, 255, 0) + const flashR = 255; + const flashG = 255; + const flashB = 0; + + // Interpolate between original and flash colors + const r = Math.round(originalR + (flashR - originalR) * flashIntensity); + const g = Math.round(originalG + (flashG - originalG) * flashIntensity); + const b = Math.round(originalB + (flashB - originalB) * flashIntensity); + + // Set the new color + const newColor = (r << 16) | (g << 8) | b; + material.color.setHex(newColor); + + // Continue animation + requestAnimationFrame(animateFlash); +} + +/** + * Stops the current flash animation and restores the original color + */ +function stopCurrentFlash(): void { + if (!currentFlashAnimation) return; + + // Restore original color + const material = currentFlashAnimation.mesh.material as MeshBasicMaterial; + material.color.setHex(currentFlashAnimation.originalColor); + + currentFlashAnimation = null; +} + +/** + * Gets a list of all available province names + */ +export function getAvailableProvinces(): string[] { + return Object.values(ProvinceENUM); +} \ No newline at end of file diff --git a/ai_animation/src/domElements.ts b/ai_animation/src/domElements.ts index 0bcc249..2875893 100644 --- a/ai_animation/src/domElements.ts +++ b/ai_animation/src/domElements.ts @@ -74,6 +74,15 @@ if (null === leaderboard) throw new Error("Element with ID 'leaderboard' not fou export const standingsBtn = document.getElementById('standings-btn'); if (null === standingsBtn) throw new Error("Element with ID 'standings-btn' not found"); +export const debugProvincePanel = document.getElementById('debug-province-panel'); +if (null === debugProvincePanel) throw new Error("Element with ID 'debug-province-panel' not found"); + +export const provinceInput = document.getElementById('province-input') as HTMLInputElement; +if (null === provinceInput) throw new Error("Element with ID 'province-input' not found"); + +export const highlightProvinceBtn = document.getElementById('highlight-province-btn'); +if (null === highlightProvinceBtn) throw new Error("Element with ID 'highlight-province-btn' not found"); + // Add roundRect polyfill for browsers that don't support it diff --git a/ai_animation/src/main.ts b/ai_animation/src/main.ts index f3e077d..b9a907b 100644 --- a/ai_animation/src/main.ts +++ b/ai_animation/src/main.ts @@ -12,6 +12,8 @@ import { Tween, Group, Easing } from "@tweenjs/tween.js"; import { initRotatingDisplay, updateRotatingDisplay } from "./components/rotatingDisplay"; import { closeTwoPowerConversation, showTwoPowerConversation } from "./components/twoPowerConversation"; import { PowerENUM } from "./types/map"; +import { debugProvincePanel, provinceInput, highlightProvinceBtn } from "./domElements"; +import { highlightProvince, getAvailableProvinces } from "./debug/provinceHighlight"; //TODO: Create a function that finds a suitable unit location within a given polygon, for placing units better // Currently the location for label, unit, and SC are all the same manually picked location @@ -47,6 +49,11 @@ function initScene() { if (isDebugMode || isStreamingMode) { gameState.loadGameFile(0); } + + // Show debug province panel if in debug mode + if (isDebugMode) { + debugProvincePanel.style.display = 'block'; + } if (isStreamingMode) { setTimeout(() => { togglePlayback() @@ -288,6 +295,43 @@ speedSelector.addEventListener('change', e => { } }); +// Debug province highlighting event handlers +if (isDebugMode) { + highlightProvinceBtn.addEventListener('click', () => { + const provinceName = provinceInput.value.trim(); + if (provinceName) { + highlightProvince(provinceName); + } else { + console.warn('Please enter a province name'); + } + }); + + // Allow highlighting on Enter key press + provinceInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + const provinceName = provinceInput.value.trim(); + if (provinceName) { + highlightProvince(provinceName); + } + } + }); + + // Add input validation and autocomplete suggestions + provinceInput.addEventListener('input', () => { + const input = provinceInput.value.toUpperCase().trim(); + const availableProvinces = getAvailableProvinces(); + + // Basic validation - turn input red if it doesn't match any province + if (input && !availableProvinces.some(p => p.startsWith(input))) { + provinceInput.style.borderColor = '#ff4444'; + provinceInput.style.backgroundColor = '#ffe6e6'; + } else { + provinceInput.style.borderColor = '#4f3b16'; + provinceInput.style.backgroundColor = '#faf0d8'; + } + }); +} + // --- BOOTSTRAP ON PAGE LOAD --- window.addEventListener('load', initScene); diff --git a/ai_animation/src/style.css b/ai_animation/src/style.css index 88140de..6d5bf15 100644 --- a/ai_animation/src/style.css +++ b/ai_animation/src/style.css @@ -754,3 +754,50 @@ min-width: 300px; } } + + /* ----------------- + Debug Province Panel + ----------------- */ + #debug-province-panel { + position: absolute; + top: 80px; + right: 10px; + background: radial-gradient(ellipse at center, #f7ecd1 0%, #dbc08c 100%); + border: 3px solid #4f3b16; + border-radius: 8px; + padding: 15px; + box-shadow: 0 0 10px rgba(0,0,0,0.4); + z-index: 100; + font-family: "Book Antiqua", Palatino, serif; + } + + #province-input { + padding: 8px 12px; + border: 2px solid #4f3b16; + border-radius: 4px; + background-color: #faf0d8; + color: #2f260b; + font-family: "Book Antiqua", Palatino, serif; + width: 200px; + margin-right: 10px; + } + + #province-input:focus { + outline: none; + border-color: #8d5a2b; + background-color: #fff; + } + + #highlight-province-btn { + padding: 8px 12px; + background-color: #8d5a2b; + color: #f0e6d2; + border: 2px solid #2e1c10; + border-radius: 4px; + cursor: pointer; + font-family: "Book Antiqua", Palatino, serif; + } + + #highlight-province-btn:hover { + background-color: #a4703a; + }