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 { Entity } from "./entity";
|
||||||
import { ACHIEVEMENTS } from "../platform/achievement_provider";
|
import { ACHIEVEMENTS } from "../platform/achievement_provider";
|
||||||
import { GameRoot } from "./root";
|
import { GameRoot } from "./root";
|
||||||
|
import { ActionBuilder } from "./history_manager";
|
||||||
|
|
||||||
export class Blueprint {
|
export class Blueprint {
|
||||||
/**
|
/**
|
||||||
@ -149,6 +150,7 @@ export class Blueprint {
|
|||||||
*/
|
*/
|
||||||
tryPlace(root, tile) {
|
tryPlace(root, tile) {
|
||||||
return root.logic.performBulkOperation(() => {
|
return root.logic.performBulkOperation(() => {
|
||||||
|
const actionBuilder = new ActionBuilder(root);
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (let i = 0; i < this.entities.length; ++i) {
|
for (let i = 0; i < this.entities.length; ++i) {
|
||||||
const entity = this.entities[i];
|
const entity = this.entities[i];
|
||||||
@ -158,11 +160,13 @@ export class Blueprint {
|
|||||||
|
|
||||||
const clone = entity.clone();
|
const clone = entity.clone();
|
||||||
clone.components.StaticMapEntity.origin.addInplace(tile);
|
clone.components.StaticMapEntity.origin.addInplace(tile);
|
||||||
root.logic.freeEntityAreaBeforeBuild(clone);
|
const removed = root.logic.freeEntityAreaBeforeBuild(clone);
|
||||||
root.map.placeStaticEntity(clone);
|
root.map.placeStaticEntity(clone);
|
||||||
root.entityMgr.registerEntity(clone);
|
root.entityMgr.registerEntity(clone);
|
||||||
|
actionBuilder.placeBuilding(clone, removed);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
root.historyMgr.addAction(actionBuilder.build());
|
||||||
|
|
||||||
root.signals.bulkAchievementCheck.dispatch(
|
root.signals.bulkAchievementCheck.dispatch(
|
||||||
ACHIEVEMENTS.placeBlueprint,
|
ACHIEVEMENTS.placeBlueprint,
|
||||||
|
@ -1,6 +1,167 @@
|
|||||||
import { Entity } from "./entity";
|
import { Entity } from "./entity";
|
||||||
import { SOUNDS } from "../platform/sound";
|
import { SOUNDS } from "../platform/sound";
|
||||||
import { KEYMAPPINGS } from "./key_action_mapper";
|
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 {
|
class LiFoQueue {
|
||||||
constructor(size = 20) {
|
constructor(size = 20) {
|
||||||
@ -28,11 +189,6 @@ class LiFoQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActionType = {
|
|
||||||
add: 0,
|
|
||||||
remove: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
export class HistoryManager {
|
export class HistoryManager {
|
||||||
constructor(root) {
|
constructor(root) {
|
||||||
this.root = root;
|
this.root = root;
|
||||||
@ -55,59 +211,34 @@ export class HistoryManager {
|
|||||||
return !this._forRedo.isEmpty();
|
return !this._forRedo.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
addAction(action) {
|
||||||
* @param {Entity} entity
|
|
||||||
*/
|
|
||||||
addAction(entity) {
|
|
||||||
this._forRedo.clear();
|
this._forRedo.clear();
|
||||||
this._forUndo.enqueue({ type: ActionType.add, entity });
|
this._forUndo.enqueue(action);
|
||||||
}
|
|
||||||
|
|
||||||
removeAction(entity) {
|
|
||||||
this._forRedo.clear();
|
|
||||||
this._forUndo.enqueue({ type: ActionType.remove, entity });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_undo() {
|
_undo() {
|
||||||
const { type, entity } = this._forUndo.dequeue() || {};
|
const action = this._forUndo.dequeue();
|
||||||
if (!entity) {
|
if (!action) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (type === ActionType.add && this.root.logic.canDeleteBuilding(entity)) {
|
if (!action.canUndo) {
|
||||||
this._forRedo.enqueue({ type: ActionType.remove, entity: entity.clone() });
|
return;
|
||||||
this._removeEntity(entity);
|
|
||||||
}
|
|
||||||
if (type === ActionType.remove && this.root.logic.checkCanPlaceEntity(entity)) {
|
|
||||||
this._forRedo.enqueue({ type: ActionType.add, entity: entity });
|
|
||||||
this._placeEntity(entity);
|
|
||||||
}
|
}
|
||||||
|
action.undo();
|
||||||
|
action.postUndo();
|
||||||
|
this._forRedo.enqueue(action.redoAction());
|
||||||
}
|
}
|
||||||
|
|
||||||
_redo() {
|
_redo() {
|
||||||
const { type, entity } = this._forRedo.dequeue() || {};
|
const action = this._forRedo.dequeue();
|
||||||
if (!entity) {
|
if (!action) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (type === ActionType.remove && this.root.logic.checkCanPlaceEntity(entity)) {
|
if (!action.canUndo) {
|
||||||
this._placeEntity(entity);
|
return;
|
||||||
this._forUndo.enqueue({ type: ActionType.add, entity });
|
|
||||||
}
|
}
|
||||||
if (type === ActionType.add && this.root.logic.canDeleteBuilding(entity)) {
|
action.undo();
|
||||||
this._forUndo.enqueue({ type: ActionType.remove, entity: entity.clone() });
|
action.postUndo();
|
||||||
this._removeEntity(entity);
|
this._forUndo.enqueue(action.redoAction());
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,34 +94,8 @@ export class HUDMassSelector extends BaseHUDPart {
|
|||||||
|
|
||||||
doDelete() {
|
doDelete() {
|
||||||
const entityUids = Array.from(this.selectedUids);
|
const entityUids = Array.from(this.selectedUids);
|
||||||
|
const count = this.root.logic.tryBulkDelete(entityUids);
|
||||||
// Build mapping from uid to entity
|
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.destroy1000, count);
|
||||||
/**
|
|
||||||
* @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
|
|
||||||
this.selectedUids = new Set();
|
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.
|
// copy code relies on entities still existing, so must copy before deleting.
|
||||||
this.root.hud.signals.buildingsSelectedForCopy.dispatch(entityUids);
|
this.root.hud.signals.buildingsSelectedForCopy.dispatch(entityUids);
|
||||||
|
|
||||||
for (let i = 0; i < entityUids.length; ++i) {
|
this.root.logic.tryBulkDelete(entityUids);
|
||||||
const uid = entityUids[i];
|
this.selectedUids = new Set();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const blueprint = Blueprint.fromUids(this.root, entityUids);
|
const blueprint = Blueprint.fromUids(this.root, entityUids);
|
||||||
@ -195,7 +163,6 @@ export class HUDMassSelector extends BaseHUDPart {
|
|||||||
);
|
);
|
||||||
ok.add(cutAction);
|
ok.add(cutAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.root.soundProxy.playUiClick();
|
this.root.soundProxy.playUiClick();
|
||||||
} else {
|
} else {
|
||||||
this.root.soundProxy.playUiError();
|
this.root.soundProxy.playUiError();
|
||||||
|
@ -10,6 +10,7 @@ import { CHUNK_OVERLAY_RES } from "./map_chunk_view";
|
|||||||
import { MetaBuilding } from "./meta_building";
|
import { MetaBuilding } from "./meta_building";
|
||||||
import { GameRoot } from "./root";
|
import { GameRoot } from "./root";
|
||||||
import { WireNetwork } from "./systems/wire";
|
import { WireNetwork } from "./systems/wire";
|
||||||
|
import { ActionBuilder } from "./history_manager";
|
||||||
|
|
||||||
const logger = createLogger("ingame/logic");
|
const logger = createLogger("ingame/logic");
|
||||||
|
|
||||||
@ -107,10 +108,11 @@ export class GameLogic {
|
|||||||
variant,
|
variant,
|
||||||
});
|
});
|
||||||
if (this.checkCanPlaceEntity(entity)) {
|
if (this.checkCanPlaceEntity(entity)) {
|
||||||
this.freeEntityAreaBeforeBuild(entity);
|
const removed = this.freeEntityAreaBeforeBuild(entity);
|
||||||
this.root.map.placeStaticEntity(entity);
|
this.root.map.placeStaticEntity(entity);
|
||||||
this.root.entityMgr.registerEntity(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 entity;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -120,10 +122,12 @@ export class GameLogic {
|
|||||||
* Removes all entities with a RemovableMapEntityComponent which need to get
|
* Removes all entities with a RemovableMapEntityComponent which need to get
|
||||||
* removed before placing this entity
|
* removed before placing this entity
|
||||||
* @param {Entity} entity
|
* @param {Entity} entity
|
||||||
|
* @return {Entity[]} removedEntities
|
||||||
*/
|
*/
|
||||||
freeEntityAreaBeforeBuild(entity) {
|
freeEntityAreaBeforeBuild(entity) {
|
||||||
const staticComp = entity.components.StaticMapEntity;
|
const staticComp = entity.components.StaticMapEntity;
|
||||||
const rect = staticComp.getTileSpaceBounds();
|
const rect = staticComp.getTileSpaceBounds();
|
||||||
|
const toRemove = [];
|
||||||
// Remove any removeable colliding entities on the same layer
|
// Remove any removeable colliding entities on the same layer
|
||||||
for (let x = rect.x; x < rect.x + rect.w; ++x) {
|
for (let x = rect.x; x < rect.x + rect.w; ++x) {
|
||||||
for (let y = rect.y; y < rect.y + rect.h; ++y) {
|
for (let y = rect.y; y < rect.y + rect.h; ++y) {
|
||||||
@ -133,15 +137,19 @@ export class GameLogic {
|
|||||||
contents.components.StaticMapEntity.getMetaBuilding().getIsReplaceable(),
|
contents.components.StaticMapEntity.getMetaBuilding().getIsReplaceable(),
|
||||||
"Tried to replace non-repleaceable entity"
|
"Tried to replace non-repleaceable entity"
|
||||||
);
|
);
|
||||||
if (!this.tryDeleteBuilding(contents)) {
|
if (!this.canDeleteBuilding(contents)) {
|
||||||
assertAlways(false, "Tried to replace non-repleaceable entity #2");
|
assertAlways(false, "Tried to replace non-repleaceable entity #2");
|
||||||
}
|
}
|
||||||
|
toRemove.push(contents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toRemove.forEach(e => this.unsafeDeleteBuilding(e));
|
||||||
|
|
||||||
// Perform other callbacks
|
// Perform other callbacks
|
||||||
this.root.signals.freeEntityAreaBeforeBuild.dispatch(entity);
|
this.root.signals.freeEntityAreaBeforeBuild.dispatch(entity);
|
||||||
|
return toRemove;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -179,11 +187,38 @@ export class GameLogic {
|
|||||||
if (!this.canDeleteBuilding(building)) {
|
if (!this.canDeleteBuilding(building)) {
|
||||||
return false;
|
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.map.removeStaticEntity(building);
|
||||||
this.root.entityMgr.destroyEntity(building);
|
this.root.entityMgr.destroyEntity(building);
|
||||||
this.root.entityMgr.processDestroyList();
|
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