1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2024-10-27 20:34:29 +00:00

Initial support for blueprints (Buggy)

This commit is contained in:
tobspr 2020-05-27 14:30:59 +02:00
parent f5f08a08e2
commit 0cd324c82b
41 changed files with 633 additions and 77 deletions

View File

@ -8,6 +8,7 @@
"files.exclude": { "files.exclude": {
"**/build": true, "**/build": true,
"**/node_modules": true, "**/node_modules": true,
"**/tmp_standalone_files": true,
"**/typedefs_gen": true "**/typedefs_gen": true
}, },
"vetur.format.defaultFormatter.js": "vscode-typescript", "vetur.format.defaultFormatter.js": "vscode-typescript",

View File

@ -4,7 +4,6 @@
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
$toolbarBg: rgba($accentColorBright, 0.9);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: rgb(255, 255, 255); background-color: rgb(255, 255, 255);
@ -12,8 +11,7 @@
border-bottom-width: 0; border-bottom-width: 0;
transition: transform 0.12s ease-in-out; transition: transform 0.12s ease-in-out;
background: uiResource("toolbar_bg.lossless.png") center center / 100% 100% no-repeat; background: rgba(mix(#ddd, $colorBlueBright, 80%), 0.89);
@include S(padding, 20px, 100px, 0);
&:not(.visible) { &:not(.visible) {
transform: translateX(-50%) translateY(#{D(100px)}); transform: translateX(-50%) translateY(#{D(100px)});
@ -59,7 +57,7 @@
@include S(border-radius, $globalBorderRadius); @include S(border-radius, $globalBorderRadius);
&.selected { &.selected {
background-color: rgba($colorBlueBright, 0.3) !important; background-color: rgba($colorBlueBright, 0.6) !important;
transform: scale(1.05); transform: scale(1.05);
.keybinding { .keybinding {
color: #111; color: #111;

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>shapez.io - Build your own shape factory!</title> <title>shapez.io - Build automated factories to build, combine and color shapes!</title>
<!-- mobile stuff --> <!-- mobile stuff -->
<meta name="format-detection" content="telephone=no" /> <meta name="format-detection" content="telephone=no" />

View File

@ -1,4 +1,12 @@
export const CHANGELOG = [ export const CHANGELOG = [
{
version: "1.1.0",
date: "unreleased",
entries: [
"<strong>UX</strong> Added background to toolbar to increase contrast",
"<strong>UX</strong> Added confirmation when deleting more than 500 buildings at a time",
],
},
{ {
version: "1.0.4", version: "1.0.4",
date: "26.05.2020", date: "26.05.2020",

View File

@ -93,7 +93,7 @@ export const globalConfig = {
// disableZoomLimits: true, // disableZoomLimits: true,
// showChunkBorders: true, // showChunkBorders: true,
// rewardsInstant: true, // rewardsInstant: true,
// allBuildingsUnlocked: true, allBuildingsUnlocked: true,
// upgradesNoCost: true, // upgradesNoCost: true,
// disableUnlockDialog: true, // disableUnlockDialog: true,
// disableLogicTicks: true, // disableLogicTicks: true,
@ -103,6 +103,8 @@ export const globalConfig = {
// enableEntityInspector: true, // enableEntityInspector: true,
// testAds: true, // testAds: true,
// disableMapOverview: true, // disableMapOverview: true,
disableTutorialHints: true,
disableUpgradeNotification: true,
/* dev:end */ /* dev:end */
}, },

View File

@ -103,6 +103,17 @@ export class Vector {
return new Vector(this.x - other.x, this.y - other.y); return new Vector(this.x - other.x, this.y - other.y);
} }
/**
* Subs a vector
* @param {Vector} other
* @returns {Vector}
*/
subInplace(other) {
this.x -= other.x;
this.y -= other.y;
return this;
}
/** /**
* Multiplies with a vector and return a new vector * Multiplies with a vector and return a new vector
* @param {Vector} other * @param {Vector} other

View File

@ -24,6 +24,10 @@ export class MetaHubBuilding extends MetaBuilding {
return false; return false;
} }
getBlueprintSprite() {
return null;
}
/** /**
* Creates the entity at the given location * Creates the entity at the given location
* @param {Entity} entity * @param {Entity} entity

View File

@ -17,6 +17,14 @@ export class Component extends BasicSerializableObject {
return {}; return {};
} }
/**
* Should duplicate the component but without its contents
* @returns {object}
*/
duplicateWithoutContents() {
abstract;
}
/* dev:start */ /* dev:start */
/** /**

View File

@ -18,6 +18,10 @@ export class BeltComponent extends Component {
}; };
} }
duplicateWithoutContents() {
return new BeltComponent({ direction: this.direction });
}
/** /**
* *
* @param {object} param0 * @param {object} param0

View File

@ -54,6 +54,32 @@ export class ItemAcceptorComponent extends Component {
}; };
} }
duplicateWithoutContents() {
const slotsCopy = [];
for (let i = 0; i < this.slots.length; ++i) {
const slot = this.slots[i];
slotsCopy.push({
pos: slot.pos.copy(),
directions: slot.directions.slice(),
});
}
const beltUnderlaysCopy = [];
for (let i = 0; i < this.beltUnderlays.length; ++i) {
const underlay = this.beltUnderlays[i];
beltUnderlaysCopy.push({
pos: underlay.pos.copy(),
direction: underlay.direction,
});
}
return new ItemAcceptorComponent({
slots: slotsCopy,
beltUnderlays: beltUnderlaysCopy,
animated: this.animated,
});
}
/** /**
* *
* @param {object} param0 * @param {object} param0

View File

@ -32,6 +32,22 @@ export class ItemEjectorComponent extends Component {
}; };
} }
duplicateWithoutContents() {
const slotsCopy = [];
for (let i = 0; i < this.slots.length; ++i) {
const slot = this.slots[i];
slotsCopy.push({
pos: slot.pos.copy(),
direction: slot.direction,
});
}
return new ItemEjectorComponent({
slots: slotsCopy,
instantEject: false,
});
}
/** /**
* *
* @param {object} param0 * @param {object} param0

View File

@ -48,6 +48,13 @@ export class ItemProcessorComponent extends Component {
}; };
} }
duplicateWithoutContents() {
return new ItemProcessorComponent({
processorType: this.type,
inputsPerCharge: this.inputsPerCharge,
});
}
/** /**
* *
* @param {object} param0 * @param {object} param0

View File

@ -19,6 +19,12 @@ export class MinerComponent extends Component {
}; };
} }
duplicateWithoutContents() {
return new MinerComponent({
chainable: this.chainable,
});
}
/** /**
*/ */
constructor({ chainable = false }) { constructor({ chainable = false }) {

View File

@ -8,4 +8,8 @@ export class ReplaceableMapEntityComponent extends Component {
static getId() { static getId() {
return "ReplaceableMapEntity"; return "ReplaceableMapEntity";
} }
duplicateWithoutContents() {
return new ReplaceableMapEntityComponent();
}
} }

View File

@ -19,10 +19,23 @@ export class StaticMapEntityComponent extends Component {
rotation: types.float, rotation: types.float,
originalRotation: types.float, originalRotation: types.float,
spriteKey: types.nullable(types.string), spriteKey: types.nullable(types.string),
blueprintSpriteKey: types.string,
silhouetteColor: types.nullable(types.string), silhouetteColor: types.nullable(types.string),
}; };
} }
duplicateWithoutContents() {
return new StaticMapEntityComponent({
origin: this.origin.copy(),
tileSize: this.tileSize.copy(),
rotation: this.rotation,
originalRotation: this.originalRotation,
spriteKey: this.spriteKey,
silhouetteColor: this.silhouetteColor,
blueprintSpriteKey: this.blueprintSpriteKey,
});
}
/** /**
* *
* @param {object} param0 * @param {object} param0
@ -31,6 +44,7 @@ export class StaticMapEntityComponent extends Component {
* @param {number=} param0.rotation Rotation in degrees. Must be multiple of 90 * @param {number=} param0.rotation Rotation in degrees. Must be multiple of 90
* @param {number=} param0.originalRotation Original Rotation in degrees. Must be multiple of 90 * @param {number=} param0.originalRotation Original Rotation in degrees. Must be multiple of 90
* @param {string=} param0.spriteKey Optional sprite * @param {string=} param0.spriteKey Optional sprite
* @param {string} param0.blueprintSpriteKey Blueprint sprite, required
* @param {string=} param0.silhouetteColor Optional silhouette color override * @param {string=} param0.silhouetteColor Optional silhouette color override
*/ */
constructor({ constructor({
@ -40,6 +54,7 @@ export class StaticMapEntityComponent extends Component {
originalRotation = 0, originalRotation = 0,
spriteKey = null, spriteKey = null,
silhouetteColor = null, silhouetteColor = null,
blueprintSpriteKey = null,
}) { }) {
super(); super();
assert( assert(
@ -53,6 +68,7 @@ export class StaticMapEntityComponent extends Component {
this.rotation = rotation; this.rotation = rotation;
this.originalRotation = originalRotation; this.originalRotation = originalRotation;
this.silhouetteColor = silhouetteColor; this.silhouetteColor = silhouetteColor;
this.blueprintSpriteKey = blueprintSpriteKey;
} }
/** /**
@ -202,14 +218,25 @@ export class StaticMapEntityComponent extends Component {
* @param {AtlasSprite} sprite * @param {AtlasSprite} sprite
* @param {number=} extrudePixels How many pixels to extrude the sprite * @param {number=} extrudePixels How many pixels to extrude the sprite
* @param {boolean=} clipping Whether to clip * @param {boolean=} clipping Whether to clip
* @param {Vector=} overridePosition Whether to drwa the entity at a different location
*/ */
drawSpriteOnFullEntityBounds(parameters, sprite, extrudePixels = 0, clipping = true) { drawSpriteOnFullEntityBounds(
const worldX = this.origin.x * globalConfig.tileSize; parameters,
const worldY = this.origin.y * globalConfig.tileSize; sprite,
extrudePixels = 0,
if (!this.shouldBeDrawn(parameters)) { clipping = true,
overridePosition = null
) {
if (!this.shouldBeDrawn(parameters) && !overridePosition) {
return; return;
} }
let worldX = this.origin.x * globalConfig.tileSize;
let worldY = this.origin.y * globalConfig.tileSize;
if (overridePosition) {
worldX = overridePosition.x * globalConfig.tileSize;
worldY = overridePosition.y * globalConfig.tileSize;
}
if (this.rotation === 0) { if (this.rotation === 0) {
// Early out, is faster // Early out, is faster

View File

@ -19,6 +19,10 @@ export class StorageComponent extends Component {
}; };
} }
duplicateWithoutContents() {
return new StorageComponent({ maximumStorage: this.maximumStorage });
}
/** /**
* @param {object} param0 * @param {object} param0
* @param {number=} param0.maximumStorage How much this storage can hold * @param {number=} param0.maximumStorage How much this storage can hold

View File

@ -23,6 +23,13 @@ export class UndergroundBeltComponent extends Component {
}; };
} }
duplicateWithoutContents() {
return new UndergroundBeltComponent({
mode: this.mode,
tier: this.tier,
});
}
/** /**
* *
* @param {object} param0 * @param {object} param0

View File

@ -8,4 +8,8 @@ export class UnremovableComponent extends Component {
static getSchema() { static getSchema() {
return {}; return {};
} }
duplicateWithoutContents() {
return new UnremovableComponent();
}
} }

View File

@ -77,11 +77,14 @@ export class Entity extends BasicSerializableObject {
} }
/** /**
* Returns whether the entity is still alive * Returns a clone of this entity without contents
* @returns {boolean}
*/ */
isAlive() { duplicateWithoutContents() {
return !this.destroyed && !this.queuedForDestroy; const clone = new Entity(this.root);
for (const key in this.components) {
clone.components[key] = this.components[key].duplicateWithoutContents();
}
return clone;
} }
/** /**

View File

@ -97,8 +97,8 @@ export class HubGoals extends BasicSerializableObject {
// Allow quickly switching goals in dev mode with key "C" // Allow quickly switching goals in dev mode with key "C"
if (G_IS_DEV) { if (G_IS_DEV) {
this.root.gameState.inputReciever.keydown.add(key => { this.root.gameState.inputReciever.keydown.add(key => {
if (key.keyCode === 67) { if (key.keyCode === 66) {
// Key: c // Key: b
this.onGoalCompleted(); this.onGoalCompleted();
} }
}); });

View File

@ -8,6 +8,7 @@ import { HUDProcessingOverlay } from "./parts/processing_overlay";
import { HUDBuildingsToolbar } from "./parts/buildings_toolbar"; import { HUDBuildingsToolbar } from "./parts/buildings_toolbar";
import { HUDBuildingPlacer } from "./parts/building_placer"; import { HUDBuildingPlacer } from "./parts/building_placer";
import { HUDBetaOverlay } from "./parts/beta_overlay"; import { HUDBetaOverlay } from "./parts/beta_overlay";
import { HUDBlueprintPlacer } from "./parts/blueprint_placer";
import { HUDKeybindingOverlay } from "./parts/keybinding_overlay"; import { HUDKeybindingOverlay } from "./parts/keybinding_overlay";
import { HUDUnlockNotification } from "./parts/unlock_notification"; import { HUDUnlockNotification } from "./parts/unlock_notification";
import { HUDGameMenu } from "./parts/game_menu"; import { HUDGameMenu } from "./parts/game_menu";
@ -45,6 +46,7 @@ export class GameHUD {
buildingsToolbar: new HUDBuildingsToolbar(this.root), buildingsToolbar: new HUDBuildingsToolbar(this.root),
buildingPlacer: new HUDBuildingPlacer(this.root), buildingPlacer: new HUDBuildingPlacer(this.root),
blueprintPlacer: new HUDBlueprintPlacer(this.root),
unlockNotification: new HUDUnlockNotification(this.root), unlockNotification: new HUDUnlockNotification(this.root),
@ -72,6 +74,7 @@ export class GameHUD {
selectedPlacementBuildingChanged: /** @type {TypedSignal<[MetaBuilding|null]>} */ (new Signal()), selectedPlacementBuildingChanged: /** @type {TypedSignal<[MetaBuilding|null]>} */ (new Signal()),
shapePinRequested: /** @type {TypedSignal<[ShapeDefinition, number]>} */ (new Signal()), shapePinRequested: /** @type {TypedSignal<[ShapeDefinition, number]>} */ (new Signal()),
notification: /** @type {TypedSignal<[string, enumNotificationType]>} */ (new Signal()), notification: /** @type {TypedSignal<[string, enumNotificationType]>} */ (new Signal()),
buildingsSelectedForCopy: /** @type {TypedSignal<[Array<number>]>} */ (new Signal()),
}; };
if (!IS_MOBILE) { if (!IS_MOBILE) {
@ -185,7 +188,7 @@ export class GameHUD {
* @param {DrawParameters} parameters * @param {DrawParameters} parameters
*/ */
draw(parameters) { draw(parameters) {
const partsOrder = ["massSelector", "buildingPlacer"]; const partsOrder = ["massSelector", "buildingPlacer", "blueprintPlacer"];
for (let i = 0; i < partsOrder.length; ++i) { for (let i = 0; i < partsOrder.length; ++i) {
if (this.parts[partsOrder[i]]) { if (this.parts[partsOrder[i]]) {

View File

@ -0,0 +1,176 @@
import { GameRoot } from "../../root";
import { Vector } from "../../../core/vector";
import { Entity } from "../../entity";
import { DrawParameters } from "../../../core/draw_parameters";
import { StaticMapEntityComponent } from "../../components/static_map_entity";
import { createLogger } from "../../../core/logging";
import { Loader } from "../../../core/loader";
const logger = createLogger("blueprint");
export class Blueprint {
/**
* @param {Array<Entity>} entities
*/
constructor(entities) {
this.entities = entities;
}
/**
* @param {GameRoot} root
* @param {Array<number>} uids
*/
static fromUids(root, uids) {
const newEntities = [];
let averagePosition = new Vector();
// First, create a copy
for (let i = 0; i < uids.length; ++i) {
const entity = root.entityMgr.findByUid(uids[i]);
assert(entity, "Entity for blueprint not found:" + uids[i]);
const clone = entity.duplicateWithoutContents();
newEntities.push(clone);
const pos = entity.components.StaticMapEntity.getTileSpaceBounds().getCenter();
averagePosition.addInplace(pos);
}
averagePosition.divideScalarInplace(uids.length);
const blueprintOrigin = averagePosition.floor();
for (let i = 0; i < uids.length; ++i) {
newEntities[i].components.StaticMapEntity.origin.subInplace(blueprintOrigin);
}
// Now, make sure the origin is 0,0
return new Blueprint(newEntities);
}
/**
*
* @param {DrawParameters} parameters
*/
draw(parameters, tile) {
parameters.context.globalAlpha = 0.8;
for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i];
const staticComp = entity.components.StaticMapEntity;
if (!staticComp.blueprintSpriteKey) {
logger.warn("Blueprint entity without sprite!");
return;
}
const newPos = staticComp.origin.add(tile);
const rect = staticComp.getTileSpaceBounds();
rect.moveBy(tile.x, tile.y);
let placeable = true;
placementCheck: for (let x = rect.x; x < rect.right(); ++x) {
for (let y = rect.y; y < rect.bottom(); ++y) {
if (parameters.root.map.isTileUsedXY(x, y)) {
placeable = false;
break placementCheck;
}
}
}
if (!placeable) {
parameters.context.globalAlpha = 0.3;
} else {
parameters.context.globalAlpha = 1;
}
staticComp.drawSpriteOnFullEntityBounds(
parameters,
Loader.getSprite(staticComp.blueprintSpriteKey),
0,
true,
newPos
);
}
parameters.context.globalAlpha = 1;
}
/**
* @param {GameRoot} root
* @param {Vector} tile
*/
canPlace(root, tile) {
let anyPlaceable = false;
for (let i = 0; i < this.entities.length; ++i) {
let placeable = true;
const entity = this.entities[i];
const staticComp = entity.components.StaticMapEntity;
const rect = staticComp.getTileSpaceBounds();
rect.moveBy(tile.x, tile.y);
placementCheck: for (let x = rect.x; x < rect.right(); ++x) {
for (let y = rect.y; y < rect.bottom(); ++y) {
if (root.map.isTileUsedXY(x, y)) {
placeable = false;
break placementCheck;
}
}
}
if (placeable) {
anyPlaceable = true;
}
}
return anyPlaceable;
}
/**
* @param {GameRoot} root
* @param {Vector} tile
*/
tryPlace(root, tile) {
let anyPlaced = false;
for (let i = 0; i < this.entities.length; ++i) {
let placeable = true;
const entity = this.entities[i];
const staticComp = entity.components.StaticMapEntity;
const rect = staticComp.getTileSpaceBounds();
rect.moveBy(tile.x, tile.y);
placementCheck: 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 && !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) {
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;
}
}
}
}
const clone = entity.duplicateWithoutContents();
clone.components.StaticMapEntity.origin.addInplace(tile);
root.map.placeStaticEntity(clone);
root.entityMgr.registerEntity(clone);
anyPlaced = true;
}
}
return anyPlaced;
}
}

View File

@ -0,0 +1,103 @@
import { DrawParameters } from "../../../core/draw_parameters";
import { STOP_PROPAGATION } from "../../../core/signal";
import { TrackedState } from "../../../core/tracked_state";
import { Vector } from "../../../core/vector";
import { enumMouseButton } from "../../camera";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { BaseHUDPart } from "../base_hud_part";
import { Blueprint } from "./blueprint";
export class HUDBlueprintPlacer extends BaseHUDPart {
createElements(parent) {}
initialize() {
this.root.hud.signals.buildingsSelectedForCopy.add(this.onBuildingsSelected, this);
/** @type {TypedTrackedState<Blueprint?>} */
this.currentBlueprint = new TrackedState(this.onBlueprintChanged, this);
const keyActionMapper = this.root.keyMapper;
keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this);
keyActionMapper
.getBinding(KEYMAPPINGS.placement.abortBuildingPlacement)
.add(this.abortPlacement, this);
this.root.camera.downPreHandler.add(this.onMouseDown, this);
this.root.camera.movePreHandler.add(this.onMouseMove, this);
this.root.hud.signals.selectedPlacementBuildingChanged.add(this.abortPlacement, this);
}
abortPlacement() {
if (this.currentBlueprint.get()) {
this.currentBlueprint.set(null);
return STOP_PROPAGATION;
}
}
onBlueprintChanged(blueprint) {}
/**
* mouse down pre handler
* @param {Vector} pos
* @param {enumMouseButton} button
*/
onMouseDown(pos, button) {
if (button === enumMouseButton.right) {
this.abortPlacement();
return STOP_PROPAGATION;
}
const blueprint = this.currentBlueprint.get();
if (!blueprint) {
return;
}
console.log("down");
const worldPos = this.root.camera.screenToWorld(pos);
const tile = worldPos.toTileSpace();
if (blueprint.tryPlace(this.root, tile)) {
if (!this.root.app.inputMgr.shiftIsDown) {
this.currentBlueprint.set(null);
}
}
}
onMouseMove() {
// Prevent movement while blueprint is selected
if (this.currentBlueprint.get()) {
return STOP_PROPAGATION;
}
}
/**
* @param {Array<number>} uids
*/
onBuildingsSelected(uids) {
if (uids.length === 0) {
return;
}
this.currentBlueprint.set(Blueprint.fromUids(this.root, uids));
}
/**
*
* @param {DrawParameters} parameters
*/
draw(parameters) {
const blueprint = this.currentBlueprint.get();
if (!blueprint) {
return;
}
const mousePosition = this.root.app.mousePosition;
if (!mousePosition) {
// Not on screen
return;
}
const worldPos = this.root.camera.screenToWorld(mousePosition);
const tile = worldPos.toTileSpace();
blueprint.draw(parameters, tile);
}
}

View File

@ -39,6 +39,8 @@ export class HUDBuildingPlacer extends BaseHUDPart {
keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.tryRotate, this); keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.tryRotate, this);
keyActionMapper.getBinding(KEYMAPPINGS.placement.cycleBuildingVariants).add(this.cycleVariants, this); keyActionMapper.getBinding(KEYMAPPINGS.placement.cycleBuildingVariants).add(this.cycleVariants, this);
this.root.hud.signals.buildingsSelectedForCopy.add(this.abortPlacement, this);
this.domAttach = new DynamicDomAttach(this.root, this.element, {}); this.domAttach = new DynamicDomAttach(this.root, this.element, {});
this.root.camera.downPreHandler.add(this.onMouseDown, this); this.root.camera.downPreHandler.add(this.onMouseDown, this);
@ -255,6 +257,7 @@ export class HUDBuildingPlacer extends BaseHUDPart {
origin: new Vector(0, 0), origin: new Vector(0, 0),
rotation: 0, rotation: 0,
tileSize: metaBuilding.getDimensions(this.currentVariant.get()).copy(), tileSize: metaBuilding.getDimensions(this.currentVariant.get()).copy(),
blueprintSpriteKey: "",
}) })
); );
metaBuilding.updateVariants(this.fakeEntity, 0, this.currentVariant.get()); metaBuilding.updateVariants(this.fakeEntity, 0, this.currentVariant.get());

View File

@ -5,12 +5,13 @@ import { DrawParameters } from "../../../core/draw_parameters";
import { Entity } from "../../entity"; import { Entity } from "../../entity";
import { Loader } from "../../../core/loader"; import { Loader } from "../../../core/loader";
import { globalConfig } from "../../../core/config"; import { globalConfig } from "../../../core/config";
import { makeDiv } from "../../../core/utils"; import { makeDiv, formatBigNumber, formatBigNumberFull } from "../../../core/utils";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { createLogger } from "../../../core/logging"; import { createLogger } from "../../../core/logging";
import { enumMouseButton } from "../../camera"; import { enumMouseButton } from "../../camera";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { KEYMAPPINGS } from "../../key_action_mapper"; import { KEYMAPPINGS } from "../../key_action_mapper";
import { THEME } from "../../theme";
const logger = createLogger("hud/mass_selector"); const logger = createLogger("hud/mass_selector");
@ -20,13 +21,17 @@ export class HUDMassSelector extends BaseHUDPart {
.getBinding(KEYMAPPINGS.massSelect.confirmMassDelete) .getBinding(KEYMAPPINGS.massSelect.confirmMassDelete)
.getKeyCodeString(); .getKeyCodeString();
const abortKeybinding = this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).getKeyCodeString(); const abortKeybinding = this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).getKeyCodeString();
const copyKeybinding = this.root.keyMapper
.getBinding(KEYMAPPINGS.massSelect.massSelectCopy)
.getKeyCodeString();
this.element = makeDiv( this.element = makeDiv(
parent, parent,
"ingame_HUD_MassSelector", "ingame_HUD_MassSelector",
[], [],
T.ingame.massDelete.infoText T.ingame.massSelect.infoText
.replace("<keyDelete>", removalKeybinding) .replace("<keyDelete>", removalKeybinding)
.replace("<keyCopy>", copyKeybinding)
.replace("<keyCancel>", abortKeybinding) .replace("<keyCancel>", abortKeybinding)
); );
} }
@ -36,7 +41,7 @@ export class HUDMassSelector extends BaseHUDPart {
this.currentSelectionStart = null; this.currentSelectionStart = null;
this.currentSelectionEnd = null; this.currentSelectionEnd = null;
this.entityUidsMarkedForDeletion = new Set(); this.selectedUids = new Set();
this.root.signals.entityQueuedForDestroy.add(this.onEntityDestroyed, this); this.root.signals.entityQueuedForDestroy.add(this.onEntityDestroyed, this);
@ -48,6 +53,7 @@ export class HUDMassSelector extends BaseHUDPart {
this.root.keyMapper this.root.keyMapper
.getBinding(KEYMAPPINGS.massSelect.confirmMassDelete) .getBinding(KEYMAPPINGS.massSelect.confirmMassDelete)
.add(this.confirmDelete, this); .add(this.confirmDelete, this);
this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectCopy).add(this.startCopy, this);
this.domAttach = new DynamicDomAttach(this.root, this.element); this.domAttach = new DynamicDomAttach(this.root, this.element);
} }
@ -57,7 +63,7 @@ export class HUDMassSelector extends BaseHUDPart {
* @param {Entity} entity * @param {Entity} entity
*/ */
onEntityDestroyed(entity) { onEntityDestroyed(entity) {
this.entityUidsMarkedForDeletion.delete(entity.uid); this.selectedUids.delete(entity.uid);
} }
/** /**
@ -65,24 +71,50 @@ export class HUDMassSelector extends BaseHUDPart {
*/ */
onBack() { onBack() {
// Clear entities on escape // Clear entities on escape
if (this.entityUidsMarkedForDeletion.size > 0) { if (this.selectedUids.size > 0) {
this.entityUidsMarkedForDeletion = new Set(); this.selectedUids = new Set();
return STOP_PROPAGATION; return STOP_PROPAGATION;
} }
} }
confirmDelete() { confirmDelete() {
const entityUids = Array.from(this.entityUidsMarkedForDeletion); if (this.selectedUids.size > 500) {
const { ok } = this.root.hud.parts.dialogs.showWarning(
T.dialogs.massDeleteConfirm.title,
T.dialogs.massDeleteConfirm.desc.replace(
"<count>",
"" + formatBigNumberFull(this.selectedUids.size)
),
["cancel:good", "ok:bad"]
);
ok.add(() => this.doDelete());
} else {
this.doDelete();
}
}
doDelete() {
const entityUids = Array.from(this.selectedUids);
for (let i = 0; i < entityUids.length; ++i) { for (let i = 0; i < entityUids.length; ++i) {
const uid = entityUids[i]; const uid = entityUids[i];
const entity = this.root.entityMgr.findByUid(uid); const entity = this.root.entityMgr.findByUid(uid);
if (!this.root.logic.tryDeleteBuilding(entity)) { if (!this.root.logic.tryDeleteBuilding(entity)) {
logger.error("Error in mass delete, could not remove building"); logger.error("Error in mass delete, could not remove building");
this.entityUidsMarkedForDeletion.delete(uid); this.selectedUids.delete(uid);
} }
} }
} }
startCopy() {
if (this.selectedUids.size > 0) {
this.root.hud.signals.buildingsSelectedForCopy.dispatch(Array.from(this.selectedUids));
this.selectedUids = new Set();
this.root.soundProxy.playUiClick();
} else {
this.root.soundProxy.playUiError();
}
}
/** /**
* mouse down pre handler * mouse down pre handler
* @param {Vector} pos * @param {Vector} pos
@ -99,7 +131,7 @@ export class HUDMassSelector extends BaseHUDPart {
if (!this.root.app.inputMgr.shiftIsDown) { if (!this.root.app.inputMgr.shiftIsDown) {
// Start new selection // Start new selection
this.entityUidsMarkedForDeletion = new Set(); this.selectedUids = new Set();
} }
this.currentSelectionStart = pos.copy(); this.currentSelectionStart = pos.copy();
@ -132,7 +164,7 @@ export class HUDMassSelector extends BaseHUDPart {
for (let y = realTileStart.y; y <= realTileEnd.y; ++y) { for (let y = realTileStart.y; y <= realTileEnd.y; ++y) {
const contents = this.root.map.getTileContentXY(x, y); const contents = this.root.map.getTileContentXY(x, y);
if (contents && this.root.logic.canDeleteBuilding(contents)) { if (contents && this.root.logic.canDeleteBuilding(contents)) {
this.entityUidsMarkedForDeletion.add(contents.uid); this.selectedUids.add(contents.uid);
} }
} }
} }
@ -143,7 +175,7 @@ export class HUDMassSelector extends BaseHUDPart {
} }
update() { update() {
this.domAttach.update(this.entityUidsMarkedForDeletion.size > 0); this.domAttach.update(this.selectedUids.size > 0);
} }
/** /**
@ -151,6 +183,8 @@ export class HUDMassSelector extends BaseHUDPart {
* @param {DrawParameters} parameters * @param {DrawParameters} parameters
*/ */
draw(parameters) { draw(parameters) {
const boundsBorder = 2;
if (this.currentSelectionStart) { if (this.currentSelectionStart) {
const worldStart = this.root.camera.screenToWorld(this.currentSelectionStart); const worldStart = this.root.camera.screenToWorld(this.currentSelectionStart);
const worldEnd = this.root.camera.screenToWorld(this.currentSelectionEnd); const worldEnd = this.root.camera.screenToWorld(this.currentSelectionEnd);
@ -165,8 +199,8 @@ export class HUDMassSelector extends BaseHUDPart {
const realTileEnd = tileStart.max(tileEnd); const realTileEnd = tileStart.max(tileEnd);
parameters.context.lineWidth = 1; parameters.context.lineWidth = 1;
parameters.context.fillStyle = "rgba(255, 127, 127, 0.2)"; parameters.context.fillStyle = THEME.map.selectionBackground;
parameters.context.strokeStyle = "rgba(255, 127, 127, 0.5)"; parameters.context.strokeStyle = THEME.map.selectionOutline;
parameters.context.beginPath(); parameters.context.beginPath();
parameters.context.rect( parameters.context.rect(
realWorldStart.x, realWorldStart.x,
@ -177,34 +211,40 @@ export class HUDMassSelector extends BaseHUDPart {
parameters.context.fill(); parameters.context.fill();
parameters.context.stroke(); parameters.context.stroke();
parameters.context.fillStyle = THEME.map.selectionOverlay;
for (let x = realTileStart.x; x <= realTileEnd.x; ++x) { for (let x = realTileStart.x; x <= realTileEnd.x; ++x) {
for (let y = realTileStart.y; y <= realTileEnd.y; ++y) { for (let y = realTileStart.y; y <= realTileEnd.y; ++y) {
const contents = this.root.map.getTileContentXY(x, y); const contents = this.root.map.getTileContentXY(x, y);
if (contents && this.root.logic.canDeleteBuilding(contents)) { if (contents && this.root.logic.canDeleteBuilding(contents)) {
const staticComp = contents.components.StaticMapEntity; const staticComp = contents.components.StaticMapEntity;
const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); const bounds = staticComp.getTileSpaceBounds();
this.deletionMarker.drawCachedCentered( parameters.context.beginRoundedRect(
parameters, bounds.x * globalConfig.tileSize + boundsBorder,
center.x, bounds.y * globalConfig.tileSize + boundsBorder,
center.y, bounds.w * globalConfig.tileSize - 2 * boundsBorder,
globalConfig.tileSize * 0.5 bounds.h * globalConfig.tileSize - 2 * boundsBorder,
2
); );
parameters.context.fill();
} }
} }
} }
} }
this.entityUidsMarkedForDeletion.forEach(uid => { parameters.context.fillStyle = THEME.map.selectionOverlay;
this.selectedUids.forEach(uid => {
const entity = this.root.entityMgr.findByUid(uid); const entity = this.root.entityMgr.findByUid(uid);
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); const bounds = staticComp.getTileSpaceBounds();
parameters.context.beginRoundedRect(
this.deletionMarker.drawCachedCentered( bounds.x * globalConfig.tileSize + boundsBorder,
parameters, bounds.y * globalConfig.tileSize + boundsBorder,
center.x, bounds.w * globalConfig.tileSize - 2 * boundsBorder,
center.y, bounds.h * globalConfig.tileSize - 2 * boundsBorder,
globalConfig.tileSize * 0.5 2
); );
parameters.context.fill();
}); });
} }
} }

View File

@ -6,6 +6,7 @@ import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { globalConfig } from "../../../core/config";
const tutorialVideos = [1, 2, 3, 4, 5, 6, 7, 9, 10, 11]; const tutorialVideos = [1, 2, 3, 4, 5, 6, 7, 9, 10, 11];
@ -56,7 +57,7 @@ export class HUDPartTutorialHints extends BaseHUDPart {
this.currentShownLevel = new TrackedState(this.updateVideoUrl, this); this.currentShownLevel = new TrackedState(this.updateVideoUrl, this);
this.root.signals.postLoadHook.add(() => { this.root.signals.postLoadHook.add(() => {
if (this.root.hubGoals.level === 1) { if (this.root.hubGoals.level === 1 && !(G_IS_DEV && globalConfig.debug.disableTutorialHints)) {
this.root.hud.parts.dialogs.showInfo( this.root.hud.parts.dialogs.showInfo(
T.dialogs.hintDescription.title, T.dialogs.hintDescription.title,
T.dialogs.hintDescription.desc T.dialogs.hintDescription.desc

View File

@ -60,6 +60,7 @@ export const KEYMAPPINGS = {
massSelect: { massSelect: {
massSelectStart: { keyCode: 17, builtin: true }, // CTRL massSelectStart: { keyCode: 17, builtin: true }, // CTRL
massSelectSelectMultiple: { keyCode: 16, builtin: true }, // SHIFT massSelectSelectMultiple: { keyCode: 16, builtin: true }, // SHIFT
massSelectCopy: { keyCode: key("C") },
confirmMassDelete: { keyCode: key("X") }, confirmMassDelete: { keyCode: key("X") },
}, },

View File

@ -60,6 +60,7 @@ export class GameLogic {
origin, origin,
tileSize: building.getDimensions(variant), tileSize: building.getDimensions(variant),
rotation, rotation,
blueprintSpriteKey: "",
}); });
const rect = checker.getTileSpaceBounds(); const rect = checker.getTileSpaceBounds();
@ -168,6 +169,7 @@ export class GameLogic {
origin, origin,
tileSize: building.getDimensions(variant), tileSize: building.getDimensions(variant),
rotation, rotation,
blueprintSpriteKey: "",
}); });
const rect = checker.getTileSpaceBounds(); const rect = checker.getTileSpaceBounds();

View File

@ -147,6 +147,17 @@ export class BaseMap extends BasicSerializableObject {
return chunk && chunk.getTileContentFromWorldCoords(tile.x, tile.y) != null; return chunk && chunk.getTileContentFromWorldCoords(tile.x, tile.y) != null;
} }
/**
* Checks if the tile is used
* @param {number} x
* @param {number} y
* @returns {boolean}
*/
isTileUsedXY(x, y) {
const chunk = this.getChunkAtTileOrNull(x, y);
return chunk && chunk.getTileContentFromWorldCoords(x, y) != null;
}
/** /**
* Sets the tiles content * Sets the tiles content
* @param {Vector} tile * @param {Vector} tile

View File

@ -154,6 +154,9 @@ export class MetaBuilding {
*/ */
createAndPlaceEntity({ root, origin, rotation, originalRotation, rotationVariant, variant }) { createAndPlaceEntity({ root, origin, rotation, originalRotation, rotationVariant, variant }) {
const entity = new Entity(root); const entity = new Entity(root);
const blueprintSprite = this.getBlueprintSprite(rotationVariant, variant);
entity.addComponent( entity.addComponent(
new StaticMapEntityComponent({ new StaticMapEntityComponent({
spriteKey: spriteKey:
@ -166,6 +169,7 @@ export class MetaBuilding {
originalRotation, originalRotation,
tileSize: this.getDimensions(variant).copy(), tileSize: this.getDimensions(variant).copy(),
silhouetteColor: this.getSilhouetteColor(), silhouetteColor: this.getSilhouetteColor(),
blueprintSpriteKey: blueprintSprite ? blueprintSprite.spriteName : "",
}) })
); );

View File

@ -4,7 +4,7 @@
"background": "#2e2f37", "background": "#2e2f37",
"grid": "rgba(255, 255, 255, 0.02)", "grid": "rgba(255, 255, 255, 0.02)",
"gridLineWidth": 0.5, "gridLineWidth": 0.5,
"selectionColor": "rgba(127, 127, 255, 0.5)",
"resources": { "resources": {
"shape": "#3d3f4a", "shape": "#3d3f4a",
"red": "#4a3d3f", "red": "#4a3d3f",

View File

@ -5,6 +5,10 @@
"grid": "#fafafa", "grid": "#fafafa",
"gridLineWidth": 1, "gridLineWidth": 1,
"selectionOverlay": "rgba(74, 163, 223, 0.7)",
"selectionOutline": "rgba(74, 163, 223, 0.5)",
"selectionBackground": "rgba(74, 163, 223, 0.2)",
"resources": { "resources": {
"shape": "#eaebec", "shape": "#eaebec",
"red": "#ffbfc1", "red": "#ffbfc1",

View File

@ -11,8 +11,7 @@ import { createLogger } from "../core/logging";
import { globalConfig } from "../core/config"; import { globalConfig } from "../core/config";
import { SavegameInterface_V1000 } from "./schemas/1000"; import { SavegameInterface_V1000 } from "./schemas/1000";
import { getSavegameInterface } from "./savegame_interface_registry"; import { getSavegameInterface } from "./savegame_interface_registry";
import { compressObject } from "./savegame_compressor"; import { SavegameInterface_V1001 } from "./schemas/1001";
import { compressX64 } from "../core/lzstring";
const logger = createLogger("savegame"); const logger = createLogger("savegame");
@ -29,7 +28,7 @@ export class Savegame extends ReadWriteProxy {
this.internalId = internalId; this.internalId = internalId;
this.metaDataRef = metaDataRef; this.metaDataRef = metaDataRef;
/** @type {SavegameData} */ /** @type {import("./savegame_typedefs").SavegameData} */
this.currentData = this.getDefaultData(); this.currentData = this.getDefaultData();
} }
@ -39,14 +38,14 @@ export class Savegame extends ReadWriteProxy {
* @returns {number} * @returns {number}
*/ */
static getCurrentVersion() { static getCurrentVersion() {
return 1000; return 1001;
} }
/** /**
* @returns {typeof BaseSavegameInterface} * @returns {typeof BaseSavegameInterface}
*/ */
static getReaderClass() { static getReaderClass() {
return SavegameInterface_V1000; return SavegameInterface_V1001;
} }
/** /**
@ -58,7 +57,7 @@ export class Savegame extends ReadWriteProxy {
/** /**
* Returns the savegames default data * Returns the savegames default data
* @returns {SavegameData} * @returns {import("./savegame_typedefs").SavegameData}
*/ */
getDefaultData() { getDefaultData() {
return { return {
@ -73,18 +72,25 @@ export class Savegame extends ReadWriteProxy {
/** /**
* Migrates the savegames data * Migrates the savegames data
* @param {SavegameData} data * @param {import("./savegame_typedefs").SavegameData} data
*/ */
migrate(data) { migrate(data) {
if (data.version < 1000) { if (data.version < 1000) {
return ExplainedResult.bad("Can not migrate savegame, too old"); return ExplainedResult.bad("Can not migrate savegame, too old");
} }
console.log("TODO: Migrate from", data.version);
if (data.version === 1000) {
SavegameInterface_V1001.migrate1000to1001(data);
data.version = 1001;
}
return ExplainedResult.good(); return ExplainedResult.good();
} }
/** /**
* Verifies the savegames data * Verifies the savegames data
* @param {SavegameData} data * @param {import("./savegame_typedefs").SavegameData} data
*/ */
verify(data) { verify(data) {
if (!data.dump) { if (!data.dump) {
@ -109,7 +115,7 @@ export class Savegame extends ReadWriteProxy {
} }
/** /**
* Returns the statistics of the savegame * Returns the statistics of the savegame
* @returns {SavegameStats} * @returns {import("./savegame_typedefs").SavegameStats}
*/ */
getStatistics() { getStatistics() {
return this.currentData.stats; return this.currentData.stats;
@ -132,7 +138,7 @@ export class Savegame extends ReadWriteProxy {
/** /**
* Returns the current game dump * Returns the current game dump
* @returns {SerializedGame} * @returns {import("./savegame_typedefs").SerializedGame}
*/ */
getCurrentDump() { getCurrentDump() {
return this.currentData.dump; return this.currentData.dump;

View File

@ -1,10 +1,12 @@
import { BaseSavegameInterface } from "./savegame_interface"; import { BaseSavegameInterface } from "./savegame_interface";
import { SavegameInterface_V1000 } from "./schemas/1000"; import { SavegameInterface_V1000 } from "./schemas/1000";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { SavegameInterface_V1001 } from "./schemas/1001";
/** @type {Object.<number, typeof BaseSavegameInterface>} */ /** @type {Object.<number, typeof BaseSavegameInterface>} */
const interfaces = { const interfaces = {
1000: SavegameInterface_V1000, 1000: SavegameInterface_V1000,
1001: SavegameInterface_V1001,
}; };
const logger = createLogger("savegame_interface_registry"); const logger = createLogger("savegame_interface_registry");

View File

@ -4,14 +4,7 @@
* }} SavegameStats * }} SavegameStats
*/ */
/** import { Entity } from "../game/entity";
* @typedef {{
* x: number,
* y: number,
* uid: number,
* key: string
* }} SerializedMapResource
*/
/** /**
* @typedef {{ * @typedef {{
@ -20,7 +13,7 @@
* entityMgr: any, * entityMgr: any,
* map: any, * map: any,
* hubGoals: any, * hubGoals: any,
* entities: Array<any> * entities: Array<Entity>
* }} SerializedGame * }} SerializedGame
*/ */

View File

@ -0,0 +1,52 @@
import { SavegameInterface_V1000 } from "./1000.js";
import { createLogger } from "../../core/logging.js";
const schema = require("./1001.json");
const logger = createLogger("savegame_interface/1001");
export class SavegameInterface_V1001 extends SavegameInterface_V1000 {
getVersion() {
return 1001;
}
getSchemaUncached() {
return schema;
}
/**
* @param {import("../savegame_typedefs.js").SavegameData} data
*/
static migrate1000to1001(data) {
logger.log("Migrating 1000 to 1001");
const dump = data.dump;
if (!dump) {
return true;
}
const entities = dump.entities;
for (let i = 0; i < entities.length; ++i) {
const entity = entities[i];
const staticComp = entity.components.StaticMapEntity;
const beltComp = entity.components.Belt;
if (staticComp) {
if (staticComp.spriteKey) {
staticComp.blueprintSpriteKey = staticComp.spriteKey.replace(
"sprites/buildings",
"sprites/blueprints"
);
} else {
if (entity.components.Hub) {
staticComp.blueprintSpriteKey = "";
} else if (beltComp) {
const direction = beltComp.direction;
staticComp.blueprintSpriteKey = "sprites/blueprints/belt_" + direction + ".png";
} else {
assertAlways(false, "Could not deduct entity type for migrating 1000 -> 1001");
}
}
}
}
}
}

View File

@ -0,0 +1,5 @@
{
"type": "object",
"required": [],
"additionalProperties": true
}

View File

@ -43,7 +43,7 @@ export class SerializerInternal {
* @param {Entity} payload * @param {Entity} payload
*/ */
deserializeEntity(root, payload) { deserializeEntity(root, payload) {
const entity = new Entity(null); const entity = new Entity(root);
this.deserializeComponents(entity, payload.components); this.deserializeComponents(entity, payload.components);
root.entityMgr.registerEntity(entity, payload.uid); root.entityMgr.registerEntity(entity, payload.uid);

View File

@ -198,12 +198,12 @@ export class MainMenuState extends GameState {
this.trackClicks(qs(".mainContainer .importButton"), this.requestImportSavegame); this.trackClicks(qs(".mainContainer .importButton"), this.requestImportSavegame);
if (G_IS_DEV && globalConfig.debug.fastGameEnter) { if (G_IS_DEV && globalConfig.debug.fastGameEnter) {
// // const games = this.app.savegameMgr.getSavegamesMetaData(); const games = this.app.savegameMgr.getSavegamesMetaData();
// if (games.length > 0) { if (games.length > 0) {
// this.resumeGame(games[0]); this.resumeGame(games[0]);
// } else { } else {
this.onPlayButtonClicked(); this.onPlayButtonClicked();
// } }
} }
// Initialize video // Initialize video

View File

@ -6,6 +6,7 @@ import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper";
import { T } from "../translations"; import { T } from "../translations";
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { CHANGELOG } from "../changelog"; import { CHANGELOG } from "../changelog";
import { globalConfig } from "../core/config";
const logger = createLogger("state/preload"); const logger = createLogger("state/preload");
@ -179,6 +180,10 @@ export class PreloadState extends GameState {
.then(() => this.setStatus("Checking changelog")) .then(() => this.setStatus("Checking changelog"))
.then(() => { .then(() => {
if (G_IS_DEV && globalConfig.debug.disableUpgradeNotification) {
return;
}
return this.app.storage return this.app.storage
.readFileAsync("lastversion.bin") .readFileAsync("lastversion.bin")
.catch(err => { .catch(err => {

View File

@ -172,6 +172,11 @@ dialogs:
All shapes you produce can be used to unlock upgrades - <strong>Don't destroy your old factories!</strong> All shapes you produce can be used to unlock upgrades - <strong>Don't destroy your old factories!</strong>
The upgrades tab can be found on the top right corner of the screen. The upgrades tab can be found on the top right corner of the screen.
massDeleteConfirm:
title: Confirm delete
desc: >-
You are deleting a lot of buildings (<count> to be exact)! Are you sure you want to do this?
ingame: ingame:
# This is shown in the top left corner and displays useful keybindings in # This is shown in the top left corner and displays useful keybindings in
# every situation # every situation
@ -221,10 +226,10 @@ ingame:
newUpgrade: A new upgrade is available! newUpgrade: A new upgrade is available!
gameSaved: Your game has been saved. gameSaved: Your game has been saved.
# Mass delete information, this is when you hold CTRL and then drag with your mouse # Mass select information, this is when you hold CTRL and then drag with your mouse
# to select multiple buildings to delete # to select multiple buildings
massDelete: massSelect:
infoText: Press <keyDelete> to remove selected buildings and <keyCancel> to cancel. infoText: Press <keyCopy> to copy, <keyDelete> to remove and <keyCancel> to cancel.
# The "Upgrades" window # The "Upgrades" window
shop: shop: