diff --git a/src/js/game/blueprint.js b/src/js/game/blueprint.js index 3aaef831..ba769b11 100644 --- a/src/js/game/blueprint.js +++ b/src/js/game/blueprint.js @@ -5,6 +5,7 @@ import { Vector } from "../core/vector"; import { Entity } from "./entity"; import { ACHIEVEMENTS } from "../platform/achievement_provider"; import { GameRoot } from "./root"; +import { ActionBuilder } from "./history_manager"; export class Blueprint { /** @@ -149,6 +150,7 @@ export class Blueprint { */ tryPlace(root, tile) { return root.logic.performBulkOperation(() => { + const actionBuilder = new ActionBuilder(root); let count = 0; for (let i = 0; i < this.entities.length; ++i) { const entity = this.entities[i]; @@ -158,11 +160,13 @@ export class Blueprint { const clone = entity.clone(); clone.components.StaticMapEntity.origin.addInplace(tile); - root.logic.freeEntityAreaBeforeBuild(clone); + const removed = root.logic.freeEntityAreaBeforeBuild(clone); root.map.placeStaticEntity(clone); root.entityMgr.registerEntity(clone); + actionBuilder.placeBuilding(clone, removed); count++; } + root.historyMgr.addAction(actionBuilder.build()); root.signals.bulkAchievementCheck.dispatch( ACHIEVEMENTS.placeBlueprint, diff --git a/src/js/game/history_manager.js b/src/js/game/history_manager.js index 37424d3f..32a3e8af 100644 --- a/src/js/game/history_manager.js +++ b/src/js/game/history_manager.js @@ -1,6 +1,167 @@ import { Entity } from "./entity"; import { SOUNDS } from "../platform/sound"; import { KEYMAPPINGS } from "./key_action_mapper"; +import { createLogger } from "../core/logging"; +const logger = createLogger("ingame/history"); + +class UserAction { + constructor(size = 1) { + this.size = size; + } + + get canUndo() { + assert(false, "NOT IMPLEMENTED !!!"); + return false; + } + + undo() { + assert(false, "NOT IMPLEMENTED !!!"); + } + + postUndo() { + assert(false, "NOT IMPLEMENTED !!!"); + } + + /** + * @return {UserAction} + */ + redoAction() { + assert(false, "NOT IMPLEMENTED !!!"); + return this; + } +} + +class ActionBuildOne extends UserAction { + constructor(root, entity) { + super(); + this.root = root; + this.entity = entity; + } + + undo() { + this.root.map.removeStaticEntity(this.entity); + this.root.entityMgr.destroyEntity(this.entity); + } + + postUndo() { + this.root.entityMgr.processDestroyList(); + this.root.soundProxy.playUi(SOUNDS.destroyBuilding); + } + + redoAction() { + return new ActionRemoveOne(this.root, this.entity); + } + + get canUndo() { + return this.root.logic.canDeleteBuilding(this.entity); + } +} + +class ActionRemoveOne extends UserAction { + constructor(root, entity) { + super(); + this.root = root; + this.entity = entity; + } + + undo() { + const entity = this.entity; + entity.destroyed = false; + entity.queuedForDestroy = false; + this.root.logic.freeEntityAreaBeforeBuild(entity); + this.root.map.placeStaticEntity(entity); + this.root.entityMgr.registerEntity(entity); + } + + postUndo() { + this.root.soundProxy.playUi(SOUNDS.placeBuilding); + } + + get canUndo() { + return this.root.logic.checkCanPlaceEntity(this.entity); + } + + redoAction() { + return new ActionBuildOne(this.root, this.entity); + } +} + +class ComplexAction extends UserAction { + constructor(root, actionArray) { + super(); + this.root = root; + this.actionArray = [...actionArray].reverse(); + } + + get canUndo() { + return this.actionArray.every(a => a.canUndo); + } + + undo() { + this.actionArray.forEach(a => a.undo()); + } + + postUndo() { + const haveRemove = this.actionArray.some(e => e instanceof ActionRemoveOne); + const haveBuild = this.actionArray.some(e => e instanceof ActionBuildOne); + + if (haveRemove) { + this.root.soundProxy.playUi(SOUNDS.placeBuilding); + } + + if (haveBuild) { + this.root.entityMgr.processDestroyList(); + this.root.soundProxy.playUi(SOUNDS.destroyBuilding); + } + } + + redoAction() { + const redoActions = this.actionArray.map(a => a.redoAction()); + return new ComplexAction(this.root, redoActions); + } +} + +export class ActionBuilder { + constructor(root) { + this.root = root; + this.actionArray = []; + } + + /** + * @param {Entity} entity - newly build entity + * @param {Entity[]} removed - entities in the way + */ + placeBuilding(entity, removed = []) { + removed.forEach(e => this.removeBuilding(e)); + this.actionArray.push(new ActionBuildOne(this.root, entity)); + return this; + } + + /** + * @param {Entity} entity + */ + removeBuilding(entity) { + this.actionArray.push(new ActionRemoveOne(this.root, entity)); + return this; + } + + /** + * @param {Entity[]} entities + */ + bulkRemoveBuildings(entities) { + entities.forEach(e => this.actionArray.push(new ActionRemoveOne(this.root, e))); + return this; + } + + build() { + if (this.actionArray.length > 1) { + return new ComplexAction(this.root, this.actionArray); + } + if (this.actionArray.length === 1) { + return this.actionArray.pop(); + } + } +} class LiFoQueue { constructor(size = 20) { @@ -28,11 +189,6 @@ class LiFoQueue { } } -const ActionType = { - add: 0, - remove: 1, -}; - export class HistoryManager { constructor(root) { this.root = root; @@ -55,59 +211,34 @@ export class HistoryManager { return !this._forRedo.isEmpty(); } - /** - * @param {Entity} entity - */ - addAction(entity) { + addAction(action) { this._forRedo.clear(); - this._forUndo.enqueue({ type: ActionType.add, entity }); - } - - removeAction(entity) { - this._forRedo.clear(); - this._forUndo.enqueue({ type: ActionType.remove, entity }); + this._forUndo.enqueue(action); } _undo() { - const { type, entity } = this._forUndo.dequeue() || {}; - if (!entity) { + const action = this._forUndo.dequeue(); + if (!action) { return; } - if (type === ActionType.add && this.root.logic.canDeleteBuilding(entity)) { - this._forRedo.enqueue({ type: ActionType.remove, entity: entity.clone() }); - this._removeEntity(entity); - } - if (type === ActionType.remove && this.root.logic.checkCanPlaceEntity(entity)) { - this._forRedo.enqueue({ type: ActionType.add, entity: entity }); - this._placeEntity(entity); + if (!action.canUndo) { + return; } + action.undo(); + action.postUndo(); + this._forRedo.enqueue(action.redoAction()); } _redo() { - const { type, entity } = this._forRedo.dequeue() || {}; - if (!entity) { + const action = this._forRedo.dequeue(); + if (!action) { return; } - if (type === ActionType.remove && this.root.logic.checkCanPlaceEntity(entity)) { - this._placeEntity(entity); - this._forUndo.enqueue({ type: ActionType.add, entity }); + if (!action.canUndo) { + return; } - if (type === ActionType.add && this.root.logic.canDeleteBuilding(entity)) { - this._forUndo.enqueue({ type: ActionType.remove, entity: entity.clone() }); - this._removeEntity(entity); - } - } - - _removeEntity(entity) { - this.root.map.removeStaticEntity(entity); - this.root.entityMgr.destroyEntity(entity); - this.root.entityMgr.processDestroyList(); - this.root.soundProxy.playUi(SOUNDS.destroyBuilding); - } - - _placeEntity(entity) { - this.root.logic.freeEntityAreaBeforeBuild(entity); - this.root.map.placeStaticEntity(entity); - this.root.entityMgr.registerEntity(entity); + action.undo(); + action.postUndo(); + this._forUndo.enqueue(action.redoAction()); } } diff --git a/src/js/game/hud/parts/mass_selector.js b/src/js/game/hud/parts/mass_selector.js index d73e3be3..ebe6fb00 100644 --- a/src/js/game/hud/parts/mass_selector.js +++ b/src/js/game/hud/parts/mass_selector.js @@ -94,34 +94,8 @@ export class HUDMassSelector extends BaseHUDPart { doDelete() { const entityUids = Array.from(this.selectedUids); - - // Build mapping from uid to entity - /** - * @type {Map} - */ - const mapUidToEntity = this.root.entityMgr.getFrozenUidSearchMap(); - - let count = 0; - this.root.logic.performBulkOperation(() => { - for (let i = 0; i < entityUids.length; ++i) { - const uid = entityUids[i]; - const entity = mapUidToEntity.get(uid); - if (!entity) { - logger.error("Entity not found by uid:", uid); - continue; - } - - if (!this.root.logic.tryDeleteBuilding(entity)) { - logger.error("Error in mass delete, could not remove building"); - } else { - count++; - } - } - - this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.destroy1000, count); - }); - - // Clear uids later + const count = this.root.logic.tryBulkDelete(entityUids); + this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.destroy1000, count); this.selectedUids = new Set(); } @@ -174,14 +148,8 @@ export class HUDMassSelector extends BaseHUDPart { // copy code relies on entities still existing, so must copy before deleting. this.root.hud.signals.buildingsSelectedForCopy.dispatch(entityUids); - for (let i = 0; i < entityUids.length; ++i) { - const uid = entityUids[i]; - const entity = this.root.entityMgr.findByUid(uid); - if (!this.root.logic.tryDeleteBuilding(entity)) { - logger.error("Error in mass cut, could not remove building"); - this.selectedUids.delete(uid); - } - } + this.root.logic.tryBulkDelete(entityUids); + this.selectedUids = new Set(); }; const blueprint = Blueprint.fromUids(this.root, entityUids); @@ -195,7 +163,6 @@ export class HUDMassSelector extends BaseHUDPart { ); ok.add(cutAction); } - this.root.soundProxy.playUiClick(); } else { this.root.soundProxy.playUiError(); diff --git a/src/js/game/logic.js b/src/js/game/logic.js index 51665ae0..8ad83163 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -10,6 +10,7 @@ import { CHUNK_OVERLAY_RES } from "./map_chunk_view"; import { MetaBuilding } from "./meta_building"; import { GameRoot } from "./root"; import { WireNetwork } from "./systems/wire"; +import { ActionBuilder } from "./history_manager"; const logger = createLogger("ingame/logic"); @@ -107,10 +108,11 @@ export class GameLogic { variant, }); if (this.checkCanPlaceEntity(entity)) { - this.freeEntityAreaBeforeBuild(entity); + const removed = this.freeEntityAreaBeforeBuild(entity); this.root.map.placeStaticEntity(entity); this.root.entityMgr.registerEntity(entity); - this.root.historyMgr.addAction(entity); + const action = new ActionBuilder(this.root).placeBuilding(entity, removed).build(); + this.root.historyMgr.addAction(action); return entity; } return null; @@ -120,10 +122,12 @@ export class GameLogic { * Removes all entities with a RemovableMapEntityComponent which need to get * removed before placing this entity * @param {Entity} entity + * @return {Entity[]} removedEntities */ freeEntityAreaBeforeBuild(entity) { const staticComp = entity.components.StaticMapEntity; const rect = staticComp.getTileSpaceBounds(); + const toRemove = []; // Remove any removeable colliding entities on the same layer for (let x = rect.x; x < rect.x + rect.w; ++x) { for (let y = rect.y; y < rect.y + rect.h; ++y) { @@ -133,15 +137,19 @@ export class GameLogic { contents.components.StaticMapEntity.getMetaBuilding().getIsReplaceable(), "Tried to replace non-repleaceable entity" ); - if (!this.tryDeleteBuilding(contents)) { + if (!this.canDeleteBuilding(contents)) { assertAlways(false, "Tried to replace non-repleaceable entity #2"); } + toRemove.push(contents); } } } + toRemove.forEach(e => this.unsafeDeleteBuilding(e)); + // Perform other callbacks this.root.signals.freeEntityAreaBeforeBuild.dispatch(entity); + return toRemove; } /** @@ -179,11 +187,38 @@ export class GameLogic { if (!this.canDeleteBuilding(building)) { return false; } - this.root.historyMgr.removeAction(building.clone()); + const action = new ActionBuilder(this.root).removeBuilding(building.clone()).build(); + this.root.historyMgr.addAction(action); + this.unsafeDeleteBuilding(building); + return true; + } + + unsafeDeleteBuilding(building) { this.root.map.removeStaticEntity(building); this.root.entityMgr.destroyEntity(building); this.root.entityMgr.processDestroyList(); - return true; + } + + /** + * @param {number[]} entityUids + * @return {number} number of removed entities + */ + tryBulkDelete(entityUids) { + return this.performBulkOperation(() => { + const mapUidToEntity = this.root.entityMgr.getFrozenUidSearchMap(); + const entitiesToDelete = entityUids + .map(uid => mapUidToEntity.get(uid)) + .filter(entity => !!entity) + .filter(entity => this.canDeleteBuilding(entity)); + const action = new ActionBuilder(this.root).bulkRemoveBuildings(entitiesToDelete).build(); + this.root.historyMgr.addAction(action); + entitiesToDelete.forEach(entity => { + this.root.map.removeStaticEntity(entity); + this.root.entityMgr.destroyEntity(entity); + }); + this.root.entityMgr.processDestroyList(); + return entitiesToDelete.length; + }); } /**