Further performance improvements, show indicator while game is saving

pull/670/head
tobspr 4 years ago
parent bba29b8a8b
commit 1ebfafd8de

@ -56,6 +56,17 @@
transform: scale(1.1, 1.1); transform: scale(1.1, 1.1);
} }
} }
&.saving {
@include InlineAnimation(0.4s ease-in-out infinite) {
50% {
opacity: 0.5;
transform: scale(0.8);
}
}
pointer-events: none;
cursor: default;
}
} }
&.settings { &.settings {

@ -1,230 +1,230 @@
import { makeOffscreenBuffer } from "./buffer_utils"; import { makeOffscreenBuffer } from "./buffer_utils";
import { AtlasSprite, BaseSprite, RegularSprite, SpriteAtlasLink } from "./sprites"; import { AtlasSprite, BaseSprite, RegularSprite, SpriteAtlasLink } from "./sprites";
import { cachebust } from "./cachebust"; import { cachebust } from "./cachebust";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
/** /**
* @typedef {import("../application").Application} Application * @typedef {import("../application").Application} Application
* @typedef {import("./atlas_definitions").AtlasDefinition} AtlasDefinition; * @typedef {import("./atlas_definitions").AtlasDefinition} AtlasDefinition;
*/ */
const logger = createLogger("loader"); const logger = createLogger("loader");
const missingSpriteIds = {}; const missingSpriteIds = {};
class LoaderImpl { class LoaderImpl {
constructor() { constructor() {
this.app = null; this.app = null;
/** @type {Map<string, BaseSprite>} */ /** @type {Map<string, BaseSprite>} */
this.sprites = new Map(); this.sprites = new Map();
this.rawImages = []; this.rawImages = [];
} }
/** /**
* @param {Application} app * @param {Application} app
*/ */
linkAppAfterBoot(app) { linkAppAfterBoot(app) {
this.app = app; this.app = app;
this.makeSpriteNotFoundCanvas(); this.makeSpriteNotFoundCanvas();
} }
/** /**
* Fetches a given sprite from the cache * Fetches a given sprite from the cache
* @param {string} key * @param {string} key
* @returns {BaseSprite} * @returns {BaseSprite}
*/ */
getSpriteInternal(key) { getSpriteInternal(key) {
const sprite = this.sprites.get(key); const sprite = this.sprites.get(key);
if (!sprite) { if (!sprite) {
if (!missingSpriteIds[key]) { if (!missingSpriteIds[key]) {
// Only show error once // Only show error once
missingSpriteIds[key] = true; missingSpriteIds[key] = true;
logger.error("Sprite '" + key + "' not found!"); logger.error("Sprite '" + key + "' not found!");
} }
return this.spriteNotFoundSprite; return this.spriteNotFoundSprite;
} }
return sprite; return sprite;
} }
/** /**
* Returns an atlas sprite from the cache * Returns an atlas sprite from the cache
* @param {string} key * @param {string} key
* @returns {AtlasSprite} * @returns {AtlasSprite}
*/ */
getSprite(key) { getSprite(key) {
const sprite = this.getSpriteInternal(key); const sprite = this.getSpriteInternal(key);
assert(sprite instanceof AtlasSprite || sprite === this.spriteNotFoundSprite, "Not an atlas sprite"); assert(sprite instanceof AtlasSprite || sprite === this.spriteNotFoundSprite, "Not an atlas sprite");
return /** @type {AtlasSprite} */ (sprite); return /** @type {AtlasSprite} */ (sprite);
} }
/** /**
* Returns a regular sprite from the cache * Returns a regular sprite from the cache
* @param {string} key * @param {string} key
* @returns {RegularSprite} * @returns {RegularSprite}
*/ */
getRegularSprite(key) { getRegularSprite(key) {
const sprite = this.getSpriteInternal(key); const sprite = this.getSpriteInternal(key);
assert( assert(
sprite instanceof RegularSprite || sprite === this.spriteNotFoundSprite, sprite instanceof RegularSprite || sprite === this.spriteNotFoundSprite,
"Not a regular sprite" "Not a regular sprite"
); );
return /** @type {RegularSprite} */ (sprite); return /** @type {RegularSprite} */ (sprite);
} }
/** /**
* *
* @param {string} key * @param {string} key
* @returns {Promise<HTMLImageElement|null>} * @returns {Promise<HTMLImageElement|null>}
*/ */
internalPreloadImage(key) { internalPreloadImage(key) {
const url = cachebust("res/" + key); const url = cachebust("res/" + key);
const image = new Image(); const image = new Image();
let triesSoFar = 0; let triesSoFar = 0;
return Promise.race([ return Promise.race([
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
setTimeout(reject, G_IS_DEV ? 500 : 10000); setTimeout(reject, G_IS_DEV ? 500 : 10000);
}), }),
new Promise(resolve => { new Promise(resolve => {
image.onload = () => { image.onload = () => {
image.onerror = null; image.onerror = null;
image.onload = null; image.onload = null;
if (typeof image.decode === "function") { if (typeof image.decode === "function") {
// SAFARI: Image.decode() fails on safari with svgs -> we dont want to fail // SAFARI: Image.decode() fails on safari with svgs -> we dont want to fail
// on that // on that
// FIREFOX: Decode never returns if the image is in cache, so call it in background // FIREFOX: Decode never returns if the image is in cache, so call it in background
image.decode().then( image.decode().then(
() => null, () => null,
() => null () => null
); );
} }
resolve(image); resolve(image);
}; };
image.onerror = reason => { image.onerror = reason => {
logger.warn("Failed to load '" + url + "':", reason); logger.warn("Failed to load '" + url + "':", reason);
if (++triesSoFar < 4) { if (++triesSoFar < 4) {
logger.log("Retrying to load image from", url); logger.log("Retrying to load image from", url);
image.src = url + "?try=" + triesSoFar; image.src = url + "?try=" + triesSoFar;
} else { } else {
logger.warn("Failed to load", url, "after", triesSoFar, "tries with reason", reason); logger.warn("Failed to load", url, "after", triesSoFar, "tries with reason", reason);
image.onerror = null; image.onerror = null;
image.onload = null; image.onload = null;
resolve(null); resolve(null);
} }
}; };
image.src = url; image.src = url;
}), }),
]); ]);
} }
/** /**
* Preloads a sprite * Preloads a sprite
* @param {string} key * @param {string} key
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
preloadCSSSprite(key) { preloadCSSSprite(key) {
return this.internalPreloadImage(key).then(image => { return this.internalPreloadImage(key).then(image => {
if (key.indexOf("game_misc") >= 0) { if (key.indexOf("game_misc") >= 0) {
// Allow access to regular sprites // Allow access to regular sprites
this.sprites.set(key, new RegularSprite(image, image.width, image.height)); this.sprites.set(key, new RegularSprite(image, image.width, image.height));
} }
this.rawImages.push(image); this.rawImages.push(image);
}); });
} }
/** /**
* Preloads an atlas * Preloads an atlas
* @param {AtlasDefinition} atlas * @param {AtlasDefinition} atlas
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
preloadAtlas(atlas) { preloadAtlas(atlas) {
return this.internalPreloadImage(atlas.getFullSourcePath()).then(image => { return this.internalPreloadImage(atlas.getFullSourcePath()).then(image => {
// @ts-ignore // @ts-ignore
image.label = atlas.sourceFileName; image.label = atlas.sourceFileName;
return this.internalParseAtlas(atlas, image); return this.internalParseAtlas(atlas, image);
}); });
} }
/** /**
* *
* @param {AtlasDefinition} atlas * @param {AtlasDefinition} atlas
* @param {HTMLImageElement} loadedImage * @param {HTMLImageElement} loadedImage
*/ */
internalParseAtlas({ meta: { scale }, sourceData }, loadedImage) { internalParseAtlas({ meta: { scale }, sourceData }, loadedImage) {
this.rawImages.push(loadedImage); this.rawImages.push(loadedImage);
for (const spriteName in sourceData) { for (const spriteName in sourceData) {
const { frame, sourceSize, spriteSourceSize } = sourceData[spriteName]; const { frame, sourceSize, spriteSourceSize } = sourceData[spriteName];
let sprite = /** @type {AtlasSprite} */ (this.sprites.get(spriteName)); let sprite = /** @type {AtlasSprite} */ (this.sprites.get(spriteName));
if (!sprite) { if (!sprite) {
sprite = new AtlasSprite(spriteName); sprite = new AtlasSprite(spriteName);
this.sprites.set(spriteName, sprite); this.sprites.set(spriteName, sprite);
} }
const link = new SpriteAtlasLink({ const link = new SpriteAtlasLink({
packedX: frame.x, packedX: frame.x,
packedY: frame.y, packedY: frame.y,
packedW: frame.w, packedW: frame.w,
packedH: frame.h, packedH: frame.h,
packOffsetX: spriteSourceSize.x, packOffsetX: spriteSourceSize.x,
packOffsetY: spriteSourceSize.y, packOffsetY: spriteSourceSize.y,
atlas: loadedImage, atlas: loadedImage,
w: sourceSize.w, w: sourceSize.w,
h: sourceSize.h, h: sourceSize.h,
}); });
sprite.linksByResolution[scale] = link; sprite.linksByResolution[scale] = link;
} }
} }
/** /**
* Makes the canvas which shows the question mark, shown when a sprite was not found * Makes the canvas which shows the question mark, shown when a sprite was not found
*/ */
makeSpriteNotFoundCanvas() { makeSpriteNotFoundCanvas() {
const dims = 128; const dims = 128;
const [canvas, context] = makeOffscreenBuffer(dims, dims, { const [canvas, context] = makeOffscreenBuffer(dims, dims, {
smooth: false, smooth: false,
label: "not-found-sprite", label: "not-found-sprite",
}); });
context.fillStyle = "#f77"; context.fillStyle = "#f77";
context.fillRect(0, 0, dims, dims); context.fillRect(0, 0, dims, dims);
context.textAlign = "center"; context.textAlign = "center";
context.textBaseline = "middle"; context.textBaseline = "middle";
context.fillStyle = "#eee"; context.fillStyle = "#eee";
context.font = "25px Arial"; context.font = "25px Arial";
context.fillText("???", dims / 2, dims / 2); context.fillText("???", dims / 2, dims / 2);
// TODO: Not sure why this is set here // TODO: Not sure why this is set here
// @ts-ignore // @ts-ignore
canvas.src = "not-found"; canvas.src = "not-found";
const sprite = new AtlasSprite("not-found"); const sprite = new AtlasSprite("not-found");
["0.1", "0.25", "0.5", "0.75", "1"].forEach(resolution => { ["0.1", "0.25", "0.5", "0.75", "1"].forEach(resolution => {
sprite.linksByResolution[resolution] = new SpriteAtlasLink({ sprite.linksByResolution[resolution] = new SpriteAtlasLink({
packedX: 0, packedX: 0,
packedY: 0, packedY: 0,
w: dims, w: dims,
h: dims, h: dims,
packOffsetX: 0, packOffsetX: 0,
packOffsetY: 0, packOffsetY: 0,
packedW: dims, packedW: dims,
packedH: dims, packedH: dims,
atlas: canvas, atlas: canvas,
}); });
}); });
this.spriteNotFoundSprite = sprite; this.spriteNotFoundSprite = sprite;
} }
} }
export const Loader = new LoaderImpl(); export const Loader = new LoaderImpl();

@ -1,83 +1,92 @@
/* typehints:start */ /* typehints:start */
import { MetaBuilding } from "./meta_building"; import { MetaBuilding } from "./meta_building";
import { AtlasSprite } from "../core/sprites"; import { AtlasSprite } from "../core/sprites";
import { Vector } from "../core/vector"; import { Vector } from "../core/vector";
/* typehints:end */ /* typehints:end */
/** /**
* @typedef {{ * @typedef {{
* metaClass: typeof MetaBuilding, * metaClass: typeof MetaBuilding,
* metaInstance?: MetaBuilding, * metaInstance?: MetaBuilding,
* variant?: string, * variant?: string,
* rotationVariant?: number, * rotationVariant?: number,
* tileSize?: Vector, * tileSize?: Vector,
* sprite?: AtlasSprite, * sprite?: AtlasSprite,
* blueprintSprite?: AtlasSprite, * blueprintSprite?: AtlasSprite,
* silhouetteColor?: string * silhouetteColor?: string
* }} BuildingVariantIdentifier * }} BuildingVariantIdentifier
*/ */
/** /**
* Stores a lookup table for all building variants (for better performance) * Stores a lookup table for all building variants (for better performance)
* @type {Object<number, BuildingVariantIdentifier>} * @type {Object<number, BuildingVariantIdentifier>}
*/ */
export const gBuildingVariants = { export const gBuildingVariants = {
// Set later // Set later
}; };
/** /**
* Registers a new variant * Mapping from 'metaBuildingId/variant/rotationVariant' to building code
* @param {number} id * @type {Map<string, number>}
* @param {typeof MetaBuilding} meta */
* @param {string} variant const variantsCache = new Map();
* @param {number} rotationVariant
*/ /**
export function registerBuildingVariant( * Registers a new variant
id, * @param {number} code
meta, * @param {typeof MetaBuilding} meta
variant = "default" /* FIXME: Circular dependency, actually its defaultBuildingVariant */, * @param {string} variant
rotationVariant = 0 * @param {number} rotationVariant
) { */
assert(!gBuildingVariants[id], "Duplicate id: " + id); export function registerBuildingVariant(
gBuildingVariants[id] = { code,
metaClass: meta, meta,
variant, variant = "default" /* FIXME: Circular dependency, actually its defaultBuildingVariant */,
rotationVariant, rotationVariant = 0
// @ts-ignore ) {
tileSize: new meta().getDimensions(variant), assert(!gBuildingVariants[code], "Duplicate id: " + code);
}; gBuildingVariants[code] = {
} metaClass: meta,
variant,
/** rotationVariant,
* // @ts-ignore
* @param {number} code tileSize: new meta().getDimensions(variant),
* @returns {BuildingVariantIdentifier} };
*/ }
export function getBuildingDataFromCode(code) {
assert(gBuildingVariants[code], "Invalid building code: " + code); /**
return gBuildingVariants[code]; *
} * @param {number} code
* @returns {BuildingVariantIdentifier}
/** */
* Finds the code for a given variant export function getBuildingDataFromCode(code) {
* @param {MetaBuilding} metaBuilding assert(gBuildingVariants[code], "Invalid building code: " + code);
* @param {string} variant return gBuildingVariants[code];
* @param {number} rotationVariant }
*/
export function getCodeFromBuildingData(metaBuilding, variant, rotationVariant) { /**
for (const key in gBuildingVariants) { * Builds the cache for the codes
const data = gBuildingVariants[key]; */
if ( export function buildBuildingCodeCache() {
data.metaInstance.getId() === metaBuilding.getId() && for (const code in gBuildingVariants) {
data.variant === variant && const data = gBuildingVariants[code];
data.rotationVariant === rotationVariant const hash = data.metaInstance.getId() + "/" + data.variant + "/" + data.rotationVariant;
) { variantsCache.set(hash, +code);
return +key; }
} }
}
assertAlways( /**
false, * Finds the code for a given variant
"Building not found by data: " + metaBuilding.getId() + " / " + variant + " / " + rotationVariant * @param {MetaBuilding} metaBuilding
); * @param {string} variant
return 0; * @param {number} rotationVariant
} * @returns {number}
*/
export function getCodeFromBuildingData(metaBuilding, variant, rotationVariant) {
const hash = metaBuilding.getId() + "/" + variant + "/" + rotationVariant;
const result = variantsCache.get(hash);
if (G_IS_DEV) {
assertAlways(!!result, "Building not found by data: " + hash);
}
return result;
}

@ -67,11 +67,8 @@ export class EntityManager extends BasicSerializableObject {
} }
assert(!entity.destroyed, `Attempting to register destroyed entity ${entity}`); assert(!entity.destroyed, `Attempting to register destroyed entity ${entity}`);
if (G_IS_DEV && uid !== null) { if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts && uid !== null) {
assert(!this.findByUid(uid, false), "Entity uid already taken: " + uid); assert(!this.findByUid(uid, false), "Entity uid already taken: " + uid);
}
if (uid !== null) {
assert(uid >= 0 && uid < Number.MAX_SAFE_INTEGER, "Invalid uid passed: " + uid); assert(uid >= 0 && uid < Number.MAX_SAFE_INTEGER, "Invalid uid passed: " + uid);
} }

@ -5,6 +5,7 @@ import { enumNotificationType } from "./notifications";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { KEYMAPPINGS } from "../../key_action_mapper"; import { KEYMAPPINGS } from "../../key_action_mapper";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { TrackedState } from "../../../core/tracked_state";
export class HUDGameMenu extends BaseHUDPart { export class HUDGameMenu extends BaseHUDPart {
createElements(parent) { createElements(parent) {
@ -97,12 +98,17 @@ export class HUDGameMenu extends BaseHUDPart {
initialize() { initialize() {
this.root.signals.gameSaved.add(this.onGameSaved, this); this.root.signals.gameSaved.add(this.onGameSaved, this);
this.trackedIsSaving = new TrackedState(this.onIsSavingChanged, this);
} }
update() { update() {
let playSound = false; let playSound = false;
let notifications = new Set(); let notifications = new Set();
// Check whether we are saving
this.trackedIsSaving.set(!!this.root.gameState.currentSavePromise);
// Update visibility of buttons // Update visibility of buttons
for (let i = 0; i < this.visibilityToUpdate.length; ++i) { for (let i = 0; i < this.visibilityToUpdate.length; ++i) {
const { condition, domAttach } = this.visibilityToUpdate[i]; const { condition, domAttach } = this.visibilityToUpdate[i];
@ -154,6 +160,10 @@ export class HUDGameMenu extends BaseHUDPart {
}); });
} }
onIsSavingChanged(isSaving) {
this.saveButton.classList.toggle("saving", isSaving);
}
onGameSaved() { onGameSaved() {
this.saveButton.classList.toggle("animEven"); this.saveButton.classList.toggle("animEven");
this.saveButton.classList.toggle("animOdd"); this.saveButton.classList.toggle("animOdd");

@ -1,56 +1,55 @@
import { BaseHUDPart } from "../base_hud_part"; import { makeDiv } from "../../../core/utils";
import { makeDiv } from "../../../core/utils"; import { T } from "../../../translations";
import { T } from "../../../translations"; import { BaseHUDPart } from "../base_hud_part";
import { IS_DEMO } from "../../../core/config";
/** @enum {string} */
/** @enum {string} */ export const enumNotificationType = {
export const enumNotificationType = { saved: "saved",
saved: "saved", upgrade: "upgrade",
upgrade: "upgrade", success: "success",
success: "success", };
};
const notificationDuration = 3;
const notificationDuration = 3;
export class HUDNotifications extends BaseHUDPart {
export class HUDNotifications extends BaseHUDPart { createElements(parent) {
createElements(parent) { this.element = makeDiv(parent, "ingame_HUD_Notifications", [], ``);
this.element = makeDiv(parent, "ingame_HUD_Notifications", [], ``); }
}
initialize() {
initialize() { this.root.hud.signals.notification.add(this.onNotification, this);
this.root.hud.signals.notification.add(this.onNotification, this);
/** @type {Array<{ element: HTMLElement, expireAt: number}>} */
/** @type {Array<{ element: HTMLElement, expireAt: number}>} */ this.notificationElements = [];
this.notificationElements = [];
// Automatic notifications
// Automatic notifications this.root.signals.gameSaved.add(() =>
this.root.signals.gameSaved.add(() => this.onNotification(T.ingame.notifications.gameSaved, enumNotificationType.saved)
this.onNotification(T.ingame.notifications.gameSaved, enumNotificationType.saved) );
); }
}
/**
/** * @param {string} message
* @param {string} message * @param {enumNotificationType} type
* @param {enumNotificationType} type */
*/ onNotification(message, type) {
onNotification(message, type) { const element = makeDiv(this.element, null, ["notification", "type-" + type], message);
const element = makeDiv(this.element, null, ["notification", "type-" + type], message); element.setAttribute("data-icon", "icons/notification_" + type + ".png");
element.setAttribute("data-icon", "icons/notification_" + type + ".png");
this.notificationElements.push({
this.notificationElements.push({ element,
element, expireAt: this.root.time.realtimeNow() + notificationDuration,
expireAt: this.root.time.realtimeNow() + notificationDuration, });
}); }
}
update() {
update() { const now = this.root.time.realtimeNow();
const now = this.root.time.realtimeNow(); for (let i = 0; i < this.notificationElements.length; ++i) {
for (let i = 0; i < this.notificationElements.length; ++i) { const handle = this.notificationElements[i];
const handle = this.notificationElements[i]; if (handle.expireAt <= now) {
if (handle.expireAt <= now) { handle.element.remove();
handle.element.remove(); this.notificationElements.splice(i, 1);
this.notificationElements.splice(i, 1); }
} }
} }
} }
}

@ -1,236 +1,236 @@
import { globalConfig } from "../core/config"; import { globalConfig } from "../core/config";
import { Vector } from "../core/vector"; import { Vector } from "../core/vector";
import { BasicSerializableObject, types } from "../savegame/serialization"; import { BasicSerializableObject, types } from "../savegame/serialization";
import { BaseItem } from "./base_item"; import { BaseItem } from "./base_item";
import { Entity } from "./entity"; import { Entity } from "./entity";
import { MapChunkView } from "./map_chunk_view"; import { MapChunkView } from "./map_chunk_view";
import { GameRoot } from "./root"; import { GameRoot } from "./root";
export class BaseMap extends BasicSerializableObject { export class BaseMap extends BasicSerializableObject {
static getId() { static getId() {
return "Map"; return "Map";
} }
static getSchema() { static getSchema() {
return { return {
seed: types.uint, seed: types.uint,
}; };
} }
/** /**
* *
* @param {GameRoot} root * @param {GameRoot} root
*/ */
constructor(root) { constructor(root) {
super(); super();
this.root = root; this.root = root;
this.seed = 0; this.seed = 0;
/** /**
* Mapping of 'X|Y' to chunk * Mapping of 'X|Y' to chunk
* @type {Map<string, MapChunkView>} */ * @type {Map<string, MapChunkView>} */
this.chunksById = new Map(); this.chunksById = new Map();
} }
/** /**
* Returns the given chunk by index * Returns the given chunk by index
* @param {number} chunkX * @param {number} chunkX
* @param {number} chunkY * @param {number} chunkY
*/ */
getChunk(chunkX, chunkY, createIfNotExistent = false) { getChunk(chunkX, chunkY, createIfNotExistent = false) {
const chunkIdentifier = chunkX + "|" + chunkY; const chunkIdentifier = chunkX + "|" + chunkY;
let storedChunk; let storedChunk;
if ((storedChunk = this.chunksById.get(chunkIdentifier))) { if ((storedChunk = this.chunksById.get(chunkIdentifier))) {
return storedChunk; return storedChunk;
} }
if (createIfNotExistent) { if (createIfNotExistent) {
const instance = new MapChunkView(this.root, chunkX, chunkY); const instance = new MapChunkView(this.root, chunkX, chunkY);
this.chunksById.set(chunkIdentifier, instance); this.chunksById.set(chunkIdentifier, instance);
return instance; return instance;
} }
return null; return null;
} }
/** /**
* Gets or creates a new chunk if not existent for the given tile * Gets or creates a new chunk if not existent for the given tile
* @param {number} tileX * @param {number} tileX
* @param {number} tileY * @param {number} tileY
* @returns {MapChunkView} * @returns {MapChunkView}
*/ */
getOrCreateChunkAtTile(tileX, tileY) { getOrCreateChunkAtTile(tileX, tileY) {
const chunkX = Math.floor(tileX / globalConfig.mapChunkSize); const chunkX = Math.floor(tileX / globalConfig.mapChunkSize);
const chunkY = Math.floor(tileY / globalConfig.mapChunkSize); const chunkY = Math.floor(tileY / globalConfig.mapChunkSize);
return this.getChunk(chunkX, chunkY, true); return this.getChunk(chunkX, chunkY, true);
} }
/** /**
* Gets a chunk if not existent for the given tile * Gets a chunk if not existent for the given tile
* @param {number} tileX * @param {number} tileX
* @param {number} tileY * @param {number} tileY
* @returns {MapChunkView?} * @returns {MapChunkView?}
*/ */
getChunkAtTileOrNull(tileX, tileY) { getChunkAtTileOrNull(tileX, tileY) {
const chunkX = Math.floor(tileX / globalConfig.mapChunkSize); const chunkX = Math.floor(tileX / globalConfig.mapChunkSize);
const chunkY = Math.floor(tileY / globalConfig.mapChunkSize); const chunkY = Math.floor(tileY / globalConfig.mapChunkSize);
return this.getChunk(chunkX, chunkY, false); return this.getChunk(chunkX, chunkY, false);
} }
/** /**
* Checks if a given tile is within the map bounds * Checks if a given tile is within the map bounds
* @param {Vector} tile * @param {Vector} tile
* @returns {boolean} * @returns {boolean}
*/ */
isValidTile(tile) { isValidTile(tile) {
if (G_IS_DEV) { if (G_IS_DEV) {
assert(tile instanceof Vector, "tile is not a vector"); assert(tile instanceof Vector, "tile is not a vector");
} }
return Number.isInteger(tile.x) && Number.isInteger(tile.y); return Number.isInteger(tile.x) && Number.isInteger(tile.y);
} }
/** /**
* Returns the tile content of a given tile * Returns the tile content of a given tile
* @param {Vector} tile * @param {Vector} tile
* @param {Layer} layer * @param {Layer} layer
* @returns {Entity} Entity or null * @returns {Entity} Entity or null
*/ */
getTileContent(tile, layer) { getTileContent(tile, layer) {
if (G_IS_DEV) { if (G_IS_DEV) {
this.internalCheckTile(tile); this.internalCheckTile(tile);
} }
const chunk = this.getChunkAtTileOrNull(tile.x, tile.y); const chunk = this.getChunkAtTileOrNull(tile.x, tile.y);
return chunk && chunk.getLayerContentFromWorldCoords(tile.x, tile.y, layer); return chunk && chunk.getLayerContentFromWorldCoords(tile.x, tile.y, layer);
} }
/** /**
* Returns the lower layers content of the given tile * Returns the lower layers content of the given tile
* @param {number} x * @param {number} x
* @param {number} y * @param {number} y
* @returns {BaseItem=} * @returns {BaseItem=}
*/ */
getLowerLayerContentXY(x, y) { getLowerLayerContentXY(x, y) {
return this.getOrCreateChunkAtTile(x, y).getLowerLayerFromWorldCoords(x, y); return this.getOrCreateChunkAtTile(x, y).getLowerLayerFromWorldCoords(x, y);
} }
/** /**
* Returns the tile content of a given tile * Returns the tile content of a given tile
* @param {number} x * @param {number} x
* @param {number} y * @param {number} y
* @param {Layer} layer * @param {Layer} layer
* @returns {Entity} Entity or null * @returns {Entity} Entity or null
*/ */
getLayerContentXY(x, y, layer) { getLayerContentXY(x, y, layer) {
const chunk = this.getChunkAtTileOrNull(x, y); const chunk = this.getChunkAtTileOrNull(x, y);
return chunk && chunk.getLayerContentFromWorldCoords(x, y, layer); return chunk && chunk.getLayerContentFromWorldCoords(x, y, layer);
} }
/** /**
* Returns the tile contents of a given tile * Returns the tile contents of a given tile
* @param {number} x * @param {number} x
* @param {number} y * @param {number} y
* @returns {Array<Entity>} Entity or null * @returns {Array<Entity>} Entity or null
*/ */
getLayersContentsMultipleXY(x, y) { getLayersContentsMultipleXY(x, y) {
const chunk = this.getChunkAtTileOrNull(x, y); const chunk = this.getChunkAtTileOrNull(x, y);
if (!chunk) { if (!chunk) {
return []; return [];
} }
return chunk.getLayersContentsMultipleFromWorldCoords(x, y); return chunk.getLayersContentsMultipleFromWorldCoords(x, y);
} }
/** /**
* Checks if the tile is used * Checks if the tile is used
* @param {Vector} tile * @param {Vector} tile
* @param {Layer} layer * @param {Layer} layer
* @returns {boolean} * @returns {boolean}
*/ */
isTileUsed(tile, layer) { isTileUsed(tile, layer) {
if (G_IS_DEV) { if (G_IS_DEV) {
this.internalCheckTile(tile); this.internalCheckTile(tile);
} }
const chunk = this.getChunkAtTileOrNull(tile.x, tile.y); const chunk = this.getChunkAtTileOrNull(tile.x, tile.y);
return chunk && chunk.getLayerContentFromWorldCoords(tile.x, tile.y, layer) != null; return chunk && chunk.getLayerContentFromWorldCoords(tile.x, tile.y, layer) != null;
} }
/** /**
* Checks if the tile is used * Checks if the tile is used
* @param {number} x * @param {number} x
* @param {number} y * @param {number} y
* @param {Layer} layer * @param {Layer} layer
* @returns {boolean} * @returns {boolean}
*/ */
isTileUsedXY(x, y, layer) { isTileUsedXY(x, y, layer) {
const chunk = this.getChunkAtTileOrNull(x, y); const chunk = this.getChunkAtTileOrNull(x, y);
return chunk && chunk.getLayerContentFromWorldCoords(x, y, layer) != null; return chunk && chunk.getLayerContentFromWorldCoords(x, y, layer) != null;
} }
/** /**
* Sets the tiles content * Sets the tiles content
* @param {Vector} tile * @param {Vector} tile
* @param {Entity} entity * @param {Entity} entity
*/ */
setTileContent(tile, entity) { setTileContent(tile, entity) {
if (G_IS_DEV) { if (G_IS_DEV) {
this.internalCheckTile(tile); this.internalCheckTile(tile);
} }
this.getOrCreateChunkAtTile(tile.x, tile.y).setLayerContentFromWorldCords( this.getOrCreateChunkAtTile(tile.x, tile.y).setLayerContentFromWorldCords(
tile.x, tile.x,
tile.y, tile.y,
entity, entity,
entity.layer entity.layer
); );
const staticComponent = entity.components.StaticMapEntity; const staticComponent = entity.components.StaticMapEntity;
assert(staticComponent, "Can only place static map entities in tiles"); assert(staticComponent, "Can only place static map entities in tiles");
} }
/** /**
* Places an entity with the StaticMapEntity component * Places an entity with the StaticMapEntity component
* @param {Entity} entity * @param {Entity} entity
*/ */
placeStaticEntity(entity) { placeStaticEntity(entity) {
assert(entity.components.StaticMapEntity, "Entity is not static"); assert(entity.components.StaticMapEntity, "Entity is not static");
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
const rect = staticComp.getTileSpaceBounds(); const rect = staticComp.getTileSpaceBounds();
for (let dx = 0; dx < rect.w; ++dx) { for (let dx = 0; dx < rect.w; ++dx) {
for (let dy = 0; dy < rect.h; ++dy) { for (let dy = 0; dy < rect.h; ++dy) {
const x = rect.x + dx; const x = rect.x + dx;
const y = rect.y + dy; const y = rect.y + dy;
this.getOrCreateChunkAtTile(x, y).setLayerContentFromWorldCords(x, y, entity, entity.layer); this.getOrCreateChunkAtTile(x, y).setLayerContentFromWorldCords(x, y, entity, entity.layer);
} }
} }
} }
/** /**
* Removes an entity with the StaticMapEntity component * Removes an entity with the StaticMapEntity component
* @param {Entity} entity * @param {Entity} entity
*/ */
removeStaticEntity(entity) { removeStaticEntity(entity) {
assert(entity.components.StaticMapEntity, "Entity is not static"); assert(entity.components.StaticMapEntity, "Entity is not static");
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
const rect = staticComp.getTileSpaceBounds(); const rect = staticComp.getTileSpaceBounds();
for (let dx = 0; dx < rect.w; ++dx) { for (let dx = 0; dx < rect.w; ++dx) {
for (let dy = 0; dy < rect.h; ++dy) { for (let dy = 0; dy < rect.h; ++dy) {
const x = rect.x + dx; const x = rect.x + dx;
const y = rect.y + dy; const y = rect.y + dy;
this.getOrCreateChunkAtTile(x, y).setLayerContentFromWorldCords(x, y, null, entity.layer); this.getOrCreateChunkAtTile(x, y).setLayerContentFromWorldCords(x, y, null, entity.layer);
} }
} }
} }
// Internal // Internal
/** /**
* Checks a given tile for validty * Checks a given tile for validty
* @param {Vector} tile * @param {Vector} tile
*/ */
internalCheckTile(tile) { internalCheckTile(tile) {
assert(tile instanceof Vector, "tile is not a vector: " + tile); assert(tile instanceof Vector, "tile is not a vector: " + tile);
assert(tile.x % 1 === 0, "Tile X is not a valid integer: " + tile.x); assert(tile.x % 1 === 0, "Tile X is not a valid integer: " + tile.x);
assert(tile.y % 1 === 0, "Tile Y is not a valid integer: " + tile.y); assert(tile.y % 1 === 0, "Tile Y is not a valid integer: " + tile.y);
} }
} }

@ -66,32 +66,34 @@ export class MapView extends BaseMap {
* @param {DrawParameters} drawParameters * @param {DrawParameters} drawParameters
*/ */
drawStaticEntityDebugOverlays(drawParameters) { drawStaticEntityDebugOverlays(drawParameters) {
const cullRange = drawParameters.visibleRect.toTileCullRectangle(); if (G_IS_DEV && (globalConfig.debug.showAcceptorEjectors || globalConfig.debug.showEntityBounds)) {
const top = cullRange.top(); const cullRange = drawParameters.visibleRect.toTileCullRectangle();
const right = cullRange.right(); const top = cullRange.top();
const bottom = cullRange.bottom(); const right = cullRange.right();
const left = cullRange.left(); const bottom = cullRange.bottom();
const left = cullRange.left();
const border = 1;
const border = 1;
const minY = top - border;
const maxY = bottom + border; const minY = top - border;
const minX = left - border; const maxY = bottom + border;
const maxX = right + border - 1; const minX = left - border;
const maxX = right + border - 1;
// Render y from top down for proper blending
for (let y = minY; y <= maxY; ++y) { // Render y from top down for proper blending
for (let x = minX; x <= maxX; ++x) { for (let y = minY; y <= maxY; ++y) {
// const content = this.tiles[x][y]; for (let x = minX; x <= maxX; ++x) {
const chunk = this.getChunkAtTileOrNull(x, y); // const content = this.tiles[x][y];
if (!chunk) { const chunk = this.getChunkAtTileOrNull(x, y);
continue; if (!chunk) {
} continue;
const content = chunk.getTileContentFromWorldCoords(x, y); }
if (content) { const content = chunk.getTileContentFromWorldCoords(x, y);
let isBorder = x <= left - 1 || x >= right + 1 || y <= top - 1 || y >= bottom + 1; if (content) {
if (!isBorder) { let isBorder = x <= left - 1 || x >= right + 1 || y <= top - 1 || y >= bottom + 1;
content.drawDebugOverlays(drawParameters); if (!isBorder) {
content.drawDebugOverlays(drawParameters);
}
} }
} }
} }

@ -12,7 +12,7 @@ import { MetaStackerBuilding } from "./buildings/stacker";
import { enumTrashVariants, MetaTrashBuilding } from "./buildings/trash"; import { enumTrashVariants, MetaTrashBuilding } from "./buildings/trash";
import { enumUndergroundBeltVariants, MetaUndergroundBeltBuilding } from "./buildings/underground_belt"; import { enumUndergroundBeltVariants, MetaUndergroundBeltBuilding } from "./buildings/underground_belt";
import { MetaWireBuilding } from "./buildings/wire"; import { MetaWireBuilding } from "./buildings/wire";
import { gBuildingVariants, registerBuildingVariant } from "./building_codes"; import { buildBuildingCodeCache, gBuildingVariants, registerBuildingVariant } from "./building_codes";
import { defaultBuildingVariant } from "./meta_building"; import { defaultBuildingVariant } from "./meta_building";
import { MetaConstantSignalBuilding } from "./buildings/constant_signal"; import { MetaConstantSignalBuilding } from "./buildings/constant_signal";
import { MetaLogicGateBuilding, enumLogicGateVariants } from "./buildings/logic_gate"; import { MetaLogicGateBuilding, enumLogicGateVariants } from "./buildings/logic_gate";
@ -174,4 +174,7 @@ export function initBuildingCodesAfterResourcesLoaded() {
); );
variant.silhouetteColor = variant.metaInstance.getSilhouetteColor(); variant.silhouetteColor = variant.metaInstance.getSilhouetteColor();
} }
// Update caches
buildBuildingCodeCache();
} }

@ -1,101 +1,101 @@
import { GameSystemWithFilter } from "../game_system_with_filter"; import { GameSystemWithFilter } from "../game_system_with_filter";
import { StorageComponent } from "../components/storage"; import { StorageComponent } from "../components/storage";
import { DrawParameters } from "../../core/draw_parameters"; import { DrawParameters } from "../../core/draw_parameters";
import { formatBigNumber, lerp } from "../../core/utils"; import { formatBigNumber, lerp } from "../../core/utils";
import { Loader } from "../../core/loader"; import { Loader } from "../../core/loader";
import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item"; import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item";
import { MapChunkView } from "../map_chunk_view"; import { MapChunkView } from "../map_chunk_view";
export class StorageSystem extends GameSystemWithFilter { export class StorageSystem extends GameSystemWithFilter {
constructor(root) { constructor(root) {
super(root, [StorageComponent]); super(root, [StorageComponent]);
this.storageOverlaySprite = Loader.getSprite("sprites/misc/storage_overlay.png"); this.storageOverlaySprite = Loader.getSprite("sprites/misc/storage_overlay.png");
/** /**
* Stores which uids were already drawn to avoid drawing entities twice * Stores which uids were already drawn to avoid drawing entities twice
* @type {Set<number>} * @type {Set<number>}
*/ */
this.drawnUids = new Set(); this.drawnUids = new Set();
this.root.signals.gameFrameStarted.add(this.clearDrawnUids, this); this.root.signals.gameFrameStarted.add(this.clearDrawnUids, this);
} }
clearDrawnUids() { clearDrawnUids() {
this.drawnUids.clear(); this.drawnUids.clear();
} }
update() { update() {
for (let i = 0; i < this.allEntities.length; ++i) { for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i]; const entity = this.allEntities[i];
const storageComp = entity.components.Storage; const storageComp = entity.components.Storage;
const pinsComp = entity.components.WiredPins; const pinsComp = entity.components.WiredPins;
// Eject from storage // Eject from storage
if (storageComp.storedItem && storageComp.storedCount > 0) { if (storageComp.storedItem && storageComp.storedCount > 0) {
const ejectorComp = entity.components.ItemEjector; const ejectorComp = entity.components.ItemEjector;
const nextSlot = ejectorComp.getFirstFreeSlot(); const nextSlot = ejectorComp.getFirstFreeSlot();
if (nextSlot !== null) { if (nextSlot !== null) {
if (ejectorComp.tryEject(nextSlot, storageComp.storedItem)) { if (ejectorComp.tryEject(nextSlot, storageComp.storedItem)) {
storageComp.storedCount--; storageComp.storedCount--;
if (storageComp.storedCount === 0) { if (storageComp.storedCount === 0) {
storageComp.storedItem = null; storageComp.storedItem = null;
} }
} }
} }
} }
let targetAlpha = storageComp.storedCount > 0 ? 1 : 0; let targetAlpha = storageComp.storedCount > 0 ? 1 : 0;
storageComp.overlayOpacity = lerp(storageComp.overlayOpacity, targetAlpha, 0.05); storageComp.overlayOpacity = lerp(storageComp.overlayOpacity, targetAlpha, 0.05);
pinsComp.slots[0].value = storageComp.storedItem; pinsComp.slots[0].value = storageComp.storedItem;
pinsComp.slots[1].value = storageComp.getIsFull() ? BOOL_TRUE_SINGLETON : BOOL_FALSE_SINGLETON; pinsComp.slots[1].value = storageComp.getIsFull() ? BOOL_TRUE_SINGLETON : BOOL_FALSE_SINGLETON;
} }
} }
/** /**
* @param {DrawParameters} parameters * @param {DrawParameters} parameters
* @param {MapChunkView} chunk * @param {MapChunkView} chunk
*/ */
drawChunk(parameters, chunk) { drawChunk(parameters, chunk) {
const contents = chunk.containedEntitiesByLayer.regular; const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) { for (let i = 0; i < contents.length; ++i) {
const entity = contents[i]; const entity = contents[i];
const storageComp = entity.components.Storage; const storageComp = entity.components.Storage;
if (!storageComp) { if (!storageComp) {
continue; continue;
} }
const storedItem = storageComp.storedItem; const storedItem = storageComp.storedItem;
if (!storedItem) { if (!storedItem) {
continue; continue;
} }
if (this.drawnUids.has(entity.uid)) { if (this.drawnUids.has(entity.uid)) {
continue; continue;
} }
this.drawnUids.add(entity.uid); this.drawnUids.add(entity.uid);
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
const context = parameters.context; const context = parameters.context;
context.globalAlpha = storageComp.overlayOpacity; context.globalAlpha = storageComp.overlayOpacity;
const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace();
storedItem.drawItemCenteredClipped(center.x, center.y, parameters, 30); storedItem.drawItemCenteredClipped(center.x, center.y, parameters, 30);
this.storageOverlaySprite.drawCached(parameters, center.x - 15, center.y + 15, 30, 15); this.storageOverlaySprite.drawCached(parameters, center.x - 15, center.y + 15, 30, 15);
if (parameters.visibleRect.containsCircle(center.x, center.y + 25, 20)) { if (parameters.visibleRect.containsCircle(center.x, center.y + 25, 20)) {
context.font = "bold 10px GameFont"; context.font = "bold 10px GameFont";
context.textAlign = "center"; context.textAlign = "center";
context.fillStyle = "#64666e"; context.fillStyle = "#64666e";
context.fillText(formatBigNumber(storageComp.storedCount), center.x, center.y + 25.5); context.fillText(formatBigNumber(storageComp.storedCount), center.x, center.y + 25.5);
context.textAlign = "left"; context.textAlign = "left";
} }
context.globalAlpha = 1; context.globalAlpha = 1;
} }
} }
} }

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

Loading…
Cancel
Save