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:
parent
4231231f0e
commit
593773f44b
@ -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,
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user