mirror of
https://github.com/GoodStartLabs/AI_Diplomacy.git
synced 2026-04-26 17:13:19 +00:00
689 lines
No EOL
22 KiB
HTML
689 lines
No EOL
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Diplomacy Map (Fallback-Only)</title>
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
|
|
"three/examples/jsm/controls/OrbitControls.js": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/controls/OrbitControls.js"
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
}
|
|
.container {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.top-controls {
|
|
padding: 10px;
|
|
background-color: #333;
|
|
color: white;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.map-view {
|
|
flex-grow: 1;
|
|
background-color: #87CEEB;
|
|
position: relative;
|
|
}
|
|
button {
|
|
padding: 8px 16px;
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
margin-right: 10px;
|
|
}
|
|
button:hover {
|
|
background-color: #45a049;
|
|
}
|
|
#phase-display {
|
|
font-weight: bold;
|
|
margin-left: 10px;
|
|
}
|
|
#file-input {
|
|
display: none;
|
|
}
|
|
#info-panel {
|
|
position: absolute;
|
|
top: 60px;
|
|
right: 10px;
|
|
width: 300px;
|
|
background-color: rgba(255, 255, 255, 0.8);
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="top-controls">
|
|
<div>
|
|
<button id="load-btn">Load Game</button>
|
|
<button id="prev-btn" disabled>← Prev</button>
|
|
<button id="next-btn" disabled>Next →</button>
|
|
<span id="phase-display">No game loaded</span>
|
|
</div>
|
|
</div>
|
|
<div id="map-view" class="map-view"></div>
|
|
<input type="file" id="file-input" accept=".json">
|
|
<div id="info-panel"></div>
|
|
</div>
|
|
|
|
<script type="module">
|
|
import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
|
|
// --- CORE VARIABLES ---
|
|
let scene, camera, renderer, controls;
|
|
let gameData = null;
|
|
let currentPhaseIndex = 0;
|
|
let coordinateData = null;
|
|
let unitMeshes = []; // To store references for units + supply center 3D objects
|
|
let mapPlane = null; // The fallback map plane
|
|
|
|
// --- DOM ELEMENTS ---
|
|
const loadBtn = document.getElementById('load-btn');
|
|
const fileInput = document.getElementById('file-input');
|
|
const prevBtn = document.getElementById('prev-btn');
|
|
const nextBtn = document.getElementById('next-btn');
|
|
const phaseDisplay = document.getElementById('phase-display');
|
|
const infoPanel = document.getElementById('info-panel');
|
|
const mapView = document.getElementById('map-view');
|
|
|
|
// --- INITIALIZE SCENE ---
|
|
function initScene() {
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x87CEEB);
|
|
|
|
// Camera
|
|
camera = new THREE.PerspectiveCamera(
|
|
60,
|
|
mapView.clientWidth / mapView.clientHeight,
|
|
1,
|
|
3000
|
|
);
|
|
camera.position.set(0, 800, 800);
|
|
camera.lookAt(0, 0, 0);
|
|
|
|
// Renderer
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(mapView.clientWidth, mapView.clientHeight);
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
mapView.appendChild(renderer.domElement);
|
|
|
|
// Controls
|
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
controls.screenSpacePanning = true;
|
|
controls.minDistance = 100;
|
|
controls.maxDistance = 1500;
|
|
controls.maxPolarAngle = Math.PI / 2; // Limit so you don't flip under the map
|
|
|
|
// Lighting (keep it simple)
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|
scene.add(ambientLight);
|
|
|
|
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
|
|
dirLight.position.set(300, 400, 300);
|
|
scene.add(dirLight);
|
|
|
|
// Load coordinate data, then build the fallback map
|
|
loadCoordinateData()
|
|
.then(() => {
|
|
createFallbackMap(); // Create the map plane from the start
|
|
})
|
|
.catch(err => {
|
|
console.error("Error loading coordinates:", err);
|
|
infoPanel.textContent = `Error loading coords: ${err.message}`;
|
|
});
|
|
|
|
// Kick off animation loop
|
|
animate();
|
|
|
|
// Handle resizing
|
|
window.addEventListener('resize', onWindowResize);
|
|
}
|
|
|
|
// --- ANIMATION LOOP ---
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
// Update any pulsing or wave animations on supply centers or units
|
|
if (scene.userData.animatedObjects) {
|
|
scene.userData.animatedObjects.forEach(obj => {
|
|
if (obj.userData.pulseAnimation) {
|
|
const anim = obj.userData.pulseAnimation;
|
|
anim.time += anim.speed;
|
|
if (obj.userData.glowMesh) {
|
|
const pulseValue = Math.sin(anim.time) * anim.intensity + 0.5;
|
|
obj.userData.glowMesh.material.opacity = 0.2 + (pulseValue * 0.3);
|
|
obj.userData.glowMesh.scale.set(
|
|
1 + (pulseValue * 0.1),
|
|
1 + (pulseValue * 0.1),
|
|
1 + (pulseValue * 0.1)
|
|
);
|
|
}
|
|
// Subtle bobbing up/down
|
|
obj.position.y = 2 + Math.sin(anim.time) * 0.5;
|
|
}
|
|
});
|
|
}
|
|
|
|
controls.update();
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
// --- RESIZE HANDLER ---
|
|
function onWindowResize() {
|
|
camera.aspect = mapView.clientWidth / mapView.clientHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(mapView.clientWidth, mapView.clientHeight);
|
|
}
|
|
|
|
// --- LOAD COORDINATE DATA ---
|
|
function loadCoordinateData() {
|
|
return new Promise((resolve, reject) => {
|
|
fetch('./assets/maps/standard_coords.json')
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
// Try an alternate path if desired
|
|
return fetch('../assets/maps/standard_coords.json');
|
|
}
|
|
return response;
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
// Another fallback path
|
|
return fetch('/diplomacy/animation/assets/maps/standard_coords.json');
|
|
}
|
|
return response;
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to load coordinates: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
coordinateData = data;
|
|
infoPanel.textContent = 'Coordinate data loaded!';
|
|
resolve(coordinateData);
|
|
})
|
|
.catch(error => {
|
|
console.error(error);
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- CREATE THE FALLBACK MAP AS A PLANE ---
|
|
function createFallbackMap() {
|
|
// If there's already a plane from a previous phase re-draw, remove it
|
|
if (mapPlane) {
|
|
scene.remove(mapPlane);
|
|
mapPlane.geometry.dispose();
|
|
mapPlane.material.dispose();
|
|
mapPlane = null;
|
|
}
|
|
|
|
const mapGeometry = new THREE.PlaneGeometry(1000, 1000);
|
|
|
|
// We'll create a texture by drawing on a <canvas>:
|
|
const texture = drawFallbackCanvas();
|
|
const material = new THREE.MeshBasicMaterial({ map: texture, side: THREE.DoubleSide });
|
|
|
|
mapPlane = new THREE.Mesh(mapGeometry, material);
|
|
mapPlane.rotation.x = -Math.PI / 2; // Lay flat
|
|
mapPlane.position.y = -1; // Slightly below any 3D items
|
|
scene.add(mapPlane);
|
|
|
|
infoPanel.textContent = "Fallback map loaded as primary texture!";
|
|
}
|
|
|
|
// --- DRAW THE FALLBACK MAP ON A CANVAS ---
|
|
function drawFallbackCanvas() {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 2048;
|
|
canvas.height = 2048;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Fill background with a radial gradient (sea-like)
|
|
const seaGradient = ctx.createRadialGradient(
|
|
canvas.width / 2, canvas.height / 2, 0,
|
|
canvas.width / 2, canvas.height / 2, canvas.width / 1.5
|
|
);
|
|
seaGradient.addColorStop(0, '#1a3c6e');
|
|
seaGradient.addColorStop(0.7, '#2a5d9e');
|
|
seaGradient.addColorStop(0.9, '#3973ac');
|
|
seaGradient.addColorStop(1, '#4b8bc5');
|
|
ctx.fillStyle = seaGradient;
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// If we have coordinateData, we can draw an "accurate" map:
|
|
if (coordinateData && coordinateData.coordinates) {
|
|
drawAccurateMap(ctx, canvas.width, canvas.height);
|
|
} else {
|
|
// Otherwise, just some placeholder
|
|
drawSimplifiedOcean(ctx, canvas.width, canvas.height);
|
|
}
|
|
|
|
// If we have current game data (and a chosen phase), we can reflect supply center ownership:
|
|
// But note, this fallback draw is only called once here, so it won't "live-update" supply center
|
|
// colors as you press next/prev. If you want that, you can call createFallbackMap() again inside
|
|
// displayPhase() each time. For now, we just do it once on load.
|
|
|
|
return new THREE.CanvasTexture(canvas);
|
|
}
|
|
|
|
// --- MINIMAL EXAMPLE: Draw a quick "accidental polygons" map ---
|
|
// (This is the same core logic from the original fallback code, just shortened.)
|
|
function drawAccurateMap(ctx, width, height) {
|
|
// Borrowed from the original: scaling & offset for province coordinates
|
|
const scaleX = width / 1000;
|
|
const scaleY = height / 1000;
|
|
const offsetX = width / 2;
|
|
const offsetY = height / 2;
|
|
|
|
// Fill ocean pattern
|
|
drawOceanBackground(ctx, width, height);
|
|
|
|
// We can do "approx polygon" logic for each province, or do circles.
|
|
// For brevity, let's just do circles to represent each land province, ignoring seas:
|
|
for (const [prov, pos] of Object.entries(coordinateData.coordinates)) {
|
|
if (!prov.includes('_')) {
|
|
const x = pos.x * scaleX + offsetX;
|
|
const y = pos.z * scaleY + offsetY;
|
|
|
|
// Draw a circle
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 25, 0, Math.PI * 2);
|
|
ctx.fillStyle = '#b19b69'; // Land color
|
|
ctx.fill();
|
|
ctx.strokeStyle = '#333';
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
|
|
// Province label
|
|
ctx.font = 'bold 20px Arial';
|
|
ctx.fillStyle = '#000';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(prov, x, y);
|
|
}
|
|
}
|
|
|
|
// Optionally draw supply centers if coordinateData.provinces has them:
|
|
if (coordinateData.provinces) {
|
|
for (const [province, data] of Object.entries(coordinateData.provinces)) {
|
|
if (data.isSupplyCenter && coordinateData.coordinates[province]) {
|
|
const pos = coordinateData.coordinates[province];
|
|
const x = pos.x * scaleX + offsetX;
|
|
const y = pos.z * scaleY + offsetY;
|
|
// Little star
|
|
ctx.beginPath();
|
|
// simple star
|
|
starPath(ctx, x, y, 5, 10, 5);
|
|
ctx.fillStyle = '#FFD700';
|
|
ctx.fill();
|
|
ctx.strokeStyle = '#000';
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Just a helper for the star shape
|
|
function starPath(ctx, cx, cy, spikes, outerR, innerR) {
|
|
let rot = Math.PI / 2 * 3;
|
|
let x = cx;
|
|
let y = cy;
|
|
const step = Math.PI / spikes;
|
|
|
|
ctx.moveTo(cx, cy - outerR);
|
|
for (let i = 0; i < spikes; i++) {
|
|
x = cx + Math.cos(rot) * outerR;
|
|
y = cy + Math.sin(rot) * outerR;
|
|
ctx.lineTo(x, y);
|
|
rot += step;
|
|
|
|
x = cx + Math.cos(rot) * innerR;
|
|
y = cy + Math.sin(rot) * innerR;
|
|
ctx.lineTo(x, y);
|
|
rot += step;
|
|
}
|
|
ctx.lineTo(cx, cy - outerR);
|
|
ctx.closePath();
|
|
}
|
|
|
|
// Draw some faint wave lines
|
|
function drawOceanBackground(ctx, width, height) {
|
|
ctx.save();
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
|
|
ctx.lineWidth = 2;
|
|
for (let i = 0; i < 40; i++) {
|
|
const x1 = Math.random() * width;
|
|
const y1 = Math.random() * height;
|
|
const len = 40 + Math.random() * 60;
|
|
const angle = Math.random() * Math.PI;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x1, y1);
|
|
ctx.lineTo(
|
|
x1 + Math.cos(angle) * len,
|
|
y1 + Math.sin(angle) * len
|
|
);
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
}
|
|
|
|
// If coordinate data isn't available, just do a big watery rectangle
|
|
function drawSimplifiedOcean(ctx, width, height) {
|
|
ctx.fillStyle = '#1a3c6e';
|
|
ctx.fillRect(0, 0, width, height);
|
|
}
|
|
|
|
// --- 3D SUPPLY CENTERS (Optional) ---
|
|
// If you want supply centers to be dynamic, keep these next two functions.
|
|
// If you'd rather rely on the fallback canvas's "star icons," remove them.
|
|
function displaySupplyCenters() {
|
|
if (!coordinateData || !coordinateData.provinces) return;
|
|
for (const [province, data] of Object.entries(coordinateData.provinces)) {
|
|
if (data.isSupplyCenter && coordinateData.coordinates[province]) {
|
|
const pos = getProvincePosition(province);
|
|
|
|
// Build a small pillar + star in 3D
|
|
const scGroup = new THREE.Group();
|
|
|
|
const baseGeom = new THREE.CylinderGeometry(12, 12, 3, 16);
|
|
const baseMat = new THREE.MeshStandardMaterial({ color: 0x333333 });
|
|
const base = new THREE.Mesh(baseGeom, baseMat);
|
|
base.position.y = 1.5;
|
|
scGroup.add(base);
|
|
|
|
const pillarGeom = new THREE.CylinderGeometry(2.5, 2.5, 12, 8);
|
|
const pillarMat = new THREE.MeshStandardMaterial({ color: 0xcccccc });
|
|
const pillar = new THREE.Mesh(pillarGeom, pillarMat);
|
|
pillar.position.y = 7.5;
|
|
scGroup.add(pillar);
|
|
|
|
// We'll just do a cone star for simplicity
|
|
const starGeom = new THREE.ConeGeometry(6, 10, 5);
|
|
const starMat = new THREE.MeshStandardMaterial({ color: 0xFFD700 });
|
|
const starMesh = new THREE.Mesh(starGeom, starMat);
|
|
starMesh.rotation.x = Math.PI; // point upwards
|
|
starMesh.position.y = 14;
|
|
scGroup.add(starMesh);
|
|
|
|
// Optionally add a glow disc
|
|
const glowGeom = new THREE.CircleGeometry(15, 32);
|
|
const glowMat = new THREE.MeshBasicMaterial({ color: 0xFFFFAA, transparent: true, opacity: 0.3, side: THREE.DoubleSide });
|
|
const glowMesh = new THREE.Mesh(glowGeom, glowMat);
|
|
glowMesh.rotation.x = -Math.PI / 2;
|
|
glowMesh.position.y = 2;
|
|
scGroup.add(glowMesh);
|
|
|
|
// Store userData for ownership changes
|
|
scGroup.userData = {
|
|
province,
|
|
isSupplyCenter: true,
|
|
owner: null,
|
|
starMesh,
|
|
glowMesh
|
|
};
|
|
|
|
scGroup.position.set(pos.x, 2, pos.z);
|
|
scene.add(scGroup);
|
|
unitMeshes.push(scGroup);
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateSupplyCenterOwnership(centers) {
|
|
if (!centers) return;
|
|
const ownershipMap = {};
|
|
// centers is typically { "AUSTRIA":["VIE","BUD"], "FRANCE":["PAR","MAR"], ... }
|
|
for (const [power, provinces] of Object.entries(centers)) {
|
|
provinces.forEach(p => {
|
|
ownershipMap[p.toUpperCase()] = power.toUpperCase();
|
|
});
|
|
}
|
|
|
|
// Basic color scheme
|
|
const powerColors = {
|
|
'AUSTRIA': 0xFF0000,
|
|
'ENGLAND': 0x0000FF,
|
|
'FRANCE': 0x00FFFF,
|
|
'GERMANY': 0x000000,
|
|
'ITALY': 0x00FF00,
|
|
'RUSSIA': 0xFFFFFF,
|
|
'TURKEY': 0xFFFF00
|
|
};
|
|
|
|
unitMeshes.forEach(obj => {
|
|
if (obj.userData && obj.userData.isSupplyCenter) {
|
|
const prov = obj.userData.province;
|
|
const owner = ownershipMap[prov];
|
|
if (owner) {
|
|
const c = powerColors[owner] || 0xFFD700;
|
|
obj.userData.starMesh.material.color.setHex(c);
|
|
|
|
// Add a pulsing animation
|
|
if (!obj.userData.pulseAnimation) {
|
|
obj.userData.pulseAnimation = {
|
|
speed: 0.003 + Math.random() * 0.002,
|
|
intensity: 0.3,
|
|
time: Math.random() * Math.PI * 2
|
|
};
|
|
if (!scene.userData.animatedObjects) scene.userData.animatedObjects = [];
|
|
scene.userData.animatedObjects.push(obj);
|
|
}
|
|
} else {
|
|
// Neutral
|
|
obj.userData.starMesh.material.color.setHex(0xFFD700);
|
|
// remove pulse
|
|
obj.userData.pulseAnimation = null;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- UNITS ---
|
|
function displayUnit(unitData) {
|
|
// Choose color by power
|
|
const powerColors = {
|
|
'AUSTRIA': 0xFF0000,
|
|
'ENGLAND': 0x0000FF,
|
|
'FRANCE': 0x00FFFF,
|
|
'GERMANY': 0x000000,
|
|
'ITALY': 0x00FF00,
|
|
'RUSSIA': 0xFFFFFF,
|
|
'TURKEY': 0xFFFF00
|
|
};
|
|
const color = powerColors[unitData.power] || 0xAAAAAA;
|
|
|
|
let group = new THREE.Group();
|
|
// Minimal shape difference for armies vs fleets
|
|
if (unitData.type === 'A') {
|
|
// Army: a simple block + small flag
|
|
const body = new THREE.Mesh(
|
|
new THREE.BoxGeometry(15, 15, 15),
|
|
new THREE.MeshStandardMaterial({ color })
|
|
);
|
|
body.position.y = 7.5;
|
|
group.add(body);
|
|
} else {
|
|
// Fleet: a rectangle + a mast
|
|
const hull = new THREE.Mesh(
|
|
new THREE.BoxGeometry(30, 10, 15),
|
|
new THREE.MeshStandardMaterial({ color: 0x8B4513 })
|
|
);
|
|
hull.position.y = 5;
|
|
group.add(hull);
|
|
const sail = new THREE.Mesh(
|
|
new THREE.PlaneGeometry(20, 20),
|
|
new THREE.MeshStandardMaterial({ color, side: THREE.DoubleSide })
|
|
);
|
|
sail.position.y = 15;
|
|
sail.rotation.y = Math.PI / 2;
|
|
group.add(sail);
|
|
}
|
|
|
|
// Position
|
|
const pos = getProvincePosition(unitData.location);
|
|
group.position.set(pos.x, 10, pos.z);
|
|
|
|
// Store meta
|
|
group.userData = {
|
|
power: unitData.power,
|
|
type: unitData.type,
|
|
location: unitData.location
|
|
};
|
|
|
|
scene.add(group);
|
|
unitMeshes.push(group);
|
|
}
|
|
|
|
function getProvincePosition(loc) {
|
|
// Convert e.g. "Spa/sc" to "SPA_SC" if needed
|
|
const normalized = loc.toUpperCase().replace('/', '_');
|
|
const base = normalized.split('_')[0];
|
|
|
|
if (coordinateData && coordinateData.coordinates) {
|
|
if (coordinateData.coordinates[normalized]) {
|
|
return coordinateData.coordinates[normalized];
|
|
}
|
|
if (coordinateData.coordinates[base]) {
|
|
return coordinateData.coordinates[base];
|
|
}
|
|
}
|
|
// Fallback if missing
|
|
return hashStringToPosition(loc);
|
|
}
|
|
|
|
function hashStringToPosition(str) {
|
|
let hash = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
hash |= 0;
|
|
}
|
|
const x = (hash % 800) - 400;
|
|
const z = ((hash >> 8) % 800) - 400;
|
|
return { x, y: 0, z };
|
|
}
|
|
|
|
// --- LOADING & DISPLAYING GAME PHASES ---
|
|
function loadGame(file) {
|
|
const reader = new FileReader();
|
|
reader.onload = e => {
|
|
try {
|
|
gameData = JSON.parse(e.target.result);
|
|
infoPanel.textContent = `Game data loaded: ${gameData.phases?.length || 0} phases found.`;
|
|
currentPhaseIndex = 0;
|
|
if (gameData.phases?.length) {
|
|
prevBtn.disabled = false;
|
|
nextBtn.disabled = false;
|
|
displayPhase(currentPhaseIndex);
|
|
}
|
|
} catch (err) {
|
|
infoPanel.textContent = "Error parsing JSON: " + err.message;
|
|
}
|
|
};
|
|
reader.onerror = () => {
|
|
infoPanel.textContent = "Error reading file.";
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
function displayPhase(index) {
|
|
if (!gameData || !gameData.phases || index < 0 || index >= gameData.phases.length) {
|
|
infoPanel.textContent = "Invalid phase index.";
|
|
return;
|
|
}
|
|
|
|
// Clear old units
|
|
unitMeshes.forEach(m => scene.remove(m));
|
|
unitMeshes = [];
|
|
|
|
// Possibly re-draw the fallback if you want the canvas to show updated supply center ownership text:
|
|
// createFallbackMap(); // <-- you could call this here if needed
|
|
|
|
const phase = gameData.phases[index];
|
|
phaseDisplay.textContent = `${phase.name || 'Unknown Phase'} (${index + 1}/${gameData.phases.length})`;
|
|
|
|
// 1) Show supply centers (3D approach)
|
|
displaySupplyCenters();
|
|
|
|
// 2) If phase has supply center ownership data
|
|
if (phase.state?.centers) {
|
|
updateSupplyCenterOwnership(phase.state.centers);
|
|
}
|
|
|
|
// 3) Show units
|
|
if (phase.state?.units) {
|
|
for (const [power, unitArr] of Object.entries(phase.state.units)) {
|
|
unitArr.forEach(unitStr => {
|
|
const match = unitStr.match(/^([AF])\s+(.+)$/);
|
|
if (match) {
|
|
displayUnit({
|
|
power: power.toUpperCase(),
|
|
type: match[1],
|
|
location: match[2],
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Show some info in the panel
|
|
infoPanel.textContent = `Phase: ${phase.name}\nSupply centers: ${
|
|
phase.state?.centers ? JSON.stringify(phase.state.centers) : 'None'
|
|
}\nUnits: ${
|
|
phase.state?.units ? JSON.stringify(phase.state.units) : 'None'
|
|
}`;
|
|
}
|
|
|
|
// --- EVENT HANDLERS ---
|
|
loadBtn.addEventListener('click', () => fileInput.click());
|
|
fileInput.addEventListener('change', e => {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
loadGame(file);
|
|
}
|
|
});
|
|
|
|
prevBtn.addEventListener('click', () => {
|
|
if (currentPhaseIndex > 0) {
|
|
currentPhaseIndex--;
|
|
displayPhase(currentPhaseIndex);
|
|
}
|
|
});
|
|
nextBtn.addEventListener('click', () => {
|
|
if (gameData && currentPhaseIndex < gameData.phases.length - 1) {
|
|
currentPhaseIndex++;
|
|
displayPhase(currentPhaseIndex);
|
|
}
|
|
});
|
|
|
|
// --- BOOTSTRAP ON PAGE LOAD ---
|
|
window.addEventListener('load', initScene);
|
|
</script>
|
|
</body>
|
|
</html> |