1
0
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:
tobspr
2021-05-23 16:32:05 +02:00
committed by GitHub
parent 5f0a95ba11
commit 931c8a5821
167 changed files with 14001 additions and 8193 deletions

View File

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

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

View File

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

View File

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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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(

View File

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

View File

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

View 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();
}
}

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

View 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() {}
}

View 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() {}
}

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

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

View 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();
}
}

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

View File

@@ -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 &rarr; Belt</label>
<button class="styledButton minus">-</button>
<button class="styledButton plus">+</button>
</div>
<div class="upgradesExtraction plusMinus">
<label>Upgrades &rarr; Extraction</label>
<button class="styledButton minus">-</button>
<button class="styledButton plus">+</button>
</div>
<div class="upgradesProcessing plusMinus">
<label>Upgrades &rarr; Processing</label>
<button class="styledButton minus">-</button>
<button class="styledButton plus">+</button>
</div>
<div class="upgradesPainting plusMinus">
<label>Upgrades &rarr; 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);
}

View File

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

View File

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

View File

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