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,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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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) {

View File

@ -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<typeof MetaBuilding>} */
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;

View File

@ -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];
}

View File

@ -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<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 {
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,
};
}

View File

@ -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<typeof MetaBuilding>,
* }} 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.
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: