mirror of
https://github.com/GoodStartLabs/AI_Diplomacy.git
synced 2026-04-28 17:29:41 +00:00
Animation plays in replay!
This commit is contained in:
parent
e623b6f2bd
commit
3e4c8225d9
1 changed files with 381 additions and 11 deletions
|
|
@ -96,6 +96,12 @@
|
|||
<button id="load-btn">Load Game</button>
|
||||
<button id="prev-btn" disabled>← Prev</button>
|
||||
<button id="next-btn" disabled>Next →</button>
|
||||
<button id="play-btn" disabled>▶ Play</button>
|
||||
<select id="speed-selector" disabled>
|
||||
<option value="1000">Slow</option>
|
||||
<option value="500" selected>Medium</option>
|
||||
<option value="200">Fast</option>
|
||||
</select>
|
||||
<span id="phase-display">No game loaded</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -117,16 +123,24 @@
|
|||
let coordinateData = null;
|
||||
let unitMeshes = []; // To store references for units + supply center 3D objects
|
||||
let mapPlane = null; // The fallback map plane
|
||||
let isPlaying = false; // Track playback state
|
||||
let playbackSpeed = 500; // Default speed in ms
|
||||
let playbackTimer = null; // Timer reference for playback
|
||||
let animationDuration = 1500; // Duration of unit movement animation in ms
|
||||
let unitAnimations = []; // Track ongoing unit animations
|
||||
let territoryTransitions = []; // Track territory color transitions
|
||||
|
||||
// --- 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');
|
||||
const leaderboard = document.getElementById('leaderboard');
|
||||
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 playBtn = document.getElementById('play-btn');
|
||||
const speedSelector = document.getElementById('speed-selector');
|
||||
const phaseDisplay = document.getElementById('phase-display');
|
||||
const infoPanel = document.getElementById('info-panel');
|
||||
const mapView = document.getElementById('map-view');
|
||||
const leaderboard = document.getElementById('leaderboard');
|
||||
|
||||
// Add roundRect polyfill for browsers that don't support it
|
||||
if (!CanvasRenderingContext2D.prototype.roundRect) {
|
||||
|
|
@ -231,6 +245,74 @@
|
|||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Process unit movement animations
|
||||
if (unitAnimations.length > 0) {
|
||||
unitAnimations.forEach((anim, index) => {
|
||||
// Calculate progress (0 to 1)
|
||||
const elapsed = currentTime - anim.startTime;
|
||||
const progress = Math.min(1, elapsed / anim.duration);
|
||||
|
||||
// Apply movement
|
||||
if (progress < 1) {
|
||||
// Apply easing for more natural movement - ease in and out
|
||||
const easedProgress = easeInOutCubic(progress);
|
||||
|
||||
// Update position
|
||||
anim.object.position.x = anim.startPos.x + (anim.endPos.x - anim.startPos.x) * easedProgress;
|
||||
anim.object.position.z = anim.startPos.z + (anim.endPos.z - anim.startPos.z) * easedProgress;
|
||||
|
||||
// Subtle bobbing up and down during movement
|
||||
anim.object.position.y = 10 + Math.sin(progress * Math.PI * 2) * 5;
|
||||
|
||||
// For fleets (ships), add a gentle rocking motion
|
||||
if (anim.object.userData.type === 'F') {
|
||||
anim.object.rotation.z = Math.sin(progress * Math.PI * 3) * 0.05;
|
||||
anim.object.rotation.x = Math.sin(progress * Math.PI * 2) * 0.05;
|
||||
}
|
||||
} else {
|
||||
// Animation complete, remove from active animations
|
||||
unitAnimations.splice(index, 1);
|
||||
|
||||
// Set final position
|
||||
anim.object.position.x = anim.endPos.x;
|
||||
anim.object.position.z = anim.endPos.z;
|
||||
anim.object.position.y = 10; // Reset height
|
||||
|
||||
// Reset rotation for ships
|
||||
if (anim.object.userData.type === 'F') {
|
||||
anim.object.rotation.z = 0;
|
||||
anim.object.rotation.x = 0;
|
||||
}
|
||||
|
||||
// If this was the last animation and playback is active, continue after delay
|
||||
if (unitAnimations.length === 0 && isPlaying) {
|
||||
// Schedule next phase after a pause delay
|
||||
playbackTimer = setTimeout(() => advanceToNextPhase(), playbackSpeed);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Process territory color transitions
|
||||
if (territoryTransitions.length > 0) {
|
||||
territoryTransitions.forEach((transition, index) => {
|
||||
const elapsed = currentTime - transition.startTime;
|
||||
const progress = Math.min(1, elapsed / transition.duration);
|
||||
|
||||
if (progress < 1) {
|
||||
// Interpolate colors
|
||||
const easedProgress = easeInOutCubic(progress);
|
||||
transition.canvas.style.opacity = easedProgress;
|
||||
} else {
|
||||
// Transition complete
|
||||
territoryTransitions.splice(index, 1);
|
||||
transition.canvas.style.opacity = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update any pulsing or wave animations on supply centers or units
|
||||
if (scene.userData.animatedObjects) {
|
||||
scene.userData.animatedObjects.forEach(obj => {
|
||||
|
|
@ -256,6 +338,11 @@
|
|||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
// Easing function for smooth animations
|
||||
function easeInOutCubic(t) {
|
||||
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
}
|
||||
|
||||
// --- RESIZE HANDLER ---
|
||||
function onWindowResize() {
|
||||
camera.aspect = mapView.clientWidth / mapView.clientHeight;
|
||||
|
|
@ -415,7 +502,7 @@
|
|||
|
||||
// Apply bubble-physics to make territories squish against each other
|
||||
applyTerritorySquishing(provinces);
|
||||
|
||||
|
||||
// Draw each province as an irregular territory
|
||||
// Draw in reverse order so water is on bottom, land on top
|
||||
provinces
|
||||
|
|
@ -1284,6 +1371,8 @@
|
|||
if (gameData.phases?.length) {
|
||||
prevBtn.disabled = false;
|
||||
nextBtn.disabled = false;
|
||||
playBtn.disabled = false;
|
||||
speedSelector.disabled = false;
|
||||
displayPhase(currentPhaseIndex);
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -1305,7 +1394,7 @@
|
|||
// Clear old units
|
||||
unitMeshes.forEach(m => scene.remove(m));
|
||||
unitMeshes = [];
|
||||
|
||||
|
||||
const phase = gameData.phases[index];
|
||||
phaseDisplay.textContent = `${phase.name || 'Unknown Phase'} (${index + 1}/${gameData.phases.length})`;
|
||||
|
||||
|
|
@ -1320,7 +1409,7 @@
|
|||
ownershipMap[p.toUpperCase()] = power.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Then add territories where units are located (if not already claimed by supply centers)
|
||||
for (const [power, unitArray] of Object.entries(units)) {
|
||||
unitArray.forEach(unitStr => {
|
||||
|
|
@ -1425,6 +1514,276 @@
|
|||
leaderboard.innerHTML = html;
|
||||
}
|
||||
|
||||
// --- PLAYBACK CONTROLS ---
|
||||
function togglePlayback() {
|
||||
if (!gameData || gameData.phases.length <= 1) return;
|
||||
|
||||
isPlaying = !isPlaying;
|
||||
|
||||
if (isPlaying) {
|
||||
// Update button text to show pause
|
||||
playBtn.textContent = "⏸ Pause";
|
||||
|
||||
// Disable manual navigation during playback
|
||||
prevBtn.disabled = true;
|
||||
nextBtn.disabled = true;
|
||||
|
||||
// Start playback
|
||||
advanceToNextPhase();
|
||||
} else {
|
||||
// Update button text to show play
|
||||
playBtn.textContent = "▶ Play";
|
||||
|
||||
// Clear any pending timers
|
||||
if (playbackTimer) {
|
||||
clearTimeout(playbackTimer);
|
||||
playbackTimer = null;
|
||||
}
|
||||
|
||||
// Cancel any ongoing animations
|
||||
unitAnimations = [];
|
||||
|
||||
// Re-enable manual navigation
|
||||
prevBtn.disabled = false;
|
||||
nextBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function advanceToNextPhase() {
|
||||
// If we've reached the end, loop back to the beginning
|
||||
if (currentPhaseIndex >= gameData.phases.length - 1) {
|
||||
currentPhaseIndex = 0;
|
||||
} else {
|
||||
currentPhaseIndex++;
|
||||
}
|
||||
|
||||
// Display the new phase with animation
|
||||
displayPhaseWithAnimation(currentPhaseIndex);
|
||||
}
|
||||
|
||||
function displayPhaseWithAnimation(index) {
|
||||
if (!gameData || !gameData.phases || index < 0 || index >= gameData.phases.length) {
|
||||
infoPanel.textContent = "Invalid phase index.";
|
||||
return;
|
||||
}
|
||||
|
||||
const previousIndex = index > 0 ? index - 1 : gameData.phases.length - 1;
|
||||
const currentPhase = gameData.phases[index];
|
||||
const previousPhase = gameData.phases[previousIndex];
|
||||
|
||||
phaseDisplay.textContent = `${currentPhase.name || 'Unknown Phase'} (${index + 1}/${gameData.phases.length})`;
|
||||
|
||||
// Build ownership maps for territory coloring
|
||||
const currentCenters = currentPhase.state?.centers || {};
|
||||
const currentUnits = currentPhase.state?.units || {};
|
||||
const previousCenters = previousPhase.state?.centers || {};
|
||||
const previousUnits = previousPhase.state?.units || {};
|
||||
|
||||
const currentOwnershipMap = buildOwnershipMap(currentCenters, currentUnits);
|
||||
const previousOwnershipMap = buildOwnershipMap(previousCenters, previousUnits);
|
||||
|
||||
// Update map with new territory colors
|
||||
createFallbackMap(currentOwnershipMap);
|
||||
|
||||
// Clear previous unit meshes (except supply centers)
|
||||
const supplyCenters = unitMeshes.filter(m => m.userData && m.userData.isSupplyCenter);
|
||||
const oldUnits = unitMeshes.filter(m => m.userData && !m.userData.isSupplyCenter);
|
||||
|
||||
oldUnits.forEach(m => scene.remove(m));
|
||||
unitMeshes = supplyCenters; // Keep supply centers
|
||||
|
||||
// Update supply center ownership
|
||||
if (currentPhase.state?.centers) {
|
||||
updateSupplyCenterOwnership(currentPhase.state.centers);
|
||||
}
|
||||
|
||||
// Create unit position maps for animation
|
||||
const previousUnitPositions = {};
|
||||
const currentUnitPositions = {};
|
||||
|
||||
// Map previous unit positions
|
||||
if (previousPhase.state?.units) {
|
||||
for (const [power, unitArr] of Object.entries(previousPhase.state.units)) {
|
||||
unitArr.forEach(unitStr => {
|
||||
const match = unitStr.match(/^([AF])\s+(.+)$/);
|
||||
if (match) {
|
||||
const key = `${power}-${match[1]}-${match[2]}`;
|
||||
previousUnitPositions[key] = getProvincePosition(match[2]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create and position new units, with animation from previous positions if available
|
||||
if (currentPhase.state?.units) {
|
||||
for (const [power, unitArr] of Object.entries(currentPhase.state.units)) {
|
||||
unitArr.forEach(unitStr => {
|
||||
const match = unitStr.match(/^([AF])\s+(.+)$/);
|
||||
if (match) {
|
||||
const unitType = match[1];
|
||||
const location = match[2];
|
||||
const key = `${power}-${unitType}-${location}`;
|
||||
|
||||
// Create the unit mesh
|
||||
const unitMesh = createUnitMesh({
|
||||
power: power.toUpperCase(),
|
||||
type: unitType,
|
||||
location: location,
|
||||
});
|
||||
|
||||
// Get current position
|
||||
const currentPos = getProvincePosition(location);
|
||||
currentUnitPositions[key] = currentPos;
|
||||
|
||||
// Choose starting position
|
||||
let startPos;
|
||||
|
||||
// Try to find a matching unit in the previous phase
|
||||
let matchFound = false;
|
||||
for (const prevKey in previousUnitPositions) {
|
||||
// Check if same power and type (but possibly different location)
|
||||
if (prevKey.startsWith(`${power}-${unitType}`)) {
|
||||
startPos = previousUnitPositions[prevKey];
|
||||
matchFound = true;
|
||||
delete previousUnitPositions[prevKey]; // Mark as matched
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchFound) {
|
||||
// New unit - create with a "spawn" animation
|
||||
startPos = {
|
||||
x: currentPos.x,
|
||||
y: -20, // Start below the map
|
||||
z: currentPos.z
|
||||
};
|
||||
}
|
||||
|
||||
// Position at start position
|
||||
unitMesh.position.set(startPos.x, 10, startPos.z);
|
||||
scene.add(unitMesh);
|
||||
unitMeshes.push(unitMesh);
|
||||
|
||||
// Add animation to move to final position
|
||||
unitAnimations.push({
|
||||
object: unitMesh,
|
||||
startPos: startPos,
|
||||
endPos: currentPos,
|
||||
startTime: Date.now(),
|
||||
duration: animationDuration
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update the leaderboard
|
||||
updateLeaderboard(currentPhase);
|
||||
|
||||
// Show phase info
|
||||
infoPanel.textContent = `Phase: ${currentPhase.name}\nSupply centers: ${
|
||||
currentPhase.state?.centers ? JSON.stringify(currentPhase.state.centers) : 'None'
|
||||
}\nUnits: ${
|
||||
currentPhase.state?.units ? JSON.stringify(currentPhase.state.units) : 'None'
|
||||
}`;
|
||||
}
|
||||
|
||||
// Helper function to create ownership map from centers and units
|
||||
function buildOwnershipMap(centers, units) {
|
||||
const ownershipMap = {};
|
||||
|
||||
// First add supply centers
|
||||
for (const [power, provinces] of Object.entries(centers)) {
|
||||
provinces.forEach(p => {
|
||||
ownershipMap[p.toUpperCase()] = power.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
// Then add territories with units
|
||||
for (const [power, unitArray] of Object.entries(units)) {
|
||||
unitArray.forEach(unitStr => {
|
||||
const match = unitStr.match(/^([AF])\s+(.+)$/);
|
||||
if (match) {
|
||||
const location = match[2].toUpperCase().split('/')[0];
|
||||
if (!ownershipMap[location]) {
|
||||
ownershipMap[location] = power.toUpperCase();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return ownershipMap;
|
||||
}
|
||||
|
||||
// Create a unit mesh (extracted from displayUnit for reuse)
|
||||
function createUnitMesh(unitData) {
|
||||
// Choose color by power
|
||||
const powerColors = {
|
||||
'AUSTRIA': 0xc40000,
|
||||
'ENGLAND': 0x00008B,
|
||||
'FRANCE': 0x0fa0d0,
|
||||
'GERMANY': 0x444444,
|
||||
'ITALY': 0x008000,
|
||||
'RUSSIA': 0xcccccc,
|
||||
'TURKEY': 0xe0c846
|
||||
};
|
||||
const color = powerColors[unitData.power] || 0xAAAAAA;
|
||||
|
||||
let group = new THREE.Group();
|
||||
// Minimal shape difference for armies vs fleets
|
||||
if (unitData.type === 'A') {
|
||||
// Army: a block + small head for soldier-like appearance
|
||||
const body = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(15, 20, 10),
|
||||
new THREE.MeshStandardMaterial({ color })
|
||||
);
|
||||
body.position.y = 10;
|
||||
group.add(body);
|
||||
|
||||
// Head
|
||||
const head = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(4, 12, 12),
|
||||
new THREE.MeshStandardMaterial({ color })
|
||||
);
|
||||
head.position.set(0, 24, 0);
|
||||
group.add(head);
|
||||
} else {
|
||||
// Fleet: a rectangle + a mast and sail
|
||||
const hull = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(30, 8, 15),
|
||||
new THREE.MeshStandardMaterial({ color: 0x8B4513 })
|
||||
);
|
||||
hull.position.y = 4;
|
||||
group.add(hull);
|
||||
|
||||
// Mast
|
||||
const mast = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(1, 1, 30, 8),
|
||||
new THREE.MeshStandardMaterial({ color: 0x000000 })
|
||||
);
|
||||
mast.position.y = 15;
|
||||
group.add(mast);
|
||||
|
||||
// Sail
|
||||
const sail = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(20, 15),
|
||||
new THREE.MeshStandardMaterial({ color, side: THREE.DoubleSide })
|
||||
);
|
||||
sail.rotation.y = Math.PI/2;
|
||||
sail.position.set(0, 15, 0);
|
||||
group.add(sail);
|
||||
}
|
||||
|
||||
// Store metadata
|
||||
group.userData = {
|
||||
power: unitData.power,
|
||||
type: unitData.type,
|
||||
location: unitData.location
|
||||
};
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
// --- EVENT HANDLERS ---
|
||||
loadBtn.addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', e => {
|
||||
|
|
@ -1447,6 +1806,17 @@
|
|||
}
|
||||
});
|
||||
|
||||
playBtn.addEventListener('click', togglePlayback);
|
||||
|
||||
speedSelector.addEventListener('change', e => {
|
||||
playbackSpeed = parseInt(e.target.value);
|
||||
// If we're currently playing, restart the timer with the new speed
|
||||
if (isPlaying && playbackTimer) {
|
||||
clearTimeout(playbackTimer);
|
||||
playbackTimer = setTimeout(() => advanceToNextPhase(), playbackSpeed);
|
||||
}
|
||||
});
|
||||
|
||||
// --- BOOTSTRAP ON PAGE LOAD ---
|
||||
window.addEventListener('load', initScene);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue