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:
parent
f5f08a08e2
commit
0cd324c82b
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -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" />
|
||||||
|
@ -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",
|
||||||
|
@ -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 */
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,6 +18,10 @@ export class BeltComponent extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
duplicateWithoutContents() {
|
||||||
|
return new BeltComponent({ direction: this.direction });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {object} param0
|
* @param {object} param0
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -19,6 +19,12 @@ export class MinerComponent extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
duplicateWithoutContents() {
|
||||||
|
return new MinerComponent({
|
||||||
|
chainable: this.chainable,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
constructor({ chainable = false }) {
|
constructor({ chainable = false }) {
|
||||||
|
@ -8,4 +8,8 @@ export class ReplaceableMapEntityComponent extends Component {
|
|||||||
static getId() {
|
static getId() {
|
||||||
return "ReplaceableMapEntity";
|
return "ReplaceableMapEntity";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
duplicateWithoutContents() {
|
||||||
|
return new ReplaceableMapEntityComponent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -8,4 +8,8 @@ export class UnremovableComponent extends Component {
|
|||||||
static getSchema() {
|
static getSchema() {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
duplicateWithoutContents() {
|
||||||
|
return new UnremovableComponent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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]]) {
|
||||||
|
176
src/js/game/hud/parts/blueprint.js
Normal file
176
src/js/game/hud/parts/blueprint.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
103
src/js/game/hud/parts/blueprint_placer.js
Normal file
103
src/js/game/hud/parts/blueprint_placer.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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") },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
||||||
|
@ -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 : "",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -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");
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
52
src/js/savegame/schemas/1001.js
Normal file
52
src/js/savegame/schemas/1001.js
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
src/js/savegame/schemas/1001.json
Normal file
5
src/js/savegame/schemas/1001.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [],
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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 => {
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user