1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-06-13 13:04:03 +00:00

Merge pull request #2 from EmeraldBlock/master

make things make sense
This commit is contained in:
EmeraldBlock 2020-09-28 15:54:30 -05:00 committed by GitHub
commit 1d65a7302a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 18065 additions and 13877 deletions

4
.gitattributes vendored
View File

@ -1,4 +0,0 @@
*.wav filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text

View File

@ -22,8 +22,6 @@ Your goal is to produce shapes by cutting, rotating, merging and painting parts
## Building ## Building
- Make sure git `git lfs` extension is on your path
- Run `git lfs pull` to download sound assets
- Make sure `ffmpeg` is on your path - Make sure `ffmpeg` is on your path
- Install Node.js and Yarn - Install Node.js and Yarn
- Run `yarn` in the root folder - Run `yarn` in the root folder

1
gulp/.gitattributes vendored
View File

@ -1 +0,0 @@
*.wav filter=lfs diff=lfs merge=lfs -text

View File

@ -54,19 +54,19 @@ function gulptasksHTML($, gulp, buildFolder) {
document.head.appendChild(css); document.head.appendChild(css);
// Append async css // Append async css
const asyncCss = document.createElement("link"); // const asyncCss = document.createElement("link");
asyncCss.rel = "stylesheet"; // asyncCss.rel = "stylesheet";
asyncCss.type = "text/css"; // asyncCss.type = "text/css";
asyncCss.media = "none"; // asyncCss.media = "none";
asyncCss.setAttribute("onload", "this.media='all'"); // asyncCss.setAttribute("onload", "this.media='all'");
asyncCss.href = cachebust("async-resources.css"); // asyncCss.href = cachebust("async-resources.css");
if (integrity) { // if (integrity) {
asyncCss.setAttribute( // asyncCss.setAttribute(
"integrity", // "integrity",
computeIntegrityHash(path.join(buildFolder, "async-resources.css")) // computeIntegrityHash(path.join(buildFolder, "async-resources.css"))
); // );
} // }
document.head.appendChild(asyncCss); // document.head.appendChild(asyncCss);
if (app) { if (app) {
// Append cordova link // Append cordova link

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 283 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 713 KiB

After

Width:  |  Height:  |  Size: 706 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df7487fb5e8cb34cecee2519b9c3162a5107d2d7b1301c4a550904cfb108a015
size 223361394

BIN
res_raw/sounds/sfx/copy.wav Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,8 +1,24 @@
#ingame_HUD_BetaOverlay { #ingame_HUD_BetaOverlay {
position: fixed; position: fixed;
@include S(top, 10px); @include S(top, 10px);
@include S(right, 15px); left: 50%;
transform: translateX(-50%);
color: $colorRedBright; color: $colorRedBright;
@include Heading; @include Heading;
text-transform: uppercase; text-transform: uppercase;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
h2 {
@include PlainText;
}
span {
color: #555;
@include SuperSmallText;
}
} }

View File

@ -178,6 +178,27 @@
display: list-item; display: list-item;
} }
} }
.ingameItemChooser {
@include S(margin, 20px, 0);
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
@include S(grid-column-gap, 3px);
canvas {
pointer-events: all;
@include S(width, 25px);
@include S(height, 25px);
position: relative;
cursor: pointer;
@include IncreasedClickArea(3px);
&:hover {
opacity: 0.9;
}
}
}
} }
> .buttons { > .buttons {

View File

@ -1,215 +1,232 @@
/* typehints:start */ /* typehints:start */
import { Application } from "../application"; import { Application } from "../application";
/* typehints:end */ /* typehints:end */
import { Loader } from "./loader"; import { Loader } from "./loader";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { Signal } from "./signal"; import { Signal } from "./signal";
import { SOUNDS, MUSIC } from "../platform/sound"; import { SOUNDS, MUSIC } from "../platform/sound";
import { AtlasDefinition, atlasFiles } from "./atlas_definitions"; import { AtlasDefinition, atlasFiles } from "./atlas_definitions";
import { initBuildingCodesAfterResourcesLoaded } from "../game/meta_building_registry"; import { initBuildingCodesAfterResourcesLoaded } from "../game/meta_building_registry";
import { cachebust } from "./cachebust";
const logger = createLogger("background_loader");
const logger = createLogger("background_loader");
const essentialMainMenuSprites = [
"logo.png", const essentialMainMenuSprites = [
...G_ALL_UI_IMAGES.filter(src => src.startsWith("ui/") && src.indexOf(".gif") < 0), "logo.png",
]; ...G_ALL_UI_IMAGES.filter(src => src.startsWith("ui/") && src.indexOf(".gif") < 0),
const essentialMainMenuSounds = [ ];
SOUNDS.uiClick, const essentialMainMenuSounds = [
SOUNDS.uiError, SOUNDS.uiClick,
SOUNDS.dialogError, SOUNDS.uiError,
SOUNDS.dialogOk, SOUNDS.dialogError,
SOUNDS.swishShow, SOUNDS.dialogOk,
SOUNDS.swishHide, SOUNDS.swishShow,
]; SOUNDS.swishHide,
];
const essentialBareGameAtlases = atlasFiles;
const essentialBareGameSprites = G_ALL_UI_IMAGES.filter(src => src.indexOf(".gif") < 0); const essentialBareGameAtlases = atlasFiles;
const essentialBareGameSounds = [MUSIC.theme]; const essentialBareGameSprites = G_ALL_UI_IMAGES.filter(src => src.indexOf(".gif") < 0);
const essentialBareGameSounds = [MUSIC.theme];
const additionalGameSprites = [];
// @ts-ignore const additionalGameSprites = [];
const additionalGameSounds = [...Object.values(SOUNDS), ...Object.values(MUSIC)]; // @ts-ignore
const additionalGameSounds = [...Object.values(SOUNDS), ...Object.values(MUSIC)];
export class BackgroundResourcesLoader {
/** export class BackgroundResourcesLoader {
* /**
* @param {Application} app *
*/ * @param {Application} app
constructor(app) { */
this.app = app; constructor(app) {
this.app = app;
this.registerReady = false;
this.mainMenuReady = false; this.registerReady = false;
this.bareGameReady = false; this.mainMenuReady = false;
this.additionalReady = false; this.bareGameReady = false;
this.additionalReady = false;
this.signalMainMenuLoaded = new Signal();
this.signalBareGameLoaded = new Signal(); this.signalMainMenuLoaded = new Signal();
this.signalAdditionalLoaded = new Signal(); this.signalBareGameLoaded = new Signal();
this.signalAdditionalLoaded = new Signal();
this.numAssetsLoaded = 0;
this.numAssetsToLoadTotal = 0; this.numAssetsLoaded = 0;
this.numAssetsToLoadTotal = 0;
// Avoid loading stuff twice
this.spritesLoaded = []; // Avoid loading stuff twice
this.soundsLoaded = []; this.spritesLoaded = [];
} this.soundsLoaded = [];
}
getNumAssetsLoaded() {
return this.numAssetsLoaded; getNumAssetsLoaded() {
} return this.numAssetsLoaded;
}
getNumAssetsTotal() {
return this.numAssetsToLoadTotal; getNumAssetsTotal() {
} return this.numAssetsToLoadTotal;
}
getPromiseForMainMenu() {
if (this.mainMenuReady) { getPromiseForMainMenu() {
return Promise.resolve(); if (this.mainMenuReady) {
} return Promise.resolve();
}
return new Promise(resolve => {
this.signalMainMenuLoaded.add(resolve); return new Promise(resolve => {
}); this.signalMainMenuLoaded.add(resolve);
} });
}
getPromiseForBareGame() {
if (this.bareGameReady) { getPromiseForBareGame() {
return Promise.resolve(); if (this.bareGameReady) {
} return Promise.resolve();
}
return new Promise(resolve => {
this.signalBareGameLoaded.add(resolve); return new Promise(resolve => {
}); this.signalBareGameLoaded.add(resolve);
} });
}
startLoading() {
this.internalStartLoadingEssentialsForMainMenu(); startLoading() {
} this.internalStartLoadingEssentialsForMainMenu();
}
internalStartLoadingEssentialsForMainMenu() {
logger.log("⏰ Start load: main menu"); internalStartLoadingEssentialsForMainMenu() {
this.internalLoadSpritesAndSounds(essentialMainMenuSprites, essentialMainMenuSounds) logger.log("⏰ Start load: main menu");
.catch(err => { this.internalLoadSpritesAndSounds(essentialMainMenuSprites, essentialMainMenuSounds)
logger.warn("⏰ Failed to load essentials for main menu:", err); .catch(err => {
}) logger.warn("⏰ Failed to load essentials for main menu:", err);
.then(() => { })
logger.log("⏰ Finish load: main menu"); .then(() => {
this.mainMenuReady = true; logger.log("⏰ Finish load: main menu");
this.signalMainMenuLoaded.dispatch(); this.mainMenuReady = true;
this.internalStartLoadingEssentialsForBareGame(); this.signalMainMenuLoaded.dispatch();
}); this.internalStartLoadingEssentialsForBareGame();
} });
}
internalStartLoadingEssentialsForBareGame() {
logger.log("⏰ Start load: bare game"); internalStartLoadingEssentialsForBareGame() {
this.internalLoadSpritesAndSounds( logger.log("⏰ Start load: bare game");
essentialBareGameSprites, this.internalLoadSpritesAndSounds(
essentialBareGameSounds, essentialBareGameSprites,
essentialBareGameAtlases essentialBareGameSounds,
) essentialBareGameAtlases
.catch(err => { )
logger.warn("⏰ Failed to load essentials for bare game:", err); .then(() => this.internalPreloadCss("async-resources.scss"))
}) .catch(err => {
.then(() => { logger.warn("⏰ Failed to load essentials for bare game:", err);
logger.log("⏰ Finish load: bare game"); })
this.bareGameReady = true; .then(() => {
initBuildingCodesAfterResourcesLoaded(); logger.log("⏰ Finish load: bare game");
this.signalBareGameLoaded.dispatch(); this.bareGameReady = true;
this.internalStartLoadingAdditionalGameAssets(); initBuildingCodesAfterResourcesLoaded();
}); this.signalBareGameLoaded.dispatch();
} this.internalStartLoadingAdditionalGameAssets();
});
internalStartLoadingAdditionalGameAssets() { }
const additionalAtlases = [];
logger.log("⏰ Start load: additional assets (", additionalAtlases.length, "images)"); internalStartLoadingAdditionalGameAssets() {
this.internalLoadSpritesAndSounds(additionalGameSprites, additionalGameSounds, additionalAtlases) const additionalAtlases = [];
.catch(err => { logger.log("⏰ Start load: additional assets (", additionalAtlases.length, "images)");
logger.warn("⏰ Failed to load additional assets:", err); this.internalLoadSpritesAndSounds(additionalGameSprites, additionalGameSounds, additionalAtlases)
}) .catch(err => {
.then(() => { logger.warn("⏰ Failed to load additional assets:", err);
logger.log("⏰ Finish load: additional assets"); })
this.additionalReady = true; .then(() => {
this.signalAdditionalLoaded.dispatch(); logger.log("⏰ Finish load: additional assets");
}); this.additionalReady = true;
} this.signalAdditionalLoaded.dispatch();
});
/** }
* @param {Array<string>} sprites
* @param {Array<string>} sounds internalPreloadCss(name) {
* @param {Array<AtlasDefinition>} atlases return new Promise((resolve, reject) => {
* @returns {Promise<void>} const link = document.createElement("link");
*/
internalLoadSpritesAndSounds(sprites, sounds, atlases = []) { link.onload = resolve;
this.numAssetsToLoadTotal = sprites.length + sounds.length + atlases.length; link.onerror = reject;
this.numAssetsLoaded = 0;
link.setAttribute("rel", "stylesheet");
let promises = []; link.setAttribute("media", "all");
link.setAttribute("type", "text/css");
for (let i = 0; i < sounds.length; ++i) { link.setAttribute("href", cachebust("async-resources.css"));
if (this.soundsLoaded.indexOf(sounds[i]) >= 0) { document.head.appendChild(link);
// Already loaded });
continue; }
}
/**
this.soundsLoaded.push(sounds[i]); * @param {Array<string>} sprites
promises.push( * @param {Array<string>} sounds
this.app.sound * @param {Array<AtlasDefinition>} atlases
.loadSound(sounds[i]) * @returns {Promise<void>}
.catch(err => { */
logger.warn("Failed to load sound:", sounds[i]); internalLoadSpritesAndSounds(sprites, sounds, atlases = []) {
}) this.numAssetsToLoadTotal = sprites.length + sounds.length + atlases.length;
.then(() => { this.numAssetsLoaded = 0;
this.numAssetsLoaded++;
}) let promises = [];
);
} for (let i = 0; i < sounds.length; ++i) {
if (this.soundsLoaded.indexOf(sounds[i]) >= 0) {
for (let i = 0; i < sprites.length; ++i) { // Already loaded
if (this.spritesLoaded.indexOf(sprites[i]) >= 0) { continue;
// Already loaded }
continue;
} this.soundsLoaded.push(sounds[i]);
this.spritesLoaded.push(sprites[i]); promises.push(
promises.push( this.app.sound
Loader.preloadCSSSprite(sprites[i]) .loadSound(sounds[i])
.catch(err => { .catch(err => {
logger.warn("Failed to load css sprite:", sprites[i]); logger.warn("Failed to load sound:", sounds[i]);
}) })
.then(() => { .then(() => {
this.numAssetsLoaded++; this.numAssetsLoaded++;
}) })
); );
} }
for (let i = 0; i < atlases.length; ++i) { for (let i = 0; i < sprites.length; ++i) {
const atlas = atlases[i]; if (this.spritesLoaded.indexOf(sprites[i]) >= 0) {
promises.push( // Already loaded
Loader.preloadAtlas(atlas) continue;
.catch(err => { }
logger.warn("Failed to load atlas:", atlas.sourceFileName); this.spritesLoaded.push(sprites[i]);
}) promises.push(
.then(() => { Loader.preloadCSSSprite(sprites[i])
this.numAssetsLoaded++; .catch(err => {
}) logger.warn("Failed to load css sprite:", sprites[i]);
); })
} .then(() => {
this.numAssetsLoaded++;
return ( })
Promise.all(promises) );
}
// // Remove some pressure by waiting a bit
// .then(() => { for (let i = 0; i < atlases.length; ++i) {
// return new Promise(resolve => { const atlas = atlases[i];
// setTimeout(resolve, 200); promises.push(
// }); Loader.preloadAtlas(atlas)
// }) .catch(err => {
.then(() => { logger.warn("Failed to load atlas:", atlas.sourceFileName);
this.numAssetsToLoadTotal = 0; })
this.numAssetsLoaded = 0; .then(() => {
}) this.numAssetsLoaded++;
); })
} );
} }
return (
Promise.all(promises)
// // Remove some pressure by waiting a bit
// .then(() => {
// return new Promise(resolve => {
// setTimeout(resolve, 200);
// });
// })
.then(() => {
this.numAssetsToLoadTotal = 0;
this.numAssetsLoaded = 0;
})
);
}
}

View File

@ -20,6 +20,7 @@ export const THIRDPARTY_URLS = {
discord: "https://discord.gg/HN7EVzV", discord: "https://discord.gg/HN7EVzV",
github: "https://github.com/tobspr/shapez.io", github: "https://github.com/tobspr/shapez.io",
reddit: "https://www.reddit.com/r/shapezio", reddit: "https://www.reddit.com/r/shapezio",
shapeViewer: "https://viewer.shapez.io",
standaloneStorePage: "https://store.steampowered.com/app/1318690/shapezio/", standaloneStorePage: "https://store.steampowered.com/app/1318690/shapezio/",
}; };

View File

@ -60,6 +60,8 @@ export class Dialog {
this.buttonSignals[buttonId] = new Signal(); this.buttonSignals[buttonId] = new Signal();
} }
this.valueChosen = new Signal();
this.timeouts = []; this.timeouts = [];
this.clickDetectors = []; this.clickDetectors = [];
@ -431,10 +433,12 @@ export class DialogWithForm extends Dialog {
for (let i = 0; i < this.formElements.length; ++i) { for (let i = 0; i < this.formElements.length; ++i) {
const elem = this.formElements[i]; const elem = this.formElements[i];
elem.bindEvents(div, this.clickDetectors); elem.bindEvents(div, this.clickDetectors);
elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested);
elem.valueChosen.add(this.valueChosen.dispatch, this.valueChosen);
} }
waitNextFrame().then(() => { waitNextFrame().then(() => {
this.formElements[0].focus(); this.formElements[this.formElements.length - 1].focus();
}); });
return div; return div;

View File

@ -1,150 +1,221 @@
import { ClickDetector } from "./click_detector"; import { BaseItem } from "../game/base_item";
import { ClickDetector } from "./click_detector";
export class FormElement { import { Signal } from "./signal";
constructor(id, label) {
this.id = id; export class FormElement {
this.label = label; constructor(id, label) {
} this.id = id;
this.label = label;
getHtml() {
abstract; this.valueChosen = new Signal();
return ""; }
}
getHtml() {
getFormElement(parent) { abstract;
return parent.querySelector("[data-formId='" + this.id + "']"); return "";
} }
bindEvents(parent, clickTrackers) { getFormElement(parent) {
abstract; return parent.querySelector("[data-formId='" + this.id + "']");
} }
focus() {} bindEvents(parent, clickTrackers) {
abstract;
isValid() { }
return true;
} focus() {}
/** @returns {any} */ isValid() {
getValue() { return true;
abstract; }
}
} /** @returns {any} */
getValue() {
export class FormElementInput extends FormElement { abstract;
constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) { }
super(id, label); }
this.placeholder = placeholder;
this.defaultValue = defaultValue; export class FormElementInput extends FormElement {
this.inputType = inputType; constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) {
this.validator = validator; super(id, label);
this.placeholder = placeholder;
this.element = null; this.defaultValue = defaultValue;
} this.inputType = inputType;
this.validator = validator;
getHtml() {
let classes = []; this.element = null;
let inputType = "text"; }
let maxlength = 256;
switch (this.inputType) { getHtml() {
case "text": { let classes = [];
classes.push("input-text"); let inputType = "text";
break; let maxlength = 256;
} switch (this.inputType) {
case "text": {
case "email": { classes.push("input-text");
classes.push("input-email"); break;
inputType = "email"; }
break;
} case "email": {
classes.push("input-email");
case "token": { inputType = "email";
classes.push("input-token"); break;
inputType = "text"; }
maxlength = 4;
break; case "token": {
} classes.push("input-token");
} inputType = "text";
maxlength = 4;
return ` break;
<div class="formElement input"> }
${this.label ? `<label>${this.label}</label>` : ""} }
<input
type="${inputType}" return `
value="${this.defaultValue.replace(/["\\]+/gi, "")}" <div class="formElement input">
maxlength="${maxlength}" ${this.label ? `<label>${this.label}</label>` : ""}
autocomplete="off" <input
autocorrect="off" type="${inputType}"
autocapitalize="off" value="${this.defaultValue.replace(/["\\]+/gi, "")}"
spellcheck="false" maxlength="${maxlength}"
class="${classes.join(" ")}" autocomplete="off"
placeholder="${this.placeholder.replace(/["\\]+/gi, "")}" autocorrect="off"
data-formId="${this.id}"> autocapitalize="off"
</div> spellcheck="false"
`; class="${classes.join(" ")}"
} placeholder="${this.placeholder.replace(/["\\]+/gi, "")}"
data-formId="${this.id}">
bindEvents(parent, clickTrackers) { </div>
this.element = this.getFormElement(parent); `;
this.element.addEventListener("input", event => this.updateErrorState()); }
this.updateErrorState();
} bindEvents(parent, clickTrackers) {
this.element = this.getFormElement(parent);
updateErrorState() { this.element.addEventListener("input", event => this.updateErrorState());
this.element.classList.toggle("errored", !this.isValid()); this.updateErrorState();
} }
isValid() { updateErrorState() {
return !this.validator || this.validator(this.element.value); this.element.classList.toggle("errored", !this.isValid());
} }
getValue() { isValid() {
return this.element.value; return !this.validator || this.validator(this.element.value);
} }
focus() { getValue() {
this.element.focus(); return this.element.value;
} }
}
focus() {
export class FormElementCheckbox extends FormElement { this.element.focus();
constructor({ id, label, defaultValue = true }) { }
super(id, label); }
this.defaultValue = defaultValue;
this.value = this.defaultValue; export class FormElementCheckbox extends FormElement {
constructor({ id, label, defaultValue = true }) {
this.element = null; super(id, label);
} this.defaultValue = defaultValue;
this.value = this.defaultValue;
getHtml() {
return ` this.element = null;
<div class="formElement checkBoxFormElem"> }
${this.label ? `<label>${this.label}</label>` : ""}
<div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'> getHtml() {
<span class="knob"></span > return `
</div > <div class="formElement checkBoxFormElem">
</div> ${this.label ? `<label>${this.label}</label>` : ""}
`; <div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'>
} <span class="knob"></span >
</div >
bindEvents(parent, clickTrackers) { </div>
this.element = this.getFormElement(parent); `;
const detector = new ClickDetector(this.element, { }
consumeEvents: false,
preventDefault: false, bindEvents(parent, clickTrackers) {
}); this.element = this.getFormElement(parent);
clickTrackers.push(detector); const detector = new ClickDetector(this.element, {
detector.click.add(this.toggle, this); consumeEvents: false,
} preventDefault: false,
});
getValue() { clickTrackers.push(detector);
return this.value; detector.click.add(this.toggle, this);
} }
toggle() { getValue() {
this.value = !this.value; return this.value;
this.element.classList.toggle("checked", this.value); }
}
toggle() {
focus(parent) {} this.value = !this.value;
} this.element.classList.toggle("checked", this.value);
}
focus(parent) {}
}
export class FormElementItemChooser extends FormElement {
/**
*
* @param {object} param0
* @param {string} param0.id
* @param {string=} param0.label
* @param {Array<BaseItem>} param0.items
*/
constructor({ id, label, items = [] }) {
super(id, label);
this.items = items;
this.element = null;
/**
* @type {BaseItem}
*/
this.chosenItem = null;
}
getHtml() {
let classes = [];
return `
<div class="formElement">
${this.label ? `<label>${this.label}</label>` : ""}
<div class="ingameItemChooser input" data-formId="${this.id}"></div>
</div>
`;
}
/**
* @param {HTMLElement} parent
* @param {Array<ClickDetector>} clickTrackers
*/
bindEvents(parent, clickTrackers) {
this.element = this.getFormElement(parent);
for (let i = 0; i < this.items.length; ++i) {
const item = this.items[i];
const canvas = document.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
const context = canvas.getContext("2d");
item.drawFullSizeOnCanvas(context, 128);
this.element.appendChild(canvas);
const detector = new ClickDetector(canvas, {});
clickTrackers.push(detector);
detector.click.add(() => {
this.chosenItem = item;
this.valueChosen.dispatch(item);
});
}
}
isValid() {
return true;
}
getValue() {
return null;
}
focus() {}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,82 +1,100 @@
import { globalConfig } from "../core/config"; import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters"; import { DrawParameters } from "../core/draw_parameters";
import { BasicSerializableObject } from "../savegame/serialization"; import { BasicSerializableObject } from "../savegame/serialization";
/** @type {ItemType[]} **/ /** @type {ItemType[]} **/
export const itemTypes = ["shape", "color", "boolean"]; export const itemTypes = ["shape", "color", "boolean"];
/** /**
* Class for items on belts etc. Not an entity for performance reasons * Class for items on belts etc. Not an entity for performance reasons
*/ */
export class BaseItem extends BasicSerializableObject { export class BaseItem extends BasicSerializableObject {
constructor() { constructor() {
super(); super();
} }
static getId() { static getId() {
return "base_item"; return "base_item";
} }
/** @returns {object} */ /** @returns {object} */
static getSchema() { static getSchema() {
return {}; return {};
} }
/** @returns {ItemType} **/ /** @returns {ItemType} **/
getItemType() { getItemType() {
abstract; abstract;
return "shape"; return "shape";
} }
/** /**
* Returns if the item equals the other itme * Returns a string id of the item
* @param {BaseItem} other * @returns {string}
* @returns {boolean} */
*/ getAsCopyableKey() {
equals(other) { abstract;
if (this.getItemType() !== other.getItemType()) { return "";
return false; }
}
return this.equalsImpl(other); /**
} * Returns if the item equals the other itme
* @param {BaseItem} other
/** * @returns {boolean}
* Override for custom comparison */
* @abstract equals(other) {
* @param {BaseItem} other if (this.getItemType() !== other.getItemType()) {
* @returns {boolean} return false;
*/ }
equalsImpl(other) { return this.equalsImpl(other);
abstract; }
return false;
} /**
* Override for custom comparison
/** * @abstract
* Draws the item at the given position * @param {BaseItem} other
* @param {number} x * @returns {boolean}
* @param {number} y */
* @param {DrawParameters} parameters equalsImpl(other) {
* @param {number=} diameter abstract;
*/ return false;
drawItemCenteredClipped(x, y, parameters, diameter = globalConfig.defaultItemDiameter) { }
if (parameters.visibleRect.containsCircle(x, y, diameter / 2)) {
this.drawItemCenteredImpl(x, y, parameters, diameter); /**
} * Draws the item to a canvas
} * @param {CanvasRenderingContext2D} context
* @param {number} size
/** */
* INTERNAL drawFullSizeOnCanvas(context, size) {
* @param {number} x abstract;
* @param {number} y }
* @param {DrawParameters} parameters
* @param {number=} diameter /**
*/ * Draws the item at the given position
drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) { * @param {number} x
abstract; * @param {number} y
} * @param {DrawParameters} parameters
* @param {number=} diameter
getBackgroundColorAsResource() { */
abstract; drawItemCenteredClipped(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
return ""; if (parameters.visibleRect.containsCircle(x, y, diameter / 2)) {
} this.drawItemCenteredImpl(x, y, parameters, diameter);
} }
}
/**
* INTERNAL
* @param {number} x
* @param {number} y
* @param {DrawParameters} parameters
* @param {number=} diameter
*/
drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
abstract;
}
getBackgroundColorAsResource() {
abstract;
return "";
}
}

View File

@ -1,12 +1,11 @@
import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters"; import { DrawParameters } from "../core/draw_parameters";
import { Loader } from "../core/loader";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { findNiceIntegerValue } from "../core/utils";
import { Vector } from "../core/vector"; import { Vector } from "../core/vector";
import { Entity } from "./entity"; import { Entity } from "./entity";
import { GameRoot } from "./root"; import { GameRoot } from "./root";
import { findNiceIntegerValue } from "../core/utils";
import { blueprintShape } from "./upgrades"; import { blueprintShape } from "./upgrades";
import { globalConfig } from "../core/config";
const logger = createLogger("blueprint"); const logger = createLogger("blueprint");

View File

@ -53,6 +53,6 @@ export class MetaWireTunnelBuilding extends MetaBuilding {
* @param {Entity} entity * @param {Entity} entity
*/ */
setupEntityComponents(entity) { setupEntityComponents(entity) {
entity.addComponent(new WireTunnelComponent({})); entity.addComponent(new WireTunnelComponent());
} }
} }

View File

@ -502,10 +502,10 @@ export class Camera extends BasicSerializableObject {
} }
const prevZoom = this.zoomLevel; const prevZoom = this.zoomLevel;
const delta = Math.sign(event.deltaY) * -0.15 * this.root.app.settings.getScrollWheelSensitivity(); const scale = 1 + 0.15 * this.root.app.settings.getScrollWheelSensitivity();
assert(Number.isFinite(delta), "Got invalid delta in mouse wheel event: " + event.deltaY); assert(Number.isFinite(delta), "Got invalid delta in mouse wheel event: " + event.deltaY);
assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *before* wheel: " + this.zoomLevel); assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *before* wheel: " + this.zoomLevel);
this.zoomLevel *= 1 + delta; this.zoomLevel *= (event.deltaY < 0) ? scale : 1/scale;
assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *after* wheel: " + this.zoomLevel); assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *after* wheel: " + this.zoomLevel);
this.clampZoomLevel(); this.clampZoomLevel();

View File

@ -1,8 +1,10 @@
import { globalConfig } from "../core/config"; import { globalConfig } from "../core/config";
import { RandomNumberGenerator } from "../core/rng";
import { clamp, findNiceIntegerValue, randomChoice, randomInt } from "../core/utils"; import { clamp, findNiceIntegerValue, randomChoice, randomInt } from "../core/utils";
import { BasicSerializableObject, types } from "../savegame/serialization"; import { BasicSerializableObject, types } from "../savegame/serialization";
import { enumColors } from "./colors"; import { enumColors } from "./colors";
import { enumItemProcessorTypes } from "./components/item_processor"; import { enumItemProcessorTypes } from "./components/item_processor";
import { enumAnalyticsDataSource } from "./production_analytics";
import { GameRoot } from "./root"; import { GameRoot } from "./root";
import { enumSubShape, ShapeDefinition } from "./shape_definition"; import { enumSubShape, ShapeDefinition } from "./shape_definition";
import { enumHubGoalRewards, tutorialGoals } from "./tutorial_goals"; import { enumHubGoalRewards, tutorialGoals } from "./tutorial_goals";
@ -18,12 +20,6 @@ export class HubGoals extends BasicSerializableObject {
level: types.uint, level: types.uint,
storedShapes: types.keyValueMap(types.uint), storedShapes: types.keyValueMap(types.uint),
upgradeLevels: types.keyValueMap(types.uint), upgradeLevels: types.keyValueMap(types.uint),
currentGoal: types.structured({
definition: types.knownType(ShapeDefinition),
required: types.uint,
reward: types.nullable(types.enum(enumHubGoalRewards)),
}),
}; };
} }
@ -53,15 +49,7 @@ export class HubGoals extends BasicSerializableObject {
} }
// Compute current goal // Compute current goal
const goal = tutorialGoals[this.level - 1]; this.computeNextGoal();
if (goal) {
this.currentGoal = {
/** @type {ShapeDefinition} */
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(goal.shape),
required: goal.required,
reward: goal.reward,
};
}
} }
/** /**
@ -106,7 +94,7 @@ export class HubGoals extends BasicSerializableObject {
this.upgradeImprovements[key] = 1; this.upgradeImprovements[key] = 1;
} }
this.createNextGoal(); this.computeNextGoal();
// Allow quickly switching goals in dev mode // Allow quickly switching goals in dev mode
if (G_IS_DEV) { if (G_IS_DEV) {
@ -155,6 +143,13 @@ export class HubGoals extends BasicSerializableObject {
* Returns how much of the current goal was already delivered * Returns how much of the current goal was already delivered
*/ */
getCurrentGoalDelivered() { getCurrentGoalDelivered() {
if (this.currentGoal.throughputOnly) {
return this.root.productionAnalytics.getCurrentShapeRate(
enumAnalyticsDataSource.delivered,
this.currentGoal.definition
);
}
return this.getShapesStored(this.currentGoal.definition); return this.getShapesStored(this.currentGoal.definition);
} }
@ -189,9 +184,8 @@ export class HubGoals extends BasicSerializableObject {
this.root.signals.shapeDelivered.dispatch(definition); this.root.signals.shapeDelivered.dispatch(definition);
// Check if we have enough for the next level // Check if we have enough for the next level
const targetHash = this.currentGoal.definition.getHash();
if ( if (
this.storedShapes[targetHash] >= this.currentGoal.required || this.getCurrentGoalDelivered() >= this.currentGoal.required ||
(G_IS_DEV && globalConfig.debug.rewardsInstant) (G_IS_DEV && globalConfig.debug.rewardsInstant)
) { ) {
this.onGoalCompleted(); this.onGoalCompleted();
@ -201,24 +195,28 @@ export class HubGoals extends BasicSerializableObject {
/** /**
* Creates the next goal * Creates the next goal
*/ */
createNextGoal() { computeNextGoal() {
const storyIndex = this.level - 1; const storyIndex = this.level - 1;
if (storyIndex < tutorialGoals.length) { if (storyIndex < tutorialGoals.length) {
const { shape, required, reward } = tutorialGoals[storyIndex]; const { shape, required, reward, throughputOnly } = tutorialGoals[storyIndex];
this.currentGoal = { this.currentGoal = {
/** @type {ShapeDefinition} */ /** @type {ShapeDefinition} */
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape), definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape),
required, required,
reward, reward,
throughputOnly,
}; };
return; return;
} }
const required = 4 + (this.level - 27) * 0.25;
this.currentGoal = { this.currentGoal = {
/** @type {ShapeDefinition} */ /** @type {ShapeDefinition} */
definition: this.createRandomShape(), definition: this.computeFreeplayShape(this.level),
required: findNiceIntegerValue(1000 + Math.pow(this.level * 2000, 0.8)), required,
reward: enumHubGoalRewards.no_reward_freeplay, reward: enumHubGoalRewards.no_reward_freeplay,
throughputOnly: true,
}; };
} }
@ -231,7 +229,7 @@ export class HubGoals extends BasicSerializableObject {
this.root.app.gameAnalytics.handleLevelCompleted(this.level); this.root.app.gameAnalytics.handleLevelCompleted(this.level);
++this.level; ++this.level;
this.createNextGoal(); this.computeNextGoal();
this.root.signals.storyGoalCompleted.dispatch(this.level - 1, reward); this.root.signals.storyGoalCompleted.dispatch(this.level - 1, reward);
} }
@ -325,15 +323,85 @@ export class HubGoals extends BasicSerializableObject {
} }
/** /**
* Picks random colors which are close to each other
* @param {RandomNumberGenerator} rng
*/
generateRandomColorSet(rng, allowUncolored = false) {
const colorWheel = [
enumColors.red,
enumColors.yellow,
enumColors.green,
enumColors.cyan,
enumColors.blue,
enumColors.purple,
enumColors.red,
enumColors.yellow,
];
const universalColors = [enumColors.white];
if (allowUncolored) {
universalColors.push(enumColors.uncolored);
}
const index = rng.nextIntRangeInclusive(0, colorWheel.length - 3);
const pickedColors = colorWheel.slice(index, index + 3);
pickedColors.push(rng.choice(universalColors));
return pickedColors;
}
/**
* Creates a (seeded) random shape
* @param {number} level
* @returns {ShapeDefinition} * @returns {ShapeDefinition}
*/ */
createRandomShape() { computeFreeplayShape(level) {
const layerCount = clamp(this.level / 25, 2, 4); const layerCount = clamp(this.level / 25, 2, 4);
/** @type {Array<import("./shape_definition").ShapeLayer>} */ /** @type {Array<import("./shape_definition").ShapeLayer>} */
let layers = []; let layers = [];
const randomColor = () => randomChoice(Object.values(enumColors)); const rng = new RandomNumberGenerator(this.root.map.seed + "/" + level);
const randomShape = () => randomChoice(Object.values(enumSubShape));
const colors = this.generateRandomColorSet(rng, level > 35);
let pickedSymmetry = null; // pairs of quadrants that must be the same
let availableShapes = [enumSubShape.rect, enumSubShape.circle, enumSubShape.star];
if (rng.next() < 0.5) {
pickedSymmetry = [
// radial symmetry
[0, 2],
[1, 3],
];
availableShapes.push(enumSubShape.windmill); // windmill looks good only in radial symmetry
} else {
const symmetries = [
[
// horizontal axis
[0, 3],
[1, 2],
],
[
// vertical axis
[0, 1],
[2, 3],
],
[
// diagonal axis
[0, 2],
[1],
[3],
],
[
// other diagonal axis
[1, 3],
[0],
[2],
],
];
pickedSymmetry = rng.choice(symmetries);
}
const randomColor = () => rng.choice(colors);
const randomShape = () => rng.choice(Object.values(enumSubShape));
let anyIsMissingTwo = false; let anyIsMissingTwo = false;
@ -341,23 +409,24 @@ export class HubGoals extends BasicSerializableObject {
/** @type {import("./shape_definition").ShapeLayer} */ /** @type {import("./shape_definition").ShapeLayer} */
const layer = [null, null, null, null]; const layer = [null, null, null, null];
for (let quad = 0; quad < 4; ++quad) { for (let j = 0; j < pickedSymmetry.length; ++j) {
layer[quad] = { const group = pickedSymmetry[j];
subShape: randomShape(), const shape = randomShape();
color: randomColor(), const color = randomColor();
}; for (let k = 0; k < group.length; ++k) {
} const quad = group[k];
layer[quad] = {
// Sometimes shapes are missing subShape: shape,
if (Math.random() > 0.85) { color,
layer[randomInt(0, 3)] = null; };
}
} }
// Sometimes they actually are missing *two* ones! // Sometimes they actually are missing *two* ones!
// Make sure at max only one layer is missing it though, otherwise we could // Make sure at max only one layer is missing it though, otherwise we could
// create an uncreateable shape // create an uncreateable shape
if (Math.random() > 0.95 && !anyIsMissingTwo) { if (level > 75 && rng.next() > 0.95 && !anyIsMissingTwo) {
layer[randomInt(0, 3)] = null; layer[rng.nextIntRange(0, 4)] = null;
anyIsMissingTwo = true; anyIsMissingTwo = true;
} }

View File

@ -44,6 +44,8 @@ import { HUDWireInfo } from "./parts/wire_info";
import { HUDLeverToggle } from "./parts/lever_toggle"; import { HUDLeverToggle } from "./parts/lever_toggle";
import { HUDLayerPreview } from "./parts/layer_preview"; import { HUDLayerPreview } from "./parts/layer_preview";
import { HUDMinerHighlight } from "./parts/miner_highlight"; import { HUDMinerHighlight } from "./parts/miner_highlight";
import { HUDBetaOverlay } from "./parts/beta_overlay";
import { HUDPerformanceWarning } from "./parts/performance_warning";
export class GameHUD { export class GameHUD {
/** /**
@ -75,7 +77,6 @@ export class GameHUD {
pinnedShapes: new HUDPinnedShapes(this.root), pinnedShapes: new HUDPinnedShapes(this.root),
notifications: new HUDNotifications(this.root), notifications: new HUDNotifications(this.root),
settingsMenu: new HUDSettingsMenu(this.root), settingsMenu: new HUDSettingsMenu(this.root),
// betaOverlay: new HUDBetaOverlay(this.root),
debugInfo: new HUDDebugInfo(this.root), debugInfo: new HUDDebugInfo(this.root),
dialogs: new HUDModalDialogs(this.root), dialogs: new HUDModalDialogs(this.root),
screenshotExporter: new HUDScreenshotExporter(this.root), screenshotExporter: new HUDScreenshotExporter(this.root),
@ -85,6 +86,7 @@ export class GameHUD {
layerPreview: new HUDLayerPreview(this.root), layerPreview: new HUDLayerPreview(this.root),
minerHighlight: new HUDMinerHighlight(this.root), minerHighlight: new HUDMinerHighlight(this.root),
performanceWarning: new HUDPerformanceWarning(this.root),
// Typing hints // Typing hints
/* typehints:start */ /* typehints:start */
@ -137,6 +139,10 @@ export class GameHUD {
this.parts.sandboxController = new HUDSandboxController(this.root); this.parts.sandboxController = new HUDSandboxController(this.root);
} }
if (!G_IS_RELEASE) {
this.parts.betaOverlay = new HUDBetaOverlay(this.root);
}
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
for (const key in this.parts) { for (const key in this.parts) {
this.parts[key].createElements(frag); this.parts[key].createElements(frag);

View File

@ -3,7 +3,12 @@ import { makeDiv } from "../../../core/utils";
export class HUDBetaOverlay extends BaseHUDPart { export class HUDBetaOverlay extends BaseHUDPart {
createElements(parent) { createElements(parent) {
this.element = makeDiv(parent, "ingame_HUD_BetaOverlay", [], "CLOSED BETA"); this.element = makeDiv(
parent,
"ingame_HUD_BetaOverlay",
[],
"<h2>CLOSED BETA VERSION</h2><span>This version is unstable, might crash and is not final!</span>"
);
} }
initialize() {} initialize() {}

View File

@ -334,7 +334,11 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
const tileBelow = this.root.map.getLowerLayerContentXY(tile.x, tile.y); const tileBelow = this.root.map.getLowerLayerContentXY(tile.x, tile.y);
// Check if there's a shape or color item below, if so select the miner // Check if there's a shape or color item below, if so select the miner
if (tileBelow && this.root.app.settings.getAllSettings().pickMinerOnPatch) { if (
tileBelow &&
this.root.app.settings.getAllSettings().pickMinerOnPatch &&
this.root.currentLayer === "regular"
) {
this.currentMetaBuilding.set(gMetaBuildingRegistry.findByClass(MetaMinerBuilding)); this.currentMetaBuilding.set(gMetaBuildingRegistry.findByClass(MetaMinerBuilding));
// Select chained miner if available, since that's always desired once unlocked // Select chained miner if available, since that's always desired once unlocked

View File

@ -0,0 +1,16 @@
import { T } from "../../../translations";
import { BaseHUDPart } from "../base_hud_part";
export class HUDPerformanceWarning extends BaseHUDPart {
initialize() {
this.warningShown = false;
this.root.signals.entityManuallyPlaced.add(this.checkAfterPlace, this);
}
checkAfterPlace() {
if (!this.warningShown && this.root.entityMgr.entities.length > 10000) {
this.root.hud.parts.dialogs.showInfo(T.dialogs.entityWarning.title, T.dialogs.entityWarning.desc);
this.warningShown = true;
}
}
}

View File

@ -4,6 +4,8 @@ import { ShapeDefinition } from "../../shape_definition";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { blueprintShape, UPGRADES } from "../../upgrades"; import { blueprintShape, UPGRADES } from "../../upgrades";
import { enumHubGoalRewards } from "../../tutorial_goals"; import { enumHubGoalRewards } from "../../tutorial_goals";
import { enumAnalyticsDataSource } from "../../production_analytics";
import { T } from "../../../translations";
/** /**
* Manages the pinned shapes on the left side of the screen * Manages the pinned shapes on the left side of the screen
@ -22,11 +24,13 @@ export class HUDPinnedShapes extends BaseHUDPart {
* convenient. Also allows for cleaning up handles. * convenient. Also allows for cleaning up handles.
* @type {Array<{ * @type {Array<{
* key: string, * key: string,
* definition: ShapeDefinition,
* amountLabel: HTMLElement, * amountLabel: HTMLElement,
* lastRenderedValue: string, * lastRenderedValue: string,
* element: HTMLElement, * element: HTMLElement,
* detector?: ClickDetector, * detector?: ClickDetector,
* infoDetector?: ClickDetector * infoDetector?: ClickDetector,
* throughputOnly?: boolean
* }>} * }>}
*/ */
this.handles = []; this.handles = [];
@ -163,29 +167,40 @@ export class HUDPinnedShapes extends BaseHUDPart {
this.handles = []; this.handles = [];
// Pin story goal // Pin story goal
this.internalPinShape(currentKey, false, "goal"); this.internalPinShape({
key: currentKey,
canUnpin: false,
className: "goal",
throughputOnly: currentGoal.throughputOnly,
});
// Pin blueprint shape as well // Pin blueprint shape as well
if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) { if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) {
this.internalPinShape(blueprintShape, false, "blueprint"); this.internalPinShape({
key: blueprintShape,
canUnpin: false,
className: "blueprint",
});
} }
// Pin manually pinned shapes // Pin manually pinned shapes
for (let i = 0; i < this.pinnedShapes.length; ++i) { for (let i = 0; i < this.pinnedShapes.length; ++i) {
const key = this.pinnedShapes[i]; const key = this.pinnedShapes[i];
if (key !== currentKey) { if (key !== currentKey) {
this.internalPinShape(key); this.internalPinShape({ key });
} }
} }
} }
/** /**
* Pins a new shape * Pins a new shape
* @param {string} key * @param {object} param0
* @param {boolean} canUnpin * @param {string} param0.key
* @param {string=} className * @param {boolean=} param0.canUnpin
* @param {string=} param0.className
* @param {boolean=} param0.throughputOnly
*/ */
internalPinShape(key, canUnpin = true, className = null) { internalPinShape({ key, canUnpin = true, className = null, throughputOnly = false }) {
const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key); const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key);
const element = makeDiv(this.element, null, ["shape"]); const element = makeDiv(this.element, null, ["shape"]);
@ -229,11 +244,13 @@ export class HUDPinnedShapes extends BaseHUDPart {
this.handles.push({ this.handles.push({
key, key,
definition,
element, element,
amountLabel, amountLabel,
lastRenderedValue: "", lastRenderedValue: "",
detector, detector,
infoDetector, infoDetector,
throughputOnly,
}); });
} }
@ -244,8 +261,20 @@ export class HUDPinnedShapes extends BaseHUDPart {
for (let i = 0; i < this.handles.length; ++i) { for (let i = 0; i < this.handles.length; ++i) {
const handle = this.handles[i]; const handle = this.handles[i];
const currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key); let currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key);
const currentValueFormatted = formatBigNumber(currentValue); let currentValueFormatted = formatBigNumber(currentValue);
if (handle.throughputOnly) {
currentValue = this.root.productionAnalytics.getCurrentShapeRate(
enumAnalyticsDataSource.delivered,
handle.definition
);
currentValueFormatted = T.ingame.statistics.shapesDisplayUnits.second.replace(
"<shapes>",
String(currentValue)
);
}
if (currentValueFormatted !== handle.lastRenderedValue) { if (currentValueFormatted !== handle.lastRenderedValue) {
handle.lastRenderedValue = currentValueFormatted; handle.lastRenderedValue = currentValueFormatted;
handle.amountLabel.innerText = currentValueFormatted; handle.amountLabel.innerText = currentValueFormatted;

View File

@ -113,7 +113,7 @@ export class HUDSandboxController extends BaseHUDPart {
modifyLevel(amount) { modifyLevel(amount) {
const hubGoals = this.root.hubGoals; const hubGoals = this.root.hubGoals;
hubGoals.level = Math.max(1, hubGoals.level + amount); hubGoals.level = Math.max(1, hubGoals.level + amount);
hubGoals.createNextGoal(); hubGoals.computeNextGoal();
// Clear all shapes of this level // Clear all shapes of this level
hubGoals.storedShapes[hubGoals.currentGoal.definition.getHash()] = 0; hubGoals.storedShapes[hubGoals.currentGoal.definition.getHash()] = 0;

View File

@ -90,17 +90,15 @@ export class HUDShop extends BaseHUDPart {
// Max level // Max level
handle.elemDescription.innerText = T.ingame.shop.maximumLevel.replace( handle.elemDescription.innerText = T.ingame.shop.maximumLevel.replace(
"<currentMult>", "<currentMult>",
currentTierMultiplier.toString() formatBigNumber(currentTierMultiplier)
); );
continue; continue;
} }
// Set description // Set description
handle.elemDescription.innerText = T.shopUpgrades[upgradeId].description handle.elemDescription.innerText = T.shopUpgrades[upgradeId].description
.replace("<currentMult>", currentTierMultiplier.toString()) .replace("<currentMult>", formatBigNumber(currentTierMultiplier))
.replace("<newMult>", (currentTierMultiplier + tierHandle.improvement).toString()) .replace("<newMult>", formatBigNumber(currentTierMultiplier + tierHandle.improvement));
// Backwards compatibility
.replace("<gain>", (tierHandle.improvement * 100.0).toString());
tierHandle.required.forEach(({ shape, amount }) => { tierHandle.required.forEach(({ shape, amount }) => {
const container = makeDiv(handle.elemRequirements, null, ["requirement"]); const container = makeDiv(handle.elemRequirements, null, ["requirement"]);

View File

@ -4,11 +4,12 @@ import { makeDiv } from "../../../core/utils";
import { SOUNDS } from "../../../platform/sound"; import { SOUNDS } from "../../../platform/sound";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { defaultBuildingVariant } from "../../meta_building"; import { defaultBuildingVariant } from "../../meta_building";
import { enumHubGoalRewards } from "../../tutorial_goals"; import { enumHubGoalRewards, tutorialGoals } from "../../tutorial_goals";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings"; import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings";
import { InputReceiver } from "../../../core/input_receiver"; import { InputReceiver } from "../../../core/input_receiver";
import { enumNotificationType } from "./notifications";
export class HUDUnlockNotification extends BaseHUDPart { export class HUDUnlockNotification extends BaseHUDPart {
initialize() { initialize() {
@ -50,6 +51,14 @@ export class HUDUnlockNotification extends BaseHUDPart {
* @param {enumHubGoalRewards} reward * @param {enumHubGoalRewards} reward
*/ */
showForLevel(level, reward) { showForLevel(level, reward) {
if (level > tutorialGoals.length) {
this.root.hud.signals.notification.dispatch(
T.ingame.notifications.freeplayLevelComplete.replace("<level>", String(level)),
enumNotificationType.success
);
return;
}
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
this.elemTitle.innerText = T.ingame.levelCompleteNotification.levelTitle.replace( this.elemTitle.innerText = T.ingame.levelCompleteNotification.levelTitle.replace(
"<level>", "<level>",

View File

@ -1,12 +1,18 @@
import { makeOffscreenBuffer } from "../../../core/buffer_utils"; import { makeOffscreenBuffer } from "../../../core/buffer_utils";
import { globalConfig, IS_DEMO } from "../../../core/config"; import { globalConfig, IS_DEMO, THIRDPARTY_URLS } from "../../../core/config";
import { DrawParameters } from "../../../core/draw_parameters"; import { DrawParameters } from "../../../core/draw_parameters";
import { Loader } from "../../../core/loader"; import { Loader } from "../../../core/loader";
import { DialogWithForm } from "../../../core/modal_dialog_elements"; import { DialogWithForm } from "../../../core/modal_dialog_elements";
import { FormElementInput } from "../../../core/modal_dialog_forms"; import { FormElementInput } from "../../../core/modal_dialog_forms";
import { Rectangle } from "../../../core/rectangle"; import { Rectangle } from "../../../core/rectangle";
import { STOP_PROPAGATION } from "../../../core/signal"; import { STOP_PROPAGATION } from "../../../core/signal";
import { arrayDeleteValue, lerp, makeDiv, removeAllChildren } from "../../../core/utils"; import {
arrayDeleteValue,
fillInLinkIntoTranslation,
lerp,
makeDiv,
removeAllChildren,
} from "../../../core/utils";
import { Vector } from "../../../core/vector"; import { Vector } from "../../../core/vector";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { BaseItem } from "../../base_item"; import { BaseItem } from "../../base_item";
@ -272,7 +278,7 @@ export class HUDWaypoints extends BaseHUDPart {
const dialog = new DialogWithForm({ const dialog = new DialogWithForm({
app: this.root.app, app: this.root.app,
title: waypoint ? T.dialogs.createMarker.titleEdit : T.dialogs.createMarker.title, title: waypoint ? T.dialogs.createMarker.titleEdit : T.dialogs.createMarker.title,
desc: T.dialogs.createMarker.desc, desc: fillInLinkIntoTranslation(T.dialogs.createMarker.desc, THIRDPARTY_URLS.shapeViewer),
formElements: [markerNameInput], formElements: [markerNameInput],
buttons: waypoint ? ["delete:bad", "cancel", "ok:good"] : ["cancel", "ok:good"], buttons: waypoint ? ["delete:bad", "cancel", "ok:good"] : ["cancel", "ok:good"],
}); });

View File

@ -1,13 +1,14 @@
import { makeOffscreenBuffer } from "../../../core/buffer_utils"; import { makeOffscreenBuffer } from "../../../core/buffer_utils";
import { globalConfig } from "../../../core/config"; import { globalConfig } from "../../../core/config";
import { DrawParameters } from "../../../core/draw_parameters"; import { DrawParameters } from "../../../core/draw_parameters";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { THEME } from "../../theme";
import { BaseHUDPart } from "../base_hud_part";
import { Loader } from "../../../core/loader"; import { Loader } from "../../../core/loader";
import { lerp } from "../../../core/utils"; import { lerp } from "../../../core/utils";
import { SOUNDS } from "../../../platform/sound";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { enumHubGoalRewards } from "../../tutorial_goals"; import { enumHubGoalRewards } from "../../tutorial_goals";
import { BaseHUDPart } from "../base_hud_part";
const copy = require("clipboard-copy");
const wiresBackgroundDpi = 4; const wiresBackgroundDpi = 4;
export class HUDWiresOverlay extends BaseHUDPart { export class HUDWiresOverlay extends BaseHUDPart {
@ -16,6 +17,7 @@ export class HUDWiresOverlay extends BaseHUDPart {
initialize() { initialize() {
// Probably not the best location, but the one which makes most sense // Probably not the best location, but the one which makes most sense
this.root.keyMapper.getBinding(KEYMAPPINGS.ingame.switchLayers).add(this.switchLayers, this); this.root.keyMapper.getBinding(KEYMAPPINGS.ingame.switchLayers).add(this.switchLayers, this);
this.root.keyMapper.getBinding(KEYMAPPINGS.placement.copyWireValue).add(this.copyWireValue, this);
this.generateTilePattern(); this.generateTilePattern();
@ -54,7 +56,53 @@ export class HUDWiresOverlay extends BaseHUDPart {
update() { update() {
const desiredAlpha = this.root.currentLayer === "wires" ? 1.0 : 0.0; const desiredAlpha = this.root.currentLayer === "wires" ? 1.0 : 0.0;
this.currentAlpha = lerp(this.currentAlpha, desiredAlpha, 0.12);
// On low performance, skip the fade
if (this.root.entityMgr.entities.length > 5000 || this.root.dynamicTickrate.averageFps < 50) {
this.currentAlpha = desiredAlpha;
} else {
this.currentAlpha = lerp(this.currentAlpha, desiredAlpha, 0.12);
}
}
/**
* Copies the wires value below the cursor
*/
copyWireValue() {
if (this.root.currentLayer !== "wires") {
return;
}
const mousePos = this.root.app.mousePosition;
if (!mousePos) {
return;
}
const tile = this.root.camera.screenToWorld(mousePos).toTileSpace();
const contents = this.root.map.getLayerContentXY(tile.x, tile.y, "wires");
if (!contents) {
return;
}
let value = null;
if (contents.components.Wire) {
const network = contents.components.Wire.linkedNetwork;
if (network && network.hasValue()) {
value = network.currentValue;
}
}
if (contents.components.ConstantSignal) {
value = contents.components.ConstantSignal.signal;
}
if (value) {
copy(value.getAsCopyableKey());
this.root.soundProxy.playUi(SOUNDS.copy);
} else {
copy("");
this.root.soundProxy.playUiError();
}
} }
/** /**

View File

@ -8,6 +8,9 @@ import { MetaVirtualProcessorBuilding } from "../../buildings/virtual_processor"
import { MetaTransistorBuilding } from "../../buildings/transistor"; import { MetaTransistorBuilding } from "../../buildings/transistor";
import { MetaAnalyzerBuilding } from "../../buildings/analyzer"; import { MetaAnalyzerBuilding } from "../../buildings/analyzer";
import { MetaComparatorBuilding } from "../../buildings/comparator"; import { MetaComparatorBuilding } from "../../buildings/comparator";
import { MetaReaderBuilding } from "../../buildings/reader";
import { MetaFilterBuilding } from "../../buildings/filter";
import { MetaDisplayBuilding } from "../../buildings/display";
export class HUDWiresToolbar extends HUDBaseToolbar { export class HUDWiresToolbar extends HUDBaseToolbar {
constructor(root) { constructor(root) {
@ -16,13 +19,18 @@ export class HUDWiresToolbar extends HUDBaseToolbar {
MetaWireBuilding, MetaWireBuilding,
MetaWireTunnelBuilding, MetaWireTunnelBuilding,
MetaConstantSignalBuilding, MetaConstantSignalBuilding,
MetaLeverBuilding,
MetaLogicGateBuilding, MetaLogicGateBuilding,
MetaVirtualProcessorBuilding, MetaVirtualProcessorBuilding,
MetaAnalyzerBuilding, MetaAnalyzerBuilding,
MetaComparatorBuilding, MetaComparatorBuilding,
MetaTransistorBuilding, MetaTransistorBuilding,
], ],
secondaryBuildings: [
MetaReaderBuilding,
MetaLeverBuilding,
MetaFilterBuilding,
MetaDisplayBuilding,
],
visibilityCondition: () => visibilityCondition: () =>
!this.root.camera.getIsMapOverlayActive() && this.root.currentLayer === "wires", !this.root.camera.getIsMapOverlayActive() && this.root.currentLayer === "wires",
htmlElementId: "ingame_HUD_wires_toolbar", htmlElementId: "ingame_HUD_wires_toolbar",

View File

@ -26,6 +26,13 @@ export class BooleanItem extends BaseItem {
return "boolean"; return "boolean";
} }
/**
* @returns {string}
*/
getAsCopyableKey() {
return this.value ? "1" : "0";
}
/** /**
* @param {number} value * @param {number} value
*/ */
@ -56,6 +63,21 @@ export class BooleanItem extends BaseItem {
} }
sprite.drawCachedCentered(parameters, x, y, diameter); sprite.drawCachedCentered(parameters, x, y, diameter);
} }
/**
* Draws the item to a canvas
* @param {CanvasRenderingContext2D} context
* @param {number} size
*/
drawFullSizeOnCanvas(context, size) {
let sprite;
if (this.value) {
sprite = Loader.getSprite("sprites/wires/boolean_true.png");
} else {
sprite = Loader.getSprite("sprites/wires/boolean_false.png");
}
sprite.drawCentered(context, size / 2, size / 2, size);
}
} }
export const BOOL_FALSE_SINGLETON = new BooleanItem(0); export const BOOL_FALSE_SINGLETON = new BooleanItem(0);

View File

@ -28,6 +28,13 @@ export class ColorItem extends BaseItem {
return "color"; return "color";
} }
/**
* @returns {string}
*/
getAsCopyableKey() {
return this.color;
}
/** /**
* @param {BaseItem} other * @param {BaseItem} other
*/ */
@ -47,6 +54,18 @@ export class ColorItem extends BaseItem {
return THEME.map.resources[this.color]; return THEME.map.resources[this.color];
} }
/**
* Draws the item to a canvas
* @param {CanvasRenderingContext2D} context
* @param {number} size
*/
drawFullSizeOnCanvas(context, size) {
if (!this.cachedSprite) {
this.cachedSprite = Loader.getSprite("sprites/colors/" + this.color + ".png");
}
this.cachedSprite.drawCentered(context, size / 2, size / 2, size);
}
/** /**
* @param {number} x * @param {number} x
* @param {number} y * @param {number} y

View File

@ -1,62 +1,78 @@
import { DrawParameters } from "../../core/draw_parameters"; import { DrawParameters } from "../../core/draw_parameters";
import { types } from "../../savegame/serialization"; import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item"; import { BaseItem } from "../base_item";
import { ShapeDefinition } from "../shape_definition"; import { ShapeDefinition } from "../shape_definition";
import { THEME } from "../theme"; import { THEME } from "../theme";
import { globalConfig } from "../../core/config"; import { globalConfig } from "../../core/config";
export class ShapeItem extends BaseItem { export class ShapeItem extends BaseItem {
static getId() { static getId() {
return "shape"; return "shape";
} }
static getSchema() { static getSchema() {
return types.string; return types.string;
} }
serialize() { serialize() {
return this.definition.getHash(); return this.definition.getHash();
} }
deserialize(data) { deserialize(data) {
this.definition = ShapeDefinition.fromShortKey(data); this.definition = ShapeDefinition.fromShortKey(data);
} }
/** @returns {"shape"} **/ /** @returns {"shape"} **/
getItemType() { getItemType() {
return "shape"; return "shape";
} }
/** /**
* @param {BaseItem} other * @returns {string}
*/ */
equalsImpl(other) { getAsCopyableKey() {
return this.definition.getHash() === /** @type {ShapeItem} */ (other).definition.getHash(); return this.definition.getHash();
} }
/** /**
* @param {ShapeDefinition} definition * @param {BaseItem} other
*/ */
constructor(definition) { equalsImpl(other) {
super(); return this.definition.getHash() === /** @type {ShapeItem} */ (other).definition.getHash();
}
/**
* This property must not be modified on runtime, you have to clone the class in order to change the definition /**
*/ * @param {ShapeDefinition} definition
this.definition = definition; */
} constructor(definition) {
super();
getBackgroundColorAsResource() {
return THEME.map.resources.shape; /**
} * This property must not be modified on runtime, you have to clone the class in order to change the definition
*/
/** this.definition = definition;
* @param {number} x }
* @param {number} y
* @param {DrawParameters} parameters getBackgroundColorAsResource() {
* @param {number=} diameter return THEME.map.resources.shape;
*/ }
drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
this.definition.drawCentered(x, y, parameters, diameter); /**
} * Draws the item to a canvas
} * @param {CanvasRenderingContext2D} context
* @param {number} size
*/
drawFullSizeOnCanvas(context, size) {
this.definition.drawFullSizeOnCanvas(context, size);
}
/**
* @param {number} x
* @param {number} y
* @param {DrawParameters} parameters
* @param {number=} diameter
*/
drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
this.definition.drawCentered(x, y, parameters, diameter);
}
}

View File

@ -67,12 +67,11 @@ export const KEYMAPPINGS = {
wire: { keyCode: key("1") }, wire: { keyCode: key("1") },
wire_tunnel: { keyCode: key("2") }, wire_tunnel: { keyCode: key("2") },
constant_signal: { keyCode: key("3") }, constant_signal: { keyCode: key("3") },
lever_wires: { keyCode: key("4") }, logic_gate: { keyCode: key("4") },
logic_gate: { keyCode: key("5") }, virtual_processor: { keyCode: key("5") },
virtual_processor: { keyCode: key("6") }, analyzer: { keyCode: key("6") },
analyzer: { keyCode: key("7") }, comparator: { keyCode: key("7") },
comparator: { keyCode: key("8") }, transistor: { keyCode: key("8") },
transistor: { keyCode: key("9") },
}, },
placement: { placement: {
@ -82,6 +81,8 @@ export const KEYMAPPINGS = {
cycleBuildingVariants: { keyCode: key("T") }, cycleBuildingVariants: { keyCode: key("T") },
cycleBuildings: { keyCode: 9 }, // TAB cycleBuildings: { keyCode: 9 }, // TAB
switchDirectionLockSide: { keyCode: key("R") }, switchDirectionLockSide: { keyCode: key("R") },
copyWireValue: { keyCode: key("Z") },
}, },
massSelect: { massSelect: {

View File

@ -297,6 +297,15 @@ export class ShapeDefinition extends BasicSerializableObject {
parameters.context.drawImage(canvas, x - diameter / 2, y - diameter / 2, diameter, diameter); parameters.context.drawImage(canvas, x - diameter / 2, y - diameter / 2, diameter, diameter);
} }
/**
* Draws the item to a canvas
* @param {CanvasRenderingContext2D} context
* @param {number} size
*/
drawFullSizeOnCanvas(context, size) {
this.internalGenerateShapeBuffer(null, context, size, size, 1);
}
/** /**
* Generates this shape as a canvas * Generates this shape as a canvas
* @param {number} size * @param {number} size

View File

@ -1,6 +1,9 @@
import trim from "trim"; import trim from "trim";
import { THIRDPARTY_URLS } from "../../core/config";
import { DialogWithForm } from "../../core/modal_dialog_elements"; import { DialogWithForm } from "../../core/modal_dialog_elements";
import { FormElementInput } from "../../core/modal_dialog_forms"; import { FormElementInput, FormElementItemChooser } from "../../core/modal_dialog_forms";
import { fillInLinkIntoTranslation } from "../../core/utils";
import { T } from "../../translations";
import { BaseItem } from "../base_item"; import { BaseItem } from "../base_item";
import { enumColors } from "../colors"; import { enumColors } from "../colors";
import { ConstantSignalComponent } from "../components/constant_signal"; import { ConstantSignalComponent } from "../components/constant_signal";
@ -9,6 +12,7 @@ import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item"; import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item";
import { COLOR_ITEM_SINGLETONS } from "../items/color_item"; import { COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { ShapeDefinition } from "../shape_definition"; import { ShapeDefinition } from "../shape_definition";
import { blueprintShape } from "../upgrades";
export class ConstantSignalSystem extends GameSystemWithFilter { export class ConstantSignalSystem extends GameSystemWithFilter {
constructor(root) { constructor(root) {
@ -41,23 +45,35 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
const signalValueInput = new FormElementInput({ const signalValueInput = new FormElementInput({
id: "signalValue", id: "signalValue",
label: null, label: fillInLinkIntoTranslation(T.dialogs.editSignal.descShortKey, THIRDPARTY_URLS.shapeViewer),
placeholder: "", placeholder: "",
defaultValue: "", defaultValue: "",
validator: val => this.parseSignalCode(val), validator: val => this.parseSignalCode(val),
}); });
const itemInput = new FormElementItemChooser({
id: "signalItem",
label: null,
items: [
BOOL_FALSE_SINGLETON,
BOOL_TRUE_SINGLETON,
...Object.values(COLOR_ITEM_SINGLETONS),
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(blueprintShape),
],
});
const dialog = new DialogWithForm({ const dialog = new DialogWithForm({
app: this.root.app, app: this.root.app,
title: "Set Signal", title: T.dialogs.editSignal.title,
desc: "Enter a shape code, color or '0' or '1'", desc: T.dialogs.editSignal.descItems,
formElements: [signalValueInput], formElements: [itemInput, signalValueInput],
buttons: ["cancel:bad:escape", "ok:good:enter"], buttons: ["cancel:bad:escape", "ok:good:enter"],
closeButton: false, closeButton: false,
}); });
this.root.hud.parts.dialogs.internalShowDialog(dialog); this.root.hud.parts.dialogs.internalShowDialog(dialog);
// When confirmed, set the signal // When confirmed, set the signal
dialog.buttonSignals.ok.add(() => { const closeHandler = () => {
if (!this.root || !this.root.entityMgr) { if (!this.root || !this.root.entityMgr) {
// Game got stopped // Game got stopped
return; return;
@ -75,8 +91,16 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
return; return;
} }
constantComp.signal = this.parseSignalCode(signalValueInput.getValue()); if (itemInput.chosenItem) {
}); console.log(itemInput.chosenItem);
constantComp.signal = itemInput.chosenItem;
} else {
constantComp.signal = this.parseSignalCode(signalValueInput.getValue());
}
};
dialog.buttonSignals.ok.add(closeHandler);
dialog.valueChosen.add(closeHandler);
// When cancelled, destroy the entity again // When cancelled, destroy the entity again
dialog.buttonSignals.cancel.add(() => { dialog.buttonSignals.cancel.add(() => {

View File

@ -1,15 +1,15 @@
import { globalConfig } from "../../core/config";
import { smoothenDpi } from "../../core/dpi_manager";
import { DrawParameters } from "../../core/draw_parameters"; import { DrawParameters } from "../../core/draw_parameters";
import { drawSpriteClipped } from "../../core/draw_utils";
import { Loader } from "../../core/loader"; import { Loader } from "../../core/loader";
import { Rectangle } from "../../core/rectangle";
import { ORIGINAL_SPRITE_SCALE } from "../../core/sprites";
import { formatBigNumber } from "../../core/utils"; import { formatBigNumber } from "../../core/utils";
import { T } from "../../translations"; import { T } from "../../translations";
import { HubComponent } from "../components/hub"; import { HubComponent } from "../components/hub";
import { Entity } from "../entity"; import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter"; import { GameSystemWithFilter } from "../game_system_with_filter";
import { globalConfig } from "../../core/config";
import { smoothenDpi } from "../../core/dpi_manager";
import { drawSpriteClipped } from "../../core/draw_utils";
import { Rectangle } from "../../core/rectangle";
import { ORIGINAL_SPRITE_SCALE } from "../../core/sprites";
const HUB_SIZE_TILES = 4; const HUB_SIZE_TILES = 4;
const HUB_SIZE_PIXELS = HUB_SIZE_TILES * globalConfig.tileSize; const HUB_SIZE_PIXELS = HUB_SIZE_TILES * globalConfig.tileSize;
@ -73,25 +73,36 @@ export class HubSystem extends GameSystemWithFilter {
const textOffsetX = 70; const textOffsetX = 70;
const textOffsetY = 61; const textOffsetY = 61;
// Deliver count if (goals.throughputOnly) {
const delivered = this.root.hubGoals.getCurrentGoalDelivered(); // Throughput
const deliveredText = "" + formatBigNumber(delivered); const deliveredText = T.ingame.statistics.shapesDisplayUnits.second.replace(
"<shapes>",
formatBigNumber(goals.required)
);
if (delivered > 9999) { context.font = "bold 12px GameFont";
context.font = "bold 16px GameFont"; context.fillStyle = "#64666e";
} else if (delivered > 999) { context.textAlign = "left";
context.font = "bold 20px GameFont"; context.fillText(deliveredText, textOffsetX, textOffsetY);
} else { } else {
context.font = "bold 25px GameFont"; // Deliver count
} const delivered = this.root.hubGoals.getCurrentGoalDelivered();
context.fillStyle = "#64666e"; const deliveredText = "" + formatBigNumber(delivered);
context.textAlign = "left";
context.fillText(deliveredText, textOffsetX, textOffsetY);
// Required if (delivered > 999) {
context.font = "13px GameFont"; context.font = "bold 16px GameFont";
context.fillStyle = "#a4a6b0"; } else {
context.fillText("/ " + formatBigNumber(goals.required), textOffsetX, textOffsetY + 13); context.font = "bold 25px GameFont";
}
context.fillStyle = "#64666e";
context.textAlign = "left";
context.fillText(deliveredText, textOffsetX, textOffsetY);
// Required
context.font = "13px GameFont";
context.fillStyle = "#a4a6b0";
context.fillText("/ " + formatBigNumber(goals.required), textOffsetX, textOffsetY + 13);
}
// Reward // Reward
const rewardText = T.storyRewards[goals.reward].title.toUpperCase(); const rewardText = T.storyRewards[goals.reward].title.toUpperCase();

View File

@ -3,7 +3,7 @@ import { enumColors } from "../colors";
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate"; import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
import { enumPinSlotType } from "../components/wired_pins"; import { enumPinSlotType } from "../components/wired_pins";
import { GameSystemWithFilter } from "../game_system_with_filter"; import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON, isTruthyItem } from "../items/boolean_item"; import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON, BooleanItem, isTruthyItem } from "../items/boolean_item";
import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item"; import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { ShapeItem } from "../items/shape_item"; import { ShapeItem } from "../items/shape_item";
import { ShapeDefinition } from "../shape_definition"; import { ShapeDefinition } from "../shape_definition";

View File

@ -143,10 +143,10 @@ export const tutorialGoals = [
// 14 // 14
// Belt reader // Belt reader
{ {
// @todo shape: "--Cg----:--Cr----", // unused
shape: "CuCuCuCu", required: 16, // Per second!
required: 0,
reward: enumHubGoalRewards.reward_belt_reader, reward: enumHubGoalRewards.reward_belt_reader,
throughputOnly: true,
}, },
// 15 // 15
@ -176,8 +176,7 @@ export const tutorialGoals = [
// 18 // 18
// Rotater (180deg) // Rotater (180deg)
{ {
// @TODO shape: "Sg----Sg:CgCgCgCg:--CyCy--", // unused
shape: "CuCuCuCu",
required: 20000, required: 20000,
reward: enumHubGoalRewards.reward_rotater_180, reward: enumHubGoalRewards.reward_rotater_180,
}, },
@ -185,8 +184,7 @@ export const tutorialGoals = [
// 19 // 19
// Compact splitter // Compact splitter
{ {
// @TODO shape: "CpRpCp--:SwSwSwSw",
shape: "CuCuCuCu",
required: 25000, required: 25000,
reward: enumHubGoalRewards.reward_splitter, reward: enumHubGoalRewards.reward_splitter,
}, },
@ -195,15 +193,14 @@ export const tutorialGoals = [
// WIRES // WIRES
{ {
shape: finalGameShape, shape: finalGameShape,
required: 50000, required: 25000,
reward: enumHubGoalRewards.reward_wires_filters_and_levers, reward: enumHubGoalRewards.reward_wires_filters_and_levers,
}, },
// 21 // 21
// Display // Display
{ {
// @TODO shape: "CrCrCrCr:CwCwCwCw:CrCrCrCr:CwCwCwCw",
shape: "CuCuCuCu",
required: 25000, required: 25000,
reward: enumHubGoalRewards.reward_display, reward: enumHubGoalRewards.reward_display,
}, },
@ -211,43 +208,37 @@ export const tutorialGoals = [
// 22 // 22
// Constant signal // Constant signal
{ {
// @TODO shape: "Cg----Cr:Cw----Cw:Sy------:Cy----Cy",
shape: "CuCuCuCu", required: 25000,
required: 30000,
reward: enumHubGoalRewards.reward_constant_signal, reward: enumHubGoalRewards.reward_constant_signal,
}, },
// 23 // 23
// Quad Painter // Quad Painter
{ {
// @TODO shape: "CcSyCcSy:SyCcSyCc:CcSyCcSy",
shape: "CuCuCuCu", required: 5000,
// shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", // processors t4 (two variants)
required: 35000,
reward: enumHubGoalRewards.reward_painter_quad, reward: enumHubGoalRewards.reward_painter_quad,
}, },
// 24 Logic gates // 24 Logic gates
{ {
// @TODO shape: "CcRcCcRc:RwCwRwCw:Sr--Sw--:CyCyCyCy",
shape: "CuCuCuCu", required: 10000,
required: 40000,
reward: enumHubGoalRewards.reward_logic_gates, reward: enumHubGoalRewards.reward_logic_gates,
}, },
// 25 Virtual Processing // 25 Virtual Processing
{ {
// @TODO shape: "Rg--Rg--:CwRwCwRw:--Rg--Rg",
shape: "CuCuCuCu", required: 10000,
required: 45000,
reward: enumHubGoalRewards.reward_virtual_processing, reward: enumHubGoalRewards.reward_virtual_processing,
}, },
// 26 Freeplay // 26 Freeplay
{ {
// @TODO shape: "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw",
shape: "CuCuCuCu", required: 10000,
required: 100000,
reward: enumHubGoalRewards.reward_freeplay, reward: enumHubGoalRewards.reward_freeplay,
}, },
]; ];

View File

@ -2,10 +2,28 @@ import { findNiceIntegerValue } from "../core/utils";
import { ShapeDefinition } from "./shape_definition"; import { ShapeDefinition } from "./shape_definition";
export const finalGameShape = "RuCw--Cw:----Ru--"; export const finalGameShape = "RuCw--Cw:----Ru--";
export const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
export const blueprintShape = "CbCbCbRb:CwCwCwCw"; export const blueprintShape = "CbCbCbRb:CwCwCwCw";
const fixedImprovements = [0.5, 0.5, 1, 1, 2, 2]; const fixedImprovements = [0.5, 0.5, 1, 1, 2, 2];
const numEndgameUpgrades = G_IS_DEV || G_IS_STANDALONE ? 20 - fixedImprovements.length - 1 : 0;
function generateEndgameUpgrades() {
return new Array(numEndgameUpgrades).fill(null).map((_, i) => ({
required: [
{ shape: blueprintShape, amount: 30000 + i * 10000 },
{ shape: finalGameShape, amount: 20000 + i * 5000 },
{ shape: rocketShape, amount: 20000 + i * 5000 },
],
excludePrevious: true,
}));
}
for (let i = 0; i < numEndgameUpgrades; ++i) {
fixedImprovements.push(0.1);
}
/** @typedef {{ /** @typedef {{
* shape: string, * shape: string,
* amount: number * amount: number
@ -41,6 +59,7 @@ export const UPGRADES = {
required: [{ shape: finalGameShape, amount: 50000 }], required: [{ shape: finalGameShape, amount: 50000 }],
excludePrevious: true, excludePrevious: true,
}, },
...generateEndgameUpgrades(),
], ],
miner: [ miner: [
@ -63,6 +82,7 @@ export const UPGRADES = {
required: [{ shape: finalGameShape, amount: 50000 }], required: [{ shape: finalGameShape, amount: 50000 }],
excludePrevious: true, excludePrevious: true,
}, },
...generateEndgameUpgrades(),
], ],
processors: [ processors: [
@ -85,6 +105,7 @@ export const UPGRADES = {
required: [{ shape: finalGameShape, amount: 50000 }], required: [{ shape: finalGameShape, amount: 50000 }],
excludePrevious: true, excludePrevious: true,
}, },
...generateEndgameUpgrades(),
], ],
painting: [ painting: [
@ -107,6 +128,7 @@ export const UPGRADES = {
required: [{ shape: finalGameShape, amount: 50000 }], required: [{ shape: finalGameShape, amount: 50000 }],
excludePrevious: true, excludePrevious: true,
}, },
...generateEndgameUpgrades(),
], ],
}; };

3
src/js/globals.d.ts vendored
View File

@ -19,9 +19,6 @@ declare const G_BUILD_VERSION: string;
declare const G_ALL_UI_IMAGES: Array<string>; declare const G_ALL_UI_IMAGES: Array<string>;
declare const G_IS_RELEASE: boolean; declare const G_IS_RELEASE: boolean;
// Node require
declare function require(...args): any;
// Polyfills // Polyfills
declare interface String { declare interface String {
replaceAll(search: string, replacement: string): string; replaceAll(search: string, replacement: string): string;

View File

@ -25,6 +25,7 @@ export const SOUNDS = {
destroyBuilding: "destroy_building", destroyBuilding: "destroy_building",
placeBuilding: "place_building", placeBuilding: "place_building",
placeBelt: "place_belt", placeBelt: "place_belt",
copy: "copy",
}; };
export const MUSIC = { export const MUSIC = {

View File

@ -19,6 +19,7 @@ import { getCodeFromBuildingData } from "../../game/building_codes.js";
import { StaticMapEntityComponent } from "../../game/components/static_map_entity.js"; import { StaticMapEntityComponent } from "../../game/components/static_map_entity.js";
import { Entity } from "../../game/entity.js"; import { Entity } from "../../game/entity.js";
import { defaultBuildingVariant, MetaBuilding } from "../../game/meta_building.js"; import { defaultBuildingVariant, MetaBuilding } from "../../game/meta_building.js";
import { finalGameShape } from "../../game/upgrades.js";
import { SavegameInterface_V1005 } from "./1005.js"; import { SavegameInterface_V1005 } from "./1005.js";
const schema = require("./1006.json"); const schema = require("./1006.json");
@ -151,11 +152,26 @@ export class SavegameInterface_V1006 extends SavegameInterface_V1005 {
stored[shapeKey] = rebalance(stored[shapeKey]); stored[shapeKey] = rebalance(stored[shapeKey]);
} }
stored[finalGameShape] = 0;
// Reduce goals // Reduce goals
if (dump.hubGoals.currentGoal) { if (dump.hubGoals.currentGoal) {
dump.hubGoals.currentGoal.required = rebalance(dump.hubGoals.currentGoal.required); dump.hubGoals.currentGoal.required = rebalance(dump.hubGoals.currentGoal.required);
} }
let level = Math.min(19, dump.hubGoals.level);
const levelMapping = {
14: 15,
15: 16,
16: 17,
17: 18,
18: 19,
19: 20,
};
dump.hubGoals.level = levelMapping[level] || level;
// Update entities // Update entities
const entities = dump.entities; const entities = dump.entities;
for (let i = 0; i < entities.length; ++i) { for (let i = 0; i < entities.length; ++i) {

View File

@ -1,82 +1,80 @@
// Synchronizes all translations // Synchronizes all translations
const fs = require("fs"); const fs = require("fs");
const matchAll = require("match-all"); const matchAll = require("match-all");
const path = require("path"); const path = require("path");
const YAWN = require("yawn-yaml/cjs"); const YAML = require("yaml");
const YAML = require("yaml");
const files = fs
const files = fs .readdirSync(path.join(__dirname, "translations"))
.readdirSync(path.join(__dirname, "translations")) .filter(x => x.endsWith(".yaml"))
.filter(x => x.endsWith(".yaml")) .filter(x => x.indexOf("base-en") < 0);
.filter(x => x.indexOf("base-en") < 0);
const originalContents = fs
const originalContents = fs .readFileSync(path.join(__dirname, "translations", "base-en.yaml"))
.readFileSync(path.join(__dirname, "translations", "base-en.yaml")) .toString("utf-8");
.toString("utf-8");
const original = YAML.parse(originalContents);
const original = YAML.parse(originalContents);
const placeholderRegexp = /[[<]([a-zA-Z_0-9]+)[\]<]/gi;
const placeholderRegexp = /[[<]([a-zA-Z_0-9]+)[\]<]/gi;
function match(originalObj, translatedObj, path = "/") {
function match(originalObj, translatedObj, path = "/") { for (const key in originalObj) {
for (const key in originalObj) { if (!translatedObj.hasOwnProperty(key)) {
if (!translatedObj.hasOwnProperty(key)) { console.warn(" | Missing key", path + key);
console.warn(" | Missing key", path + key); translatedObj[key] = originalObj[key];
translatedObj[key] = originalObj[key]; continue;
continue; }
} const valueOriginal = originalObj[key];
const valueOriginal = originalObj[key]; const valueMatching = translatedObj[key];
const valueMatching = translatedObj[key]; if (typeof valueOriginal !== typeof valueMatching) {
if (typeof valueOriginal !== typeof valueMatching) { console.warn(" | MISMATCHING type (obj|non-obj) in", path + key);
console.warn(" | MISMATCHING type (obj|non-obj) in", path + key); continue;
continue; }
}
if (typeof valueOriginal === "object") {
if (typeof valueOriginal === "object") { match(valueOriginal, valueMatching, path + key + "/");
match(valueOriginal, valueMatching, path + key + "/"); } else if (typeof valueOriginal === "string") {
} else if (typeof valueOriginal === "string") { // todo
// todo const originalPlaceholders = matchAll(valueOriginal, placeholderRegexp).toArray();
const originalPlaceholders = matchAll(valueOriginal, placeholderRegexp).toArray(); const translatedPlaceholders = matchAll(valueMatching, placeholderRegexp).toArray();
const translatedPlaceholders = matchAll(valueMatching, placeholderRegexp).toArray();
if (originalPlaceholders.length !== translatedPlaceholders.length) {
if (originalPlaceholders.length !== translatedPlaceholders.length) { console.warn(
console.warn( " | Mismatching placeholders in",
" | Mismatching placeholders in", path + key,
path + key, "->",
"->", originalPlaceholders,
originalPlaceholders, "vs",
"vs", translatedPlaceholders
translatedPlaceholders );
); translatedObj[key] = originalObj[key];
translatedObj[key] = originalObj[key]; continue;
continue; }
} } else {
} else { console.warn(" | Unknown type: ", typeof valueOriginal);
console.warn(" | Unknown type: ", typeof valueOriginal); }
} }
// const matching = translatedObj[key]; for (const key in translatedObj) {
} if (!originalObj.hasOwnProperty(key)) {
console.warn(" | Obsolete key", path + key);
for (const key in translatedObj) { delete translatedObj[key];
if (!originalObj.hasOwnProperty(key)) { }
console.warn(" | Obsolete key", path + key); }
delete translatedObj[key]; }
}
} for (let i = 0; i < files.length; ++i) {
} const filePath = path.join(__dirname, "translations", files[i]);
console.log("Processing", files[i]);
for (let i = 0; i < files.length; ++i) { const translatedContents = fs.readFileSync(filePath).toString("utf-8");
const filePath = path.join(__dirname, "translations", files[i]);
console.log("Processing", files[i]); const json = YAML.parse(translatedContents);
const translatedContents = fs.readFileSync(filePath).toString("utf-8"); match(original, json, "/");
const translated = YAML.parse(translatedContents);
const handle = new YAWN(translatedContents); const stringified = YAML.stringify(json, {
indent: 4,
const json = handle.json; simpleKeys: true,
match(original, json, "/"); });
handle.json = json; fs.writeFileSync(filePath, stringified, "utf-8");
}
fs.writeFileSync(filePath, handle.yaml, "utf-8");
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -268,7 +268,13 @@ dialogs:
createMarker: createMarker:
title: New Marker title: New Marker
titleEdit: Edit Marker titleEdit: Edit Marker
desc: Give it a meaningful name, you can also include a <strong>short key</strong> of a shape (Which you can generate <a href="https://viewer.shapez.io" target="_blank">here</a>) desc: Give it a meaningful name, you can also include a <strong>short key</strong> of a shape (Which you can generate <link>here</link>)
editSignal:
title: Set Signal
descItems: >-
Choose a pre-defined item:
descShortKey: ... or enter the <strong>short key</strong> of a shape (Which you can generate <link>here</link>)
markerDemoLimit: markerDemoLimit:
desc: You can only create two custom markers in the demo. Get the standalone for unlimited markers! desc: You can only create two custom markers in the demo. Get the standalone for unlimited markers!
@ -281,6 +287,10 @@ dialogs:
title: Rename Savegame title: Rename Savegame
desc: You can rename your savegame here. desc: You can rename your savegame here.
entityWarning:
title: Performance Warning
desc: You have placed a lot of buildings, this is just a friendly reminder that the game can not handle an endless count of buildings - So try to keep your factories compact!
ingame: ingame:
# This is shown in the top left corner and displays useful keybindings in # This is shown in the top left corner and displays useful keybindings in
# every situation # every situation
@ -350,6 +360,7 @@ ingame:
notifications: notifications:
newUpgrade: A new upgrade is available! newUpgrade: A new upgrade is available!
gameSaved: Your game has been saved. gameSaved: Your game has been saved.
freeplayLevelComplete: Level <level> has been completed!
# The "Upgrades" window # The "Upgrades" window
shop: shop:
@ -360,7 +371,8 @@ ingame:
tier: Tier <x> tier: Tier <x>
# The roman number for each tier # The roman number for each tier
tierLabels: [I, II, III, IV, V, VI, VII, VIII, IX, X] tierLabels:
[I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, XIII, XIV, XV, XVI, XVII, XVIII, XIX, XX]
maximumLevel: MAXIMUM LEVEL (Speed x<currentMult>) maximumLevel: MAXIMUM LEVEL (Speed x<currentMult>)
@ -788,13 +800,13 @@ storyRewards:
no_reward_freeplay: no_reward_freeplay:
title: Next level title: Next level
desc: >- desc: >-
Congratulations! By the way, more content is planned for the standalone! Congratulations!
reward_freeplay: reward_freeplay:
title: Freeplay title: Freeplay
desc: >- desc: >-
You did it! You unlocked the <strong>free-play mode</strong>! This means that shapes are now <strong>randomly</strong> generated!<br><br> You did it! You unlocked the <strong>free-play mode</strong>! This means that shapes are now <strong>randomly</strong> generated!<br><br>
Since the hub will only require low quantities from now on, I highly recommend to build a machine which automatically delivers the requested shape!<br><br> Since the hub will require a <strong>throughput</strong> from now on, I highly recommend to build a machine which automatically delivers the requested shape!<br><br>
The HUB outputs the requested shape on the wires layer, so all you have to do is to analyze it and automatically configure your factory based on that. The HUB outputs the requested shape on the wires layer, so all you have to do is to analyze it and automatically configure your factory based on that.
settings: settings:
@ -1039,8 +1051,7 @@ keybindings:
wire: *wire wire: *wire
constant_signal: *constant_signal constant_signal: *constant_signal
logic_gate: Logic Gate logic_gate: Logic Gate
lever: Switch (regular) lever: *lever
lever_wires: Switch (wires)
filter: *filter filter: *filter
wire_tunnel: *wire_tunnel wire_tunnel: *wire_tunnel
display: *display display: *display
@ -1062,7 +1073,8 @@ keybindings:
lockBeltDirection: Enable belt planner lockBeltDirection: Enable belt planner
switchDirectionLockSide: >- switchDirectionLockSide: >-
Planner: Switch side Planner: Switch side
copyWireValue: >-
Wires: Copy value below cursor
massSelectStart: Hold and drag to start massSelectStart: Hold and drag to start
massSelectSelectMultiple: Select multiple areas massSelectSelectMultiple: Select multiple areas
massSelectCopy: Copy area massSelectCopy: Copy area

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More