1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-16 11:41:50 +00:00
This commit is contained in:
Daan Breur 2021-11-27 14:37:32 +00:00 committed by GitHub
commit 35aba7606a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1363 additions and 1071 deletions

View File

@ -214,6 +214,35 @@
} }
} }
} }
.inline {
display: flex;
flex-direction: row;
align-items: center;
> * {
@include S(margin-right, 7.5px);
}
}
.detailsFormElem {
> .object {
pointer-events: all;
> summary {
transition: opacity 0.1s ease-in-out;
cursor: pointer;
pointer-events: all;
&:hover {
opacity: 0.8;
}
}
> div {
@include S(margin-left, 4px);
cursor: pointer;
}
}
}
} }
> .buttons { > .buttons {

View File

@ -47,13 +47,67 @@ export class FormElement {
} }
} }
export class FormElementDetails extends FormElement {
/**
*
* @param {object} param0
* @param {string} param0.id
* @param {string} param0.label
* @param {Array<FormElement>} param0.formElements
*/
constructor({ id, label, formElements }) {
super(id, label);
this.formElements = formElements;
this.element = null;
}
getHtml() {
return `<div class="formElement detailsFormElem"><details class='object'>
${this.label ? `<summary>${this.label}</summary>` : ""}
<div class="content">
${this.formElements.map(e => e.getHtml()).join("")}
</div></details></div>`;
}
bindEvents(parent, clickTrackers) {
this.element = this.getFormElement(parent);
for (let i = 0; i < this.formElements.length; ++i) {
const elem = this.formElements[i];
elem.bindEvents(parent, clickTrackers);
elem.valueChosen.add(this.valueChosen.dispatch, this.valueChosen);
}
}
getValue() {
let formElementValues = {};
for (let i = 0; i < this.formElements.length; ++i) {
const elem = this.formElements[i];
formElementValues[elem.id] = elem.getValue();
}
return formElementValues;
}
focus() {}
}
export class FormElementInput extends FormElement { export class FormElementInput extends FormElement {
constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) { constructor({
id,
label = null,
placeholder,
defaultValue = "",
inputType = "text",
validator = null,
inline = false,
}) {
super(id, label); super(id, label);
this.placeholder = placeholder; this.placeholder = placeholder;
this.defaultValue = defaultValue; this.defaultValue = defaultValue;
this.inputType = inputType; this.inputType = inputType;
this.validator = validator; this.validator = validator;
this.inline = inline;
this.element = null; this.element = null;
} }
@ -83,7 +137,7 @@ export class FormElementInput extends FormElement {
} }
return ` return `
<div class="formElement input"> <div class="formElement input ${this.inline ? "inline" : ""}">
${this.label ? `<label>${this.label}</label>` : ""} ${this.label ? `<label>${this.label}</label>` : ""}
<input <input
type="${inputType}" type="${inputType}"
@ -143,21 +197,22 @@ export class FormElementInput extends FormElement {
} }
export class FormElementCheckbox extends FormElement { export class FormElementCheckbox extends FormElement {
constructor({ id, label, defaultValue = true }) { constructor({ id, label, defaultValue = true, inline = false }) {
super(id, label); super(id, label);
this.defaultValue = defaultValue; this.defaultValue = defaultValue;
this.value = this.defaultValue; this.value = this.defaultValue;
this.inline = inline;
this.element = null; this.element = null;
} }
getHtml() { getHtml() {
return ` return `
<div class="formElement checkBoxFormElem"> <div class="formElement checkBoxFormElem ${this.inline ? "inline" : ""}">
${this.label ? `<label>${this.label}</label>` : ""} ${this.label ? `<label>${this.label}</label>` : ""}
<div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'> <div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'>
<span class="knob"></span > <span class="knob"></span>
</div > </div>
</div> </div>
`; `;
} }
@ -181,7 +236,7 @@ export class FormElementCheckbox extends FormElement {
this.element.classList.toggle("checked", this.value); this.element.classList.toggle("checked", this.value);
} }
focus(parent) {} focus() {}
} }
export class FormElementItemChooser extends FormElement { export class FormElementItemChooser extends FormElement {

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,9 @@ export class BaseMap extends BasicSerializableObject {
static getSchema() { static getSchema() {
return { return {
seed: types.uint, seed: types.uint,
allowNonPrimaryColors: types.bool,
fullShapePercentage: types.uint,
wierdShapePercentage: types.uint,
}; };
} }
@ -27,6 +30,9 @@ export class BaseMap extends BasicSerializableObject {
this.root = root; this.root = root;
this.seed = 0; this.seed = 0;
this.allowNonPrimaryColors = false;
this.fullShapePercentage = 0;
this.wierdShapePercentage = 0;
/** /**
* Mapping of 'X|Y' to chunk * Mapping of 'X|Y' to chunk

View File

@ -168,6 +168,11 @@ export class MapChunk {
let availableColors = [enumColors.red, enumColors.green]; let availableColors = [enumColors.red, enumColors.green];
if (distanceToOriginInChunks > 2) { if (distanceToOriginInChunks > 2) {
availableColors.push(enumColors.blue); availableColors.push(enumColors.blue);
if (this.root.map.allowNonPrimaryColors) {
availableColors.push(enumColors.yellow);
availableColors.push(enumColors.purple);
availableColors.push(enumColors.cyan);
}
} }
this.internalGeneratePatch(rng, colorPatchSize, COLOR_ITEM_SINGLETONS[rng.choice(availableColors)]); this.internalGeneratePatch(rng, colorPatchSize, COLOR_ITEM_SINGLETONS[rng.choice(availableColors)]);
} }
@ -198,7 +203,19 @@ export class MapChunk {
weights[enumSubShape.windmill] = 0; weights[enumSubShape.windmill] = 0;
} }
if (distanceToOriginInChunks < 10) { if (rng.nextRange(0, 100) <= this.root.map.fullShapePercentage) {
// Spawn full shape based on percentage.
const subShape = this.internalGenerateRandomSubShape(rng, weights);
subShapes = [subShape, subShape, subShape, subShape];
} else if (rng.nextRange(0, 100) <= this.root.map.wierdShapePercentage) {
// Spawn wierd shape based on percentage.
subShapes = [
this.internalGenerateRandomSubShape(rng, weights),
this.internalGenerateRandomSubShape(rng, weights),
this.internalGenerateRandomSubShape(rng, weights),
this.internalGenerateRandomSubShape(rng, weights),
];
} else if (distanceToOriginInChunks < 10) {
// Initial chunk patches always have the same shape // Initial chunk patches always have the same shape
const subShape = this.internalGenerateRandomSubShape(rng, weights); const subShape = this.internalGenerateRandomSubShape(rng, weights);
subShapes = [subShape, subShape, subShape, subShape]; subShapes = [subShape, subShape, subShape, subShape];

View File

@ -28,7 +28,10 @@
"shape": "#5d5f6a", "shape": "#5d5f6a",
"red": "#854f56", "red": "#854f56",
"green": "#667964", "green": "#667964",
"blue": "#5e7ca4" "blue": "#5e7ca4",
"purple": "#8776bc",
"yellow": "#cab57d",
"cyan": "#00b5b8"
}, },
"chunkOverview": { "chunkOverview": {
"empty": "#444856", "empty": "#444856",

View File

@ -28,7 +28,10 @@
"shape": "#eaebec", "shape": "#eaebec",
"red": "#ffbfc1", "red": "#ffbfc1",
"green": "#cbffc4", "green": "#cbffc4",
"blue": "#bfdaff" "blue": "#bfdaff",
"purple": "#ecb3fc",
"yellow": "#fcf99c",
"cyan": "#85fdff"
}, },
"chunkOverview": { "chunkOverview": {

View File

@ -14,6 +14,7 @@ import { SavegameInterface_V1006 } from "./schemas/1006";
import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1007 } from "./schemas/1007";
import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1008 } from "./schemas/1008";
import { SavegameInterface_V1009 } from "./schemas/1009"; import { SavegameInterface_V1009 } from "./schemas/1009";
import { SavegameInterface_V1010 } from "./schemas/1010";
const logger = createLogger("savegame"); const logger = createLogger("savegame");
@ -54,7 +55,7 @@ export class Savegame extends ReadWriteProxy {
* @returns {number} * @returns {number}
*/ */
static getCurrentVersion() { static getCurrentVersion() {
return 1009; return 1010;
} }
/** /**
@ -160,6 +161,11 @@ export class Savegame extends ReadWriteProxy {
data.version = 1009; data.version = 1009;
} }
if (data.version === 1009) {
SavegameInterface_V1010.migrate1009to1010(data);
data.version = 1010;
}
return ExplainedResult.good(); return ExplainedResult.good();
} }

View File

@ -10,6 +10,7 @@ import { SavegameInterface_V1006 } from "./schemas/1006";
import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1007 } from "./schemas/1007";
import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1008 } from "./schemas/1008";
import { SavegameInterface_V1009 } from "./schemas/1009"; import { SavegameInterface_V1009 } from "./schemas/1009";
import { SavegameInterface_V1010 } from "./schemas/1010";
/** @type {Object.<number, typeof BaseSavegameInterface>} */ /** @type {Object.<number, typeof BaseSavegameInterface>} */
export const savegameInterfaces = { export const savegameInterfaces = {
@ -23,6 +24,7 @@ export const savegameInterfaces = {
1007: SavegameInterface_V1007, 1007: SavegameInterface_V1007,
1008: SavegameInterface_V1008, 1008: SavegameInterface_V1008,
1009: SavegameInterface_V1009, 1009: SavegameInterface_V1009,
1010: SavegameInterface_V1010,
}; };
const logger = createLogger("savegame_interface_registry"); const logger = createLogger("savegame_interface_registry");

View File

@ -0,0 +1,38 @@
import { createLogger } from "../../core/logging.js";
import { SavegameInterface_V1009 } from "./1009.js";
const schema = require("./1010.json");
const logger = createLogger("savegame_interface/1010");
export class SavegameInterface_V1010 extends SavegameInterface_V1009 {
getVersion() {
return 1010;
}
getSchemaUncached() {
return schema;
}
/**
* @param {import("../savegame_typedefs.js").SavegameData} data
*/
static migrate1009to1010(data) {
logger.log("Migrating 1009 to 1010");
const dump = data.dump;
if (!dump) {
return true;
}
if (!dump.map.hasOwnProperty("allowNonPrimaryColors")) {
dump.map.allowNonPrimaryColors = false;
}
if (!dump.map.hasOwnProperty("fullShapePercentage")) {
dump.map.fullShapePercentage = 0;
}
if (!dump.map.hasOwnProperty("wierdShapePercentage")) {
dump.map.wierdShapePercentage = 0;
}
}
}

View File

@ -0,0 +1,5 @@
{
"type": "object",
"required": [],
"additionalProperties": true
}

View File

@ -1,480 +1,513 @@
import { APPLICATION_ERROR_OCCURED } from "../core/error_handler"; import { APPLICATION_ERROR_OCCURED } from "../core/error_handler";
import { GameState } from "../core/game_state"; import { GameState } from "../core/game_state";
import { logSection, createLogger } from "../core/logging"; import { logSection, createLogger } from "../core/logging";
import { waitNextFrame } from "../core/utils"; import { waitNextFrame } from "../core/utils";
import { globalConfig } from "../core/config"; import { globalConfig } from "../core/config";
import { GameLoadingOverlay } from "../game/game_loading_overlay"; import { GameLoadingOverlay } from "../game/game_loading_overlay";
import { KeyActionMapper } from "../game/key_action_mapper"; import { KeyActionMapper } from "../game/key_action_mapper";
import { Savegame } from "../savegame/savegame"; import { Savegame } from "../savegame/savegame";
import { GameCore } from "../game/core"; import { GameCore } from "../game/core";
import { MUSIC } from "../platform/sound"; import { MUSIC } from "../platform/sound";
import { enumGameModeIds } from "../game/game_mode"; import { enumGameModeIds } from "../game/game_mode";
const logger = createLogger("state/ingame"); const logger = createLogger("state/ingame");
// Different sub-states // Different sub-states
const stages = { const stages = {
s3_createCore: "🌈 3: Create core", s3_createCore: "🌈 3: Create core",
s4_A_initEmptyGame: "🌈 4/A: Init empty game", s4_A_initEmptyGame: "🌈 4/A: Init empty game",
s4_B_resumeGame: "🌈 4/B: Resume game", s4_B_resumeGame: "🌈 4/B: Resume game",
s5_firstUpdate: "🌈 5: First game update", s5_firstUpdate: "🌈 5: First game update",
s6_postLoadHook: "🌈 6: Post load hook", s6_postLoadHook: "🌈 6: Post load hook",
s7_warmup: "🌈 7: Warmup", s7_warmup: "🌈 7: Warmup",
s10_gameRunning: "🌈 10: Game finally running", s10_gameRunning: "🌈 10: Game finally running",
leaving: "🌈 Saving, then leaving the game", leaving: "🌈 Saving, then leaving the game",
destroyed: "🌈 DESTROYED: Core is empty and waits for state leave", destroyed: "🌈 DESTROYED: Core is empty and waits for state leave",
initFailed: "🌈 ERROR: Initialization failed!", initFailed: "🌈 ERROR: Initialization failed!",
}; };
export const gameCreationAction = { export const gameCreationAction = {
new: "new-game", new: "new-game",
resume: "resume-game", resume: "resume-game",
}; };
// Typehints // Typehints
export class GameCreationPayload { export class GameCreationPayload {
constructor() { constructor() {
/** @type {boolean|undefined} */ /** @type {boolean|undefined} */
this.fastEnter; this.fastEnter;
/** @type {string} */ /** @type {string} */
this.gameModeId; this.gameModeId;
/** @type {Savegame} */ /** @type {Savegame} */
this.savegame; this.savegame;
/** @type {object|undefined} */ /** @type {object|undefined} */
this.gameModeParameters; this.gameModeParameters;
}
} /** @type {number} */
this.seed;
export class InGameState extends GameState {
constructor() { /** @type {boolean} */
super("InGameState"); this.allowNonPrimaryColors;
/** @type {GameCreationPayload} */ /** @type {number} */
this.creationPayload = null; this.fullShapePercentage;
// Stores current stage /** @type {number} */
this.stage = ""; this.wierdShapePercentage;
}
/** @type {GameCore} */ }
this.core = null;
export class InGameState extends GameState {
/** @type {KeyActionMapper} */ constructor() {
this.keyActionMapper = null; super("InGameState");
/** @type {GameLoadingOverlay} */ /** @type {GameCreationPayload} */
this.loadingOverlay = null; this.creationPayload = null;
/** @type {Savegame} */ // Stores current stage
this.savegame = null; this.stage = "";
this.boundInputFilter = this.filterInput.bind(this); /** @type {GameCore} */
this.core = null;
/**
* Whether we are currently saving the game /** @type {KeyActionMapper} */
* @TODO: This doesn't realy fit here this.keyActionMapper = null;
*/
this.currentSavePromise = null; /** @type {GameLoadingOverlay} */
} this.loadingOverlay = null;
/** /** @type {Savegame} */
* Switches the game into another sub-state this.savegame = null;
* @param {string} stage
*/ /** @type {number} */
switchStage(stage) { this.seed = null;
assert(stage, "Got empty stage");
if (stage !== this.stage) { /** @type {boolean} */
this.stage = stage; this.allowNonPrimaryColors = null;
logger.log(this.stage);
return true; /** @type {number} */
} else { this.fullShapePercentage = null;
// log(this, "Re entering", stage);
return false; /** @type {number} */
} this.wierdShapePercentage = null;
}
this.boundInputFilter = this.filterInput.bind(this);
// GameState implementation
getInnerHTML() { /**
return ""; * Whether we are currently saving the game
} * @TODO: This doesn't realy fit here
*/
getThemeMusic() { this.currentSavePromise = null;
if (this.creationPayload.gameModeId && this.creationPayload.gameModeId.includes("puzzle")) { }
return MUSIC.puzzle;
} /**
return MUSIC.theme; * Switches the game into another sub-state
} * @param {string} stage
*/
onBeforeExit() { switchStage(stage) {
// logger.log("Saving before quitting"); assert(stage, "Got empty stage");
// return this.doSave().then(() => { if (stage !== this.stage) {
// logger.log(this, "Successfully saved"); this.stage = stage;
// // this.stageDestroyed(); logger.log(this.stage);
// }); return true;
} } else {
// log(this, "Re entering", stage);
onAppPause() { return false;
// if (this.stage === stages.s10_gameRunning) { }
// logger.log("Saving because app got paused"); }
// this.doSave();
// } // GameState implementation
} getInnerHTML() {
return "";
getHasFadeIn() { }
return false;
} getThemeMusic() {
if (this.creationPayload.gameModeId && this.creationPayload.gameModeId.includes("puzzle")) {
getPauseOnFocusLost() { return MUSIC.puzzle;
return false; }
} return MUSIC.theme;
}
getHasUnloadConfirmation() {
return true; onBeforeExit() {
} // logger.log("Saving before quitting");
// return this.doSave().then(() => {
onLeave() { // logger.log(this, "Successfully saved");
if (this.core) { // // this.stageDestroyed();
this.stageDestroyed(); // });
} }
this.app.inputMgr.dismountFilter(this.boundInputFilter);
} onAppPause() {
// if (this.stage === stages.s10_gameRunning) {
onResized(w, h) { // logger.log("Saving because app got paused");
super.onResized(w, h); // this.doSave();
if (this.stage === stages.s10_gameRunning) { // }
this.core.resize(w, h); }
}
} getHasFadeIn() {
return false;
// ---- End of GameState implementation }
/** getPauseOnFocusLost() {
* Goes back to the menu state return false;
*/ }
goBackToMenu() {
if ([enumGameModeIds.puzzleEdit, enumGameModeIds.puzzlePlay].includes(this.gameModeId)) { getHasUnloadConfirmation() {
this.saveThenGoToState("PuzzleMenuState"); return true;
} else { }
this.saveThenGoToState("MainMenuState");
} onLeave() {
} if (this.core) {
this.stageDestroyed();
/** }
* Goes back to the settings state this.app.inputMgr.dismountFilter(this.boundInputFilter);
*/ }
goToSettings() {
this.saveThenGoToState("SettingsState", { onResized(w, h) {
backToStateId: this.key, super.onResized(w, h);
backToStatePayload: this.creationPayload, if (this.stage === stages.s10_gameRunning) {
}); this.core.resize(w, h);
} }
}
/**
* Goes back to the settings state // ---- End of GameState implementation
*/
goToKeybindings() { /**
this.saveThenGoToState("KeybindingsState", { * Goes back to the menu state
backToStateId: this.key, */
backToStatePayload: this.creationPayload, goBackToMenu() {
}); if ([enumGameModeIds.puzzleEdit, enumGameModeIds.puzzlePlay].includes(this.gameModeId)) {
} this.saveThenGoToState("PuzzleMenuState");
} else {
/** this.saveThenGoToState("MainMenuState");
* Moves to a state outside of the game }
* @param {string} stateId }
* @param {any=} payload
*/ /**
saveThenGoToState(stateId, payload) { * Goes back to the settings state
if (this.stage === stages.leaving || this.stage === stages.destroyed) { */
logger.warn( goToSettings() {
"Tried to leave game twice or during destroy:", this.saveThenGoToState("SettingsState", {
this.stage, backToStateId: this.key,
"(attempted to move to", backToStatePayload: this.creationPayload,
stateId, });
")" }
);
return; /**
} * Goes back to the settings state
this.stageLeavingGame(); */
this.doSave().then(() => { goToKeybindings() {
this.stageDestroyed(); this.saveThenGoToState("KeybindingsState", {
this.moveToState(stateId, payload); backToStateId: this.key,
}); backToStatePayload: this.creationPayload,
} });
}
onBackButton() {
// do nothing /**
} * Moves to a state outside of the game
* @param {string} stateId
/** * @param {any=} payload
* Called when the game somehow failed to initialize. Resets everything to basic state and */
* then goes to the main menu, showing the error saveThenGoToState(stateId, payload) {
* @param {string} err if (this.stage === stages.leaving || this.stage === stages.destroyed) {
*/ logger.warn(
onInitializationFailure(err) { "Tried to leave game twice or during destroy:",
if (this.switchStage(stages.initFailed)) { this.stage,
logger.error("Init failure:", err); "(attempted to move to",
this.stageDestroyed(); stateId,
this.moveToState("MainMenuState", { loadError: err }); ")"
} );
} return;
}
// STAGES this.stageLeavingGame();
this.doSave().then(() => {
/** this.stageDestroyed();
* Creates the game core instance, and thus the root this.moveToState(stateId, payload);
*/ });
stage3CreateCore() { }
if (this.switchStage(stages.s3_createCore)) {
logger.log("Creating new game core"); onBackButton() {
this.core = new GameCore(this.app); // do nothing
}
this.core.initializeRoot(this, this.savegame, this.gameModeId);
/**
if (this.savegame.hasGameDump()) { * Called when the game somehow failed to initialize. Resets everything to basic state and
this.stage4bResumeGame(); * then goes to the main menu, showing the error
} else { * @param {string} err
this.app.gameAnalytics.handleGameStarted(); */
this.stage4aInitEmptyGame(); onInitializationFailure(err) {
} if (this.switchStage(stages.initFailed)) {
} logger.error("Init failure:", err);
} this.stageDestroyed();
this.moveToState("MainMenuState", { loadError: err });
/** }
* Initializes a new empty game }
*/
stage4aInitEmptyGame() { // STAGES
if (this.switchStage(stages.s4_A_initEmptyGame)) {
this.core.initNewGame(); /**
this.stage5FirstUpdate(); * Creates the game core instance, and thus the root
} */
} stage3CreateCore() {
if (this.switchStage(stages.s3_createCore)) {
/** logger.log("Creating new game core");
* Resumes an existing game this.core = new GameCore(this.app);
*/
stage4bResumeGame() { this.core.initializeRoot(this, this.savegame, this.gameModeId);
if (this.switchStage(stages.s4_B_resumeGame)) {
if (!this.core.initExistingGame()) { if (this.savegame.hasGameDump()) {
this.onInitializationFailure("Savegame is corrupt and can not be restored."); this.stage4bResumeGame();
return; } else {
} this.app.gameAnalytics.handleGameStarted();
this.app.gameAnalytics.handleGameResumed(); this.stage4aInitEmptyGame();
this.stage5FirstUpdate(); }
} }
} }
/** /**
* Performs the first game update on the game which initializes most caches * Initializes a new empty game
*/ */
stage5FirstUpdate() { stage4aInitEmptyGame() {
if (this.switchStage(stages.s5_firstUpdate)) { if (this.switchStage(stages.s4_A_initEmptyGame)) {
this.core.root.logicInitialized = true; this.core.initNewGame({
this.core.updateLogic(); seed: this.seed,
this.stage6PostLoadHook(); allowNonPrimaryColors: this.allowNonPrimaryColors,
} fullShapePercentage: this.fullShapePercentage,
} wierdShapePercentage: this.wierdShapePercentage,
});
/** this.stage5FirstUpdate();
* Call the post load hook, this means that we have loaded the game, and all systems }
* can operate and start to work now. }
*/
stage6PostLoadHook() { /**
if (this.switchStage(stages.s6_postLoadHook)) { * Resumes an existing game
logger.log("Post load hook"); */
this.core.postLoadHook(); stage4bResumeGame() {
this.stage7Warmup(); if (this.switchStage(stages.s4_B_resumeGame)) {
} if (!this.core.initExistingGame()) {
} this.onInitializationFailure("Savegame is corrupt and can not be restored.");
return;
/** }
* This makes the game idle and draw for a while, because we run most code this way this.app.gameAnalytics.handleGameResumed();
* the V8 engine can already start to optimize it. Also this makes sure the resources this.stage5FirstUpdate();
* are in the VRAM and we have a smooth experience once we start. }
*/ }
stage7Warmup() {
if (this.switchStage(stages.s7_warmup)) { /**
if (this.creationPayload.fastEnter) { * Performs the first game update on the game which initializes most caches
this.warmupTimeSeconds = globalConfig.warmupTimeSecondsFast; */
} else { stage5FirstUpdate() {
this.warmupTimeSeconds = globalConfig.warmupTimeSecondsRegular; if (this.switchStage(stages.s5_firstUpdate)) {
} this.core.root.logicInitialized = true;
} this.core.updateLogic();
} this.stage6PostLoadHook();
}
/** }
* The final stage where this game is running and updating regulary.
*/ /**
stage10GameRunning() { * Call the post load hook, this means that we have loaded the game, and all systems
if (this.switchStage(stages.s10_gameRunning)) { * can operate and start to work now.
this.core.root.signals.readyToRender.dispatch(); */
stage6PostLoadHook() {
logSection("GAME STARTED", "#26a69a"); if (this.switchStage(stages.s6_postLoadHook)) {
logger.log("Post load hook");
// Initial resize, might have changed during loading (this is possible) this.core.postLoadHook();
this.core.resize(this.app.screenWidth, this.app.screenHeight); this.stage7Warmup();
} }
} }
/** /**
* This stage destroys the whole game, used to cleanup * This makes the game idle and draw for a while, because we run most code this way
*/ * the V8 engine can already start to optimize it. Also this makes sure the resources
stageDestroyed() { * are in the VRAM and we have a smooth experience once we start.
if (this.switchStage(stages.destroyed)) { */
// Cleanup all api calls stage7Warmup() {
this.cancelAllAsyncOperations(); if (this.switchStage(stages.s7_warmup)) {
if (this.creationPayload.fastEnter) {
if (this.syncer) { this.warmupTimeSeconds = globalConfig.warmupTimeSecondsFast;
this.syncer.cancelSync(); } else {
this.syncer = null; this.warmupTimeSeconds = globalConfig.warmupTimeSecondsRegular;
} }
}
// Cleanup core }
if (this.core) {
this.core.destruct(); /**
this.core = null; * The final stage where this game is running and updating regulary.
} */
} stage10GameRunning() {
} if (this.switchStage(stages.s10_gameRunning)) {
this.core.root.signals.readyToRender.dispatch();
/**
* When leaving the game logSection("GAME STARTED", "#26a69a");
*/
stageLeavingGame() { // Initial resize, might have changed during loading (this is possible)
if (this.switchStage(stages.leaving)) { this.core.resize(this.app.screenWidth, this.app.screenHeight);
// ... }
} }
}
/**
// END STAGES * This stage destroys the whole game, used to cleanup
*/
/** stageDestroyed() {
* Filters the input (keybindings) if (this.switchStage(stages.destroyed)) {
*/ // Cleanup all api calls
filterInput() { this.cancelAllAsyncOperations();
return this.stage === stages.s10_gameRunning;
} if (this.syncer) {
this.syncer.cancelSync();
/** this.syncer = null;
* @param {GameCreationPayload} payload }
*/
onEnter(payload) { // Cleanup core
this.app.inputMgr.installFilter(this.boundInputFilter); if (this.core) {
this.core.destruct();
this.creationPayload = payload; this.core = null;
this.savegame = payload.savegame; }
this.gameModeId = payload.gameModeId; }
}
this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement());
this.loadingOverlay.showBasic(); /**
* When leaving the game
// Remove unneded default element */
document.body.querySelector(".modalDialogParent").remove(); stageLeavingGame() {
if (this.switchStage(stages.leaving)) {
this.asyncChannel // ...
.watch(waitNextFrame()) }
.then(() => this.stage3CreateCore()) }
.catch(ex => {
logger.error(ex); // END STAGES
throw ex;
}); /**
} * Filters the input (keybindings)
*/
/** filterInput() {
* Render callback return this.stage === stages.s10_gameRunning;
* @param {number} dt }
*/
onRender(dt) { /**
if (APPLICATION_ERROR_OCCURED) { * @param {GameCreationPayload} payload
// Application somehow crashed, do not do anything */
return; onEnter(payload) {
} this.app.inputMgr.installFilter(this.boundInputFilter);
if (this.stage === stages.s7_warmup) { this.creationPayload = payload;
this.core.draw(); this.savegame = payload.savegame;
this.warmupTimeSeconds -= dt / 1000.0; this.gameModeId = payload.gameModeId;
if (this.warmupTimeSeconds < 0) { this.seed = payload.seed;
logger.log("Warmup completed"); this.allowNonPrimaryColors = payload.allowNonPrimaryColors;
this.stage10GameRunning(); this.fullShapePercentage = payload.fullShapePercentage;
} this.wierdShapePercentage = payload.wierdShapePercentage;
}
this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement());
if (this.stage === stages.s10_gameRunning) { this.loadingOverlay.showBasic();
this.core.tick(dt);
} // Remove unneded default element
document.body.querySelector(".modalDialogParent").remove();
// If the stage is still active (This might not be the case if tick() moved us to game over)
if (this.stage === stages.s10_gameRunning) { this.asyncChannel
// Only draw if page visible .watch(waitNextFrame())
if (this.app.pageVisible) { .then(() => this.stage3CreateCore())
this.core.draw(); .catch(ex => {
} logger.error(ex);
throw ex;
this.loadingOverlay.removeIfAttached(); });
} else { }
if (!this.loadingOverlay.isAttached()) {
this.loadingOverlay.showBasic(); /**
} * Render callback
} * @param {number} dt
} */
onRender(dt) {
onBackgroundTick(dt) { if (APPLICATION_ERROR_OCCURED) {
this.onRender(dt); // Application somehow crashed, do not do anything
} return;
}
/**
* Saves the game if (this.stage === stages.s7_warmup) {
*/ this.core.draw();
this.warmupTimeSeconds -= dt / 1000.0;
doSave() { if (this.warmupTimeSeconds < 0) {
if (!this.savegame || !this.savegame.isSaveable()) { logger.log("Warmup completed");
return Promise.resolve(); this.stage10GameRunning();
} }
}
if (APPLICATION_ERROR_OCCURED) {
logger.warn("skipping save because application crashed"); if (this.stage === stages.s10_gameRunning) {
return Promise.resolve(); this.core.tick(dt);
} }
if ( // If the stage is still active (This might not be the case if tick() moved us to game over)
this.stage !== stages.s10_gameRunning && if (this.stage === stages.s10_gameRunning) {
this.stage !== stages.s7_warmup && // Only draw if page visible
this.stage !== stages.leaving if (this.app.pageVisible) {
) { this.core.draw();
logger.warn("Skipping save because game is not ready"); }
return Promise.resolve();
} this.loadingOverlay.removeIfAttached();
} else {
if (this.currentSavePromise) { if (!this.loadingOverlay.isAttached()) {
logger.warn("Skipping double save and returning same promise"); this.loadingOverlay.showBasic();
return this.currentSavePromise; }
} }
}
if (!this.core.root.gameMode.getIsSaveable()) {
return Promise.resolve(); onBackgroundTick(dt) {
} this.onRender(dt);
}
logger.log("Starting to save game ...");
this.savegame.updateData(this.core.root); /**
* Saves the game
this.currentSavePromise = this.savegame */
.writeSavegameAndMetadata()
.catch(err => { doSave() {
// Catch errors if (!this.savegame || !this.savegame.isSaveable()) {
logger.warn("Failed to save:", err); return Promise.resolve();
}) }
.then(() => {
// Clear promise if (APPLICATION_ERROR_OCCURED) {
logger.log("Saved!"); logger.warn("skipping save because application crashed");
this.core.root.signals.gameSaved.dispatch(); return Promise.resolve();
this.currentSavePromise = null; }
});
if (
return this.currentSavePromise; this.stage !== stages.s10_gameRunning &&
} this.stage !== stages.s7_warmup &&
} this.stage !== stages.leaving
) {
logger.warn("Skipping save because game is not ready");
return Promise.resolve();
}
if (this.currentSavePromise) {
logger.warn("Skipping double save and returning same promise");
return this.currentSavePromise;
}
if (!this.core.root.gameMode.getIsSaveable()) {
return Promise.resolve();
}
logger.log("Starting to save game ...");
this.savegame.updateData(this.core.root);
this.currentSavePromise = this.savegame
.writeSavegameAndMetadata()
.catch(err => {
// Catch errors
logger.warn("Failed to save:", err);
})
.then(() => {
// Clear promise
logger.log("Saved!");
this.core.root.signals.gameSaved.dispatch();
this.currentSavePromise = null;
});
return this.currentSavePromise;
}
}

View File

@ -3,7 +3,7 @@ import { cachebust } from "../core/cachebust";
import { A_B_TESTING_LINK_TYPE, globalConfig, THIRDPARTY_URLS } from "../core/config"; import { A_B_TESTING_LINK_TYPE, globalConfig, THIRDPARTY_URLS } from "../core/config";
import { GameState } from "../core/game_state"; import { GameState } from "../core/game_state";
import { DialogWithForm } from "../core/modal_dialog_elements"; import { DialogWithForm } from "../core/modal_dialog_elements";
import { FormElementInput } from "../core/modal_dialog_forms"; import { FormElementCheckbox, FormElementDetails, FormElementInput } from "../core/modal_dialog_forms";
import { ReadWriteProxy } from "../core/read_write_proxy"; import { ReadWriteProxy } from "../core/read_write_proxy";
import { import {
formatSecondsToTimeAgo, formatSecondsToTimeAgo,
@ -12,6 +12,7 @@ import {
makeButton, makeButton,
makeButtonElement, makeButtonElement,
makeDiv, makeDiv,
randomInt,
removeAllChildren, removeAllChildren,
startFileChoose, startFileChoose,
waitNextFrame, waitNextFrame,
@ -672,14 +673,96 @@ export class MainMenuState extends GameState {
return; return;
} }
this.app.analytics.trackUiClick("startgame"); const regex = /^[a-zA-Z0-9_\- ]{1,20}$/;
this.app.adProvider.showVideoAd().then(() => {
const savegame = this.app.savegameMgr.createNewSavegame();
this.moveToState("InGameState", { const nameInput = new FormElementInput({
savegame, id: "nameInput",
// @TODO: Add translation (T.dialogs.newSavegame.nameInputLabel)
label: "Name:",
placeholder: "",
defaultValue: "Unnamed",
validator: val => val.match(regex) && trim(val).length > 0,
inline: true,
});
const seedInput = new FormElementInput({
id: "seedInput",
// @TODO: Add translation (T.dialogs.newSavegame.seedInputLabel)
label: "Seed:",
placeholder: "",
defaultValue: randomInt(0, 100000).toString(),
validator: val => Number.isInteger(Number(val)) && Number(val) >= 0 && Number(val) <= 100000,
inline: true,
});
const allowColorsCheckbox = new FormElementCheckbox({
id: "allowColorsCheckbox",
// @TODO: Add translation (T.dialogs.newSavegame.allowColorsCheckboxLabel)
label: "Allow non-primarycolors: ",
defaultValue: false,
inline: true,
});
const fullShapePercentageInput = new FormElementInput({
id: "fullShapePercentageInput",
label: "fullShape %:",
placeholder: "",
defaultValue: Number(0).toString(),
validator: val => Number.isInteger(Number(val)) && Number(val) >= 0 && Number(val) <= 100,
inline: true,
});
const wierdShapePercentageInput = new FormElementInput({
id: "wierdShapePercentageInput",
label: "wierdShape %:",
placeholder: "",
defaultValue: Number(0).toString(),
validator: val => Number.isInteger(Number(val)) && Number(val) >= 0 && Number(val) <= 100,
inline: true,
});
const advancedContainer = new FormElementDetails({
id: "advancedContainer",
// @TODO Add translation (T.dialogs.newSavegame.advanced)
label: "Advanced Options",
formElements: [
seedInput,
allowColorsCheckbox,
fullShapePercentageInput,
wierdShapePercentageInput,
],
});
const dialog = new DialogWithForm({
app: this.app,
// @TODO: Add translation (T.dialogs.newSavegame.title)
title: "New Game Options",
// @TODO: Add translation (T.dialogs.newSavegame.desc)
desc: "Configure your new savegame",
formElements: [nameInput, advancedContainer],
buttons: ["ok:good:enter"],
});
this.dialogs.internalShowDialog(dialog);
dialog.buttonSignals.ok.add(() => {
this.app.analytics.trackUiClick("startgame");
this.app.adProvider.showVideoAd().then(async () => {
const savegame = this.app.savegameMgr.createNewSavegame();
const savegameMetadata = this.app.savegameMgr.getGameMetaDataByInternalId(
savegame.internalId
);
savegameMetadata.name = trim(nameInput.getValue());
await this.app.savegameMgr.writeAsync();
this.moveToState("InGameState", {
savegame,
seed: Number(seedInput.getValue()),
allowNonPrimaryColors: allowColorsCheckbox.getValue(),
fullShapePercentage: Number(fullShapePercentageInput.getValue()),
wierdShapePercentage: Number(wierdShapePercentageInput.getValue()),
});
this.app.analytics.trackUiClick("startgame_adcomplete");
}); });
this.app.analytics.trackUiClick("startgame_adcomplete");
}); });
} }