diff --git a/.vscode/launch.json b/.vscode/launch.json index 274ae86..e5eaf8a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,11 +4,11 @@ { "type": "pwa-chrome", "request": "launch", - "name": "Chrome Debug 5179", - "url": "http://localhost:5179", + "name": "Chrome Debug 5173", + "url": "http://localhost:5173", "webRoot": "${workspaceFolder}/ai_animation/", "sourceMapPathOverrides": { - "http://localhost:5179/*": "${webRoot}/*" + "http://localhost:5173/*": "${webRoot}/*" }, "runtimeArgs": [ "--remote-debugging-port=9223" diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..8497252 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,14 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run Game", + "type": "shell", + "command": "npm start", + "presentation": { + "panel": "shared" + }, + "problemMatcher": [] + } + ] + } \ No newline at end of file diff --git a/ai_animation/src/types/events.ts b/ai_animation/src/types/events.ts index a3c98b5..e70d2e8 100644 --- a/ai_animation/src/types/events.ts +++ b/ai_animation/src/types/events.ts @@ -61,12 +61,7 @@ export class EventQueue { * Remove resolved events from the queue */ cleanup(): void { - let clearedQueue = this.events.filter(event => !event.resolved); - if (clearedQueue.length <= 1) { - console.log(this.events) - throw new Error("We've cleared all the messages out of the queue") - } - this.events = clearedQueue + this.events = this.events.filter(e => !e.resolved); } /** diff --git a/ai_animation/src/units/animate.ts b/ai_animation/src/units/animate.ts index 79452cb..0264808 100644 --- a/ai_animation/src/units/animate.ts +++ b/ai_animation/src/units/animate.ts @@ -9,6 +9,66 @@ import { config } from "../config"; // Assuming config is defined in a separate import { PowerENUM, ProvinceENUM } from "../types/map"; import { UnitTypeENUM } from "../types/units"; import { sineWave, getTimeInSeconds } from "../utils/timing"; +//import { color } from "chart.js/helpers"; +//import { depth } from "three/tsl"; + + +function buildFancyArrow(length: number, colorDeter: string): THREE.Mesh { +//Specs to change arrow size + const tw = 3.5; + const headLength = tw * 4; + const hw = tw * 4; + + const shape = new THREE.Shape() + .moveTo(0,tw/2) + .lineTo(length- headLength,tw/2) + .lineTo(length-headLength,hw/2) + .lineTo(length,0) + .lineTo(length-headLength,-hw/2) + .lineTo(length-headLength,-tw/2) + .lineTo(0,-tw/2) + .closePath(); + + const extrude= new THREE.ExtrudeGeometry(shape, {depth: 1}) + let mat: THREE.Material; + + if (colorDeter=='Move') { + mat = new THREE.MeshStandardMaterial({color: 0x00FF00}) + } else if (colorDeter=='Bounce') { + mat = new THREE.MeshStandardMaterial({color: 0xFF0000}) + } + const mesh= new THREE.Mesh(extrude, mat) + //(Potential Addition: Outline of arrow) + return mesh; +} + +function createShield() { + +//Shield Specs +const BSH= 10 +const SW= 10 +const TSH= 10 + + +const outL= new THREE.Shape() + .moveTo(0, TSH) + .lineTo(SW, TSH) + .lineTo(SW, 0) + .quadraticCurveTo(SW, -BSH, 0, -BSH) + .quadraticCurveTo(-SW, -BSH, -SW, 0) + .lineTo(-SW,TSH) + .lineTo(0, TSH) + .closePath(); + +const SExtrude= new THREE.ExtrudeGeometry(outL, {depth: 5}) + +const SMat= new THREE.MeshStandardMaterial({color: 0x00FF00}) + +const SMesh= new THREE.Mesh(SExtrude, SMat ) + +return SMesh + +} function getUnit(unitOrder: UnitOrder, power: string) { if (power === undefined) throw new Error("Must pass the power argument, cannot be undefined") @@ -54,7 +114,34 @@ function createMoveAnimation(unitMesh: THREE.Group, orderDestination: ProvinceEN // Store animation start time for consistent wave motion const animStartTime = getTimeInSeconds(); - let anim = new Tween(unitMesh.position) + const start = new THREE.Vector3(); + //prevents the arrow mesh from using local coords which mess with the alignment + unitMesh.getWorldPosition(start); + const end = getProvincePosition(orderDestination)!; + //lines to determine direction and length of the arrow, minus a little from the length so it's offset + const direct= new THREE.Vector3().subVectors(end, start); + const length= Math.max(direct.length() - 2, 0); + const arrowMesh= buildFancyArrow(length,'Move'); + const dir = direct.clone().normalize(); + + arrowMesh.position.copy(start); + + //Core of the arrow alignment, won't work without this + const q = new THREE.Quaternion().setFromUnitVectors( + new THREE.Vector3(1, 0, 0),dir + ); + arrowMesh.setRotationFromQuaternion(q); + + arrowMesh.scale.set(0, 1, 1); + + gameState.scene.add(arrowMesh); + unitMesh.userData.moveArrow = arrowMesh; +//Value beside x:1 controls the speed of the growth +const ArrowGrow = new Tween(arrowMesh.scale) + .to({ x: 1 }, 1000) + .easing(Easing.Quadratic.Out) + .onComplete(() => { + const anim = new Tween(unitMesh.position) .to({ x: destinationVector.x, y: 10, @@ -76,20 +163,109 @@ function createMoveAnimation(unitMesh: THREE.Group, orderDestination: ProvinceEN unitMesh.rotation.z = 0; unitMesh.rotation.x = 0; } + gameState.scene.remove(arrowMesh); + delete unitMesh.userData.moveArrow; unitMesh.userData.isAnimating = false }) .start(); - gameState.unitAnimations.push(anim); - return anim + gameState.unitAnimations.push(anim);}) + .start() + gameState.unitAnimations.push(ArrowGrow) + return ArrowGrow } +//Animation for bounce + +function createBounceAnimation(unitMesh: THREE.Group, attemptedDestination: ProvinceENUM): Tween { + const end = getProvincePosition(attemptedDestination)!; + if (!end) throw new Error(`No position found for attempted destination: ${attemptedDestination}`); + + unitMesh.userData.isAnimating = true; + + + const start = new THREE.Vector3(); + //prevents the arrow mesh from using local coords which mess with the alignment + unitMesh.getWorldPosition(start); + //lines to determine direction and length of the arrow, minus a little from the length so it's offset + const direct= new THREE.Vector3().subVectors(end, start); + const length= Math.max(direct.length() - 2, 0); + const arrowMesh= buildFancyArrow(length,'Bounce'); + const dir = direct.clone().normalize(); + + arrowMesh.position.copy(start); + + //Core of the arrow alignment, won't work without this + const q = new THREE.Quaternion().setFromUnitVectors( + new THREE.Vector3(1, 0, 0), dir + ); + arrowMesh.setRotationFromQuaternion(q); + + arrowMesh.scale.set(0, 1, 1); + + gameState.scene.add(arrowMesh); + unitMesh.userData.moveArrow = arrowMesh; + const growBounce= new Tween (arrowMesh.scale) + //Number beside x:1 controls speed in ms + .to({ x: 1 }, 1000) + .easing(Easing.Quadratic.Out) + .onComplete(() => { + const bounceOut = new Tween(unitMesh.position) + .to({ x: end.x, y: 10, z: end.z }, config.effectiveAnimationDuration / 2) + .easing(Easing.Quadratic.Out) + .repeat(1) + .yoyo(true) + .onComplete(() => { + if (unitMesh.userData.moveArrow) { + gameState.scene.remove(arrowMesh); + delete unitMesh.userData.moveArrow; + unitMesh.userData.isAnimating = false + } + }); + + bounceOut.start(); + + gameState.unitAnimations.push(bounceOut);}) + growBounce.start() + gameState.unitAnimations.push(growBounce) + return growBounce; +} + + +function createHoldAnimation(unitMesh: THREE.Group): Tween { + // 1) Build the shield mesh + const shield = createShield(); + + // 2) Figure out where the unit’s feet are + const worldPos = new THREE.Vector3(); + unitMesh.getWorldPosition(worldPos); + + shield.position.set(worldPos.x, 16, worldPos.z+8); + shield.scale.set(1, 0, 1); // collapse height + + gameState.scene.add(shield); + unitMesh.userData.newshield = shield; + unitMesh.userData.isAnimating = true; + + const growTween = new Tween(shield.scale) + .to({ x: 1, y: 1, z: 1 }, 2000) + .easing(Easing.Quadratic.Out) + .onComplete(() => { + gameState.scene.remove(shield); + unitMesh.userData.isAnimating = false; + }) + .start(); + + gameState.unitAnimations.push(growTween); + + return growTween; + } /** * Creates animations for unit movements based on orders from the previous phase * **/ export function createAnimationsForNextPhase() { let previousPhase = gameState.gameData?.phases[gameState.phaseIndex == 0 ? 0 : gameState.phaseIndex - 1] - + // const sequence = ["build", "disband", "hold", "move", "bounce", "retreat"] // Safety check - if no previous phase or no orders, return if (!previousPhase) { logger.log("No previous phase to animate"); @@ -152,10 +328,14 @@ export function createAnimationsForNextPhase() { break; case "bounce": - // TODO: implement bounce animation - break; + if (unitIndex < 0) throw new Error("Unable to find unit for order " + order.raw) + if (!order.destination) throw new Error("Bounce order without destination") + createBounceAnimation(gameState.unitMeshes[unitIndex], order.destination as ProvinceENUM); + break; + case "hold": //TODO: Hold animation, maybe a sheild or something? + createHoldAnimation(gameState.unitMeshes[unitIndex]) break; case "convoy":