mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-06-13 13:04:03 +00:00
Puzzle mode, part 3
This commit is contained in:
parent
f64714ec25
commit
b732db58b9
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
#ingame_HUD_ModeMenuNext {
|
||||
#ingame_HUD_PuzzleReview {
|
||||
position: absolute;
|
||||
@include S(top, 15px);
|
||||
@include S(right, 10px);
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
<div class='option ${value === options.active ? "active" : ""} ${
|
||||
iconPrefix ? "hasIcon" : ""
|
||||
}' data-optionvalue='${value}'>
|
||||
${iconHtml}
|
||||
${iconHtml}
|
||||
<span class='title'>${text}</span>
|
||||
${descHtml}
|
||||
</div>
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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({}));
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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";
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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() {}
|
||||
}
|
167
src/js/game/hud/parts/mode_puzzle_review.js
Normal file
167
src/js/game/hud/parts/mode_puzzle_review.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,9 +8,8 @@ export class HUDPuzzleEditorControls extends BaseHUDPart {
|
||||
this.element.innerHTML = `
|
||||
|
||||
<span>1. Build constant producers to generate resources.</span>
|
||||
<span>2. Build goal acceptors the capture shapes.</span>
|
||||
<span>3. Produce your desired shape(s) within the puzzle area and deliver it to the goal acceptors, which will capture it.</span>
|
||||
<span>4. Once you are done, press 'Playtest' to validate your puzzle.</span>
|
||||
<span>2. Build goal acceptors and deliver shapes to set the puzzle goals.</span>
|
||||
<span>3. Once you are done, press 'Playtest' to validate your puzzle.</span>
|
||||
`;
|
||||
|
||||
this.titleElement = makeDiv(parent, "ingame_HUD_PuzzleEditorTitle");
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -66,4 +66,8 @@ export class PuzzleEditGameMode extends PuzzleGameMode {
|
||||
|
||||
this.zone = this.createCenteredRectangle(this.zoneWidth, this.zoneHeight);
|
||||
}
|
||||
|
||||
getIsEditor() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
18
src/js/savegame/puzzle_serializer.js
Normal file
18
src/js/savegame/puzzle_serializer.js
Normal file
@ -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",
|
||||
};
|
||||
}
|
||||
}
|
@ -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 <strong>short key</strong> of a shape (Which you can generate <link>here</link>)
|
||||
|
||||
editGoalAcceptor:
|
||||
title: Set Goal
|
||||
desc: Enter the <strong>short key</strong> of a shape (Which you can generate <link>here</link>). 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 <link>here</link>, 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:
|
||||
|
Loading…
Reference in New Issue
Block a user