Further blueprint improvements

This commit is contained in:
tobspr 2020-05-27 15:03:36 +02:00
parent 19c770201f
commit dfe1e64b27
17 changed files with 159 additions and 152 deletions

View File

@ -20,8 +20,7 @@
@include SuperSmallText; @include SuperSmallText;
@include S(padding-left, 20px); @include S(padding-left, 20px);
strong { strong {
background: $colorBlueBright; color: #aaa;
color: #fff;
text-transform: uppercase; text-transform: uppercase;
@include S(padding, 1px, 2px); @include S(padding, 1px, 2px);
@include S(margin-right, 3px); @include S(margin-right, 3px);

View File

@ -3,24 +3,27 @@ export const CHANGELOG = [
version: "1.1.0", version: "1.1.0",
date: "unreleased", date: "unreleased",
entries: [ entries: [
"<strong>UX</strong> Added background to toolbar to increase contrast", "Allow changing all keybindings, including CTRL, ALT and SHIFT",
"<strong>UX</strong> Added confirmation when deleting more than 500 buildings at a time", "Allow holding SHIFT to rotate counter clockwise",
"Added confirmation when deleting more than 500 buildings at a time",
"Added background to toolbar to increase contrast",
"Allow placing extractors anywhere again, but they don't work at all if not placed on a resource",
], ],
}, },
{ {
version: "1.0.4", version: "1.0.4",
date: "26.05.2020", date: "26.05.2020",
entries: [ entries: [
"<strong>Balancing</strong> Reduce cost of first painting upgrade, and change 'Shape Processing' to 'Cutting, Rotating & Stacking'", "Reduce cost of first painting upgrade, and change 'Shape Processing' to 'Cutting, Rotating & Stacking'",
"<strong>Tutorial</strong> Add dialog after completing level 2 to check out the upgrades tab.", "Add dialog after completing level 2 to check out the upgrades tab.",
"<strong>Misc</strong> Allow changing the keybindings in the demo version", "Allow changing the keybindings in the demo version",
], ],
}, },
{ {
version: "1.0.3", version: "1.0.3",
date: "24.05.2020", date: "24.05.2020",
entries: [ entries: [
"<strong>Balancing</strong> Reduced the amount of shapes required for the first 5 levels to make it easier to get into the game.", "Reduced the amount of shapes required for the first 5 levels to make it easier to get into the game.",
], ],
}, },
{ {

View File

@ -40,7 +40,7 @@ export const globalConfig = {
// Map // Map
mapChunkSize: 16, mapChunkSize: 16,
mapChunkPrerenderMinZoom: 1.3, mapChunkPrerenderMinZoom: 1.15,
mapChunkOverviewMinZoom: 0.7, mapChunkOverviewMinZoom: 0.7,
// Belt speeds // Belt speeds

View File

@ -23,10 +23,6 @@ export class InputDistributor {
/** @type {Array<function(any) : boolean>} */ /** @type {Array<function(any) : boolean>} */
this.filters = []; this.filters = [];
this.shiftIsDown = false;
this.altIsDown = false;
this.ctrlIsDown = false;
this.bindToEvents(); this.bindToEvents();
} }
@ -176,27 +172,13 @@ export class InputDistributor {
* Handles when the page got blurred * Handles when the page got blurred
*/ */
handleBlur() { handleBlur() {
this.ctrlIsDown = false;
this.shiftIsDown = false;
this.altIsDown = false;
this.forwardToReceiver("pageBlur", {}); this.forwardToReceiver("pageBlur", {});
this.forwardToReceiver("shiftUp", {});
} }
/** /**
* @param {KeyboardEvent} event * @param {KeyboardEvent} event
*/ */
handleKeydown(event) { handleKeydown(event) {
if (event.keyCode === 16) {
this.shiftIsDown = true;
}
if (event.keyCode === 17) {
this.ctrlIsDown = true;
}
if (event.keyCode === 18) {
this.altIsDown = true;
}
if ( if (
// TAB // TAB
event.keyCode === 9 || event.keyCode === 9 ||
@ -230,19 +212,6 @@ export class InputDistributor {
* @param {KeyboardEvent} event * @param {KeyboardEvent} event
*/ */
handleKeyup(event) { handleKeyup(event) {
if (event.keyCode === 16) {
this.shiftIsDown = false;
this.forwardToReceiver("shiftUp", {});
}
if (event.keyCode === 17) {
this.ctrlIsDown = false;
this.forwardToReceiver("ctrlUp", {});
}
if (event.keyCode === 18) {
this.altIsDown = false;
this.forwardToReceiver("altUp", {});
}
this.forwardToReceiver("keyup", { this.forwardToReceiver("keyup", {
keyCode: event.keyCode, keyCode: event.keyCode,
shift: event.shiftKey, shift: event.shiftKey,

View File

@ -9,9 +9,6 @@ export class InputReceiver {
this.keydown = new Signal(); this.keydown = new Signal();
this.keyup = new Signal(); this.keyup = new Signal();
this.pageBlur = new Signal(); this.pageBlur = new Signal();
this.shiftUp = new Signal();
this.altUp = new Signal();
this.ctrlUp = new Signal();
// Dispatched on destroy // Dispatched on destroy
this.destroyed = new Signal(); this.destroyed = new Signal();

View File

@ -41,23 +41,6 @@ export class MetaMinerBuilding extends MetaBuilding {
return super.getAvailableVariants(root); return super.getAvailableVariants(root);
} }
/**
* @param {GameRoot} root
* @param {object} param0
* @param {Vector} param0.origin
* @param {number} param0.rotation
* @param {number} param0.rotationVariant
* @param {string} param0.variant
*/
performAdditionalPlacementChecks(root, { origin, rotation, rotationVariant, variant }) {
// Make sure its placed above a resource
const lowerLayer = root.map.getLowerLayerContentXY(origin.x, origin.y);
if (!lowerLayer) {
return false;
}
return true;
}
/** /**
* Creates the entity at the given location * Creates the entity at the given location
* @param {Entity} entity * @param {Entity} entity

View File

@ -34,6 +34,7 @@ export class GameSystemWithFilter extends GameSystem {
this.root.signals.entityQueuedForDestroy.add(this.internalPopEntityIfMatching, this); this.root.signals.entityQueuedForDestroy.add(this.internalPopEntityIfMatching, this);
this.root.signals.postLoadHook.add(this.internalPostLoadHook, this); this.root.signals.postLoadHook.add(this.internalPostLoadHook, this);
this.root.signals.bulkOperationFinished.add(this.refreshCaches, this);
} }
/** /**
@ -175,7 +176,7 @@ export class GameSystemWithFilter extends GameSystem {
internalRegisterEntity(entity) { internalRegisterEntity(entity) {
this.allEntities.push(entity); this.allEntities.push(entity);
if (this.root.gameInitialized) { if (this.root.gameInitialized && !this.root.bulkOperationRunning) {
// Sort entities by uid so behaviour is predictable // Sort entities by uid so behaviour is predictable
this.allEntities.sort((a, b) => a.uid - b.uid); this.allEntities.sort((a, b) => a.uid - b.uid);
} }

View File

@ -1,10 +1,9 @@
import { GameRoot } from "../../root"; import { DrawParameters } from "../../../core/draw_parameters";
import { Loader } from "../../../core/loader";
import { createLogger } from "../../../core/logging";
import { Vector } from "../../../core/vector"; import { Vector } from "../../../core/vector";
import { Entity } from "../../entity"; import { Entity } from "../../entity";
import { DrawParameters } from "../../../core/draw_parameters"; import { GameRoot } from "../../root";
import { StaticMapEntityComponent } from "../../components/static_map_entity";
import { createLogger } from "../../../core/logging";
import { Loader } from "../../../core/loader";
const logger = createLogger("blueprint"); const logger = createLogger("blueprint");
@ -17,6 +16,7 @@ export class Blueprint {
} }
/** /**
* Creates a new blueprint from the given entity uids
* @param {GameRoot} root * @param {GameRoot} root
* @param {Array<number>} uids * @param {Array<number>} uids
*/ */
@ -48,7 +48,7 @@ export class Blueprint {
} }
/** /**
* * Draws the blueprint at the given origin
* @param {DrawParameters} parameters * @param {DrawParameters} parameters
*/ */
draw(parameters, tile) { draw(parameters, tile) {
@ -93,6 +93,31 @@ export class Blueprint {
} }
/** /**
* Rotates the blueprint clockwise
*/
rotateCw() {
for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i];
const staticComp = entity.components.StaticMapEntity;
staticComp.rotation = (staticComp.rotation + 90) % 360;
staticComp.originalRotation = (staticComp.originalRotation + 90) % 360;
staticComp.origin = staticComp.origin.rotateFastMultipleOf90(90);
}
}
/**
* Rotates the blueprint counter clock wise
*/
rotateCcw() {
// Well ...
for (let i = 0; i < 3; ++i) {
this.rotateCw();
}
}
/**
* Checks if the blueprint can be placed at the given tile
* @param {GameRoot} root * @param {GameRoot} root
* @param {Vector} tile * @param {Vector} tile
*/ */
@ -123,54 +148,57 @@ export class Blueprint {
} }
/** /**
* Attempts to place the blueprint at the given tile
* @param {GameRoot} root * @param {GameRoot} root
* @param {Vector} tile * @param {Vector} tile
*/ */
tryPlace(root, tile) { tryPlace(root, tile) {
let anyPlaced = false; return root.logic.performBulkOperation(() => {
for (let i = 0; i < this.entities.length; ++i) { let anyPlaced = false;
let placeable = true; for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i]; let placeable = true;
const staticComp = entity.components.StaticMapEntity; const entity = this.entities[i];
const rect = staticComp.getTileSpaceBounds(); const staticComp = entity.components.StaticMapEntity;
rect.moveBy(tile.x, tile.y); const rect = staticComp.getTileSpaceBounds();
placementCheck: for (let x = rect.x; x < rect.right(); ++x) { rect.moveBy(tile.x, tile.y);
for (let y = rect.y; y < rect.bottom(); ++y) { placementCheck: for (let x = rect.x; x < rect.right(); ++x) {
const contents = root.map.getTileContentXY(x, y);
if (contents && !contents.components.ReplaceableMapEntity) {
placeable = false;
break placementCheck;
}
}
}
if (placeable) {
for (let x = rect.x; x < rect.right(); ++x) {
for (let y = rect.y; y < rect.bottom(); ++y) { for (let y = rect.y; y < rect.bottom(); ++y) {
const contents = root.map.getTileContentXY(x, y); const contents = root.map.getTileContentXY(x, y);
if (contents) { if (contents && !contents.components.ReplaceableMapEntity) {
assert( placeable = false;
contents.components.ReplaceableMapEntity, break placementCheck;
"Can not delete entity for blueprint"
);
if (!root.logic.tryDeleteBuilding(contents)) {
logger.error(
"Building has replaceable component but is also unremovable in blueprint"
);
return false;
}
} }
} }
} }
const clone = entity.duplicateWithoutContents(); if (placeable) {
clone.components.StaticMapEntity.origin.addInplace(tile); for (let x = rect.x; x < rect.right(); ++x) {
for (let y = rect.y; y < rect.bottom(); ++y) {
const contents = root.map.getTileContentXY(x, y);
if (contents) {
assert(
contents.components.ReplaceableMapEntity,
"Can not delete entity for blueprint"
);
if (!root.logic.tryDeleteBuilding(contents)) {
logger.error(
"Building has replaceable component but is also unremovable in blueprint"
);
return false;
}
}
}
}
root.map.placeStaticEntity(clone); const clone = entity.duplicateWithoutContents();
root.entityMgr.registerEntity(clone); clone.components.StaticMapEntity.origin.addInplace(tile);
anyPlaced = true;
root.map.placeStaticEntity(clone);
root.entityMgr.registerEntity(clone);
anyPlaced = true;
}
} }
} return anyPlaced;
return anyPlaced; });
} }
} }

View File

@ -21,6 +21,7 @@ export class HUDBlueprintPlacer extends BaseHUDPart {
keyActionMapper keyActionMapper
.getBinding(KEYMAPPINGS.placement.abortBuildingPlacement) .getBinding(KEYMAPPINGS.placement.abortBuildingPlacement)
.add(this.abortPlacement, this); .add(this.abortPlacement, this);
keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.rotateBlueprint, this);
this.root.camera.downPreHandler.add(this.onMouseDown, this); this.root.camera.downPreHandler.add(this.onMouseDown, this);
this.root.camera.movePreHandler.add(this.onMouseMove, this); this.root.camera.movePreHandler.add(this.onMouseMove, this);
@ -54,13 +55,13 @@ export class HUDBlueprintPlacer extends BaseHUDPart {
return; return;
} }
console.log("down");
const worldPos = this.root.camera.screenToWorld(pos); const worldPos = this.root.camera.screenToWorld(pos);
const tile = worldPos.toTileSpace(); const tile = worldPos.toTileSpace();
if (blueprint.tryPlace(this.root, tile)) { if (blueprint.tryPlace(this.root, tile)) {
if (!this.root.app.inputMgr.shiftIsDown) { // This actually feels weird
this.currentBlueprint.set(null); // if (!this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeMultiple).currentlyDown) {
} // this.currentBlueprint.set(null);
// }
} }
} }
@ -81,6 +82,16 @@ export class HUDBlueprintPlacer extends BaseHUDPart {
this.currentBlueprint.set(Blueprint.fromUids(this.root, uids)); this.currentBlueprint.set(Blueprint.fromUids(this.root, uids));
} }
rotateBlueprint() {
if (this.currentBlueprint.get()) {
if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).currentlyDown) {
this.currentBlueprint.get().rotateCcw();
} else {
this.currentBlueprint.get().rotateCw();
}
}
}
/** /**
* *
* @param {DrawParameters} parameters * @param {DrawParameters} parameters

View File

@ -161,14 +161,19 @@ export class HUDBuildingPlacer extends BaseHUDPart {
if ( if (
metaBuilding && metaBuilding &&
metaBuilding.getRotateAutomaticallyWhilePlacing(this.currentVariant.get()) && metaBuilding.getRotateAutomaticallyWhilePlacing(this.currentVariant.get()) &&
!this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placementDisableAutoOrientation).currentlyDown !this.root.keyMapper.getBinding(
KEYMAPPINGS.placementModifiers.placementDisableAutoOrientation
).currentlyDown
) { ) {
const delta = newPos.sub(oldPos); const delta = newPos.sub(oldPos);
const angleDeg = Math_degrees(delta.angle()); const angleDeg = Math_degrees(delta.angle());
this.currentBaseRotation = (Math.round(angleDeg / 90) * 90 + 360) % 360; this.currentBaseRotation = (Math.round(angleDeg / 90) * 90 + 360) % 360;
// Holding alt inverts the placement // Holding alt inverts the placement
if (this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeInverse).currentlyDown) { if (
this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeInverse)
.currentlyDown
) {
this.currentBaseRotation = (180 + this.currentBaseRotation) % 360; this.currentBaseRotation = (180 + this.currentBaseRotation) % 360;
} }
} }
@ -389,7 +394,12 @@ export class HUDBuildingPlacer extends BaseHUDPart {
tryRotate() { tryRotate() {
const selectedBuilding = this.currentMetaBuilding.get(); const selectedBuilding = this.currentMetaBuilding.get();
if (selectedBuilding) { if (selectedBuilding) {
this.currentBaseRotation = (this.currentBaseRotation + 90) % 360; if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).currentlyDown) {
this.currentBaseRotation = (this.currentBaseRotation + 270) % 360;
} else {
this.currentBaseRotation = (this.currentBaseRotation + 90) % 360;
}
const staticComp = this.fakeEntity.components.StaticMapEntity; const staticComp = this.fakeEntity.components.StaticMapEntity;
staticComp.rotation = this.currentBaseRotation; staticComp.rotation = this.currentBaseRotation;
} }
@ -469,7 +479,9 @@ export class HUDBuildingPlacer extends BaseHUDPart {
if ( if (
metaBuilding.getFlipOrientationAfterPlacement() && metaBuilding.getFlipOrientationAfterPlacement() &&
!this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placementDisableAutoOrientation).currentlyDown !this.root.keyMapper.getBinding(
KEYMAPPINGS.placementModifiers.placementDisableAutoOrientation
).currentlyDown
) { ) {
this.currentBaseRotation = (180 + this.currentBaseRotation) % 360; this.currentBaseRotation = (180 + this.currentBaseRotation) % 360;
} }

View File

@ -1,24 +1,16 @@
import { BaseHUDPart } from "../base_hud_part";
import { makeDiv } from "../../../core/utils"; import { makeDiv } from "../../../core/utils";
import { getStringForKeyCode, KEYMAPPINGS } from "../../key_action_mapper";
import { TrackedState } from "../../../core/tracked_state";
import { queryParamOptions } from "../../../core/query_parameters";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { getStringForKeyCode, KEYMAPPINGS } from "../../key_action_mapper";
import { BaseHUDPart } from "../base_hud_part";
export class HUDKeybindingOverlay extends BaseHUDPart { export class HUDKeybindingOverlay extends BaseHUDPart {
initialize() { initialize() {
this.shiftDownTracker = new TrackedState(this.onShiftStateChanged, this);
this.root.hud.signals.selectedPlacementBuildingChanged.add( this.root.hud.signals.selectedPlacementBuildingChanged.add(
this.onSelectedBuildingForPlacementChanged, this.onSelectedBuildingForPlacementChanged,
this this
); );
} }
onShiftStateChanged(shiftDown) {
this.element.classList.toggle("shiftDown", shiftDown);
}
createElements(parent) { createElements(parent) {
const mapper = this.root.keyMapper; const mapper = this.root.keyMapper;
@ -70,7 +62,9 @@ export class HUDKeybindingOverlay extends BaseHUDPart {
</div> </div>
<div class="binding placementOnly"> <div class="binding placementOnly">
<code class="keybinding builtinKey shift"> ${T.global.keys.shift}</code> <code class="keybinding builtinKey shift"> ${getKeycode(
KEYMAPPINGS.placementModifiers.placeMultiple
)}</code>
<label>${T.ingame.keybindingsOverlay.placeMultiple}</label> <label>${T.ingame.keybindingsOverlay.placeMultiple}</label>
</div> </div>
` `
@ -81,7 +75,5 @@ export class HUDKeybindingOverlay extends BaseHUDPart {
this.element.classList.toggle("placementActive", !!selectedMetaBuilding); this.element.classList.toggle("placementActive", !!selectedMetaBuilding);
} }
update() { update() {}
this.shiftDownTracker.set(this.root.app.inputMgr.shiftIsDown);
}
} }

View File

@ -53,6 +53,7 @@ export const KEYMAPPINGS = {
placement: { placement: {
abortBuildingPlacement: { keyCode: key("Q") }, abortBuildingPlacement: { keyCode: key("Q") },
rotateWhilePlacing: { keyCode: key("R") }, rotateWhilePlacing: { keyCode: key("R") },
rotateInverseModifier: { keyCode: 16 }, // SHIFT
cycleBuildingVariants: { keyCode: key("T") }, cycleBuildingVariants: { keyCode: key("T") },
cycleBuildings: { keyCode: 9 }, // TAB cycleBuildings: { keyCode: 9 }, // TAB
}, },

View File

@ -3,10 +3,11 @@ import { Entity } from "./entity";
import { Vector, enumDirectionToVector, enumDirection } from "../core/vector"; import { Vector, enumDirectionToVector, enumDirection } from "../core/vector";
import { MetaBuilding } from "./meta_building"; import { MetaBuilding } from "./meta_building";
import { StaticMapEntityComponent } from "./components/static_map_entity"; import { StaticMapEntityComponent } from "./components/static_map_entity";
import { Math_abs } from "../core/builtins"; import { Math_abs, performanceNow } from "../core/builtins";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { MetaBeltBaseBuilding, arrayBeltVariantToRotation } from "./buildings/belt_base"; import { MetaBeltBaseBuilding, arrayBeltVariantToRotation } from "./buildings/belt_base";
import { SOUNDS } from "../platform/sound"; import { SOUNDS } from "../platform/sound";
import { round2Digits } from "../core/utils";
const logger = createLogger("ingame/logic"); const logger = createLogger("ingame/logic");
@ -132,17 +133,6 @@ export class GameLogic {
return false; return false;
} }
if (
!building.performAdditionalPlacementChecks(this.root, {
origin,
rotation,
rotationVariant,
variant,
})
) {
return false;
}
return this.isAreaFreeToBuild({ return this.isAreaFreeToBuild({
origin, origin,
rotation, rotation,
@ -202,6 +192,24 @@ export class GameLogic {
return false; return false;
} }
/**
* Performs a bulk operation, not updating caches in the meantime
* @param {function} operation
*/
performBulkOperation(operation) {
logger.log("Running bulk operation ...");
assert(!this.root.bulkOperationRunning, "Can not run two bulk operations twice");
this.root.bulkOperationRunning = true;
const now = performanceNow();
const returnValue = operation();
const duration = performanceNow() - now;
logger.log("Done in", round2Digits(duration), "ms");
assert(this.root.bulkOperationRunning, "Bulk operation = false while bulk operation was running");
this.root.bulkOperationRunning = false;
this.root.signals.bulkOperationFinished.dispatch();
return returnValue;
}
/** /**
* Returns whether the given building can get removed * Returns whether the given building can get removed
* @param {Entity} building * @param {Entity} building

View File

@ -129,19 +129,6 @@ export class MetaBuilding {
return null; return null;
} }
/**
* Should perform additional placement checks
* @param {GameRoot} root
* @param {object} param0
* @param {Vector} param0.origin
* @param {number} param0.rotation
* @param {number} param0.rotationVariant
* @param {string} param0.variant
*/
performAdditionalPlacementChecks(root, { origin, rotation, rotationVariant, variant }) {
return true;
}
/** /**
* Creates the entity at the given location * Creates the entity at the given location
* @param {object} param0 * @param {object} param0

View File

@ -70,6 +70,11 @@ export class GameRoot {
/** @type {boolean} */ /** @type {boolean} */
this.gameInitialized = false; this.gameInitialized = false;
/**
* Whether a bulk operation is running
*/
this.bulkOperationRunning = false;
//////// Other properties /////// //////// Other properties ///////
/** @type {Camera} */ /** @type {Camera} */
@ -151,6 +156,8 @@ export class GameRoot {
shapeDelivered: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()), shapeDelivered: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()),
itemProduced: /** @type {TypedSignal<[BaseItem]>} */ (new Signal()), itemProduced: /** @type {TypedSignal<[BaseItem]>} */ (new Signal()),
bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()),
}; };
// RNG's // RNG's

View File

@ -17,9 +17,16 @@ export class MinerSystem extends GameSystemWithFilter {
for (let i = 0; i < this.allEntities.length; ++i) { for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i]; const entity = this.allEntities[i];
// Check if miner is above an actual tile
const minerComp = entity.components.Miner; const minerComp = entity.components.Miner;
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
const tileBelow = this.root.map.getLowerLayerContentXY(staticComp.origin.x, staticComp.origin.y);
if (!tileBelow) {
continue;
}
// First, try to get rid of chained items // First, try to get rid of chained items
if (minerComp.itemChainBuffer.length > 0) { if (minerComp.itemChainBuffer.length > 0) {
if (this.tryPerformMinerEject(entity, minerComp.itemChainBuffer[0])) { if (this.tryPerformMinerEject(entity, minerComp.itemChainBuffer[0])) {

View File

@ -557,6 +557,8 @@ keybindings:
abortBuildingPlacement: Abort Placement abortBuildingPlacement: Abort Placement
rotateWhilePlacing: Rotate rotateWhilePlacing: Rotate
rotateInverseModifier: >-
Modifier: Rotate CCW instead
cycleBuildingVariants: Cycle Variants cycleBuildingVariants: Cycle Variants
confirmMassDelete: Confirm Mass Delete confirmMassDelete: Confirm Mass Delete
cycleBuildings: Cycle Buildings cycleBuildings: Cycle Buildings