Further performance improvements, show indicator while game is saving

This commit is contained in:
tobspr 2020-09-18 20:41:40 +02:00
parent bba29b8a8b
commit 1ebfafd8de
11 changed files with 1223 additions and 1172 deletions

View File

@ -56,6 +56,17 @@
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 {

View File

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

View File

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

View File

@ -67,11 +67,8 @@ export class EntityManager extends BasicSerializableObject {
}
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);
}
if (uid !== null) {
assert(uid >= 0 && uid < Number.MAX_SAFE_INTEGER, "Invalid uid passed: " + uid);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,438 +1,458 @@
import { APPLICATION_ERROR_OCCURED } from "../core/error_handler";
import { GameState } from "../core/game_state";
import { logSection, createLogger } from "../core/logging";
import { waitNextFrame } from "../core/utils";
import { globalConfig } from "../core/config";
import { GameLoadingOverlay } from "../game/game_loading_overlay";
import { KeyActionMapper } from "../game/key_action_mapper";
import { Savegame } from "../savegame/savegame";
import { GameCore } from "../game/core";
import { MUSIC } from "../platform/sound";
const logger = createLogger("state/ingame");
// Different sub-states
const stages = {
s3_createCore: "🌈 3: Create core",
s4_A_initEmptyGame: "🌈 4/A: Init empty game",
s4_B_resumeGame: "🌈 4/B: Resume game",
s5_firstUpdate: "🌈 5: First game update",
s6_postLoadHook: "🌈 6: Post load hook",
s7_warmup: "🌈 7: Warmup",
s10_gameRunning: "🌈 10: Game finally running",
leaving: "🌈 Saving, then leaving the game",
destroyed: "🌈 DESTROYED: Core is empty and waits for state leave",
initFailed: "🌈 ERROR: Initialization failed!",
};
export const gameCreationAction = {
new: "new-game",
resume: "resume-game",
};
// Typehints
export class GameCreationPayload {
constructor() {
/** @type {boolean|undefined} */
this.fastEnter;
/** @type {Savegame} */
this.savegame;
}
}
export class InGameState extends GameState {
constructor() {
super("InGameState");
/** @type {GameCreationPayload} */
this.creationPayload = null;
// Stores current stage
this.stage = "";
/** @type {GameCore} */
this.core = null;
/** @type {KeyActionMapper} */
this.keyActionMapper = null;
/** @type {GameLoadingOverlay} */
this.loadingOverlay = null;
/** @type {Savegame} */
this.savegame;
this.boundInputFilter = this.filterInput.bind(this);
}
/**
* Switches the game into another sub-state
* @param {string} stage
*/
switchStage(stage) {
assert(stage, "Got empty stage");
if (stage !== this.stage) {
this.stage = stage;
logger.log(this.stage);
return true;
} else {
// log(this, "Re entering", stage);
return false;
}
}
// GameState implementation
getInnerHTML() {
return "";
}
getThemeMusic() {
return MUSIC.theme;
}
onBeforeExit() {
// logger.log("Saving before quitting");
// return this.doSave().then(() => {
// logger.log(this, "Successfully saved");
// // this.stageDestroyed();
// });
}
onAppPause() {
// if (this.stage === stages.s10_gameRunning) {
// logger.log("Saving because app got paused");
// this.doSave();
// }
}
getHasFadeIn() {
return false;
}
getPauseOnFocusLost() {
return false;
}
getHasUnloadConfirmation() {
return true;
}
onLeave() {
if (this.core) {
this.stageDestroyed();
}
this.app.inputMgr.dismountFilter(this.boundInputFilter);
}
onResized(w, h) {
super.onResized(w, h);
if (this.stage === stages.s10_gameRunning) {
this.core.resize(w, h);
}
}
// ---- End of GameState implementation
/**
* Goes back to the menu state
*/
goBackToMenu() {
this.saveThenGoToState("MainMenuState");
}
/**
* Goes back to the settings state
*/
goToSettings() {
this.saveThenGoToState("SettingsState", {
backToStateId: this.key,
backToStatePayload: this.creationPayload,
});
}
/**
* Goes back to the settings state
*/
goToKeybindings() {
this.saveThenGoToState("KeybindingsState", {
backToStateId: this.key,
backToStatePayload: this.creationPayload,
});
}
/**
* Moves to a state outside of the game
* @param {string} stateId
* @param {any=} payload
*/
saveThenGoToState(stateId, payload) {
if (this.stage === stages.leaving || this.stage === stages.destroyed) {
logger.warn(
"Tried to leave game twice or during destroy:",
this.stage,
"(attempted to move to",
stateId,
")"
);
return;
}
this.stageLeavingGame();
this.doSave().then(() => {
this.stageDestroyed();
this.moveToState(stateId, payload);
});
}
onBackButton() {
// do nothing
}
/**
* Called when the game somehow failed to initialize. Resets everything to basic state and
* then goes to the main menu, showing the error
* @param {string} err
*/
onInitializationFailure(err) {
if (this.switchStage(stages.initFailed)) {
logger.error("Init failure:", err);
this.stageDestroyed();
this.moveToState("MainMenuState", { loadError: err });
}
}
// STAGES
/**
* Creates the game core instance, and thus the root
*/
stage3CreateCore() {
if (this.switchStage(stages.s3_createCore)) {
logger.log("Creating new game core");
this.core = new GameCore(this.app);
this.core.initializeRoot(this, this.savegame);
if (this.savegame.hasGameDump()) {
this.stage4bResumeGame();
} else {
this.app.gameAnalytics.handleGameStarted();
this.stage4aInitEmptyGame();
}
}
}
/**
* Initializes a new empty game
*/
stage4aInitEmptyGame() {
if (this.switchStage(stages.s4_A_initEmptyGame)) {
this.core.initNewGame();
this.stage5FirstUpdate();
}
}
/**
* Resumes an existing game
*/
stage4bResumeGame() {
if (this.switchStage(stages.s4_B_resumeGame)) {
if (!this.core.initExistingGame()) {
this.onInitializationFailure("Savegame is corrupt and can not be restored.");
return;
}
this.app.gameAnalytics.handleGameResumed();
this.stage5FirstUpdate();
}
}
/**
* Performs the first game update on the game which initializes most caches
*/
stage5FirstUpdate() {
if (this.switchStage(stages.s5_firstUpdate)) {
this.core.root.logicInitialized = true;
this.core.updateLogic();
this.stage6PostLoadHook();
}
}
/**
* 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)) {
logger.log("Post load hook");
this.core.postLoadHook();
this.stage7Warmup();
}
}
/**
* 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
* 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.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");
// Initial resize, might have changed during loading (this is possible)
this.core.resize(this.app.screenWidth, this.app.screenHeight);
}
}
/**
* This stage destroys the whole game, used to cleanup
*/
stageDestroyed() {
if (this.switchStage(stages.destroyed)) {
// Cleanup all api calls
this.cancelAllAsyncOperations();
if (this.syncer) {
this.syncer.cancelSync();
this.syncer = null;
}
// Cleanup core
if (this.core) {
this.core.destruct();
this.core = null;
}
}
}
/**
* When leaving the game
*/
stageLeavingGame() {
if (this.switchStage(stages.leaving)) {
// ...
}
}
// END STAGES
/**
* Filters the input (keybindings)
*/
filterInput() {
return this.stage === stages.s10_gameRunning;
}
/**
* @param {GameCreationPayload} payload
*/
onEnter(payload) {
this.app.inputMgr.installFilter(this.boundInputFilter);
this.creationPayload = payload;
this.savegame = payload.savegame;
this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement());
this.loadingOverlay.showBasic();
// Remove unneded default element
document.body.querySelector(".modalDialogParent").remove();
this.asyncChannel.watch(waitNextFrame()).then(() => this.stage3CreateCore());
}
/**
* Render callback
* @param {number} dt
*/
onRender(dt) {
if (APPLICATION_ERROR_OCCURED) {
// Application somehow crashed, do not do anything
return;
}
if (this.stage === stages.s7_warmup) {
this.core.draw();
this.warmupTimeSeconds -= dt / 1000.0;
if (this.warmupTimeSeconds < 0) {
logger.log("Warmup completed");
this.stage10GameRunning();
}
}
if (this.stage === stages.s10_gameRunning) {
this.core.tick(dt);
}
// 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.app.pageVisible) {
this.core.draw();
}
this.loadingOverlay.removeIfAttached();
} else {
if (!this.loadingOverlay.isAttached()) {
this.loadingOverlay.showBasic();
}
}
}
onBackgroundTick(dt) {
this.onRender(dt);
}
/**
* Saves the game
*/
doSave() {
if (!this.savegame || !this.savegame.isSaveable()) {
return Promise.resolve();
}
if (APPLICATION_ERROR_OCCURED) {
logger.warn("skipping save because application crashed");
return Promise.resolve();
}
if (
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();
}
// First update the game data
logger.log("Starting to save game ...");
this.core.root.signals.gameSaved.dispatch();
this.savegame.updateData(this.core.root);
return this.savegame.writeSavegameAndMetadata().catch(err => {
logger.warn("Failed to save:", err);
});
}
}
import { APPLICATION_ERROR_OCCURED } from "../core/error_handler";
import { GameState } from "../core/game_state";
import { logSection, createLogger } from "../core/logging";
import { waitNextFrame } from "../core/utils";
import { globalConfig } from "../core/config";
import { GameLoadingOverlay } from "../game/game_loading_overlay";
import { KeyActionMapper } from "../game/key_action_mapper";
import { Savegame } from "../savegame/savegame";
import { GameCore } from "../game/core";
import { MUSIC } from "../platform/sound";
const logger = createLogger("state/ingame");
// Different sub-states
const stages = {
s3_createCore: "🌈 3: Create core",
s4_A_initEmptyGame: "🌈 4/A: Init empty game",
s4_B_resumeGame: "🌈 4/B: Resume game",
s5_firstUpdate: "🌈 5: First game update",
s6_postLoadHook: "🌈 6: Post load hook",
s7_warmup: "🌈 7: Warmup",
s10_gameRunning: "🌈 10: Game finally running",
leaving: "🌈 Saving, then leaving the game",
destroyed: "🌈 DESTROYED: Core is empty and waits for state leave",
initFailed: "🌈 ERROR: Initialization failed!",
};
export const gameCreationAction = {
new: "new-game",
resume: "resume-game",
};
// Typehints
export class GameCreationPayload {
constructor() {
/** @type {boolean|undefined} */
this.fastEnter;
/** @type {Savegame} */
this.savegame;
}
}
export class InGameState extends GameState {
constructor() {
super("InGameState");
/** @type {GameCreationPayload} */
this.creationPayload = null;
// Stores current stage
this.stage = "";
/** @type {GameCore} */
this.core = null;
/** @type {KeyActionMapper} */
this.keyActionMapper = null;
/** @type {GameLoadingOverlay} */
this.loadingOverlay = null;
/** @type {Savegame} */
this.savegame = null;
this.boundInputFilter = this.filterInput.bind(this);
/**
* Whether we are currently saving the game
* @TODO: This doesn't realy fit here
*/
this.currentSavePromise = null;
}
/**
* Switches the game into another sub-state
* @param {string} stage
*/
switchStage(stage) {
assert(stage, "Got empty stage");
if (stage !== this.stage) {
this.stage = stage;
logger.log(this.stage);
return true;
} else {
// log(this, "Re entering", stage);
return false;
}
}
// GameState implementation
getInnerHTML() {
return "";
}
getThemeMusic() {
return MUSIC.theme;
}
onBeforeExit() {
// logger.log("Saving before quitting");
// return this.doSave().then(() => {
// logger.log(this, "Successfully saved");
// // this.stageDestroyed();
// });
}
onAppPause() {
// if (this.stage === stages.s10_gameRunning) {
// logger.log("Saving because app got paused");
// this.doSave();
// }
}
getHasFadeIn() {
return false;
}
getPauseOnFocusLost() {
return false;
}
getHasUnloadConfirmation() {
return true;
}
onLeave() {
if (this.core) {
this.stageDestroyed();
}
this.app.inputMgr.dismountFilter(this.boundInputFilter);
}
onResized(w, h) {
super.onResized(w, h);
if (this.stage === stages.s10_gameRunning) {
this.core.resize(w, h);
}
}
// ---- End of GameState implementation
/**
* Goes back to the menu state
*/
goBackToMenu() {
this.saveThenGoToState("MainMenuState");
}
/**
* Goes back to the settings state
*/
goToSettings() {
this.saveThenGoToState("SettingsState", {
backToStateId: this.key,
backToStatePayload: this.creationPayload,
});
}
/**
* Goes back to the settings state
*/
goToKeybindings() {
this.saveThenGoToState("KeybindingsState", {
backToStateId: this.key,
backToStatePayload: this.creationPayload,
});
}
/**
* Moves to a state outside of the game
* @param {string} stateId
* @param {any=} payload
*/
saveThenGoToState(stateId, payload) {
if (this.stage === stages.leaving || this.stage === stages.destroyed) {
logger.warn(
"Tried to leave game twice or during destroy:",
this.stage,
"(attempted to move to",
stateId,
")"
);
return;
}
this.stageLeavingGame();
this.doSave().then(() => {
this.stageDestroyed();
this.moveToState(stateId, payload);
});
}
onBackButton() {
// do nothing
}
/**
* Called when the game somehow failed to initialize. Resets everything to basic state and
* then goes to the main menu, showing the error
* @param {string} err
*/
onInitializationFailure(err) {
if (this.switchStage(stages.initFailed)) {
logger.error("Init failure:", err);
this.stageDestroyed();
this.moveToState("MainMenuState", { loadError: err });
}
}
// STAGES
/**
* Creates the game core instance, and thus the root
*/
stage3CreateCore() {
if (this.switchStage(stages.s3_createCore)) {
logger.log("Creating new game core");
this.core = new GameCore(this.app);
this.core.initializeRoot(this, this.savegame);
if (this.savegame.hasGameDump()) {
this.stage4bResumeGame();
} else {
this.app.gameAnalytics.handleGameStarted();
this.stage4aInitEmptyGame();
}
}
}
/**
* Initializes a new empty game
*/
stage4aInitEmptyGame() {
if (this.switchStage(stages.s4_A_initEmptyGame)) {
this.core.initNewGame();
this.stage5FirstUpdate();
}
}
/**
* Resumes an existing game
*/
stage4bResumeGame() {
if (this.switchStage(stages.s4_B_resumeGame)) {
if (!this.core.initExistingGame()) {
this.onInitializationFailure("Savegame is corrupt and can not be restored.");
return;
}
this.app.gameAnalytics.handleGameResumed();
this.stage5FirstUpdate();
}
}
/**
* Performs the first game update on the game which initializes most caches
*/
stage5FirstUpdate() {
if (this.switchStage(stages.s5_firstUpdate)) {
this.core.root.logicInitialized = true;
this.core.updateLogic();
this.stage6PostLoadHook();
}
}
/**
* 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)) {
logger.log("Post load hook");
this.core.postLoadHook();
this.stage7Warmup();
}
}
/**
* 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
* 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.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");
// Initial resize, might have changed during loading (this is possible)
this.core.resize(this.app.screenWidth, this.app.screenHeight);
}
}
/**
* This stage destroys the whole game, used to cleanup
*/
stageDestroyed() {
if (this.switchStage(stages.destroyed)) {
// Cleanup all api calls
this.cancelAllAsyncOperations();
if (this.syncer) {
this.syncer.cancelSync();
this.syncer = null;
}
// Cleanup core
if (this.core) {
this.core.destruct();
this.core = null;
}
}
}
/**
* When leaving the game
*/
stageLeavingGame() {
if (this.switchStage(stages.leaving)) {
// ...
}
}
// END STAGES
/**
* Filters the input (keybindings)
*/
filterInput() {
return this.stage === stages.s10_gameRunning;
}
/**
* @param {GameCreationPayload} payload
*/
onEnter(payload) {
this.app.inputMgr.installFilter(this.boundInputFilter);
this.creationPayload = payload;
this.savegame = payload.savegame;
this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement());
this.loadingOverlay.showBasic();
// Remove unneded default element
document.body.querySelector(".modalDialogParent").remove();
this.asyncChannel.watch(waitNextFrame()).then(() => this.stage3CreateCore());
}
/**
* Render callback
* @param {number} dt
*/
onRender(dt) {
if (APPLICATION_ERROR_OCCURED) {
// Application somehow crashed, do not do anything
return;
}
if (this.stage === stages.s7_warmup) {
this.core.draw();
this.warmupTimeSeconds -= dt / 1000.0;
if (this.warmupTimeSeconds < 0) {
logger.log("Warmup completed");
this.stage10GameRunning();
}
}
if (this.stage === stages.s10_gameRunning) {
this.core.tick(dt);
}
// 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.app.pageVisible) {
this.core.draw();
}
this.loadingOverlay.removeIfAttached();
} else {
if (!this.loadingOverlay.isAttached()) {
this.loadingOverlay.showBasic();
}
}
}
onBackgroundTick(dt) {
this.onRender(dt);
}
/**
* Saves the game
*/
doSave() {
if (!this.savegame || !this.savegame.isSaveable()) {
return Promise.resolve();
}
if (APPLICATION_ERROR_OCCURED) {
logger.warn("skipping save because application crashed");
return Promise.resolve();
}
if (
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;
}
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;
}
}