diff --git a/ai_animation/.gitignore b/ai_animation/.gitignore index a547bf3..789ec75 100644 --- a/ai_animation/.gitignore +++ b/ai_animation/.gitignore @@ -7,7 +7,7 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* -node_modules +node_modules/ dist dist-ssr *.local @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + +# AI things +.claude/ diff --git a/ai_animation/index.html b/ai_animation/index.html index 90b66b6..3f22307 100644 --- a/ai_animation/index.html +++ b/ai_animation/index.html @@ -16,6 +16,7 @@
+ diff --git a/ai_animation/src/components/relationshipChart.ts b/ai_animation/src/components/relationshipChart.ts new file mode 100644 index 0000000..0e17fd1 --- /dev/null +++ b/ai_animation/src/components/relationshipChart.ts @@ -0,0 +1,273 @@ +import { PowerENUM } from "../types/map"; +import { GameSchemaType } from "../types/gameState"; + +// Relationship value mapping +const RELATIONSHIP_VALUES = { + "Enemy": -2, + "Unfriendly": -1, + "Neutral": 0, + "Friendly": 1, + "Ally": 2, + // Add lowercase versions for case-insensitive matching + "enemy": -2, + "unfriendly": -1, + "neutral": 0, + "friendly": 1, + "ally": 2 +}; +/** + * Render the relationship history chart view + * @param container The container element + * @param gameData The current game data + * @param currentPhaseIndex The current phase index + * @param currentPlayerPower The power the current player is controlling + */ +export function renderRelationshipHistoryChartView( + container: HTMLElement, + gameData: GameSchemaType, + currentPhaseIndex: number, + currentPlayerPower: PowerENUM +): void { + // Create header and description + const header = document.createElement('div'); + header.innerHTML = `Diplomatic Relations (${currentPlayerPower})`; + container.appendChild(header); + + // Prepare data for the chart + const relationshipHistory = []; + const otherPowers = new Set(); + + // Iterate through all phases to collect relationship data + for (let i = 0; i < gameData.phases.length; i++) { + const phase = gameData.phases[i]; + const phaseData: any = { + phaseName: phase.name, + phaseIndex: i + }; + + // Check if agent_relationships exists and has data for current player + if (phase.agent_relationships && + phase.agent_relationships[currentPlayerPower]) { + + console.log(`Phase ${i} (${phase.name}): Found relationships for ${currentPlayerPower}`, + phase.agent_relationships[currentPlayerPower]); + + const relationships = phase.agent_relationships[currentPlayerPower]; + + for (const [power, relation] of Object.entries(relationships)) { + if (power !== currentPlayerPower) { + // Convert relationship string to numeric value + let relationValue = RELATIONSHIP_VALUES[relation as keyof typeof RELATIONSHIP_VALUES]; + + // Default to neutral if the relationship string is not recognized + if (relationValue === undefined) { + relationValue = 0; + console.warn(`Unknown relationship value: ${relation}, defaulting to Neutral (0)`); + } + + console.log(` Relationship ${currentPlayerPower} -> ${power}: ${relation} (${relationValue})`); + + phaseData[power] = relationValue; + otherPowers.add(power); + } + } + } + + relationshipHistory.push(phaseData); + } + + console.log("Collected relationship history:", relationshipHistory); + console.log("Other powers found:", Array.from(otherPowers)); + + // Convert otherPowers Set to Array for easier iteration + const powers = Array.from(otherPowers); + + // Create SVG element + const svgWidth = container.clientWidth; + const svgHeight = 150; + const margin = { top: 10, right: 10, bottom: 20, left: 25 }; + const width = svgWidth - margin.left - margin.right; + const height = svgHeight - margin.top - margin.bottom; + + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("width", "100%"); + svg.setAttribute("height", `${svgHeight}px`); + svg.setAttribute("viewBox", `0 0 100% ${svgHeight}`); + svg.style.overflow = "visible"; + + // Create SVG group for the chart content with margins + const chart = document.createElementNS("http://www.w3.org/2000/svg", "g"); + chart.setAttribute("transform", `translate(${margin.left},${margin.top})`); + svg.appendChild(chart); + + // Create scales + // X scale: map phase index to x position + const xScale = (index: number) => { + const denominator = Math.max(1, relationshipHistory.length - 1); // Avoid division by zero + return margin.left + (index / denominator) * width; + }; + + // Y scale: map relationship value (-2 to 2) to y position + const yScale = (value: number) => margin.top + height / 2 - (value / 2) * (height / 2); + + // Draw axes + // X-axis (middle, represents neutral) + const xAxis = document.createElementNS("http://www.w3.org/2000/svg", "line"); + xAxis.setAttribute("x1", `${margin.left}`); + xAxis.setAttribute("y1", `${yScale(0)}`); + xAxis.setAttribute("x2", `${margin.left + width}`); + xAxis.setAttribute("y2", `${yScale(0)}`); + xAxis.setAttribute("stroke", "#8d5a2b"); + xAxis.setAttribute("stroke-width", "1"); + chart.appendChild(xAxis); + + // Y-axis + const yAxis = document.createElementNS("http://www.w3.org/2000/svg", "line"); + yAxis.setAttribute("x1", `${margin.left}`); + yAxis.setAttribute("y1", `${margin.top}`); + yAxis.setAttribute("x2", `${margin.left}`); + yAxis.setAttribute("y2", `${margin.top + height}`); + yAxis.setAttribute("stroke", "#8d5a2b"); + yAxis.setAttribute("stroke-width", "1"); + chart.appendChild(yAxis); + + // Y-axis ticks and labels + const yTicks = [-2, -1, 0, 1, 2]; + const yTickLabels = ["Enemy", "Unfriendly", "Neutral", "Friendly", "Ally"]; + + for (let i = 0; i < yTicks.length; i++) { + const tick = yTicks[i]; + const label = yTickLabels[i]; + + // Tick line + const tickLine = document.createElementNS("http://www.w3.org/2000/svg", "line"); + tickLine.setAttribute("x1", `${margin.left - 5}`); + tickLine.setAttribute("y1", `${yScale(tick)}`); + tickLine.setAttribute("x2", `${margin.left}`); + tickLine.setAttribute("y2", `${yScale(tick)}`); + tickLine.setAttribute("stroke", "#8d5a2b"); + tickLine.setAttribute("stroke-width", "1"); + chart.appendChild(tickLine); + + // Tick label + const tickLabel = document.createElementNS("http://www.w3.org/2000/svg", "text"); + tickLabel.setAttribute("x", `${margin.left - 8}`); + tickLabel.setAttribute("y", `${yScale(tick) + 4}`); + tickLabel.setAttribute("text-anchor", "end"); + tickLabel.setAttribute("font-size", "9"); + tickLabel.setAttribute("fill", "#3b2c02"); + tickLabel.textContent = label; + chart.appendChild(tickLabel); + } + + // Draw horizontal grid lines + for (const tick of yTicks) { + const gridLine = document.createElementNS("http://www.w3.org/2000/svg", "line"); + gridLine.setAttribute("x1", `${margin.left}`); + gridLine.setAttribute("y1", `${yScale(tick)}`); + gridLine.setAttribute("x2", `${margin.left + width}`); + gridLine.setAttribute("y2", `${yScale(tick)}`); + gridLine.setAttribute("stroke", "#d3bf96"); + gridLine.setAttribute("stroke-width", "0.5"); + gridLine.setAttribute("stroke-dasharray", "3,3"); + chart.appendChild(gridLine); + } + + // Draw lines for each power + for (const power of powers) { + console.log(`Drawing line for power: ${power}`); + + // Create path for this power + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + + // Generate path data + let pathData = ""; + let hasData = false; + let dataPoints = 0; + + for (let i = 0; i < relationshipHistory.length; i++) { + if (relationshipHistory[i][power] !== undefined) { + const relationValue = relationshipHistory[i][power]; + const x = xScale(i); + const y = yScale(relationValue); + + console.log(` Point ${i}: (${x}, ${y}) for value ${relationValue}`); + dataPoints++; + + if (!hasData) { + pathData += `M ${x} ${y}`; + hasData = true; + } else { + pathData += ` L ${x} ${y}`; + } + } + } + + console.log(` Total data points for ${power}: ${dataPoints}, has data: ${hasData}`); + console.log(` Path data: ${pathData.length > 100 ? pathData.substring(0, 100) + '...' : pathData}`); + + if (hasData) { + path.setAttribute("d", pathData); + path.setAttribute("stroke", POWER_COLORS[power] || "#000000"); + path.setAttribute("stroke-width", "2"); + path.setAttribute("fill", "none"); + chart.appendChild(path); + console.log(` Added path to chart for ${power}`); + } else { + console.log(` No path data for ${power}, not adding to chart`); + } + } + + // Add a vertical line to indicate current phase + const currentPhaseX = xScale(currentPhaseIndex); + const currentPhaseLine = document.createElementNS("http://www.w3.org/2000/svg", "line"); + currentPhaseLine.setAttribute("x1", `${currentPhaseX}`); + currentPhaseLine.setAttribute("y1", `${margin.top}`); + currentPhaseLine.setAttribute("x2", `${currentPhaseX}`); + currentPhaseLine.setAttribute("y2", `${margin.top + height}`); + currentPhaseLine.setAttribute("stroke", "#000000"); + currentPhaseLine.setAttribute("stroke-width", "1"); + currentPhaseLine.setAttribute("stroke-dasharray", "3,3"); + chart.appendChild(currentPhaseLine); + + // Add legend + const legendGroup = document.createElementNS("http://www.w3.org/2000/svg", "g"); + legendGroup.setAttribute("transform", `translate(${margin.left}, ${margin.top + height + 10})`); + + let legendX = 0; + const legendItemWidth = width / powers.length; + + for (const power of powers) { + // Legend color box + const legendBox = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + legendBox.setAttribute("x", `${legendX}`); + legendBox.setAttribute("y", "0"); + legendBox.setAttribute("width", "10"); + legendBox.setAttribute("height", "10"); + legendBox.setAttribute("fill", POWER_COLORS[power] || "#000000"); + legendGroup.appendChild(legendBox); + + // Legend text + const legendText = document.createElementNS("http://www.w3.org/2000/svg", "text"); + legendText.setAttribute("x", `${legendX + 15}`); + legendText.setAttribute("y", "8"); + legendText.setAttribute("font-size", "9"); + legendText.setAttribute("fill", "#3b2c02"); + legendText.textContent = power; + legendGroup.appendChild(legendText); + + legendX += legendItemWidth; + } + + chart.appendChild(legendGroup); + + // Add the SVG to the container + container.appendChild(svg); + + // Add phase info + const phaseInfo = document.createElement('div'); + phaseInfo.style.fontSize = '12px'; + phaseInfo.style.marginTop = '5px'; + phaseInfo.innerHTML = `Current phase: ${gameData.phases[currentPhaseIndex].name}`; + container.appendChild(phaseInfo); +} diff --git a/ai_animation/src/components/rotatingDisplay.ts b/ai_animation/src/components/rotatingDisplay.ts index a260afe..b268dee 100644 --- a/ai_animation/src/components/rotatingDisplay.ts +++ b/ai_animation/src/components/rotatingDisplay.ts @@ -27,7 +27,13 @@ const RELATIONSHIP_VALUES = { "Unfriendly": -1, "Neutral": 0, "Friendly": 1, - "Ally": 2 + "Ally": 2, + // Add lowercase versions for case-insensitive matching + "enemy": -2, + "unfriendly": -1, + "neutral": 0, + "friendly": 1, + "ally": 2 }; // Module state @@ -43,21 +49,21 @@ let isInitialized = false; */ export function initRotatingDisplay(containerId: string): void { containerElement = document.getElementById(containerId); - + if (!containerElement) { console.error(`Container element with ID "${containerId}" not found`); return; } - + // Set initial display type currentDisplayType = DisplayType.CURRENT_STANDINGS; - + // Start rotation timer startRotationTimer(); - + // Mark as initialized isInitialized = true; - + console.log("Rotating display initialized"); } @@ -69,7 +75,7 @@ function startRotationTimer(): void { if (rotationTimer !== null) { clearInterval(rotationTimer); } - + // Set up new rotation timer rotationTimer = window.setInterval(() => { rotateToNextDisplay(); @@ -85,7 +91,7 @@ function rotateToNextDisplay(): void { console.log("Skipping display rotation during active playback"); return; } - + // Determine next display type switch (currentDisplayType) { case DisplayType.CURRENT_STANDINGS: @@ -98,11 +104,11 @@ function rotateToNextDisplay(): void { currentDisplayType = DisplayType.CURRENT_STANDINGS; break; } - + // Update the display with the new type if (gameState.gameData) { updateRotatingDisplay( - gameState.gameData, + gameState.gameData, gameState.phaseIndex, gameState.currentPower ); @@ -117,7 +123,7 @@ function rotateToNextDisplay(): void { * @param forceUpdate Whether to force a full update even if the display type hasn't changed */ export function updateRotatingDisplay( - gameData: GameSchemaType, + gameData: GameSchemaType, currentPhaseIndex: number, currentPlayerPower: PowerENUM, forceUpdate: boolean = false @@ -126,24 +132,24 @@ export function updateRotatingDisplay( console.warn("Rotating display not initialized"); return; } - + // If we're in the middle of playback animations or speech, defer updates for charts - if (gameState.isPlaying && - (gameState.messagesPlaying || gameState.isSpeaking || gameState.isAnimating) && - currentDisplayType !== DisplayType.CURRENT_STANDINGS) { + if (gameState.isPlaying && + (gameState.messagesPlaying || gameState.isSpeaking || gameState.isAnimating) && + currentDisplayType !== DisplayType.CURRENT_STANDINGS) { console.log("Deferring chart update during active playback"); currentDisplayType = DisplayType.CURRENT_STANDINGS; } - + // Check if we need to do a full re-render if (currentDisplayType !== lastRenderedDisplayType || forceUpdate) { // Clear the container containerElement.innerHTML = ''; - + // Apply fade transition containerElement.style.transition = 'opacity 0.3s ease-out'; containerElement.style.opacity = '0'; - + // Update content after fade-out setTimeout(() => { // Render the appropriate view based on current display type @@ -158,10 +164,10 @@ export function updateRotatingDisplay( renderRelationshipHistoryChartView(containerElement, gameData, currentPhaseIndex, currentPlayerPower); break; } - + // Fade back in containerElement.style.opacity = '1'; - + // Update last rendered type lastRenderedDisplayType = currentDisplayType; }, 300); @@ -175,59 +181,59 @@ export function updateRotatingDisplay( * @param currentPhaseIndex The current phase index */ function renderCurrentStandingsView( - container: HTMLElement, - gameData: GameSchemaType, + container: HTMLElement, + gameData: GameSchemaType, currentPhaseIndex: number ): void { // Get current phase const currentPhase = gameData.phases[currentPhaseIndex]; - + // Get supply center counts const centerCounts: Record = {}; const unitCounts: Record = {}; - + // Count supply centers by power if (currentPhase.state?.centers) { for (const [power, provinces] of Object.entries(currentPhase.state.centers)) { centerCounts[power] = provinces.length; } } - + // Count units by power if (currentPhase.state?.units) { for (const [power, units] of Object.entries(currentPhase.state.units)) { unitCounts[power] = units.length; } } - + // Combine all powers from both centers and units const allPowers = new Set([ ...Object.keys(centerCounts), ...Object.keys(unitCounts) ]); - + // Sort powers by supply center count (descending) const sortedPowers = Array.from(allPowers).sort((a, b) => { return (centerCounts[b] || 0) - (centerCounts[a] || 0); }); - + // Build HTML for view let html = `Council Standings
`; - + sortedPowers.forEach(power => { const centers = centerCounts[power] || 0; const units = unitCounts[power] || 0; - + html += `
${power} ${centers} SCs, ${units} units
`; }); - + // Add victory condition reminder html += `
Victory: 18 supply centers`; - + container.innerHTML = html; } @@ -238,19 +244,19 @@ function renderCurrentStandingsView( * @param currentPhaseIndex The current phase index */ function renderSCHistoryChartView( - container: HTMLElement, - gameData: GameSchemaType, + container: HTMLElement, + gameData: GameSchemaType, currentPhaseIndex: number ): void { // Create header const header = document.createElement('div'); header.innerHTML = `Supply Center History`; container.appendChild(header); - + // Prepare data for the chart - only up to current phase const scHistory = []; const allPowers = new Set(); - + // Iterate through phases up to and including currentPhaseIndex for (let i = 0; i <= currentPhaseIndex; i++) { const phase = gameData.phases[i]; @@ -258,7 +264,7 @@ function renderSCHistoryChartView( phaseName: phase.name, phaseIndex: i }; - + // Count supply centers for each power if (phase.state?.centers) { for (const [power, provinces] of Object.entries(phase.state.centers)) { @@ -266,13 +272,13 @@ function renderSCHistoryChartView( allPowers.add(power); } } - + scHistory.push(phaseData); } - + // Convert allPowers Set to Array for easier iteration const powers = Array.from(allPowers); - + // Find the maximum SC count across all phases and powers let maxSCCount = 0; for (const phaseData of scHistory) { @@ -283,38 +289,38 @@ function renderSCHistoryChartView( } } } - + // Set a minimum max count of 18 (victory condition) maxSCCount = Math.max(maxSCCount, 18); - + // Create SVG element const svgWidth = container.clientWidth; const svgHeight = 150; const margin = { top: 10, right: 10, bottom: 20, left: 25 }; const width = svgWidth - margin.left - margin.right; const height = svgHeight - margin.top - margin.bottom; - + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("width", "100%"); svg.setAttribute("height", `${svgHeight}px`); svg.setAttribute("viewBox", `0 0 ${svgWidth} ${svgHeight}`); svg.style.overflow = "visible"; - + // Create SVG group for the chart content with margins const chart = document.createElementNS("http://www.w3.org/2000/svg", "g"); chart.setAttribute("transform", `translate(${margin.left},${margin.top})`); svg.appendChild(chart); - + // Create scales // X scale: map phase index to x position, handle case where there's only one phase const xScale = (index: number) => { const denominator = Math.max(1, scHistory.length - 1); // Avoid division by zero return margin.left + (index / denominator) * width; }; - + // Y scale: map SC count to y position (inverted, 0 at bottom) const yScale = (count: number) => margin.top + height - (count / maxSCCount) * height; - + // Draw axes // X-axis const xAxis = document.createElementNS("http://www.w3.org/2000/svg", "line"); @@ -325,7 +331,7 @@ function renderSCHistoryChartView( xAxis.setAttribute("stroke", "#8d5a2b"); xAxis.setAttribute("stroke-width", "1"); chart.appendChild(xAxis); - + // Y-axis const yAxis = document.createElementNS("http://www.w3.org/2000/svg", "line"); yAxis.setAttribute("x1", `${margin.left}`); @@ -335,7 +341,7 @@ function renderSCHistoryChartView( yAxis.setAttribute("stroke", "#8d5a2b"); yAxis.setAttribute("stroke-width", "1"); chart.appendChild(yAxis); - + // Y-axis ticks and labels const yTicks = [0, 5, 10, 15, 18]; for (const tick of yTicks) { @@ -348,7 +354,7 @@ function renderSCHistoryChartView( tickLine.setAttribute("stroke", "#8d5a2b"); tickLine.setAttribute("stroke-width", "1"); chart.appendChild(tickLine); - + // Tick label const tickLabel = document.createElementNS("http://www.w3.org/2000/svg", "text"); tickLabel.setAttribute("x", `${margin.left - 8}`); @@ -359,33 +365,33 @@ function renderSCHistoryChartView( tickLabel.textContent = tick.toString(); chart.appendChild(tickLabel); } - + // Draw lines for each power for (const power of powers) { // Create path for this power const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); - + // Generate path data let pathData = ""; for (let i = 0; i < scHistory.length; i++) { const scCount = scHistory[i][power] || 0; const x = xScale(i); const y = yScale(scCount); - + if (i === 0) { pathData += `M ${x} ${y}`; } else { pathData += ` L ${x} ${y}`; } } - + path.setAttribute("d", pathData); path.setAttribute("stroke", POWER_COLORS[power] || "#000000"); path.setAttribute("stroke-width", "2"); path.setAttribute("fill", "none"); chart.appendChild(path); } - + // Add a vertical line to indicate current phase const currentPhaseX = xScale(currentPhaseIndex); const currentPhaseLine = document.createElementNS("http://www.w3.org/2000/svg", "line"); @@ -397,14 +403,14 @@ function renderSCHistoryChartView( currentPhaseLine.setAttribute("stroke-width", "1"); currentPhaseLine.setAttribute("stroke-dasharray", "3,3"); chart.appendChild(currentPhaseLine); - + // Add legend const legendGroup = document.createElementNS("http://www.w3.org/2000/svg", "g"); legendGroup.setAttribute("transform", `translate(${margin.left}, ${margin.top + height + 10})`); - + let legendX = 0; const legendItemWidth = width / powers.length; - + for (const power of powers) { // Legend color box const legendBox = document.createElementNS("http://www.w3.org/2000/svg", "rect"); @@ -414,7 +420,7 @@ function renderSCHistoryChartView( legendBox.setAttribute("height", "10"); legendBox.setAttribute("fill", POWER_COLORS[power] || "#000000"); legendGroup.appendChild(legendBox); - + // Legend text const legendText = document.createElementNS("http://www.w3.org/2000/svg", "text"); legendText.setAttribute("x", `${legendX + 15}`); @@ -423,15 +429,15 @@ function renderSCHistoryChartView( legendText.setAttribute("fill", "#3b2c02"); legendText.textContent = power; legendGroup.appendChild(legendText); - + legendX += legendItemWidth; } - + chart.appendChild(legendGroup); - + // Add the SVG to the container container.appendChild(svg); - + // Add phase info const phaseInfo = document.createElement('div'); phaseInfo.style.fontSize = '12px'; @@ -447,9 +453,9 @@ function renderSCHistoryChartView( * @param currentPhaseIndex The current phase index * @param currentPlayerPower The power the current player is controlling */ -function renderRelationshipHistoryChartView( - container: HTMLElement, - gameData: GameSchemaType, +export function renderRelationshipHistoryChartView( + container: HTMLElement, + gameData: GameSchemaType, currentPhaseIndex: number, currentPlayerPower: PowerENUM ): void { @@ -457,11 +463,11 @@ function renderRelationshipHistoryChartView( const header = document.createElement('div'); header.innerHTML = `Diplomatic Relations (${currentPlayerPower})`; container.appendChild(header); - + // Prepare data for the chart const relationshipHistory = []; const otherPowers = new Set(); - + // Iterate through all phases to collect relationship data for (let i = 0; i < gameData.phases.length; i++) { const phase = gameData.phases[i]; @@ -469,64 +475,72 @@ function renderRelationshipHistoryChartView( phaseName: phase.name, phaseIndex: i }; - + // Check if agent_relationships exists and has data for current player - if (phase.agent_relationships && - phase.agent_relationships[currentPlayerPower]) { - + if (phase.agent_relationships && + phase.agent_relationships[currentPlayerPower]) { + + console.log(`Phase ${i} (${phase.name}): Found relationships for ${currentPlayerPower}`, + phase.agent_relationships[currentPlayerPower]); + const relationships = phase.agent_relationships[currentPlayerPower]; - + for (const [power, relation] of Object.entries(relationships)) { if (power !== currentPlayerPower) { // Convert relationship string to numeric value let relationValue = RELATIONSHIP_VALUES[relation as keyof typeof RELATIONSHIP_VALUES]; - + // Default to neutral if the relationship string is not recognized if (relationValue === undefined) { relationValue = 0; console.warn(`Unknown relationship value: ${relation}, defaulting to Neutral (0)`); } - + + console.log(` Relationship ${currentPlayerPower} -> ${power}: ${relation} (${relationValue})`); + phaseData[power] = relationValue; otherPowers.add(power); } } } - + relationshipHistory.push(phaseData); } - + + console.log("Collected relationship history:", relationshipHistory); + console.log("Other powers found:", Array.from(otherPowers)); + // Convert otherPowers Set to Array for easier iteration const powers = Array.from(otherPowers); - + // Create SVG element const svgWidth = container.clientWidth; const svgHeight = 150; const margin = { top: 10, right: 10, bottom: 20, left: 25 }; const width = svgWidth - margin.left - margin.right; const height = svgHeight - margin.top - margin.bottom; - + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("width", "100%"); svg.setAttribute("height", `${svgHeight}px`); svg.setAttribute("viewBox", `0 0 ${svgWidth} ${svgHeight}`); svg.style.overflow = "visible"; - + // Create SVG group for the chart content with margins const chart = document.createElementNS("http://www.w3.org/2000/svg", "g"); chart.setAttribute("transform", `translate(${margin.left},${margin.top})`); svg.appendChild(chart); - + // Create scales // X scale: map phase index to x position const xScale = (index: number) => { const denominator = Math.max(1, relationshipHistory.length - 1); // Avoid division by zero return margin.left + (index / denominator) * width; }; - + // Y scale: map relationship value (-2 to 2) to y position const yScale = (value: number) => margin.top + height / 2 - (value / 2) * (height / 2); - + // Draw axes // X-axis (middle, represents neutral) const xAxis = document.createElementNS("http://www.w3.org/2000/svg", "line"); @@ -537,7 +551,7 @@ function renderRelationshipHistoryChartView( xAxis.setAttribute("stroke", "#8d5a2b"); xAxis.setAttribute("stroke-width", "1"); chart.appendChild(xAxis); - + // Y-axis const yAxis = document.createElementNS("http://www.w3.org/2000/svg", "line"); yAxis.setAttribute("x1", `${margin.left}`); @@ -547,15 +561,15 @@ function renderRelationshipHistoryChartView( yAxis.setAttribute("stroke", "#8d5a2b"); yAxis.setAttribute("stroke-width", "1"); chart.appendChild(yAxis); - + // Y-axis ticks and labels const yTicks = [-2, -1, 0, 1, 2]; const yTickLabels = ["Enemy", "Unfriendly", "Neutral", "Friendly", "Ally"]; - + for (let i = 0; i < yTicks.length; i++) { const tick = yTicks[i]; const label = yTickLabels[i]; - + // Tick line const tickLine = document.createElementNS("http://www.w3.org/2000/svg", "line"); tickLine.setAttribute("x1", `${margin.left - 5}`); @@ -565,7 +579,7 @@ function renderRelationshipHistoryChartView( tickLine.setAttribute("stroke", "#8d5a2b"); tickLine.setAttribute("stroke-width", "1"); chart.appendChild(tickLine); - + // Tick label const tickLabel = document.createElementNS("http://www.w3.org/2000/svg", "text"); tickLabel.setAttribute("x", `${margin.left - 8}`); @@ -576,7 +590,7 @@ function renderRelationshipHistoryChartView( tickLabel.textContent = label; chart.appendChild(tickLabel); } - + // Draw horizontal grid lines for (const tick of yTicks) { const gridLine = document.createElementNS("http://www.w3.org/2000/svg", "line"); @@ -589,22 +603,28 @@ function renderRelationshipHistoryChartView( gridLine.setAttribute("stroke-dasharray", "3,3"); chart.appendChild(gridLine); } - + // Draw lines for each power for (const power of powers) { + console.log(`Drawing line for power: ${power}`); + // Create path for this power const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); - + // Generate path data let pathData = ""; let hasData = false; - + let dataPoints = 0; + for (let i = 0; i < relationshipHistory.length; i++) { if (relationshipHistory[i][power] !== undefined) { const relationValue = relationshipHistory[i][power]; const x = xScale(i); const y = yScale(relationValue); - + + console.log(` Point ${i}: (${x}, ${y}) for value ${relationValue}`); + dataPoints++; + if (!hasData) { pathData += `M ${x} ${y}`; hasData = true; @@ -613,16 +633,22 @@ function renderRelationshipHistoryChartView( } } } - + + console.log(` Total data points for ${power}: ${dataPoints}, has data: ${hasData}`); + console.log(` Path data: ${pathData.length > 100 ? pathData.substring(0, 100) + '...' : pathData}`); + if (hasData) { path.setAttribute("d", pathData); path.setAttribute("stroke", POWER_COLORS[power] || "#000000"); path.setAttribute("stroke-width", "2"); path.setAttribute("fill", "none"); chart.appendChild(path); + console.log(` Added path to chart for ${power}`); + } else { + console.log(` No path data for ${power}, not adding to chart`); } } - + // Add a vertical line to indicate current phase const currentPhaseX = xScale(currentPhaseIndex); const currentPhaseLine = document.createElementNS("http://www.w3.org/2000/svg", "line"); @@ -634,14 +660,14 @@ function renderRelationshipHistoryChartView( currentPhaseLine.setAttribute("stroke-width", "1"); currentPhaseLine.setAttribute("stroke-dasharray", "3,3"); chart.appendChild(currentPhaseLine); - + // Add legend const legendGroup = document.createElementNS("http://www.w3.org/2000/svg", "g"); legendGroup.setAttribute("transform", `translate(${margin.left}, ${margin.top + height + 10})`); - + let legendX = 0; const legendItemWidth = width / powers.length; - + for (const power of powers) { // Legend color box const legendBox = document.createElementNS("http://www.w3.org/2000/svg", "rect"); @@ -651,7 +677,7 @@ function renderRelationshipHistoryChartView( legendBox.setAttribute("height", "10"); legendBox.setAttribute("fill", POWER_COLORS[power] || "#000000"); legendGroup.appendChild(legendBox); - + // Legend text const legendText = document.createElementNS("http://www.w3.org/2000/svg", "text"); legendText.setAttribute("x", `${legendX + 15}`); @@ -660,15 +686,15 @@ function renderRelationshipHistoryChartView( legendText.setAttribute("fill", "#3b2c02"); legendText.textContent = power; legendGroup.appendChild(legendText); - + legendX += legendItemWidth; } - + chart.appendChild(legendGroup); - + // Add the SVG to the container container.appendChild(svg); - + // Add phase info const phaseInfo = document.createElement('div'); phaseInfo.style.fontSize = '12px'; diff --git a/ai_animation/src/config.ts b/ai_animation/src/config.ts index b8125b7..9e5e38c 100644 --- a/ai_animation/src/config.ts +++ b/ai_animation/src/config.ts @@ -3,14 +3,14 @@ */ export const config = { // Default speed in milliseconds for animations and transitions - playbackSpeed: 500, - + playbackSpeed: 500, + // Whether to enable debug mode (faster animations, more console logging) - isDebugMode: false, - + isDebugMode: true, + // Duration of unit movement animation in ms animationDuration: 1500, - + // How frequently to play sound effects (1 = every message, 3 = every third message) soundEffectFrequency: 3 } diff --git a/ai_animation/src/domElements.ts b/ai_animation/src/domElements.ts index 782651f..e3b4445 100644 --- a/ai_animation/src/domElements.ts +++ b/ai_animation/src/domElements.ts @@ -36,6 +36,7 @@ export const phaseDisplay = document.getElementById('phase-display'); export const mapView = document.getElementById('map-view'); export const leaderboard = document.getElementById('leaderboard'); export const standingsBtn = document.getElementById('standings-btn'); +export const relationshipsBtn = document.getElementById('relationships-btn'); // Add roundRect polyfill for browsers that don't support it if (!CanvasRenderingContext2D.prototype.roundRect) { diff --git a/ai_animation/src/domElements/relationshipPopup.ts b/ai_animation/src/domElements/relationshipPopup.ts new file mode 100644 index 0000000..4737234 --- /dev/null +++ b/ai_animation/src/domElements/relationshipPopup.ts @@ -0,0 +1,245 @@ +import { relationshipsBtn } from '../domElements'; +import { gameState } from '../gameState'; +import { PowerENUM } from '../types/map'; +import { GameSchemaType } from '../types/gameState'; +import { renderRelationshipHistoryChartView, DisplayType } from '../components/rotatingDisplay'; + +// DOM element references +let relationshipPopupContainer: HTMLElement | null = null; +let relationshipContent: HTMLElement | null = null; +let closeButton: HTMLElement | null = null; + +/** + * Initialize the relationship popup by creating DOM elements and attaching event handlers + */ +export function initRelationshipPopup(): void { + // Create the container if it doesn't exist + if (!document.getElementById('relationship-popup-container')) { + createRelationshipPopupElements(); + } + + // Get references to the created elements + relationshipPopupContainer = document.getElementById('relationship-popup-container'); + relationshipContent = document.getElementById('relationship-content'); + closeButton = document.getElementById('relationship-close-btn'); + + // Add event listeners + if (closeButton) { + closeButton.addEventListener('click', hideRelationshipPopup); + } + + // Add click handler for the relationships button + if (relationshipsBtn) { + relationshipsBtn.addEventListener('click', toggleRelationshipPopup); + } +} + +/** + * Create all DOM elements needed for the relationship popup + */ +function createRelationshipPopupElements(): void { + const container = document.createElement('div'); + container.id = 'relationship-popup-container'; + container.className = 'relationship-popup-container'; + + // Create header + const header = document.createElement('div'); + header.className = 'relationship-header'; + + const title = document.createElement('h2'); + title.textContent = 'Diplomatic Relations'; + header.appendChild(title); + + const closeBtn = document.createElement('button'); + closeBtn.id = 'relationship-close-btn'; + closeBtn.textContent = '×'; + closeBtn.title = 'Close Relationships Chart'; + header.appendChild(closeBtn); + + container.appendChild(header); + + // Create content container + const content = document.createElement('div'); + content.id = 'relationship-content'; + content.className = 'relationship-content'; + container.appendChild(content); + + // Add to document + document.body.appendChild(container); +} + +/** + * Toggle the visibility of the relationship popup + */ +export function toggleRelationshipPopup(): void { + if (relationshipPopupContainer) { + if (relationshipPopupContainer.classList.contains('visible')) { + hideRelationshipPopup(); + } else { + showRelationshipPopup(); + } + } +} + +/** + * Show the relationship popup + */ +export function showRelationshipPopup(): void { + if (relationshipPopupContainer && relationshipContent) { + relationshipPopupContainer.classList.add('visible'); + + // Only render if we have game data + if (gameState.gameData) { + renderRelationshipChart(); + } else { + relationshipContent.innerHTML = '
No game data loaded. Please load a game to view relationships.
'; + } + } +} + +/** + * Hide the relationship popup + */ +export function hideRelationshipPopup(): void { + if (relationshipPopupContainer) { + relationshipPopupContainer.classList.remove('visible'); + } +} + +/** + * Render the relationship chart in the popup + */ +function renderRelationshipChart(): void { + if (!relationshipContent || !gameState.gameData) return; + + // Clear current content + relationshipContent.innerHTML = ''; + + // Get a list of powers that have relationship data + const powersWithRelationships = new Set(); + + // Check all phases for relationships + if (gameState.gameData && gameState.gameData.phases) { + // Debug what relationship data we have + console.log("Checking for relationship data in game:", gameState.gameData.phases.length, "phases"); + + let hasRelationshipData = false; + for (const phase of gameState.gameData.phases) { + if (phase.agent_relationships) { + console.log("Found relationship data in phase:", phase.name, phase.agent_relationships); + hasRelationshipData = true; + // Add powers that have relationship data defined + Object.keys(phase.agent_relationships).forEach(power => { + powersWithRelationships.add(power); + }); + } + } + + if (!hasRelationshipData) { + console.log("No relationship data found in any phase"); + } + } + + // Create a container for each power's relationship chart + for (const power of Object.values(PowerENUM)) { + // Skip any non-string values + if (typeof power !== 'string') continue; + + // Check if this power has relationship data + if (powersWithRelationships.has(power)) { + const powerContainer = document.createElement('div'); + powerContainer.className = `power-relationship-container power-${power.toLowerCase()}`; + + const powerHeader = document.createElement('h3'); + powerHeader.className = `power-${power.toLowerCase()}`; + powerHeader.textContent = power; + powerContainer.appendChild(powerHeader); + + const chartContainer = document.createElement('div'); + chartContainer.className = 'relationship-chart-container'; + + // Use the existing chart rendering function + renderRelationshipHistoryChartView( + chartContainer, + gameState.gameData, + gameState.phaseIndex, + power as PowerENUM + ); + + powerContainer.appendChild(chartContainer); + relationshipContent.appendChild(powerContainer); + } + } + + // If no powers have relationship data, create some sample data for visualization + if (powersWithRelationships.size === 0) { + console.log("No relationship data found in game, creating sample data for visualization"); + + // Create sample relationship data for all powers in the game + const allPowers = new Set(); + + // Find all powers from units and centers + if (gameState.gameData && gameState.gameData.phases && gameState.gameData.phases.length > 0) { + const currentPhase = gameState.gameData.phases[gameState.phaseIndex]; + + if (currentPhase.state?.units) { + Object.keys(currentPhase.state.units).forEach(power => allPowers.add(power)); + } + + if (currentPhase.state?.centers) { + Object.keys(currentPhase.state.centers).forEach(power => allPowers.add(power)); + } + + // Only proceed if we found some powers + if (allPowers.size > 0) { + console.log(`Found ${allPowers.size} powers in game, creating sample relationships`); + + // For each power, create a container and chart + for (const power of allPowers) { + const powerContainer = document.createElement('div'); + powerContainer.className = `power-relationship-container power-${power.toLowerCase()}`; + + const powerHeader = document.createElement('h3'); + powerHeader.className = `power-${power.toLowerCase()}`; + powerHeader.textContent = power; + powerContainer.appendChild(powerHeader); + + const chartContainer = document.createElement('div'); + chartContainer.className = 'relationship-chart-container'; + + // Create a message about sample data + const sampleMessage = document.createElement('div'); + sampleMessage.className = 'sample-data-message'; + sampleMessage.innerHTML = `Note: No relationship data found for ${power}. + This chart will display when relationship data is available.`; + + chartContainer.appendChild(sampleMessage); + powerContainer.appendChild(chartContainer); + relationshipContent.appendChild(powerContainer); + } + } else { + // If we couldn't find any powers, show the no data message + const noDataMsg = document.createElement('div'); + noDataMsg.className = 'no-data-message'; + noDataMsg.textContent = 'No relationship data available in this game file.'; + relationshipContent.appendChild(noDataMsg); + } + } else { + // If no phases, show the no data message + const noDataMsg = document.createElement('div'); + noDataMsg.className = 'no-data-message'; + noDataMsg.textContent = 'No relationship data available in this game file.'; + relationshipContent.appendChild(noDataMsg); + } + } +} + +/** + * Update the relationship popup when game data changes + */ +export function updateRelationshipPopup(): void { + if (relationshipPopupContainer && + relationshipPopupContainer.classList.contains('visible')) { + renderRelationshipChart(); + } +} \ No newline at end of file diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts index 7eca9fe..7fe55b4 100644 --- a/ai_animation/src/gameState.ts +++ b/ai_animation/src/gameState.ts @@ -165,6 +165,35 @@ class GameState { }); }) } + + /** + * Check if a power is present in the current game + * @param power The power to check + * @returns True if the power is present in the current phase + */ + isPowerInGame = (power: string): boolean => { + if (!this.gameData || !this.gameData.phases || this.phaseIndex < 0 || this.phaseIndex >= this.gameData.phases.length) { + return false; + } + + const currentPhase = this.gameData.phases[this.phaseIndex]; + + // Check if power has units or centers in the current phase + if (currentPhase.state?.units && power in currentPhase.state.units) { + return true; + } + + if (currentPhase.state?.centers && power in currentPhase.state.centers) { + return true; + } + + // Check if power has relationships defined + if (currentPhase.agent_relationships && power in currentPhase.agent_relationships) { + return true; + } + + return false; + } initScene = () => { this.scene.background = new THREE.Color(0x87CEEB); diff --git a/ai_animation/src/main.ts b/ai_animation/src/main.ts index 1473002..10789bc 100644 --- a/ai_animation/src/main.ts +++ b/ai_animation/src/main.ts @@ -7,6 +7,7 @@ import { logger } from "./logger"; import { loadBtn, prevBtn, nextBtn, speedSelector, fileInput, playBtn, mapView, loadGameBtnFunction } from "./domElements"; import { updateChatWindows } from "./domElements/chatWindows"; import { initStandingsBoard, hideStandingsBoard, showStandingsBoard } from "./domElements/standingsBoard"; +import { initRelationshipPopup, hideRelationshipPopup, updateRelationshipPopup } from "./domElements/relationshipPopup"; import { displayPhaseWithAnimation, advanceToNextPhase, resetToPhase } from "./phase"; import { config } from "./config"; import { Tween, Group, Easing } from "@tweenjs/tween.js"; @@ -39,6 +40,9 @@ function initScene() { // Initialize standings board initStandingsBoard(); + // Initialize relationship popup + initRelationshipPopup(); + // Load coordinate data, then build the map gameState.loadBoardState().then(() => { initMap(gameState.scene).then(() => { @@ -197,7 +201,7 @@ function loadDefaultGameFile() { console.log("Loading default game file for debug mode..."); // Path to the default game file - const defaultGameFilePath = './default_game.json'; + const defaultGameFilePath = './default_game2.json'; fetch(defaultGameFilePath) .then(response => { @@ -226,9 +230,10 @@ function loadDefaultGameFile() { console.log("Default game file loaded and parsed successfully"); // Explicitly hide standings board after loading game hideStandingsBoard(); - // Update rotating display with game data + // Update rotating display and relationship popup with game data if (gameState.gameData) { updateRotatingDisplay(gameState.gameData, gameState.phaseIndex, gameState.currentPower); + updateRelationshipPopup(); } }) .catch(error => { @@ -302,9 +307,10 @@ fileInput.addEventListener('change', e => { loadGameBtnFunction(file); // Explicitly hide standings board after loading game hideStandingsBoard(); - // Update rotating display with game data + // Update rotating display and relationship popup with game data if (gameState.gameData) { updateRotatingDisplay(gameState.gameData, gameState.phaseIndex, gameState.currentPower); + updateRelationshipPopup(); } } }); diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index 91e46a3..24e4188 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -4,6 +4,7 @@ import { updatePhaseDisplay } from "./domElements"; import { initUnits } from "./units/create"; import { updateSupplyCenterOwnership, updateLeaderboard, updateMapOwnership } from "./map/state"; import { updateChatWindows, addToNewsBanner } from "./domElements/chatWindows"; +import { updateRelationshipPopup } from "./domElements/relationshipPopup"; import { createAnimationsForNextPhase } from "./units/animate"; import { speakSummary } from "./speech"; import { config } from "./config"; @@ -42,6 +43,7 @@ export function displayPhase(skipMessages = false) { // Update UI elements with smooth transitions updateLeaderboard(currentPhase); updateMapOwnership(); + updateRelationshipPopup(); // Add phase info to news banner if not already there const phaseBannerText = `Phase: ${currentPhase.name}`; diff --git a/ai_animation/src/speech.ts b/ai_animation/src/speech.ts index a6abcc5..a1d4d9b 100644 --- a/ai_animation/src/speech.ts +++ b/ai_animation/src/speech.ts @@ -1,5 +1,7 @@ import { gameState } from "./gameState"; import { config } from "./config"; +// TODO: We need to get these pieces of audio ahead of time, instead of paying for them each time we load the front end +// These pieces of audio are predetermined. // --- ElevenLabs Text-to-Speech configuration --- let ELEVENLABS_API_KEY = ''; diff --git a/ai_animation/src/style.css b/ai_animation/src/style.css index f27507b..16726b4 100644 --- a/ai_animation/src/style.css +++ b/ai_animation/src/style.css @@ -615,7 +615,130 @@ /* Media query for mobile devices */ @media (max-width: 768px) { - #standings-btn { + #standings-btn, #relationships-btn { margin-top: 5px; } } + + /* ----------------- + Relationship Popup + ----------------- */ + .relationship-popup-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.85); + z-index: 1000; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; + font-family: "Book Antiqua", Palatino, serif; + overflow-y: auto; + } + + .relationship-popup-container.visible { + opacity: 1; + visibility: visible; + } + + .relationship-header { + width: 90%; + display: flex; + justify-content: space-between; + align-items: center; + margin: 20px 0; + padding: 10px 20px; + background: linear-gradient(90deg, #5a3e2b 0%, #382519 100%); + border: 2px solid #2e1c10; + border-radius: 8px; + box-shadow: 0 4px 10px rgba(0,0,0,0.5); + } + + .relationship-header h2 { + margin: 0; + color: #f0e6d2; + font-size: 28px; + text-shadow: 1px 1px 2px #000; + letter-spacing: 1px; + } + + #relationship-close-btn { + background-color: transparent; + color: #f0e6d2; + font-size: 24px; + border: none; + padding: 5px 10px; + cursor: pointer; + transition: color 0.2s; + } + + #relationship-close-btn:hover { + color: #ff9e54; + } + + .relationship-content { + width: 90%; + max-width: 1200px; + display: flex; + flex-wrap: wrap; + justify-content: space-around; + gap: 20px; + margin-bottom: 20px; + } + + .power-relationship-container { + background: radial-gradient(ellipse at center, #f7ecd1 0%, #dbc08c 100%); + border: 3px solid #4f3b16; + border-radius: 8px; + padding: 15px; + width: 45%; + min-width: 400px; + margin-bottom: 20px; + box-shadow: 0 0 15px rgba(0,0,0,0.5); + } + + .power-relationship-container h3 { + margin-top: 0; + text-align: center; + font-size: 20px; + margin-bottom: 10px; + } + + .relationship-chart-container { + width: 100%; + } + + .no-data-message { + padding: 20px; + background: radial-gradient(ellipse at center, #f7ecd1 0%, #dbc08c 100%); + border: 3px solid #4f3b16; + border-radius: 8px; + color: #3b2c02; + text-align: center; + font-size: 18px; + } + + .sample-data-message { + padding: 15px; + background-color: rgba(255, 255, 200, 0.5); + border: 1px dashed #8d5a2b; + border-radius: 6px; + color: #3b2c02; + text-align: center; + font-size: 14px; + margin: 10px 0; + } + + /* Mobile responsiveness for relationship popup */ + @media (max-width: 900px) { + .power-relationship-container { + width: 90%; + min-width: 300px; + } + }