diff --git a/public/assets/images/world/world_terrain_atlas.png b/public/assets/images/world/world_terrain_atlas.png index 20683e7..0d109c1 100644 Binary files a/public/assets/images/world/world_terrain_atlas.png and b/public/assets/images/world/world_terrain_atlas.png differ diff --git a/src/Game/Game.js b/src/Game/Game.js index 4f62baf..25910bd 100644 --- a/src/Game/Game.js +++ b/src/Game/Game.js @@ -68,10 +68,11 @@ function setupInGameSelector() { // npccc.moveTo(new Point2D(getTileAt(t.x, t.y, ChunkStorageTypes.TYPE_TERRAIN).worldPosition.getX(), getTileAt(t.x, t.y, ChunkStorageTypes.TYPE_TERRAIN).worldPosition.getY())); let tile = getNavigationGridTile(t.x, t.y); if(tile.isObstacle) return; - npccc.moveTo(new PointInt2D(tile.position.getX(), tile.position.getY()), - (cb)=>{ - console.log(cb); - }); + npccc.startTask(new PointInt2D(tile.position.getX(), tile.position.getY())); + // npccc.moveTo(new PointInt2D(tile.position.getX(), tile.position.getY()), + // (cb)=>{ + // console.log(cb); + // }); } } diff --git a/src/Game/NPC/NPCBehavior/NPCBehavior.js b/src/Game/NPC/NPCBehavior/NPCBehavior.js new file mode 100644 index 0000000..ec0831a --- /dev/null +++ b/src/Game/NPC/NPCBehavior/NPCBehavior.js @@ -0,0 +1,447 @@ +import { PointInt2D } from "../../Utils/Math.utils"; +// import { findPathOnNavigationGridIfExists } from "../../World/NavigationGrid/NavigationGrid"; +import { NPCController } from "../NPCController/NPCController"; +// import { NPCProto } from "../NPCProto/NPCProto"; + +async function NPCJobFunctor(props, action) { + return new NPCActionCallbackResult(); +} + +/** + * + * @param {NPCActionCallbackResult} lastActionResult + */ +function NPCActionCallback(lastActionResult) {} + +/** + * + * @param {NPCTaskCallbackResult} taskResult + */ +function NPCTaskCallback(taskResult) {} + +export class NPCActionCallbackResult { + /** + * status of this action. -1 - error, 0 - waiting for execution, 1 - completed, 2 - can not be completed + */ + status = -1; + statusText = ""; + /** + * @type {NPCAction} + */ + action = undefined; + + /** + * statuses: -1 - error, 0 - waiting for execution, 1 - completed, 2 - can not be completed + * @param {Number} status + * @param {String} text + * @param {NPCAction} action + */ + constructor(status, text = "", action = undefined) { + this.status = status; + this.statusText = text; + this.action = action; + } + + clone() { + return new NPCActionCallbackResult(this.status, this.statusText, this.action); + } +} + +export class NPCTaskCallbackResult { + /** + * status of this task. -1 - error, 0 - completed, 1 - can not be completed, 2 - reset + */ + status = -1; + statusText = ""; + /** + * @type {NPCActionCallbackResult} + */ + lastActionCallbackResult; + /** + * @type {NPCTask} + */ + task = undefined; + + /** + * + * @param {Number} status + * @param {String} statusText + * @param {NPCActionCallbackResult} lastActionCallbackResult + * @param {NPCTask} task + */ + constructor(status, statusText, lastActionCallbackResult, task) { + this.status = status; + this.statusText = statusText; + this.lastActionCallbackResult = lastActionCallbackResult; + this.task = task; + } + + clone() + { + return new NPCTaskCallbackResult(this.status, this.statusText, this.lastActionCallbackResult, this.task); + } +} + +/** + * NPC Action is the smallest part of NPC's behavior. + * There are 2 types of actions: + * 0: move to tile + * 1: do something + */ +export class NPCAction { + //invalid action type by default + type = -1; + //target position + targetPosition = new PointInt2D(); + /** + * target job function + * @type {NPCJobFunctor} + */ + targetJob; + /** + * these props will passed to job function on it's execution + */ + targetJobProps; + /** + * status of this action. -1 - error, 0 - waiting for execution, 1 - completed, 2 - can not be completed + */ + status = -1; + /** + * @type {NPCProto} + */ + owner = null; + /** + * will this task block chain of execution + */ + skipToNextIgnoringStatus = false; + + usePause = false; + pauseTime = 0; + + /** + * NPC Action is the smallest part of NPC's behavior. + * There are 2 types of actions: + * 0: move to tile + * 1: do something + * @param {Number} type + * @param {PointInt2D} pos + * @param {NPCJobFunctor} job + * @param {any} jobProps + * @param {NPCController} owner + * @param {Boolean} skipToNext + * @param {Boolean} usePause + * @param {Number} pauseTime + */ + constructor(type, pos, job, jobProps = {}, owner = null, skipToNext = false, usePause = false, pauseTime = 0) { + this.type = type; + this.targetPosition = pos; + this.targetJob = job; + this.targetJobProps = jobProps; + this.status = 0; + this.owner = owner; + this.skipToNextIgnoringStatus = skipToNext; + this.usePause = usePause; + this.pauseTime = pauseTime; + } +} + +/** + * NPC Task is a container with NPC Actions. It is like a timeline with frames (frames are NPC Actions) + */ +export class NPCTask { + /** + * @type {Array} + */ + actionsContainer = []; + _currentIndex = 0; + _startIndex = 0; + /** + * @type {NPCController} + */ + owner = null; + //is task active and will execute actions + active = false; + //will task start again after completion + loop = false; + onPause = false; + + _lastActionCompleted = false; + /** + * @type {NPCActionCallbackResult} + */ + _lastActionCallbackResult; + + /** + * @type {NPCActionCallback} + */ + actionCallback; + /** + * @type {NPCTaskCallback} + */ + taskCallback; + + _pauseTimeout; + + /** + * @param {Array} actions + * @param {NPCController} owner + * @param {boolean} loop + * @param {NPCActionCallback} actionCallback + * @param {NPCTaskCallback} taskCallback + */ + constructor(actions, owner = null, loop = false, actionCallback = undefined, taskCallback = undefined) { + this.actionsContainer = actions; + this.owner = owner; + this.loop = loop; + this.actionCallback = actionCallback; + this.taskCallback = taskCallback; + } + + startTask() { + if (!this.owner || this._lastActionCompleted) return false; + this._currentIndex = this._startIndex; + this.active = true; + return this._pushNewAction(); + } + + resetTask() { + this.active = false; + this.onPause = false; + this._resetAllActions(); + this._currentIndex = this._startIndex; + clearTimeout(this._pauseTimeout); + this.taskCallback(new NPCTaskCallbackResult(2, "reset", this._lastActionCallbackResult.clone(), this)); + return true; + } + + /** + * set owner for all actions and this task + * @param {NPCController} owner + */ + setOwnerToAll(owner) { + this.owner = owner; + for (const action of this.actionsContainer) { + action.owner = owner; + } + } + + handleTaskTick(ticker) { + if (!this.active || this.onPause) return; + if (this._lastActionCompleted) { + this._lastActionCompleted = false; + this.actionCallback(this._lastActionCallbackResult.clone()); + if (!this.active) return; + if (!this.actionsContainer[this._currentIndex].skipToNextIgnoringStatus) { + if (this._lastActionCallbackResult.status !== 1) { + this.taskCallback(new NPCTaskCallbackResult(1, "chain blocked by action status", this._lastActionCallbackResult.clone(), this)); + this.resetTask(); + return; + } + } + //pay attention in timeout logic + if (this.actionsContainer[this._currentIndex].usePause) { + this.onPause = true; + clearTimeout(this._pauseTimeout); + let thisCopy = this; + this._pauseTimeout = setTimeout(() => { + thisCopy.onPause = false; + thisCopy._currentIndex++; + if (thisCopy._currentIndex >= thisCopy.actionsContainer.length) { + if (thisCopy.loop) { + thisCopy._currentIndex = thisCopy._startIndex; + thisCopy._resetAllActions(); + thisCopy._pushNewAction(); + } else thisCopy.active = false; + } else { + thisCopy._pushNewAction(); + } + }, this.actionsContainer[this._currentIndex].pauseTime); + return; + } + // + this._currentIndex++; + if (this._currentIndex >= this.actionsContainer.length) { + if (this.loop) { + this._currentIndex = this._startIndex; + this._resetAllActions(); + this._pushNewAction(); + } else { + this.active = false; + this.taskCallback(new NPCTaskCallbackResult(0, "finished", this._lastActionCallbackResult.clone(), this)); + } + } else { + this._pushNewAction(); + } + } + } + + _pushNewAction() { + switch (this.actionsContainer[this._currentIndex].type) { + case 0: + this.owner.moveTo(this.actionsContainer[this._currentIndex].targetPosition, (r) => { + this._handleMovementResult(r); + }); + break; + case 1: + this.actionsContainer[this._currentIndex].targetJob(this.actionsContainer[this._currentIndex].targetJobProps, this.actionsContainer[this._currentIndex]).then((r) => { + this._handleJobResult(r); + }); + break; + default: + return false; + } + return true; + } + + _resetAllActions() { + for (const action of this.actionsContainer) { + action.status = 0; + } + } + + /** + * @param {NPCActionCallbackResult} resultFromMoving + */ + _handleMovementResult(resultFromMoving) { + this.actionsContainer[this._currentIndex].status = resultFromMoving === 0 ? 1 : resultFromMoving === -1 ? -1 : 2; + this._lastActionCompleted = true; + this._lastActionCallbackResult = new NPCActionCallbackResult(resultFromMoving === 0 ? 1 : resultFromMoving === -1 ? -1 : 2, "", this.actionsContainer[this._currentIndex]); + } + + /** + * @param {NPCActionCallbackResult} resultFromJob + */ + _handleJobResult(resultFromJob) { + this._lastActionCallbackResult = resultFromJob; + this._lastActionCompleted = true; + } +} + +/** + * @param {NPCTask} task + */ +function TaskChanger(task) {} + +export class NPCBehaviorCallbackResult { + /** + * 0 - action callback + * 1 - task callback + * @type {Number} + */ + type; + /** + * status of the callback (see more in taskCallback or actionCallback) + * @type {Number} + */ + status; + /** + * @type {String} + */ + statusText; + /** + * @type {NPCTaskCallbackResult} + */ + taskCallbackResult; + /** + * @type {NPCActionCallbackResult} + */ + actionCallbackResult; + + /** + * + * @param {Number} type + * @param {Number} status + * @param {String} statusText + * @param {NPCTaskCallbackResult} taskCallbackResult + * @param {NPCActionCallbackResult} actionCallbackResult + */ + constructor(type, status, statusText, taskCallbackResult, actionCallbackResult) { + this.type = type; + this.status = status; + this.statusText = statusText; + this.taskCallbackResult = taskCallbackResult; + this.actionCallbackResult = actionCallbackResult; + } +} + +export class NPCBehavior { + /** + * @type {Map} + */ + taskMap = new Map(); + owner = undefined; + isTaskInProgress = false; + currentTask = ""; + behaviorCallback = undefined; + + /** + * @param {NPCController} owner + */ + constructor(owner, behaviorCallback) { + this.owner = owner; + this.behaviorCallback = behaviorCallback; + } + + /** + * @param {String} key + * @param {NPCTask} task + */ + addTask(key, task) { + task.setOwnerToAll(this.owner); + task.actionCallback = this._handleActionCallback.bind(this); + task.taskCallback = this._handleTaskCallback.bind(this); + this.taskMap.set(key, task); + } + + /** + * @param {String} key + */ + startTask(key) { + if (this.isTaskInProgress) return false; + if (this.taskMap.get(key).startTask()) { + this.currentTask = key; + this.isTaskInProgress = true; + return true; + } + return false; + } + + abortCurrentTask() { + if (this.taskMap.get(this.currentTask)?.resetTask()) { + this.isTaskInProgress = false; + this.currentTask = ""; + } + } + + /** + * + * @param {String} key + * @param {TaskChanger} changer + */ + changeTask(key, changer) { + changer(this.taskMap.get(key)); + } + + handleNPCBehaviorTick(ticker) { + this.taskMap.get(this.currentTask)?.handleTaskTick(ticker); + } + + /** + * + * @param {NPCTaskCallbackResult} result + */ + _handleTaskCallback(result) { + this.isTaskInProgress = false; + this.behaviorCallback(new NPCBehaviorCallbackResult(1, result.status, result.statusText, result.clone(), undefined)); + console.log(result); + } + + /** + * + * @param {NPCActionCallbackResult} result + */ + _handleActionCallback(result) { + this.behaviorCallback(new NPCBehaviorCallbackResult(0, result.status, result.statusText, undefined, result.clone())); + console.log(result); + } +} diff --git a/src/Game/NPC/NPCController/NPCController.js b/src/Game/NPC/NPCController/NPCController.js index 5ecc741..af6154d 100644 --- a/src/Game/NPC/NPCController/NPCController.js +++ b/src/Game/NPC/NPCController/NPCController.js @@ -2,12 +2,19 @@ import { GameObject } from "../../GameObject/GameObject"; import { PointInt2D } from "../../Utils/Math.utils"; import { NavigationPath, PathFinder } from "../../Utils/PathFinding.utils"; import { findPathOnNavigationGridIfExists } from "../../World/NavigationGrid/NavigationGrid"; +import { NPCAction, NPCActionCallbackResult, NPCBehavior, NPCTask } from "../NPCBehavior/NPCBehavior"; import { NPCProto } from "../NPCProto/NPCProto"; + +/** + * + * @param {Number} result + */ +function NavigationCallbackFunctor(result) {} + /** * NPCController defines NPC behavior. Many NPC can have same NPCController for the same behavior. */ -export class NPCController extends GameObject -{ +export class NPCController extends GameObject { /** * NPC controlled by this controller * @type NPCProto @@ -20,82 +27,103 @@ export class NPCController extends GameObject navigationPathQueue = new Array(); navigationInProgress = false; navigationFollowMidPoint = false; - navigationCallback = ()=>{}; + navigationCallback = () => {}; - constructor(tickAble = true) - { + taskInstanceRef = new NPCTask( + [ + new NPCAction(0, new PointInt2D(0, 0), undefined, undefined, undefined, false, true, 1000), + new NPCAction( + 1, + undefined, + async (myOwner, action) => { + console.log(myOwner); + return new NPCActionCallbackResult(1, "", action); + }, + "Привет!", + undefined, + false, + false, + 0 + ), + ], + undefined, + false + ); + + behavior = new NPCBehavior(this, () => {}); + + constructor(tickAble = true) { super(tickAble); + this.behavior.addTask("MoveToCursor", this.taskInstanceRef); } - + /** - * moves NPC to position - * @param {PointInt2D} position - * @param {Function} callback + * moves NPC to position. callback contains result status: -1 - error happened; 0 - success; 1 - unreachable + * @param {PointInt2D} position + * @param {NavigationCallbackFunctor} callback */ - moveTo(position, callback) - { + moveTo(position, callback) { // let pf = new PathFinder(); // let nPath = pf.findPathIfExist(new PointInt2D(this.controlledNPC.worldPosition.getX(), this.controlledNPC.worldPosition.getY()), position); - let nPath = findPathOnNavigationGridIfExists(new PointInt2D(this.controlledNPC.worldPosition.getX(), this.controlledNPC.worldPosition.getY()), position); - nPath.then((r)=>{ - if(r.error) - { - callback("failed"); + let nPath = findPathOnNavigationGridIfExists( + new PointInt2D(this.controlledNPC.worldPosition.getX(), this.controlledNPC.worldPosition.getY()), + position + ); + nPath.then((r) => { + if (r.error) { + this.controlledNPC.isMoving = false; + callback(-1); //error + return; + } else if (r.state === 0) { + this.controlledNPC.isMoving = false; + callback(1); //unreachable return; } - else if (r.result.path.length < 2) - { - callback("success"); - return; - } - for (let i = r.result.path.length-1; i > 0; i--) { + this.navigationPathQueue = []; + for (let i = r.result.path.length - 1; i > 0; i--) { this.navigationPathQueue.push(r.result.path[i]); } this.navigationCallback = callback; this.navigationInProgress = true; + this.controlledNPC.isMoving = true; }); // console.log("boba"); // console.log(nPath); } /** - * - * @param {NavigationPath} pathToFollow - * @param {Function} callback + * + * @param {PointInt2D} pos */ - _moveByPath(pathToFollow, callback) - { - + startTask(pos) { + this.behavior.abortCurrentTask(); + this.behavior.changeTask("MoveToCursor", (task)=>{ + task.actionsContainer[0].targetPosition = pos; + }); + this.behavior.startTask("MoveToCursor"); } - tick(ticker) - { - if(this.navigationInProgress) - { - if(!this.navigationFollowMidPoint) - { + tick(ticker) { + this.taskInstanceRef.handleTaskTick(ticker); + if (this.navigationInProgress) { + if (!this.navigationFollowMidPoint) { let target = this.navigationPathQueue.pop(); - if(!target) - { + if (!target) { this.navigationInProgress = false; - this.navigationCallback("success"); - } - else - { + this.controlledNPC.isMoving = false; + this.navigationCallback(0); //success + } else { this.controlledNPC.worldPosition = target; this.navigationFollowMidPoint = true; } - } - else - { - if( + } else { + if ( Math.abs(this.controlledNPC.drawObject.x - this.controlledNPC.worldPosition.getX()) < 0.5 && Math.abs(this.controlledNPC.drawObject.y - this.controlledNPC.worldPosition.getY()) < 0.5 - ) - { + ) { this.navigationFollowMidPoint = false; } } } } -}; \ No newline at end of file +} diff --git a/src/Game/NPC/NPCProto/NPCProto.js b/src/Game/NPC/NPCProto/NPCProto.js index 7afe020..801f44b 100644 --- a/src/Game/NPC/NPCProto/NPCProto.js +++ b/src/Game/NPC/NPCProto/NPCProto.js @@ -2,6 +2,7 @@ import { Rectangle } from "../../../pixi/pixi.mjs"; import { SceneObject } from "../../SceneObjects/SceneObject"; import { Point2D, interpolate } from "../../Utils/Math.utils"; import { getSpriteFromAtlas } from "../../Utils/Sprites.utils"; +import { getNavigationGridTile } from "../../World/NavigationGrid/NavigationGrid"; import { NPCController } from "../NPCController/NPCController"; export class NPCProto extends SceneObject @@ -13,6 +14,7 @@ export class NPCProto extends SceneObject controller = null; _positionInterpolationSpeed = 30; + _basicPositionInterpolationSpeed = 30; /** * path to NPC spritesheet @@ -24,6 +26,8 @@ export class NPCProto extends SceneObject */ frame = new Rectangle(); + isMoving = false; + /** * creates new NPC object * @param {Boolean} tickAble @@ -43,7 +47,15 @@ export class NPCProto extends SceneObject tick(ticker) { - this._positionInterpolation(ticker.deltaMS / 1000 * this._positionInterpolationSpeed); + if(this.isMoving) + { + let gridTile = getNavigationGridTile(this.worldPosition.getX(), this.worldPosition.getY()); + if(gridTile && !gridTile.isObstacle) + { + this._positionInterpolationSpeed = this._basicPositionInterpolationSpeed / gridTile.movementCost; + } + this._positionInterpolation(ticker.deltaMS / 1000 * this._positionInterpolationSpeed); + } }; _positionInterpolation(delta) diff --git a/src/Game/Utils/PathFinding.utils.js b/src/Game/Utils/PathFinding.utils.js index 7c9ba61..7e277ab 100644 --- a/src/Game/Utils/PathFinding.utils.js +++ b/src/Game/Utils/PathFinding.utils.js @@ -45,22 +45,22 @@ export class NavigationPath { export class NavigationResult { /** - * @type Boolean + * @type {Boolean} */ error; /** - * @type String + * @type {String} */ errorText; /** - * @type NavigationPath | undefined + * @type {NavigationPath | undefined} */ result; /** * 0 - point is unreachable * 1 - path found * 2 - error - * @type Int + * @type {Number} */ state; @@ -194,14 +194,14 @@ export class PathFinder { */ _reconstructPath(cameFrom, current) { let totalPath = [current.position]; - console.log(cameFrom); + // console.log(cameFrom); let keys = [...cameFrom.keys()]; while (keys.includes(current.id)) { if (current.id === cameFrom.get(current.id).id) break; current = cameFrom.get(current.id); totalPath.push(current.position); } - return totalPath.reverse(); + return totalPath; } /** diff --git a/src/Game/World/DayNightCycle.js b/src/Game/World/DayNightCycle.js index 7e6ece9..dd1800a 100644 --- a/src/Game/World/DayNightCycle.js +++ b/src/Game/World/DayNightCycle.js @@ -5,7 +5,7 @@ import { RGBColor, RGBCue } from "../Utils/DataTypes.utils"; let timeElapsed = PRNG() * 37992648739; // ex: scaling = 10; 1 game minute = 6 real seconds; -let timeScaling = 300.0; +let timeScaling = 100.0; const gameSecondsInGameMinute = 60; const gameMinutesInGameHour = 60; @@ -14,7 +14,7 @@ const gameDaysInGameWeek = 7; const gameWeeksInGameMonth = 4; const gameMonthsInGameYear = 12; -let dayColor = new RGBColor(255, 255, 255); +// let dayColor = new RGBColor(255, 255, 255); let currentColor = new RGBColor(0, 0, 0); let dayColorsCue = new RGBCue( diff --git a/src/Game/World/NavigationGrid/NavigationGrid.js b/src/Game/World/NavigationGrid/NavigationGrid.js index af32fc0..d33fd79 100644 --- a/src/Game/World/NavigationGrid/NavigationGrid.js +++ b/src/Game/World/NavigationGrid/NavigationGrid.js @@ -131,7 +131,7 @@ async function _findPathAsync(start, goal) export async function findPathOnNavigationGridIfExists(start, goal) { let r0 = await _findPathAsync(goal, start); if(!r0.error){ - r0.result.path.reverse(); + // r0.result.path.reverse(); return r0; } let r1 = await _findPathAsync(start, goal); diff --git a/src/Game/WorldGeneration/WorldGen.js b/src/Game/WorldGeneration/WorldGen.js index 60356f2..364ac33 100644 --- a/src/Game/WorldGeneration/WorldGen.js +++ b/src/Game/WorldGeneration/WorldGen.js @@ -17,10 +17,10 @@ const WorldChunksStorage = new Map(); /* #### REWRITE PART START ####*/ const terrainSpriteList = { - 0: { x: 21, y: 21 }, //water - 1: { x: 2, y: 21 }, //sand + 0: { x: 20, y: 20 }, //water + 1: { x: 2, y: 20 }, //sand 2: { x: 2, y: 2 }, //grass - 3: { x: 21, y: 2 }, //stone + 3: { x: 20, y: 2 }, //stone }; const terrainTypeList = { 0: "ter_water", //water @@ -30,9 +30,9 @@ const terrainTypeList = { }; const terrainNavigationCostList = { 0: 100000000000, //water - 1: 3, //sand - 2: 2, //grass - 3: 1, //stone + 1: 1.7, //sand + 2: 1, //grass + 3: 1.5, //stone }; const grassVegetationSpriteList = { 0: { x: 10, y: 11 },