mirror of
https://github.com/tobspr/shapez.io.git
synced 2026-03-02 03:39:21 +00:00
Puzzle DLC (#1172)
* Puzzle mode (#1135) * Add mode button to main menu * [WIP] Add mode menu. Add factory-based gameMode creation * Add savefile migration, serialize, deserialize * Add hidden HUD elements, zone, and zoom, boundary constraints * Clean up lint issues * Add building, HUD exclusion, building exclusion, and refactor - [WIP] Add ConstantProducer building that combines ConstantSignal and ItemProducer functionality. Currently using temp assets. - Add pre-placement check to the zone - Use Rectangles for zone and boundary - Simplify zone drawing - Account for exclusion in savegame data - [WIP] Add puzzle play and edit buttons in puzzle mode menu * [WIP] Add building, component, and systems for producing and accepting user-specified items and checking goal criteria * Add ingame puzzle mode UI elements - Add minimal menus in puzzle mode for back, next navigation - Add lower menu for changing zone dimenensions Co-authored-by: Greg Considine <gconsidine@users.noreply.github.com> * Performance optimizations (#1154) * 1.3.1 preparations * Minor fixes, update translations * Fix achievements not working * Lots of belt optimizations, ~15% performance boost * Puzzle mode, part 1 * Puzzle mode, part 2 * Fix missing import * Puzzle mode, part 3 * Fix typo * Puzzle mode, part 4 * Puzzle Mode fixes: Correct zone restrictions and more (#1155) * Hide Puzzle Editor Controls in regular game mode, fix typo * Disallow shrinking zone if there are buildings * Fix multi-tile buildings for shrinking * Puzzle mode, Refactor hud * Puzzle mode * Fixed typo in latest puzzle commit (#1156) * Allow completing puzzles * Puzzle mode, almost done * Bump version to 1.4.0 * Fixes * [puzzle] Prevent pipette cheats (miners, emitters) (#1158) * Puzzle mode, almost done * Allow clearing belts with 'B' * Multiple users for the puzzle dlc * Bump api key * Minor adjustments * Update * Minor fixes * Fix throughput * Fix belts * Minor puzzle adjustments * New difficulty * Minor puzzle improvements * Fix belt path * Update translations * Added a button to return to the menu after a puzzle is completed (#1170) * added another button to return to the menu * improved menu return * fixed continue button to not go back to menu * [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 * Add soundtrack, puzzle dlc fixes Co-authored-by: Greg Considine <gconsidine@users.noreply.github.com> Co-authored-by: dengr1065 <dengr1065@gmail.com> Co-authored-by: Sense101 <67970865+Sense101@users.noreply.github.com>
This commit is contained in:
@@ -1,54 +1,22 @@
|
||||
/* typehints:start */
|
||||
import { GameRoot } from "../root";
|
||||
/* typehints:end */
|
||||
|
||||
/* dev:start */
|
||||
import { TrailerMaker } from "./trailer_maker";
|
||||
/* dev:end */
|
||||
|
||||
import { Signal } from "../../core/signal";
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { DrawParameters } from "../../core/draw_parameters";
|
||||
import { Signal } from "../../core/signal";
|
||||
import { KEYMAPPINGS } from "../key_action_mapper";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { ShapeDefinition } from "../shape_definition";
|
||||
import { HUDBetaOverlay } from "./parts/beta_overlay";
|
||||
import { HUDBuildingsToolbar } from "./parts/buildings_toolbar";
|
||||
import { HUDBuildingPlacer } from "./parts/building_placer";
|
||||
import { HUDBlueprintPlacer } from "./parts/blueprint_placer";
|
||||
import { HUDKeybindingOverlay } from "./parts/keybinding_overlay";
|
||||
import { HUDUnlockNotification } from "./parts/unlock_notification";
|
||||
import { HUDGameMenu } from "./parts/game_menu";
|
||||
import { HUDShop } from "./parts/shop";
|
||||
import { IS_MOBILE, globalConfig } from "../../core/config";
|
||||
import { HUDMassSelector } from "./parts/mass_selector";
|
||||
import { HUDVignetteOverlay } from "./parts/vignette_overlay";
|
||||
import { HUDStatistics } from "./parts/statistics";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
import { HUDPinnedShapes } from "./parts/pinned_shapes";
|
||||
import { ShapeDefinition } from "../shape_definition";
|
||||
import { HUDNotifications, enumNotificationType } from "./parts/notifications";
|
||||
import { HUDSettingsMenu } from "./parts/settings_menu";
|
||||
import { HUDColorBlindHelper } from "./parts/color_blind_helper";
|
||||
import { HUDChangesDebugger } from "./parts/debug_changes";
|
||||
import { HUDDebugInfo } from "./parts/debug_info";
|
||||
import { HUDEntityDebugger } from "./parts/entity_debugger";
|
||||
import { KEYMAPPINGS } from "../key_action_mapper";
|
||||
import { HUDWatermark } from "./parts/watermark";
|
||||
import { HUDModalDialogs } from "./parts/modal_dialogs";
|
||||
import { HUDPartTutorialHints } from "./parts/tutorial_hints";
|
||||
import { HUDWaypoints } from "./parts/waypoints";
|
||||
import { HUDInteractiveTutorial } from "./parts/interactive_tutorial";
|
||||
import { HUDScreenshotExporter } from "./parts/screenshot_exporter";
|
||||
import { HUDColorBlindHelper } from "./parts/color_blind_helper";
|
||||
import { HUDShapeViewer } from "./parts/shape_viewer";
|
||||
import { HUDWiresOverlay } from "./parts/wires_overlay";
|
||||
import { HUDChangesDebugger } from "./parts/debug_changes";
|
||||
import { queryParamOptions } from "../../core/query_parameters";
|
||||
import { HUDSandboxController } from "./parts/sandbox_controller";
|
||||
import { HUDWiresToolbar } from "./parts/wires_toolbar";
|
||||
import { HUDWireInfo } from "./parts/wire_info";
|
||||
import { HUDLeverToggle } from "./parts/lever_toggle";
|
||||
import { HUDLayerPreview } from "./parts/layer_preview";
|
||||
import { HUDMinerHighlight } from "./parts/miner_highlight";
|
||||
import { HUDBetaOverlay } from "./parts/beta_overlay";
|
||||
import { HUDStandaloneAdvantages } from "./parts/standalone_advantages";
|
||||
import { HUDCatMemes } from "./parts/cat_memes";
|
||||
import { HUDTutorialVideoOffer } from "./parts/tutorial_video_offer";
|
||||
import { HUDConstantSignalEdit } from "./parts/constant_signal_edit";
|
||||
import { enumNotificationType } from "./parts/notifications";
|
||||
import { HUDSettingsMenu } from "./parts/settings_menu";
|
||||
import { HUDVignetteOverlay } from "./parts/vignette_overlay";
|
||||
import { TrailerMaker } from "./trailer_maker";
|
||||
|
||||
export class GameHUD {
|
||||
/**
|
||||
@@ -76,33 +44,12 @@ export class GameHUD {
|
||||
|
||||
this.parts = {
|
||||
buildingsToolbar: new HUDBuildingsToolbar(this.root),
|
||||
wiresToolbar: new HUDWiresToolbar(this.root),
|
||||
blueprintPlacer: new HUDBlueprintPlacer(this.root),
|
||||
buildingPlacer: new HUDBuildingPlacer(this.root),
|
||||
unlockNotification: new HUDUnlockNotification(this.root),
|
||||
gameMenu: new HUDGameMenu(this.root),
|
||||
massSelector: new HUDMassSelector(this.root),
|
||||
shop: new HUDShop(this.root),
|
||||
statistics: new HUDStatistics(this.root),
|
||||
waypoints: new HUDWaypoints(this.root),
|
||||
wireInfo: new HUDWireInfo(this.root),
|
||||
leverToggle: new HUDLeverToggle(this.root),
|
||||
constantSignalEdit: new HUDConstantSignalEdit(this.root),
|
||||
|
||||
// Must always exist
|
||||
pinnedShapes: new HUDPinnedShapes(this.root),
|
||||
notifications: new HUDNotifications(this.root),
|
||||
settingsMenu: new HUDSettingsMenu(this.root),
|
||||
debugInfo: new HUDDebugInfo(this.root),
|
||||
dialogs: new HUDModalDialogs(this.root),
|
||||
screenshotExporter: new HUDScreenshotExporter(this.root),
|
||||
shapeViewer: new HUDShapeViewer(this.root),
|
||||
|
||||
wiresOverlay: new HUDWiresOverlay(this.root),
|
||||
layerPreview: new HUDLayerPreview(this.root),
|
||||
|
||||
minerHighlight: new HUDMinerHighlight(this.root),
|
||||
tutorialVideoOffer: new HUDTutorialVideoOffer(this.root),
|
||||
|
||||
// Typing hints
|
||||
/* typehints:start */
|
||||
@@ -111,29 +58,14 @@ export class GameHUD {
|
||||
/* typehints:end */
|
||||
};
|
||||
|
||||
if (!IS_MOBILE) {
|
||||
this.parts.keybindingOverlay = new HUDKeybindingOverlay(this.root);
|
||||
}
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.enableEntityInspector) {
|
||||
this.parts.entityDebugger = new HUDEntityDebugger(this.root);
|
||||
}
|
||||
|
||||
if (this.root.app.restrictionMgr.getIsStandaloneMarketingActive()) {
|
||||
this.parts.watermark = new HUDWatermark(this.root);
|
||||
this.parts.standaloneAdvantages = new HUDStandaloneAdvantages(this.root);
|
||||
this.parts.catMemes = new HUDCatMemes(this.root);
|
||||
}
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.renderChanges) {
|
||||
this.parts.changesDebugger = new HUDChangesDebugger(this.root);
|
||||
}
|
||||
|
||||
if (this.root.app.settings.getAllSettings().offerHints) {
|
||||
this.parts.tutorialHints = new HUDPartTutorialHints(this.root);
|
||||
this.parts.interactiveTutorial = new HUDInteractiveTutorial(this.root);
|
||||
}
|
||||
|
||||
if (this.root.app.settings.getAllSettings().vignette) {
|
||||
this.parts.vignetteOverlay = new HUDVignetteOverlay(this.root);
|
||||
}
|
||||
@@ -142,14 +74,15 @@ export class GameHUD {
|
||||
this.parts.colorBlindHelper = new HUDColorBlindHelper(this.root);
|
||||
}
|
||||
|
||||
if (queryParamOptions.sandboxMode || G_IS_DEV) {
|
||||
this.parts.sandboxController = new HUDSandboxController(this.root);
|
||||
}
|
||||
|
||||
if (!G_IS_RELEASE && !G_IS_DEV) {
|
||||
this.parts.betaOverlay = new HUDBetaOverlay(this.root);
|
||||
}
|
||||
|
||||
const additionalParts = this.root.gameMode.additionalHudParts;
|
||||
for (const [partId, part] of Object.entries(additionalParts)) {
|
||||
this.parts[partId] = new part(this.root);
|
||||
}
|
||||
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const key in this.parts) {
|
||||
this.parts[key].createElements(frag);
|
||||
|
||||
@@ -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";
|
||||
@@ -23,8 +27,8 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
||||
) {
|
||||
super(root);
|
||||
|
||||
this.primaryBuildings = primaryBuildings;
|
||||
this.secondaryBuildings = secondaryBuildings;
|
||||
this.primaryBuildings = this.filterBuildings(primaryBuildings);
|
||||
this.secondaryBuildings = this.filterBuildings(secondaryBuildings);
|
||||
this.visibilityCondition = visibilityCondition;
|
||||
this.htmlElementId = htmlElementId;
|
||||
this.layer = layer;
|
||||
@@ -35,6 +39,7 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
||||
* selected: boolean,
|
||||
* element: HTMLElement,
|
||||
* index: number
|
||||
* puzzleLocked: boolean;
|
||||
* }>} */
|
||||
this.buildingHandles = {};
|
||||
}
|
||||
@@ -47,6 +52,24 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
||||
this.element = makeDiv(parent, this.htmlElementId, ["ingame_buildingsToolbar"], "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<typeof MetaBuilding>} buildings
|
||||
* @returns {Array<typeof MetaBuilding>}
|
||||
*/
|
||||
filterBuildings(buildings) {
|
||||
const filtered = [];
|
||||
|
||||
for (let i = 0; i < buildings.length; i++) {
|
||||
if (this.root.gameMode.isBuildingExcluded(buildings[i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filtered.push(buildings[i]);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all buildings
|
||||
* @returns {Array<typeof MetaBuilding>}
|
||||
@@ -87,19 +110,31 @@ 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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,7 +162,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);
|
||||
@@ -216,6 +251,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];
|
||||
@@ -229,4 +272,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
|
||||
* @param {DrawParameters} parameters
|
||||
*/
|
||||
draw(parameters) {
|
||||
if (this.root.camera.zoomLevel < globalConfig.mapChunkOverviewMinZoom) {
|
||||
if (this.root.camera.getIsMapOverlayActive()) {
|
||||
// Dont allow placing in overview mode
|
||||
this.domAttach.update(false);
|
||||
this.variantsAttach.update(false);
|
||||
@@ -275,11 +275,13 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
|
||||
const worldPosition = this.root.camera.screenToWorld(mousePosition);
|
||||
|
||||
// Draw peeker
|
||||
this.root.hud.parts.layerPreview.renderPreview(
|
||||
parameters,
|
||||
worldPosition,
|
||||
1 / this.root.camera.zoomLevel
|
||||
);
|
||||
if (this.root.hud.parts.layerPreview) {
|
||||
this.root.hud.parts.layerPreview.renderPreview(
|
||||
parameters,
|
||||
worldPosition,
|
||||
1 / this.root.camera.zoomLevel
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -366,7 +366,8 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
|
||||
if (
|
||||
tileBelow &&
|
||||
this.root.app.settings.getAllSettings().pickMinerOnPatch &&
|
||||
this.root.currentLayer === "regular"
|
||||
this.root.currentLayer === "regular" &&
|
||||
this.root.gameMode.hasResources()
|
||||
) {
|
||||
this.currentMetaBuilding.set(gMetaBuildingRegistry.findByClass(MetaMinerBuilding));
|
||||
|
||||
@@ -390,6 +391,12 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disallow picking excluded buildings
|
||||
if (this.root.gameMode.isBuildingExcluded(extracted.metaClass)) {
|
||||
this.currentMetaBuilding.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the building we are picking is the same as the one we have, clear the cursor.
|
||||
if (
|
||||
this.currentMetaBuilding.get() &&
|
||||
@@ -430,7 +437,7 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
|
||||
* @param {Vector} tile
|
||||
*/
|
||||
tryPlaceCurrentBuildingAt(tile) {
|
||||
if (this.root.camera.zoomLevel < globalConfig.mapChunkOverviewMinZoom) {
|
||||
if (this.root.camera.getIsMapOverlayActive()) {
|
||||
// Dont allow placing in overview mode
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15,23 +15,28 @@ import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt";
|
||||
import { HUDBaseToolbar } from "./base_toolbar";
|
||||
import { MetaStorageBuilding } from "../../buildings/storage";
|
||||
import { MetaItemProducerBuilding } from "../../buildings/item_producer";
|
||||
import { queryParamOptions } from "../../../core/query_parameters";
|
||||
import { MetaConstantProducerBuilding } from "../../buildings/constant_producer";
|
||||
import { MetaGoalAcceptorBuilding } from "../../buildings/goal_acceptor";
|
||||
import { MetaBlockBuilding } from "../../buildings/block";
|
||||
|
||||
export class HUDBuildingsToolbar extends HUDBaseToolbar {
|
||||
constructor(root) {
|
||||
super(root, {
|
||||
primaryBuildings: [
|
||||
MetaConstantProducerBuilding,
|
||||
MetaGoalAcceptorBuilding,
|
||||
MetaBeltBuilding,
|
||||
MetaBalancerBuilding,
|
||||
MetaUndergroundBeltBuilding,
|
||||
MetaMinerBuilding,
|
||||
MetaBlockBuilding,
|
||||
MetaCutterBuilding,
|
||||
MetaRotaterBuilding,
|
||||
MetaStackerBuilding,
|
||||
MetaMixerBuilding,
|
||||
MetaPainterBuilding,
|
||||
MetaTrashBuilding,
|
||||
...(queryParamOptions.sandboxMode || G_IS_DEV ? [MetaItemProducerBuilding] : []),
|
||||
MetaItemProducerBuilding,
|
||||
],
|
||||
secondaryBuildings: [
|
||||
MetaStorageBuilding,
|
||||
|
||||
@@ -254,6 +254,13 @@ export class HUDKeybindingOverlay extends BaseHUDPart {
|
||||
condition: () => this.anythingSelectedOnMap,
|
||||
},
|
||||
|
||||
{
|
||||
// [SELECTION] Clear
|
||||
label: T.ingame.keybindingsOverlay.clearBelts,
|
||||
keys: [k.massSelect.massSelectClear],
|
||||
condition: () => this.anythingSelectedOnMap,
|
||||
},
|
||||
|
||||
{
|
||||
// Switch layers
|
||||
label: T.ingame.keybindingsOverlay.switchLayers,
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { Vector } from "../../../core/vector";
|
||||
import { STOP_PROPAGATION } from "../../../core/signal";
|
||||
import { DrawParameters } from "../../../core/draw_parameters";
|
||||
import { Entity } from "../../entity";
|
||||
import { Loader } from "../../../core/loader";
|
||||
import { globalConfig } from "../../../core/config";
|
||||
import { makeDiv, formatBigNumber, formatBigNumberFull } from "../../../core/utils";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
import { DrawParameters } from "../../../core/draw_parameters";
|
||||
import { createLogger } from "../../../core/logging";
|
||||
import { STOP_PROPAGATION } from "../../../core/signal";
|
||||
import { formatBigNumberFull } from "../../../core/utils";
|
||||
import { Vector } from "../../../core/vector";
|
||||
import { ACHIEVEMENTS } from "../../../platform/achievement_provider";
|
||||
import { enumMouseButton } from "../../camera";
|
||||
import { T } from "../../../translations";
|
||||
import { Blueprint } from "../../blueprint";
|
||||
import { enumMouseButton } from "../../camera";
|
||||
import { Component } from "../../component";
|
||||
import { Entity } from "../../entity";
|
||||
import { KEYMAPPINGS } from "../../key_action_mapper";
|
||||
import { THEME } from "../../theme";
|
||||
import { enumHubGoalRewards } from "../../tutorial_goals";
|
||||
import { Blueprint } from "../../blueprint";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
const logger = createLogger("hud/mass_selector");
|
||||
|
||||
@@ -33,12 +32,13 @@ export class HUDMassSelector extends BaseHUDPart {
|
||||
this.root.camera.movePreHandler.add(this.onMouseMove, this);
|
||||
this.root.camera.upPostHandler.add(this.onMouseUp, this);
|
||||
|
||||
this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).add(this.onBack, this);
|
||||
this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).addToTop(this.onBack, this);
|
||||
this.root.keyMapper
|
||||
.getBinding(KEYMAPPINGS.massSelect.confirmMassDelete)
|
||||
.add(this.confirmDelete, this);
|
||||
this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectCut).add(this.confirmCut, this);
|
||||
this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectCopy).add(this.startCopy, this);
|
||||
this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectClear).add(this.clearBelts, this);
|
||||
|
||||
this.root.hud.signals.selectedPlacementBuildingChanged.add(this.clearSelection, this);
|
||||
this.root.signals.editModeChanged.add(this.clearSelection, this);
|
||||
@@ -142,6 +142,16 @@ export class HUDMassSelector extends BaseHUDPart {
|
||||
}
|
||||
}
|
||||
|
||||
clearBelts() {
|
||||
for (const uid of this.selectedUids) {
|
||||
const entity = this.root.entityMgr.findByUid(uid);
|
||||
for (const component of Object.values(entity.components)) {
|
||||
/** @type {Component} */ (component).clear();
|
||||
}
|
||||
}
|
||||
this.selectedUids = new Set();
|
||||
}
|
||||
|
||||
confirmCut() {
|
||||
if (!this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) {
|
||||
this.root.hud.parts.dialogs.showInfo(
|
||||
|
||||
@@ -29,11 +29,14 @@ export class HUDModalDialogs extends BaseHUDPart {
|
||||
}
|
||||
|
||||
shouldPauseRendering() {
|
||||
return this.dialogStack.length > 0;
|
||||
// return this.dialogStack.length > 0;
|
||||
// @todo: Check if change this affects anything
|
||||
return false;
|
||||
}
|
||||
|
||||
shouldPauseGame() {
|
||||
return this.shouldPauseRendering();
|
||||
// @todo: Check if this change affects anything
|
||||
return false;
|
||||
}
|
||||
|
||||
createElements(parent) {
|
||||
@@ -139,8 +142,8 @@ export class HUDModalDialogs extends BaseHUDPart {
|
||||
}
|
||||
|
||||
// Returns method to be called when laoding finishd
|
||||
showLoadingDialog() {
|
||||
const dialog = new DialogLoading(this.app);
|
||||
showLoadingDialog(text = "") {
|
||||
const dialog = new DialogLoading(this.app, text);
|
||||
this.internalShowDialog(dialog);
|
||||
return this.closeDialog.bind(this, dialog);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export class HUDPinnedShapes extends BaseHUDPart {
|
||||
*/
|
||||
deserialize(data) {
|
||||
if (!data || !data.shapes || !Array.isArray(data.shapes)) {
|
||||
return "Invalid pinned shapes data";
|
||||
return "Invalid pinned shapes data: " + JSON.stringify(data);
|
||||
}
|
||||
this.pinnedShapes = data.shapes;
|
||||
}
|
||||
|
||||
21
src/js/game/hud/parts/puzzle_back_to_menu.js
Normal file
21
src/js/game/hud/parts/puzzle_back_to_menu.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
export class HUDPuzzleBackToMenu extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
const key = this.root.gameMode.getId();
|
||||
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzleBackToMenu");
|
||||
this.button = document.createElement("button");
|
||||
this.button.classList.add("button");
|
||||
this.element.appendChild(this.button);
|
||||
|
||||
this.trackClicks(this.button, this.back);
|
||||
}
|
||||
|
||||
initialize() {}
|
||||
|
||||
back() {
|
||||
this.root.gameState.goBackToMenu();
|
||||
}
|
||||
}
|
||||
112
src/js/game/hud/parts/puzzle_complete_notification.js
Normal file
112
src/js/game/hud/parts/puzzle_complete_notification.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/* typehints:start */
|
||||
import { PuzzlePlayGameMode } from "../../modes/puzzle_play";
|
||||
/* typehints:end */
|
||||
|
||||
import { InputReceiver } from "../../../core/input_receiver";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { SOUNDS } from "../../../platform/sound";
|
||||
import { T } from "../../../translations";
|
||||
import { enumColors } from "../../colors";
|
||||
import { ColorItem } from "../../items/color_item";
|
||||
import { finalGameShape, rocketShape } from "../../modes/regular";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
import { ShapeItem } from "../../items/shape_item";
|
||||
import { ShapeDefinition } from "../../shape_definition";
|
||||
|
||||
export class HUDPuzzleCompleteNotification extends BaseHUDPart {
|
||||
initialize() {
|
||||
this.visible = false;
|
||||
|
||||
this.domAttach = new DynamicDomAttach(this.root, this.element, {
|
||||
timeToKeepSeconds: 0,
|
||||
});
|
||||
|
||||
this.root.signals.puzzleComplete.add(this.show, this);
|
||||
|
||||
this.userDidLikePuzzle = false;
|
||||
this.timeOfCompletion = 0;
|
||||
}
|
||||
|
||||
createElements(parent) {
|
||||
this.inputReciever = new InputReceiver("puzzle-complete");
|
||||
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzleCompleteNotification", ["noBlur"]);
|
||||
|
||||
const dialog = makeDiv(this.element, null, ["dialog"]);
|
||||
|
||||
this.elemTitle = makeDiv(dialog, null, ["title"], T.ingame.puzzleCompletion.title);
|
||||
this.elemContents = makeDiv(dialog, null, ["contents"]);
|
||||
this.elemActions = makeDiv(dialog, null, ["actions"]);
|
||||
|
||||
const stepLike = makeDiv(this.elemContents, null, ["step", "stepLike"]);
|
||||
makeDiv(stepLike, null, ["title"], T.ingame.puzzleCompletion.titleLike);
|
||||
|
||||
const likeButtons = makeDiv(stepLike, null, ["buttons"]);
|
||||
|
||||
this.buttonLikeYes = document.createElement("button");
|
||||
this.buttonLikeYes.classList.add("liked-yes");
|
||||
likeButtons.appendChild(this.buttonLikeYes);
|
||||
this.trackClicks(this.buttonLikeYes, () => {
|
||||
this.userDidLikePuzzle = !this.userDidLikePuzzle;
|
||||
this.updateState();
|
||||
});
|
||||
|
||||
const buttonBar = document.createElement("div");
|
||||
buttonBar.classList.add("buttonBar");
|
||||
this.elemContents.appendChild(buttonBar);
|
||||
|
||||
this.continueBtn = document.createElement("button");
|
||||
this.continueBtn.classList.add("continue", "styledButton");
|
||||
this.continueBtn.innerText = T.ingame.puzzleCompletion.continueBtn;
|
||||
buttonBar.appendChild(this.continueBtn);
|
||||
this.trackClicks(this.continueBtn, () => {
|
||||
this.close(false);
|
||||
});
|
||||
|
||||
this.menuBtn = document.createElement("button");
|
||||
this.menuBtn.classList.add("menu", "styledButton");
|
||||
this.menuBtn.innerText = T.ingame.puzzleCompletion.menuBtn;
|
||||
buttonBar.appendChild(this.menuBtn);
|
||||
|
||||
this.trackClicks(this.menuBtn, () => {
|
||||
this.close(true);
|
||||
});
|
||||
}
|
||||
|
||||
updateState() {
|
||||
this.buttonLikeYes.classList.toggle("active", this.userDidLikePuzzle === true);
|
||||
}
|
||||
|
||||
show() {
|
||||
this.root.soundProxy.playUi(SOUNDS.levelComplete);
|
||||
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
|
||||
this.visible = true;
|
||||
this.timeOfCompletion = this.root.time.now();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
|
||||
}
|
||||
|
||||
isBlockingOverlay() {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
close(toMenu) {
|
||||
/** @type {PuzzlePlayGameMode} */ (this.root.gameMode)
|
||||
.trackCompleted(this.userDidLikePuzzle, Math.round(this.timeOfCompletion))
|
||||
.then(() => {
|
||||
if (toMenu) {
|
||||
this.root.gameState.moveToState("PuzzleMenuState");
|
||||
} else {
|
||||
this.visible = false;
|
||||
this.cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
this.domAttach.update(this.visible);
|
||||
}
|
||||
}
|
||||
13
src/js/game/hud/parts/puzzle_dlc_logo.js
Normal file
13
src/js/game/hud/parts/puzzle_dlc_logo.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
export class HUDPuzzleDLCLogo extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzleDLCLogo");
|
||||
parent.appendChild(this.element);
|
||||
}
|
||||
|
||||
initialize() {}
|
||||
|
||||
next() {}
|
||||
}
|
||||
18
src/js/game/hud/parts/puzzle_editor_controls.js
Normal file
18
src/js/game/hud/parts/puzzle_editor_controls.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { T } from "../../../translations";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
export class HUDPuzzleEditorControls extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzleEditorControls");
|
||||
|
||||
this.element.innerHTML = T.ingame.puzzleEditorControls.instructions
|
||||
.map(text => `<span>${text}</span>`)
|
||||
.join("");
|
||||
|
||||
this.titleElement = makeDiv(parent, "ingame_HUD_PuzzleEditorTitle");
|
||||
this.titleElement.innerText = T.ingame.puzzleEditorControls.title;
|
||||
}
|
||||
|
||||
initialize() {}
|
||||
}
|
||||
233
src/js/game/hud/parts/puzzle_editor_review.js
Normal file
233
src/js/game/hud/parts/puzzle_editor_review.js
Normal file
@@ -0,0 +1,233 @@
|
||||
import { globalConfig, THIRDPARTY_URLS } from "../../../core/config";
|
||||
import { createLogger } from "../../../core/logging";
|
||||
import { DialogWithForm } from "../../../core/modal_dialog_elements";
|
||||
import { FormElementInput, FormElementItemChooser } from "../../../core/modal_dialog_forms";
|
||||
import { STOP_PROPAGATION } from "../../../core/signal";
|
||||
import { fillInLinkIntoTranslation, makeDiv } from "../../../core/utils";
|
||||
import { PuzzleSerializer } from "../../../savegame/puzzle_serializer";
|
||||
import { T } from "../../../translations";
|
||||
import { ConstantSignalComponent } from "../../components/constant_signal";
|
||||
import { GoalAcceptorComponent } from "../../components/goal_acceptor";
|
||||
import { StaticMapEntityComponent } from "../../components/static_map_entity";
|
||||
import { ShapeItem } from "../../items/shape_item";
|
||||
import { ShapeDefinition } from "../../shape_definition";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
const trim = require("trim");
|
||||
const logger = createLogger("puzzle-review");
|
||||
|
||||
export class HUDPuzzleEditorReview extends BaseHUDPart {
|
||||
constructor(root) {
|
||||
super(root);
|
||||
}
|
||||
|
||||
createElements(parent) {
|
||||
const key = this.root.gameMode.getId();
|
||||
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzleEditorReview");
|
||||
this.button = document.createElement("button");
|
||||
this.button.classList.add("button");
|
||||
this.button.textContent = T.puzzleMenu.reviewPuzzle;
|
||||
this.element.appendChild(this.button);
|
||||
|
||||
this.trackClicks(this.button, this.startReview);
|
||||
}
|
||||
|
||||
initialize() {}
|
||||
|
||||
startReview() {
|
||||
const validationError = this.validatePuzzle();
|
||||
if (validationError) {
|
||||
this.root.hud.parts.dialogs.showWarning(T.puzzleMenu.validation.title, validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog(T.puzzleMenu.validatingPuzzle);
|
||||
|
||||
// Wait a bit, so the user sees the puzzle actually got validated
|
||||
setTimeout(() => {
|
||||
// Manually simulate ticks
|
||||
this.root.logic.clearAllBeltsAndItems();
|
||||
|
||||
const maxTicks =
|
||||
this.root.gameMode.getFixedTickrate() * globalConfig.puzzleValidationDurationSeconds;
|
||||
const deltaMs = this.root.dynamicTickrate.deltaMs;
|
||||
logger.log("Simulating up to", maxTicks, "ticks, start=", this.root.time.now().toFixed(1));
|
||||
const now = performance.now();
|
||||
|
||||
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",
|
||||
simulatedTicks,
|
||||
"ticks, end=",
|
||||
this.root.time.now().toFixed(1),
|
||||
"duration=",
|
||||
duration.toFixed(2),
|
||||
"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 (simulatedTicks == maxTicks && validationError) {
|
||||
this.root.hud.parts.dialogs.showWarning(T.puzzleMenu.validation.title, validationError);
|
||||
return;
|
||||
}
|
||||
this.startSubmit();
|
||||
}, 750);
|
||||
}
|
||||
|
||||
startSubmit(title = "", shortKey = "") {
|
||||
const regex = /^[a-zA-Z0-9_\- ]{4,20}$/;
|
||||
const nameInput = new FormElementInput({
|
||||
id: "nameInput",
|
||||
label: T.dialogs.submitPuzzle.descName,
|
||||
placeholder: T.dialogs.submitPuzzle.placeholderName,
|
||||
defaultValue: title,
|
||||
validator: val => trim(val).match(regex) && trim(val).length > 0,
|
||||
});
|
||||
|
||||
let items = new Set();
|
||||
const acceptors = this.root.entityMgr.getAllWithComponent(GoalAcceptorComponent);
|
||||
for (const acceptor of acceptors) {
|
||||
const item = acceptor.components.GoalAcceptor.item;
|
||||
if (item.getItemType() === "shape") {
|
||||
items.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
while (items.size < 8) {
|
||||
// add some randoms
|
||||
const item = this.root.hubGoals.computeFreeplayShape(Math.round(10 + Math.random() * 10000));
|
||||
items.add(new ShapeItem(item));
|
||||
}
|
||||
|
||||
const itemInput = new FormElementItemChooser({
|
||||
id: "signalItem",
|
||||
label: fillInLinkIntoTranslation(T.dialogs.submitPuzzle.descIcon, THIRDPARTY_URLS.shapeViewer),
|
||||
items: Array.from(items),
|
||||
});
|
||||
|
||||
const shapeKeyInput = new FormElementInput({
|
||||
id: "shapeKeyInput",
|
||||
label: null,
|
||||
placeholder: "CuCuCuCu",
|
||||
defaultValue: shortKey,
|
||||
validator: val => ShapeDefinition.isValidShortKey(trim(val)),
|
||||
});
|
||||
|
||||
const dialog = new DialogWithForm({
|
||||
app: this.root.app,
|
||||
title: T.dialogs.submitPuzzle.title,
|
||||
desc: "",
|
||||
formElements: [nameInput, itemInput, shapeKeyInput],
|
||||
buttons: ["ok:good:enter"],
|
||||
});
|
||||
|
||||
itemInput.valueChosen.add(value => {
|
||||
shapeKeyInput.setValue(value.definition.getHash());
|
||||
});
|
||||
|
||||
this.root.hud.parts.dialogs.internalShowDialog(dialog);
|
||||
|
||||
dialog.buttonSignals.ok.add(() => {
|
||||
const title = trim(nameInput.getValue());
|
||||
const shortKey = trim(shapeKeyInput.getValue());
|
||||
this.doSubmitPuzzle(title, shortKey);
|
||||
});
|
||||
}
|
||||
|
||||
doSubmitPuzzle(title, shortKey) {
|
||||
const serialized = new PuzzleSerializer().generateDumpFromGameRoot(this.root);
|
||||
|
||||
logger.log("Submitting puzzle, title=", title, "shortKey=", shortKey);
|
||||
if (G_IS_DEV) {
|
||||
logger.log("Serialized data:", serialized);
|
||||
}
|
||||
|
||||
const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog(T.puzzleMenu.submittingPuzzle);
|
||||
|
||||
this.root.app.clientApi
|
||||
.apiSubmitPuzzle({
|
||||
title,
|
||||
shortKey,
|
||||
data: serialized,
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
closeLoading();
|
||||
const { ok } = this.root.hud.parts.dialogs.showInfo(
|
||||
T.dialogs.puzzleSubmitOk.title,
|
||||
T.dialogs.puzzleSubmitOk.desc
|
||||
);
|
||||
ok.add(() => this.root.gameState.moveToState("PuzzleMenuState"));
|
||||
},
|
||||
err => {
|
||||
closeLoading();
|
||||
logger.warn("Failed to submit puzzle:", err);
|
||||
const signals = this.root.hud.parts.dialogs.showWarning(
|
||||
T.dialogs.puzzleSubmitError.title,
|
||||
T.dialogs.puzzleSubmitError.desc + " " + err,
|
||||
["cancel", "retry:good"]
|
||||
);
|
||||
signals.retry.add(() => this.startSubmit(title, shortKey));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
validatePuzzle() {
|
||||
// Check there is at least one constant producer and goal acceptor
|
||||
const producers = this.root.entityMgr.getAllWithComponent(ConstantSignalComponent);
|
||||
const acceptors = this.root.entityMgr.getAllWithComponent(GoalAcceptorComponent);
|
||||
|
||||
if (producers.length === 0) {
|
||||
return T.puzzleMenu.validation.noProducers;
|
||||
}
|
||||
|
||||
if (acceptors.length === 0) {
|
||||
return T.puzzleMenu.validation.noGoalAcceptors;
|
||||
}
|
||||
|
||||
// Check if all acceptors satisfy the constraints
|
||||
for (const acceptor of acceptors) {
|
||||
const goalComp = acceptor.components.GoalAcceptor;
|
||||
if (!goalComp.item) {
|
||||
return T.puzzleMenu.validation.goalAcceptorNoItem;
|
||||
}
|
||||
const required = goalComp.getRequiredDeliveryHistorySize();
|
||||
if (goalComp.deliveryHistory.length < required) {
|
||||
return T.puzzleMenu.validation.goalAcceptorRateNotMet;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all buildings are within the area
|
||||
const entities = this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent);
|
||||
for (const entity of entities) {
|
||||
if (this.root.systemMgr.systems.zone.prePlacementCheck(entity) === STOP_PROPAGATION) {
|
||||
return T.puzzleMenu.validation.buildingOutOfBounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
200
src/js/game/hud/parts/puzzle_editor_settings.js
Normal file
200
src/js/game/hud/parts/puzzle_editor_settings.js
Normal file
@@ -0,0 +1,200 @@
|
||||
/* typehints:start */
|
||||
import { PuzzleGameMode } from "../../modes/puzzle";
|
||||
/* typehints:end */
|
||||
|
||||
import { globalConfig } from "../../../core/config";
|
||||
import { createLogger } from "../../../core/logging";
|
||||
import { Rectangle } from "../../../core/rectangle";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { T } from "../../../translations";
|
||||
import { StaticMapEntityComponent } from "../../components/static_map_entity";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
const logger = createLogger("puzzle-editor");
|
||||
|
||||
export class HUDPuzzleEditorSettings extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzleEditorSettings");
|
||||
|
||||
if (this.root.gameMode.getBuildableZones()) {
|
||||
const bind = (selector, handler) =>
|
||||
this.trackClicks(this.element.querySelector(selector), handler);
|
||||
this.zone = makeDiv(
|
||||
this.element,
|
||||
null,
|
||||
["section", "zone"],
|
||||
`
|
||||
<label>${T.ingame.puzzleEditorSettings.zoneTitle}</label>
|
||||
|
||||
<div class="buttons">
|
||||
<div class="zoneWidth plusMinus">
|
||||
<label>${T.ingame.puzzleEditorSettings.zoneWidth}</label>
|
||||
<button class="styledButton minus">-</button>
|
||||
<span class="value"></span>
|
||||
<button class="styledButton plus">+</button>
|
||||
</div>
|
||||
|
||||
<div class="zoneHeight plusMinus">
|
||||
<label>${T.ingame.puzzleEditorSettings.zoneHeight}</label>
|
||||
<button class="styledButton minus">-</button>
|
||||
<span class="value"></span>
|
||||
<button class="styledButton plus">+</button>
|
||||
</div>
|
||||
|
||||
<div class="buttonBar">
|
||||
<button class="styledButton trim">${T.ingame.puzzleEditorSettings.trimZone}</button>
|
||||
<button class="styledButton clear">${T.ingame.puzzleEditorSettings.clearItems}</button>
|
||||
</div>
|
||||
</div>`
|
||||
);
|
||||
|
||||
bind(".zoneWidth .minus", () => this.modifyZone(-1, 0));
|
||||
bind(".zoneWidth .plus", () => this.modifyZone(1, 0));
|
||||
bind(".zoneHeight .minus", () => this.modifyZone(0, -1));
|
||||
bind(".zoneHeight .plus", () => this.modifyZone(0, 1));
|
||||
bind("button.trim", this.trim);
|
||||
bind("button.clear", this.clear);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.root.logic.clearAllBeltsAndItems();
|
||||
}
|
||||
|
||||
trim() {
|
||||
// Now, find the center
|
||||
const buildings = this.root.entityMgr.entities.slice();
|
||||
|
||||
if (buildings.length === 0) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
let minRect = null;
|
||||
|
||||
for (const building of buildings) {
|
||||
const staticComp = building.components.StaticMapEntity;
|
||||
const bounds = staticComp.getTileSpaceBounds();
|
||||
|
||||
if (!minRect) {
|
||||
minRect = bounds;
|
||||
} else {
|
||||
minRect = minRect.getUnion(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
const mode = /** @type {PuzzleGameMode} */ (this.root.gameMode);
|
||||
const moveByInverse = minRect.getCenter().round();
|
||||
|
||||
// move buildings
|
||||
if (moveByInverse.length() > 0) {
|
||||
// increase area size
|
||||
mode.zoneWidth = globalConfig.puzzleMaxBoundsSize;
|
||||
mode.zoneHeight = globalConfig.puzzleMaxBoundsSize;
|
||||
|
||||
// First, remove any items etc
|
||||
this.root.logic.clearAllBeltsAndItems();
|
||||
|
||||
this.root.logic.performImmutableOperation(() => {
|
||||
// 1. remove all buildings
|
||||
for (const building of buildings) {
|
||||
if (!this.root.logic.tryDeleteBuilding(building)) {
|
||||
assertAlways(false, "Failed to remove building in trim");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. place them again, but centered
|
||||
for (const building of buildings) {
|
||||
const staticComp = building.components.StaticMapEntity;
|
||||
const result = this.root.logic.tryPlaceBuilding({
|
||||
origin: staticComp.origin.sub(moveByInverse),
|
||||
building: staticComp.getMetaBuilding(),
|
||||
originalRotation: staticComp.originalRotation,
|
||||
rotation: staticComp.rotation,
|
||||
rotationVariant: staticComp.getRotationVariant(),
|
||||
variant: staticComp.getVariant(),
|
||||
});
|
||||
if (!result) {
|
||||
this.root.bulkOperationRunning = false;
|
||||
assertAlways(false, "Failed to re-place building in trim");
|
||||
}
|
||||
|
||||
if (building.components.ConstantSignal) {
|
||||
result.components.ConstantSignal.signal = building.components.ConstantSignal.signal;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Actually trim
|
||||
let w = mode.zoneWidth;
|
||||
let h = mode.zoneHeight;
|
||||
|
||||
while (!this.anyBuildingOutsideZone(w - 1, h)) {
|
||||
--w;
|
||||
}
|
||||
|
||||
while (!this.anyBuildingOutsideZone(w, h - 1)) {
|
||||
--h;
|
||||
}
|
||||
|
||||
mode.zoneWidth = w;
|
||||
mode.zoneHeight = h;
|
||||
this.updateZoneValues();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.visible = true;
|
||||
this.updateZoneValues();
|
||||
}
|
||||
|
||||
anyBuildingOutsideZone(width, height) {
|
||||
if (Math.min(width, height) < globalConfig.puzzleMinBoundsSize) {
|
||||
return true;
|
||||
}
|
||||
const newZone = Rectangle.centered(width, height);
|
||||
const entities = this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent);
|
||||
|
||||
for (const entity of entities) {
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const bounds = staticComp.getTileSpaceBounds();
|
||||
if (!newZone.intersectsFully(bounds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modifyZone(deltaW, deltaH) {
|
||||
const mode = /** @type {PuzzleGameMode} */ (this.root.gameMode);
|
||||
|
||||
const newWidth = mode.zoneWidth + deltaW;
|
||||
const newHeight = mode.zoneHeight + deltaH;
|
||||
|
||||
if (Math.min(newWidth, newHeight) < globalConfig.puzzleMinBoundsSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.max(newWidth, newHeight) > globalConfig.puzzleMaxBoundsSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.anyBuildingOutsideZone(newWidth, newHeight)) {
|
||||
this.root.hud.parts.dialogs.showWarning(
|
||||
T.dialogs.puzzleResizeBadBuildings.title,
|
||||
T.dialogs.puzzleResizeBadBuildings.desc
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
mode.zoneWidth = newWidth;
|
||||
mode.zoneHeight = newHeight;
|
||||
this.updateZoneValues();
|
||||
}
|
||||
|
||||
updateZoneValues() {
|
||||
const mode = /** @type {PuzzleGameMode} */ (this.root.gameMode);
|
||||
|
||||
this.element.querySelector(".zoneWidth > .value").textContent = String(mode.zoneWidth);
|
||||
this.element.querySelector(".zoneHeight > .value").textContent = String(mode.zoneHeight);
|
||||
}
|
||||
}
|
||||
72
src/js/game/hud/parts/puzzle_play_metadata.js
Normal file
72
src/js/game/hud/parts/puzzle_play_metadata.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/* typehints:start */
|
||||
import { PuzzlePlayGameMode } from "../../modes/puzzle_play";
|
||||
/* typehints:end */
|
||||
|
||||
import { formatBigNumberFull, formatSeconds, makeDiv } from "../../../core/utils";
|
||||
import { T } from "../../../translations";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
const copy = require("clipboard-copy");
|
||||
|
||||
export class HUDPuzzlePlayMetadata extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.titleElement = makeDiv(parent, "ingame_HUD_PuzzlePlayTitle");
|
||||
this.titleElement.innerText = "PUZZLE";
|
||||
|
||||
const mode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
|
||||
const puzzle = mode.puzzle;
|
||||
|
||||
this.puzzleNameElement = makeDiv(this.titleElement, null, ["name"]);
|
||||
this.puzzleNameElement.innerText = puzzle.meta.title;
|
||||
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzlePlayMetadata");
|
||||
this.element.innerHTML = `
|
||||
|
||||
<div class="plays">
|
||||
<span class="downloads">${formatBigNumberFull(puzzle.meta.downloads)}</span>
|
||||
<span class="likes">${formatBigNumberFull(puzzle.meta.likes)}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="info author"><label>${T.ingame.puzzleMetadata.author}</label><span></span></div>
|
||||
<div class="info key">
|
||||
<label>${T.ingame.puzzleMetadata.shortKey}</label><span>${puzzle.meta.shortKey}</span>
|
||||
</div>
|
||||
<div class="info rating">
|
||||
<label>${T.ingame.puzzleMetadata.averageDuration}</label>
|
||||
<span>${puzzle.meta.averageTime ? formatSeconds(puzzle.meta.averageTime) : "-"}</span>
|
||||
</div>
|
||||
<div class="info rating">
|
||||
<label>${T.ingame.puzzleMetadata.completionRate}</label>
|
||||
<span>${
|
||||
puzzle.meta.downloads > 0
|
||||
? ((puzzle.meta.completions / puzzle.meta.downloads) * 100.0).toFixed(1) + "%"
|
||||
: "-"
|
||||
}</span>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button class="styledButton share">${T.ingame.puzzleEditorSettings.share}</button>
|
||||
<button class="styledButton report">${T.ingame.puzzleEditorSettings.report}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.trackClicks(this.element.querySelector("button.share"), this.share);
|
||||
this.trackClicks(this.element.querySelector("button.report"), this.report);
|
||||
|
||||
/** @type {HTMLElement} */ (this.element.querySelector(".author span")).innerText =
|
||||
puzzle.meta.author;
|
||||
}
|
||||
|
||||
initialize() {}
|
||||
|
||||
share() {
|
||||
const mode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
|
||||
mode.sharePuzzle();
|
||||
}
|
||||
|
||||
report() {
|
||||
const mode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
|
||||
mode.reportPuzzle();
|
||||
}
|
||||
}
|
||||
36
src/js/game/hud/parts/puzzle_play_settings.js
Normal file
36
src/js/game/hud/parts/puzzle_play_settings.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createLogger } from "../../../core/logging";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { T } from "../../../translations";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
const logger = createLogger("puzzle-play");
|
||||
|
||||
export class HUDPuzzlePlaySettings extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzlePlaySettings");
|
||||
|
||||
if (this.root.gameMode.getBuildableZones()) {
|
||||
const bind = (selector, handler) =>
|
||||
this.trackClicks(this.element.querySelector(selector), handler);
|
||||
makeDiv(
|
||||
this.element,
|
||||
null,
|
||||
["section"],
|
||||
`
|
||||
<button class="styledButton clear">${T.ingame.puzzleEditorSettings.clearItems}</button>
|
||||
|
||||
`
|
||||
);
|
||||
|
||||
bind("button.clear", this.clear);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.root.logic.clearAllBeltsAndItems();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.visible = true;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { queryParamOptions } from "../../../core/query_parameters";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
@@ -19,25 +20,25 @@ export class HUDSandboxController extends BaseHUDPart {
|
||||
<button class="styledButton minus">-</button>
|
||||
<button class="styledButton plus">+</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="upgradesBelt plusMinus">
|
||||
<label>Upgrades → Belt</label>
|
||||
<button class="styledButton minus">-</button>
|
||||
<button class="styledButton plus">+</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="upgradesExtraction plusMinus">
|
||||
<label>Upgrades → Extraction</label>
|
||||
<button class="styledButton minus">-</button>
|
||||
<button class="styledButton plus">+</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="upgradesProcessing plusMinus">
|
||||
<label>Upgrades → Processing</label>
|
||||
<button class="styledButton minus">-</button>
|
||||
<button class="styledButton plus">+</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="upgradesPainting plusMinus">
|
||||
<label>Upgrades → Painting</label>
|
||||
<button class="styledButton minus">-</button>
|
||||
@@ -117,7 +118,9 @@ export class HUDSandboxController extends BaseHUDPart {
|
||||
// Clear all shapes of this level
|
||||
hubGoals.storedShapes[hubGoals.currentGoal.definition.getHash()] = 0;
|
||||
|
||||
this.root.hud.parts.pinnedShapes.rerenderFull();
|
||||
if (this.root.hud.parts.pinnedShapes) {
|
||||
this.root.hud.parts.pinnedShapes.rerenderFull();
|
||||
}
|
||||
|
||||
// Compute gained rewards
|
||||
hubGoals.gainedRewards = {};
|
||||
@@ -144,7 +147,7 @@ export class HUDSandboxController extends BaseHUDPart {
|
||||
}
|
||||
});
|
||||
|
||||
this.visible = !G_IS_DEV;
|
||||
this.visible = false;
|
||||
this.domAttach = new DynamicDomAttach(this.root, this.element);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,17 +13,19 @@ export class HUDSettingsMenu extends BaseHUDPart {
|
||||
|
||||
this.menuElement = makeDiv(this.background, null, ["menuElement"]);
|
||||
|
||||
this.statsElement = makeDiv(
|
||||
this.background,
|
||||
null,
|
||||
["statsElement"],
|
||||
`
|
||||
if (this.root.gameMode.hasHub()) {
|
||||
this.statsElement = makeDiv(
|
||||
this.background,
|
||||
null,
|
||||
["statsElement"],
|
||||
`
|
||||
<strong>${T.ingame.settingsMenu.beltsPlaced}</strong><span class="beltsPlaced"></span>
|
||||
<strong>${T.ingame.settingsMenu.buildingsPlaced}</strong><span class="buildingsPlaced"></span>
|
||||
<strong>${T.ingame.settingsMenu.playtime}</strong><span class="playtime"></span>
|
||||
|
||||
`
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
this.buttonContainer = makeDiv(this.menuElement, null, ["buttons"]);
|
||||
|
||||
@@ -94,23 +96,25 @@ export class HUDSettingsMenu extends BaseHUDPart {
|
||||
|
||||
const totalMinutesPlayed = Math.ceil(this.root.time.now() / 60);
|
||||
|
||||
/** @type {HTMLElement} */
|
||||
const playtimeElement = this.statsElement.querySelector(".playtime");
|
||||
/** @type {HTMLElement} */
|
||||
const buildingsPlacedElement = this.statsElement.querySelector(".buildingsPlaced");
|
||||
/** @type {HTMLElement} */
|
||||
const beltsPlacedElement = this.statsElement.querySelector(".beltsPlaced");
|
||||
if (this.root.gameMode.hasHub()) {
|
||||
/** @type {HTMLElement} */
|
||||
const playtimeElement = this.statsElement.querySelector(".playtime");
|
||||
/** @type {HTMLElement} */
|
||||
const buildingsPlacedElement = this.statsElement.querySelector(".buildingsPlaced");
|
||||
/** @type {HTMLElement} */
|
||||
const beltsPlacedElement = this.statsElement.querySelector(".beltsPlaced");
|
||||
|
||||
playtimeElement.innerText = T.global.time.xMinutes.replace("<x>", `${totalMinutesPlayed}`);
|
||||
playtimeElement.innerText = T.global.time.xMinutes.replace("<x>", `${totalMinutesPlayed}`);
|
||||
|
||||
buildingsPlacedElement.innerText = formatBigNumberFull(
|
||||
this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length -
|
||||
buildingsPlacedElement.innerText = formatBigNumberFull(
|
||||
this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length -
|
||||
this.root.entityMgr.getAllWithComponent(BeltComponent).length
|
||||
);
|
||||
|
||||
beltsPlacedElement.innerText = formatBigNumberFull(
|
||||
this.root.entityMgr.getAllWithComponent(BeltComponent).length
|
||||
);
|
||||
|
||||
beltsPlacedElement.innerText = formatBigNumberFull(
|
||||
this.root.entityMgr.getAllWithComponent(BeltComponent).length
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
|
||||
@@ -100,16 +100,14 @@ export class HUDWaypoints extends BaseHUDPart {
|
||||
|
||||
this.directionIndicatorSprite = Loader.getSprite("sprites/misc/hub_direction_indicator.png");
|
||||
|
||||
/** @type {Array<Waypoint>}
|
||||
*/
|
||||
this.waypoints = [
|
||||
{
|
||||
label: null,
|
||||
center: { x: 0, y: 0 },
|
||||
zoomLevel: 3,
|
||||
layer: gMetaBuildingRegistry.findByClass(MetaHubBuilding).getLayer(),
|
||||
},
|
||||
];
|
||||
/** @type {Array<Waypoint>} */
|
||||
this.waypoints = [];
|
||||
this.waypoints.push({
|
||||
label: null,
|
||||
center: { x: 0, y: 0 },
|
||||
zoomLevel: 3,
|
||||
layer: gMetaBuildingRegistry.findByClass(MetaHubBuilding).getLayer(),
|
||||
});
|
||||
|
||||
// Create a buffer we can use to measure text
|
||||
this.dummyBuffer = makeOffscreenBuffer(1, 1, {
|
||||
|
||||
@@ -28,6 +28,9 @@ export class HUDWiresOverlay extends BaseHUDPart {
|
||||
* Switches between layers
|
||||
*/
|
||||
switchLayers() {
|
||||
if (!this.root.gameMode.getSupportsWires()) {
|
||||
return;
|
||||
}
|
||||
if (this.root.currentLayer === "regular") {
|
||||
if (
|
||||
this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers) ||
|
||||
|
||||
Reference in New Issue
Block a user