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;
+ }
+ }