From 24c1f427479963d04cd060adcf19d3c8e1adbde3 Mon Sep 17 00:00:00 2001 From: akupiec Date: Mon, 25 Jan 2021 17:38:02 +0100 Subject: [PATCH] undo / and simple redo - entity block --- src/js/core/input_distributor.js | 1 + src/js/game/core.js | 2 + src/js/game/history_manager.js | 101 +++++++++++++++++++++++++++++++ src/js/game/key_action_mapper.js | 21 +++++-- src/js/game/logic.js | 2 + src/js/game/root.js | 4 ++ translations/base-en.yaml | 4 ++ 7 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 src/js/game/history_manager.js diff --git a/src/js/core/input_distributor.js b/src/js/core/input_distributor.js index 03ad8e0c..98324b44 100644 --- a/src/js/core/input_distributor.js +++ b/src/js/core/input_distributor.js @@ -210,6 +210,7 @@ export class InputDistributor { this.forwardToReceiver("keydown", { keyCode: keyCode, shift: event.shiftKey, + ctrl: event.ctrlKey, alt: event.altKey, initial: isInitial, event, diff --git a/src/js/game/core.js b/src/js/game/core.js index f4b3e9ee..69f3c1be 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -38,6 +38,7 @@ import { ShapeDefinitionManager } from "./shape_definition_manager"; import { AchievementProxy } from "./achievement_proxy"; import { SoundProxy } from "./sound_proxy"; import { GameTime } from "./time/game_time"; +import { HistoryManager } from "./history_manager"; const logger = createLogger("ingame/core"); @@ -123,6 +124,7 @@ export class GameCore { root.hubGoals = new HubGoals(root); root.productionAnalytics = new ProductionAnalytics(root); root.buffers = new BufferMaintainer(root); + root.historyMgr = new HistoryManager(root); // Initialize the hud once everything is loaded this.root.hud.initialize(); diff --git a/src/js/game/history_manager.js b/src/js/game/history_manager.js new file mode 100644 index 00000000..8eef0768 --- /dev/null +++ b/src/js/game/history_manager.js @@ -0,0 +1,101 @@ +import { Entity } from "./entity"; +import { SOUNDS } from "../platform/sound"; +import { KEYMAPPINGS } from "./key_action_mapper"; + +class LiFoQueue { + constructor(size = 20) { + this.size = size; + this.items = []; + } + + enqueue(element) { + if (this.size < this.items.length + 1) { + this.items.shift(); + } + this.items.push(element); + } + + dequeue() { + return this.items.pop(); + } + + clear() { + this.items = []; + } +} + +const ActionType = { + add: "ADD", + remove: "REMOVE", +}; + +export class HistoryManager { + constructor(root) { + this.root = root; + this._entities = new LiFoQueue(); + this._forRedo = new LiFoQueue(); + + this.initializeBindings(); + } + + initializeBindings() { + this.root.keyMapper.getBinding(KEYMAPPINGS.placement.undo).add(this._undo, this); + this.root.keyMapper.getBinding(KEYMAPPINGS.placement.redo).add(this._redo, this); + } + + /** + * @param {Entity} entity + */ + addAction(entity) { + this._forRedo.clear(); + this._entities.enqueue({ type: ActionType.add, entity }); + } + + removeAction(entity) { + this._forRedo.clear(); + this._entities.enqueue({ type: ActionType.remove, entity }); + } + + _undo() { + const { type, entity } = this._entities.dequeue() || {}; + if (!entity) { + 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); + } + } + + _redo() { + const { type, entity } = this._forRedo.dequeue() || {}; + if (!entity) { + return; + } + if (type === ActionType.remove && this.root.logic.checkCanPlaceEntity(entity)) { + this._placeEntity(entity); + this._entities.enqueue({ type: ActionType.add, entity }); + } + if (type === ActionType.add && this.root.logic.canDeleteBuilding(entity)) { + this._entities.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); + } +} diff --git a/src/js/game/key_action_mapper.js b/src/js/game/key_action_mapper.js index 13f33d66..2b6a41d6 100644 --- a/src/js/game/key_action_mapper.js +++ b/src/js/game/key_action_mapper.js @@ -7,6 +7,7 @@ import { Application } from "../application"; import { Signal, STOP_PROPAGATION } from "../core/signal"; import { IS_MOBILE } from "../core/config"; import { T } from "../translations"; + function key(str) { return str.toUpperCase().charCodeAt(0); } @@ -95,6 +96,9 @@ export const KEYMAPPINGS = { switchDirectionLockSide: { keyCode: key("R") }, copyWireValue: { keyCode: key("Z") }, + + undo: { keyCode: key("Z"), ctrl: true, shift: false }, + redo: { keyCode: key("Z"), ctrl: true, shift: true }, }, massSelect: { @@ -284,14 +288,18 @@ export class Keybinding { * @param {Application} app * @param {object} param0 * @param {number} param0.keyCode + * @param {boolean | undefined} param0.ctrl + * @param {boolean | undefined} param0.shift * @param {boolean=} param0.builtin * @param {boolean=} param0.repeated */ - constructor(keyMapper, app, { keyCode, builtin = false, repeated = false }) { + constructor(keyMapper, app, { keyCode, ctrl, shift, builtin = false, repeated = false }) { assert(keyCode && Number.isInteger(keyCode), "Invalid key code: " + keyCode); this.keyMapper = keyMapper; this.app = app; this.keyCode = keyCode; + this.ctrl = ctrl; + this.shift = shift; this.builtin = builtin; this.repeated = repeated; @@ -440,17 +448,22 @@ export class KeyActionMapper { * @param {number} param0.keyCode * @param {boolean} param0.shift * @param {boolean} param0.alt + * @param {boolean} param0.ctrl * @param {boolean=} param0.initial */ - handleKeydown({ keyCode, shift, alt, initial }) { + handleKeydown({ keyCode, shift, alt, ctrl, initial }) { let stop = false; // Find mapping for (const key in this.keybindings) { /** @type {Keybinding} */ const binding = this.keybindings[key]; - if (binding.keyCode === keyCode && (initial || binding.repeated)) { - /** @type {Signal} */ + let isPressed = + binding.keyCode === keyCode && + (initial || binding.repeated) && + (binding.ctrl === undefined || binding.ctrl === ctrl) && + (binding.shift === undefined || binding.shift === shift); + if (isPressed) { const signal = this.keybindings[key].signal; if (signal.dispatch() === STOP_PROPAGATION) { return; diff --git a/src/js/game/logic.js b/src/js/game/logic.js index 7ec7b8ab..51665ae0 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -110,6 +110,7 @@ export class GameLogic { this.freeEntityAreaBeforeBuild(entity); this.root.map.placeStaticEntity(entity); this.root.entityMgr.registerEntity(entity); + this.root.historyMgr.addAction(entity); return entity; } return null; @@ -178,6 +179,7 @@ export class GameLogic { if (!this.canDeleteBuilding(building)) { return false; } + this.root.historyMgr.removeAction(building.clone()); this.root.map.removeStaticEntity(building); this.root.entityMgr.destroyEntity(building); this.root.entityMgr.processDestroyList(); diff --git a/src/js/game/root.js b/src/js/game/root.js index 82d1e49f..34cd81b5 100644 --- a/src/js/game/root.js +++ b/src/js/game/root.js @@ -29,6 +29,7 @@ import { DynamicTickrate } from "./dynamic_tickrate"; import { KeyActionMapper } from "./key_action_mapper"; import { Vector } from "../core/vector"; import { GameMode } from "./game_mode"; +import { HistoryManager } from "./history_manager"; /* typehints:end */ const logger = createLogger("game/root"); @@ -138,6 +139,9 @@ export class GameRoot { /** @type {GameMode} */ this.gameMode = null; + /** @type {HistoryManager} */ + this.historyMgr = null; + this.signals = { // Entities entityManuallyPlaced: /** @type {TypedSignal<[Entity]>} */ (new Signal()), diff --git a/translations/base-en.yaml b/translations/base-en.yaml index a409f18e..228ec979 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -275,6 +275,8 @@ ingame: copySelection: Copy clearSelection: Clear selection pipette: Pipette + undo: Undo + redo: Redo switchLayers: Switch layers # Names of the colors, used for the color blind mode @@ -1131,6 +1133,8 @@ keybindings: # --- pipette: Pipette + undo: Undo + redo: Redo rotateWhilePlacing: Rotate rotateInverseModifier: >- Modifier: Rotate CCW instead