From b732db58b9eeac60c0e141930f33f5aa038e10b2 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 30 Apr 2021 14:34:11 +0200 Subject: [PATCH] Puzzle mode, part 3 --- res_raw/sprites/buildings/goal_acceptor.png | Bin 15770 -> 14771 bytes src/css/ingame_hud/dialogs.scss | 13 ++ src/css/ingame_hud/mode_menu_next.scss | 2 +- src/css/main.scss | 4 +- src/js/core/config.js | 4 + src/js/core/modal_dialog_elements.js | 15 +- src/js/core/modal_dialog_forms.js | 5 + src/js/game/buildings/balancer.js | 12 ++ src/js/game/buildings/goal_acceptor.js | 8 +- src/js/game/camera.js | 4 +- src/js/game/components/goal_acceptor.js | 27 ++- src/js/game/core.js | 6 +- src/js/game/dynamic_tickrate.js | 16 +- src/js/game/game_mode.js | 15 ++ src/js/game/hub_goals.js | 12 +- src/js/game/hud/hud.js | 4 +- src/js/game/hud/parts/modal_dialogs.js | 11 +- src/js/game/hud/parts/mode_menu_next.js | 23 --- src/js/game/hud/parts/mode_puzzle_review.js | 167 ++++++++++++++++++ .../game/hud/parts/puzzle_editor_controls.js | 5 +- src/js/game/map_chunk_view.js | 1 + src/js/game/modes/puzzle.js | 12 ++ src/js/game/modes/puzzle_edit.js | 4 + src/js/game/modes/regular.js | 6 +- src/js/game/systems/constant_producer.js | 17 +- src/js/game/systems/constant_signal.js | 34 ++-- src/js/game/systems/goal_acceptor.js | 165 ++++++++--------- src/js/game/systems/item_processor.js | 16 +- src/js/savegame/puzzle_serializer.js | 18 ++ translations/base-en.yaml | 43 ++--- 30 files changed, 478 insertions(+), 191 deletions(-) delete mode 100644 src/js/game/hud/parts/mode_menu_next.js create mode 100644 src/js/game/hud/parts/mode_puzzle_review.js create mode 100644 src/js/savegame/puzzle_serializer.js diff --git a/res_raw/sprites/buildings/goal_acceptor.png b/res_raw/sprites/buildings/goal_acceptor.png index 2ebf879a242cfe1688eebb896124e862f8dc3897..17fa224f1ab3196717bc84f9abe958fa960719bf 100644 GIT binary patch delta 14118 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 LU9M8bJm~)bP-53i literal 15770 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 diff --git a/src/css/ingame_hud/dialogs.scss b/src/css/ingame_hud/dialogs.scss index ad3f76d0..cc742d42 100644 --- a/src/css/ingame_hud/dialogs.scss +++ b/src/css/ingame_hud/dialogs.scss @@ -67,6 +67,14 @@ * { color: #fff; } + + display: flex; + flex-direction: column; + + .text { + text-transform: uppercase; + @include S(margin-bottom, 10px); + } } > .dialogInner { @@ -168,6 +176,11 @@ &.errored { background-color: rgb(250, 206, 206); + + &::placeholder { + color: #fff; + opacity: 0.8; + } } } diff --git a/src/css/ingame_hud/mode_menu_next.scss b/src/css/ingame_hud/mode_menu_next.scss index 2deb4965..02a6ec66 100644 --- a/src/css/ingame_hud/mode_menu_next.scss +++ b/src/css/ingame_hud/mode_menu_next.scss @@ -1,4 +1,4 @@ -#ingame_HUD_ModeMenuNext { +#ingame_HUD_PuzzleReview { position: absolute; @include S(top, 15px); @include S(right, 10px); diff --git a/src/css/main.scss b/src/css/main.scss index d703663c..9c027403 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -80,7 +80,7 @@ ingame_HUD_PinnedShapes, ingame_HUD_GameMenu, ingame_HUD_KeybindingOverlay, ingame_HUD_ModeMenuBack, -ingame_HUD_ModeMenuNext, +ingame_HUD_PuzzleReview, ingame_HUD_PuzzleEditorControls, ingame_HUD_PuzzleEditorTitle, ingame_HUD_ModeMenu, @@ -128,7 +128,7 @@ body.uiHidden { #ingame_HUD_GameMenu, #ingame_HUD_PinnedShapes, #ingame_HUD_ModeMenuBack, - #ingame_HUD_ModeMenuNext, + #ingame_HUD_PuzzleReview, #ingame_HUD_Notifications, #ingame_HUD_TutorialHints, #ingame_HUD_Waypoints, diff --git a/src/js/core/config.js b/src/js/core/config.js index d5dc7089..9a9a0a3e 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -71,6 +71,10 @@ export const globalConfig = { readerAnalyzeIntervalSeconds: 10, + goalAcceptorMinimumDurationSeconds: G_IS_DEV ? 1 : 5, + goalAcceptorsPerProducer: G_IS_DEV ? 4 : 4, + puzzleModeSpeed: 3, + buildingSpeeds: { cutter: 1 / 4, cutterQuad: 1 / 4, diff --git a/src/js/core/modal_dialog_elements.js b/src/js/core/modal_dialog_elements.js index 5f0ed59f..ee552aa9 100644 --- a/src/js/core/modal_dialog_elements.js +++ b/src/js/core/modal_dialog_elements.js @@ -267,7 +267,7 @@ export class Dialog { * Dialog which simply shows a loading spinner */ export class DialogLoading extends Dialog { - constructor(app) { + constructor(app, text = "") { super({ app, title: "", @@ -279,6 +279,8 @@ export class DialogLoading extends Dialog { // Loading dialog can not get closed with back button this.inputReciever.backButton.removeAll(); this.inputReciever.context = "dialog-loading"; + + this.text = text; } createElement() { @@ -287,6 +289,13 @@ export class DialogLoading extends Dialog { elem.classList.add("loadingDialog"); this.element = elem; + if (this.text) { + const text = document.createElement("div"); + text.classList.add("text"); + text.innerText = this.text; + elem.appendChild(text); + } + const loader = document.createElement("div"); loader.classList.add("prefab_LoadingTextWithAnim"); loader.classList.add("loadingIndicator"); @@ -309,7 +318,7 @@ export class DialogOptionChooser extends Dialog {
- ${iconHtml} + ${iconHtml} ${text} ${descHtml}
@@ -444,7 +453,7 @@ export class DialogWithForm extends Dialog { for (let i = 0; i < this.formElements.length; ++i) { const elem = this.formElements[i]; elem.bindEvents(div, this.clickDetectors); - elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested); + // elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested); elem.valueChosen.add(this.valueChosen.dispatch, this.valueChosen); } diff --git a/src/js/core/modal_dialog_forms.js b/src/js/core/modal_dialog_forms.js index 1c5b1986..aac81d82 100644 --- a/src/js/core/modal_dialog_forms.js +++ b/src/js/core/modal_dialog_forms.js @@ -117,6 +117,11 @@ export class FormElementInput extends FormElement { return this.element.value; } + setValue(value) { + this.element.value = value; + this.updateErrorState(); + } + focus() { this.element.focus(); } diff --git a/src/js/game/buildings/balancer.js b/src/js/game/buildings/balancer.js index 2f14e36c..99c7e44f 100644 --- a/src/js/game/buildings/balancer.js +++ b/src/js/game/buildings/balancer.js @@ -98,6 +98,18 @@ export class MetaBalancerBuilding extends MetaBuilding { available.push(enumBalancerVariants.splitter, enumBalancerVariants.splitterInverse); } + if (root.gameMode.getIsDeterministic()) { + // mergers are not deterministic + available = available.filter( + v => + ![ + enumBalancerVariants.merger, + enumBalancerVariants.mergerInverse, + defaultBuildingVariant, + ].includes(v) + ); + } + return available; } diff --git a/src/js/game/buildings/goal_acceptor.js b/src/js/game/buildings/goal_acceptor.js index bb50cd47..7e89f74a 100644 --- a/src/js/game/buildings/goal_acceptor.js +++ b/src/js/game/buildings/goal_acceptor.js @@ -29,7 +29,7 @@ export class MetaGoalAcceptorBuilding extends MetaBuilding { slots: [ { pos: new Vector(0, 0), - directions: [enumDirection.top], + directions: [enumDirection.bottom], }, ], }) @@ -41,12 +41,6 @@ export class MetaGoalAcceptorBuilding extends MetaBuilding { }) ); - entity.addComponent( - new BeltReaderComponent({ - type: enumBeltReaderType.wireless, - }) - ); - entity.addComponent(new GoalAcceptorComponent({})); } } diff --git a/src/js/game/camera.js b/src/js/game/camera.js index d59f1059..fc90f4de 100644 --- a/src/js/game/camera.js +++ b/src/js/game/camera.js @@ -393,11 +393,11 @@ export class Camera extends BasicSerializableObject { } getMaximumZoom() { - return this.root.gameMode.getMaximumZoom() * this.root.app.platformWrapper.getScreenScale(); + return this.root.gameMode.getMaximumZoom(); } getMinimumZoom() { - return this.root.gameMode.getMinimumZoom() * this.root.app.platformWrapper.getScreenScale(); + return this.root.gameMode.getMinimumZoom(); } /** diff --git a/src/js/game/components/goal_acceptor.js b/src/js/game/components/goal_acceptor.js index 869dd3f6..e0e53914 100644 --- a/src/js/game/components/goal_acceptor.js +++ b/src/js/game/components/goal_acceptor.js @@ -1,11 +1,19 @@ +import { globalConfig } from "../../core/config"; import { BaseItem } from "../base_item"; import { Component } from "../component"; +import { typeItemSingleton } from "../item_resolver"; export class GoalAcceptorComponent extends Component { static getId() { return "GoalAcceptor"; } + static getSchema() { + return { + item: typeItemSingleton, + }; + } + /** * @param {object} param0 * @param {BaseItem=} param0.item @@ -13,8 +21,25 @@ export class GoalAcceptorComponent extends Component { */ constructor({ item = null, rate = null }) { super(); + + // ths item to produce + /** @type {BaseItem | undefined} */ this.item = item; - this.achieved = false; + // the last items we delivered + /** @type {{ item: BaseItem; time: number; }[]} */ + this.deliveryHistory = []; + + // Used for animations + this.displayPercentage = 0; + } + + getRequiredDeliveryHistorySize() { + return ( + (globalConfig.puzzleModeSpeed * + globalConfig.goalAcceptorMinimumDurationSeconds * + globalConfig.beltSpeedItemsPerSecond) / + globalConfig.goalAcceptorsPerProducer + ); } } diff --git a/src/js/game/core.js b/src/js/game/core.js index 3333d1da..383a08dc 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -102,12 +102,12 @@ export class GameCore { // This isn't nice, but we need it right here root.keyMapper = new KeyActionMapper(root, this.root.gameState.inputReciever); - // Needs to come first - root.dynamicTickrate = new DynamicTickrate(root); - // Init game mode root.gameMode = GameMode.create(root, gameModeId); + // Needs to come first + root.dynamicTickrate = new DynamicTickrate(root); + // Init classes root.camera = new Camera(root); root.map = new MapView(root); diff --git a/src/js/game/dynamic_tickrate.js b/src/js/game/dynamic_tickrate.js index 3e29aba3..c76fa2e1 100644 --- a/src/js/game/dynamic_tickrate.js +++ b/src/js/game/dynamic_tickrate.js @@ -23,10 +23,16 @@ export class DynamicTickrate { this.averageFps = 60; - this.setTickRate(this.root.app.settings.getDesiredFps()); + const fixedRate = this.root.gameMode.getFixedTickrate(); + if (fixedRate) { + logger.log("Setting fixed tickrate of", fixedRate); + this.setTickRate(fixedRate); + } else { + this.setTickRate(this.root.app.settings.getDesiredFps()); - if (G_IS_DEV && globalConfig.debug.renderForTrailer) { - this.setTickRate(300); + if (G_IS_DEV && globalConfig.debug.renderForTrailer) { + this.setTickRate(300); + } } } @@ -99,9 +105,7 @@ export class DynamicTickrate { this.averageTickDuration = average; - const desiredFps = this.root.app.settings.getDesiredFps(); - - // Disabled for now: Dynamicall adjusting tick rate + // Disabled for now: Dynamically adjusting tick rate // if (this.averageFps > desiredFps * 0.9) { // // if (average < maxTickDuration) { // this.increaseTickRate(); diff --git a/src/js/game/game_mode.js b/src/js/game/game_mode.js index b9d830d3..f90981a9 100644 --- a/src/js/game/game_mode.js +++ b/src/js/game/game_mode.js @@ -172,6 +172,21 @@ export class GameMode extends BasicSerializableObject { return true; } + /** @returns {boolean} */ + getIsDeterministic() { + return false; + } + + /** @returns {boolean} */ + getIsEditor() { + return false; + } + + /** @returns {number | undefined} */ + getFixedTickrate() { + return; + } + /** @returns {string} */ getBlueprintShapeKey() { return "CbCbCbRb:CwCwCwCw"; diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index fee1bd79..4ebd5cc7 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -184,10 +184,6 @@ export class HubGoals extends BasicSerializableObject { * @param {string} upgradeId */ getUpgradeLevel(upgradeId) { - if (this.root.gameMode.throughputDoesNotMatter()) { - return 10; - } - return this.upgradeLevels[upgradeId] || 0; } @@ -481,7 +477,7 @@ export class HubGoals extends BasicSerializableObject { */ getBeltBaseSpeed() { if (this.root.gameMode.throughputDoesNotMatter()) { - return globalConfig.beltSpeedItemsPerSecond * 5; + return globalConfig.beltSpeedItemsPerSecond * globalConfig.puzzleModeSpeed; } return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt; } @@ -492,7 +488,7 @@ export class HubGoals extends BasicSerializableObject { */ getUndergroundBeltBaseSpeed() { if (this.root.gameMode.throughputDoesNotMatter()) { - return globalConfig.beltSpeedItemsPerSecond * 5; + return globalConfig.beltSpeedItemsPerSecond * globalConfig.puzzleModeSpeed; } return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt; } @@ -503,7 +499,7 @@ export class HubGoals extends BasicSerializableObject { */ getMinerBaseSpeed() { if (this.root.gameMode.throughputDoesNotMatter()) { - return globalConfig.minerSpeedItemsPerSecond * 5; + return globalConfig.minerSpeedItemsPerSecond * globalConfig.puzzleModeSpeed; } return globalConfig.minerSpeedItemsPerSecond * this.upgradeImprovements.miner; } @@ -515,7 +511,7 @@ export class HubGoals extends BasicSerializableObject { */ getProcessorBaseSpeed(processorType) { if (this.root.gameMode.throughputDoesNotMatter()) { - return 10; + return globalConfig.beltSpeedItemsPerSecond * globalConfig.puzzleModeSpeed * 10; } switch (processorType) { diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index 3b6192fc..e7a0d8fc 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -25,7 +25,7 @@ import { HUDMinerHighlight } from "./parts/miner_highlight"; import { HUDModalDialogs } from "./parts/modal_dialogs"; import { HUDModeMenu } from "./parts/mode_menu"; import { HUDModeMenuBack } from "./parts/mode_menu_back"; -import { HUDModeMenuNext } from "./parts/mode_menu_next"; +import { HUDPuzzleReview } from "./parts/mode_puzzle_review"; import { HUDModeSettings } from "./parts/mode_settings"; import { enumNotificationType, HUDNotifications } from "./parts/notifications"; import { HUDPinnedShapes } from "./parts/pinned_shapes"; @@ -88,7 +88,7 @@ export class GameHUD { leverToggle: HUDLeverToggle, constantSignalEdit: HUDConstantSignalEdit, modeMenuBack: HUDModeMenuBack, - modeMenuNext: HUDModeMenuNext, + PuzzleReview: HUDPuzzleReview, modeMenu: HUDModeMenu, modeSettings: HUDModeSettings, puzzleDlcLogo: HUDPuzzleDLCLogo, diff --git a/src/js/game/hud/parts/modal_dialogs.js b/src/js/game/hud/parts/modal_dialogs.js index 263b23dd..a43260e3 100644 --- a/src/js/game/hud/parts/modal_dialogs.js +++ b/src/js/game/hud/parts/modal_dialogs.js @@ -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); } diff --git a/src/js/game/hud/parts/mode_menu_next.js b/src/js/game/hud/parts/mode_menu_next.js deleted file mode 100644 index 6453a793..00000000 --- a/src/js/game/hud/parts/mode_menu_next.js +++ /dev/null @@ -1,23 +0,0 @@ -import { BaseHUDPart } from "../base_hud_part"; -import { makeDiv } from "../../../core/utils"; -import { T } from "../../../translations"; - -export class HUDModeMenuNext extends BaseHUDPart { - createElements(parent) { - const key = this.root.gameMode.getId(); - - this.element = makeDiv(parent, "ingame_HUD_ModeMenuNext"); - this.button = document.createElement("button"); - this.button.classList.add("button"); - this.button.textContent = T.ingame.modeMenu[key].next.title; - this.element.appendChild(this.button); - - this.content = makeDiv(this.element, null, ["content"], T.ingame.modeMenu[key].next.desc); - - this.trackClicks(this.button, this.next); - } - - initialize() {} - - next() {} -} diff --git a/src/js/game/hud/parts/mode_puzzle_review.js b/src/js/game/hud/parts/mode_puzzle_review.js new file mode 100644 index 00000000..293ed74b --- /dev/null +++ b/src/js/game/hud/parts/mode_puzzle_review.js @@ -0,0 +1,167 @@ +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 { 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 { 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 HUDPuzzleReview extends BaseHUDPart { + constructor(root) { + super(root); + + this.validationEndsIn = null; + this.callOnceValidationEnded = null; + } + + createElements(parent) { + const key = this.root.gameMode.getId(); + + this.element = makeDiv(parent, "ingame_HUD_PuzzleReview"); + 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.validtingPuzzle); + this.validationEndsIn = this.root.time.now() + globalConfig.goalAcceptorMinimumDurationSeconds; + this.callOnceValidationEnded = () => { + closeLoading(); + const validationError = this.validatePuzzle(); + if (validationError) { + this.root.hud.parts.dialogs.showWarning(T.puzzleMenu.validation.title, validationError); + return; + } + this.startSubmit(); + }; + } + + startSubmit() { + const regex = /^[a-zA-Z0-9_\- ]{1,20}$/; + const nameInput = new FormElementInput({ + id: "nameInput", + label: T.dialogs.submitPuzzle.descName, + placeholder: T.dialogs.submitPuzzle.placeholderName, + defaultValue: "", + validator: val => 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: "", + 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: ["cancel:bad:escape", "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); + logger.log("Serialized data:", serialized); + + const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog(T.puzzleMenu.submittingPuzzle); + + // @todo + } + + update() { + if ( + this.validationEndsIn && + this.validationEndsIn < this.root.time.now() && + this.callOnceValidationEnded + ) { + const callMethod = this.callOnceValidationEnded; + this.callOnceValidationEnded = null; + callMethod(); + } + } + + 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; + } + } + } +} diff --git a/src/js/game/hud/parts/puzzle_editor_controls.js b/src/js/game/hud/parts/puzzle_editor_controls.js index 77d609b7..2968e107 100644 --- a/src/js/game/hud/parts/puzzle_editor_controls.js +++ b/src/js/game/hud/parts/puzzle_editor_controls.js @@ -8,9 +8,8 @@ export class HUDPuzzleEditorControls extends BaseHUDPart { this.element.innerHTML = ` 1. Build constant producers to generate resources. - 2. Build goal acceptors the capture shapes. - 3. Produce your desired shape(s) within the puzzle area and deliver it to the goal acceptors, which will capture it. - 4. Once you are done, press 'Playtest' to validate your puzzle. + 2. Build goal acceptors and deliver shapes to set the puzzle goals. + 3. Once you are done, press 'Playtest' to validate your puzzle. `; this.titleElement = makeDiv(parent, "ingame_HUD_PuzzleEditorTitle"); diff --git a/src/js/game/map_chunk_view.js b/src/js/game/map_chunk_view.js index 59bff340..131ce37b 100644 --- a/src/js/game/map_chunk_view.js +++ b/src/js/game/map_chunk_view.js @@ -77,6 +77,7 @@ export class MapChunkView extends MapChunk { systems.display.drawChunk(parameters, this); systems.storage.drawChunk(parameters, this); systems.constantProducer.drawChunk(parameters, this); + systems.goalAcceptor.drawChunk(parameters, this); systems.itemProcessorOverlays.drawChunk(parameters, this); } diff --git a/src/js/game/modes/puzzle.js b/src/js/game/modes/puzzle.js index 15b0c868..ac7c3eef 100644 --- a/src/js/game/modes/puzzle.js +++ b/src/js/game/modes/puzzle.js @@ -82,6 +82,10 @@ export class PuzzleGameMode extends GameMode { return 1; } + getMaximumZoom() { + return 4; + } + getIsSaveable() { return false; } @@ -98,6 +102,14 @@ export class PuzzleGameMode extends GameMode { return false; } + getIsDeterministic() { + return true; + } + + getFixedTickrate() { + return 300; + } + /** @returns {boolean} */ getIsFreeplayAvailable() { return true; diff --git a/src/js/game/modes/puzzle_edit.js b/src/js/game/modes/puzzle_edit.js index 680778aa..5c90a0a7 100644 --- a/src/js/game/modes/puzzle_edit.js +++ b/src/js/game/modes/puzzle_edit.js @@ -66,4 +66,8 @@ export class PuzzleEditGameMode extends PuzzleGameMode { this.zone = this.createCenteredRectangle(this.zoneWidth, this.zoneHeight); } + + getIsEditor() { + return true; + } } diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js index 1e84a115..60f80dd7 100644 --- a/src/js/game/modes/regular.js +++ b/src/js/game/modes/regular.js @@ -2,13 +2,11 @@ import { GameRoot } from "../root"; /* typehints:end */ -import { queryParamOptions } from "../../core/query_parameters"; import { findNiceIntegerValue } from "../../core/utils"; import { MetaConstantProducerBuilding } from "../buildings/constant_producer"; import { MetaGoalAcceptorBuilding } from "../buildings/goal_acceptor"; -import { MetaItemProducerBuilding } from "../buildings/item_producer"; import { HUDModeMenuBack } from "../hud/parts/mode_menu_back"; -import { HUDModeMenuNext } from "../hud/parts/mode_menu_next"; +import { HUDPuzzleReview } from "../hud/parts/mode_puzzle_review"; import { HUDModeMenu } from "../hud/parts/mode_menu"; import { HUDModeSettings } from "../hud/parts/mode_settings"; import { enumGameModeIds, enumGameModeTypes, GameMode } from "../game_mode"; @@ -520,7 +518,7 @@ export class RegularGameMode extends GameMode { this.hiddenHurtParts = { [HUDModeMenuBack.name]: false, - [HUDModeMenuNext.name]: false, + [HUDPuzzleReview.name]: false, [HUDModeMenu.name]: false, [HUDModeSettings.name]: false, [HUDPuzzleDLCLogo.name]: false, diff --git a/src/js/game/systems/constant_producer.js b/src/js/game/systems/constant_producer.js index 0f167829..fa9f9e52 100644 --- a/src/js/game/systems/constant_producer.js +++ b/src/js/game/systems/constant_producer.js @@ -1,7 +1,6 @@ -/* typehints:start */ -/* typehints:end */ import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; +import { Vector } from "../../core/vector"; import { ConstantSignalComponent } from "../components/constant_signal"; import { ItemProducerComponent } from "../components/item_producer"; import { GameSystemWithFilter } from "../game_system_with_filter"; @@ -43,19 +42,25 @@ export class ConstantProducerSystem extends GameSystemWithFilter { const signalComp = contents[i].components.ConstantSignal; if (!producerComp || !producerComp.isWireless() || !signalComp || !signalComp.isWireless()) { - return; + continue; } const staticComp = contents[i].components.StaticMapEntity; const item = signalComp.signal; if (!item) { - return; + continue; } - // TODO: Better looking overlay const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); - item.drawItemCenteredClipped(center.x, center.y + 1, parameters, globalConfig.tileSize * 0.65); + + const localOffset = new Vector(0, 1).rotateFastMultipleOf90(staticComp.rotation); + item.drawItemCenteredClipped( + center.x + localOffset.x, + center.y + localOffset.y, + parameters, + globalConfig.tileSize * 0.65 + ); } } } diff --git a/src/js/game/systems/constant_signal.js b/src/js/game/systems/constant_signal.js index 2fab1eb8..8d033f61 100644 --- a/src/js/game/systems/constant_signal.js +++ b/src/js/game/systems/constant_signal.js @@ -57,7 +57,7 @@ export class ConstantSignalSystem extends GameSystemWithFilter { label: fillInLinkIntoTranslation(T.dialogs.editSignal.descShortKey, THIRDPARTY_URLS.shapeViewer), placeholder: "", defaultValue: "", - validator: val => this.parseSignalCode(val), + validator: val => this.parseSignalCode(entity.components.ConstantSignal.type, val), }); const items = [ @@ -93,7 +93,7 @@ export class ConstantSignalSystem extends GameSystemWithFilter { const dialog = new DialogWithForm({ app: this.root.app, - title: T.dialogs.editSignal.title, + title: T.dialogs.editConstantProducer.title, desc: T.dialogs.editSignal.descItems, formElements: [itemInput, signalValueInput], buttons: ["cancel:bad:escape", "ok:good:enter"], @@ -123,12 +123,20 @@ export class ConstantSignalSystem extends GameSystemWithFilter { if (itemInput.chosenItem) { constantComp.signal = itemInput.chosenItem; } else { - constantComp.signal = this.parseSignalCode(signalValueInput.getValue()); + constantComp.signal = this.parseSignalCode( + entity.components.ConstantSignal.type, + signalValueInput.getValue() + ); } }; - dialog.buttonSignals.ok.add(closeHandler); - dialog.valueChosen.add(closeHandler); + dialog.buttonSignals.ok.add(() => { + closeHandler(); + }); + dialog.valueChosen.add(() => { + dialog.closeRequested.dispatch(); + closeHandler(); + }); // When cancelled, destroy the entity again if (deleteOnCancel) { @@ -157,10 +165,11 @@ export class ConstantSignalSystem extends GameSystemWithFilter { /** * Tries to parse a signal code + * @param {string} type * @param {string} code * @returns {BaseItem} */ - parseSignalCode(code) { + parseSignalCode(type, code) { if (!this.root || !this.root.shapeDefinitionMgr) { // Stale reference return null; @@ -172,12 +181,15 @@ export class ConstantSignalSystem extends GameSystemWithFilter { if (enumColors[codeLower]) { return COLOR_ITEM_SINGLETONS[codeLower]; } - if (code === "1" || codeLower === "true") { - return BOOL_TRUE_SINGLETON; - } - if (code === "0" || codeLower === "false") { - return BOOL_FALSE_SINGLETON; + if (type === enumConstantSignalType.wired) { + if (code === "1" || codeLower === "true") { + return BOOL_TRUE_SINGLETON; + } + + if (code === "0" || codeLower === "false") { + return BOOL_FALSE_SINGLETON; + } } if (ShapeDefinition.isValidShortKey(code)) { diff --git a/src/js/game/systems/goal_acceptor.js b/src/js/game/systems/goal_acceptor.js index 6fcb479e..36e48164 100644 --- a/src/js/game/systems/goal_acceptor.js +++ b/src/js/game/systems/goal_acceptor.js @@ -1,111 +1,114 @@ -/* typehints:start */ -import { GameRoot } from "../root"; -/* typehints:end */ - -import { THIRDPARTY_URLS, globalConfig } from "../../core/config"; -import { DialogWithForm } from "../../core/modal_dialog_elements"; -import { FormElementInput, FormElementItemChooser } from "../../core/modal_dialog_forms"; -import { fillInLinkIntoTranslation } from "../../core/utils"; -import { T } from "../../translations"; +import { globalConfig } from "../../core/config"; +import { DrawParameters } from "../../core/draw_parameters"; +import { clamp, lerp } from "../../core/utils"; +import { Vector } from "../../core/vector"; import { GoalAcceptorComponent } from "../components/goal_acceptor"; import { GameSystemWithFilter } from "../game_system_with_filter"; -// import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item"; -// import { COLOR_ITEM_SINGLETONS } from "../items/color_item"; +import { MapChunk } from "../map_chunk"; +import { GameRoot } from "../root"; export class GoalAcceptorSystem extends GameSystemWithFilter { /** @param {GameRoot} root */ constructor(root) { super(root, [GoalAcceptorComponent]); - - this.root.signals.entityManuallyPlaced.add(this.editGoal, this); } update() { + const now = this.root.time.now(); + for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; const goalComp = entity.components.GoalAcceptor; - const readerComp = entity.components.BeltReader; - // Check against goals (set on placement) + // filter the ones which are no longer active, or which are not the same + goalComp.deliveryHistory = goalComp.deliveryHistory.filter( + d => + now - d.time < globalConfig.goalAcceptorMinimumDurationSeconds && d.item === goalComp.item + ); } - - // Check if goal criteria has been met for all goals } + /** + * + * @param {DrawParameters} parameters + * @param {MapChunk} chunk + * @returns + */ drawChunk(parameters, chunk) { - /* - *const contents = chunk.containedEntitiesByLayer.regular; - *for (let i = 0; i < contents.length; ++i) {} - */ - } + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const goalComp = contents[i].components.GoalAcceptor; - editGoal(entity) { - if (!entity.components.GoalAcceptor) { - return; - } - - const uid = entity.uid; - const goalComp = entity.components.GoalAcceptor; - - const itemInput = new FormElementInput({ - id: "goalItemInput", - label: fillInLinkIntoTranslation(T.dialogs.editGoalAcceptor.desc, THIRDPARTY_URLS.shapeViewer), - placeholder: "CuCuCuCu", - defaultValue: "CuCuCuCu", - validator: val => this.parseItem(val), - }); - - const dialog = new DialogWithForm({ - app: this.root.app, - title: T.dialogs.editGoalAcceptor.title, - desc: "", - formElements: [itemInput], - buttons: ["cancel:bad:escape", "ok:good:enter"], - closeButton: false, - }); - this.root.hud.parts.dialogs.internalShowDialog(dialog); - - const closeHandler = () => { - if (this.isEntityStale(uid)) { - return; + if (!goalComp) { + continue; } - goalComp.item = this.parseItem(itemInput.getValue()); - }; + const staticComp = contents[i].components.StaticMapEntity; + const item = goalComp.item; - dialog.buttonSignals.ok.add(closeHandler); - dialog.buttonSignals.cancel.add(() => { - if (this.isEntityStale(uid)) { - return; + const requiredItemsForSuccess = goalComp.getRequiredDeliveryHistorySize(); + const percentage = clamp(goalComp.deliveryHistory.length / requiredItemsForSuccess, 0, 1); + + const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); + if (item) { + const localOffset = new Vector(0, -1.8).rotateFastMultipleOf90(staticComp.rotation); + item.drawItemCenteredClipped( + center.x + localOffset.x, + center.y + localOffset.y, + parameters, + globalConfig.tileSize * 0.65 + ); } - this.root.logic.tryDeleteBuilding(entity); - }); - } + const isValid = item && goalComp.deliveryHistory.length >= requiredItemsForSuccess; - parseRate(value) { - return Number(value); - } + parameters.context.translate(center.x, center.y); + parameters.context.rotate((staticComp.rotation / 180) * Math.PI); - parseItem(value) { - return this.root.systemMgr.systems.constantSignal.parseSignalCode(value); - } + parameters.context.lineWidth = 1; + parameters.context.fillStyle = "#8de255"; + parameters.context.strokeStyle = "#64666e"; + parameters.context.lineCap = "round"; - isEntityStale(uid) { - if (!this.root || !this.root.entityMgr) { - return true; + // progress arc + + goalComp.displayPercentage = lerp(goalComp.displayPercentage, percentage, 0.3); + + const startAngle = Math.PI * 0.595; + const maxAngle = Math.PI * 1.82; + parameters.context.beginPath(); + parameters.context.arc( + 0.25, + -1.5, + 11.6, + startAngle, + startAngle + goalComp.displayPercentage * maxAngle, + false + ); + parameters.context.arc( + 0.25, + -1.5, + 15.5, + startAngle + goalComp.displayPercentage * maxAngle, + startAngle, + true + ); + parameters.context.closePath(); + parameters.context.fill(); + parameters.context.stroke(); + parameters.context.lineCap = "butt"; + + // LED indicator + + parameters.context.lineWidth = 1; + parameters.context.strokeStyle = "#64666e"; + parameters.context.fillStyle = isValid ? "#8de255" : "#e2555f"; + parameters.context.beginCircle(10, 11.8, 3); + parameters.context.fill(); + parameters.context.stroke(); + + parameters.context.rotate((-staticComp.rotation / 180) * Math.PI); + parameters.context.translate(-center.x, -center.y); } - - const entity = this.root.entityMgr.findByUid(uid, false); - if (!entity) { - return true; - } - - const goalComp = entity.components.GoalAcceptor; - if (!goalComp) { - return true; - } - - return false; } } diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index 17d64e4d..4df7eecb 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -568,8 +568,18 @@ export class ItemProcessorSystem extends GameSystemWithFilter { * @param {ProcessorImplementationPayload} payload */ process_GOAL(payload) { - const readerComp = payload.entity.components.BeltReader; - readerComp.lastItemTimes.push(this.root.time.now()); - readerComp.lastItem = payload.items[payload.items.length - 1].item; + const goalComp = payload.entity.components.GoalAcceptor; + if (this.root.gameMode.getIsEditor()) { + // while playing in editor, assign the item + goalComp.item = payload.items[0].item; + } + + const now = this.root.time.now(); + + // push our new entry + goalComp.deliveryHistory.push({ + item: payload.items[0].item, + time: now, + }); } } diff --git a/src/js/savegame/puzzle_serializer.js b/src/js/savegame/puzzle_serializer.js new file mode 100644 index 00000000..94d9cbc3 --- /dev/null +++ b/src/js/savegame/puzzle_serializer.js @@ -0,0 +1,18 @@ +import { GameRoot } from "../game/root"; + +export class PuzzleSerializer { + /** + * Serializes the game root into a dump + * @param {GameRoot} root + * @param {boolean=} sanityChecks Whether to check for validity + * @returns {object} + */ + generateDumpFromGameRoot(root, sanityChecks = true) { + console.log("serializing", root); + + return { + type: "puzzle", + contents: "foo", + }; + } +} diff --git a/translations/base-en.yaml b/translations/base-en.yaml index f02b8335..9877a47c 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -124,6 +124,9 @@ puzzleMenu: edit: Edit title: Puzzle Mode createPuzzle: Create Puzzle + reviewPuzzle: Review & Publish + validtingPuzzle: Validating Puzzle + submittingPuzzle: Submitting Puzzle categories: levels: Levels @@ -131,6 +134,14 @@ puzzleMenu: topRated: Top Rated myPuzzles: My Puzzles + validation: + title: Invalid Puzzle + noProducers: Please place a Constant Producer! + noGoalAcceptors: Please place a Goal Acceptor! + goalAcceptorNoItem: >- + One or more Goal Acceptors have not yet assigned an item. Deliver a shape to them to set a goal. + goalAcceptorRateNotMet: >- + One or more Goal Acceptors are not getting enough items. Make sure that the indicators are green for all acceptors. dialogs: buttons: ok: OK @@ -248,9 +259,8 @@ dialogs: Choose a pre-defined item: descShortKey: ... or enter the short key of a shape (Which you can generate here) - editGoalAcceptor: - title: Set Goal - desc: Enter the short key of a shape (Which you can generate here). The goal will count as completed once 1 item /s is delivered. + editConstantProducer: + title: Set Item markerDemoLimit: desc: You can only create two custom markers in the demo. Get the standalone for unlimited markers! @@ -276,6 +286,15 @@ dialogs: desc: >- Unfortunately the puzzles could not be loaded: + submitPuzzle: + title: Submit Puzzle + descName: >- + Give your puzzle a name: + descIcon: >- + Please enter a unique short key, which will be shown as the icon of your puzzle (You can generate them here, or choose one of the randomly suggested shapes below): + + placeholderName: Puzzle Title + ingame: # This is shown in the top left corner and displays useful keybindings in # every situation @@ -500,24 +519,6 @@ ingame: title: Support me desc: I develop the game in my spare time! - modeMenu: - puzzleEditMode: - back: - title: Menu - next: - title: Playtest - desc: Required for publishing - puzzleEditTestMode: - back: - title: Edit - next: - title: Publish - puzzlePlayMode: - back: - title: Menu - next: - title: Next - # All shop upgrades shopUpgrades: belt: