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

Merge branch 'master' into patch-1

This commit is contained in:
BananoGamer 2020-10-08 19:26:52 +02:00 committed by GitHub
commit 105cfea1ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
129 changed files with 6889 additions and 7049 deletions

8
.editorconfig Executable file
View File

@ -0,0 +1,8 @@
root = true
[{src, translations}/*]
end_of_line = crlf
insert_final_newline = true
indent_style = space
indent_size = 4
charset = utf-8

View File

@ -1,30 +1,31 @@
FROM node:12 as base
EXPOSE 3001 3005
WORKDIR /shapez.io
COPY . .
EXPOSE 3005
EXPOSE 3001
RUN apt-get update \
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get dist-upgrade -y \
&& apt-get install -y --no-install-recommends \
ffmpeg \
RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg default-jre \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
FROM base as shape_base
COPY package.json yarn.lock ./
RUN yarn
COPY gulp ./gulp
WORKDIR /shapez.io/gulp
RUN yarn
WORKDIR /shapez.io
RUN yarn
COPY res ./res
COPY src/html ./src/html
COPY src/css ./src/css
COPY version ./version
COPY sync-translations.js ./
COPY translations ./translations
COPY src/js ./src/js
COPY res_raw ./res_raw
COPY .git ./.git
WORKDIR /shapez.io/gulp
RUN yarn
WORKDIR /shapez.io/gulp
ENTRYPOINT ["yarn", "gulp"]

View File

@ -86,8 +86,16 @@ gulp.task("utils.cleanBuildTempFolder", () => {
.src(path.join(__dirname, "..", "src", "js", "built-temp"), { read: false, allowEmpty: true })
.pipe($.clean({ force: true }));
});
gulp.task("utils.cleanImageBuildFolder", () => {
return gulp
.src(path.join(__dirname, "res_built"), { read: false, allowEmpty: true })
.pipe($.clean({ force: true }));
});
gulp.task("utils.cleanup", gulp.series("utils.cleanBuildFolder", "utils.cleanBuildTempFolder"));
gulp.task(
"utils.cleanup",
gulp.series("utils.cleanBuildFolder", "utils.cleanImageBuildFolder", "utils.cleanBuildTempFolder")
);
// Requires no uncomitted files
gulp.task("utils.requireCleanWorkingTree", cb => {
@ -234,12 +242,13 @@ gulp.task(
"build.standalone.dev",
gulp.series(
"utils.cleanup",
"imgres.buildAtlas",
"imgres.atlasToJson",
"imgres.atlas",
"sounds.dev",
"imgres.copyImageResources",
"imgres.copyNonImageResources",
"translations.fullBuild",
"js.standalone-dev",
"css.dev",
"html.standalone-dev"
)

View File

@ -173,6 +173,8 @@ function gulptasksImageResources($, gulp, buildFolder) {
gulp.task(
"imgres.allOptimized",
gulp.parallel(
"imgres.buildAtlas",
"imgres.atlasToJson",
"imgres.atlasOptimized",
"imgres.copyNonImageResources",
"imgres.copyImageResourcesOptimized"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1,36 +1,38 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path style="fill:#FF4B55;" d="M400,0H112C50.144,0,0,50.144,0,112v288c0,61.856,50.144,112,112,112h288 c61.856,0,112-50.144,112-112V112C512,50.144,461.856,0,400,0z"/>
<polygon style="fill:#F5F5F5;" points="512,229.517 211.862,229.517 211.862,0 158.897,0 158.897,229.517 0,229.517 0,282.483 158.897,282.483 158.897,512 211.862,512 211.862,282.483 512,282.483 "/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<rect style="fill:#FF4B55;" width="512" height="512"/>
<polygon style="fill:#F5F5F5;" points="512,229.517 211.862,229.517 211.862,0 158.897,0 158.897,229.517 0,229.517 0,282.483
158.897,282.483 158.897,512 211.862,512 211.862,282.483 512,282.483 "/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 858 B

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -27,7 +27,7 @@
@include S(border-radius, $globalBorderRadius);
@include DarkThemeOverride {
background-color: rgba(darken($darkModeGameBackground, 15), 0.4);
background-color: rgba(darken($darkModeGameBackground, 15), 0.95);
}
&.secondary {

View File

@ -17,13 +17,10 @@
grid-template-rows: 1fr 1fr;
@include S(margin-bottom, 4px);
color: #333438;
// text-shadow: #{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.2);
&.unpinable {
> canvas {
cursor: pointer;
pointer-events: all;
}
&.removable {
cursor: pointer;
pointer-events: all;
}
> canvas {
@ -31,16 +28,9 @@
@include S(height, 25px);
grid-column: 1 / 2;
grid-row: 1 / 3;
pointer-events: all;
transition: transform 0.1s ease-in-out;
transform-origin: D(2px) center;
will-change: transform;
position: relative;
pointer-events: none;
z-index: 20;
&:hover {
transform: scale(2);
z-index: 21;
}
position: relative;
}
> .amountLabel,

View File

@ -73,8 +73,8 @@ ingame_HUD_KeybindingOverlay,
ingame_HUD_Notifications,
ingame_HUD_DebugInfo,
ingame_HUD_EntityDebugger,
ingame_HUD_InteractiveTutorial,
ingame_HUD_TutorialHints,
ingame_HUD_InteractiveTutorial,
ingame_HUD_BuildingsToolbar,
ingame_HUD_wires_toolbar,
ingame_HUD_BlueprintPlacer,

View File

@ -29,6 +29,7 @@ import { MobileWarningState } from "./states/mobile_warning";
import { PreloadState } from "./states/preload";
import { SettingsState } from "./states/settings";
import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
import { RestrictionManager } from "./core/restriction_manager";
/**
* @typedef {import("./platform/game_analytics").GameAnalyticsInterface} GameAnalyticsInterface
@ -70,6 +71,9 @@ export class Application {
this.inputMgr = new InputDistributor(this);
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
// Restrictions (Like demo etc)
this.restrictionMgr = new RestrictionManager(this);
// Platform dependent stuff
/** @type {StorageInterface} */

View File

@ -1,5 +1,3 @@
import { queryParamOptions } from "./query_parameters";
export const IS_DEBUG =
G_IS_DEV &&
typeof window !== "undefined" &&
@ -7,13 +5,10 @@ export const IS_DEBUG =
(window.location.host.indexOf("localhost:") >= 0 || window.location.host.indexOf("192.168.0.") >= 0) &&
window.location.search.indexOf("nodebug") < 0;
export const IS_DEMO = queryParamOptions.fullVersion
? false
: (!G_IS_DEV && !G_IS_STANDALONE) ||
(typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0);
export const SUPPORT_TOUCH = false;
export const IS_MAC = navigator.platform.toLowerCase().indexOf("mac") >= 0;
const smoothCanvas = true;
export const THIRDPARTY_URLS = {
@ -64,7 +59,7 @@ export const globalConfig = {
undergroundBeltMaxTilesByTier: [5, 9],
readerAnalyzeIntervalSeconds: G_IS_DEV ? 3 : 10,
readerAnalyzeIntervalSeconds: 10,
buildingSpeeds: {
cutter: 1 / 4,
@ -137,3 +132,8 @@ if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
if (globalConfig.debug.fastGameEnter) {
globalConfig.debug.noArtificialDelays = true;
}
if (G_IS_DEV && globalConfig.debug.noArtificialDelays) {
globalConfig.warmupTimeSecondsFast = 0;
globalConfig.warmupTimeSecondsRegular = 0;
}

View File

@ -13,6 +13,17 @@ import { getStringForKeyCode } from "../game/key_action_mapper";
import { createLogger } from "./logging";
import { T } from "../translations";
/*
* ***************************************************
*
* LEGACY CODE WARNING
*
* This is old code from yorg3.io and needs to be refactored
* @TODO
*
* ***************************************************
*/
const kbEnter = 13;
const kbCancel = 27;

View File

@ -2,6 +2,17 @@ import { BaseItem } from "../game/base_item";
import { ClickDetector } from "./click_detector";
import { Signal } from "./signal";
/*
* ***************************************************
*
* LEGACY CODE WARNING
*
* This is old code from yorg3.io and needs to be refactored
* @TODO
*
* ***************************************************
*/
export class FormElement {
constructor(id, label) {
this.id = id;

View File

@ -81,10 +81,6 @@ export class ReadWriteProxy {
return this.writeAsync();
}
getCurrentData() {
return this.currentData;
}
/**
*
* @param {object} obj

View File

@ -0,0 +1,155 @@
/* typehints:start */
import { Application } from "../application";
/* typehints:end */
import { IS_MAC } from "./config";
import { ExplainedResult } from "./explained_result";
import { queryParamOptions } from "./query_parameters";
import { ReadWriteProxy } from "./read_write_proxy";
export class RestrictionManager extends ReadWriteProxy {
/**
* @param {Application} app
*/
constructor(app) {
super(app, "restriction-flags.bin");
this.currentData = this.getDefaultData();
}
// -- RW Proxy Impl
/**
* @param {any} data
*/
verify(data) {
return ExplainedResult.good();
}
/**
*/
getDefaultData() {
return {
version: this.getCurrentVersion(),
savegameV1119Imported: false,
};
}
/**
*/
getCurrentVersion() {
return 1;
}
/**
* @param {any} data
*/
migrate(data) {
// Todo
return ExplainedResult.good();
}
initialize() {
return this.readAsync().then(() => {
if (this.currentData.savegameV1119Imported) {
console.warn("Levelunlock is granted to current user due to past savegame");
}
});
}
// -- End RW Proxy Impl
/**
* Checks if there are any savegames from the 1.1.19 version
*/
onHasLegacySavegamesChanged(has119Savegames = false) {
if (has119Savegames && !this.currentData.savegameV1119Imported) {
this.currentData.savegameV1119Imported = true;
console.warn("Current user now has access to all levels due to 1119 savegame");
return this.writeAsync();
}
return Promise.resolve();
}
/**
* Returns if the app is currently running as the limited version
* @returns {boolean}
*/
isLimitedVersion() {
if (IS_MAC) {
// On mac, the full version is always active
return false;
}
if (G_IS_STANDALONE) {
// Standalone is never limited
return false;
}
if (queryParamOptions.fullVersion) {
// Full version is activated via flag
return false;
}
if (G_IS_DEV) {
return typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0;
}
return true;
}
/**
* Returns if the app markets the standalone version on steam
* @returns {boolean}
*/
getIsStandaloneMarketingActive() {
return this.isLimitedVersion();
}
/**
* Returns if exporting the base as a screenshot is possible
* @returns {boolean}
*/
getIsExportingScreenshotsPossible() {
return !this.isLimitedVersion();
}
/**
* Returns the maximum number of supported waypoints
* @returns {number}
*/
getMaximumWaypoints() {
return this.isLimitedVersion() ? 2 : 1e20;
}
/**
* Returns if the user has unlimited savegames
* @returns {boolean}
*/
getHasUnlimitedSavegames() {
return !this.isLimitedVersion();
}
/**
* Returns if the app has all settings available
* @returns {boolean}
*/
getHasExtendedSettings() {
return !this.isLimitedVersion();
}
/**
* Returns if all upgrades are available
* @returns {boolean}
*/
getHasExtendedUpgrades() {
return !this.isLimitedVersion() || this.currentData.savegameV1119Imported;
}
/**
* Returns if all levels & freeplay is available
* @returns {boolean}
*/
getHasExtendedLevelsAndFreeplay() {
return !this.isLimitedVersion() || this.currentData.savegameV1119Imported;
}
}

View File

@ -108,17 +108,6 @@ export class RandomNumberGenerator {
assert(max > min, "rng: max <= min");
return Math.floor(this.next() * (max - min) + min);
}
/**
* @param {number} min
* @param {number} max
* @returns {number} Integer in range [min, max]
*/
nextIntRangeInclusive(min, max) {
assert(Number.isFinite(min), "Minimum is no integer");
assert(Number.isFinite(max), "Maximum is no integer");
assert(max > min, "rng: max <= min");
return Math.round(this.next() * (max - min) + min);
}
/**
* @param {number} min

View File

@ -681,3 +681,72 @@ export function fillInLinkIntoTranslation(translation, link) {
.replace("<link>", "<a href='" + link + "' target='_blank'>")
.replace("</link>", "</a>");
}
/**
* Generates a file download
* @param {string} filename
* @param {string} text
*/
export function generateFileDownload(filename, text) {
var element = document.createElement("a");
element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text));
element.setAttribute("download", filename);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
/**
* Starts a file chooser
* @param {string} acceptedType
*/
export function startFileChoose(acceptedType = ".bin") {
var input = document.createElement("input");
input.type = "file";
input.accept = acceptedType;
return new Promise(resolve => {
input.onchange = _ => resolve(input.files[0]);
input.click();
});
}
const romanLiterals = [
"0", // NULL
"I",
"II",
"III",
"IV",
"V",
"VI",
"VII",
"VIII",
"IX",
"X",
"XI",
"XII",
"XIII",
"XIV",
"XV",
"XVI",
"XVII",
"XVIII",
"XIX",
"XX",
];
/**
*
* @param {number} number
* @returns {string}
*/
export function getRomanNumber(number) {
number = Math.max(0, Math.round(number));
if (number < romanLiterals.length) {
return romanLiterals[number];
}
return String(number);
}

View File

@ -1111,7 +1111,7 @@ export class BeltPath extends BasicSerializableObject {
isFirstItemProcessed = false;
this.spacingToFirstItem += clampedProgress;
if (remainingVelocity < 0.01) {
if (remainingVelocity < 1e-7) {
break;
}
}

View File

@ -1,13 +1,9 @@
import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters";
import { createLogger } from "../core/logging";
import { findNiceIntegerValue } from "../core/utils";
import { Vector } from "../core/vector";
import { Entity } from "./entity";
import { GameRoot } from "./root";
import { blueprintShape } from "./upgrades";
const logger = createLogger("blueprint");
export class Blueprint {
/**
@ -142,7 +138,7 @@ export class Blueprint {
* @param {GameRoot} root
*/
canAfford(root) {
return root.hubGoals.getShapesStoredByKey(blueprintShape) >= this.getCost();
return root.hubGoals.getShapesStoredByKey(root.gameMode.getBlueprintShapeKey()) >= this.getCost();
}
/**

View File

@ -7,6 +7,7 @@ import { BeltComponent } from "../components/belt";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { THEME } from "../theme";
export const arrayBeltVariantToRotation = [enumDirection.top, enumDirection.left, enumDirection.right];
@ -22,7 +23,7 @@ export class MetaBeltBuilding extends MetaBuilding {
}
getSilhouetteColor() {
return "#777";
return THEME.map.chunkOverview.beltColor;
}
getPlacementSound() {

View File

@ -110,12 +110,7 @@ export class MetaVirtualProcessorBuilding extends MetaBuilding {
pinComp.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.left,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 0),
direction: enumDirection.right,
direction: enumDirection.top,
type: enumPinSlotType.logicalEjector,
},
{

View File

@ -511,7 +511,11 @@ export class Camera extends BasicSerializableObject {
this.clampZoomLevel();
this.desiredZoom = null;
const mousePosition = this.root.app.mousePosition;
let mousePosition = this.root.app.mousePosition;
if (!this.root.app.settings.getAllSettings().zoomToCursor) {
mousePosition = new Vector(this.root.gameWidth / 2, this.root.gameHeight / 2);
}
if (mousePosition) {
const worldPos = this.root.camera.screenToWorld(mousePosition);
const worldDelta = worldPos.sub(this.center);

View File

@ -26,7 +26,7 @@ export class ItemEjectorComponent extends Component {
static getSchema() {
// The cachedDestSlot, cachedTargetEntity fields are not serialized.
return {
slots: types.array(
slots: types.fixedSizeArray(
types.structured({
item: types.nullable(typeItemSingleton),
progress: types.float,

View File

@ -31,7 +31,7 @@ export class WiredPinsComponent extends Component {
static getSchema() {
return {
slots: types.array(
slots: types.fixedSizeArray(
types.structured({
value: types.nullable(typeItemSingleton),
})

View File

@ -31,6 +31,7 @@ import { KeyActionMapper } from "./key_action_mapper";
import { GameLogic } from "./logic";
import { MapView } from "./map_view";
import { defaultBuildingVariant } from "./meta_building";
import { RegularGameMode } from "./modes/regular";
import { ProductionAnalytics } from "./production_analytics";
import { GameRoot } from "./root";
import { ShapeDefinitionManager } from "./shape_definition_manager";
@ -101,6 +102,9 @@ export class GameCore {
// Needs to come first
root.dynamicTickrate = new DynamicTickrate(root);
// Init game mode
root.gameMode = new RegularGameMode(root);
// Init classes
root.camera = new Camera(root);
root.map = new MapView(root);

71
src/js/game/game_mode.js Normal file
View File

@ -0,0 +1,71 @@
/* typehints:start */
import { enumHubGoalRewards } from "./tutorial_goals";
/* typehints:end */
import { GameRoot } from "./root";
/** @typedef {{
* shape: string,
* amount: number
* }} UpgradeRequirement */
/** @typedef {{
* required: Array<UpgradeRequirement>
* improvement?: number,
* excludePrevious?: boolean
* }} TierRequirement */
/** @typedef {Array<TierRequirement>} UpgradeTiers */
/** @typedef {{
* shape: string,
* required: number,
* reward: enumHubGoalRewards,
* throughputOnly?: boolean
* }} LevelDefinition */
export class GameMode {
/**
*
* @param {GameRoot} root
*/
constructor(root) {
this.root = root;
}
/**
* Should return all available upgrades
* @returns {Object<string, UpgradeTiers>}
*/
getUpgrades() {
abstract;
return null;
}
/**
* Returns the blueprint shape key
* @returns {string}
*/
getBlueprintShapeKey() {
abstract;
return null;
}
/**
* Returns the goals for all levels including their reward
* @returns {Array<LevelDefinition>}
*/
getLevelDefinitions() {
abstract;
return null;
}
/**
* Should return whether free play is available or if the game stops
* after the predefined levels
* @returns {boolean}
*/
getIsFreeplayAvailable() {
return true;
}
}

View File

@ -1,14 +1,13 @@
import { globalConfig, IS_DEMO } from "../core/config";
import { globalConfig } from "../core/config";
import { RandomNumberGenerator } from "../core/rng";
import { clamp, findNiceIntegerValue, randomChoice, randomInt } from "../core/utils";
import { clamp } from "../core/utils";
import { BasicSerializableObject, types } from "../savegame/serialization";
import { enumColors } from "./colors";
import { enumItemProcessorTypes } from "./components/item_processor";
import { enumAnalyticsDataSource } from "./production_analytics";
import { GameRoot } from "./root";
import { enumSubShape, ShapeDefinition } from "./shape_definition";
import { enumHubGoalRewards, tutorialGoals } from "./tutorial_goals";
import { UPGRADES } from "./upgrades";
import { enumHubGoalRewards } from "./tutorial_goals";
export class HubGoals extends BasicSerializableObject {
static getId() {
@ -23,27 +22,36 @@ export class HubGoals extends BasicSerializableObject {
};
}
deserialize(data) {
/**
*
* @param {*} data
* @param {GameRoot} root
*/
deserialize(data, root) {
const errorCode = super.deserialize(data);
if (errorCode) {
return errorCode;
}
if (IS_DEMO) {
this.level = Math.min(this.level, tutorialGoals.length);
const levels = root.gameMode.getLevelDefinitions();
// If freeplay is not available, clamp the level
if (!root.gameMode.getIsFreeplayAvailable()) {
this.level = Math.min(this.level, levels.length);
}
// Compute gained rewards
for (let i = 0; i < this.level - 1; ++i) {
if (i < tutorialGoals.length) {
const reward = tutorialGoals[i].reward;
if (i < levels.length) {
const reward = levels[i].reward;
this.gainedRewards[reward] = (this.gainedRewards[reward] || 0) + 1;
}
}
// Compute upgrade improvements
for (const upgradeId in UPGRADES) {
const tiers = UPGRADES[upgradeId];
const upgrades = this.root.gameMode.getUpgrades();
for (const upgradeId in upgrades) {
const tiers = upgrades[upgradeId];
const level = this.upgradeLevels[upgradeId] || 0;
let totalImprovement = 1;
for (let i = 0; i < level; ++i) {
@ -84,17 +92,16 @@ export class HubGoals extends BasicSerializableObject {
*/
this.upgradeLevels = {};
// Reset levels
for (const key in UPGRADES) {
this.upgradeLevels[key] = 0;
}
/**
* Stores the improvements for all upgrades
* @type {Object<string, number>}
*/
this.upgradeImprovements = {};
for (const key in UPGRADES) {
// Reset levels first
const upgrades = this.root.gameMode.getUpgrades();
for (const key in upgrades) {
this.upgradeLevels[key] = 0;
this.upgradeImprovements[key] = 1;
}
@ -120,7 +127,10 @@ export class HubGoals extends BasicSerializableObject {
* @returns {boolean}
*/
isEndOfDemoReached() {
return IS_DEMO && this.level >= tutorialGoals.length;
return (
!this.root.gameMode.getIsFreeplayAvailable() &&
this.level >= this.root.gameMode.getLevelDefinitions().length
);
}
/**
@ -215,8 +225,9 @@ export class HubGoals extends BasicSerializableObject {
*/
computeNextGoal() {
const storyIndex = this.level - 1;
if (storyIndex < tutorialGoals.length) {
const { shape, required, reward, throughputOnly } = tutorialGoals[storyIndex];
const levels = this.root.gameMode.getLevelDefinitions();
if (storyIndex < levels.length) {
const { shape, required, reward, throughputOnly } = levels[storyIndex];
this.currentGoal = {
/** @type {ShapeDefinition} */
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape),
@ -254,7 +265,7 @@ export class HubGoals extends BasicSerializableObject {
* Returns whether we are playing in free-play
*/
isFreePlay() {
return this.level >= tutorialGoals.length;
return this.level >= this.root.gameMode.getLevelDefinitions().length;
}
/**
@ -262,7 +273,7 @@ export class HubGoals extends BasicSerializableObject {
* @param {string} upgradeId
*/
canUnlockUpgrade(upgradeId) {
const tiers = UPGRADES[upgradeId];
const tiers = this.root.gameMode.getUpgrades()[upgradeId];
const currentLevel = this.getUpgradeLevel(upgradeId);
if (currentLevel >= tiers.length) {
@ -270,11 +281,6 @@ export class HubGoals extends BasicSerializableObject {
return false;
}
if (IS_DEMO && currentLevel >= 4) {
// DEMO
return false;
}
if (G_IS_DEV && globalConfig.debug.upgradesNoCost) {
return true;
}
@ -296,7 +302,7 @@ export class HubGoals extends BasicSerializableObject {
*/
getAvailableUpgradeCount() {
let count = 0;
for (const upgradeId in UPGRADES) {
for (const upgradeId in this.root.gameMode.getUpgrades()) {
if (this.canUnlockUpgrade(upgradeId)) {
++count;
}
@ -314,7 +320,7 @@ export class HubGoals extends BasicSerializableObject {
return false;
}
const upgradeTiers = UPGRADES[upgradeId];
const upgradeTiers = this.root.gameMode.getUpgrades()[upgradeId];
const currentLevel = this.getUpgradeLevel(upgradeId);
const tierData = upgradeTiers[currentLevel];
@ -363,7 +369,7 @@ export class HubGoals extends BasicSerializableObject {
if (allowUncolored) {
universalColors.push(enumColors.uncolored);
}
const index = rng.nextIntRangeInclusive(0, colorWheel.length - 3);
const index = rng.nextIntRange(0, colorWheel.length - 2);
const pickedColors = colorWheel.slice(index, index + 3);
pickedColors.push(rng.choice(universalColors));
return pickedColors;

View File

@ -15,7 +15,7 @@ import { HUDKeybindingOverlay } from "./parts/keybinding_overlay";
import { HUDUnlockNotification } from "./parts/unlock_notification";
import { HUDGameMenu } from "./parts/game_menu";
import { HUDShop } from "./parts/shop";
import { IS_MOBILE, globalConfig, IS_DEMO } from "../../core/config";
import { IS_MOBILE, globalConfig } from "../../core/config";
import { HUDMassSelector } from "./parts/mass_selector";
import { HUDVignetteOverlay } from "./parts/vignette_overlay";
import { HUDStatistics } from "./parts/statistics";
@ -45,7 +45,6 @@ import { HUDLeverToggle } from "./parts/lever_toggle";
import { HUDLayerPreview } from "./parts/layer_preview";
import { HUDMinerHighlight } from "./parts/miner_highlight";
import { HUDBetaOverlay } from "./parts/beta_overlay";
import { HUDPerformanceWarning } from "./parts/performance_warning";
import { HUDStandaloneAdvantages } from "./parts/standalone_advantages";
import { HUDCatMemes } from "./parts/cat_memes";
@ -88,7 +87,6 @@ export class GameHUD {
layerPreview: new HUDLayerPreview(this.root),
minerHighlight: new HUDMinerHighlight(this.root),
performanceWarning: new HUDPerformanceWarning(this.root),
// Typing hints
/* typehints:start */
@ -116,7 +114,7 @@ export class GameHUD {
this.parts.entityDebugger = new HUDEntityDebugger(this.root);
}
if (IS_DEMO) {
if (this.root.app.restrictionMgr.getIsStandaloneMarketingActive()) {
this.parts.watermark = new HUDWatermark(this.root);
this.parts.standaloneAdvantages = new HUDStandaloneAdvantages(this.root);
this.parts.catMemes = new HUDCatMemes(this.root);

View File

@ -7,7 +7,7 @@ export class HUDBetaOverlay extends BaseHUDPart {
parent,
"ingame_HUD_BetaOverlay",
[],
"<h2>UNSTABLE BETA VERSION</h2><span>Steam Release: 9th October 2020!</span>"
"<h2>UNSTABLE BETA VERSION</h2><span>Unfinalized & potential buggy content!</span>"
);
}

View File

@ -1,202 +1,203 @@
import { DrawParameters } from "../../../core/draw_parameters";
import { STOP_PROPAGATION } from "../../../core/signal";
import { TrackedState } from "../../../core/tracked_state";
import { makeDiv } from "../../../core/utils";
import { Vector } from "../../../core/vector";
import { T } from "../../../translations";
import { enumMouseButton } from "../../camera";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { blueprintShape } from "../../upgrades";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach";
import { Blueprint } from "../../blueprint";
import { SOUNDS } from "../../../platform/sound";
export class HUDBlueprintPlacer extends BaseHUDPart {
createElements(parent) {
const blueprintCostShape = this.root.shapeDefinitionMgr.getShapeFromShortKey(blueprintShape);
const blueprintCostShapeCanvas = blueprintCostShape.generateAsCanvas(80);
this.costDisplayParent = makeDiv(parent, "ingame_HUD_BlueprintPlacer", [], ``);
makeDiv(this.costDisplayParent, null, ["label"], T.ingame.blueprintPlacer.cost);
const costContainer = makeDiv(this.costDisplayParent, null, ["costContainer"], "");
this.costDisplayText = makeDiv(costContainer, null, ["costText"], "");
costContainer.appendChild(blueprintCostShapeCanvas);
}
initialize() {
this.root.hud.signals.buildingsSelectedForCopy.add(this.createBlueprintFromBuildings, this);
/** @type {TypedTrackedState<Blueprint?>} */
this.currentBlueprint = new TrackedState(this.onBlueprintChanged, this);
/** @type {Blueprint?} */
this.lastBlueprintUsed = null;
const keyActionMapper = this.root.keyMapper;
keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this);
keyActionMapper.getBinding(KEYMAPPINGS.placement.pipette).add(this.abortPlacement, this);
keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.rotateBlueprint, this);
keyActionMapper.getBinding(KEYMAPPINGS.massSelect.pasteLastBlueprint).add(this.pasteBlueprint, this);
this.root.camera.downPreHandler.add(this.onMouseDown, this);
this.root.camera.movePreHandler.add(this.onMouseMove, this);
this.root.hud.signals.selectedPlacementBuildingChanged.add(this.abortPlacement, this);
this.root.signals.editModeChanged.add(this.onEditModeChanged, this);
this.domAttach = new DynamicDomAttach(this.root, this.costDisplayParent);
this.trackedCanAfford = new TrackedState(this.onCanAffordChanged, this);
}
abortPlacement() {
if (this.currentBlueprint.get()) {
this.currentBlueprint.set(null);
return STOP_PROPAGATION;
}
}
/**
* Called when the layer was changed
* @param {Layer} layer
*/
onEditModeChanged(layer) {
// Check if the layer of the blueprint differs and thus we have to deselect it
const blueprint = this.currentBlueprint.get();
if (blueprint) {
if (blueprint.layer !== layer) {
this.currentBlueprint.set(null);
}
}
}
/**
* Called when the blueprint is now affordable or not
* @param {boolean} canAfford
*/
onCanAffordChanged(canAfford) {
this.costDisplayParent.classList.toggle("canAfford", canAfford);
}
update() {
const currentBlueprint = this.currentBlueprint.get();
this.domAttach.update(currentBlueprint && currentBlueprint.getCost() > 0);
this.trackedCanAfford.set(currentBlueprint && currentBlueprint.canAfford(this.root));
}
/**
* Called when the blueprint was changed
* @param {Blueprint} blueprint
*/
onBlueprintChanged(blueprint) {
if (blueprint) {
this.lastBlueprintUsed = blueprint;
this.costDisplayText.innerText = "" + blueprint.getCost();
}
}
/**
* mouse down pre handler
* @param {Vector} pos
* @param {enumMouseButton} button
*/
onMouseDown(pos, button) {
if (button === enumMouseButton.right) {
if (this.currentBlueprint.get()) {
this.abortPlacement();
return STOP_PROPAGATION;
}
}
const blueprint = this.currentBlueprint.get();
if (!blueprint) {
return;
}
if (!blueprint.canAfford(this.root)) {
this.root.soundProxy.playUiError();
return;
}
const worldPos = this.root.camera.screenToWorld(pos);
const tile = worldPos.toTileSpace();
if (blueprint.tryPlace(this.root, tile)) {
const cost = blueprint.getCost();
this.root.hubGoals.takeShapeByKey(blueprintShape, cost);
this.root.soundProxy.playUi(SOUNDS.placeBuilding);
}
}
/**
* Mose move handler
*/
onMouseMove() {
// Prevent movement while blueprint is selected
if (this.currentBlueprint.get()) {
return STOP_PROPAGATION;
}
}
/**
* Called when an array of bulidings was selected
* @param {Array<number>} uids
*/
createBlueprintFromBuildings(uids) {
if (uids.length === 0) {
return;
}
this.currentBlueprint.set(Blueprint.fromUids(this.root, uids));
}
/**
* Attempts to rotate the current blueprint
*/
rotateBlueprint() {
if (this.currentBlueprint.get()) {
if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).pressed) {
this.currentBlueprint.get().rotateCcw();
} else {
this.currentBlueprint.get().rotateCw();
}
}
}
/**
* Attempts to paste the last blueprint
*/
pasteBlueprint() {
if (this.lastBlueprintUsed !== null) {
if (this.lastBlueprintUsed.layer !== this.root.currentLayer) {
// Not compatible
this.root.soundProxy.playUiError();
return;
}
this.root.hud.signals.pasteBlueprintRequested.dispatch();
this.currentBlueprint.set(this.lastBlueprintUsed);
} else {
this.root.soundProxy.playUiError();
}
}
/**
*
* @param {DrawParameters} parameters
*/
draw(parameters) {
const blueprint = this.currentBlueprint.get();
if (!blueprint) {
return;
}
const mousePosition = this.root.app.mousePosition;
if (!mousePosition) {
// Not on screen
return;
}
const worldPos = this.root.camera.screenToWorld(mousePosition);
const tile = worldPos.toTileSpace();
blueprint.draw(parameters, tile);
}
}
import { DrawParameters } from "../../../core/draw_parameters";
import { STOP_PROPAGATION } from "../../../core/signal";
import { TrackedState } from "../../../core/tracked_state";
import { makeDiv } from "../../../core/utils";
import { Vector } from "../../../core/vector";
import { SOUNDS } from "../../../platform/sound";
import { T } from "../../../translations";
import { Blueprint } from "../../blueprint";
import { enumMouseButton } from "../../camera";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach";
export class HUDBlueprintPlacer extends BaseHUDPart {
createElements(parent) {
const blueprintCostShape = this.root.shapeDefinitionMgr.getShapeFromShortKey(
this.root.gameMode.getBlueprintShapeKey()
);
const blueprintCostShapeCanvas = blueprintCostShape.generateAsCanvas(80);
this.costDisplayParent = makeDiv(parent, "ingame_HUD_BlueprintPlacer", [], ``);
makeDiv(this.costDisplayParent, null, ["label"], T.ingame.blueprintPlacer.cost);
const costContainer = makeDiv(this.costDisplayParent, null, ["costContainer"], "");
this.costDisplayText = makeDiv(costContainer, null, ["costText"], "");
costContainer.appendChild(blueprintCostShapeCanvas);
}
initialize() {
this.root.hud.signals.buildingsSelectedForCopy.add(this.createBlueprintFromBuildings, this);
/** @type {TypedTrackedState<Blueprint?>} */
this.currentBlueprint = new TrackedState(this.onBlueprintChanged, this);
/** @type {Blueprint?} */
this.lastBlueprintUsed = null;
const keyActionMapper = this.root.keyMapper;
keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this);
keyActionMapper.getBinding(KEYMAPPINGS.placement.pipette).add(this.abortPlacement, this);
keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.rotateBlueprint, this);
keyActionMapper.getBinding(KEYMAPPINGS.massSelect.pasteLastBlueprint).add(this.pasteBlueprint, this);
this.root.camera.downPreHandler.add(this.onMouseDown, this);
this.root.camera.movePreHandler.add(this.onMouseMove, this);
this.root.hud.signals.selectedPlacementBuildingChanged.add(this.abortPlacement, this);
this.root.signals.editModeChanged.add(this.onEditModeChanged, this);
this.domAttach = new DynamicDomAttach(this.root, this.costDisplayParent);
this.trackedCanAfford = new TrackedState(this.onCanAffordChanged, this);
}
abortPlacement() {
if (this.currentBlueprint.get()) {
this.currentBlueprint.set(null);
return STOP_PROPAGATION;
}
}
/**
* Called when the layer was changed
* @param {Layer} layer
*/
onEditModeChanged(layer) {
// Check if the layer of the blueprint differs and thus we have to deselect it
const blueprint = this.currentBlueprint.get();
if (blueprint) {
if (blueprint.layer !== layer) {
this.currentBlueprint.set(null);
}
}
}
/**
* Called when the blueprint is now affordable or not
* @param {boolean} canAfford
*/
onCanAffordChanged(canAfford) {
this.costDisplayParent.classList.toggle("canAfford", canAfford);
}
update() {
const currentBlueprint = this.currentBlueprint.get();
this.domAttach.update(currentBlueprint && currentBlueprint.getCost() > 0);
this.trackedCanAfford.set(currentBlueprint && currentBlueprint.canAfford(this.root));
}
/**
* Called when the blueprint was changed
* @param {Blueprint} blueprint
*/
onBlueprintChanged(blueprint) {
if (blueprint) {
this.lastBlueprintUsed = blueprint;
this.costDisplayText.innerText = "" + blueprint.getCost();
}
}
/**
* mouse down pre handler
* @param {Vector} pos
* @param {enumMouseButton} button
*/
onMouseDown(pos, button) {
if (button === enumMouseButton.right) {
if (this.currentBlueprint.get()) {
this.abortPlacement();
return STOP_PROPAGATION;
}
}
const blueprint = this.currentBlueprint.get();
if (!blueprint) {
return;
}
if (!blueprint.canAfford(this.root)) {
this.root.soundProxy.playUiError();
return;
}
const worldPos = this.root.camera.screenToWorld(pos);
const tile = worldPos.toTileSpace();
if (blueprint.tryPlace(this.root, tile)) {
const cost = blueprint.getCost();
this.root.hubGoals.takeShapeByKey(this.root.gameMode.getBlueprintShapeKey(), cost);
this.root.soundProxy.playUi(SOUNDS.placeBuilding);
}
}
/**
* Mose move handler
*/
onMouseMove() {
// Prevent movement while blueprint is selected
if (this.currentBlueprint.get()) {
return STOP_PROPAGATION;
}
}
/**
* Called when an array of bulidings was selected
* @param {Array<number>} uids
*/
createBlueprintFromBuildings(uids) {
if (uids.length === 0) {
return;
}
this.currentBlueprint.set(Blueprint.fromUids(this.root, uids));
}
/**
* Attempts to rotate the current blueprint
*/
rotateBlueprint() {
if (this.currentBlueprint.get()) {
if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).pressed) {
this.currentBlueprint.get().rotateCcw();
} else {
this.currentBlueprint.get().rotateCw();
}
}
}
/**
* Attempts to paste the last blueprint
*/
pasteBlueprint() {
if (this.lastBlueprintUsed !== null) {
if (this.lastBlueprintUsed.layer !== this.root.currentLayer) {
// Not compatible
this.root.soundProxy.playUiError();
return;
}
this.root.hud.signals.pasteBlueprintRequested.dispatch();
this.currentBlueprint.set(this.lastBlueprintUsed);
} else {
this.root.soundProxy.playUiError();
}
}
/**
*
* @param {DrawParameters} parameters
*/
draw(parameters) {
const blueprint = this.currentBlueprint.get();
if (!blueprint) {
return;
}
const mousePosition = this.root.app.mousePosition;
if (!mousePosition) {
// Not on screen
return;
}
const worldPos = this.root.camera.screenToWorld(mousePosition);
const tile = worldPos.toTileSpace();
blueprint.draw(parameters, tile);
}
}

View File

@ -6,6 +6,8 @@ import { DynamicDomAttach } from "../dynamic_dom_attach";
import { TrackedState } from "../../../core/tracked_state";
import { cachebust } from "../../../core/cachebust";
import { T } from "../../../translations";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../../components/item_processor";
import { ShapeItem } from "../../items/shape_item";
const tutorialsByLevel = [
// Level 1
@ -30,6 +32,68 @@ const tutorialsByLevel = [
condition: () => true,
},
],
// Level 2
[
// 2.1 place a cutter
{
id: "2_1_place_cutter",
condition: /** @param {GameRoot} root */ root => {
return (
root.entityMgr
.getAllWithComponent(ItemProcessorComponent)
.filter(e => e.components.ItemProcessor.type === enumItemProcessorTypes.cutter)
.length === 0
);
},
},
// 2.2 place trash
{
id: "2_2_place_trash",
condition: /** @param {GameRoot} root */ root => {
return (
root.entityMgr
.getAllWithComponent(ItemProcessorComponent)
.filter(e => e.components.ItemProcessor.type === enumItemProcessorTypes.trash)
.length === 0
);
},
},
// 2.3 place more cutters
{
id: "2_3_more_cutters",
condition: /** @param {GameRoot} root */ root => {
return (
root.entityMgr
.getAllWithComponent(ItemProcessorComponent)
.filter(e => e.components.ItemProcessor.type === enumItemProcessorTypes.cutter)
.length < 3
);
},
},
],
// Level 2
[
// 3.1. rectangles
{
id: "3_1_rectangles",
condition: /** @param {GameRoot} root */ root => {
return (
// 4 miners placed above rectangles and 10 delivered
root.hubGoals.getCurrentGoalDelivered() < 10 ||
root.entityMgr.getAllWithComponent(MinerComponent).filter(entity => {
const tile = entity.components.StaticMapEntity.origin;
const below = root.map.getLowerLayerContentXY(tile.x, tile.y);
if (below && below.getItemType() === "shape") {
const shape = /** @type {ShapeItem} */ (below).definition.getHash();
return shape === "RuRuRuRu";
}
return false;
}).length < 4
);
},
},
],
];
export class HUDInteractiveTutorial extends BaseHUDPart {

View File

@ -1,16 +0,0 @@
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

@ -1,12 +1,11 @@
import { ClickDetector } from "../../../core/click_detector";
import { formatBigNumber, makeDiv, arrayDeleteValue } from "../../../core/utils";
import { ShapeDefinition } from "../../shape_definition";
import { BaseHUDPart } from "../base_hud_part";
import { blueprintShape, UPGRADES } from "../../upgrades";
import { enumHubGoalRewards } from "../../tutorial_goals";
import { enumAnalyticsDataSource } from "../../production_analytics";
import { T } from "../../../translations";
import { globalConfig } from "../../../core/config";
import { arrayDeleteValue, formatBigNumber, makeDiv } from "../../../core/utils";
import { T } from "../../../translations";
import { enumAnalyticsDataSource } from "../../production_analytics";
import { ShapeDefinition } from "../../shape_definition";
import { enumHubGoalRewards } from "../../tutorial_goals";
import { BaseHUDPart } from "../base_hud_part";
/**
* Manages the pinned shapes on the left side of the screen
@ -82,7 +81,7 @@ export class HUDPinnedShapes extends BaseHUDPart {
updateShapesAfterUpgrade() {
for (let i = 0; i < this.pinnedShapes.length; ++i) {
const key = this.pinnedShapes[i];
if (key === blueprintShape) {
if (key === this.root.gameMode.getBlueprintShapeKey()) {
// Ignore blueprint shapes
continue;
}
@ -107,13 +106,14 @@ export class HUDPinnedShapes extends BaseHUDPart {
if (key === this.root.hubGoals.currentGoal.definition.getHash()) {
return this.root.hubGoals.currentGoal.required;
}
if (key === blueprintShape) {
if (key === this.root.gameMode.getBlueprintShapeKey()) {
return null;
}
// Check if this shape is required for any upgrade
for (const upgradeId in UPGRADES) {
const upgradeTiers = UPGRADES[upgradeId];
const upgrades = this.root.gameMode.getUpgrades();
for (const upgradeId in upgrades) {
const upgradeTiers = upgrades[upgradeId];
const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId);
const tierHandle = upgradeTiers[currentTier];
@ -138,7 +138,10 @@ export class HUDPinnedShapes extends BaseHUDPart {
* @param {string} key
*/
isShapePinned(key) {
if (key === this.root.hubGoals.currentGoal.definition.getHash() || key === blueprintShape) {
if (
key === this.root.hubGoals.currentGoal.definition.getHash() ||
key === this.root.gameMode.getBlueprintShapeKey()
) {
// This is a "special" shape which is always pinned
return true;
}
@ -178,7 +181,7 @@ export class HUDPinnedShapes extends BaseHUDPart {
// Pin blueprint shape as well
if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) {
this.internalPinShape({
key: blueprintShape,
key: this.root.gameMode.getBlueprintShapeKey(),
canUnpin: false,
className: "blueprint",
});
@ -214,11 +217,11 @@ export class HUDPinnedShapes extends BaseHUDPart {
let detector = null;
if (canUnpin) {
element.classList.add("unpinable");
element.classList.add("removable");
detector = new ClickDetector(element, {
consumeEvents: true,
preventDefault: true,
targetOnly: true,
targetOnly: false,
});
detector.click.add(() => this.unpinShape(key));
} else {
@ -291,6 +294,7 @@ export class HUDPinnedShapes extends BaseHUDPart {
* @param {string} key
*/
unpinShape(key) {
console.log("unpin", key);
arrayDeleteValue(this.pinnedShapes, key);
this.rerenderFull();
}
@ -306,7 +310,7 @@ export class HUDPinnedShapes extends BaseHUDPart {
return;
}
if (key === blueprintShape) {
if (key === this.root.gameMode.getBlueprintShapeKey()) {
// Can not pin the blueprint shape
return;
}

View File

@ -1,9 +1,7 @@
import { BaseHUDPart } from "../base_hud_part";
import { makeDiv } from "../../../core/utils";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach";
import { blueprintShape, UPGRADES } from "../../upgrades";
import { enumNotificationType } from "./notifications";
import { tutorialGoals } from "../../tutorial_goals";
export class HUDSandboxController extends BaseHUDPart {
createElements(parent) {
@ -75,10 +73,11 @@ export class HUDSandboxController extends BaseHUDPart {
}
giveBlueprints() {
if (!this.root.hubGoals.storedShapes[blueprintShape]) {
this.root.hubGoals.storedShapes[blueprintShape] = 0;
const shape = this.root.gameMode.getBlueprintShapeKey();
if (!this.root.hubGoals.storedShapes[shape]) {
this.root.hubGoals.storedShapes[shape] = 0;
}
this.root.hubGoals.storedShapes[blueprintShape] += 1e9;
this.root.hubGoals.storedShapes[shape] += 1e9;
}
maxOutAll() {
@ -89,7 +88,7 @@ export class HUDSandboxController extends BaseHUDPart {
}
modifyUpgrade(id, amount) {
const upgradeTiers = UPGRADES[id];
const upgradeTiers = this.root.gameMode.getUpgrades()[id];
const maxLevel = upgradeTiers.length;
this.root.hubGoals.upgradeLevels[id] = Math.max(
@ -122,9 +121,10 @@ export class HUDSandboxController extends BaseHUDPart {
// Compute gained rewards
hubGoals.gainedRewards = {};
const levels = this.root.gameMode.getLevelDefinitions();
for (let i = 0; i < hubGoals.level - 1; ++i) {
if (i < tutorialGoals.length) {
const reward = tutorialGoals[i].reward;
if (i < levels.length) {
const reward = levels[i].reward;
hubGoals.gainedRewards[reward] = (hubGoals.gainedRewards[reward] || 0) + 1;
}
}

View File

@ -1,13 +1,13 @@
import { BaseHUDPart } from "../base_hud_part";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { IS_DEMO, globalConfig } from "../../../core/config";
import { T } from "../../../translations";
import { createLogger } from "../../../core/logging";
import { StaticMapEntityComponent } from "../../components/static_map_entity";
import { Vector } from "../../../core/vector";
import { makeOffscreenBuffer } from "../../../core/buffer_utils";
import { globalConfig } from "../../../core/config";
import { DrawParameters } from "../../../core/draw_parameters";
import { createLogger } from "../../../core/logging";
import { Rectangle } from "../../../core/rectangle";
import { Vector } from "../../../core/vector";
import { T } from "../../../translations";
import { StaticMapEntityComponent } from "../../components/static_map_entity";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { BaseHUDPart } from "../base_hud_part";
const logger = createLogger("screenshot_exporter");
@ -19,7 +19,7 @@ export class HUDScreenshotExporter extends BaseHUDPart {
}
startExport() {
if (IS_DEMO) {
if (!this.root.app.restrictionMgr.getIsExportingScreenshotsPossible()) {
this.root.hud.parts.dialogs.showFeatureRestrictionInfo(T.demo.features.exportingBase);
return;
}
@ -87,7 +87,7 @@ export class HUDScreenshotExporter extends BaseHUDPart {
const parameters = new DrawParameters({
context,
visibleRect,
desiredAtlasScale: chunkScale,
desiredAtlasScale: 0.25,
root: this.root,
zoomLevel: chunkScale,
});

View File

@ -1,9 +1,8 @@
import { ClickDetector } from "../../../core/click_detector";
import { InputReceiver } from "../../../core/input_receiver";
import { formatBigNumber, makeDiv } from "../../../core/utils";
import { formatBigNumber, getRomanNumber, makeDiv } from "../../../core/utils";
import { T } from "../../../translations";
import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper";
import { UPGRADES } from "../../upgrades";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach";
@ -21,7 +20,7 @@ export class HUDShop extends BaseHUDPart {
this.upgradeToElements = {};
// Upgrades
for (const upgradeId in UPGRADES) {
for (const upgradeId in this.root.gameMode.getUpgrades()) {
const handle = {};
handle.requireIndexToElement = [];
@ -59,7 +58,7 @@ export class HUDShop extends BaseHUDPart {
rerenderFull() {
for (const upgradeId in this.upgradeToElements) {
const handle = this.upgradeToElements[upgradeId];
const upgradeTiers = UPGRADES[upgradeId];
const upgradeTiers = this.root.gameMode.getUpgrades()[upgradeId];
const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId);
const currentTierMultiplier = this.root.hubGoals.upgradeImprovements[upgradeId];
@ -68,7 +67,7 @@ export class HUDShop extends BaseHUDPart {
// Set tier
handle.elemTierLabel.innerText = T.ingame.shop.tier.replace(
"<x>",
"" + T.ingame.shop.tierLabels[currentTier]
getRomanNumber(currentTier + 1)
);
handle.elemTierLabel.setAttribute("data-tier", currentTier);

View File

@ -1,14 +1,14 @@
import { globalConfig } from "../../../core/config";
import { gMetaBuildingRegistry } from "../../../core/global_registries";
import { InputReceiver } from "../../../core/input_receiver";
import { makeDiv } from "../../../core/utils";
import { SOUNDS } from "../../../platform/sound";
import { T } from "../../../translations";
import { defaultBuildingVariant } from "../../meta_building";
import { enumHubGoalRewards, tutorialGoals } from "../../tutorial_goals";
import { enumHubGoalRewards } from "../../tutorial_goals";
import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach";
import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings";
import { InputReceiver } from "../../../core/input_receiver";
import { enumNotificationType } from "./notifications";
export class HUDUnlockNotification extends BaseHUDPart {
@ -53,7 +53,9 @@ export class HUDUnlockNotification extends BaseHUDPart {
showForLevel(level, reward) {
this.root.soundProxy.playUi(SOUNDS.levelComplete);
if (level > tutorialGoals.length) {
const levels = this.root.gameMode.getLevelDefinitions();
// Don't use getIsFreeplay() because we want the freeplay level up to show
if (level > levels.length) {
this.root.hud.signals.notification.dispatch(
T.ingame.notifications.freeplayLevelComplete.replace("<level>", String(level)),
enumNotificationType.success

View File

@ -1,5 +1,5 @@
import { makeOffscreenBuffer } from "../../../core/buffer_utils";
import { globalConfig, IS_DEMO, THIRDPARTY_URLS } from "../../../core/config";
import { globalConfig, THIRDPARTY_URLS } from "../../../core/config";
import { DrawParameters } from "../../../core/draw_parameters";
import { Loader } from "../../../core/loader";
import { DialogWithForm } from "../../../core/modal_dialog_elements";
@ -302,7 +302,7 @@ export class HUDWaypoints extends BaseHUDPart {
// Show info that you can have only N markers in the demo,
// actually show this *after* entering the name so you want the
// standalone even more (I'm evil :P)
if (IS_DEMO && this.waypoints.length > 2) {
if (this.waypoints.length > this.root.app.restrictionMgr.getMaximumWaypoints()) {
this.root.hud.parts.dialogs.showFeatureRestrictionInfo(
"",
T.dialogs.markerDemoLimit.desc

View File

@ -11,6 +11,7 @@ import { MetaComparatorBuilding } from "../../buildings/comparator";
import { MetaReaderBuilding } from "../../buildings/reader";
import { MetaFilterBuilding } from "../../buildings/filter";
import { MetaDisplayBuilding } from "../../buildings/display";
import { MetaStorageBuilding } from "../../buildings/storage";
export class HUDWiresToolbar extends HUDBaseToolbar {
constructor(root) {
@ -26,6 +27,7 @@ export class HUDWiresToolbar extends HUDBaseToolbar {
MetaTransistorBuilding,
],
secondaryBuildings: [
MetaStorageBuilding,
MetaReaderBuilding,
MetaLeverBuilding,
MetaFilterBuilding,

View File

@ -122,6 +122,7 @@ export const KEYCODE_RMB = 3;
* @returns {string}
*/
export function getStringForKeyCode(code) {
// @todo: Refactor into dictionary
switch (code) {
case KEYCODE_LMB:
return "LMB";

View File

@ -104,15 +104,17 @@ export class MapChunkView extends MapChunk {
});
parameters.context.imageSmoothingEnabled = true;
const resourcesScale = this.root.app.settings.getAllSettings().mapResourcesScale;
// Draw patch items
if (this.root.currentLayer === "regular") {
if (this.root.currentLayer === "regular" && resourcesScale > 0.05) {
const diameter = (70 / Math.pow(parameters.zoomLevel, 0.35)) * (0.2 + 2 * resourcesScale);
for (let i = 0; i < this.patches.length; ++i) {
const patch = this.patches[i];
if (patch.item.getItemType() === "shape") {
const destX = this.x * dims + patch.pos.x * globalConfig.tileSize;
const destY = this.y * dims + patch.pos.y * globalConfig.tileSize;
const diameter = 80 / Math.pow(parameters.zoomLevel, 0.35);
patch.item.drawItemCenteredClipped(destX, destY, parameters, diameter);
}
}

View File

@ -0,0 +1,480 @@
import { findNiceIntegerValue } from "../../core/utils";
import { GameMode } from "../game_mode";
import { ShapeDefinition } from "../shape_definition";
import { enumHubGoalRewards } from "../tutorial_goals";
const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
const finalGameShape = "RuCw--Cw:----Ru--";
const preparementShape = "CpRpCp--:SwSwSwSw";
const blueprintShape = "CbCbCbRb:CwCwCwCw";
// Tiers need % of the previous tier as requirement too
const tierGrowth = 2.5;
/**
* Generates all upgrades
* @returns {Object<string, import("../game_mode").UpgradeTiers>} */
function generateUpgrades(limitedVersion = false) {
const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1];
const numEndgameUpgrades = limitedVersion ? 0 : 1000 - fixedImprovements.length - 1;
function generateInfiniteUnlocks() {
return new Array(numEndgameUpgrades).fill(null).map((_, i) => ({
required: [
{ shape: preparementShape, amount: 30000 + i * 10000 },
{ shape: finalGameShape, amount: 20000 + i * 5000 },
{ shape: rocketShape, amount: 20000 + i * 5000 },
],
excludePrevious: true,
}));
}
// Fill in endgame upgrades
for (let i = 0; i < numEndgameUpgrades; ++i) {
if (i < 20) {
fixedImprovements.push(0.1);
} else if (i < 50) {
fixedImprovements.push(0.05);
} else if (i < 100) {
fixedImprovements.push(0.025);
} else {
fixedImprovements.push(0.0125);
}
}
const upgrades = {
belt: [
{
required: [{ shape: "CuCuCuCu", amount: 30 }],
},
{
required: [{ shape: "--CuCu--", amount: 500 }],
},
{
required: [{ shape: "CpCpCpCp", amount: 1000 }],
},
{
required: [{ shape: "SrSrSrSr:CyCyCyCy", amount: 6000 }],
},
{
required: [{ shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", amount: 25000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateInfiniteUnlocks(),
],
miner: [
{
required: [{ shape: "RuRuRuRu", amount: 300 }],
},
{
required: [{ shape: "Cu------", amount: 800 }],
},
{
required: [{ shape: "ScScScSc", amount: 3500 }],
},
{
required: [{ shape: "CwCwCwCw:WbWbWbWb", amount: 23000 }],
},
{
required: [{ shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", amount: 50000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateInfiniteUnlocks(),
],
processors: [
{
required: [{ shape: "SuSuSuSu", amount: 500 }],
},
{
required: [{ shape: "RuRu----", amount: 600 }],
},
{
required: [{ shape: "CgScScCg", amount: 3500 }],
},
{
required: [{ shape: "CwCrCwCr:SgSgSgSg", amount: 25000 }],
},
{
required: [{ shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", amount: 50000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateInfiniteUnlocks(),
],
painting: [
{
required: [{ shape: "RbRb----", amount: 600 }],
},
{
required: [{ shape: "WrWrWrWr", amount: 3800 }],
},
{
required: [{ shape: "RpRpRpRp:CwCwCwCw", amount: 6500 }],
},
{
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp", amount: 25000 }],
},
{
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp:CwCwCwCw", amount: 50000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateInfiniteUnlocks(),
],
};
// Automatically generate tier levels
for (const upgradeId in upgrades) {
const upgradeTiers = upgrades[upgradeId];
let currentTierRequirements = [];
for (let i = 0; i < upgradeTiers.length; ++i) {
const tierHandle = upgradeTiers[i];
tierHandle.improvement = fixedImprovements[i];
const originalRequired = tierHandle.required.slice();
for (let k = currentTierRequirements.length - 1; k >= 0; --k) {
const oldTierRequirement = currentTierRequirements[k];
if (!tierHandle.excludePrevious) {
tierHandle.required.unshift({
shape: oldTierRequirement.shape,
amount: oldTierRequirement.amount,
});
}
}
currentTierRequirements.push(
...originalRequired.map(req => ({
amount: req.amount,
shape: req.shape,
}))
);
currentTierRequirements.forEach(tier => {
tier.amount = findNiceIntegerValue(tier.amount * tierGrowth);
});
}
}
// VALIDATE
if (G_IS_DEV) {
for (const upgradeId in upgrades) {
upgrades[upgradeId].forEach(tier => {
tier.required.forEach(({ shape }) => {
try {
ShapeDefinition.fromShortKey(shape);
} catch (ex) {
throw new Error("Invalid upgrade goal: '" + ex + "' for shape" + shape);
}
});
});
}
}
return upgrades;
}
/**
* Generates the level definitions
* @param {boolean} limitedVersion
*/
export function generateLevelDefinitions(limitedVersion = false) {
const levelDefinitions = [
// 1
// Circle
{
shape: "CuCuCuCu", // belts t1
required: 30,
reward: enumHubGoalRewards.reward_cutter_and_trash,
},
// 2
// Cutter
{
shape: "----CuCu", //
required: 40,
reward: enumHubGoalRewards.no_reward,
},
// 3
// Rectangle
{
shape: "RuRuRuRu", // miners t1
required: 70,
reward: enumHubGoalRewards.reward_balancer,
},
// 4
{
shape: "RuRu----", // processors t2
required: 70,
reward: enumHubGoalRewards.reward_rotater,
},
// 5
// Rotater
{
shape: "Cu----Cu", // belts t2
required: 170,
reward: enumHubGoalRewards.reward_tunnel,
},
// 6
{
shape: "Cu------", // miners t2
required: 270,
reward: enumHubGoalRewards.reward_painter,
},
// 7
// Painter
{
shape: "CrCrCrCr", // unused
required: 300,
reward: enumHubGoalRewards.reward_rotater_ccw,
},
// 8
{
shape: "RbRb----", // painter t2
required: 480,
reward: enumHubGoalRewards.reward_mixer,
},
// 9
// Mixing (purple)
{
shape: "CpCpCpCp", // belts t3
required: 600,
reward: enumHubGoalRewards.reward_merger,
},
// 10
// STACKER: Star shape + cyan
{
shape: "ScScScSc", // miners t3
required: 800,
reward: enumHubGoalRewards.reward_stacker,
},
// 11
// Chainable miner
{
shape: "CgScScCg", // processors t3
required: 1000,
reward: enumHubGoalRewards.reward_miner_chainable,
},
// 12
// Blueprints
{
shape: "CbCbCbRb:CwCwCwCw",
required: 1000,
reward: enumHubGoalRewards.reward_blueprints,
},
// 13
// Tunnel Tier 2
{
shape: "RpRpRpRp:CwCwCwCw", // painting t3
required: 3800,
reward: enumHubGoalRewards.reward_underground_belt_tier_2,
},
// DEMO STOPS HERE
...(limitedVersion
? [
{
shape: "RpRpRpRp:CwCwCwCw",
required: 0,
reward: enumHubGoalRewards.reward_demo_end,
},
]
: [
// 14
// Belt reader
{
shape: "--Cg----:--Cr----", // unused
required: 16, // Per second!
reward: enumHubGoalRewards.reward_belt_reader,
throughputOnly: true,
},
// 15
// Storage
{
shape: "SrSrSrSr:CyCyCyCy", // unused
required: 10000,
reward: enumHubGoalRewards.reward_storage,
},
// 16
// Quad Cutter
{
shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", // belts t4 (two variants)
required: 6000,
reward: enumHubGoalRewards.reward_cutter_quad,
},
// 17
// Double painter
{
shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", // miner t4 (two variants)
required: 20000,
reward: enumHubGoalRewards.reward_painter_double,
},
// 18
// Rotater (180deg)
{
shape: "Sg----Sg:CgCgCgCg:--CyCy--", // unused
required: 20000,
reward: enumHubGoalRewards.reward_rotater_180,
},
// 19
// Compact splitter
{
shape: "CpRpCp--:SwSwSwSw",
required: 25000,
reward: enumHubGoalRewards.reward_splitter,
},
// 20
// WIRES
{
shape: finalGameShape,
required: 25000,
reward: enumHubGoalRewards.reward_wires_painter_and_levers,
},
// 21
// Filter
{
shape: "CrCwCrCw:CwCrCwCr:CrCwCrCw:CwCrCwCr",
required: 25000,
reward: enumHubGoalRewards.reward_filter,
},
// 22
// Constant signal
{
shape: "Cg----Cr:Cw----Cw:Sy------:Cy----Cy",
required: 25000,
reward: enumHubGoalRewards.reward_constant_signal,
},
// 23
// Display
{
shape: "CcSyCcSy:SyCcSyCc:CcSyCcSy",
required: 25000,
reward: enumHubGoalRewards.reward_display,
},
// 24 Logic gates
{
shape: "CcRcCcRc:RwCwRwCw:Sr--Sw--:CyCyCyCy",
required: 25000,
reward: enumHubGoalRewards.reward_logic_gates,
},
// 25 Virtual Processing
{
shape: "Rg--Rg--:CwRwCwRw:--Rg--Rg",
required: 25000,
reward: enumHubGoalRewards.reward_virtual_processing,
},
// 26 Freeplay
{
shape: "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw",
required: 50000,
reward: enumHubGoalRewards.reward_freeplay,
},
]),
];
if (G_IS_DEV) {
levelDefinitions.forEach(({ shape }) => {
try {
ShapeDefinition.fromShortKey(shape);
} catch (ex) {
throw new Error("Invalid tutorial goal: '" + ex + "' for shape" + shape);
}
});
}
return levelDefinitions;
}
const fullVersionUpgrades = generateUpgrades(false);
const demoVersionUpgrades = generateUpgrades(true);
const fullVersionLevels = generateLevelDefinitions(false);
const demoVersionLevels = generateLevelDefinitions(true);
export class RegularGameMode extends GameMode {
constructor(root) {
super(root);
}
getUpgrades() {
return this.root.app.restrictionMgr.getHasExtendedUpgrades()
? fullVersionUpgrades
: demoVersionUpgrades;
}
getIsFreeplayAvailable() {
return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay();
}
getBlueprintShapeKey() {
return blueprintShape;
}
getLevelDefinitions() {
return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay()
? fullVersionLevels
: demoVersionLevels;
}
}

View File

@ -1,221 +1,225 @@
/* eslint-disable no-unused-vars */
import { Signal } from "../core/signal";
import { RandomNumberGenerator } from "../core/rng";
import { createLogger } from "../core/logging";
// Type hints
/* typehints:start */
import { GameTime } from "./time/game_time";
import { EntityManager } from "./entity_manager";
import { GameSystemManager } from "./game_system_manager";
import { GameHUD } from "./hud/hud";
import { MapView } from "./map_view";
import { Camera } from "./camera";
import { InGameState } from "../states/ingame";
import { AutomaticSave } from "./automatic_save";
import { Application } from "../application";
import { SoundProxy } from "./sound_proxy";
import { Savegame } from "../savegame/savegame";
import { GameLogic } from "./logic";
import { ShapeDefinitionManager } from "./shape_definition_manager";
import { HubGoals } from "./hub_goals";
import { BufferMaintainer } from "../core/buffer_maintainer";
import { ProductionAnalytics } from "./production_analytics";
import { Entity } from "./entity";
import { ShapeDefinition } from "./shape_definition";
import { BaseItem } from "./base_item";
import { DynamicTickrate } from "./dynamic_tickrate";
import { KeyActionMapper } from "./key_action_mapper";
import { Vector } from "../core/vector";
/* typehints:end */
const logger = createLogger("game/root");
/** @type {Array<Layer>} */
export const layers = ["regular", "wires"];
/**
* The game root is basically the whole game state at a given point,
* combining all important classes. We don't have globals, but this
* class is passed to almost all game classes.
*/
export class GameRoot {
/**
* Constructs a new game root
* @param {Application} app
*/
constructor(app) {
this.app = app;
/** @type {Savegame} */
this.savegame = null;
/** @type {InGameState} */
this.gameState = null;
/** @type {KeyActionMapper} */
this.keyMapper = null;
// Store game dimensions
this.gameWidth = 500;
this.gameHeight = 500;
// Stores whether the current session is a fresh game (true), or was continued (false)
/** @type {boolean} */
this.gameIsFresh = true;
// Stores whether the logic is already initialized
/** @type {boolean} */
this.logicInitialized = false;
// Stores whether the game is already initialized, that is, all systems etc have been created
/** @type {boolean} */
this.gameInitialized = false;
/**
* Whether a bulk operation is running
*/
this.bulkOperationRunning = false;
//////// Other properties ///////
/** @type {Camera} */
this.camera = null;
/** @type {HTMLCanvasElement} */
this.canvas = null;
/** @type {CanvasRenderingContext2D} */
this.context = null;
/** @type {MapView} */
this.map = null;
/** @type {GameLogic} */
this.logic = null;
/** @type {EntityManager} */
this.entityMgr = null;
/** @type {GameHUD} */
this.hud = null;
/** @type {GameSystemManager} */
this.systemMgr = null;
/** @type {GameTime} */
this.time = null;
/** @type {HubGoals} */
this.hubGoals = null;
/** @type {BufferMaintainer} */
this.buffers = null;
/** @type {AutomaticSave} */
this.automaticSave = null;
/** @type {SoundProxy} */
this.soundProxy = null;
/** @type {ShapeDefinitionManager} */
this.shapeDefinitionMgr = null;
/** @type {ProductionAnalytics} */
this.productionAnalytics = null;
/** @type {DynamicTickrate} */
this.dynamicTickrate = null;
/** @type {Layer} */
this.currentLayer = "regular";
this.signals = {
// Entities
entityManuallyPlaced: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityAdded: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityChanged: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityGotNewComponent: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityComponentRemoved: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityQueuedForDestroy: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityDestroyed: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
// Global
resized: /** @type {TypedSignal<[number, number]>} */ (new Signal()),
readyToRender: /** @type {TypedSignal<[]>} */ (new Signal()),
aboutToDestruct: /** @type {TypedSignal<[]>} */ new Signal(),
// Game Hooks
gameSaved: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got saved
gameRestored: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got restored
gameFrameStarted: /** @type {TypedSignal<[]>} */ (new Signal()), // New frame
storyGoalCompleted: /** @type {TypedSignal<[number, string]>} */ (new Signal()),
upgradePurchased: /** @type {TypedSignal<[string]>} */ (new Signal()),
// Called right after game is initialized
postLoadHook: /** @type {TypedSignal<[]>} */ (new Signal()),
shapeDelivered: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()),
itemProduced: /** @type {TypedSignal<[BaseItem]>} */ (new Signal()),
bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()),
editModeChanged: /** @type {TypedSignal<[Layer]>} */ (new Signal()),
// Called to check if an entity can be placed, second parameter is an additional offset.
// Use to introduce additional placement checks
prePlacementCheck: /** @type {TypedSignal<[Entity, Vector]>} */ (new Signal()),
// Called before actually placing an entity, use to perform additional logic
// for freeing space before actually placing.
freeEntityAreaBeforeBuild: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
};
// RNG's
/** @type {Object.<string, Object.<string, RandomNumberGenerator>>} */
this.rngs = {};
// Work queue
this.queue = {
requireRedraw: false,
};
}
/**
* Destructs the game root
*/
destruct() {
logger.log("destructing root");
this.signals.aboutToDestruct.dispatch();
this.reset();
}
/**
* Resets the whole root and removes all properties
*/
reset() {
if (this.signals) {
// Destruct all signals
for (let i = 0; i < this.signals.length; ++i) {
this.signals[i].removeAll();
}
}
if (this.hud) {
this.hud.cleanup();
}
if (this.camera) {
this.camera.cleanup();
}
// Finally free all properties
for (let prop in this) {
if (this.hasOwnProperty(prop)) {
delete this[prop];
}
}
}
}
/* eslint-disable no-unused-vars */
import { Signal } from "../core/signal";
import { RandomNumberGenerator } from "../core/rng";
import { createLogger } from "../core/logging";
// Type hints
/* typehints:start */
import { GameTime } from "./time/game_time";
import { EntityManager } from "./entity_manager";
import { GameSystemManager } from "./game_system_manager";
import { GameHUD } from "./hud/hud";
import { MapView } from "./map_view";
import { Camera } from "./camera";
import { InGameState } from "../states/ingame";
import { AutomaticSave } from "./automatic_save";
import { Application } from "../application";
import { SoundProxy } from "./sound_proxy";
import { Savegame } from "../savegame/savegame";
import { GameLogic } from "./logic";
import { ShapeDefinitionManager } from "./shape_definition_manager";
import { HubGoals } from "./hub_goals";
import { BufferMaintainer } from "../core/buffer_maintainer";
import { ProductionAnalytics } from "./production_analytics";
import { Entity } from "./entity";
import { ShapeDefinition } from "./shape_definition";
import { BaseItem } from "./base_item";
import { DynamicTickrate } from "./dynamic_tickrate";
import { KeyActionMapper } from "./key_action_mapper";
import { Vector } from "../core/vector";
import { GameMode } from "./game_mode";
/* typehints:end */
const logger = createLogger("game/root");
/** @type {Array<Layer>} */
export const layers = ["regular", "wires"];
/**
* The game root is basically the whole game state at a given point,
* combining all important classes. We don't have globals, but this
* class is passed to almost all game classes.
*/
export class GameRoot {
/**
* Constructs a new game root
* @param {Application} app
*/
constructor(app) {
this.app = app;
/** @type {Savegame} */
this.savegame = null;
/** @type {InGameState} */
this.gameState = null;
/** @type {KeyActionMapper} */
this.keyMapper = null;
// Store game dimensions
this.gameWidth = 500;
this.gameHeight = 500;
// Stores whether the current session is a fresh game (true), or was continued (false)
/** @type {boolean} */
this.gameIsFresh = true;
// Stores whether the logic is already initialized
/** @type {boolean} */
this.logicInitialized = false;
// Stores whether the game is already initialized, that is, all systems etc have been created
/** @type {boolean} */
this.gameInitialized = false;
/**
* Whether a bulk operation is running
*/
this.bulkOperationRunning = false;
//////// Other properties ///////
/** @type {Camera} */
this.camera = null;
/** @type {HTMLCanvasElement} */
this.canvas = null;
/** @type {CanvasRenderingContext2D} */
this.context = null;
/** @type {MapView} */
this.map = null;
/** @type {GameLogic} */
this.logic = null;
/** @type {EntityManager} */
this.entityMgr = null;
/** @type {GameHUD} */
this.hud = null;
/** @type {GameSystemManager} */
this.systemMgr = null;
/** @type {GameTime} */
this.time = null;
/** @type {HubGoals} */
this.hubGoals = null;
/** @type {BufferMaintainer} */
this.buffers = null;
/** @type {AutomaticSave} */
this.automaticSave = null;
/** @type {SoundProxy} */
this.soundProxy = null;
/** @type {ShapeDefinitionManager} */
this.shapeDefinitionMgr = null;
/** @type {ProductionAnalytics} */
this.productionAnalytics = null;
/** @type {DynamicTickrate} */
this.dynamicTickrate = null;
/** @type {Layer} */
this.currentLayer = "regular";
/** @type {GameMode} */
this.gameMode = null;
this.signals = {
// Entities
entityManuallyPlaced: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityAdded: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityChanged: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityGotNewComponent: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityComponentRemoved: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityQueuedForDestroy: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityDestroyed: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
// Global
resized: /** @type {TypedSignal<[number, number]>} */ (new Signal()),
readyToRender: /** @type {TypedSignal<[]>} */ (new Signal()),
aboutToDestruct: /** @type {TypedSignal<[]>} */ new Signal(),
// Game Hooks
gameSaved: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got saved
gameRestored: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got restored
gameFrameStarted: /** @type {TypedSignal<[]>} */ (new Signal()), // New frame
storyGoalCompleted: /** @type {TypedSignal<[number, string]>} */ (new Signal()),
upgradePurchased: /** @type {TypedSignal<[string]>} */ (new Signal()),
// Called right after game is initialized
postLoadHook: /** @type {TypedSignal<[]>} */ (new Signal()),
shapeDelivered: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()),
itemProduced: /** @type {TypedSignal<[BaseItem]>} */ (new Signal()),
bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()),
editModeChanged: /** @type {TypedSignal<[Layer]>} */ (new Signal()),
// Called to check if an entity can be placed, second parameter is an additional offset.
// Use to introduce additional placement checks
prePlacementCheck: /** @type {TypedSignal<[Entity, Vector]>} */ (new Signal()),
// Called before actually placing an entity, use to perform additional logic
// for freeing space before actually placing.
freeEntityAreaBeforeBuild: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
};
// RNG's
/** @type {Object.<string, Object.<string, RandomNumberGenerator>>} */
this.rngs = {};
// Work queue
this.queue = {
requireRedraw: false,
};
}
/**
* Destructs the game root
*/
destruct() {
logger.log("destructing root");
this.signals.aboutToDestruct.dispatch();
this.reset();
}
/**
* Resets the whole root and removes all properties
*/
reset() {
if (this.signals) {
// Destruct all signals
for (let i = 0; i < this.signals.length; ++i) {
this.signals[i].removeAll();
}
}
if (this.hud) {
this.hud.cleanup();
}
if (this.camera) {
this.camera.cleanup();
}
// Finally free all properties
for (let prop in this) {
if (this.hasOwnProperty(prop)) {
delete this[prop];
}
}
}
}

View File

@ -12,7 +12,6 @@ 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 { ShapeDefinition } from "../shape_definition";
import { blueprintShape } from "../upgrades";
export class ConstantSignalSystem extends GameSystemWithFilter {
constructor(root) {
@ -61,7 +60,9 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
this.root.shapeDefinitionMgr.getShapeItemFromDefinition(
this.root.hubGoals.currentGoal.definition
),
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(blueprintShape),
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(
this.root.gameMode.getBlueprintShapeKey()
),
...this.root.hud.parts.pinnedShapes.pinnedShapes.map(key =>
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key)
),

View File

@ -1,4 +1,4 @@
import { globalConfig, IS_DEMO } from "../../core/config";
import { globalConfig } from "../../core/config";
import { smoothenDpi } from "../../core/dpi_manager";
import { DrawParameters } from "../../core/draw_parameters";
import { drawSpriteClipped } from "../../core/draw_utils";

View File

@ -154,22 +154,18 @@ export class LogicGateSystem extends GameSystemWithFilter {
/**
* @param {Array<BaseItem|null>} parameters
* @returns {[BaseItem, BaseItem]}
* @returns {BaseItem}
*/
compute_ROTATE(parameters) {
const item = parameters[0];
if (!item || item.getItemType() !== "shape") {
// Not a shape
return [null, null];
return null;
}
const definition = /** @type {ShapeItem} */ (item).definition;
const rotatedDefinitionCCW = this.root.shapeDefinitionMgr.shapeActionRotateCCW(definition);
const rotatedDefinitionCW = this.root.shapeDefinitionMgr.shapeActionRotateCW(definition);
return [
this.root.shapeDefinitionMgr.getShapeItemFromDefinition(rotatedDefinitionCCW),
this.root.shapeDefinitionMgr.getShapeItemFromDefinition(rotatedDefinitionCW),
];
return this.root.shapeDefinitionMgr.getShapeItemFromDefinition(rotatedDefinitionCW);
}
/**

View File

@ -32,7 +32,8 @@
},
"chunkOverview": {
"empty": "#444856",
"filled": "#646b7d"
"filled": "#646b7d",
"beltColor": "#9096a3"
},
"wires": {

View File

@ -33,7 +33,8 @@
"chunkOverview": {
"empty": "#a6afbb",
"filled": "#c5ccd6"
"filled": "#c5ccd6",
"beltColor": "#777"
},
"wires": {

View File

@ -1,7 +1,3 @@
import { IS_DEMO } from "../core/config";
import { ShapeDefinition } from "./shape_definition";
import { finalGameShape } from "./upgrades";
/**
* Don't forget to also update tutorial_goals_mappings.js as well as the translations!
* @enum {string}
@ -40,229 +36,3 @@ export const enumHubGoalRewards = {
no_reward: "no_reward",
no_reward_freeplay: "no_reward_freeplay",
};
export const tutorialGoals = [
// 1
// Circle
{
shape: "CuCuCuCu", // belts t1
required: 30,
reward: enumHubGoalRewards.reward_cutter_and_trash,
},
// 2
// Cutter
{
shape: "----CuCu", //
required: 40,
reward: enumHubGoalRewards.no_reward,
},
// 3
// Rectangle
{
shape: "RuRuRuRu", // miners t1
required: 70,
reward: enumHubGoalRewards.reward_balancer,
},
// 4
{
shape: "RuRu----", // processors t2
required: 70,
reward: enumHubGoalRewards.reward_rotater,
},
// 5
// Rotater
{
shape: "Cu----Cu", // belts t2
required: 170,
reward: enumHubGoalRewards.reward_tunnel,
},
// 6
{
shape: "Cu------", // miners t2
required: 270,
reward: enumHubGoalRewards.reward_painter,
},
// 7
// Painter
{
shape: "CrCrCrCr", // unused
required: 300,
reward: enumHubGoalRewards.reward_rotater_ccw,
},
// 8
{
shape: "RbRb----", // painter t2
required: 480,
reward: enumHubGoalRewards.reward_mixer,
},
// 9
// Mixing (purple)
{
shape: "CpCpCpCp", // belts t3
required: 600,
reward: enumHubGoalRewards.reward_merger,
},
// 10
// STACKER: Star shape + cyan
{
shape: "ScScScSc", // miners t3
required: 800,
reward: enumHubGoalRewards.reward_stacker,
},
// 11
// Chainable miner
{
shape: "CgScScCg", // processors t3
required: 1000,
reward: enumHubGoalRewards.reward_miner_chainable,
},
// 12
// Blueprints
{
shape: "CbCbCbRb:CwCwCwCw",
required: 1000,
reward: enumHubGoalRewards.reward_blueprints,
},
// 13
// Tunnel Tier 2
{
shape: "RpRpRpRp:CwCwCwCw", // painting t3
required: 3800,
reward: enumHubGoalRewards.reward_underground_belt_tier_2,
},
// DEMO STOPS HERE
...(IS_DEMO
? [
{
shape: "RpRpRpRp:CwCwCwCw",
required: 0,
reward: enumHubGoalRewards.reward_demo_end,
},
]
: [
// 14
// Belt reader
{
shape: "--Cg----:--Cr----", // unused
required: 16, // Per second!
reward: enumHubGoalRewards.reward_belt_reader,
throughputOnly: true,
},
// 15
// Storage
{
shape: "SrSrSrSr:CyCyCyCy", // unused
required: 10000,
reward: enumHubGoalRewards.reward_storage,
},
// 16
// Quad Cutter
{
shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", // belts t4 (two variants)
required: 6000,
reward: enumHubGoalRewards.reward_cutter_quad,
},
// 17
// Double painter
{
shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", // miner t4 (two variants)
required: 20000,
reward: enumHubGoalRewards.reward_painter_double,
},
// 18
// Rotater (180deg)
{
shape: "Sg----Sg:CgCgCgCg:--CyCy--", // unused
required: 20000,
reward: enumHubGoalRewards.reward_rotater_180,
},
// 19
// Compact splitter
{
shape: "CpRpCp--:SwSwSwSw",
required: 25000,
reward: enumHubGoalRewards.reward_splitter,
},
// 20
// WIRES
{
shape: finalGameShape,
required: 25000,
reward: enumHubGoalRewards.reward_wires_painter_and_levers,
},
// 21
// Filter
{
shape: "CrCwCrCw:CwCrCwCr:CrCwCrCw:CwCrCwCr",
required: 25000,
reward: enumHubGoalRewards.reward_filter,
},
// 22
// Constant signal
{
shape: "Cg----Cr:Cw----Cw:Sy------:Cy----Cy",
required: 25000,
reward: enumHubGoalRewards.reward_constant_signal,
},
// 23
// Display
{
shape: "CcSyCcSy:SyCcSyCc:CcSyCcSy",
required: 25000,
reward: enumHubGoalRewards.reward_display,
},
// 24 Logic gates
{
shape: "CcRcCcRc:RwCwRwCw:Sr--Sw--:CyCyCyCy",
required: 25000,
reward: enumHubGoalRewards.reward_logic_gates,
},
// 25 Virtual Processing
{
shape: "Rg--Rg--:CwRwCwRw:--Rg--Rg",
required: 25000,
reward: enumHubGoalRewards.reward_virtual_processing,
},
// 26 Freeplay
{
shape: "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw",
required: 50000,
reward: enumHubGoalRewards.reward_freeplay,
},
]),
];
if (G_IS_DEV) {
tutorialGoals.forEach(({ shape }) => {
try {
ShapeDefinition.fromShortKey(shape);
} catch (ex) {
throw new Error("Invalid tutorial goal: '" + ex + "' for shape" + shape);
}
});
}

View File

@ -1,212 +0,0 @@
import { IS_DEMO } from "../core/config";
import { findNiceIntegerValue } from "../core/utils";
import { ShapeDefinition } from "./shape_definition";
export const preparementShape = "CpRpCp--:SwSwSwSw";
export const finalGameShape = "RuCw--Cw:----Ru--";
export const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
export const blueprintShape = "CbCbCbRb:CwCwCwCw";
const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1];
const numEndgameUpgrades = !IS_DEMO ? 20 - fixedImprovements.length - 1 : 0;
function generateEndgameUpgrades() {
return new Array(numEndgameUpgrades).fill(null).map((_, i) => ({
required: [
{ shape: preparementShape, 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 {{
* shape: string,
* amount: number
* }} UpgradeRequirement */
/** @typedef {{
* required: Array<UpgradeRequirement>
* improvement?: number,
* excludePrevious?: boolean
* }} TierRequirement */
/** @typedef {Array<TierRequirement>} UpgradeTiers */
/** @type {Object<string, UpgradeTiers>} */
export const UPGRADES = {
belt: [
{
required: [{ shape: "CuCuCuCu", amount: 60 }],
},
{
required: [{ shape: "--CuCu--", amount: 500 }],
},
{
required: [{ shape: "CpCpCpCp", amount: 1000 }],
},
{
required: [{ shape: "SrSrSrSr:CyCyCyCy", amount: 6000 }],
},
{
required: [{ shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", amount: 25000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
miner: [
{
required: [{ shape: "RuRuRuRu", amount: 300 }],
},
{
required: [{ shape: "Cu------", amount: 800 }],
},
{
required: [{ shape: "ScScScSc", amount: 3500 }],
},
{
required: [{ shape: "CwCwCwCw:WbWbWbWb", amount: 23000 }],
},
{
required: [{ shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", amount: 50000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
processors: [
{
required: [{ shape: "SuSuSuSu", amount: 500 }],
},
{
required: [{ shape: "RuRu----", amount: 600 }],
},
{
required: [{ shape: "CgScScCg", amount: 3500 }],
},
{
required: [{ shape: "CwCrCwCr:SgSgSgSg", amount: 25000 }],
},
{
required: [{ shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", amount: 50000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
painting: [
{
required: [{ shape: "RbRb----", amount: 600 }],
},
{
required: [{ shape: "WrWrWrWr", amount: 3800 }],
},
{
required: [{ shape: "RpRpRpRp:CwCwCwCw", amount: 6500 }],
},
{
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp", amount: 25000 }],
},
{
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp:CwCwCwCw", amount: 50000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
};
// Tiers need % of the previous tier as requirement too
const tierGrowth = 2.5;
// Automatically generate tier levels
for (const upgradeId in UPGRADES) {
const upgradeTiers = UPGRADES[upgradeId];
let currentTierRequirements = [];
for (let i = 0; i < upgradeTiers.length; ++i) {
const tierHandle = upgradeTiers[i];
tierHandle.improvement = fixedImprovements[i];
const originalRequired = tierHandle.required.slice();
for (let k = currentTierRequirements.length - 1; k >= 0; --k) {
const oldTierRequirement = currentTierRequirements[k];
if (!tierHandle.excludePrevious) {
tierHandle.required.unshift({
shape: oldTierRequirement.shape,
amount: oldTierRequirement.amount,
});
}
}
currentTierRequirements.push(
...originalRequired.map(req => ({
amount: req.amount,
shape: req.shape,
}))
);
currentTierRequirements.forEach(tier => {
tier.amount = findNiceIntegerValue(tier.amount * tierGrowth);
});
}
}
// VALIDATE
if (G_IS_DEV) {
for (const upgradeId in UPGRADES) {
UPGRADES[upgradeId].forEach(tier => {
tier.required.forEach(({ shape }) => {
try {
ShapeDefinition.fromShortKey(shape);
} catch (ex) {
throw new Error("Invalid upgrade goal: '" + ex + "' for shape" + shape);
}
});
});
}
}

View File

@ -1,113 +1,120 @@
/**
* @type {Object<string, {name: string, data: any, code: string, region: string}>}
*/
export const LANGUAGES = {
"en": {
name: "English",
data: null,
code: "en",
region: "",
},
"de": {
name: "Deutsch",
data: require("./built-temp/base-de.json"),
code: "de",
region: "",
},
"fr": {
name: "Français",
data: require("./built-temp/base-fr.json"),
code: "fr",
region: "",
},
"ja": {
name: "日本語",
data: require("./built-temp/base-ja.json"),
code: "ja",
region: "",
},
"pt-PT": {
name: "Português (Portugal)",
data: require("./built-temp/base-pt-PT.json"),
code: "pt",
region: "PT",
},
"pt-BR": {
name: "Português (Brasil)",
data: require("./built-temp/base-pt-BR.json"),
code: "pt",
region: "BR",
},
"ru": {
name: "Русский",
data: require("./built-temp/base-ru.json"),
code: "ru",
region: "",
},
"cs": {
name: "Čeština",
data: require("./built-temp/base-cz.json"),
code: "cs",
region: "",
},
"es-419": {
name: "Español",
data: require("./built-temp/base-es.json"),
code: "es",
region: "",
},
"pl": {
name: "Polski",
data: require("./built-temp/base-pl.json"),
code: "pl",
region: "",
},
"kor": {
name: "한국어",
data: require("./built-temp/base-kor.json"),
code: "kor",
region: "",
},
"nl": {
name: "Nederlands",
data: require("./built-temp/base-nl.json"),
code: "nl",
region: "",
},
"no": {
name: "Norsk",
data: require("./built-temp/base-no.json"),
code: "no",
region: "",
},
"tr": {
name: "Türkçe",
data: require("./built-temp/base-tr.json"),
code: "tr",
region: "",
},
"zh-CN": {
// simplified
name: "中文简体",
data: require("./built-temp/base-zh-CN.json"),
code: "zh",
region: "CN",
},
"zh-TW": {
// traditional
name: "中文繁體",
data: require("./built-temp/base-zh-TW.json"),
code: "zh",
region: "TW",
},
"sv": {
name: "Svenska",
data: require("./built-temp/base-sv.json"),
code: "sv",
region: "",
},
};
/**
* @type {Object<string, {name: string, data: any, code: string, region: string}>}
*/
export const LANGUAGES = {
"en": {
name: "English",
data: null,
code: "en",
region: "",
},
"de": {
name: "Deutsch",
data: require("./built-temp/base-de.json"),
code: "de",
region: "",
},
"fr": {
name: "Français",
data: require("./built-temp/base-fr.json"),
code: "fr",
region: "",
},
"ja": {
name: "日本語",
data: require("./built-temp/base-ja.json"),
code: "ja",
region: "",
},
"pt-PT": {
name: "Português (Portugal)",
data: require("./built-temp/base-pt-PT.json"),
code: "pt",
region: "PT",
},
"pt-BR": {
name: "Português (Brasil)",
data: require("./built-temp/base-pt-BR.json"),
code: "pt",
region: "BR",
},
"ru": {
name: "Русский",
data: require("./built-temp/base-ru.json"),
code: "ru",
region: "",
},
"cs": {
name: "Čeština",
data: require("./built-temp/base-cz.json"),
code: "cs",
region: "",
},
"es-419": {
name: "Español",
data: require("./built-temp/base-es.json"),
code: "es",
region: "",
},
"pl": {
name: "Polski",
data: require("./built-temp/base-pl.json"),
code: "pl",
region: "",
},
"kor": {
name: "한국어",
data: require("./built-temp/base-kor.json"),
code: "kor",
region: "",
},
"nl": {
name: "Nederlands",
data: require("./built-temp/base-nl.json"),
code: "nl",
region: "",
},
"no": {
name: "Norsk",
data: require("./built-temp/base-no.json"),
code: "no",
region: "",
},
"tr": {
name: "Türkçe",
data: require("./built-temp/base-tr.json"),
code: "tr",
region: "",
},
"zh-CN": {
// simplified
name: "中文简体",
data: require("./built-temp/base-zh-CN.json"),
code: "zh",
region: "CN",
},
"zh-TW": {
// traditional
name: "中文繁體",
data: require("./built-temp/base-zh-TW.json"),
code: "zh",
region: "TW",
},
"sv": {
name: "Svenska",
data: require("./built-temp/base-sv.json"),
code: "sv",
region: "",
},
"da": {
name: "Dansk",
data: require("./built-temp/base-da.json"),
code: "da",
region: "",
},
};

View File

@ -1,14 +1,12 @@
import { globalConfig } from "../../core/config";
import { createLogger } from "../../core/logging";
import { queryParamOptions } from "../../core/query_parameters";
import { BeltComponent } from "../../game/components/belt";
import { StaticMapEntityComponent } from "../../game/components/static_map_entity";
import { GameRoot } from "../../game/root";
import { InGameState } from "../../states/ingame";
import { GameAnalyticsInterface } from "../game_analytics";
import { FILE_NOT_FOUND } from "../storage";
import { blueprintShape, UPGRADES } from "../../game/upgrades";
import { tutorialGoals } from "../../game/tutorial_goals";
import { BeltComponent } from "../../game/components/belt";
import { StaticMapEntityComponent } from "../../game/components/static_map_entity";
import { queryParamOptions } from "../../core/query_parameters";
const logger = createLogger("game_analytics");
@ -190,23 +188,26 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface {
/**
* Returns true if the shape is interesting
* @param {GameRoot} root
* @param {string} key
*/
isInterestingShape(key) {
if (key === blueprintShape) {
isInterestingShape(root, key) {
if (key === root.gameMode.getBlueprintShapeKey()) {
return true;
}
// Check if its a story goal
for (let i = 0; i < tutorialGoals.length; ++i) {
if (key === tutorialGoals[i].shape) {
const levels = root.gameMode.getLevelDefinitions();
for (let i = 0; i < levels.length; ++i) {
if (key === levels[i].shape) {
return true;
}
}
// Check if its required to unlock an upgrade
for (const upgradeKey in UPGRADES) {
const upgradeTiers = UPGRADES[upgradeKey];
const upgrades = root.gameMode.getUpgrades();
for (const upgradeKey in upgrades) {
const upgradeTiers = upgrades[upgradeKey];
for (let i = 0; i < upgradeTiers.length; ++i) {
const tier = upgradeTiers[i];
const required = tier.required;
@ -226,7 +227,9 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface {
* @param {GameRoot} root
*/
generateGameDump(root) {
const shapeIds = Object.keys(root.hubGoals.storedShapes).filter(this.isInterestingShape.bind(this));
const shapeIds = Object.keys(root.hubGoals.storedShapes).filter(key =>
this.isInterestingShape(root, key)
);
let shapes = {};
for (let i = 0; i < shapeIds.length; ++i) {
shapes[shapeIds[i]] = root.hubGoals.storedShapes[shapeIds[i]];

View File

@ -1,214 +1,202 @@
import { globalConfig, IS_DEMO, IS_MOBILE } from "../../core/config";
import { createLogger } from "../../core/logging";
import { queryParamOptions } from "../../core/query_parameters";
import { clamp } from "../../core/utils";
import { GamedistributionAdProvider } from "../ad_providers/gamedistribution";
import { NoAdProvider } from "../ad_providers/no_ad_provider";
import { PlatformWrapperInterface } from "../wrapper";
import { StorageImplBrowser } from "./storage";
import { StorageImplBrowserIndexedDB } from "./storage_indexed_db";
const logger = createLogger("platform/browser");
export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
initialize() {
this.recaptchaTokenCallback = null;
this.embedProvider = {
id: "shapezio-website",
adProvider: NoAdProvider,
iframed: false,
externalLinks: true,
iogLink: true,
unlimitedSavegames: IS_DEMO ? false : true,
showDemoBadge: IS_DEMO,
};
if (!G_IS_STANDALONE && queryParamOptions.embedProvider) {
const providerId = queryParamOptions.embedProvider;
this.embedProvider.iframed = true;
this.embedProvider.iogLink = false;
switch (providerId) {
case "armorgames": {
this.embedProvider.id = "armorgames";
break;
}
case "iogames.space": {
this.embedProvider.id = "iogames.space";
this.embedProvider.iogLink = true;
this.embedProvider.unlimitedSavegames = true;
this.embedProvider.showDemoBadge = false;
break;
}
case "miniclip": {
this.embedProvider.id = "miniclip";
break;
}
case "gamedistribution": {
this.embedProvider.id = "gamedistribution";
this.embedProvider.externalLinks = false;
this.embedProvider.adProvider = GamedistributionAdProvider;
break;
}
case "kongregate": {
this.embedProvider.id = "kongregate";
break;
}
case "crazygames": {
this.embedProvider.id = "crazygames";
break;
}
default: {
logger.error("Got unsupported embed provider:", providerId);
}
}
}
logger.log("Embed provider:", this.embedProvider.id);
return this.detectStorageImplementation()
.then(() => this.initializeAdProvider())
.then(() => super.initialize());
}
detectStorageImplementation() {
return new Promise(resolve => {
logger.log("Detecting storage");
if (!window.indexedDB) {
logger.log("Indexed DB not supported");
this.app.storage = new StorageImplBrowser(this.app);
resolve();
return;
}
// Try accessing the indexedb
let request;
try {
request = window.indexedDB.open("indexeddb_feature_detection", 1);
} catch (ex) {
logger.warn("Error while opening indexed db:", ex);
this.app.storage = new StorageImplBrowser(this.app);
resolve();
return;
}
request.onerror = err => {
logger.log("Indexed DB can *not* be accessed: ", err);
logger.log("Using fallback to local storage");
this.app.storage = new StorageImplBrowser(this.app);
resolve();
};
request.onsuccess = () => {
logger.log("Indexed DB *can* be accessed");
this.app.storage = new StorageImplBrowserIndexedDB(this.app);
resolve();
};
});
}
getHasUnlimitedSavegames() {
return this.embedProvider.unlimitedSavegames;
}
getShowDemoBadges() {
return this.embedProvider.showDemoBadge;
}
getId() {
return "browser@" + this.embedProvider.id;
}
getUiScale() {
if (IS_MOBILE) {
return 1;
}
const avgDims = Math.min(this.app.screenWidth, this.app.screenHeight);
return clamp((avgDims / 1000.0) * 1.9, 0.1, 10);
}
getSupportsRestart() {
return true;
}
getTouchPanStrength() {
return IS_MOBILE ? 1 : 0.5;
}
openExternalLink(url, force = false) {
logger.log("Opening external:", url);
if (force || this.embedProvider.externalLinks) {
window.open(url);
} else {
// Do nothing
alert(
"This platform does not allow opening external links. You can play on https://shapez.io directly to open them.\n\nClicked Link: " +
url
);
}
}
performRestart() {
logger.log("Performing restart");
window.location.reload(true);
}
/**
* Detects if there is an adblocker installed
* @returns {Promise<boolean>}
*/
detectAdblock() {
return Promise.race([
new Promise(resolve => {
// If the request wasn't blocked within a very short period of time, this means
// the adblocker is not active and the request was actually made -> ignore it then
setTimeout(() => resolve(false), 30);
}),
new Promise(resolve => {
fetch("https://googleads.g.doubleclick.net/pagead/id", {
method: "HEAD",
mode: "no-cors",
})
.then(res => {
resolve(false);
})
.catch(err => {
resolve(true);
});
}),
]);
}
initializeAdProvider() {
if (G_IS_DEV && !globalConfig.debug.testAds) {
logger.log("Ads disabled in local environment");
return Promise.resolve();
}
// First, detect adblocker
return this.detectAdblock().then(hasAdblocker => {
if (hasAdblocker) {
logger.log("Adblock detected");
return;
}
const adProvider = this.embedProvider.adProvider;
this.app.adProvider = new adProvider(this.app);
return this.app.adProvider.initialize().catch(err => {
logger.error("Failed to initialize ad provider, disabling ads:", err);
this.app.adProvider = new NoAdProvider(this.app);
});
});
}
exitApp() {
// Can not exit app
}
}
import { globalConfig, IS_MOBILE } from "../../core/config";
import { createLogger } from "../../core/logging";
import { queryParamOptions } from "../../core/query_parameters";
import { clamp } from "../../core/utils";
import { GamedistributionAdProvider } from "../ad_providers/gamedistribution";
import { NoAdProvider } from "../ad_providers/no_ad_provider";
import { PlatformWrapperInterface } from "../wrapper";
import { StorageImplBrowser } from "./storage";
import { StorageImplBrowserIndexedDB } from "./storage_indexed_db";
const logger = createLogger("platform/browser");
export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
initialize() {
this.recaptchaTokenCallback = null;
this.embedProvider = {
id: "shapezio-website",
adProvider: NoAdProvider,
iframed: false,
externalLinks: true,
iogLink: true,
};
if (!G_IS_STANDALONE && queryParamOptions.embedProvider) {
const providerId = queryParamOptions.embedProvider;
this.embedProvider.iframed = true;
this.embedProvider.iogLink = false;
switch (providerId) {
case "armorgames": {
this.embedProvider.id = "armorgames";
break;
}
case "iogames.space": {
this.embedProvider.id = "iogames.space";
this.embedProvider.iogLink = true;
break;
}
case "miniclip": {
this.embedProvider.id = "miniclip";
break;
}
case "gamedistribution": {
this.embedProvider.id = "gamedistribution";
this.embedProvider.externalLinks = false;
this.embedProvider.adProvider = GamedistributionAdProvider;
break;
}
case "kongregate": {
this.embedProvider.id = "kongregate";
break;
}
case "crazygames": {
this.embedProvider.id = "crazygames";
break;
}
default: {
logger.error("Got unsupported embed provider:", providerId);
}
}
}
logger.log("Embed provider:", this.embedProvider.id);
return this.detectStorageImplementation()
.then(() => this.initializeAdProvider())
.then(() => super.initialize());
}
detectStorageImplementation() {
return new Promise(resolve => {
logger.log("Detecting storage");
if (!window.indexedDB) {
logger.log("Indexed DB not supported");
this.app.storage = new StorageImplBrowser(this.app);
resolve();
return;
}
// Try accessing the indexedb
let request;
try {
request = window.indexedDB.open("indexeddb_feature_detection", 1);
} catch (ex) {
logger.warn("Error while opening indexed db:", ex);
this.app.storage = new StorageImplBrowser(this.app);
resolve();
return;
}
request.onerror = err => {
logger.log("Indexed DB can *not* be accessed: ", err);
logger.log("Using fallback to local storage");
this.app.storage = new StorageImplBrowser(this.app);
resolve();
};
request.onsuccess = () => {
logger.log("Indexed DB *can* be accessed");
this.app.storage = new StorageImplBrowserIndexedDB(this.app);
resolve();
};
});
}
getId() {
return "browser@" + this.embedProvider.id;
}
getUiScale() {
if (IS_MOBILE) {
return 1;
}
const avgDims = Math.min(this.app.screenWidth, this.app.screenHeight);
return clamp((avgDims / 1000.0) * 1.9, 0.1, 10);
}
getSupportsRestart() {
return true;
}
getTouchPanStrength() {
return IS_MOBILE ? 1 : 0.5;
}
openExternalLink(url, force = false) {
logger.log("Opening external:", url);
if (force || this.embedProvider.externalLinks) {
window.open(url);
} else {
// Do nothing
alert(
"This platform does not allow opening external links. You can play on https://shapez.io directly to open them.\n\nClicked Link: " +
url
);
}
}
performRestart() {
logger.log("Performing restart");
window.location.reload(true);
}
/**
* Detects if there is an adblocker installed
* @returns {Promise<boolean>}
*/
detectAdblock() {
return Promise.race([
new Promise(resolve => {
// If the request wasn't blocked within a very short period of time, this means
// the adblocker is not active and the request was actually made -> ignore it then
setTimeout(() => resolve(false), 30);
}),
new Promise(resolve => {
fetch("https://googleads.g.doubleclick.net/pagead/id", {
method: "HEAD",
mode: "no-cors",
})
.then(res => {
resolve(false);
})
.catch(err => {
resolve(true);
});
}),
]);
}
initializeAdProvider() {
if (G_IS_DEV && !globalConfig.debug.testAds) {
logger.log("Ads disabled in local environment");
return Promise.resolve();
}
// First, detect adblocker
return this.detectAdblock().then(hasAdblocker => {
if (hasAdblocker) {
logger.log("Adblock detected");
return;
}
const adProvider = this.embedProvider.adProvider;
this.app.adProvider = new adProvider(this.app);
return this.app.adProvider.initialize().catch(err => {
logger.error("Failed to initialize ad provider, disabling ads:", err);
this.app.adProvider = new NoAdProvider(this.app);
});
});
}
exitApp() {
// Can not exit app
}
}

View File

@ -1,65 +1,57 @@
import { PlatformWrapperImplBrowser } from "../browser/wrapper";
import { getIPCRenderer } from "../../core/utils";
import { createLogger } from "../../core/logging";
import { StorageImplElectron } from "./storage";
import { PlatformWrapperInterface } from "../wrapper";
const logger = createLogger("electron-wrapper");
export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser {
initialize() {
this.app.storage = new StorageImplElectron(this);
return PlatformWrapperInterface.prototype.initialize.call(this);
}
getId() {
return "electron";
}
getSupportsRestart() {
return true;
}
openExternalLink(url) {
logger.log(this, "Opening external:", url);
window.open(url, "about:blank");
}
getSupportsAds() {
return false;
}
getHasUnlimitedSavegames() {
return true;
}
getShowDemoBadges() {
return false;
}
performRestart() {
logger.log(this, "Performing restart");
window.location.reload(true);
}
initializeAdProvider() {
return Promise.resolve();
}
getSupportsFullscreen() {
return true;
}
setFullscreen(flag) {
getIPCRenderer().send("set-fullscreen", flag);
}
getSupportsAppExit() {
return true;
}
exitApp() {
logger.log(this, "Sending app exit signal");
getIPCRenderer().send("exit-app");
}
}
import { PlatformWrapperImplBrowser } from "../browser/wrapper";
import { getIPCRenderer } from "../../core/utils";
import { createLogger } from "../../core/logging";
import { StorageImplElectron } from "./storage";
import { PlatformWrapperInterface } from "../wrapper";
const logger = createLogger("electron-wrapper");
export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser {
initialize() {
this.app.storage = new StorageImplElectron(this);
return PlatformWrapperInterface.prototype.initialize.call(this);
}
getId() {
return "electron";
}
getSupportsRestart() {
return true;
}
openExternalLink(url) {
logger.log(this, "Opening external:", url);
window.open(url, "about:blank");
}
getSupportsAds() {
return false;
}
performRestart() {
logger.log(this, "Performing restart");
window.location.reload(true);
}
initializeAdProvider() {
return Promise.resolve();
}
getSupportsFullscreen() {
return true;
}
setFullscreen(flag) {
getIPCRenderer().send("set-fullscreen", flag);
}
getSupportsAppExit() {
return true;
}
exitApp() {
logger.log(this, "Sending app exit signal");
getIPCRenderer().send("exit-app");
}
}

View File

@ -6,7 +6,7 @@ import { GameRoot } from "../game/root";
import { newEmptyMap, clamp } from "../core/utils";
import { createLogger } from "../core/logging";
import { globalConfig, IS_DEMO } from "../core/config";
import { globalConfig } from "../core/config";
const logger = createLogger("sound");
@ -29,7 +29,9 @@ export const SOUNDS = {
};
export const MUSIC = {
theme: IS_DEMO ? "theme-short" : "theme-full",
// The theme always depends on the standalone only, even if running the full
// version in the browser
theme: G_IS_STANDALONE ? "theme-full" : "theme-short",
menu: "menu",
};

View File

@ -1,142 +1,131 @@
/* typehints:start */
import { Application } from "../application";
/* typehints:end */
import { IS_MOBILE } from "../core/config";
export class PlatformWrapperInterface {
constructor(app) {
/** @type {Application} */
this.app = app;
}
/** @returns {string} */
getId() {
abstract;
return "unknown-platform";
}
/**
* Returns the UI scale, called on every resize
* @returns {number} */
getUiScale() {
return 1;
}
/** @returns {boolean} */
getSupportsRestart() {
abstract;
return false;
}
/**
* Whether the user has unlimited savegames
*/
getHasUnlimitedSavegames() {
return true;
}
getShowDemoBadges() {
return false;
}
/**
* Returns the strength of touch pans with the mouse
*/
getTouchPanStrength() {
return 1;
}
/** @returns {Promise<void>} */
initialize() {
document.documentElement.classList.add("p-" + this.getId());
return Promise.resolve();
}
/**
* Should initialize the apps ad provider in case supported
* @returns {Promise<void>}
*/
initializeAdProvider() {
return Promise.resolve();
}
/**
* Should return the minimum supported zoom level
* @returns {number}
*/
getMinimumZoom() {
return 0.1 * this.getScreenScale();
}
/**
* Should return the maximum supported zoom level
* @returns {number}
*/
getMaximumZoom() {
return 3.5 * this.getScreenScale();
}
getScreenScale() {
return Math.min(window.innerWidth, window.innerHeight) / 1024.0;
}
/**
* Should return if this platform supports ads at all
*/
getSupportsAds() {
return false;
}
/**
* Attempt to open an external url
* @param {string} url
* @param {boolean=} force Whether to always open the url even if not allowed
*/
openExternalLink(url, force = false) {
abstract;
}
/**
* Attempt to restart the app
*/
performRestart() {
abstract;
}
/**
* Returns whether this platform supports a toggleable fullscreen
*/
getSupportsFullscreen() {
return false;
}
/**
* Should set the apps fullscreen state to the desired state
* @param {boolean} flag
*/
setFullscreen(flag) {
abstract;
}
/**
* Returns whether this platform supports quitting the app
*/
getSupportsAppExit() {
return false;
}
/**
* Attempts to quit the app
*/
exitApp() {
abstract;
}
/**
* Whether this platform supports a keyboard
*/
getSupportsKeyboard() {
return !IS_MOBILE;
}
}
/* typehints:start */
import { Application } from "../application";
/* typehints:end */
import { IS_MOBILE } from "../core/config";
export class PlatformWrapperInterface {
constructor(app) {
/** @type {Application} */
this.app = app;
}
/** @returns {string} */
getId() {
abstract;
return "unknown-platform";
}
/**
* Returns the UI scale, called on every resize
* @returns {number} */
getUiScale() {
return 1;
}
/** @returns {boolean} */
getSupportsRestart() {
abstract;
return false;
}
/**
* Returns the strength of touch pans with the mouse
*/
getTouchPanStrength() {
return 1;
}
/** @returns {Promise<void>} */
initialize() {
document.documentElement.classList.add("p-" + this.getId());
return Promise.resolve();
}
/**
* Should initialize the apps ad provider in case supported
* @returns {Promise<void>}
*/
initializeAdProvider() {
return Promise.resolve();
}
/**
* Should return the minimum supported zoom level
* @returns {number}
*/
getMinimumZoom() {
return 0.1 * this.getScreenScale();
}
/**
* Should return the maximum supported zoom level
* @returns {number}
*/
getMaximumZoom() {
return 3.5 * this.getScreenScale();
}
getScreenScale() {
return Math.min(window.innerWidth, window.innerHeight) / 1024.0;
}
/**
* Should return if this platform supports ads at all
*/
getSupportsAds() {
return false;
}
/**
* Attempt to open an external url
* @param {string} url
* @param {boolean=} force Whether to always open the url even if not allowed
*/
openExternalLink(url, force = false) {
abstract;
}
/**
* Attempt to restart the app
*/
performRestart() {
abstract;
}
/**
* Returns whether this platform supports a toggleable fullscreen
*/
getSupportsFullscreen() {
return false;
}
/**
* Should set the apps fullscreen state to the desired state
* @param {boolean} flag
*/
setFullscreen(flag) {
abstract;
}
/**
* Returns whether this platform supports quitting the app
*/
getSupportsAppExit() {
return false;
}
/**
* Attempts to quit the app
*/
exitApp() {
abstract;
}
/**
* Whether this platform supports a keyboard
*/
getSupportsKeyboard() {
return !IS_MOBILE;
}
}

View File

@ -6,8 +6,7 @@ import { ReadWriteProxy } from "../core/read_write_proxy";
import { BoolSetting, EnumSetting, RangeSetting, BaseSetting } from "./setting_types";
import { createLogger } from "../core/logging";
import { ExplainedResult } from "../core/explained_result";
import { THEMES, THEME, applyGameTheme } from "../game/theme";
import { IS_DEMO } from "../core/config";
import { THEMES, applyGameTheme } from "../game/theme";
import { T } from "../translations";
import { LANGUAGES } from "../languages";
@ -187,7 +186,9 @@ export const allApplicationSettings = [
app.platformWrapper.setFullscreen(value);
}
},
!IS_DEMO
/**
* @param {Application} app
*/ app => app.restrictionMgr.getHasExtendedSettings()
),
new BoolSetting(
@ -215,7 +216,9 @@ export const allApplicationSettings = [
applyGameTheme(id);
document.documentElement.setAttribute("data-theme", id);
},
enabled: !IS_DEMO,
enabledCb: /**
* @param {Application} app
*/ app => app.restrictionMgr.getHasExtendedSettings(),
}),
new EnumSetting("autosaveInterval", {
@ -255,6 +258,7 @@ export const allApplicationSettings = [
new BoolSetting("enableMousePan", enumCategories.advanced, (app, value) => {}),
new BoolSetting("alwaysMultiplace", enumCategories.advanced, (app, value) => {}),
new BoolSetting("zoomToCursor", enumCategories.advanced, (app, value) => {}),
new BoolSetting("clearCursorOnDeleteWhilePlacing", enumCategories.advanced, (app, value) => {}),
new BoolSetting("enableTunnelSmartplace", enumCategories.advanced, (app, value) => {}),
new BoolSetting("vignette", enumCategories.userInterface, (app, value) => {}),
@ -263,6 +267,7 @@ export const allApplicationSettings = [
new BoolSetting("rotationByBuilding", enumCategories.advanced, (app, value) => {}),
new BoolSetting("displayChunkBorders", enumCategories.advanced, (app, value) => {}),
new BoolSetting("pickMinerOnPatch", enumCategories.advanced, (app, value) => {}),
new RangeSetting("mapResourcesScale", enumCategories.advanced, () => null),
new EnumSetting("refreshRate", {
options: refreshRateOptions,
@ -271,7 +276,9 @@ export const allApplicationSettings = [
category: enumCategories.performance,
restartRequired: false,
changeCb: (app, id) => {},
enabled: !IS_DEMO,
enabledCb: /**
* @param {Application} app
*/ app => app.restrictionMgr.getHasExtendedSettings(),
}),
new BoolSetting("lowQualityMapResources", enumCategories.performance, (app, value) => {}),
@ -317,6 +324,8 @@ class SettingsStorage {
this.disableTileGrid = false;
this.lowQualityTextures = false;
this.simplifiedBelts = false;
this.zoomToCursor = true;
this.mapResourcesScale = 0.5;
/**
* @type {Object.<string, number>}
@ -355,7 +364,7 @@ export class ApplicationSettings extends ReadWriteProxy {
* @returns {SettingsStorage}
*/
getAllSettings() {
return this.getCurrentData().settings;
return this.currentData.settings;
}
/**
@ -527,7 +536,7 @@ export class ApplicationSettings extends ReadWriteProxy {
}
getCurrentVersion() {
return 28;
return 30;
}
/** @param {{settings: SettingsStorage, version: number}} data */
@ -660,6 +669,16 @@ export class ApplicationSettings extends ReadWriteProxy {
data.version = 28;
}
if (data.version < 29) {
data.settings.zoomToCursor = true;
data.version = 29;
}
if (data.version < 30) {
data.settings.mapResourcesScale = 0.5;
data.version = 30;
}
return ExplainedResult.good();
}
}

View File

@ -7,19 +7,30 @@ import { T } from "../translations";
const logger = createLogger("setting_types");
/*
* ***************************************************
*
* LEGACY CODE WARNING
*
* This is old code from yorg3.io and needs to be refactored
* @TODO
*
* ***************************************************
*/
export class BaseSetting {
/**
*
* @param {string} id
* @param {string} categoryId
* @param {function(Application, any):void} changeCb
* @param {boolean} enabled
* @param {function(Application) : boolean=} enabledCb
*/
constructor(id, categoryId, changeCb, enabled) {
constructor(id, categoryId, changeCb, enabledCb = null) {
this.id = id;
this.categoryId = categoryId;
this.changeCb = changeCb;
this.enabled = enabled;
this.enabledCb = enabledCb;
/** @type {Application} */
this.app = null;
@ -39,6 +50,7 @@ export class BaseSetting {
}
/**
* Binds all parameters
* @param {Application} app
* @param {HTMLElement} element
* @param {any} dialogs
@ -49,19 +61,37 @@ export class BaseSetting {
this.dialogs = dialogs;
}
getHtml() {
/**
* Returns the HTML for this setting
* @param {Application} app
*/
getHtml(app) {
abstract;
return "";
}
/**
* Returns whether this setting is enabled and available
* @param {Application} app
*/
getIsAvailable(app) {
return this.enabledCb ? this.enabledCb(app) : true;
}
syncValueToElement() {
abstract;
}
/**
* Attempts to modify the setting
*/
modify() {
abstract;
}
/**
* Shows the dialog that a restart is required
*/
showRestartRequiredDialog() {
const { restart } = this.dialogs.showInfo(
T.dialogs.restartRequired.title,
@ -74,6 +104,7 @@ export class BaseSetting {
}
/**
* Validates the set value
* @param {any} value
* @returns {boolean}
*/
@ -96,10 +127,10 @@ export class EnumSetting extends BaseSetting {
iconPrefix = null,
changeCb = null,
magicValue = null,
enabled = true,
enabledCb = null,
}
) {
super(id, category, changeCb, enabled);
super(id, category, changeCb, enabledCb);
this.options = options;
this.valueGetter = valueGetter;
@ -110,10 +141,14 @@ export class EnumSetting extends BaseSetting {
this.magicValue = magicValue;
}
getHtml() {
/**
* @param {Application} app
*/
getHtml(app) {
const available = this.getIsAvailable(app);
return `
<div class="setting cardbox ${this.enabled ? "enabled" : "disabled"}">
${this.enabled ? "" : `<span class="standaloneOnlyHint">${T.demo.settingNotAvailable}</span>`}
<div class="setting cardbox ${available ? "enabled" : "disabled"}">
${available ? "" : `<span class="standaloneOnlyHint">${T.demo.settingNotAvailable}</span>`}
<div class="row">
<label>${T.settings.labels[this.id].title}</label>
<div class="value enum" data-setting="${this.id}"></div>
@ -180,14 +215,18 @@ export class EnumSetting extends BaseSetting {
}
export class BoolSetting extends BaseSetting {
constructor(id, category, changeCb = null, enabled = true) {
super(id, category, changeCb, enabled);
constructor(id, category, changeCb = null, enabledCb = null) {
super(id, category, changeCb, enabledCb);
}
getHtml() {
/**
* @param {Application} app
*/
getHtml(app) {
const available = this.getIsAvailable(app);
return `
<div class="setting cardbox ${this.enabled ? "enabled" : "disabled"}">
${this.enabled ? "" : `<span class="standaloneOnlyHint">${T.demo.settingNotAvailable}</span>`}
<div class="setting cardbox ${available ? "enabled" : "disabled"}">
${available ? "" : `<span class="standaloneOnlyHint">${T.demo.settingNotAvailable}</span>`}
<div class="row">
<label>${T.settings.labels[this.id].title}</label>
@ -226,13 +265,13 @@ export class RangeSetting extends BaseSetting {
id,
category,
changeCb = null,
enabled = true,
defaultValue = 1.0,
minValue = 0,
maxValue = 1.0,
stepSize = 0.0001
stepSize = 0.0001,
enabledCb = null
) {
super(id, category, changeCb, enabled);
super(id, category, changeCb, enabledCb);
this.defaultValue = defaultValue;
this.minValue = minValue;
@ -240,10 +279,14 @@ export class RangeSetting extends BaseSetting {
this.stepSize = stepSize;
}
getHtml() {
/**
* @param {Application} app
*/
getHtml(app) {
const available = this.getIsAvailable(app);
return `
<div class="setting cardbox ${this.enabled ? "enabled" : "disabled"}">
${this.enabled ? "" : `<span class="standaloneOnlyHint">${T.demo.settingNotAvailable}</span>`}
<div class="setting cardbox ${available ? "enabled" : "disabled"}">
${available ? "" : `<span class="standaloneOnlyHint">${T.demo.settingNotAvailable}</span>`}
<div class="row">
<label>${T.settings.labels[this.id].title}</label>

View File

@ -40,13 +40,6 @@ export class SavegameManager extends ReadWriteProxy {
return 1002;
}
/**
* @returns {SavegamesData}
*/
getCurrentData() {
return super.getCurrentData();
}
verify(data) {
// TODO / FIXME!!!!
return ExplainedResult.good();
@ -96,6 +89,14 @@ export class SavegameManager extends ReadWriteProxy {
return new Savegame(this.app, { internalId, metaDataRef: metadata });
}
/**
* Returns if this manager has any savegame of a 1.1.19 version, which
* enables all levels
*/
getHasAnyLegacySavegames() {
return this.currentData.savegames.some(savegame => savegame.version === 1005 || savegame.level > 14);
}
/**
* Deletes a savegame
* @param {SavegameMetadata} game
@ -149,7 +150,9 @@ export class SavegameManager extends ReadWriteProxy {
});
this.currentData.savegames.push(metaData);
this.sortSavegames();
// Notice: This is async and happening in the background
this.updateAfterSavegamesChanged();
return new Savegame(this.app, {
internalId: id,
@ -157,8 +160,16 @@ export class SavegameManager extends ReadWriteProxy {
});
}
/**
* Attempts to import a savegame
* @param {object} data
*/
importSavegame(data) {
const savegame = this.createNewSavegame();
// Track legacy savegames
const isOldSavegame = data.version < 1006;
const migrationResult = savegame.migrate(data);
if (migrationResult.isBad()) {
return Promise.reject("Failed to migrate: " + migrationResult.reason);
@ -170,7 +181,19 @@ export class SavegameManager extends ReadWriteProxy {
return Promise.reject("Verification failed: " + verification.result);
}
return savegame.writeSavegameAndMetadata().then(() => this.sortSavegames());
return savegame
.writeSavegameAndMetadata()
.then(() => this.updateAfterSavegamesChanged())
.then(() => this.app.restrictionMgr.onHasLegacySavegamesChanged(isOldSavegame));
}
/**
* Hook after the savegames got changed
*/
updateAfterSavegamesChanged() {
return this.sortSavegames()
.then(() => this.writeAsync())
.then(() => this.app.restrictionMgr.onHasLegacySavegamesChanged(this.getHasAnyLegacySavegames()));
}
/**
@ -219,7 +242,7 @@ export class SavegameManager extends ReadWriteProxy {
if (G_IS_DEV && globalConfig.debug.disableSavegameWrite) {
return Promise.resolve();
}
return this.sortSavegames().then(() => this.writeAsync());
return this.updateAfterSavegamesChanged();
});
}
}

View File

@ -130,7 +130,7 @@ export class SavegameSerializer {
errorReason = errorReason || root.time.deserialize(savegame.time);
errorReason = errorReason || root.camera.deserialize(savegame.camera);
errorReason = errorReason || root.map.deserialize(savegame.map);
errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals);
errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals, root);
errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes);
errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints);
errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities);

View File

@ -19,7 +19,6 @@ import { getCodeFromBuildingData } from "../../game/building_codes.js";
import { StaticMapEntityComponent } from "../../game/components/static_map_entity.js";
import { Entity } from "../../game/entity.js";
import { defaultBuildingVariant, MetaBuilding } from "../../game/meta_building.js";
import { finalGameShape } from "../../game/upgrades.js";
import { SavegameInterface_V1005 } from "./1005.js";
const schema = require("./1006.json");
@ -152,7 +151,8 @@ export class SavegameInterface_V1006 extends SavegameInterface_V1005 {
stored[shapeKey] = rebalance(stored[shapeKey]);
}
stored[finalGameShape] = 0;
// Reset final game shape
stored["RuCw--Cw:----Ru--"] = 0;
// Reduce goals
if (dump.hubGoals.currentGoal) {
@ -248,7 +248,7 @@ export class SavegameInterface_V1006 extends SavegameInterface_V1005 {
if (components.Storage) {
// @ts-ignore
components.Storage = {
storedCount: 0,
storedCount: rebalance(components.Storage.storedCount),
storedItem: null,
};
}

View File

@ -1,344 +1,351 @@
import { createLogger } from "../core/logging";
import {
BaseDataType,
TypeArray,
TypeBoolean,
TypeClass,
TypeClassData,
TypeClassFromMetaclass,
TypeClassId,
TypeEntity,
TypeEntityWeakref,
TypeEnum,
TypeFixedClass,
TypeInteger,
TypeKeyValueMap,
TypeMetaClass,
TypeNullable,
TypeNumber,
TypePair,
TypePositiveInteger,
TypePositiveNumber,
TypeString,
TypeStructuredObject,
TypeVector,
} from "./serialization_data_types";
const logger = createLogger("serialization");
// Schema declarations
export const types = {
int: new TypeInteger(),
uint: new TypePositiveInteger(),
float: new TypeNumber(),
ufloat: new TypePositiveNumber(),
string: new TypeString(),
entity: new TypeEntity(),
weakEntityRef: new TypeEntityWeakref(),
vector: new TypeVector(),
tileVector: new TypeVector(),
bool: new TypeBoolean(),
/**
* @param {BaseDataType} wrapped
*/
nullable(wrapped) {
return new TypeNullable(wrapped);
},
/**
* @param {FactoryTemplate<*>|SingletonFactoryTemplate<*>} registry
*/
classId(registry) {
return new TypeClassId(registry);
},
/**
* @param {BaseDataType} valueType
* @param {boolean=} includeEmptyValues
*/
keyValueMap(valueType, includeEmptyValues = true) {
return new TypeKeyValueMap(valueType, includeEmptyValues);
},
/**
* @param {Object<string, any>} values
*/
enum(values) {
return new TypeEnum(values);
},
/**
* @param {FactoryTemplate<*>} registry
* @param {(GameRoot, any) => object=} resolver
*/
obj(registry, resolver = null) {
return new TypeClass(registry, resolver);
},
/**
* @param {FactoryTemplate<*>} registry
*/
objData(registry) {
return new TypeClassData(registry);
},
/**
* @param {typeof BasicSerializableObject} cls
*/
knownType(cls) {
return new TypeFixedClass(cls);
},
/**
* @param {BaseDataType} innerType
*/
array(innerType) {
return new TypeArray(innerType);
},
/**
* @param {SingletonFactoryTemplate<*>} innerType
*/
classRef(registry) {
return new TypeMetaClass(registry);
},
/**
* @param {Object.<string, BaseDataType>} descriptor
*/
structured(descriptor) {
return new TypeStructuredObject(descriptor);
},
/**
* @param {BaseDataType} a
* @param {BaseDataType} b
*/
pair(a, b) {
return new TypePair(a, b);
},
/**
* @param {typeof BasicSerializableObject} classHandle
* @param {SingletonFactoryTemplate<*>} registry
*/
classWithMetaclass(classHandle, registry) {
return new TypeClassFromMetaclass(classHandle, registry);
},
};
/**
* A full schema declaration
* @typedef {Object.<string, BaseDataType>} Schema
*/
const globalSchemaCache = {};
/* dev:start */
const classnamesCache = {};
/* dev:end*/
export class BasicSerializableObject {
/* dev:start */
/**
* Fixes typeof DerivedComponent is not assignable to typeof Component, compiled out
* in non-dev builds
*/
constructor(...args) {}
/* dev:end */
static getId() {
abstract;
}
/**
* Should return the serialization schema
* @returns {Schema}
*/
static getSchema() {
return {};
}
// Implementation
/** @returns {Schema} */
static getCachedSchema() {
const id = this.getId();
/* dev:start */
assert(
classnamesCache[id] === this || classnamesCache[id] === undefined,
"Class name taken twice: " + id + " (from " + this.name + ")"
);
classnamesCache[id] = this;
/* dev:end */
const entry = globalSchemaCache[id];
if (entry) {
return entry;
}
const schema = this.getSchema();
globalSchemaCache[id] = schema;
return schema;
}
/** @returns {object} */
serialize() {
return serializeSchema(
this,
/** @type {typeof BasicSerializableObject} */ (this.constructor).getCachedSchema()
);
}
/**
* @param {any} data
* @param {import("./savegame_serializer").GameRoot} root
* @returns {string|void}
*/
deserialize(data, root = null) {
return deserializeSchema(
this,
/** @type {typeof BasicSerializableObject} */ (this.constructor).getCachedSchema(),
data,
null,
root
);
}
/** @returns {string|void} */
static verify(data) {
return verifySchema(this.getCachedSchema(), data);
}
}
/**
* Serializes an object using the given schema, mergin with the given properties
* @param {object} obj The object to serialize
* @param {Schema} schema The schema to use
* @param {object=} mergeWith Any additional properties to merge with the schema, useful for super calls
* @returns {object} Serialized data object
*/
export function serializeSchema(obj, schema, mergeWith = {}) {
for (const key in schema) {
if (!obj.hasOwnProperty(key)) {
logger.error("Invalid schema, property", key, "does not exist on", obj, "(schema=", schema, ")");
assert(
obj.hasOwnProperty(key),
"serialization: invalid schema, property does not exist on object: " + key
);
}
if (!schema[key]) {
assert(false, "Invalid schema (bad key '" + key + "'): " + JSON.stringify(schema));
}
if (G_IS_DEV) {
try {
mergeWith[key] = schema[key].serialize(obj[key]);
} catch (ex) {
logger.error(
"Serialization of",
obj,
"failed on key '" + key + "' ->",
ex,
"(schema was",
schema,
")"
);
throw ex;
}
} else {
mergeWith[key] = schema[key].serialize(obj[key]);
}
}
return mergeWith;
}
/**
* Deserializes data into an object
* @param {object} obj The object to store the deserialized data into
* @param {Schema} schema The schema to use
* @param {object} data The serialized data
* @param {string|void|null=} baseclassErrorResult Convenience, if this is a string error code, do nothing and return it
* @param {import("../game/root").GameRoot=} root Optional game root reference
* @returns {string|void} String error code or nothing on success
*/
export function deserializeSchema(obj, schema, data, baseclassErrorResult = null, root) {
if (baseclassErrorResult) {
return baseclassErrorResult;
}
if (!data) {
logger.error("Got 'NULL' data for", obj, "and schema", schema, "!");
return "Got null data";
}
for (const key in schema) {
if (!data.hasOwnProperty(key)) {
logger.error("Data", data, "does not contain", key, "(schema:", schema, ")");
return "Missing key in schema: " + key + " of class " + obj.constructor.name;
}
if (!schema[key].allowNull() && (data[key] === null || data[key] === undefined)) {
logger.error("Data", data, "has null value for", key, "(schema:", schema, ")");
return "Non-nullable entry is null: " + key + " of class " + obj.constructor.name;
}
const errorStatus = schema[key].deserializeWithVerify(data[key], obj, key, obj.root || root);
if (errorStatus) {
logger.error(
"Deserialization failed with error '" + errorStatus + "' on object",
obj,
"and key",
key,
"(root? =",
obj.root ? "y" : "n",
")"
);
return errorStatus;
}
}
}
/**
* Verifies stored data using the given schema
* @param {Schema} schema The schema to use
* @param {object} data The data to verify
* @returns {string|void} String error code or nothing on success
*/
export function verifySchema(schema, data) {
for (const key in schema) {
if (!data.hasOwnProperty(key)) {
logger.error("Data", data, "does not contain", key, "(schema:", schema, ")");
return "verify: missing key required by schema in stored data: " + key;
}
if (!schema[key].allowNull() && (data[key] === null || data[key] === undefined)) {
logger.error("Data", data, "has null value for", key, "(schema:", schema, ")");
return "verify: non-nullable entry is null: " + key;
}
const errorStatus = schema[key].verifySerializedValue(data[key]);
if (errorStatus) {
logger.error(errorStatus);
return "verify: " + errorStatus;
}
}
}
/**
* Extends a schema by adding the properties from the new schema to the existing base schema
* @param {Schema} base
* @param {Schema} newOne
* @returns {Schema}
*/
export function extendSchema(base, newOne) {
/** @type {Schema} */
const result = Object.assign({}, base);
for (const key in newOne) {
if (result.hasOwnProperty(key)) {
logger.error("Extend schema got duplicate key:", key);
continue;
}
result[key] = newOne[key];
}
return result;
}
import { createLogger } from "../core/logging";
import {
BaseDataType,
TypeArray,
TypeBoolean,
TypeClass,
TypeClassData,
TypeClassFromMetaclass,
TypeClassId,
TypeEntity,
TypeEntityWeakref,
TypeEnum,
TypeFixedClass,
TypeInteger,
TypeKeyValueMap,
TypeMetaClass,
TypeNullable,
TypeNumber,
TypePair,
TypePositiveInteger,
TypePositiveNumber,
TypeString,
TypeStructuredObject,
TypeVector,
} from "./serialization_data_types";
const logger = createLogger("serialization");
// Schema declarations
export const types = {
int: new TypeInteger(),
uint: new TypePositiveInteger(),
float: new TypeNumber(),
ufloat: new TypePositiveNumber(),
string: new TypeString(),
entity: new TypeEntity(),
weakEntityRef: new TypeEntityWeakref(),
vector: new TypeVector(),
tileVector: new TypeVector(),
bool: new TypeBoolean(),
/**
* @param {BaseDataType} wrapped
*/
nullable(wrapped) {
return new TypeNullable(wrapped);
},
/**
* @param {FactoryTemplate<*>|SingletonFactoryTemplate<*>} registry
*/
classId(registry) {
return new TypeClassId(registry);
},
/**
* @param {BaseDataType} valueType
* @param {boolean=} includeEmptyValues
*/
keyValueMap(valueType, includeEmptyValues = true) {
return new TypeKeyValueMap(valueType, includeEmptyValues);
},
/**
* @param {Object<string, any>} values
*/
enum(values) {
return new TypeEnum(values);
},
/**
* @param {FactoryTemplate<*>} registry
* @param {(GameRoot, any) => object=} resolver
*/
obj(registry, resolver = null) {
return new TypeClass(registry, resolver);
},
/**
* @param {FactoryTemplate<*>} registry
*/
objData(registry) {
return new TypeClassData(registry);
},
/**
* @param {typeof BasicSerializableObject} cls
*/
knownType(cls) {
return new TypeFixedClass(cls);
},
/**
* @param {BaseDataType} innerType
*/
array(innerType) {
return new TypeArray(innerType);
},
/**
* @param {BaseDataType} innerType
*/
fixedSizeArray(innerType) {
return new TypeArray(innerType, true);
},
/**
* @param {SingletonFactoryTemplate<*>} innerType
*/
classRef(registry) {
return new TypeMetaClass(registry);
},
/**
* @param {Object.<string, BaseDataType>} descriptor
*/
structured(descriptor) {
return new TypeStructuredObject(descriptor);
},
/**
* @param {BaseDataType} a
* @param {BaseDataType} b
*/
pair(a, b) {
return new TypePair(a, b);
},
/**
* @param {typeof BasicSerializableObject} classHandle
* @param {SingletonFactoryTemplate<*>} registry
*/
classWithMetaclass(classHandle, registry) {
return new TypeClassFromMetaclass(classHandle, registry);
},
};
/**
* A full schema declaration
* @typedef {Object.<string, BaseDataType>} Schema
*/
const globalSchemaCache = {};
/* dev:start */
const classnamesCache = {};
/* dev:end*/
export class BasicSerializableObject {
/* dev:start */
/**
* Fixes typeof DerivedComponent is not assignable to typeof Component, compiled out
* in non-dev builds
*/
constructor(...args) {}
/* dev:end */
static getId() {
abstract;
}
/**
* Should return the serialization schema
* @returns {Schema}
*/
static getSchema() {
return {};
}
// Implementation
/** @returns {Schema} */
static getCachedSchema() {
const id = this.getId();
/* dev:start */
assert(
classnamesCache[id] === this || classnamesCache[id] === undefined,
"Class name taken twice: " + id + " (from " + this.name + ")"
);
classnamesCache[id] = this;
/* dev:end */
const entry = globalSchemaCache[id];
if (entry) {
return entry;
}
const schema = this.getSchema();
globalSchemaCache[id] = schema;
return schema;
}
/** @returns {object} */
serialize() {
return serializeSchema(
this,
/** @type {typeof BasicSerializableObject} */ (this.constructor).getCachedSchema()
);
}
/**
* @param {any} data
* @param {import("./savegame_serializer").GameRoot} root
* @returns {string|void}
*/
deserialize(data, root = null) {
return deserializeSchema(
this,
/** @type {typeof BasicSerializableObject} */ (this.constructor).getCachedSchema(),
data,
null,
root
);
}
/** @returns {string|void} */
static verify(data) {
return verifySchema(this.getCachedSchema(), data);
}
}
/**
* Serializes an object using the given schema, mergin with the given properties
* @param {object} obj The object to serialize
* @param {Schema} schema The schema to use
* @param {object=} mergeWith Any additional properties to merge with the schema, useful for super calls
* @returns {object} Serialized data object
*/
export function serializeSchema(obj, schema, mergeWith = {}) {
for (const key in schema) {
if (!obj.hasOwnProperty(key)) {
logger.error("Invalid schema, property", key, "does not exist on", obj, "(schema=", schema, ")");
assert(
obj.hasOwnProperty(key),
"serialization: invalid schema, property does not exist on object: " + key
);
}
if (!schema[key]) {
assert(false, "Invalid schema (bad key '" + key + "'): " + JSON.stringify(schema));
}
if (G_IS_DEV) {
try {
mergeWith[key] = schema[key].serialize(obj[key]);
} catch (ex) {
logger.error(
"Serialization of",
obj,
"failed on key '" + key + "' ->",
ex,
"(schema was",
schema,
")"
);
throw ex;
}
} else {
mergeWith[key] = schema[key].serialize(obj[key]);
}
}
return mergeWith;
}
/**
* Deserializes data into an object
* @param {object} obj The object to store the deserialized data into
* @param {Schema} schema The schema to use
* @param {object} data The serialized data
* @param {string|void|null=} baseclassErrorResult Convenience, if this is a string error code, do nothing and return it
* @param {import("../game/root").GameRoot=} root Optional game root reference
* @returns {string|void} String error code or nothing on success
*/
export function deserializeSchema(obj, schema, data, baseclassErrorResult = null, root) {
if (baseclassErrorResult) {
return baseclassErrorResult;
}
if (!data) {
logger.error("Got 'NULL' data for", obj, "and schema", schema, "!");
return "Got null data";
}
for (const key in schema) {
if (!data.hasOwnProperty(key)) {
logger.error("Data", data, "does not contain", key, "(schema:", schema, ")");
return "Missing key in schema: " + key + " of class " + obj.constructor.name;
}
if (!schema[key].allowNull() && (data[key] === null || data[key] === undefined)) {
logger.error("Data", data, "has null value for", key, "(schema:", schema, ")");
return "Non-nullable entry is null: " + key + " of class " + obj.constructor.name;
}
const errorStatus = schema[key].deserializeWithVerify(data[key], obj, key, obj.root || root);
if (errorStatus) {
logger.error(
"Deserialization failed with error '" + errorStatus + "' on object",
obj,
"and key",
key,
"(root? =",
obj.root ? "y" : "n",
")"
);
return errorStatus;
}
}
}
/**
* Verifies stored data using the given schema
* @param {Schema} schema The schema to use
* @param {object} data The data to verify
* @returns {string|void} String error code or nothing on success
*/
export function verifySchema(schema, data) {
for (const key in schema) {
if (!data.hasOwnProperty(key)) {
logger.error("Data", data, "does not contain", key, "(schema:", schema, ")");
return "verify: missing key required by schema in stored data: " + key;
}
if (!schema[key].allowNull() && (data[key] === null || data[key] === undefined)) {
logger.error("Data", data, "has null value for", key, "(schema:", schema, ")");
return "verify: non-nullable entry is null: " + key;
}
const errorStatus = schema[key].verifySerializedValue(data[key]);
if (errorStatus) {
logger.error(errorStatus);
return "verify: " + errorStatus;
}
}
}
/**
* Extends a schema by adding the properties from the new schema to the existing base schema
* @param {Schema} base
* @param {Schema} newOne
* @returns {Schema}
*/
export function extendSchema(base, newOne) {
/** @type {Schema} */
const result = Object.assign({}, base);
for (const key in newOne) {
if (result.hasOwnProperty(key)) {
logger.error("Extend schema got duplicate key:", key);
continue;
}
result[key] = newOne[key];
}
return result;
}

File diff suppressed because it is too large Load Diff

View File

@ -80,7 +80,10 @@ export class SerializerInternal {
for (const componentId in data) {
if (!entity.components[componentId]) {
if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts) {
logger.warn("Entity no longer has component:", componentId);
// @ts-ignore
if (++window.componentWarningsShown < 100) {
logger.warn("Entity no longer has component:", componentId);
}
}
continue;
}

View File

@ -285,14 +285,10 @@ export class InGameState extends GameState {
*/
stage7Warmup() {
if (this.switchStage(stages.s7_warmup)) {
if (G_IS_DEV && globalConfig.debug.noArtificialDelays) {
this.warmupTimeSeconds = 0.05;
if (this.creationPayload.fastEnter) {
this.warmupTimeSeconds = globalConfig.warmupTimeSecondsFast;
} else {
if (this.creationPayload.fastEnter) {
this.warmupTimeSeconds = globalConfig.warmupTimeSecondsFast;
} else {
this.warmupTimeSeconds = globalConfig.warmupTimeSecondsRegular;
}
this.warmupTimeSeconds = globalConfig.warmupTimeSecondsRegular;
}
}
}

View File

@ -1,179 +1,173 @@
import { TextualGameState } from "../core/textual_game_state";
import { SOUNDS } from "../platform/sound";
import { T } from "../translations";
import { KEYMAPPINGS, getStringForKeyCode } from "../game/key_action_mapper";
import { Dialog } from "../core/modal_dialog_elements";
import { IS_DEMO } from "../core/config";
export class KeybindingsState extends TextualGameState {
constructor() {
super("KeybindingsState");
}
getStateHeaderTitle() {
return T.keybindings.title;
}
getMainContentHTML() {
return `
<div class="topEntries">
<span class="hint">${T.keybindings.hint}</span>
<button class="styledButton resetBindings">${T.keybindings.resetKeybindings}</button>
</div>
<div class="keybindings">
</div>
`;
}
onEnter() {
const keybindingsElem = this.htmlElement.querySelector(".keybindings");
this.trackClicks(this.htmlElement.querySelector(".resetBindings"), this.resetBindings);
for (const category in KEYMAPPINGS) {
const categoryDiv = document.createElement("div");
categoryDiv.classList.add("category");
keybindingsElem.appendChild(categoryDiv);
const labelDiv = document.createElement("strong");
labelDiv.innerText = T.keybindings.categoryLabels[category];
labelDiv.classList.add("categoryLabel");
categoryDiv.appendChild(labelDiv);
for (const keybindingId in KEYMAPPINGS[category]) {
const mapped = KEYMAPPINGS[category][keybindingId];
const elem = document.createElement("div");
elem.classList.add("entry");
elem.setAttribute("data-keybinding", keybindingId);
categoryDiv.appendChild(elem);
const title = document.createElement("span");
title.classList.add("title");
title.innerText = T.keybindings.mappings[keybindingId];
elem.appendChild(title);
const mappingDiv = document.createElement("span");
mappingDiv.classList.add("mapping");
elem.appendChild(mappingDiv);
const editBtn = document.createElement("button");
editBtn.classList.add("styledButton", "editKeybinding");
const resetBtn = document.createElement("button");
resetBtn.classList.add("styledButton", "resetKeybinding");
if (mapped.builtin) {
editBtn.classList.add("disabled");
resetBtn.classList.add("disabled");
} else {
this.trackClicks(editBtn, () => this.editKeybinding(keybindingId));
this.trackClicks(resetBtn, () => this.resetKeybinding(keybindingId));
}
elem.appendChild(editBtn);
elem.appendChild(resetBtn);
}
}
this.updateKeybindings();
}
editKeybinding(id) {
// if (IS_DEMO) {
// this.dialogs.showFeatureRestrictionInfo(T.demo.features.customizeKeybindings);
// return;
// }
const dialog = new Dialog({
app: this.app,
title: T.dialogs.editKeybinding.title,
contentHTML: T.dialogs.editKeybinding.desc,
buttons: ["cancel:good"],
type: "info",
});
dialog.inputReciever.keydown.add(({ keyCode, shift, alt, event }) => {
if (keyCode === 27) {
this.dialogs.closeDialog(dialog);
return;
}
if (event) {
event.preventDefault();
}
if (event.target && event.target.tagName === "BUTTON" && keyCode === 1) {
return;
}
if (
// Enter
keyCode === 13
) {
// Ignore builtins
return;
}
this.app.settings.updateKeybindingOverride(id, keyCode);
this.dialogs.closeDialog(dialog);
this.updateKeybindings();
});
dialog.inputReciever.backButton.add(() => {});
this.dialogs.internalShowDialog(dialog);
this.app.sound.playUiSound(SOUNDS.dialogOk);
}
updateKeybindings() {
const overrides = this.app.settings.getKeybindingOverrides();
for (const category in KEYMAPPINGS) {
for (const keybindingId in KEYMAPPINGS[category]) {
const mapped = KEYMAPPINGS[category][keybindingId];
const container = this.htmlElement.querySelector("[data-keybinding='" + keybindingId + "']");
assert(container, "Container for keybinding not found: " + keybindingId);
let keyCode = mapped.keyCode;
if (overrides[keybindingId]) {
keyCode = overrides[keybindingId];
}
const mappingDiv = container.querySelector(".mapping");
mappingDiv.innerHTML = getStringForKeyCode(keyCode);
mappingDiv.classList.toggle("changed", !!overrides[keybindingId]);
const resetBtn = container.querySelector("button.resetKeybinding");
resetBtn.classList.toggle("disabled", mapped.builtin || !overrides[keybindingId]);
}
}
}
resetKeybinding(id) {
this.app.settings.resetKeybindingOverride(id);
this.updateKeybindings();
}
resetBindings() {
const { reset } = this.dialogs.showWarning(
T.dialogs.resetKeybindingsConfirmation.title,
T.dialogs.resetKeybindingsConfirmation.desc,
["cancel:good", "reset:bad"]
);
reset.add(() => {
this.app.settings.resetKeybindingOverrides();
this.updateKeybindings();
this.dialogs.showInfo(T.dialogs.keybindingsResetOk.title, T.dialogs.keybindingsResetOk.desc);
});
}
getDefaultPreviousState() {
return "SettingsState";
}
}
import { Dialog } from "../core/modal_dialog_elements";
import { TextualGameState } from "../core/textual_game_state";
import { getStringForKeyCode, KEYMAPPINGS } from "../game/key_action_mapper";
import { SOUNDS } from "../platform/sound";
import { T } from "../translations";
export class KeybindingsState extends TextualGameState {
constructor() {
super("KeybindingsState");
}
getStateHeaderTitle() {
return T.keybindings.title;
}
getMainContentHTML() {
return `
<div class="topEntries">
<span class="hint">${T.keybindings.hint}</span>
<button class="styledButton resetBindings">${T.keybindings.resetKeybindings}</button>
</div>
<div class="keybindings">
</div>
`;
}
onEnter() {
const keybindingsElem = this.htmlElement.querySelector(".keybindings");
this.trackClicks(this.htmlElement.querySelector(".resetBindings"), this.resetBindings);
for (const category in KEYMAPPINGS) {
const categoryDiv = document.createElement("div");
categoryDiv.classList.add("category");
keybindingsElem.appendChild(categoryDiv);
const labelDiv = document.createElement("strong");
labelDiv.innerText = T.keybindings.categoryLabels[category];
labelDiv.classList.add("categoryLabel");
categoryDiv.appendChild(labelDiv);
for (const keybindingId in KEYMAPPINGS[category]) {
const mapped = KEYMAPPINGS[category][keybindingId];
const elem = document.createElement("div");
elem.classList.add("entry");
elem.setAttribute("data-keybinding", keybindingId);
categoryDiv.appendChild(elem);
const title = document.createElement("span");
title.classList.add("title");
title.innerText = T.keybindings.mappings[keybindingId];
elem.appendChild(title);
const mappingDiv = document.createElement("span");
mappingDiv.classList.add("mapping");
elem.appendChild(mappingDiv);
const editBtn = document.createElement("button");
editBtn.classList.add("styledButton", "editKeybinding");
const resetBtn = document.createElement("button");
resetBtn.classList.add("styledButton", "resetKeybinding");
if (mapped.builtin) {
editBtn.classList.add("disabled");
resetBtn.classList.add("disabled");
} else {
this.trackClicks(editBtn, () => this.editKeybinding(keybindingId));
this.trackClicks(resetBtn, () => this.resetKeybinding(keybindingId));
}
elem.appendChild(editBtn);
elem.appendChild(resetBtn);
}
}
this.updateKeybindings();
}
editKeybinding(id) {
const dialog = new Dialog({
app: this.app,
title: T.dialogs.editKeybinding.title,
contentHTML: T.dialogs.editKeybinding.desc,
buttons: ["cancel:good"],
type: "info",
});
dialog.inputReciever.keydown.add(({ keyCode, shift, alt, event }) => {
if (keyCode === 27) {
this.dialogs.closeDialog(dialog);
return;
}
if (event) {
event.preventDefault();
}
if (event.target && event.target.tagName === "BUTTON" && keyCode === 1) {
return;
}
if (
// Enter
keyCode === 13
) {
// Ignore builtins
return;
}
this.app.settings.updateKeybindingOverride(id, keyCode);
this.dialogs.closeDialog(dialog);
this.updateKeybindings();
});
dialog.inputReciever.backButton.add(() => {});
this.dialogs.internalShowDialog(dialog);
this.app.sound.playUiSound(SOUNDS.dialogOk);
}
updateKeybindings() {
const overrides = this.app.settings.getKeybindingOverrides();
for (const category in KEYMAPPINGS) {
for (const keybindingId in KEYMAPPINGS[category]) {
const mapped = KEYMAPPINGS[category][keybindingId];
const container = this.htmlElement.querySelector("[data-keybinding='" + keybindingId + "']");
assert(container, "Container for keybinding not found: " + keybindingId);
let keyCode = mapped.keyCode;
if (overrides[keybindingId]) {
keyCode = overrides[keybindingId];
}
const mappingDiv = container.querySelector(".mapping");
mappingDiv.innerHTML = getStringForKeyCode(keyCode);
mappingDiv.classList.toggle("changed", !!overrides[keybindingId]);
const resetBtn = container.querySelector("button.resetKeybinding");
resetBtn.classList.toggle("disabled", mapped.builtin || !overrides[keybindingId]);
}
}
}
resetKeybinding(id) {
this.app.settings.resetKeybindingOverride(id);
this.updateKeybindings();
}
resetBindings() {
const { reset } = this.dialogs.showWarning(
T.dialogs.resetKeybindingsConfirmation.title,
T.dialogs.resetKeybindingsConfirmation.desc,
["cancel:good", "reset:bad"]
);
reset.add(() => {
this.app.settings.resetKeybindingOverrides();
this.updateKeybindings();
this.dialogs.showInfo(T.dialogs.keybindingsResetOk.title, T.dialogs.keybindingsResetOk.desc);
});
}
getDefaultPreviousState() {
return "SettingsState";
}
}

View File

@ -1,21 +1,23 @@
import { GameState } from "../core/game_state";
import { cachebust } from "../core/cachebust";
import { A_B_TESTING_LINK_TYPE, globalConfig, IS_DEMO, THIRDPARTY_URLS } from "../core/config";
import { A_B_TESTING_LINK_TYPE, globalConfig, THIRDPARTY_URLS } from "../core/config";
import { GameState } from "../core/game_state";
import { DialogWithForm } from "../core/modal_dialog_elements";
import { FormElementInput } from "../core/modal_dialog_forms";
import { ReadWriteProxy } from "../core/read_write_proxy";
import {
makeDiv,
makeButtonElement,
formatSecondsToTimeAgo,
waitNextFrame,
generateFileDownload,
isSupportedBrowser,
makeButton,
makeButtonElement,
makeDiv,
removeAllChildren,
startFileChoose,
waitNextFrame,
} from "../core/utils";
import { ReadWriteProxy } from "../core/read_write_proxy";
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { T } from "../translations";
import { getApplicationSettingById } from "../profile/application_settings";
import { FormElementInput } from "../core/modal_dialog_forms";
import { DialogWithForm } from "../core/modal_dialog_elements";
import { T } from "../translations";
const trim = require("trim");
@ -24,23 +26,6 @@ const trim = require("trim");
* @typedef {import("../profile/setting_types").EnumSetting} EnumSetting
*/
/**
* Generates a file download
* @param {string} filename
* @param {string} text
*/
function generateFileDownload(filename, text) {
var element = document.createElement("a");
element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text));
element.setAttribute("download", filename);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
export class MainMenuState extends GameState {
constructor() {
super("MainMenuState");
@ -49,18 +34,16 @@ export class MainMenuState extends GameState {
getInnerHTML() {
const bannerHtml = `
<h3>${T.demoBanners.title}</h3>
<p>${T.demoBanners.intro}</p>
<a href="#" class="steamLink ${A_B_TESTING_LINK_TYPE}" target="_blank">Get the shapez.io standalone!</a>
`;
return `
const showDemoBadges = this.app.restrictionMgr.getIsStandaloneMarketingActive();
return `
<div class="topButtons">
<button class="languageChoose" data-languageicon="${this.app.settings.getLanguage()}"></button>
<button class="settingsButton"></button>
${
G_IS_STANDALONE || G_IS_DEV
? `
@ -74,17 +57,14 @@ export class MainMenuState extends GameState {
<source src="${cachebust("res/bg_render.webm")}" type="video/webm">
</video>
<div class="logo">
<img src="${cachebust("res/logo.png")}" alt="shapez.io Logo">
<span class="updateLabel">Wires update!</span>
</div>
<div class="mainWrapper ${IS_DEMO ? "demo" : "noDemo"}">
<div class="mainWrapper ${showDemoBadges ? "demo" : "noDemo"}">
<div class="sideContainer">
${IS_DEMO ? `<div class="standaloneBanner">${bannerHtml}</div>` : ""}
${showDemoBadges ? `<div class="standaloneBanner">${bannerHtml}</div>` : ""}
</div>
<div class="mainContainer">
@ -95,12 +75,9 @@ export class MainMenuState extends GameState {
}
<div class="buttons"></div>
</div>
</div>
<div class="footer">
<a class="githubLink boxLink" target="_blank">
${T.mainMenu.openSourceHint}
<span class="thirdpartyLogo githubLogo"></span>
@ -123,32 +100,29 @@ export class MainMenuState extends GameState {
"<author-link>",
'<a class="producerLink" target="_blank">Tobias Springer</a>'
)}</div>
</div>
`;
}
/**
* Asks the user to import a savegame
*/
requestImportSavegame() {
if (
IS_DEMO &&
this.app.savegameMgr.getSavegamesMetaData().length > 0 &&
!this.app.platformWrapper.getHasUnlimitedSavegames()
!this.app.restrictionMgr.getHasUnlimitedSavegames()
) {
this.app.analytics.trackUiClick("importgame_slot_limit_show");
this.showSavegameSlotLimit();
return;
}
var input = document.createElement("input");
input.type = "file";
input.accept = ".bin";
input.onchange = e => {
const file = input.files[0];
// Create a 'fake' file-input to accept savegames
startFileChoose(".bin").then(file => {
if (file) {
const closeLoader = this.dialogs.showLoadingDialog();
waitNextFrame().then(() => {
this.app.analytics.trackUiClick("import_savegame");
const closeLoader = this.dialogs.showLoadingDialog();
const reader = new FileReader();
reader.addEventListener("load", event => {
const contents = event.target.result;
@ -194,8 +168,7 @@ export class MainMenuState extends GameState {
reader.readAsText(file, "utf-8");
});
}
};
input.click();
});
}
onBackButton() {
@ -347,20 +320,23 @@ export class MainMenuState extends GameState {
});
optionSelected.add(value => {
this.app.settings.updateLanguage(value);
if (setting.restartRequired) {
if (this.app.platformWrapper.getSupportsRestart()) {
this.app.platformWrapper.performRestart();
} else {
this.dialogs.showInfo(T.dialogs.restartRequired.title, T.dialogs.restartRequired.text, [
"ok:good",
]);
this.app.settings.updateLanguage(value).then(() => {
if (setting.restartRequired) {
if (this.app.platformWrapper.getSupportsRestart()) {
this.app.platformWrapper.performRestart();
} else {
this.dialogs.showInfo(
T.dialogs.restartRequired.title,
T.dialogs.restartRequired.text,
["ok:good"]
);
}
}
}
if (setting.changeCb) {
setting.changeCb(this.app, value);
}
if (setting.changeCb) {
setting.changeCb(this.app, value);
}
});
// Update current icon
this.htmlElement.querySelector("button.languageChoose").setAttribute("data-languageIcon", value);
@ -557,9 +533,8 @@ export class MainMenuState extends GameState {
onPlayButtonClicked() {
if (
IS_DEMO &&
this.app.savegameMgr.getSavegamesMetaData().length > 0 &&
!this.app.platformWrapper.getHasUnlimitedSavegames()
!this.app.restrictionMgr.getHasUnlimitedSavegames()
) {
this.app.analytics.trackUiClick("startgame_slot_limit_show");
this.showSavegameSlotLimit();

View File

@ -145,6 +145,11 @@ export class PreloadState extends GameState {
this.app.backgroundResourceLoader.startLoading();
})
.then(() => this.setStatus("Initializing restrictions"))
.then(() => {
return this.app.restrictionMgr.initialize();
})
.then(() => this.setStatus("Initializing savegame"))
.then(() => {
return this.app.savegameMgr.initialize().catch(err => {

View File

@ -1,169 +1,169 @@
import { TextualGameState } from "../core/textual_game_state";
import { formatSecondsToTimeAgo } from "../core/utils";
import { allApplicationSettings, enumCategories } from "../profile/application_settings";
import { T } from "../translations";
export class SettingsState extends TextualGameState {
constructor() {
super("SettingsState");
}
getStateHeaderTitle() {
return T.settings.title;
}
getMainContentHTML() {
return `
<div class="sidebar">
${this.getCategoryButtonsHtml()}
${
this.app.platformWrapper.getSupportsKeyboard()
? `
<button class="styledButton categoryButton editKeybindings">
${T.keybindings.title}
</button>`
: ""
}
<div class="other">
<button class="styledButton about">${T.about.title}</button>
<div class="versionbar">
<div class="buildVersion">${T.global.loading} ...</div>
</div>
</div>
</div>
<div class="categoryContainer">
${this.getSettingsHtml()}
</div>
`;
}
getCategoryButtonsHtml() {
return Object.keys(enumCategories)
.map(key => enumCategories[key])
.map(
category =>
`
<button class="styledButton categoryButton" data-category-btn="${category}">
${T.settings.categories[category]}
</button>
`
)
.join("");
}
getSettingsHtml() {
const categoriesHTML = {};
Object.keys(enumCategories).forEach(key => {
const catName = enumCategories[key];
categoriesHTML[catName] = `<div class="category" data-category="${catName}">`;
});
for (let i = 0; i < allApplicationSettings.length; ++i) {
const setting = allApplicationSettings[i];
categoriesHTML[setting.categoryId] += setting.getHtml();
}
return Object.keys(categoriesHTML)
.map(k => categoriesHTML[k] + "</div>")
.join("");
}
renderBuildText() {
const labelVersion = this.htmlElement.querySelector(".buildVersion");
const lastBuildMs = new Date().getTime() - G_BUILD_TIME;
const lastBuildText = formatSecondsToTimeAgo(lastBuildMs / 1000.0);
const version = T.settings.versionBadges[G_APP_ENVIRONMENT];
labelVersion.innerHTML = `
<span class='version'>
${G_BUILD_VERSION} @ ${version} @ ${G_BUILD_COMMIT_HASH}
</span>
<span class='buildTime'>
${T.settings.buildDate.replace("<at-date>", lastBuildText)}<br />
</span>`;
}
onEnter(payload) {
this.renderBuildText();
this.trackClicks(this.htmlElement.querySelector(".about"), this.onAboutClicked, {
preventDefault: false,
});
const keybindingsButton = this.htmlElement.querySelector(".editKeybindings");
if (keybindingsButton) {
this.trackClicks(keybindingsButton, this.onKeybindingsClicked, { preventDefault: false });
}
this.initSettings();
this.initCategoryButtons();
this.htmlElement.querySelector(".category").classList.add("active");
this.htmlElement.querySelector(".categoryButton").classList.add("active");
}
setActiveCategory(category) {
const previousCategory = this.htmlElement.querySelector(".category.active");
const previousCategoryButton = this.htmlElement.querySelector(".categoryButton.active");
if (previousCategory.getAttribute("data-category") == category) {
return;
}
previousCategory.classList.remove("active");
previousCategoryButton.classList.remove("active");
const newCategory = this.htmlElement.querySelector("[data-category='" + category + "']");
const newCategoryButton = this.htmlElement.querySelector("[data-category-btn='" + category + "']");
newCategory.classList.add("active");
newCategoryButton.classList.add("active");
}
initSettings() {
allApplicationSettings.forEach(setting => {
/** @type {HTMLElement} */
const element = this.htmlElement.querySelector("[data-setting='" + setting.id + "']");
setting.bind(this.app, element, this.dialogs);
setting.syncValueToElement();
this.trackClicks(
element,
() => {
setting.modify();
},
{ preventDefault: false }
);
});
}
initCategoryButtons() {
Object.keys(enumCategories).forEach(key => {
const category = enumCategories[key];
const button = this.htmlElement.querySelector("[data-category-btn='" + category + "']");
this.trackClicks(
button,
() => {
this.setActiveCategory(category);
},
{ preventDefault: false }
);
});
}
onAboutClicked() {
this.moveToStateAddGoBack("AboutState");
}
onKeybindingsClicked() {
this.moveToStateAddGoBack("KeybindingsState");
}
}
import { TextualGameState } from "../core/textual_game_state";
import { formatSecondsToTimeAgo } from "../core/utils";
import { allApplicationSettings, enumCategories } from "../profile/application_settings";
import { T } from "../translations";
export class SettingsState extends TextualGameState {
constructor() {
super("SettingsState");
}
getStateHeaderTitle() {
return T.settings.title;
}
getMainContentHTML() {
return `
<div class="sidebar">
${this.getCategoryButtonsHtml()}
${
this.app.platformWrapper.getSupportsKeyboard()
? `
<button class="styledButton categoryButton editKeybindings">
${T.keybindings.title}
</button>`
: ""
}
<div class="other">
<button class="styledButton about">${T.about.title}</button>
<div class="versionbar">
<div class="buildVersion">${T.global.loading} ...</div>
</div>
</div>
</div>
<div class="categoryContainer">
${this.getSettingsHtml()}
</div>
`;
}
getCategoryButtonsHtml() {
return Object.keys(enumCategories)
.map(key => enumCategories[key])
.map(
category =>
`
<button class="styledButton categoryButton" data-category-btn="${category}">
${T.settings.categories[category]}
</button>
`
)
.join("");
}
getSettingsHtml() {
const categoriesHTML = {};
Object.keys(enumCategories).forEach(key => {
const catName = enumCategories[key];
categoriesHTML[catName] = `<div class="category" data-category="${catName}">`;
});
for (let i = 0; i < allApplicationSettings.length; ++i) {
const setting = allApplicationSettings[i];
categoriesHTML[setting.categoryId] += setting.getHtml(this.app);
}
return Object.keys(categoriesHTML)
.map(k => categoriesHTML[k] + "</div>")
.join("");
}
renderBuildText() {
const labelVersion = this.htmlElement.querySelector(".buildVersion");
const lastBuildMs = new Date().getTime() - G_BUILD_TIME;
const lastBuildText = formatSecondsToTimeAgo(lastBuildMs / 1000.0);
const version = T.settings.versionBadges[G_APP_ENVIRONMENT];
labelVersion.innerHTML = `
<span class='version'>
${G_BUILD_VERSION} @ ${version} @ ${G_BUILD_COMMIT_HASH}
</span>
<span class='buildTime'>
${T.settings.buildDate.replace("<at-date>", lastBuildText)}<br />
</span>`;
}
onEnter(payload) {
this.renderBuildText();
this.trackClicks(this.htmlElement.querySelector(".about"), this.onAboutClicked, {
preventDefault: false,
});
const keybindingsButton = this.htmlElement.querySelector(".editKeybindings");
if (keybindingsButton) {
this.trackClicks(keybindingsButton, this.onKeybindingsClicked, { preventDefault: false });
}
this.initSettings();
this.initCategoryButtons();
this.htmlElement.querySelector(".category").classList.add("active");
this.htmlElement.querySelector(".categoryButton").classList.add("active");
}
setActiveCategory(category) {
const previousCategory = this.htmlElement.querySelector(".category.active");
const previousCategoryButton = this.htmlElement.querySelector(".categoryButton.active");
if (previousCategory.getAttribute("data-category") == category) {
return;
}
previousCategory.classList.remove("active");
previousCategoryButton.classList.remove("active");
const newCategory = this.htmlElement.querySelector("[data-category='" + category + "']");
const newCategoryButton = this.htmlElement.querySelector("[data-category-btn='" + category + "']");
newCategory.classList.add("active");
newCategoryButton.classList.add("active");
}
initSettings() {
allApplicationSettings.forEach(setting => {
/** @type {HTMLElement} */
const element = this.htmlElement.querySelector("[data-setting='" + setting.id + "']");
setting.bind(this.app, element, this.dialogs);
setting.syncValueToElement();
this.trackClicks(
element,
() => {
setting.modify();
},
{ preventDefault: false }
);
});
}
initCategoryButtons() {
Object.keys(enumCategories).forEach(key => {
const category = enumCategories[key];
const button = this.htmlElement.querySelector("[data-category-btn='" + category + "']");
this.trackClicks(
button,
() => {
this.setActiveCategory(category);
},
{ preventDefault: false }
);
});
}
onAboutClicked() {
this.moveToStateAddGoBack("AboutState");
}
onKeybindingsClicked() {
this.moveToStateAddGoBack("KeybindingsState");
}
}

View File

@ -33,6 +33,7 @@ The base language is English and can be found [here](base-en.yaml).
- [Ukrainian](base-uk.yaml)
- [Indonesian](base-ind.yaml)
- [Serbian](base-sr.yaml)
- [Czech](base-cz.yaml)
(If you want to translate into a new language, see below!)

View File

@ -1,20 +1,17 @@
steamPage:
shortText: shapez.io is a game about building factories to automate the creation
and processing of increasingly complex shapes across an infinitely
expanding map.
shortText: لعبة شيبز (أشكال) هي لعبة تدور حول بناء مصانع وتوصيلها حتى تقوم بشكل
.آلي بصناعة أشكال مختلفة تزداد تعقيدا في خريطة لانهائية.
discordLinkShort: Official Discord
intro: >-
Shapez.io is a relaxed game in which you have to build factories for the
automated production of geometric shapes.
لعبة شيبز (أشكال) هي لعبة مريحة تقوم فيها ببناء مصانع ووتشغيلها آليا
لصناعة أشكال هندسية.
As the level increases, the shapes become more and more complex, and you have to spread out on the infinite map.
مع التقدم في المستوى، تزداد الأشكال تعقيداً، فيتوجب عليك التوسع في الخريطة اللانهائية، وذلك ليس كافياً للتقدم في مستوى اللعبة حيث عليك صناعة المزيد بأضعاف مضاعفة لتلبية الطلب، الشيء الوحيد الذي يمكنه مساعدتك هو التوسع.
And as if that wasn't enough, you also have to produce exponentially more to satisfy the demands - the only thing that helps is scaling!
بينما في البداية تقوم بصناعة أشكال مختلفة، تتطلب منك المراحل المتقدمة تلوين هذه الأشكال، حيث يتوجب عليك استخراج وخلط الألوان.
While you only process shapes at the beginning, you have to color them later - for this you have to extract and mix colors!
Buying the game on Steam gives you access to the full version, but you can also play a demo on shapez.io first and decide later!
title_advantages: Standalone Advantages
عند شراءك اللعبة على ستيم (Steam) تحصل على الإصدار الكامل للعبة، ولكن يمكن أيضاً لعبة نسخة تجريبية على موقع shapez.io ثم يمكنك القرار لاحقا
title_advantages: ميزات نسخة الحاسوب
advantages:
- <b>12 New Level</b> for a total of 26 levels
- <b>18 New Buildings</b> for a fully automated factory!
@ -199,11 +196,6 @@ dialogs:
renameSavegame:
title: Rename Savegame
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:
keybindingsOverlay:
moveMap: Move
@ -259,27 +251,6 @@ ingame:
title: Upgrades
buttonUnlock: Upgrade
tier: Tier <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>)
statistics:
title: Statistics
@ -303,10 +274,6 @@ ingame:
playtime: Playtime
buildingsPlaced: Buildings
beltsPlaced: Belts
buttons:
continue: Continue
settings: Settings
menu: Return to menu
tutorialHints:
title: Need help?
showHint: Show hint
@ -676,7 +643,7 @@ storyRewards:
title: Balancer
desc: The multifunctional <strong>balancer</strong> has been unlocked - It can
be used to build bigger factories by <strong>splitting and merging
items</strong> onto multiple belts!<br><br>
items</strong> onto multiple belts!
reward_merger:
title: Compact Merger
desc: You have unlocked a <strong>merger</strong> variant of the
@ -893,6 +860,14 @@ settings:
title: Enable Mouse Pan
description: Allows to move the map by moving the cursor to the edges of the
screen. The speed depends on the Movement Speed setting.
zoomToCursor:
title: Zoom towards Cursor
description: If activated the zoom will happen in the direction of your mouse
position, otherwise in the middle of the screen.
mapResourcesScale:
title: Map Resources Size
description: Controls the size of the shapes on the map overview (when zooming
out).
rangeSliderPercentage: <amount> %
keybindings:
title: Keybindings

View File

@ -94,7 +94,6 @@ mainMenu:
helpTranslate: Ajuda a traduir-lo!
madeBy: Creat per <author-link>
browserWarning: >-
Disculpa, però el joc funcionarà lent al teu navegador! Aconsegueix el joc complet o descarrega't chrome per una millor experiència.
savegameLevel: Nivell <x>
savegameLevelUnknown: Nivell desconegut
@ -205,11 +204,6 @@ dialogs:
renameSavegame:
title: Canviar el nom.
desc: Canviar el nom de la partida guardada.
entityWarning:
title: Alerta de rendiment
desc: Has col·locat molts edificis, això és una alerta amistosa de que el joc no
pot suportar edificis infinits, així que intenta mantenir els teus
dissenys minimament compactes!
ingame:
keybindingsOverlay:
moveMap: Moure
@ -265,27 +259,6 @@ ingame:
title: Millores
buttonUnlock: Millorar
tier: Nivell <x>
tierLabels:
- I
- II
- III
- IV
- V
- VI
- VII
- VIII
- IX
- X
- XI
- XII
- XIII
- XIV
- XV
- XVI
- XVII
- XVIII
- XIX
- XX
maximumLevel: NIVELL MÀXIM (Velocitat x<currentMult>)
statistics:
title: Estadístiques
@ -310,10 +283,6 @@ ingame:
playtime: Temps de joc
buildingsPlaced: Edificis
beltsPlaced: Cintes transportadores
buttons:
continue: Continuar
settings: Configuració
menu: Tornar al menú
tutorialHints:
title: Necessites ajuda?
showHint: Mostrar pista
@ -692,9 +661,9 @@ storyRewards:
completa - fora del web!
reward_balancer:
title: Equilibrador
desc: Has desbloquejat el multifuncional <strong>equilibrador</strong>! Pot
ésser emprat per construir fàbriques més grans <strong>dividint i
fusionant ítems</strong> a múltiples cintes!<br><br>
desc: The multifunctional <strong>balancer</strong> has been unlocked - It can
be used to build bigger factories by <strong>splitting and merging
items</strong> onto multiple belts!
reward_merger:
title: Fusionador Compacte
desc: Has desbloquejat una variant <strong>fusionadora</strong> de
@ -918,6 +887,14 @@ settings:
description: Permet moure el mapa quan mous el cursor a les vores de la
pantalla. La velocitat de moviment depèn de la configuració de
Velocitat de Moviment.
zoomToCursor:
title: Zoom towards Cursor
description: If activated the zoom will happen in the direction of your mouse
position, otherwise in the middle of the screen.
mapResourcesScale:
title: Map Resources Size
description: Controls the size of the shapes on the map overview (when zooming
out).
rangeSliderPercentage: <amount> %
keybindings:
title: Combinacions de tecles

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