diff --git a/res_raw/sprites/buildings/goal_acceptor.png b/res_raw/sprites/buildings/goal_acceptor.png index 2ebf879a..17fa224f 100644 Binary files a/res_raw/sprites/buildings/goal_acceptor.png and b/res_raw/sprites/buildings/goal_acceptor.png differ 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 {
@@ -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: