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:
parent
8e25818999
commit
0f93e13a63
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user