diff --git a/shapez.code-workspace b/shapez.code-workspace index e0766264..29dae2a2 100644 --- a/shapez.code-workspace +++ b/shapez.code-workspace @@ -8,6 +8,7 @@ "files.exclude": { "**/build": true, "**/node_modules": true, + "**/tmp_standalone_files": true, "**/typedefs_gen": true }, "vetur.format.defaultFormatter.js": "vscode-typescript", diff --git a/src/css/ingame_hud/buildings_toolbar.scss b/src/css/ingame_hud/buildings_toolbar.scss index d9f91fcc..a16acfb3 100644 --- a/src/css/ingame_hud/buildings_toolbar.scss +++ b/src/css/ingame_hud/buildings_toolbar.scss @@ -4,7 +4,6 @@ left: 50%; transform: translateX(-50%); - $toolbarBg: rgba($accentColorBright, 0.9); display: flex; flex-direction: column; background-color: rgb(255, 255, 255); @@ -12,8 +11,7 @@ border-bottom-width: 0; transition: transform 0.12s ease-in-out; - background: uiResource("toolbar_bg.lossless.png") center center / 100% 100% no-repeat; - @include S(padding, 20px, 100px, 0); + background: rgba(mix(#ddd, $colorBlueBright, 80%), 0.89); &:not(.visible) { transform: translateX(-50%) translateY(#{D(100px)}); @@ -59,7 +57,7 @@ @include S(border-radius, $globalBorderRadius); &.selected { - background-color: rgba($colorBlueBright, 0.3) !important; + background-color: rgba($colorBlueBright, 0.6) !important; transform: scale(1.05); .keybinding { color: #111; diff --git a/src/html/index.html b/src/html/index.html index ffcab1d7..b1d89377 100644 --- a/src/html/index.html +++ b/src/html/index.html @@ -1,7 +1,7 @@ - shapez.io - Build your own shape factory! + shapez.io - Build automated factories to build, combine and color shapes! diff --git a/src/js/changelog.js b/src/js/changelog.js index 44475e63..10c78d3a 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -1,4 +1,12 @@ export const CHANGELOG = [ + { + version: "1.1.0", + date: "unreleased", + entries: [ + "UX Added background to toolbar to increase contrast", + "UX Added confirmation when deleting more than 500 buildings at a time", + ], + }, { version: "1.0.4", date: "26.05.2020", diff --git a/src/js/core/config.js b/src/js/core/config.js index 99770c4b..f5bdbf76 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -93,7 +93,7 @@ export const globalConfig = { // disableZoomLimits: true, // showChunkBorders: true, // rewardsInstant: true, - // allBuildingsUnlocked: true, + allBuildingsUnlocked: true, // upgradesNoCost: true, // disableUnlockDialog: true, // disableLogicTicks: true, @@ -103,6 +103,8 @@ export const globalConfig = { // enableEntityInspector: true, // testAds: true, // disableMapOverview: true, + disableTutorialHints: true, + disableUpgradeNotification: true, /* dev:end */ }, diff --git a/src/js/core/vector.js b/src/js/core/vector.js index 2bd6cfe9..2a02f75d 100644 --- a/src/js/core/vector.js +++ b/src/js/core/vector.js @@ -103,6 +103,17 @@ export class Vector { 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 * @param {Vector} other diff --git a/src/js/game/buildings/hub.js b/src/js/game/buildings/hub.js index b7b960de..49d95005 100644 --- a/src/js/game/buildings/hub.js +++ b/src/js/game/buildings/hub.js @@ -24,6 +24,10 @@ export class MetaHubBuilding extends MetaBuilding { return false; } + getBlueprintSprite() { + return null; + } + /** * Creates the entity at the given location * @param {Entity} entity diff --git a/src/js/game/component.js b/src/js/game/component.js index 8c492351..1d44d60f 100644 --- a/src/js/game/component.js +++ b/src/js/game/component.js @@ -17,6 +17,14 @@ export class Component extends BasicSerializableObject { return {}; } + /** + * Should duplicate the component but without its contents + * @returns {object} + */ + duplicateWithoutContents() { + abstract; + } + /* dev:start */ /** diff --git a/src/js/game/components/belt.js b/src/js/game/components/belt.js index 9d64187c..dcac6ecb 100644 --- a/src/js/game/components/belt.js +++ b/src/js/game/components/belt.js @@ -18,6 +18,10 @@ export class BeltComponent extends Component { }; } + duplicateWithoutContents() { + return new BeltComponent({ direction: this.direction }); + } + /** * * @param {object} param0 diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index d5546d4b..d9505d18 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -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 diff --git a/src/js/game/components/item_ejector.js b/src/js/game/components/item_ejector.js index 5cf96754..d5881a7d 100644 --- a/src/js/game/components/item_ejector.js +++ b/src/js/game/components/item_ejector.js @@ -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 diff --git a/src/js/game/components/item_processor.js b/src/js/game/components/item_processor.js index 0c4e90c6..eab51ae2 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -48,6 +48,13 @@ export class ItemProcessorComponent extends Component { }; } + duplicateWithoutContents() { + return new ItemProcessorComponent({ + processorType: this.type, + inputsPerCharge: this.inputsPerCharge, + }); + } + /** * * @param {object} param0 diff --git a/src/js/game/components/miner.js b/src/js/game/components/miner.js index e08d2906..57de7e2f 100644 --- a/src/js/game/components/miner.js +++ b/src/js/game/components/miner.js @@ -19,6 +19,12 @@ export class MinerComponent extends Component { }; } + duplicateWithoutContents() { + return new MinerComponent({ + chainable: this.chainable, + }); + } + /** */ constructor({ chainable = false }) { diff --git a/src/js/game/components/replaceable_map_entity.js b/src/js/game/components/replaceable_map_entity.js index e6fd95d5..78861caf 100644 --- a/src/js/game/components/replaceable_map_entity.js +++ b/src/js/game/components/replaceable_map_entity.js @@ -8,4 +8,8 @@ export class ReplaceableMapEntityComponent extends Component { static getId() { return "ReplaceableMapEntity"; } + + duplicateWithoutContents() { + return new ReplaceableMapEntityComponent(); + } } diff --git a/src/js/game/components/static_map_entity.js b/src/js/game/components/static_map_entity.js index 6f9abb87..ed616213 100644 --- a/src/js/game/components/static_map_entity.js +++ b/src/js/game/components/static_map_entity.js @@ -19,10 +19,23 @@ export class StaticMapEntityComponent extends Component { rotation: types.float, originalRotation: types.float, spriteKey: types.nullable(types.string), + blueprintSpriteKey: 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 @@ -31,6 +44,7 @@ export class StaticMapEntityComponent extends Component { * @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 {string=} param0.spriteKey Optional sprite + * @param {string} param0.blueprintSpriteKey Blueprint sprite, required * @param {string=} param0.silhouetteColor Optional silhouette color override */ constructor({ @@ -40,6 +54,7 @@ export class StaticMapEntityComponent extends Component { originalRotation = 0, spriteKey = null, silhouetteColor = null, + blueprintSpriteKey = null, }) { super(); assert( @@ -53,6 +68,7 @@ export class StaticMapEntityComponent extends Component { this.rotation = rotation; this.originalRotation = originalRotation; this.silhouetteColor = silhouetteColor; + this.blueprintSpriteKey = blueprintSpriteKey; } /** @@ -202,14 +218,25 @@ export class StaticMapEntityComponent extends Component { * @param {AtlasSprite} sprite * @param {number=} extrudePixels How many pixels to extrude the sprite * @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) { - const worldX = this.origin.x * globalConfig.tileSize; - const worldY = this.origin.y * globalConfig.tileSize; - - if (!this.shouldBeDrawn(parameters)) { + drawSpriteOnFullEntityBounds( + parameters, + sprite, + extrudePixels = 0, + clipping = true, + overridePosition = null + ) { + if (!this.shouldBeDrawn(parameters) && !overridePosition) { 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) { // Early out, is faster diff --git a/src/js/game/components/storage.js b/src/js/game/components/storage.js index e024d522..69f4e367 100644 --- a/src/js/game/components/storage.js +++ b/src/js/game/components/storage.js @@ -19,6 +19,10 @@ export class StorageComponent extends Component { }; } + duplicateWithoutContents() { + return new StorageComponent({ maximumStorage: this.maximumStorage }); + } + /** * @param {object} param0 * @param {number=} param0.maximumStorage How much this storage can hold diff --git a/src/js/game/components/underground_belt.js b/src/js/game/components/underground_belt.js index e581ebe9..4fcbbb48 100644 --- a/src/js/game/components/underground_belt.js +++ b/src/js/game/components/underground_belt.js @@ -23,6 +23,13 @@ export class UndergroundBeltComponent extends Component { }; } + duplicateWithoutContents() { + return new UndergroundBeltComponent({ + mode: this.mode, + tier: this.tier, + }); + } + /** * * @param {object} param0 diff --git a/src/js/game/components/unremovable.js b/src/js/game/components/unremovable.js index 17e9f36b..f3864cf8 100644 --- a/src/js/game/components/unremovable.js +++ b/src/js/game/components/unremovable.js @@ -8,4 +8,8 @@ export class UnremovableComponent extends Component { static getSchema() { return {}; } + + duplicateWithoutContents() { + return new UnremovableComponent(); + } } diff --git a/src/js/game/entity.js b/src/js/game/entity.js index 50f4cae5..dc849851 100644 --- a/src/js/game/entity.js +++ b/src/js/game/entity.js @@ -77,11 +77,14 @@ export class Entity extends BasicSerializableObject { } /** - * Returns whether the entity is still alive - * @returns {boolean} + * Returns a clone of this entity without contents */ - isAlive() { - return !this.destroyed && !this.queuedForDestroy; + duplicateWithoutContents() { + const clone = new Entity(this.root); + for (const key in this.components) { + clone.components[key] = this.components[key].duplicateWithoutContents(); + } + return clone; } /** diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index f1fc15c9..a6f24284 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -97,8 +97,8 @@ export class HubGoals extends BasicSerializableObject { // Allow quickly switching goals in dev mode with key "C" if (G_IS_DEV) { this.root.gameState.inputReciever.keydown.add(key => { - if (key.keyCode === 67) { - // Key: c + if (key.keyCode === 66) { + // Key: b this.onGoalCompleted(); } }); diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index 2d317b7f..2294b4f1 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -8,6 +8,7 @@ import { HUDProcessingOverlay } from "./parts/processing_overlay"; import { HUDBuildingsToolbar } from "./parts/buildings_toolbar"; import { HUDBuildingPlacer } from "./parts/building_placer"; import { HUDBetaOverlay } from "./parts/beta_overlay"; +import { HUDBlueprintPlacer } from "./parts/blueprint_placer"; import { HUDKeybindingOverlay } from "./parts/keybinding_overlay"; import { HUDUnlockNotification } from "./parts/unlock_notification"; import { HUDGameMenu } from "./parts/game_menu"; @@ -45,6 +46,7 @@ export class GameHUD { buildingsToolbar: new HUDBuildingsToolbar(this.root), buildingPlacer: new HUDBuildingPlacer(this.root), + blueprintPlacer: new HUDBlueprintPlacer(this.root), unlockNotification: new HUDUnlockNotification(this.root), @@ -72,6 +74,7 @@ export class GameHUD { selectedPlacementBuildingChanged: /** @type {TypedSignal<[MetaBuilding|null]>} */ (new Signal()), shapePinRequested: /** @type {TypedSignal<[ShapeDefinition, number]>} */ (new Signal()), notification: /** @type {TypedSignal<[string, enumNotificationType]>} */ (new Signal()), + buildingsSelectedForCopy: /** @type {TypedSignal<[Array]>} */ (new Signal()), }; if (!IS_MOBILE) { @@ -185,7 +188,7 @@ export class GameHUD { * @param {DrawParameters} parameters */ draw(parameters) { - const partsOrder = ["massSelector", "buildingPlacer"]; + const partsOrder = ["massSelector", "buildingPlacer", "blueprintPlacer"]; for (let i = 0; i < partsOrder.length; ++i) { if (this.parts[partsOrder[i]]) { diff --git a/src/js/game/hud/parts/blueprint.js b/src/js/game/hud/parts/blueprint.js new file mode 100644 index 00000000..f6dccc4c --- /dev/null +++ b/src/js/game/hud/parts/blueprint.js @@ -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} entities + */ + constructor(entities) { + this.entities = entities; + } + + /** + * @param {GameRoot} root + * @param {Array} 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; + } +} diff --git a/src/js/game/hud/parts/blueprint_placer.js b/src/js/game/hud/parts/blueprint_placer.js new file mode 100644 index 00000000..32993ffc --- /dev/null +++ b/src/js/game/hud/parts/blueprint_placer.js @@ -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} */ + 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} 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); + } +} diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index fa477dbd..39fae8c9 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -39,6 +39,8 @@ export class HUDBuildingPlacer extends BaseHUDPart { keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.tryRotate, 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.root.camera.downPreHandler.add(this.onMouseDown, this); @@ -255,6 +257,7 @@ export class HUDBuildingPlacer extends BaseHUDPart { origin: new Vector(0, 0), rotation: 0, tileSize: metaBuilding.getDimensions(this.currentVariant.get()).copy(), + blueprintSpriteKey: "", }) ); metaBuilding.updateVariants(this.fakeEntity, 0, this.currentVariant.get()); diff --git a/src/js/game/hud/parts/mass_selector.js b/src/js/game/hud/parts/mass_selector.js index ddcf9117..3c251d66 100644 --- a/src/js/game/hud/parts/mass_selector.js +++ b/src/js/game/hud/parts/mass_selector.js @@ -5,12 +5,13 @@ import { DrawParameters } from "../../../core/draw_parameters"; import { Entity } from "../../entity"; import { Loader } from "../../../core/loader"; import { globalConfig } from "../../../core/config"; -import { makeDiv } from "../../../core/utils"; +import { makeDiv, formatBigNumber, formatBigNumberFull } from "../../../core/utils"; import { DynamicDomAttach } from "../dynamic_dom_attach"; import { createLogger } from "../../../core/logging"; import { enumMouseButton } from "../../camera"; import { T } from "../../../translations"; import { KEYMAPPINGS } from "../../key_action_mapper"; +import { THEME } from "../../theme"; const logger = createLogger("hud/mass_selector"); @@ -20,13 +21,17 @@ export class HUDMassSelector extends BaseHUDPart { .getBinding(KEYMAPPINGS.massSelect.confirmMassDelete) .getKeyCodeString(); const abortKeybinding = this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).getKeyCodeString(); + const copyKeybinding = this.root.keyMapper + .getBinding(KEYMAPPINGS.massSelect.massSelectCopy) + .getKeyCodeString(); this.element = makeDiv( parent, "ingame_HUD_MassSelector", [], - T.ingame.massDelete.infoText + T.ingame.massSelect.infoText .replace("", removalKeybinding) + .replace("", copyKeybinding) .replace("", abortKeybinding) ); } @@ -36,7 +41,7 @@ export class HUDMassSelector extends BaseHUDPart { this.currentSelectionStart = null; this.currentSelectionEnd = null; - this.entityUidsMarkedForDeletion = new Set(); + this.selectedUids = new Set(); this.root.signals.entityQueuedForDestroy.add(this.onEntityDestroyed, this); @@ -48,6 +53,7 @@ export class HUDMassSelector extends BaseHUDPart { this.root.keyMapper .getBinding(KEYMAPPINGS.massSelect.confirmMassDelete) .add(this.confirmDelete, this); + this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectCopy).add(this.startCopy, this); this.domAttach = new DynamicDomAttach(this.root, this.element); } @@ -57,7 +63,7 @@ export class HUDMassSelector extends BaseHUDPart { * @param {Entity} entity */ onEntityDestroyed(entity) { - this.entityUidsMarkedForDeletion.delete(entity.uid); + this.selectedUids.delete(entity.uid); } /** @@ -65,24 +71,50 @@ export class HUDMassSelector extends BaseHUDPart { */ onBack() { // Clear entities on escape - if (this.entityUidsMarkedForDeletion.size > 0) { - this.entityUidsMarkedForDeletion = new Set(); + if (this.selectedUids.size > 0) { + this.selectedUids = new Set(); return STOP_PROPAGATION; } } 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( + "", + "" + 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) { const uid = entityUids[i]; const entity = this.root.entityMgr.findByUid(uid); if (!this.root.logic.tryDeleteBuilding(entity)) { 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 * @param {Vector} pos @@ -99,7 +131,7 @@ export class HUDMassSelector extends BaseHUDPart { if (!this.root.app.inputMgr.shiftIsDown) { // Start new selection - this.entityUidsMarkedForDeletion = new Set(); + this.selectedUids = new Set(); } this.currentSelectionStart = pos.copy(); @@ -132,7 +164,7 @@ export class HUDMassSelector extends BaseHUDPart { for (let y = realTileStart.y; y <= realTileEnd.y; ++y) { const contents = this.root.map.getTileContentXY(x, y); 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() { - 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 */ draw(parameters) { + const boundsBorder = 2; + if (this.currentSelectionStart) { const worldStart = this.root.camera.screenToWorld(this.currentSelectionStart); const worldEnd = this.root.camera.screenToWorld(this.currentSelectionEnd); @@ -165,8 +199,8 @@ export class HUDMassSelector extends BaseHUDPart { const realTileEnd = tileStart.max(tileEnd); parameters.context.lineWidth = 1; - parameters.context.fillStyle = "rgba(255, 127, 127, 0.2)"; - parameters.context.strokeStyle = "rgba(255, 127, 127, 0.5)"; + parameters.context.fillStyle = THEME.map.selectionBackground; + parameters.context.strokeStyle = THEME.map.selectionOutline; parameters.context.beginPath(); parameters.context.rect( realWorldStart.x, @@ -177,34 +211,40 @@ export class HUDMassSelector extends BaseHUDPart { parameters.context.fill(); parameters.context.stroke(); + parameters.context.fillStyle = THEME.map.selectionOverlay; + for (let x = realTileStart.x; x <= realTileEnd.x; ++x) { for (let y = realTileStart.y; y <= realTileEnd.y; ++y) { const contents = this.root.map.getTileContentXY(x, y); if (contents && this.root.logic.canDeleteBuilding(contents)) { const staticComp = contents.components.StaticMapEntity; - const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); - this.deletionMarker.drawCachedCentered( - parameters, - center.x, - center.y, - globalConfig.tileSize * 0.5 + const bounds = staticComp.getTileSpaceBounds(); + parameters.context.beginRoundedRect( + bounds.x * globalConfig.tileSize + boundsBorder, + bounds.y * globalConfig.tileSize + boundsBorder, + bounds.w * globalConfig.tileSize - 2 * boundsBorder, + 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 staticComp = entity.components.StaticMapEntity; - const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); - - this.deletionMarker.drawCachedCentered( - parameters, - center.x, - center.y, - globalConfig.tileSize * 0.5 + const bounds = staticComp.getTileSpaceBounds(); + parameters.context.beginRoundedRect( + bounds.x * globalConfig.tileSize + boundsBorder, + bounds.y * globalConfig.tileSize + boundsBorder, + bounds.w * globalConfig.tileSize - 2 * boundsBorder, + bounds.h * globalConfig.tileSize - 2 * boundsBorder, + 2 ); + parameters.context.fill(); }); } } diff --git a/src/js/game/hud/parts/tutorial_hints.js b/src/js/game/hud/parts/tutorial_hints.js index 853d054f..27fd5530 100644 --- a/src/js/game/hud/parts/tutorial_hints.js +++ b/src/js/game/hud/parts/tutorial_hints.js @@ -6,6 +6,7 @@ import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; import { BaseHUDPart } from "../base_hud_part"; import { DynamicDomAttach } from "../dynamic_dom_attach"; import { T } from "../../../translations"; +import { globalConfig } from "../../../core/config"; 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.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( T.dialogs.hintDescription.title, T.dialogs.hintDescription.desc diff --git a/src/js/game/key_action_mapper.js b/src/js/game/key_action_mapper.js index 9ab089c3..34df29c8 100644 --- a/src/js/game/key_action_mapper.js +++ b/src/js/game/key_action_mapper.js @@ -60,6 +60,7 @@ export const KEYMAPPINGS = { massSelect: { massSelectStart: { keyCode: 17, builtin: true }, // CTRL massSelectSelectMultiple: { keyCode: 16, builtin: true }, // SHIFT + massSelectCopy: { keyCode: key("C") }, confirmMassDelete: { keyCode: key("X") }, }, diff --git a/src/js/game/logic.js b/src/js/game/logic.js index c96364da..dc89f8c2 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -60,6 +60,7 @@ export class GameLogic { origin, tileSize: building.getDimensions(variant), rotation, + blueprintSpriteKey: "", }); const rect = checker.getTileSpaceBounds(); @@ -168,6 +169,7 @@ export class GameLogic { origin, tileSize: building.getDimensions(variant), rotation, + blueprintSpriteKey: "", }); const rect = checker.getTileSpaceBounds(); diff --git a/src/js/game/map.js b/src/js/game/map.js index 3fd82844..ef745c6d 100644 --- a/src/js/game/map.js +++ b/src/js/game/map.js @@ -147,6 +147,17 @@ export class BaseMap extends BasicSerializableObject { 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 * @param {Vector} tile diff --git a/src/js/game/meta_building.js b/src/js/game/meta_building.js index ad360ac0..8753aac5 100644 --- a/src/js/game/meta_building.js +++ b/src/js/game/meta_building.js @@ -154,6 +154,9 @@ export class MetaBuilding { */ createAndPlaceEntity({ root, origin, rotation, originalRotation, rotationVariant, variant }) { const entity = new Entity(root); + + const blueprintSprite = this.getBlueprintSprite(rotationVariant, variant); + entity.addComponent( new StaticMapEntityComponent({ spriteKey: @@ -166,6 +169,7 @@ export class MetaBuilding { originalRotation, tileSize: this.getDimensions(variant).copy(), silhouetteColor: this.getSilhouetteColor(), + blueprintSpriteKey: blueprintSprite ? blueprintSprite.spriteName : "", }) ); diff --git a/src/js/game/themes/dark.json b/src/js/game/themes/dark.json index caf28bfb..fa4c22a8 100644 --- a/src/js/game/themes/dark.json +++ b/src/js/game/themes/dark.json @@ -4,7 +4,7 @@ "background": "#2e2f37", "grid": "rgba(255, 255, 255, 0.02)", "gridLineWidth": 0.5, - + "selectionColor": "rgba(127, 127, 255, 0.5)", "resources": { "shape": "#3d3f4a", "red": "#4a3d3f", diff --git a/src/js/game/themes/light.json b/src/js/game/themes/light.json index 4837574c..59e9e58f 100644 --- a/src/js/game/themes/light.json +++ b/src/js/game/themes/light.json @@ -5,6 +5,10 @@ "grid": "#fafafa", "gridLineWidth": 1, + "selectionOverlay": "rgba(74, 163, 223, 0.7)", + "selectionOutline": "rgba(74, 163, 223, 0.5)", + "selectionBackground": "rgba(74, 163, 223, 0.2)", + "resources": { "shape": "#eaebec", "red": "#ffbfc1", diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index 8b9d2b3b..8027c188 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -11,8 +11,7 @@ import { createLogger } from "../core/logging"; import { globalConfig } from "../core/config"; import { SavegameInterface_V1000 } from "./schemas/1000"; import { getSavegameInterface } from "./savegame_interface_registry"; -import { compressObject } from "./savegame_compressor"; -import { compressX64 } from "../core/lzstring"; +import { SavegameInterface_V1001 } from "./schemas/1001"; const logger = createLogger("savegame"); @@ -29,7 +28,7 @@ export class Savegame extends ReadWriteProxy { this.internalId = internalId; this.metaDataRef = metaDataRef; - /** @type {SavegameData} */ + /** @type {import("./savegame_typedefs").SavegameData} */ this.currentData = this.getDefaultData(); } @@ -39,14 +38,14 @@ export class Savegame extends ReadWriteProxy { * @returns {number} */ static getCurrentVersion() { - return 1000; + return 1001; } /** * @returns {typeof BaseSavegameInterface} */ static getReaderClass() { - return SavegameInterface_V1000; + return SavegameInterface_V1001; } /** @@ -58,7 +57,7 @@ export class Savegame extends ReadWriteProxy { /** * Returns the savegames default data - * @returns {SavegameData} + * @returns {import("./savegame_typedefs").SavegameData} */ getDefaultData() { return { @@ -73,18 +72,25 @@ export class Savegame extends ReadWriteProxy { /** * Migrates the savegames data - * @param {SavegameData} data + * @param {import("./savegame_typedefs").SavegameData} data */ migrate(data) { if (data.version < 1000) { 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(); } /** * Verifies the savegames data - * @param {SavegameData} data + * @param {import("./savegame_typedefs").SavegameData} data */ verify(data) { if (!data.dump) { @@ -109,7 +115,7 @@ export class Savegame extends ReadWriteProxy { } /** * Returns the statistics of the savegame - * @returns {SavegameStats} + * @returns {import("./savegame_typedefs").SavegameStats} */ getStatistics() { return this.currentData.stats; @@ -132,7 +138,7 @@ export class Savegame extends ReadWriteProxy { /** * Returns the current game dump - * @returns {SerializedGame} + * @returns {import("./savegame_typedefs").SerializedGame} */ getCurrentDump() { return this.currentData.dump; diff --git a/src/js/savegame/savegame_interface_registry.js b/src/js/savegame/savegame_interface_registry.js index 6ad22a42..2560b23e 100644 --- a/src/js/savegame/savegame_interface_registry.js +++ b/src/js/savegame/savegame_interface_registry.js @@ -1,10 +1,12 @@ import { BaseSavegameInterface } from "./savegame_interface"; import { SavegameInterface_V1000 } from "./schemas/1000"; import { createLogger } from "../core/logging"; +import { SavegameInterface_V1001 } from "./schemas/1001"; /** @type {Object.} */ const interfaces = { 1000: SavegameInterface_V1000, + 1001: SavegameInterface_V1001, }; const logger = createLogger("savegame_interface_registry"); diff --git a/src/js/savegame/savegame_typedefs.js b/src/js/savegame/savegame_typedefs.js index ca72d856..821306a4 100644 --- a/src/js/savegame/savegame_typedefs.js +++ b/src/js/savegame/savegame_typedefs.js @@ -4,14 +4,7 @@ * }} SavegameStats */ -/** - * @typedef {{ - * x: number, - * y: number, - * uid: number, - * key: string - * }} SerializedMapResource - */ +import { Entity } from "../game/entity"; /** * @typedef {{ @@ -20,7 +13,7 @@ * entityMgr: any, * map: any, * hubGoals: any, - * entities: Array + * entities: Array * }} SerializedGame */ diff --git a/src/js/savegame/schemas/1001.js b/src/js/savegame/schemas/1001.js new file mode 100644 index 00000000..7604dec4 --- /dev/null +++ b/src/js/savegame/schemas/1001.js @@ -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"); + } + } + } + } + } +} diff --git a/src/js/savegame/schemas/1001.json b/src/js/savegame/schemas/1001.json new file mode 100644 index 00000000..6682f615 --- /dev/null +++ b/src/js/savegame/schemas/1001.json @@ -0,0 +1,5 @@ +{ + "type": "object", + "required": [], + "additionalProperties": true +} diff --git a/src/js/savegame/serializer_internal.js b/src/js/savegame/serializer_internal.js index 3eb0f72b..ec761beb 100644 --- a/src/js/savegame/serializer_internal.js +++ b/src/js/savegame/serializer_internal.js @@ -43,7 +43,7 @@ export class SerializerInternal { * @param {Entity} payload */ deserializeEntity(root, payload) { - const entity = new Entity(null); + const entity = new Entity(root); this.deserializeComponents(entity, payload.components); root.entityMgr.registerEntity(entity, payload.uid); diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 9dd18909..d95f520e 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -198,12 +198,12 @@ export class MainMenuState extends GameState { this.trackClicks(qs(".mainContainer .importButton"), this.requestImportSavegame); if (G_IS_DEV && globalConfig.debug.fastGameEnter) { - // // const games = this.app.savegameMgr.getSavegamesMetaData(); - // if (games.length > 0) { - // this.resumeGame(games[0]); - // } else { - this.onPlayButtonClicked(); - // } + const games = this.app.savegameMgr.getSavegamesMetaData(); + if (games.length > 0) { + this.resumeGame(games[0]); + } else { + this.onPlayButtonClicked(); + } } // Initialize video diff --git a/src/js/states/preload.js b/src/js/states/preload.js index 43c84eb0..29d5bdd4 100644 --- a/src/js/states/preload.js +++ b/src/js/states/preload.js @@ -6,6 +6,7 @@ import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; import { T } from "../translations"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; import { CHANGELOG } from "../changelog"; +import { globalConfig } from "../core/config"; const logger = createLogger("state/preload"); @@ -179,6 +180,10 @@ export class PreloadState extends GameState { .then(() => this.setStatus("Checking changelog")) .then(() => { + if (G_IS_DEV && globalConfig.debug.disableUpgradeNotification) { + return; + } + return this.app.storage .readFileAsync("lastversion.bin") .catch(err => { diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 2318c342..889e6583 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -172,6 +172,11 @@ dialogs: All shapes you produce can be used to unlock upgrades - Don't destroy your old factories! 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 ( to be exact)! Are you sure you want to do this? + ingame: # This is shown in the top left corner and displays useful keybindings in # every situation @@ -221,10 +226,10 @@ ingame: newUpgrade: A new upgrade is available! gameSaved: Your game has been saved. - # Mass delete information, this is when you hold CTRL and then drag with your mouse - # to select multiple buildings to delete - massDelete: - infoText: Press to remove selected buildings and to cancel. + # Mass select information, this is when you hold CTRL and then drag with your mouse + # to select multiple buildings + massSelect: + infoText: Press to copy, to remove and to cancel. # The "Upgrades" window shop: