diff --git a/src/css/ingame_hud/buildings_toolbar.scss b/src/css/ingame_hud/buildings_toolbar.scss index 54205d64..ec20ff02 100644 --- a/src/css/ingame_hud/buildings_toolbar.scss +++ b/src/css/ingame_hud/buildings_toolbar.scss @@ -49,63 +49,94 @@ } .building { - color: $accentColorDark; - display: flex; - flex-direction: column; - position: relative; - align-items: center; - justify-content: center; - @include S(padding, 5px); - @include S(padding-bottom, 1px); - @include S(width, 35px); - @include S(height, 40px); + .icon { + color: $accentColorDark; + display: flex; + flex-direction: column-reverse; + position: relative; + align-items: center; + justify-content: center; + @include S(padding, 5px); + @include S(padding-bottom, 1px); + @include S(width, 35px); + @include S(height, 37px); + @include S(border-radius, $globalBorderRadius); - background: center center / 70% no-repeat; + background: center center / 70% no-repeat; + } &:not(.unlocked) { - @include S(width, 20px); - opacity: 0.15; - background-image: none !important; - - &::before { - content: " "; - - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 4; - & { - /* @load-async */ - background: uiResource("locked_building.png") center center / #{D(20px)} #{D(20px)} - no-repeat; + .icon { + @include S(width, 20px); + opacity: 0.15; + } + &.editor { + .icon { + pointer-events: all; + cursor: pointer; + &:hover { + background-color: rgba(22, 30, 68, 0.1); + } + } + } + &:not(.editor) { + .icon { + background-image: uiResource("locked_building.png") !important; } } } - @include S(border-radius, $globalBorderRadius); - &.unlocked { - pointer-events: all; - transition: all 50ms ease-in-out; - transition-property: background-color, transform; + .icon { + pointer-events: all; + transition: all 50ms ease-in-out; + transition-property: background-color, transform; - cursor: pointer; - &:hover { - background-color: rgba(30, 40, 90, 0.1); + cursor: pointer; + &:hover { + background-color: rgba(30, 40, 90, 0.1); + } + + &.pressed { + transform: scale(0.9) !important; + } + + &.selected { + // transform: scale(1.05); + background-color: rgba(lighten($colorBlueBright, 9), 0.4); + + .keybinding { + color: #111; + } + } } - &.pressed { - transform: scale(0.9) !important; - } + .puzzle-lock { + & { + /* @load-async */ + background: uiResource("locked_building.png") center center / #{D(14px)} #{D(14px)} + no-repeat; + } - &.selected { - // transform: scale(1.05); - background-color: rgba(lighten($colorBlueBright, 9), 0.4); + display: grid; + grid-auto-flow: column; + @include S(margin-top, 2px); + @include S(margin-left, 16px); + @include S(margin-bottom, 29px); - .keybinding { - color: #111; + position: absolute; + bottom: 20px; + transition: all 0.12s ease-in-out; + transition-property: opacity, transform; + + cursor: pointer; + pointer-events: all; + + @include S(width, 14px); + @include S(height, 14px); + + &:hover { + opacity: 0.5; } } } diff --git a/src/css/resources.scss b/src/css/resources.scss index c3c6ea88..3a581c30 100644 --- a/src/css/resources.scss +++ b/src/css/resources.scss @@ -5,7 +5,9 @@ $buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, tra @each $building in $buildings { [data-icon="building_icons/#{$building}.png"] { /* @load-async */ - background-image: uiResource("res/ui/building_icons/#{$building}.png") !important; + .icon { + background-image: uiResource("res/ui/building_icons/#{$building}.png") !important; + } } } diff --git a/src/js/game/hud/parts/base_toolbar.js b/src/js/game/hud/parts/base_toolbar.js index 9df362f3..4abace68 100644 --- a/src/js/game/hud/parts/base_toolbar.js +++ b/src/js/game/hud/parts/base_toolbar.js @@ -1,6 +1,10 @@ import { gMetaBuildingRegistry } from "../../../core/global_registries"; import { STOP_PROPAGATION } from "../../../core/signal"; import { makeDiv, safeModulo } from "../../../core/utils"; +import { MetaBlockBuilding } from "../../buildings/block"; +import { MetaConstantProducerBuilding } from "../../buildings/constant_producer"; +import { MetaGoalAcceptorBuilding } from "../../buildings/goal_acceptor"; +import { StaticMapEntityComponent } from "../../components/static_map_entity"; import { KEYMAPPINGS } from "../../key_action_mapper"; import { MetaBuilding } from "../../meta_building"; import { GameRoot } from "../../root"; @@ -35,6 +39,8 @@ export class HUDBaseToolbar extends BaseHUDPart { * selected: boolean, * element: HTMLElement, * index: number + * puzzleLocked: boolean; + * class: typeof MetaBuilding, * }>} */ this.buildingHandles = {}; } @@ -105,19 +111,32 @@ export class HUDBaseToolbar extends BaseHUDPart { ); itemContainer.setAttribute("data-icon", "building_icons/" + metaBuilding.getId() + ".png"); itemContainer.setAttribute("data-id", metaBuilding.getId()); - binding.add(() => this.selectBuildingForPlacement(metaBuilding)); - this.trackClicks(itemContainer, () => this.selectBuildingForPlacement(metaBuilding), { + const icon = makeDiv(itemContainer, null, ["icon"]); + + this.trackClicks(icon, () => this.selectBuildingForPlacement(metaBuilding), { clickSound: null, }); + //lock icon for puzzle editor + if (this.root.gameMode.getIsEditor() && !this.inRequiredBuildings(metaBuilding)) { + const puzzleLock = makeDiv(itemContainer, null, ["puzzle-lock"]); + + itemContainer.classList.toggle("editor", true); + this.trackClicks(puzzleLock, () => this.toggleBuildingLock(metaBuilding), { + clickSound: null, + }); + } + this.buildingHandles[metaBuilding.id] = { - metaBuilding, + metaBuilding: metaBuilding, element: itemContainer, unlocked: false, selected: false, index: i, + puzzleLocked: false, + class: allBuildings[i], }; } @@ -145,7 +164,7 @@ export class HUDBaseToolbar extends BaseHUDPart { let recomputeSecondaryToolbarVisibility = false; for (const buildingId in this.buildingHandles) { const handle = this.buildingHandles[buildingId]; - const newStatus = handle.metaBuilding.getIsUnlocked(this.root); + const newStatus = !handle.puzzleLocked && handle.metaBuilding.getIsUnlocked(this.root); if (handle.unlocked !== newStatus) { handle.unlocked = newStatus; handle.element.classList.toggle("unlocked", newStatus); @@ -234,6 +253,14 @@ export class HUDBaseToolbar extends BaseHUDPart { return STOP_PROPAGATION; } + const handle = this.buildingHandles[metaBuilding.getId()]; + if (handle.puzzleLocked) { + handle.puzzleLocked = false; + handle.element.classList.toggle("unlocked", false); + this.root.soundProxy.playUiClick(); + return; + } + // Allow clicking an item again to deselect it for (const buildingId in this.buildingHandles) { const handle = this.buildingHandles[buildingId]; @@ -247,4 +274,51 @@ export class HUDBaseToolbar extends BaseHUDPart { this.root.hud.signals.buildingSelectedForPlacement.dispatch(metaBuilding); this.onSelectedPlacementBuildingChanged(metaBuilding); } + + /** + * @param {MetaBuilding} metaBuilding + */ + toggleBuildingLock(metaBuilding) { + if (!this.visibilityCondition()) { + // Not active + return; + } + + if (this.inRequiredBuildings(metaBuilding) || !metaBuilding.getIsUnlocked(this.root)) { + this.root.soundProxy.playUiError(); + return STOP_PROPAGATION; + } + + const handle = this.buildingHandles[metaBuilding.getId()]; + handle.puzzleLocked = !handle.puzzleLocked; + handle.element.classList.toggle("unlocked", !handle.puzzleLocked); + this.root.soundProxy.playUiClick(); + + const entityManager = this.root.entityMgr; + for (const entity of entityManager.getAllWithComponent(StaticMapEntityComponent)) { + const staticComp = entity.components.StaticMapEntity; + if (staticComp.getMetaBuilding().id === metaBuilding.id) { + this.root.map.removeStaticEntity(entity); + entityManager.destroyEntity(entity); + } + } + entityManager.processDestroyList(); + + const currentMetaBuilding = this.root.hud.parts.buildingPlacer.currentMetaBuilding; + if (currentMetaBuilding.get() == metaBuilding) { + currentMetaBuilding.set(null); + } + } + + /** + * @param {MetaBuilding} metaBuilding + */ + inRequiredBuildings(metaBuilding) { + const requiredBuildings = [ + gMetaBuildingRegistry.findByClass(MetaConstantProducerBuilding), + gMetaBuildingRegistry.findByClass(MetaGoalAcceptorBuilding), + gMetaBuildingRegistry.findByClass(MetaBlockBuilding), + ]; + return requiredBuildings.includes(metaBuilding); + } } diff --git a/src/js/game/hud/parts/puzzle_editor_review.js b/src/js/game/hud/parts/puzzle_editor_review.js index f36b4ae4..b358dc6d 100644 --- a/src/js/game/hud/parts/puzzle_editor_review.js +++ b/src/js/game/hud/parts/puzzle_editor_review.js @@ -49,23 +49,27 @@ export class HUDPuzzleEditorReview extends BaseHUDPart { // Manually simulate ticks this.root.logic.clearAllBeltsAndItems(); - const ticks = + const maxTicks = this.root.gameMode.getFixedTickrate() * globalConfig.puzzleValidationDurationSeconds; const deltaMs = this.root.dynamicTickrate.deltaMs; - logger.log("Simulating", ticks, "ticks, start=", this.root.time.now().toFixed(1)); + logger.log("Simulating up to", maxTicks, "ticks, start=", this.root.time.now().toFixed(1)); const now = performance.now(); - for (let i = 0; i < ticks; ++i) { - if (i % Math.round((ticks - 1) / 10) === 0) { - console.log("Ticking", Math.round((i / ticks) * 100) + "%"); - } - // Perform logic ticks + let simulatedTicks = 0; + for (let i = 0; i < maxTicks; ++i) { + // Perform logic tick this.root.time.performTicks(deltaMs, this.root.gameState.core.boundInternalTick); + simulatedTicks++; + + if (simulatedTicks % 100 == 0 && !this.validatePuzzle()) { + break; + } } + const duration = performance.now() - now; logger.log( "Simulated", - ticks, + simulatedTicks, "ticks, end=", this.root.time.now().toFixed(1), "duration=", @@ -73,9 +77,21 @@ export class HUDPuzzleEditorReview extends BaseHUDPart { "ms" ); + console.log("duration: " + duration); closeLoading(); + + //if it took so little ticks that it must have autocompeted + if (simulatedTicks <= 300) { + this.root.hud.parts.dialogs.showWarning( + T.puzzleMenu.validation.title, + T.puzzleMenu.validation.autoComplete + ); + return; + } + + //if we reached maximum ticks and the puzzle still isn't completed const validationError = this.validatePuzzle(); - if (validationError) { + if (simulatedTicks == maxTicks && validationError) { this.root.hud.parts.dialogs.showWarning(T.puzzleMenu.validation.title, validationError); return; } diff --git a/src/js/game/modes/puzzle.js b/src/js/game/modes/puzzle.js index 058e27be..3f5febf1 100644 --- a/src/js/game/modes/puzzle.js +++ b/src/js/game/modes/puzzle.js @@ -7,6 +7,9 @@ import { types } from "../../savegame/serialization"; import { enumGameModeTypes, GameMode } from "../game_mode"; import { HUDPuzzleBackToMenu } from "../hud/parts/puzzle_back_to_menu"; import { HUDPuzzleDLCLogo } from "../hud/parts/puzzle_dlc_logo"; +import { gMetaBuildingRegistry } from "../../core/global_registries"; +import { MetaBalancerBuilding } from "../buildings/balancer"; +import { MetaUndergroundBeltBuilding } from "../buildings/underground_belt"; export class PuzzleGameMode extends GameMode { static getType() { @@ -36,6 +39,13 @@ export class PuzzleGameMode extends GameMode { this.zoneHeight = data.zoneHeight || 6; } + /** + * @param {typeof import("../meta_building").MetaBuilding} building + */ + isBuildingExcluded(building) { + return this.hiddenBuildings.indexOf(building) >= 0; + } + getSaveData() { const save = this.root.savegame.getCurrentDump(); if (!save) { diff --git a/src/js/game/modes/puzzle_play.js b/src/js/game/modes/puzzle_play.js index e4d05076..eafbd7ff 100644 --- a/src/js/game/modes/puzzle_play.js +++ b/src/js/game/modes/puzzle_play.js @@ -28,6 +28,7 @@ import { createLogger } from "../../core/logging"; import { HUDPuzzleCompleteNotification } from "../hud/parts/puzzle_complete_notification"; import { HUDPuzzlePlaySettings } from "../hud/parts/puzzle_play_settings"; import { MetaBlockBuilding } from "../buildings/block"; +import { MetaBuilding } from "../meta_building"; const logger = createLogger("puzzle-play"); const copy = require("clipboard-copy"); @@ -45,7 +46,8 @@ export class PuzzlePlayGameMode extends PuzzleGameMode { constructor(root, { puzzle }) { super(root); - this.hiddenBuildings = [ + /** @type {Array} */ + const excludedBuildings = [ MetaConstantProducerBuilding, MetaGoalAcceptorBuilding, MetaBlockBuilding, @@ -68,6 +70,8 @@ export class PuzzlePlayGameMode extends PuzzleGameMode { MetaTransistorBuilding, ]; + this.hiddenBuildings = excludedBuildings.concat(puzzle.game.excludedBuildings); + this.additionalHudParts.puzzlePlayMetadata = HUDPuzzlePlayMetadata; this.additionalHudParts.puzzlePlaySettings = HUDPuzzlePlaySettings; this.additionalHudParts.puzzleCompleteNotification = HUDPuzzleCompleteNotification; diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js index ee76ba84..0b71ff39 100644 --- a/src/js/game/modes/regular.js +++ b/src/js/game/modes/regular.js @@ -1,5 +1,6 @@ /* typehints:start */ import { GameRoot } from "../root"; +import { MetaBuilding } from "../meta_building"; /* typehints:end */ import { findNiceIntegerValue } from "../../core/utils"; @@ -582,6 +583,7 @@ export class RegularGameMode extends GameMode { this.additionalHudParts.sandboxController = HUDSandboxController; } + /** @type {(typeof MetaBuilding)[]} */ this.hiddenBuildings = [MetaConstantProducerBuilding, MetaGoalAcceptorBuilding, MetaBlockBuilding]; } diff --git a/src/js/savegame/puzzle_serializer.js b/src/js/savegame/puzzle_serializer.js index 49dd4ef6..8a032e9a 100644 --- a/src/js/savegame/puzzle_serializer.js +++ b/src/js/savegame/puzzle_serializer.js @@ -7,7 +7,7 @@ import { StaticMapEntityComponent } from "../game/components/static_map_entity"; import { ShapeItem } from "../game/items/shape_item"; import { Vector } from "../core/vector"; import { MetaConstantProducerBuilding } from "../game/buildings/constant_producer"; -import { defaultBuildingVariant } from "../game/meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../game/meta_building"; import { gMetaBuildingRegistry } from "../core/global_registries"; import { MetaGoalAcceptorBuilding } from "../game/buildings/goal_acceptor"; import { createLogger } from "../core/logging"; @@ -33,7 +33,6 @@ export class PuzzleSerializer { * @type {import("./savegame_typedefs").PuzzleGameData["buildings"]} */ let buildings = []; - for (const entity of root.entityMgr.getAllWithComponent(StaticMapEntityComponent)) { const staticComp = entity.components.StaticMapEntity; const signalComp = entity.components.ConstantSignal; @@ -83,6 +82,18 @@ export class PuzzleSerializer { const mode = /** @type {PuzzleGameMode} */ (root.gameMode); + const handles = root.hud.parts.buildingsToolbar.buildingHandles; + const ids = gMetaBuildingRegistry.getAllIds(); + + /** @type {Array} */ + let excludedBuildings = []; + for (let i = 0; i < ids.length; ++i) { + const handle = handles[ids[i]]; + if (handle && handle.puzzleLocked) { + excludedBuildings.push(handle.class); + } + } + return { version: 1, buildings, @@ -90,6 +101,8 @@ export class PuzzleSerializer { w: mode.zoneWidth, h: mode.zoneHeight, }, + //read from the toolbar when making a puzzle + excludedBuildings, }; } diff --git a/src/js/savegame/savegame_typedefs.js b/src/js/savegame/savegame_typedefs.js index 64a06dac..3abe8c21 100644 --- a/src/js/savegame/savegame_typedefs.js +++ b/src/js/savegame/savegame_typedefs.js @@ -41,6 +41,8 @@ * }} SavegamesData */ +import { MetaBuilding } from "../game/meta_building"; + // Notice: Update backend too /** * @typedef {{ @@ -84,7 +86,8 @@ * @typedef {{ * version: number; * bounds: { w: number; h: number; }, - * buildings: (PuzzleGameBuildingGoal | PuzzleGameBuildingConstantProducer | PuzzleGameBuildingBlock)[] + * buildings: (PuzzleGameBuildingGoal | PuzzleGameBuildingConstantProducer | PuzzleGameBuildingBlock)[], + * excludedBuildings: Array, * }} PuzzleGameData */ diff --git a/translations/base-en.yaml b/translations/base-en.yaml index aba636b6..94031ef1 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -151,6 +151,8 @@ puzzleMenu: One or more Goal Acceptors are not getting enough items. Make sure that the indicators are green for all acceptors. buildingOutOfBounds: >- One or more buildings are outside of the buildable area. Either increase the area or remove them. + autoComplete: >- + Your puzzle autocompletes itself! Please make sure your constant producers are not directly delivering to your goal acceptors. dialogs: buttons: