From a0bace91a0e1e04efde142be4ac8321ebacc26d0 Mon Sep 17 00:00:00 2001 From: ZGrandison <73369239+ZGrandison@users.noreply.github.com> Date: Tue, 15 Feb 2022 13:57:47 -0500 Subject: [PATCH] Origin/new engine features (#2) * isolate features --- .gitignore | 3 + src/Wolfie2D/AI/AIManager.ts | 5 +- src/Wolfie2D/AI/GoapActionPlanner.ts | 82 +++++++++ src/Wolfie2D/AI/StateMachineGoapAI.ts | 40 +++++ src/Wolfie2D/DataTypes/Graphs/Graph.ts | 2 +- src/Wolfie2D/DataTypes/Interfaces/Actor.ts | 6 +- src/Wolfie2D/DataTypes/Interfaces/GoapAI.ts | 42 +++++ .../DataTypes/Interfaces/GoapAction.ts | 61 +++++++ .../DataTypes/Interfaces/Navigable.ts | 3 +- src/Wolfie2D/Events/BattleSystem.ts | 38 ++++ src/Wolfie2D/Input/Input.ts | 84 +++++---- src/Wolfie2D/Input/InputHandler.ts | 3 +- src/Wolfie2D/Nodes/GameNode.ts | 10 +- src/Wolfie2D/Nodes/Graphic.ts | 24 +++ src/Wolfie2D/Nodes/Graphics/GraphicTypes.ts | 1 + src/Wolfie2D/Nodes/Graphics/Particle.ts | 57 ++++++ src/Wolfie2D/Nodes/Graphics/Point.ts | 5 +- src/Wolfie2D/Pathfinding/NavigationManager.ts | 5 +- src/Wolfie2D/Pathfinding/Navmesh.ts | 11 +- .../Rendering/Animations/ParticleSystem.ts | 163 ++++++++++++++++++ .../Animations/ParticleSystemManager.ts | 40 +++++ .../Scene/Factories/CanvasNodeFactory.ts | 16 +- src/Wolfie2D/Scene/Scene.ts | 4 + src/main.ts | 1 + 24 files changed, 651 insertions(+), 55 deletions(-) create mode 100644 src/Wolfie2D/AI/GoapActionPlanner.ts create mode 100644 src/Wolfie2D/AI/StateMachineGoapAI.ts create mode 100644 src/Wolfie2D/DataTypes/Interfaces/GoapAI.ts create mode 100644 src/Wolfie2D/DataTypes/Interfaces/GoapAction.ts create mode 100644 src/Wolfie2D/Events/BattleSystem.ts create mode 100644 src/Wolfie2D/Nodes/Graphics/Particle.ts create mode 100644 src/Wolfie2D/Rendering/Animations/ParticleSystem.ts create mode 100644 src/Wolfie2D/Rendering/Animations/ParticleSystemManager.ts diff --git a/.gitignore b/.gitignore index 8eddfef..834fb43 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ dist/* # Include the built-in asset folder !dist/builtin/ +# Include the hw1 assets +!dist/hw3_assets/ + # Include the hw1 assets !dist/hw4_assets/ diff --git a/src/Wolfie2D/AI/AIManager.ts b/src/Wolfie2D/AI/AIManager.ts index 026e0c6..179083e 100644 --- a/src/Wolfie2D/AI/AIManager.ts +++ b/src/Wolfie2D/AI/AIManager.ts @@ -1,6 +1,7 @@ import Actor from "../DataTypes/Interfaces/Actor"; import Updateable from "../DataTypes/Interfaces/Updateable"; import AI from "../DataTypes/Interfaces/AI"; +import GoapAI from "../DataTypes/Interfaces/GoapAI" import Map from "../DataTypes/Map"; /** @@ -39,7 +40,7 @@ export default class AIManager implements Updateable { * @param name The name of the AI to register * @param constr The constructor for the AI */ - registerAI(name: string, constr: new () => T ): void { + registerAI(name: string, constr: new () => T ): void { this.registeredAI.add(name, constr); } @@ -48,7 +49,7 @@ export default class AIManager implements Updateable { * @param name The name of the AI to add * @returns A new AI instance */ - generateAI(name: string): AI { + generateAI(name: string): AI | GoapAI { if(this.registeredAI.has(name)){ return new (this.registeredAI.get(name))(); } else { diff --git a/src/Wolfie2D/AI/GoapActionPlanner.ts b/src/Wolfie2D/AI/GoapActionPlanner.ts new file mode 100644 index 0000000..c8743ea --- /dev/null +++ b/src/Wolfie2D/AI/GoapActionPlanner.ts @@ -0,0 +1,82 @@ +import Graph from "../DataTypes/Graphs/Graph"; +import GoapAction from "../DataTypes/Interfaces/GoapAction"; +import GoapAI from "../DataTypes/Interfaces/GoapAI"; +import Queue from "../DataTypes/Queue"; +import Stack from "../DataTypes/Stack"; +import GraphUtils from "../Utils/GraphUtils"; + +export default class GoapActionPlanner { + mapping: Map; + graph: Graph; + path: Array; + + plan(goal: string, possibleActions: Array, currentStatus: Array, actor: GoapAI): Stack { + this.graph = new Graph(true); + this.mapping = new Map(); + + //0 is our root + this.graph.addNode(); + this.mapping.set(0,"Start"); + //1 is the goal + this.graph.addNode(); + this.mapping.set(1,"Goal"); + this.graph.addEdge(1,1,Number.POSITIVE_INFINITY); + + //Build tree from 0 to 1 + this.buildTree(0, goal, possibleActions, currentStatus); + console.log(this.graph.toString()); + + //Run djikstra to find shortest path + this.path = GraphUtils.djikstra(this.graph, 0); + + //Push all elements of the plan + let plan = new Stack(); + + let i = 1; + while(this.path[i] !== -1){ + console.log(this.path[i]); + if (this.path[i] !== 0){ + plan.push(this.mapping.get(this.path[i])); + } + i = this.path[i]; + } + + return plan; + } + + buildTree(root: number, goal:string, possibleActions: Array, currentStatus: Array): void { + //For each possible action + possibleActions.forEach(action => { + console.log("root:" + root + ",action precons:" + action.preconditions.toString() + + ", action effects:" + action.effects.toString() + ", current Status:" + currentStatus.toString()) + + //Can it be performed? + if (action.checkPreconditions(currentStatus)){ + //This action can be performed + //Add effects to currentStatus + let newStatus = [...currentStatus]; + newStatus.push(...action.effects); + + //Check if the new node is the goal + if (newStatus.includes(goal)){ + console.log("AT GOAL"); + let newNode = this.graph.addNode() - 1; + this.mapping.set(newNode, action); + this.graph.addEdge(root, newNode, action.cost); + this.graph.addEdge(newNode, 1, 0); + return; + } + + //Add node and edge from root + let newNode = this.graph.addNode() - 1; + this.mapping.set(newNode, action); + this.graph.addEdge(root, newNode, action.cost); + + //Recursive call + console.log(possibleActions.indexOf(action)) + let newActions = possibleActions.filter(act => act !== action) + this.buildTree(newNode, goal, newActions, action.effects); + } + }); + } +} \ No newline at end of file diff --git a/src/Wolfie2D/AI/StateMachineGoapAI.ts b/src/Wolfie2D/AI/StateMachineGoapAI.ts new file mode 100644 index 0000000..5efa34a --- /dev/null +++ b/src/Wolfie2D/AI/StateMachineGoapAI.ts @@ -0,0 +1,40 @@ +import GoapAction from "../DataTypes/Interfaces/GoapAction"; +import GoapAI from "../DataTypes/Interfaces/GoapAI"; +import Queue from "../DataTypes/Queue"; +import Stack from "../DataTypes/Stack"; +import StateMachine from "../DataTypes/State/StateMachine"; +import GameNode from "../Nodes/GameNode"; +import GoapActionPlanner from "./GoapActionPlanner"; + +/** + * A version of a @reference[StateMachine] that is configured to work as an AI controller for a @reference[GameNode] + */ + export default class StateMachineGoapAI extends StateMachine implements GoapAI { + /** The GameNode that uses this StateMachine for its AI */ + protected owner: GameNode; + + goal: string; + + currentStatus: Array; + + possibleActions: Array; + + plan: Stack; + + planner: GoapActionPlanner; + + // @implemented + initializeAI(owner: GameNode, config: Record): void {} + + // @implemented + destroy(){ + // Get rid of our reference to the owner + delete this.owner; + this.receiver.destroy(); + } + + // @implemented + activate(options: Record): void {} + + changeGoal(goal: string): void {} +} \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Graphs/Graph.ts b/src/Wolfie2D/DataTypes/Graphs/Graph.ts index 21168c2..0f05973 100644 --- a/src/Wolfie2D/DataTypes/Graphs/Graph.ts +++ b/src/Wolfie2D/DataTypes/Graphs/Graph.ts @@ -125,7 +125,7 @@ export default class Graph { for(let i = 0; i < this.numVertices; i++){ let edge = this.edges[i]; let edgeStr = ""; - while(edge !== null){ + while(edge !== undefined && edge !== null){ edgeStr += edge.y.toString(); if(this.weighted){ edgeStr += " (" + edge.weight + ")"; diff --git a/src/Wolfie2D/DataTypes/Interfaces/Actor.ts b/src/Wolfie2D/DataTypes/Interfaces/Actor.ts index c26f62e..bd64673 100644 --- a/src/Wolfie2D/DataTypes/Interfaces/Actor.ts +++ b/src/Wolfie2D/DataTypes/Interfaces/Actor.ts @@ -1,12 +1,13 @@ import NavigationPath from "../../Pathfinding/NavigationPath"; import AI from "./AI"; +import GoapAI from "./GoapAI"; /** * A game object that has an AI and can perform its own actions every update cycle */ export default interface Actor { /** The AI of the actor */ - ai: AI; + ai: AI | GoapAI; /** The activity status of the actor */ aiActive: boolean; @@ -21,8 +22,9 @@ export default interface Actor { * Adds an AI to this Actor. * @param ai The name of the AI, or the actual AI, to add to the Actor. * @param options The options to give to the AI for initialization. + * @param type The type of the AI, 0 for AI, 1 for GoapAI, defaults to assume AI */ - addAI(ai: string | (new () => T), options: Record): void; + addAI(ai: string | (new () => T), options: Record, type?: number): void; /** * Sets the AI to start/stop for this Actor. diff --git a/src/Wolfie2D/DataTypes/Interfaces/GoapAI.ts b/src/Wolfie2D/DataTypes/Interfaces/GoapAI.ts new file mode 100644 index 0000000..13d258d --- /dev/null +++ b/src/Wolfie2D/DataTypes/Interfaces/GoapAI.ts @@ -0,0 +1,42 @@ +import GoapActionPlanner from "../../AI/GoapActionPlanner"; +import GameEvent from "../../Events/GameEvent"; +import GameNode from "../../Nodes/GameNode"; +import Queue from "../Queue"; +import Stack from "../Stack"; +import GoapAction from "./GoapAction"; +import Updateable from "./Updateable"; + +/** + * Defines a controller for a bot or a human. Must be able to update + */ +export default interface GoapAI extends Updateable { + /** Current goal of the AI */ + goal: string; + + /** All current statuses this AI has */ + currentStatus: Array; + + /** All possible actions that can be carried out */ + possibleActions: Array; + + /** Current actions to be carried out */ + plan: Stack; + + /** Once we have no actions, the planner can be called to find a new sequence of actions */ + planner: GoapActionPlanner; + + /** Clears references from to the owner */ + destroy(): void; + + /** Activates this AI from a stopped state and allows variables to be passed in */ + activate(options: Record): void; + + /** Handles events from the Actor */ + handleEvent(event: GameEvent): void; + + /** Initializes the AI with the actor and any additional config */ + initializeAI(owner:GameNode, options: Record): void + + /** Change the goal to a new goal */ + changeGoal(goal: string): void +} \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Interfaces/GoapAction.ts b/src/Wolfie2D/DataTypes/Interfaces/GoapAction.ts new file mode 100644 index 0000000..0ddb63a --- /dev/null +++ b/src/Wolfie2D/DataTypes/Interfaces/GoapAction.ts @@ -0,0 +1,61 @@ +import StateMachineGoapAI from "../../AI/StateMachineGoapAI"; + +export default abstract class GoapAction { + /** Cost it takes to complete this action */ + cost: number; + + /** Preconditions that have to be satisfied for an action to happen */ + preconditions: Array; + + /** Resulting statuses after this action completes */ + effects: Array; + + /** If the action fails, do we keep trying until we succeed */ + loopAction: boolean; + + /** + * Attempt to perform an action, if successful, it will return an array of the expected effects, otherwise it will return null + * @param statuses Current statuses of the actor + * @param actor GameNode for the actor + * @param deltaT The time sine the last update + * @param target GameNode for a optional target + */ + abstract performAction(statuses: Array, actor: StateMachineGoapAI, deltaT: number, target?: StateMachineGoapAI): Array; + + /** Check preconditions with current statuses to see if action can be performed */ + checkPreconditions(statuses: Array): boolean { + // Check that every element in the preconditions array is found in the statuses array + return (this.preconditions.every((status) => { + if (!statuses.includes(status)){ + return false; + } + return true; + })); + } + + /** Add one or more preconditions to this action */ + addPrecondition(preconditions: string | string[]): void { + this.preconditions.push(...preconditions); + } + + /** Add one or more effects to this action */ + addEffect(effects: string | string[]): void { + this.effects.push(...effects); + } + + /** Removes an precondition, returns true if successful */ + removePrecondition(precondition: string): boolean { + throw new Error("Method not implemented."); + } + + /** Removes an precondition, returns true if successful */ + removeEffect(effect: string): boolean { + throw new Error("Method not implemented."); + } + + /** Update the cost of this action based on options */ + abstract updateCost(options: Record): void; + + abstract toString(): string; + +} \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Interfaces/Navigable.ts b/src/Wolfie2D/DataTypes/Interfaces/Navigable.ts index 8f80c0f..5780e6f 100644 --- a/src/Wolfie2D/DataTypes/Interfaces/Navigable.ts +++ b/src/Wolfie2D/DataTypes/Interfaces/Navigable.ts @@ -7,6 +7,7 @@ export default interface Navigable { * Gets a new navigation path based on this Navigable object. * @param fromPosition The position to start navigation from. * @param toPosition The position to navigate to. + * @param direct If true, move directly from fromPosition to toPosition */ - getNavigationPath(fromPosition: Vec2, toPosition: Vec2): NavigationPath; + getNavigationPath(fromPosition: Vec2, toPosition: Vec2, direct?: boolean): NavigationPath; } \ No newline at end of file diff --git a/src/Wolfie2D/Events/BattleSystem.ts b/src/Wolfie2D/Events/BattleSystem.ts new file mode 100644 index 0000000..eb8ff6a --- /dev/null +++ b/src/Wolfie2D/Events/BattleSystem.ts @@ -0,0 +1,38 @@ +import Updateable from "../DataTypes/Interfaces/Updateable"; +import EventQueue from "./EventQueue"; +import Receiver from "./Receiver"; + +export default abstract class BattleSystem implements Updateable { + + units: Map>; + receiver: Receiver; + eventQueue: EventQueue; + statSystem: Array; + + constructor(battleEvents: Array, statSystem: Array){ + this.eventQueue = EventQueue.getInstance(); + this.eventQueue.subscribe(this.receiver, battleEvents); + this.statSystem = statSystem; + } + + getUnitStats(id: number): Record { + return this.units.get(id); + } + + validateStats(stats: Record): Record { + this.statSystem.forEach(e => { + if (stats[e] === undefined){ + stats[e] = 0; + } + }); + return stats; + } + + initalizeUnit(id: number, stats: Record): void{ + this.units.set(id, this.validateStats(stats)); + } + + abstract update(deltaT: number): void; + + +} \ No newline at end of file diff --git a/src/Wolfie2D/Input/Input.ts b/src/Wolfie2D/Input/Input.ts index b9c7720..cbcf756 100644 --- a/src/Wolfie2D/Input/Input.ts +++ b/src/Wolfie2D/Input/Input.ts @@ -12,6 +12,7 @@ import { GameEventType } from "../Events/GameEventType"; export default class Input { private static mousePressed: boolean; private static mouseJustPressed: boolean; + private static mouseButtonPressed: number; private static keyJustPressed: Map; private static keyPressed: Map; @@ -35,7 +36,7 @@ export default class Input { * Initializes the Input object * @param viewport A reference to the viewport of the game */ - static initialize(viewport: Viewport, keyMap: Array>){ + static initialize(viewport: Viewport, keyMap: Array>) { Input.viewport = viewport; Input.mousePressed = false; Input.mouseJustPressed = false; @@ -53,7 +54,7 @@ export default class Input { Input.keyMap = new Map(); // Add all keys to the keymap - for(let entry in keyMap){ + for (let entry in keyMap) { let name = keyMap[entry].name; let keys = keyMap[entry].keys; Input.keyMap.add(name, keys); @@ -62,7 +63,7 @@ export default class Input { Input.eventQueue = EventQueue.getInstance(); // Subscribe to all input events Input.eventQueue.subscribe(Input.receiver, [GameEventType.MOUSE_DOWN, GameEventType.MOUSE_UP, GameEventType.MOUSE_MOVE, - GameEventType.KEY_DOWN, GameEventType.KEY_UP, GameEventType.CANVAS_BLUR, GameEventType.WHEEL_UP, GameEventType.WHEEL_DOWN]); + GameEventType.KEY_DOWN, GameEventType.KEY_UP, GameEventType.CANVAS_BLUR, GameEventType.WHEEL_UP, GameEventType.WHEEL_DOWN]); } static update(deltaT: number): void { @@ -72,53 +73,54 @@ export default class Input { Input.justScrolled = false; Input.scrollDirection = 0; - while(Input.receiver.hasNextEvent()){ + while (Input.receiver.hasNextEvent()) { let event = Input.receiver.getNextEvent(); - + // Handle each event type - if(event.type === GameEventType.MOUSE_DOWN){ + if (event.type === GameEventType.MOUSE_DOWN) { Input.mouseJustPressed = true; Input.mousePressed = true; - Input.mousePressPosition = event.data.get("position"); + Input.mousePressPosition = event.data.get("position"); + Input.mouseButtonPressed = event.data.get("button"); } - if(event.type === GameEventType.MOUSE_UP){ + if (event.type === GameEventType.MOUSE_UP) { Input.mousePressed = false; } - if(event.type === GameEventType.MOUSE_MOVE){ + if (event.type === GameEventType.MOUSE_MOVE) { Input.mousePosition = event.data.get("position"); } - if(event.type === GameEventType.KEY_DOWN){ + if (event.type === GameEventType.KEY_DOWN) { let key = event.data.get("key"); // Handle space bar - if(key === " "){ + if (key === " ") { key = "space"; } - if(!Input.keyPressed.get(key)){ + if (!Input.keyPressed.get(key)) { Input.keyJustPressed.set(key, true); Input.keyPressed.set(key, true); } } - if(event.type === GameEventType.KEY_UP){ + if (event.type === GameEventType.KEY_UP) { let key = event.data.get("key"); // Handle space bar - if(key === " "){ + if (key === " ") { key = "space"; } Input.keyPressed.set(key, false); } - if(event.type === GameEventType.CANVAS_BLUR){ + if (event.type === GameEventType.CANVAS_BLUR) { Input.clearKeyPresses() } - if(event.type === GameEventType.WHEEL_UP){ + if (event.type === GameEventType.WHEEL_UP) { Input.scrollDirection = -1; Input.justScrolled = true; - } else if(event.type === GameEventType.WHEEL_DOWN){ + } else if (event.type === GameEventType.WHEEL_DOWN) { Input.scrollDirection = 1; Input.justScrolled = true; } @@ -137,9 +139,9 @@ export default class Input { * @returns True if the key was just pressed, false otherwise */ static isKeyJustPressed(key: string): boolean { - if(Input.keysDisabled) return false; + if (Input.keysDisabled) return false; - if(Input.keyJustPressed.has(key)){ + if (Input.keyJustPressed.has(key)) { return Input.keyJustPressed.get(key) } else { return false; @@ -152,11 +154,11 @@ export default class Input { * @returns An array of all of the newly pressed keys. */ static getKeysJustPressed(): Array { - if(Input.keysDisabled) return []; + if (Input.keysDisabled) return []; let keys = Array(); Input.keyJustPressed.forEach(key => { - if(Input.keyJustPressed.get(key)){ + if (Input.keyJustPressed.get(key)) { keys.push(key); } }); @@ -169,9 +171,9 @@ export default class Input { * @returns True if the key is currently pressed, false otherwise */ static isKeyPressed(key: string): boolean { - if(Input.keysDisabled) return false; + if (Input.keysDisabled) return false; - if(Input.keyPressed.has(key)){ + if (Input.keyPressed.has(key)) { return Input.keyPressed.get(key) } else { return false; @@ -200,20 +202,20 @@ export default class Input { * @returns True if the input was just pressed, false otherwise */ static isJustPressed(inputName: string): boolean { - if(Input.keysDisabled) return false; + if (Input.keysDisabled) return false; - if(Input.keyMap.has(inputName)){ + if (Input.keyMap.has(inputName)) { const keys = Input.keyMap.get(inputName); let justPressed = false; - for(let key of keys){ + for (let key of keys) { justPressed = justPressed || Input.isKeyJustPressed(key); } return justPressed; } else { return false; - } + } } /** @@ -222,13 +224,13 @@ export default class Input { * @returns True if the input is pressed, false otherwise */ static isPressed(inputName: string): boolean { - if(Input.keysDisabled) return false; + if (Input.keysDisabled) return false; - if(Input.keyMap.has(inputName)){ + if (Input.keyMap.has(inputName)) { const keys = Input.keyMap.get(inputName); let pressed = false; - for(let key of keys){ + for (let key of keys) { pressed = pressed || Input.isKeyPressed(key); } @@ -237,20 +239,30 @@ export default class Input { return false; } } - /** - * Returns whether or not the mouse was newly pressed Input frame + * + * Returns whether or not the mouse was newly pressed Input frame. + * @param mouseButton Optionally specify which mouse click you want to know was pressed. + * 0 for left click, 1 for middle click, 2 for right click. * @returns True if the mouse was just pressed, false otherwise */ - static isMouseJustPressed(): boolean { + static isMouseJustPressed(mouseButton?: number): boolean { + if (mouseButton) { + return Input.mouseJustPressed && !Input.mouseDisabled && mouseButton == this.mouseButtonPressed; + } return Input.mouseJustPressed && !Input.mouseDisabled; } /** * Returns whether or not the mouse is currently pressed + * @param mouseButton Optionally specify which mouse click you want to know was pressed. + * 0 for left click, 1 for middle click, 2 for right click. * @returns True if the mouse is currently pressed, false otherwise */ - static isMousePressed(): boolean { + static isMousePressed(mouseButton?: number): boolean { + if (mouseButton) { + return Input.mousePressed && !Input.mouseDisabled && mouseButton == this.mouseButtonPressed; + } return Input.mousePressed && !Input.mouseDisabled; } @@ -275,7 +287,7 @@ export default class Input { * @returns The mouse position stored as a Vec2 */ static getMousePosition(): Vec2 { - return Input.mousePosition.scaled(1/this.viewport.getZoomLevel()); + return Input.mousePosition.scaled(1 / this.viewport.getZoomLevel()); } /** @@ -284,7 +296,7 @@ export default class Input { * @returns The mouse position stored as a Vec2 */ static getGlobalMousePosition(): Vec2 { - return Input.mousePosition.clone().scale(1/this.viewport.getZoomLevel()).add(Input.viewport.getOrigin()); + return Input.mousePosition.clone().scale(1 / this.viewport.getZoomLevel()).add(Input.viewport.getOrigin()); } /** diff --git a/src/Wolfie2D/Input/InputHandler.ts b/src/Wolfie2D/Input/InputHandler.ts index 4d34811..ae25128 100644 --- a/src/Wolfie2D/Input/InputHandler.ts +++ b/src/Wolfie2D/Input/InputHandler.ts @@ -29,7 +29,8 @@ export default class InputHandler { private handleMouseDown = (event: MouseEvent, canvas: HTMLCanvasElement): void => { let pos = this.getMousePosition(event, canvas); - let gameEvent = new GameEvent(GameEventType.MOUSE_DOWN, {position: pos}); + let button = event.button; + let gameEvent = new GameEvent(GameEventType.MOUSE_DOWN, {position: pos, button: button}); this.eventQueue.addEvent(gameEvent); } diff --git a/src/Wolfie2D/Nodes/GameNode.ts b/src/Wolfie2D/Nodes/GameNode.ts index 6ae3794..de2af5f 100644 --- a/src/Wolfie2D/Nodes/GameNode.ts +++ b/src/Wolfie2D/Nodes/GameNode.ts @@ -18,6 +18,7 @@ import TweenController from "../Rendering/Animations/TweenController"; import Debug from "../Debug/Debug"; import Color from "../Utils/Color"; import Circle from "../DataTypes/Shapes/Circle"; +import GoapAI from "../DataTypes/Interfaces/GoapAI"; /** * The representation of an object in the game world. @@ -54,7 +55,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable isColliding: boolean = false; /*---------- ACTOR ----------*/ - _ai: AI; + _ai: AI | GoapAI; aiActive: boolean; path: NavigationPath; pathfinding: boolean = false; @@ -331,11 +332,11 @@ export default abstract class GameNode implements Positioned, Unique, Updateable } /*---------- ACTOR ----------*/ - get ai(): AI { + get ai(): AI | GoapAI { return this._ai; } - set ai(ai: AI) { + set ai(ai: AI | GoapAI) { if(!this._ai){ // If we haven't been previously had an ai, register us with the ai manager this.scene.getAIManager().registerActor(this); @@ -346,7 +347,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable } // @implemented - addAI(ai: string | (new () => T), options?: Record): void { + addAI(ai: string | (new () => T), options?: Record, type?: number): void { if(!this._ai){ this.scene.getAIManager().registerActor(this); } @@ -357,6 +358,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable this._ai = new ai(); } + // Question, how much do we want different type of AI to be handled the same, i.e. should GoapAI and AI similar methods and signatures for the sake of unity this._ai.initializeAI(this, options); this.aiActive = true; diff --git a/src/Wolfie2D/Nodes/Graphic.ts b/src/Wolfie2D/Nodes/Graphic.ts index 79383ad..c90afe4 100644 --- a/src/Wolfie2D/Nodes/Graphic.ts +++ b/src/Wolfie2D/Nodes/Graphic.ts @@ -29,4 +29,28 @@ export default abstract class Graphic extends CanvasNode { setColor(color: Color){ this.color = color; } + + set colorR(r: number){ + this.color.r = r; + } + + get colorR(): number { + return this.color.r; + } + + set colorG(g: number){ + this.color.g = g; + } + + get colorG(): number { + return this.color.g; + } + + set colorB(b: number){ + this.color.b = b; + } + + get colorB(): number { + return this.color.b; + } } \ No newline at end of file diff --git a/src/Wolfie2D/Nodes/Graphics/GraphicTypes.ts b/src/Wolfie2D/Nodes/Graphics/GraphicTypes.ts index 3631c46..2dfdc6c 100644 --- a/src/Wolfie2D/Nodes/Graphics/GraphicTypes.ts +++ b/src/Wolfie2D/Nodes/Graphics/GraphicTypes.ts @@ -2,4 +2,5 @@ export enum GraphicType { POINT = "POINT", RECT = "RECT", LINE = "LINE", + PARTICLE = "PARTICLE" } \ No newline at end of file diff --git a/src/Wolfie2D/Nodes/Graphics/Particle.ts b/src/Wolfie2D/Nodes/Graphics/Particle.ts new file mode 100644 index 0000000..bb0fb87 --- /dev/null +++ b/src/Wolfie2D/Nodes/Graphics/Particle.ts @@ -0,0 +1,57 @@ +import Vec2 from "../../DataTypes/Vec2"; +import Point from "./Point"; + +/** + * - Position X +- Velocity (speed and direction) X +- Color X +- Lifetime +- Age can be handled as lifetime +- Shape X +- Size X +- Transparency X + */ + + +export default class Particle extends Point { + age: number; + + inUse: boolean; + + vel: Vec2; + + mass: number; + + constructor(position: Vec2, size: Vec2, mass: number) { + // Are we making this a circle? + super(position, size); + this.inUse = false; + this.mass = mass; + } + + setParticleActive(lifetime: number, position: Vec2) { + this.age = lifetime; + this.inUse = true; + this.visible = true; + this.position = position; + } + + decrementAge(decay: number) { + this.age -= decay; + } + + setParticleInactive(){ + this.inUse = false; + this.visible = false; + } + + set velY(y: number){ + this.vel.y = y; + } + + get velY(): number { + return this.vel.y; + } + + +} \ No newline at end of file diff --git a/src/Wolfie2D/Nodes/Graphics/Point.ts b/src/Wolfie2D/Nodes/Graphics/Point.ts index 0f75c5a..5da7a24 100644 --- a/src/Wolfie2D/Nodes/Graphics/Point.ts +++ b/src/Wolfie2D/Nodes/Graphics/Point.ts @@ -4,9 +4,10 @@ import Vec2 from "../../DataTypes/Vec2"; /** A basic point to be drawn on the screen. */ export default class Point extends Graphic { - constructor(position: Vec2){ + constructor(position: Vec2, size: Vec2) { + // Are we making this a circle? super(); this.position = position; - this.size.set(5, 5); + this.size.set(size.x, size.y); } } \ No newline at end of file diff --git a/src/Wolfie2D/Pathfinding/NavigationManager.ts b/src/Wolfie2D/Pathfinding/NavigationManager.ts index 1daf89d..9827913 100644 --- a/src/Wolfie2D/Pathfinding/NavigationManager.ts +++ b/src/Wolfie2D/Pathfinding/NavigationManager.ts @@ -30,10 +30,11 @@ export default class NavigationManager { * @param navName The name of the registered Navigable object * @param fromPosition The starting position of navigation * @param toPosition The ending position of Navigation + * @param direct If true, go direct from fromPosition to toPosition, don't use NavMesh * @returns A NavigationPath containing the route to take over the Navigable entity to get between the provided positions. */ - getPath(navName: string, fromPosition: Vec2, toPosition: Vec2): NavigationPath { + getPath(navName: string, fromPosition: Vec2, toPosition: Vec2, direct?: boolean): NavigationPath { let nav = this.navigableEntities.get(navName); - return nav.getNavigationPath(fromPosition.clone(), toPosition.clone()); + return nav.getNavigationPath(fromPosition.clone(), toPosition.clone(), direct); } } \ No newline at end of file diff --git a/src/Wolfie2D/Pathfinding/Navmesh.ts b/src/Wolfie2D/Pathfinding/Navmesh.ts index 4b62765..1e8e1b8 100644 --- a/src/Wolfie2D/Pathfinding/Navmesh.ts +++ b/src/Wolfie2D/Pathfinding/Navmesh.ts @@ -21,18 +21,23 @@ export default class Navmesh implements Navigable { } // @implemented - getNavigationPath(fromPosition: Vec2, toPosition: Vec2): NavigationPath { + getNavigationPath(fromPosition: Vec2, toPosition: Vec2, direct: boolean): NavigationPath { let start = this.getClosestNode(fromPosition); let end = this.getClosestNode(toPosition); - let parent = GraphUtils.djikstra(this.graph, start); - let pathStack = new Stack(this.graph.numVertices); // Push the final position and the final position in the graph pathStack.push(toPosition.clone()); + + if (direct) { + return new NavigationPath(pathStack); + } + pathStack.push(this.graph.positions[end]); + let parent = GraphUtils.djikstra(this.graph, start); + // Add all parents along the path let i = end; while(parent[i] !== -1){ diff --git a/src/Wolfie2D/Rendering/Animations/ParticleSystem.ts b/src/Wolfie2D/Rendering/Animations/ParticleSystem.ts new file mode 100644 index 0000000..ea7810f --- /dev/null +++ b/src/Wolfie2D/Rendering/Animations/ParticleSystem.ts @@ -0,0 +1,163 @@ +import Updateable from "../../DataTypes/Interfaces/Updateable"; +import Vec2 from "../../DataTypes/Vec2"; +import { GraphicType } from "../../Nodes/Graphics/GraphicTypes"; +import Particle from "../../Nodes/Graphics/Particle"; +import Scene from "../../Scene/Scene"; +import Timer from "../../Timing/Timer"; +import Color from "../../Utils/Color"; +import { EaseFunctionType } from "../../Utils/EaseFunctions"; +import RandUtils from "../../Utils/RandUtils"; +import ParticleSystemManager from "./ParticleSystemManager"; + +/* +-Move particle system to HW#4, particle class and particle manager(object pool), source, randomized period of decay, + semi-randomized approach for spawning, should be general purpose + and load some settings from a json (location, states, colors, randomization). + Should be effect when balloon is popped +*/ + +export default class ParticleSystem implements Updateable { + protected particlePool: Array; + + protected lifetime: number; + + protected liveParticles: number; + + protected maxLiveParticles: number; + + protected sourcePoint: Vec2; + + protected particleSize: Vec2; + + protected systemLifetime: Timer; + + protected systemRunning: boolean; + + protected color: Color = new Color(255, 0, 0); + + constructor(poolSize: number, sourcePoint: Vec2, lifetime: number, size: number, maxParticles: number) { + this.particlePool = new Array(poolSize); + this.sourcePoint = sourcePoint; + this.lifetime = lifetime; + this.particleSize = new Vec2(size, size); + this.maxLiveParticles = maxParticles; + this.systemRunning = false; + + ParticleSystemManager.getInstance().registerParticleSystem(this); + } + + initalizePool(scene: Scene, layer: string, type: ParticleSystemType, mass: number) { + for (let i = 0; i < this.particlePool.length; i++) { + this.particlePool[i] = scene.add.graphic(GraphicType.PARTICLE, layer, + { position: this.sourcePoint.clone(), size: this.particleSize.clone(), mass: mass }); + this.particlePool[i].addPhysics(); + this.particlePool[i].isCollidable = false; + this.particlePool[i].visible = false; + } + } + + startSystem(time: number, startPoint?: Vec2) { + this.systemLifetime = new Timer(time); + this.systemLifetime.start(); + this.systemRunning = true; + this.sourcePoint = startPoint; + } + + stopSystem() { + console.log(this); + this.systemRunning = false; + for (let particle of this.particlePool) { + if (particle.inUse) { + particle.setParticleInactive(); + } + } + } + + changeColor(color: Color) { + this.color = color; + } + + update(deltaT: number) { + if (!this.systemRunning) { + return; + } + if (this.systemLifetime.isStopped()) { + this.stopSystem(); + } + else { + for (let particle of this.particlePool) { + if (particle.inUse) { + particle.decrementAge(deltaT * 1000); + + if (particle.age <= 0) { + particle.setParticleInactive(); + } + + //particle.vel.y += 200*deltaT; + particle.move(particle.vel.scaled(deltaT)); + } + else { + particle.setParticleActive(this.lifetime, this.sourcePoint.clone()); + + particle.color = this.color; + particle.alpha = 1; + //particle.size.set(1) + particle.vel = RandUtils.randVec(-50, 50, -100, 100); + + particle.tweens.add("active", { + startDelay: 0, + duration: 2000, + effects: [ + { + property: "alpha", + resetOnComplete: true, + start: 1, + end: 0, + ease: EaseFunctionType.IN_OUT_SINE + }, + /*{ + property: "colorR", + resetOnComplete: true, + start: particle.color.r, + end: 255, + ease: EaseFunctionType.IN_OUT_SINE + }, + { + property: "colorG", + resetOnComplete: true, + start: particle.color.g, + end: 255, + ease: EaseFunctionType.IN_OUT_SINE + }, + { + property: "colorB", + resetOnComplete: true, + start: particle.color.b, + end: 255, + ease: EaseFunctionType.IN_OUT_SINE + },*/ + { + property: "velY", + resetOnComplete: true, + start: particle.vel.y, + end: particle.vel.y + ((this.lifetime * particle.mass)/2), + ease: EaseFunctionType.IN_OUT_SINE + } + ] + }); + + particle.tweens.play("active"); + + //particle.vel = RandUtils.randVec(-150, 150, -100, 100); + //console.log(particle.vel.toString()); + } + } + } + } + +} + +export enum ParticleSystemType { + emitter = "emitter", + burst = "burst" +} diff --git a/src/Wolfie2D/Rendering/Animations/ParticleSystemManager.ts b/src/Wolfie2D/Rendering/Animations/ParticleSystemManager.ts new file mode 100644 index 0000000..8ca9603 --- /dev/null +++ b/src/Wolfie2D/Rendering/Animations/ParticleSystemManager.ts @@ -0,0 +1,40 @@ +import Updateable from "../../DataTypes/Interfaces/Updateable"; +import ParticleSystem from "./ParticleSystem"; + +export default class ParticleSystemManager implements Updateable { + + private static instance: ParticleSystemManager = null; + + protected particleSystems: Array; + + private constructor(){ + this.particleSystems = new Array(); + } + + static getInstance(): ParticleSystemManager { + if(ParticleSystemManager.instance === null){ + ParticleSystemManager.instance = new ParticleSystemManager(); + } + + return ParticleSystemManager.instance; + } + + registerParticleSystem(system: ParticleSystem){ + this.particleSystems.push(system); + } + + deregisterParticleSystem(system: ParticleSystem){ + let index = this.particleSystems.indexOf(system); + this.particleSystems.splice(index, 1); + } + + clearParticleSystems(){ + this.particleSystems = new Array(); + } + + update(deltaT: number): void { + for(let particleSystem of this.particleSystems){ + particleSystem.update(deltaT); + } + } +} \ No newline at end of file diff --git a/src/Wolfie2D/Scene/Factories/CanvasNodeFactory.ts b/src/Wolfie2D/Scene/Factories/CanvasNodeFactory.ts index 9611850..267dc1d 100644 --- a/src/Wolfie2D/Scene/Factories/CanvasNodeFactory.ts +++ b/src/Wolfie2D/Scene/Factories/CanvasNodeFactory.ts @@ -14,6 +14,7 @@ import TextInput from "../../Nodes/UIElements/TextInput"; import Rect from "../../Nodes/Graphics/Rect"; import ResourceManager from "../../ResourceManager/ResourceManager"; import Line from "../../Nodes/Graphics/Line"; +import Particle from "../../Nodes/Graphics/Particle"; // @ignorePage @@ -143,6 +144,9 @@ export default class CanvasNodeFactory { case GraphicType.RECT: instance = this.buildRect(options); break; + case GraphicType.PARTICLE: + instance = this.buildParticle(options); + break; default: throw `GraphicType '${type}' does not exist, or is registered incorrectly.` } @@ -196,8 +200,18 @@ export default class CanvasNodeFactory { buildPoint(options?: Record): Point { this.checkIfPropExists("Point", options, "position", Vec2, "Vec2"); + this.checkIfPropExists("Point", options, "size", Vec2, "Vec2"); - return new Point(options.position); + return new Point(options.position, options.size); + } + + buildParticle(options?: Record): Point { + this.checkIfPropExists("Particle", options, "position", Vec2, "Vec2"); + this.checkIfPropExists("Particle", options, "size", Vec2, "Vec2"); + this.checkIfPropExists("Particle", options, "mass", "number", "number"); + + //Changed for testing + return new Particle(options.position, options.size, options.mass); } buildLine(options?: Record): Point { diff --git a/src/Wolfie2D/Scene/Scene.ts b/src/Wolfie2D/Scene/Scene.ts index ee7f5a3..d25e8f5 100644 --- a/src/Wolfie2D/Scene/Scene.ts +++ b/src/Wolfie2D/Scene/Scene.ts @@ -25,6 +25,7 @@ import RenderingManager from "../Rendering/RenderingManager"; import Debug from "../Debug/Debug"; import TimerManager from "../Timing/TimerManager"; import TweenManager from "../Rendering/Animations/TweenManager"; +import ParticleSystemManager from "../Rendering/Animations/ParticleSystemManager"; /** * Scenes are the main container in the game engine. @@ -173,6 +174,9 @@ export default class Scene implements Updateable { // Update all tweens TweenManager.getInstance().update(deltaT); + // Update all particle systems + ParticleSystemManager.getInstance().update(deltaT); + // Update viewport this.viewport.update(deltaT); } diff --git a/src/main.ts b/src/main.ts index 7746269..916faf1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,4 @@ + import Game from "./Wolfie2D/Loop/Game"; import default_scene from "./default_scene";