1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-06-13 13:04:03 +00:00

undo / redo - bulk operations

This commit is contained in:
akupiec 2021-05-17 21:51:14 +02:00
parent 4231231f0e
commit 593773f44b
4 changed files with 226 additions and 89 deletions

View File

@ -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,

View File

@ -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());
}
}

View File

@ -94,34 +94,8 @@ export class HUDMassSelector extends BaseHUDPart {
doDelete() {
const entityUids = Array.from(this.selectedUids);
// Build mapping from uid to entity
/**
* @type {Map<number, Entity>}
*/
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();

View File

@ -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;
});
}
/**