AI_Diplomacy/diplomacy/animation/renderer/OrderVisualizer.js
2025-03-04 11:35:02 -08:00

400 lines
No EOL
14 KiB
JavaScript

// ==============================================================================
// Copyright (C) 2023
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option) any
// later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
// details.
//
// You should have received a copy of the GNU Affero General Public License along
// with this program. If not, see <https://www.gnu.org/licenses/>.
// ==============================================================================
import * as THREE from 'three';
/**
* OrderVisualizer class for parsing and visualizing Diplomacy orders
*/
export class OrderVisualizer {
/**
* Initialize the order visualizer
* @param {AnimationEffects} animationEffects - Animation effects instance
* @param {CoordinateMapper} coordinateMapper - Coordinate mapper instance
*/
constructor(animationEffects, coordinateMapper) {
this.animationEffects = animationEffects;
this.coordinateMapper = coordinateMapper;
// Track visualized orders
this.visualizedOrders = new Map();
}
/**
* Parse and visualize a Diplomacy order
* @param {Object} orderData - The order data
* @param {string} orderData.text - The order text (e.g., "A PAR-BUR")
* @param {string} orderData.power - The power giving the order
* @param {boolean} orderData.success - Whether the order was successful
* @returns {Object} The created visual elements
*/
visualizeOrder(orderData) {
const { text, power, success = true } = orderData;
// Parse the order
const parsedOrder = this._parseOrderText(text);
if (!parsedOrder) {
console.error(`Failed to parse order: ${text}`);
return null;
}
// Create visualization based on order type
let visualization;
switch (parsedOrder.type) {
case 'move':
visualization = this._visualizeMoveOrder(parsedOrder, power, success);
break;
case 'hold':
visualization = this._visualizeHoldOrder(parsedOrder, power, success);
break;
case 'support':
visualization = this._visualizeSupportOrder(parsedOrder, power, success);
break;
case 'convoy':
visualization = this._visualizeConvoyOrder(parsedOrder, power, success);
break;
default:
console.warn(`Unsupported order type: ${parsedOrder.type}`);
return null;
}
// Store the visualization for potential cleanup
if (visualization) {
this.visualizedOrders.set(text, visualization);
}
return visualization;
}
/**
* Parse Diplomacy order text into structured format
* @param {string} orderText - The order text (e.g., "A PAR-BUR")
* @returns {Object} Parsed order object
* @private
*/
_parseOrderText(orderText) {
// Remove any extra whitespace
const text = orderText.trim();
// ==================
// Move order: A PAR-BUR, F ION-ADR
// ==================
const moveRegex = /^([AF])\s+([A-Z]{3}(?:\/[A-Z]{2})?)-([A-Z]{3}(?:\/[A-Z]{2})?)$/;
const moveMatch = text.match(moveRegex);
if (moveMatch) {
return {
type: 'move',
unitType: moveMatch[1], // A or F
location: moveMatch[2], // Source location
destination: moveMatch[3] // Destination location
};
}
// ==================
// Hold order: A PAR H, F ION H
// ==================
const holdRegex = /^([AF])\s+([A-Z]{3}(?:\/[A-Z]{2})?)\s+H$/;
const holdMatch = text.match(holdRegex);
if (holdMatch) {
return {
type: 'hold',
unitType: holdMatch[1], // A or F
location: holdMatch[2] // Location
};
}
// ==================
// Support hold order: A PAR S A BUR, F ION S F ADR
// ==================
const supportHoldRegex = /^([AF])\s+([A-Z]{3}(?:\/[A-Z]{2})?)\s+S\s+([AF])\s+([A-Z]{3}(?:\/[A-Z]{2})?)$/;
const supportHoldMatch = text.match(supportHoldRegex);
if (supportHoldMatch) {
return {
type: 'support',
unitType: supportHoldMatch[1], // A or F
location: supportHoldMatch[2], // Supporting unit location
supportedUnitType: supportHoldMatch[3], // Supported unit type
supportedLocation: supportHoldMatch[4], // Supported unit location
supportType: 'hold'
};
}
// ==================
// Support move order: A PAR S A MUN-BUR, F ION S F TYS-NAP
// ==================
const supportMoveRegex = /^([AF])\s+([A-Z]{3}(?:\/[A-Z]{2})?)\s+S\s+([AF])\s+([A-Z]{3}(?:\/[A-Z]{2})?)-([A-Z]{3}(?:\/[A-Z]{2})?)$/;
const supportMoveMatch = text.match(supportMoveRegex);
if (supportMoveMatch) {
return {
type: 'support',
unitType: supportMoveMatch[1], // A or F
from: supportMoveMatch[2], // Supporting unit location
supportedUnitType: supportMoveMatch[3], // Supported unit type
supportedFrom: supportMoveMatch[4], // Supported unit source
supportedTo: supportMoveMatch[5], // Supported unit destination
supportType: 'move'
};
}
// ==================
// Convoy order: F NTH C A LON-BEL
// ==================
const convoyRegex = /^F\s+([A-Z]{3}(?:\/[A-Z]{2})?)\s+C\s+A\s+([A-Z]{3}(?:\/[A-Z]{2})?)-([A-Z]{3}(?:\/[A-Z]{2})?)$/;
const convoyMatch = text.match(convoyRegex);
if (convoyMatch) {
return {
type: 'convoy',
from: convoyMatch[1], // Convoying fleet location
convoyedFrom: convoyMatch[2], // Convoyed army source
convoyedTo: convoyMatch[3] // Convoyed army destination
};
}
// If no match found, return null
return null;
}
/**
* Visualize a move order
* @param {Object} parsedOrder - Parsed order object
* @param {string} power - The power giving the order
* @param {boolean} success - Whether the order was successful
* @returns {Object} Created visual elements
* @private
*/
_visualizeMoveOrder(parsedOrder, power, success) {
const { unitType, location, destination } = parsedOrder;
// Extract locations
const from = location;
const to = destination;
// Get positions for the locations
const fromPos = this.coordinateMapper.getPositionForLocation(from);
const toPos = this.coordinateMapper.getPositionForLocation(to);
if (!fromPos || !toPos) {
console.error(`[OrderVisualizer] Invalid locations for move order: ${from} to ${to}`);
return null;
}
try {
// Create path with arc between positions
const pathPoints = this.coordinateMapper.getPathBetween(from, to, 10, 30);
if (!pathPoints || pathPoints.length < 2) {
console.error(`[OrderVisualizer] Failed to generate path between ${from} and ${to}`);
return null;
}
// Create visual elements
const path = this.animationEffects.createMovementPath(pathPoints, 'move', success);
// Handle case where path creation failed
if (!path) {
console.error(`[OrderVisualizer] Failed to create movement path for ${from} to ${to}`);
return null;
}
const arrow = this.animationEffects.createMovementArrow(fromPos, toPos, 'move', success);
// If unsuccessful, add bounce effect at destination
if (!success) {
const bounceEffect = this.animationEffects.createBounceEffect(toPos);
return { path, arrow, bounceEffect };
}
return { path, arrow };
} catch (error) {
console.error(`[OrderVisualizer] Error visualizing move order: ${error.message}`);
return null;
}
}
/**
* Visualize a hold order
* @param {Object} parsedOrder - Parsed order object
* @param {string} power - The power giving the order
* @param {boolean} success - Whether the order was successful
* @returns {Object} Created visual elements
* @private
*/
_visualizeHoldOrder(parsedOrder, power, success) {
const { unitType, location } = parsedOrder;
// Get position for the location
const pos = this.coordinateMapper.getPositionForLocation(location);
if (!pos) {
console.error(`Invalid location for hold order: ${location}`);
return null;
}
// Create hold indicator
const holdIndicator = this.animationEffects.createHoldIndicator(pos, success);
// If unsuccessful, add dislodge effect
if (!success) {
const dislodgeEffect = this.animationEffects.createDislodgeEffect(pos);
return { holdIndicator, dislodgeEffect };
}
return { holdIndicator };
}
/**
* Visualize a support order
* @param {Object} parsedOrder - Parsed order object
* @param {string} power - The power giving the order
* @param {boolean} success - Whether the order was successful
* @returns {Object} Created visual elements
* @private
*/
_visualizeSupportOrder(parsedOrder, power, success) {
const { unitType, from, supportType } = parsedOrder;
// Get position for the supporting unit
const fromPos = this.coordinateMapper.getPositionForLocation(from);
if (!fromPos) {
console.error(`Invalid location for support order: ${from}`);
return null;
}
// Create visual elements based on support type
if (supportType === 'hold') {
const { supportedLocation } = parsedOrder;
const supportedPos = this.coordinateMapper.getPositionForLocation(supportedLocation);
if (!supportedPos) {
console.error(`Invalid supported location: ${supportedLocation}`);
return null;
}
// Create path with arc between positions
const pathPoints = this.coordinateMapper.getPathBetween(from, supportedLocation, 10, 20);
// Create visual elements
const path = this.animationEffects.createMovementPath(pathPoints, 'support', success);
const arrow = this.animationEffects.createMovementArrow(fromPos, supportedPos, 'support', success);
// Create highlight around supported unit
const highlight = this.animationEffects.createTerritoryHighlight(supportedPos, success ? 0xFFC107 : 0xF44336);
return { path, arrow, highlight };
} else if (supportType === 'move') {
const { supportedFrom, supportedTo } = parsedOrder;
const supportedFromPos = this.coordinateMapper.getPositionForLocation(supportedFrom);
const supportedToPos = this.coordinateMapper.getPositionForLocation(supportedTo);
if (!supportedFromPos || !supportedToPos) {
console.error(`Invalid locations for supported move: ${supportedFrom} to ${supportedTo}`);
return null;
}
// Create path to the supporting unit
const pathToSupported = this.coordinateMapper.getPathBetween(from, supportedFrom, 10, 20);
// Create path for the supported move
const pathForSupportedMove = this.coordinateMapper.getPathBetween(supportedFrom, supportedTo, 10, 30);
// Create visual elements
const path1 = this.animationEffects.createMovementPath(pathToSupported, 'support', success);
const path2 = this.animationEffects.createMovementPath(pathForSupportedMove, 'support', success);
const arrow = this.animationEffects.createMovementArrow(supportedFromPos, supportedToPos, 'support', success);
return { pathToUnit: path1, pathForMove: path2, arrow };
}
return null;
}
/**
* Visualize a convoy order
* @param {Object} parsedOrder - Parsed order object
* @param {string} power - The power giving the order
* @param {boolean} success - Whether the order was successful
* @returns {Object} Created visual elements
* @private
*/
_visualizeConvoyOrder(parsedOrder, power, success) {
const { from, convoyedFrom, convoyedTo } = parsedOrder;
// Get positions for the locations
const fleetPos = this.coordinateMapper.getPositionForLocation(from);
const armyFromPos = this.coordinateMapper.getPositionForLocation(convoyedFrom);
const armyToPos = this.coordinateMapper.getPositionForLocation(convoyedTo);
if (!fleetPos || !armyFromPos || !armyToPos) {
console.error(`Invalid locations for convoy order: ${from}, ${convoyedFrom}, ${convoyedTo}`);
return null;
}
// Create paths
const pathToFleet = this.coordinateMapper.getPathBetween(convoyedFrom, from, 10, 20);
const pathFromFleet = this.coordinateMapper.getPathBetween(from, convoyedTo, 10, 20);
// Create visual elements
const path1 = this.animationEffects.createMovementPath(pathToFleet, 'convoy', success);
const path2 = this.animationEffects.createMovementPath(pathFromFleet, 'convoy', success);
// Create highlight around the convoying fleet
const highlight = this.animationEffects.createTerritoryHighlight(fleetPos, success ? 0x9C27B0 : 0xF44336);
return { pathToFleet: path1, pathFromFleet: path2, highlight };
}
/**
* Clear all visualized orders
*/
clearAllVisualizations() {
// Animation effects will handle the actual removal
this.visualizedOrders.clear();
this.animationEffects.clearAllEffects();
}
/**
* Remove visualization for a specific order
* @param {string} orderText - The order text to remove
*/
removeVisualization(orderText) {
const visualization = this.visualizedOrders.get(orderText);
if (visualization) {
// Remove each element in the visualization
Object.values(visualization).forEach(element => {
this.animationEffects.scene.remove(element);
// Remove from active effects
const index = this.animationEffects.activeEffects.indexOf(element);
if (index !== -1) {
this.animationEffects.activeEffects.splice(index, 1);
}
});
this.visualizedOrders.delete(orderText);
}
}
}