1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-06-13 13:04:03 +00:00

[Puzzle] Added ability to lock buildings in the puzzle editor! (#1164)

* initial test

* tried to get it to work

* added icon

* added test exclusion

* reverted css

* completed flow for building locking

* added lock option

* finalized look and changed locked building to same sprite

* removed unused art

* added clearing every goal acceptor on lock to prevent creating impossible puzzles

* heavily improved validation and prevented autocompletion

* validation only checks every 100 ticks to improve performance

* validation only checks every 100 ticks to improve performance

* removed clearing goal acceptors as it isn't needed because of validation
This commit is contained in:
Sense101 2021-05-23 11:32:48 +01:00 committed by GitHub
parent 8e25818999
commit 0f93e13a63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 219 additions and 62 deletions

View File

@ -49,44 +49,45 @@
} }
.building { .building {
.icon {
color: $accentColorDark; color: $accentColorDark;
display: flex; display: flex;
flex-direction: column; flex-direction: column-reverse;
position: relative; position: relative;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@include S(padding, 5px); @include S(padding, 5px);
@include S(padding-bottom, 1px); @include S(padding-bottom, 1px);
@include S(width, 35px); @include S(width, 35px);
@include S(height, 40px); @include S(height, 37px);
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;
}
}
}
@include S(border-radius, $globalBorderRadius); @include S(border-radius, $globalBorderRadius);
background: center center / 70% no-repeat;
}
&:not(.unlocked) {
.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;
}
}
}
&.unlocked { &.unlocked {
.icon {
pointer-events: all; pointer-events: all;
transition: all 50ms ease-in-out; transition: all 50ms ease-in-out;
transition-property: background-color, transform; transition-property: background-color, transform;
@ -109,6 +110,36 @@
} }
} }
} }
.puzzle-lock {
& {
/* @load-async */
background: uiResource("locked_building.png") center center / #{D(14px)} #{D(14px)}
no-repeat;
}
display: grid;
grid-auto-flow: column;
@include S(margin-top, 2px);
@include S(margin-left, 16px);
@include S(margin-bottom, 29px);
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;
}
}
}
} }
} }
} }

View File

@ -5,8 +5,10 @@ $buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, tra
@each $building in $buildings { @each $building in $buildings {
[data-icon="building_icons/#{$building}.png"] { [data-icon="building_icons/#{$building}.png"] {
/* @load-async */ /* @load-async */
.icon {
background-image: uiResource("res/ui/building_icons/#{$building}.png") !important; background-image: uiResource("res/ui/building_icons/#{$building}.png") !important;
} }
}
} }
$buildingsAndVariants: belt, balancer, underground_belt, underground_belt-tier2, miner, miner-chainable, $buildingsAndVariants: belt, balancer, underground_belt, underground_belt-tier2, miner, miner-chainable,

View File

@ -1,6 +1,10 @@
import { gMetaBuildingRegistry } from "../../../core/global_registries"; import { gMetaBuildingRegistry } from "../../../core/global_registries";
import { STOP_PROPAGATION } from "../../../core/signal"; import { STOP_PROPAGATION } from "../../../core/signal";
import { makeDiv, safeModulo } from "../../../core/utils"; 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 { KEYMAPPINGS } from "../../key_action_mapper";
import { MetaBuilding } from "../../meta_building"; import { MetaBuilding } from "../../meta_building";
import { GameRoot } from "../../root"; import { GameRoot } from "../../root";
@ -35,6 +39,8 @@ export class HUDBaseToolbar extends BaseHUDPart {
* selected: boolean, * selected: boolean,
* element: HTMLElement, * element: HTMLElement,
* index: number * index: number
* puzzleLocked: boolean;
* class: typeof MetaBuilding,
* }>} */ * }>} */
this.buildingHandles = {}; this.buildingHandles = {};
} }
@ -105,19 +111,32 @@ export class HUDBaseToolbar extends BaseHUDPart {
); );
itemContainer.setAttribute("data-icon", "building_icons/" + metaBuilding.getId() + ".png"); itemContainer.setAttribute("data-icon", "building_icons/" + metaBuilding.getId() + ".png");
itemContainer.setAttribute("data-id", metaBuilding.getId()); itemContainer.setAttribute("data-id", metaBuilding.getId());
binding.add(() => this.selectBuildingForPlacement(metaBuilding)); 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, 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] = { this.buildingHandles[metaBuilding.id] = {
metaBuilding, metaBuilding: metaBuilding,
element: itemContainer, element: itemContainer,
unlocked: false, unlocked: false,
selected: false, selected: false,
index: i, index: i,
puzzleLocked: false,
class: allBuildings[i],
}; };
} }
@ -145,7 +164,7 @@ export class HUDBaseToolbar extends BaseHUDPart {
let recomputeSecondaryToolbarVisibility = false; let recomputeSecondaryToolbarVisibility = false;
for (const buildingId in this.buildingHandles) { for (const buildingId in this.buildingHandles) {
const handle = this.buildingHandles[buildingId]; 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) { if (handle.unlocked !== newStatus) {
handle.unlocked = newStatus; handle.unlocked = newStatus;
handle.element.classList.toggle("unlocked", newStatus); handle.element.classList.toggle("unlocked", newStatus);
@ -234,6 +253,14 @@ export class HUDBaseToolbar extends BaseHUDPart {
return STOP_PROPAGATION; 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 // Allow clicking an item again to deselect it
for (const buildingId in this.buildingHandles) { for (const buildingId in this.buildingHandles) {
const handle = this.buildingHandles[buildingId]; const handle = this.buildingHandles[buildingId];
@ -247,4 +274,51 @@ export class HUDBaseToolbar extends BaseHUDPart {
this.root.hud.signals.buildingSelectedForPlacement.dispatch(metaBuilding); this.root.hud.signals.buildingSelectedForPlacement.dispatch(metaBuilding);
this.onSelectedPlacementBuildingChanged(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);
}
} }

View File

@ -49,23 +49,27 @@ export class HUDPuzzleEditorReview extends BaseHUDPart {
// Manually simulate ticks // Manually simulate ticks
this.root.logic.clearAllBeltsAndItems(); this.root.logic.clearAllBeltsAndItems();
const ticks = const maxTicks =
this.root.gameMode.getFixedTickrate() * globalConfig.puzzleValidationDurationSeconds; this.root.gameMode.getFixedTickrate() * globalConfig.puzzleValidationDurationSeconds;
const deltaMs = this.root.dynamicTickrate.deltaMs; 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(); const now = performance.now();
for (let i = 0; i < ticks; ++i) {
if (i % Math.round((ticks - 1) / 10) === 0) { let simulatedTicks = 0;
console.log("Ticking", Math.round((i / ticks) * 100) + "%"); 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;
}
} }
// Perform logic ticks
this.root.time.performTicks(deltaMs, this.root.gameState.core.boundInternalTick);
}
const duration = performance.now() - now; const duration = performance.now() - now;
logger.log( logger.log(
"Simulated", "Simulated",
ticks, simulatedTicks,
"ticks, end=", "ticks, end=",
this.root.time.now().toFixed(1), this.root.time.now().toFixed(1),
"duration=", "duration=",
@ -73,9 +77,21 @@ export class HUDPuzzleEditorReview extends BaseHUDPart {
"ms" "ms"
); );
console.log("duration: " + duration);
closeLoading(); 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(); const validationError = this.validatePuzzle();
if (validationError) { if (simulatedTicks == maxTicks && validationError) {
this.root.hud.parts.dialogs.showWarning(T.puzzleMenu.validation.title, validationError); this.root.hud.parts.dialogs.showWarning(T.puzzleMenu.validation.title, validationError);
return; return;
} }

View File

@ -7,6 +7,9 @@ import { types } from "../../savegame/serialization";
import { enumGameModeTypes, GameMode } from "../game_mode"; import { enumGameModeTypes, GameMode } from "../game_mode";
import { HUDPuzzleBackToMenu } from "../hud/parts/puzzle_back_to_menu"; import { HUDPuzzleBackToMenu } from "../hud/parts/puzzle_back_to_menu";
import { HUDPuzzleDLCLogo } from "../hud/parts/puzzle_dlc_logo"; 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 { export class PuzzleGameMode extends GameMode {
static getType() { static getType() {
@ -36,6 +39,13 @@ export class PuzzleGameMode extends GameMode {
this.zoneHeight = data.zoneHeight || 6; this.zoneHeight = data.zoneHeight || 6;
} }
/**
* @param {typeof import("../meta_building").MetaBuilding} building
*/
isBuildingExcluded(building) {
return this.hiddenBuildings.indexOf(building) >= 0;
}
getSaveData() { getSaveData() {
const save = this.root.savegame.getCurrentDump(); const save = this.root.savegame.getCurrentDump();
if (!save) { if (!save) {

View File

@ -28,6 +28,7 @@ import { createLogger } from "../../core/logging";
import { HUDPuzzleCompleteNotification } from "../hud/parts/puzzle_complete_notification"; import { HUDPuzzleCompleteNotification } from "../hud/parts/puzzle_complete_notification";
import { HUDPuzzlePlaySettings } from "../hud/parts/puzzle_play_settings"; import { HUDPuzzlePlaySettings } from "../hud/parts/puzzle_play_settings";
import { MetaBlockBuilding } from "../buildings/block"; import { MetaBlockBuilding } from "../buildings/block";
import { MetaBuilding } from "../meta_building";
const logger = createLogger("puzzle-play"); const logger = createLogger("puzzle-play");
const copy = require("clipboard-copy"); const copy = require("clipboard-copy");
@ -45,7 +46,8 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
constructor(root, { puzzle }) { constructor(root, { puzzle }) {
super(root); super(root);
this.hiddenBuildings = [ /** @type {Array<typeof MetaBuilding>} */
const excludedBuildings = [
MetaConstantProducerBuilding, MetaConstantProducerBuilding,
MetaGoalAcceptorBuilding, MetaGoalAcceptorBuilding,
MetaBlockBuilding, MetaBlockBuilding,
@ -68,6 +70,8 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
MetaTransistorBuilding, MetaTransistorBuilding,
]; ];
this.hiddenBuildings = excludedBuildings.concat(puzzle.game.excludedBuildings);
this.additionalHudParts.puzzlePlayMetadata = HUDPuzzlePlayMetadata; this.additionalHudParts.puzzlePlayMetadata = HUDPuzzlePlayMetadata;
this.additionalHudParts.puzzlePlaySettings = HUDPuzzlePlaySettings; this.additionalHudParts.puzzlePlaySettings = HUDPuzzlePlaySettings;
this.additionalHudParts.puzzleCompleteNotification = HUDPuzzleCompleteNotification; this.additionalHudParts.puzzleCompleteNotification = HUDPuzzleCompleteNotification;

View File

@ -1,5 +1,6 @@
/* typehints:start */ /* typehints:start */
import { GameRoot } from "../root"; import { GameRoot } from "../root";
import { MetaBuilding } from "../meta_building";
/* typehints:end */ /* typehints:end */
import { findNiceIntegerValue } from "../../core/utils"; import { findNiceIntegerValue } from "../../core/utils";
@ -582,6 +583,7 @@ export class RegularGameMode extends GameMode {
this.additionalHudParts.sandboxController = HUDSandboxController; this.additionalHudParts.sandboxController = HUDSandboxController;
} }
/** @type {(typeof MetaBuilding)[]} */
this.hiddenBuildings = [MetaConstantProducerBuilding, MetaGoalAcceptorBuilding, MetaBlockBuilding]; this.hiddenBuildings = [MetaConstantProducerBuilding, MetaGoalAcceptorBuilding, MetaBlockBuilding];
} }

View File

@ -7,7 +7,7 @@ import { StaticMapEntityComponent } from "../game/components/static_map_entity";
import { ShapeItem } from "../game/items/shape_item"; import { ShapeItem } from "../game/items/shape_item";
import { Vector } from "../core/vector"; import { Vector } from "../core/vector";
import { MetaConstantProducerBuilding } from "../game/buildings/constant_producer"; 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 { gMetaBuildingRegistry } from "../core/global_registries";
import { MetaGoalAcceptorBuilding } from "../game/buildings/goal_acceptor"; import { MetaGoalAcceptorBuilding } from "../game/buildings/goal_acceptor";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
@ -33,7 +33,6 @@ export class PuzzleSerializer {
* @type {import("./savegame_typedefs").PuzzleGameData["buildings"]} * @type {import("./savegame_typedefs").PuzzleGameData["buildings"]}
*/ */
let buildings = []; let buildings = [];
for (const entity of root.entityMgr.getAllWithComponent(StaticMapEntityComponent)) { for (const entity of root.entityMgr.getAllWithComponent(StaticMapEntityComponent)) {
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
const signalComp = entity.components.ConstantSignal; const signalComp = entity.components.ConstantSignal;
@ -83,6 +82,18 @@ export class PuzzleSerializer {
const mode = /** @type {PuzzleGameMode} */ (root.gameMode); const mode = /** @type {PuzzleGameMode} */ (root.gameMode);
const handles = root.hud.parts.buildingsToolbar.buildingHandles;
const ids = gMetaBuildingRegistry.getAllIds();
/** @type {Array<typeof MetaBuilding>} */
let excludedBuildings = [];
for (let i = 0; i < ids.length; ++i) {
const handle = handles[ids[i]];
if (handle && handle.puzzleLocked) {
excludedBuildings.push(handle.class);
}
}
return { return {
version: 1, version: 1,
buildings, buildings,
@ -90,6 +101,8 @@ export class PuzzleSerializer {
w: mode.zoneWidth, w: mode.zoneWidth,
h: mode.zoneHeight, h: mode.zoneHeight,
}, },
//read from the toolbar when making a puzzle
excludedBuildings,
}; };
} }

View File

@ -41,6 +41,8 @@
* }} SavegamesData * }} SavegamesData
*/ */
import { MetaBuilding } from "../game/meta_building";
// Notice: Update backend too // Notice: Update backend too
/** /**
* @typedef {{ * @typedef {{
@ -84,7 +86,8 @@
* @typedef {{ * @typedef {{
* version: number; * version: number;
* bounds: { w: number; h: number; }, * bounds: { w: number; h: number; },
* buildings: (PuzzleGameBuildingGoal | PuzzleGameBuildingConstantProducer | PuzzleGameBuildingBlock)[] * buildings: (PuzzleGameBuildingGoal | PuzzleGameBuildingConstantProducer | PuzzleGameBuildingBlock)[],
* excludedBuildings: Array<typeof MetaBuilding>,
* }} PuzzleGameData * }} PuzzleGameData
*/ */

View File

@ -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. One or more Goal Acceptors are not getting enough items. Make sure that the indicators are green for all acceptors.
buildingOutOfBounds: >- buildingOutOfBounds: >-
One or more buildings are outside of the buildable area. Either increase the area or remove them. 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: dialogs:
buttons: buttons: