Further performance improvements, show indicator while game is saving
This commit is contained in:
parent
bba29b8a8b
commit
1ebfafd8de
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Reference in New Issue