Inital tooling run

pull/1491/head
Bagel03 2 years ago
parent 5a10e73dc5
commit d3fe689b31

1
src/ts/.gitignore vendored

@ -0,0 +1 @@
built-temp

@ -0,0 +1,357 @@
import { AnimationFrame } from "./core/animation_frame";
import { BackgroundResourcesLoader } from "./core/background_resources_loader";
import { IS_MOBILE } from "./core/config";
import { GameState } from "./core/game_state";
import { GLOBAL_APP, setGlobalApp } from "./core/globals";
import { InputDistributor } from "./core/input_distributor";
import { Loader } from "./core/loader";
import { createLogger, logSection } from "./core/logging";
import { StateManager } from "./core/state_manager";
import { TrackedState } from "./core/tracked_state";
import { getPlatformName, waitNextFrame } from "./core/utils";
import { Vector } from "./core/vector";
import { AdProviderInterface } from "./platform/ad_provider";
import { NoAdProvider } from "./platform/ad_providers/no_ad_provider";
import { NoAchievementProvider } from "./platform/browser/no_achievement_provider";
import { AnalyticsInterface } from "./platform/analytics";
import { GoogleAnalyticsImpl } from "./platform/browser/google_analytics";
import { SoundImplBrowser } from "./platform/browser/sound";
import { PlatformWrapperImplBrowser } from "./platform/browser/wrapper";
import { PlatformWrapperImplElectron } from "./platform/electron/wrapper";
import { PlatformWrapperInterface } from "./platform/wrapper";
import { ApplicationSettings } from "./profile/application_settings";
import { SavegameManager } from "./savegame/savegame_manager";
import { AboutState } from "./states/about";
import { ChangelogState } from "./states/changelog";
import { InGameState } from "./states/ingame";
import { KeybindingsState } from "./states/keybindings";
import { MainMenuState } from "./states/main_menu";
import { MobileWarningState } from "./states/mobile_warning";
import { PreloadState } from "./states/preload";
import { SettingsState } from "./states/settings";
import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
import { RestrictionManager } from "./core/restriction_manager";
import { PuzzleMenuState } from "./states/puzzle_menu";
import { ClientAPI } from "./platform/api";
import { LoginState } from "./states/login";
import { WegameSplashState } from "./states/wegame_splash";
import { MODS } from "./mods/modloader";
import { MOD_SIGNALS } from "./mods/mod_signals";
import { ModsState } from "./states/mods";
export type AchievementProviderInterface = import("./platform/achievement_provider").AchievementProviderInterface;
export type SoundInterface = import("./platform/sound").SoundInterface;
export type StorageInterface = import("./platform/storage").StorageInterface;
const logger: any = createLogger("application");
// Set the name of the hidden property and the change event for visibility
let pageHiddenPropName: any, pageVisibilityEventName: any;
if (typeof document.hidden !== "undefined") {
// Opera 12.10 and Firefox 18 and later support
pageHiddenPropName = "hidden";
pageVisibilityEventName = "visibilitychange";
// @ts-ignore
}
else if (typeof document.msHidden !== "undefined") {
pageHiddenPropName = "msHidden";
pageVisibilityEventName = "msvisibilitychange";
// @ts-ignore
}
else if (typeof document.webkitHidden !== "undefined") {
pageHiddenPropName = "webkitHidden";
pageVisibilityEventName = "webkitvisibilitychange";
}
export class Application {
/**
* Boots the application
*/
async boot(): any {
console.log("Booting ...");
assert(!GLOBAL_APP, "Tried to construct application twice");
logger.log("Creating application, platform =", getPlatformName());
setGlobalApp(this);
MODS.app = this;
// MODS
try {
await MODS.initMods();
}
catch (ex: any) {
alert("Failed to load mods (launch with --dev for more info): \n\n" + ex);
}
this.unloaded = false;
// Global stuff
this.settings = new ApplicationSettings(this);
this.ticker = new AnimationFrame();
this.stateMgr = new StateManager(this);
this.savegameMgr = new SavegameManager(this);
this.inputMgr = new InputDistributor(this);
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
this.clientApi = new ClientAPI(this);
// Restrictions (Like demo etc)
this.restrictionMgr = new RestrictionManager(this);
// Platform dependent stuff
this.storage = null;
this.sound = null;
this.platformWrapper = null;
this.achievementProvider = null;
this.adProvider = null;
this.analytics = null;
this.gameAnalytics = null;
this.initPlatformDependentInstances();
// Track if the window is focused (only relevant for browser)
this.focused = true;
// Track if the window is visible
this.pageVisible = true;
// Track if the app is paused (cordova)
this.applicationPaused = false;
this.trackedIsRenderable = new TrackedState(this.onAppRenderableStateChanged, this);
this.trackedIsPlaying = new TrackedState(this.onAppPlayingStateChanged, this);
// Dimensions
this.screenWidth = 0;
this.screenHeight = 0;
// Store the timestamp where we last checked for a screen resize, since orientationchange is unreliable with cordova
this.lastResizeCheck = null;
// Store the mouse position, or null if not available
this.mousePosition = null;
this.registerStates();
this.registerEventListeners();
Loader.linkAppAfterBoot(this);
// Check for mobile
if (IS_MOBILE) {
this.stateMgr.moveToState("MobileWarningState");
}
else {
this.stateMgr.moveToState("PreloadState");
}
// Starting rendering
this.ticker.frameEmitted.add(this.onFrameEmitted, this);
this.ticker.bgFrameEmitted.add(this.onBackgroundFrame, this);
this.ticker.start();
window.focus();
MOD_SIGNALS.appBooted.dispatch();
}
/**
* Initializes all platform instances
*/
initPlatformDependentInstances(): any {
logger.log("Creating platform dependent instances (standalone=", G_IS_STANDALONE, ")");
if (G_IS_STANDALONE) {
this.platformWrapper = new PlatformWrapperImplElectron(this);
}
else {
this.platformWrapper = new PlatformWrapperImplBrowser(this);
}
// Start with empty ad provider
this.adProvider = new NoAdProvider(this);
this.sound = new SoundImplBrowser(this);
this.analytics = new GoogleAnalyticsImpl(this);
this.gameAnalytics = new ShapezGameAnalytics(this);
this.achievementProvider = new NoAchievementProvider(this);
}
/**
* Registers all game states
*/
registerStates(): any {
const states: Array<typeof GameState> = [
WegameSplashState,
PreloadState,
MobileWarningState,
MainMenuState,
InGameState,
SettingsState,
KeybindingsState,
AboutState,
ChangelogState,
PuzzleMenuState,
LoginState,
ModsState,
];
for (let i: any = 0; i < states.length; ++i) {
this.stateMgr.register(states[i]);
}
}
/**
* Registers all event listeners
*/
registerEventListeners(): any {
window.addEventListener("focus", this.onFocus.bind(this));
window.addEventListener("blur", this.onBlur.bind(this));
window.addEventListener("resize", (): any => this.checkResize(), true);
window.addEventListener("orientationchange", (): any => this.checkResize(), true);
window.addEventListener("mousemove", this.handleMousemove.bind(this));
window.addEventListener("mouseout", this.handleMousemove.bind(this));
window.addEventListener("mouseover", this.handleMousemove.bind(this));
window.addEventListener("mouseleave", this.handleMousemove.bind(this));
// Unload events
window.addEventListener("beforeunload", this.onBeforeUnload.bind(this), true);
document.addEventListener(pageVisibilityEventName, this.handleVisibilityChange.bind(this), false);
// Track touches so we can update the focus appropriately
document.addEventListener("touchstart", this.updateFocusAfterUserInteraction.bind(this), true);
document.addEventListener("touchend", this.updateFocusAfterUserInteraction.bind(this), true);
}
/**
* Checks the focus after a touch
*/
updateFocusAfterUserInteraction(event: TouchEvent): any {
const target: any = (event.target as HTMLElement);
if (!target || !target.tagName) {
// Safety check
logger.warn("Invalid touchstart/touchend event:", event);
return;
}
// When clicking an element which is not the currently focused one, defocus it
if (target !== document.activeElement) {
// @ts-ignore
if (document.activeElement.blur) {
// @ts-ignore
document.activeElement.blur();
}
}
// If we click an input field, focus it now
if (target.tagName.toLowerCase() === "input") {
// We *really* need the focus
waitNextFrame().then((): any => target.focus());
}
}
/**
* Handles a page visibility change event
*/
handleVisibilityChange(event: Event): any {
window.focus();
const pageVisible: any = !document[pageHiddenPropName];
if (pageVisible !== this.pageVisible) {
this.pageVisible = pageVisible;
logger.log("Visibility changed:", this.pageVisible);
this.trackedIsRenderable.set(this.isRenderable());
}
}
/**
* Handles a mouse move event
*/
handleMousemove(event: MouseEvent): any {
this.mousePosition = new Vector(event.clientX, event.clientY);
}
/**
* Internal on focus handler
*/
onFocus(): any {
this.focused = true;
}
/**
* Internal blur handler
*/
onBlur(): any {
this.focused = false;
}
/**
* Returns if the app is currently visible
*/
isRenderable(): any {
return !this.applicationPaused && this.pageVisible;
}
onAppRenderableStateChanged(renderable: any): any {
logger.log("Application renderable:", renderable);
window.focus();
const currentState: any = this.stateMgr.getCurrentState();
if (!renderable) {
if (currentState) {
currentState.onAppPause();
}
}
else {
if (currentState) {
currentState.onAppResume();
}
this.checkResize();
}
this.sound.onPageRenderableStateChanged(renderable);
}
onAppPlayingStateChanged(playing: any): any {
try {
this.adProvider.setPlayStatus(playing);
}
catch (ex: any) {
console.warn("Play status changed");
}
}
/**
* Internal before-unload handler
*/
onBeforeUnload(event: any): any {
logSection("BEFORE UNLOAD HANDLER", "#f77");
const currentState: any = this.stateMgr.getCurrentState();
if (!G_IS_DEV && currentState && currentState.getHasUnloadConfirmation()) {
if (!G_IS_STANDALONE) {
// Need to show a "Are you sure you want to exit"
event.preventDefault();
event.returnValue = "Are you sure you want to exit?";
}
}
}
/**
* Deinitializes the application
*/
deinitialize(): any {
return this.sound.deinitialize();
}
/**
* Background frame update callback
*/
onBackgroundFrame(dt: number): any {
if (this.isRenderable()) {
return;
}
const currentState: any = this.stateMgr.getCurrentState();
if (currentState) {
currentState.onBackgroundTick(dt);
}
}
/**
* Frame update callback
*/
onFrameEmitted(dt: number): any {
if (!this.isRenderable()) {
return;
}
const time: any = performance.now();
// Periodically check for resizes, this is expensive (takes 2-3ms so only do it once in a while!)
if (!this.lastResizeCheck || time - this.lastResizeCheck > 1000) {
this.checkResize();
this.lastResizeCheck = time;
}
const currentState: any = this.stateMgr.getCurrentState();
this.trackedIsPlaying.set(currentState && currentState.getIsIngame());
if (currentState) {
currentState.onRender(dt);
}
}
/**
* Checks if the app resized. Only does this once in a while
*/
checkResize(forceUpdate: boolean = false): any {
const w: any = window.innerWidth;
const h: any = window.innerHeight;
if (this.screenWidth !== w || this.screenHeight !== h || forceUpdate) {
this.screenWidth = w;
this.screenHeight = h;
const currentState: any = this.stateMgr.getCurrentState();
if (currentState) {
currentState.onResized(this.screenWidth, this.screenHeight);
}
const scale: any = this.getEffectiveUiScale();
waitNextFrame().then((): any => document.documentElement.style.setProperty("--ui-scale", `${scale}`));
window.focus();
}
}
/**
* Returns the effective ui sclae
*/
getEffectiveUiScale(): any {
return this.platformWrapper.getUiScale() * this.settings.getInterfaceScaleValue();
}
/**
* Callback after ui scale has changed
*/
updateAfterUiScaleChanged(): any {
this.checkResize(true);
}
}

@ -0,0 +1,423 @@
export const CHANGELOG: any = [
{
version: "1.5.6",
date: "09.12.2022",
entries: [
"⚠️ We are currently prototyping Shapez 2! <a href='https://tobspr.io/shapez-2?utm_medium=s1_steam' target='_blank'>Click here</a> to find out more. ⚠️ ",
"Minor fixes & improvements",
"Updated translations",
],
},
{
version: "1.5.5",
date: "20.06.2022",
entries: [
"You can now play the full version in your browser! <a href='https://store.steampowered.com/news/app/1318690/view/5974602444932155405' target='_blank'>Click here</a> to read all details.",
"Reworked the tutorial to be simpler and more interactive",
"General polishing",
"Fix being unable to delete savegame when the savegame file was deleted externally",
"New sfx when unlocking upgrades",
"Updated translations",
],
},
{
version: "1.5.3",
date: "05.06.2022",
entries: [
"Fixed buildings not being lockable in the Puzzle DLC Editor",
"Fixed issues launching the game with proton",
"Updated translations",
],
},
{
version: "1.5.2",
date: "02.06.2022",
entries: [
"Attempted to fix the 'vram glitch', where the map background would not redraw anymore, especially in fullscreen. If the issue still persists, please let me know in the discord server!",
"The game has been renamed from 'shapez.io' to 'shapez', since it is not really an .io game",
"Various performance improvements",
"Upgrades should now show the full precision",
"UI Polishing & Cleanup",
"Updated translations",
"PS: We are already working on shapez 2, more information will follow in the <a href='https://discord.com/invite/HN7EVzV' target='_blank'>discord</a> soon!",
],
},
{
version: "1.5.1",
date: "25.02.2022",
entries: [
"This version adds an official modloader! You can now load mods by extracting them and placing the .js file in the mods/ folder of the game.",
"Mods can be found <a href='https://shapez.mod.io'>here</a>",
"When holding shift while placing a belt, the indicator now becomes red when crossing buildings",
"Lots of performance improvements, leading to up to 50% more FPS",
],
},
{
version: "1.4.4",
date: "29.08.2021",
entries: [
"Hotfix: Fixed the balancer not distributing items evenly, caused by the 1.4.3 update. Sorry for any inconveniences!",
],
},
{
version: "1.4.3",
date: "28.08.2021",
entries: [
"You can now hold 'ALT' while hovering a building to see its output! (Thanks to Sense101) (PS: There is now a setting to have it always on!)",
"The map overview should now be much more performant! As a consequence, you can now zoom out farther! (Thanks to PFedak)",
"Puzzle DLC: There is now a 'next puzzle' button!",
"Puzzle DLC: There is now a search function!",
"Edit signal dialog now has the previous signal filled (Thanks to EmeraldBlock)",
"Further performance improvements (Thanks to PFedak)",
"Improved puzzle validation (Thanks to Sense101)",
"Input fields in dialogs should now automatically focus",
"Fix selected building being deselected at level up (Thanks to EmeraldBlock)",
"Updated translations",
],
},
{
version: "1.4.2",
date: "24.06.2021",
entries: [
"Puzzle DLC: Goal acceptors now reset after getting no items for a while (This should prevent being able to 'cheat' puzzles) (by Sense101)",
"Puzzle DLC: Added button to clear all buildings / reset the puzzle (by Sense101)",
"Puzzle DLC: Allow copy-paste in puzzle mode (by Sense101)",
"Fixed level achievements being given on the wrong level (by DJ1TJOO)",
"Fixed blueprint not properly clearing on right click",
"Updated translations",
],
},
{
version: "1.4.1",
date: "22.06.2021",
entries: [
"The <a href='https://store.steampowered.com/app/1625400/shapezio__Puzzle_DLC/?utm_medium=changelog' target='_blank'>Puzzle DLC</a> is now available on Steam!",
"The <a href='https://store.steampowered.com/app/1573840/shapezio_Soundtrack/?utm_medium=changelog' target='_blank'>Soundtrack</a> is now also available to wishlist and will be released within the next days, including the new music from the Puzzle DLC!",
],
},
{
version: "1.4.0",
date: "04.06.2021",
entries: [
"Belts in blueprints should now always paste correctly",
"You can now clear belts by selecting them and then pressing 'B'",
"Preparations for the <a href='https://store.steampowered.com/app/1625400/shapezio__Puzzle_DLC/?utm_medium=changelog2' target='_blank'>Puzzle DLC</a>, coming June 22nd!",
],
},
{
version: "1.3.0",
date: "12.03.2020",
skin: "achievements",
entries: [
"There are now <strong>45 Steam Achievements!</strong>",
"Fixed constant signals being editable from the regular layer",
"Fixed items still overlapping sometimes between buildings and belts",
"The game is now available in finnish, italian, romanian and ukrainian! (Thanks to all contributors!)",
"Updated translations (Thanks to all contributors!)",
],
},
{
version: "1.2.2",
date: "07.12.2020",
entries: [
"Fix item readers and some other buildings slowing up belts, especially if they stalled (inspired by Keterr's fix)",
"Added the ability to edit constant signals by left clicking them",
"Prevent items from being rendered on each other when a belt stalls (inspired by Keterr)",
"You can now add markers in the wire layer (partially by daanbreur)",
"Allow to cycle backwards in the toolbar with SHIFT + Tab (idea by EmeraldBlock)",
"Allow to cycle variants backwards with SHIFT + T",
"Upgrade numbers now use roman numerals until tier 50 (by LeopoldTal)",
"Add button to unpin shapes from the left side (by artemisSystem)",
"Fix middle mouse button also placing blueprints (by Eiim)",
"Hide wires grid when using the 'Disable Grid' setting (by EmeraldBlock)",
"Fix UI using multiple different save icons",
"Updated translations (Thanks to all contributors!)",
],
},
{
version: "1.2.1",
date: "31.10.2020",
entries: [
"Fixed stacking bug for level 26 which required restarting the game",
"Fix reward notification being too long sometimes (by LeopoldTal)",
"Use locale decimal separator on belt reader display (by LeopoldTal)",
"Vastly improved performance when saving games (by LeopoldTal)",
"Prevent some antivirus programs blocking the opening of external links (by LeopoldTal)",
"Match tutorials to the correct painter variants (by LeopoldTal)",
"Prevent throughput goals containing fractional numbers (by CEbbinghaus)",
"Updated translations and added Hungarian",
],
},
{
version: "1.2.0",
date: "09.10.2020",
entries: [
"⚠This update is HUGE, view the full changelog <a href='https://shapez.io/wires/' target='_blank'>here</a>! ⚠️⚠️",
],
},
{
version: "1.1.18",
date: "27.06.2020",
entries: [
"Huge performance improvements - up to double fps and tick-rate! This will wipe out all current items on belts.",
"Reduce story shapes required until unlocking blueprints",
"Allow clicking on variants to select them",
"Add 'copy key' button to shape viewer",
"Add more FPS to the belt animation and fix belt animation seeming to go 'backwards' on high belt speeds",
"Fix deconstruct sound being played when right clicking hub",
"Allow clicking 'Q' over a shape or color patch to automatically select the miner building (by Gerdon262)",
"Update belt placement performance on huge factories (by Phlosioneer)",
"Fix duplicate waypoints with a shape not rendering (by hexy)",
"Fix smart tunnel placement deleting wrong tunnels (by mordof)",
"Add setting (on by default) to store the last used rotation per building instead of globally storing it (by Magos)",
"Added chinese (traditional) translation",
"Updated translations",
],
},
{
version: "1.1.17",
date: "22.06.2020",
entries: [
"Color blind mode! You can now activate it in the settings and it will show you which color is below your cursor (Either resource or on the belt)",
"Add info buttons to all shapes so you can figure out how they are built! (And also, which colors they have)",
"Allow configuring autosave interval and disabling it in the settings",
"The smart-tunnel placement has been reworked to properly replace belts. Thus the setting has been turned on again by default",
"The soundtrack now has a higher quality on the standalone version than the web version",
"Add setting to disable cut/delete warnings (by hexy)",
"Fix bug where belts in blueprints don't orient correctly (by hexy)",
"Fix camera moving weird after dragging and holding (by hexy)",
"Fix keybinding for pipette showing while pasting blueprints",
"Improve visibility of shape background in dark mode",
"Added sound when destroying a building",
"Added swedish translation",
"Update tutorial image for tier 2 tunnels to explain mix/match (by jimmyshadow1)",
],
},
{
version: "1.1.16",
date: "21.06.2020",
entries: [
"You can now pickup buildings below your cursor with 'Q'!",
"The game soundtrack has been extended! There are now 4 songs with over 13 minutes of playtime from <a href='https://soundcloud.com/pettersumelius' target='blank'>Peppsen</a>!",
"Refactor keybindings overlay to show more appropriate keybindings",
"Show keybindings for area-select in the upper left instead",
"Automatically deselect area when selecting a new building",
"Raise markers limit from 14 characters to 71 (by Joker-vD)",
"Optimize performance by caching extractor items (by Phlosioneer)",
"Added setting to enable compact building infos, which only show ratios and hide the image / description",
"Apply dark theme to menu as well (by dengr1065)",
"Fix belt planner not placing the last belt",
"Fix buildings getting deleted when right clicking while placing a blueprint",
"Fix for exporting screenshots for huge bases (It was showing an empty file) (by xSparfuchs)",
"Fix buttons not responding when using right click directly after left click (by davidburhans)",
"Fix hub marker being hidden by building info panel",
"Disable dialog background blur since it can cause performance issues",
"Added simplified chinese translations",
"Update translations (Thanks to all translators!)",
],
},
{
version: "1.1.15",
date: "17.06.2020",
entries: [
"You can now place straight belts (and tunnels) by holding SHIFT! (For you, @giantwaffle ❤️)",
"Added continue button to main menu and add separate 'New game' button (by jaysc)",
"Added setting to disable smart tunnel placement introduced with the last update",
"Added setting to disable vignette",
"Update translations",
],
},
{
version: "1.1.14",
date: "16.06.2020",
entries: [
"There is now an indicator (compass) to the HUB for the HUB Marker!",
"You can now include shape short keys in markers to render shape icons instead of text!",
"Added mirrored variant of the painter",
"When placing tunnels, unnecessary belts inbetween are now removed!",
"You can now drag tunnels and they will automatically expand! (Just try it out, its intuitive)",
],
},
{
version: "1.1.13",
date: "15.06.2020",
entries: [
"Added shift modifier for faster pan (by jaysc)",
"Added Japanese translations",
"Added Portuguese (Portugal) translations",
"Updated icon for Spanish (Latin America) - It was showing a Spanish flag before",
"Updated existing translations",
],
},
{
version: "1.1.12",
date: "14.06.2020",
entries: [
"Huge performance improvements! The game should now run up to 60% faster!",
"Added norwegian translation",
],
},
{
version: "1.1.11",
date: "13.06.2020",
entries: [
"Pinned shapes are now smart, they dynamically update their goal and also unpin when no longer required. Completed objectives are now rendered transparent.",
"You can now cut areas, and also paste the last blueprint again! (by hexy)",
"You can now export your whole base as an image by pressing F3!",
"Improve upgrade number rounding, so there are no goals like '37.4k', instead it will now be '35k'",
"You can now configure the camera movement speed when using WASD (by mini-bomba)",
"Selecting an area now is relative to the world and thus does not move when moving the screen (by Dimava)",
"Allow higher tick-rates up to 500hz (This will burn your PC!)",
"Fix bug regarding number rounding",
"Fix dialog text being hardly readable in dark theme",
"Fix app not starting when the savegames were corrupted - there is now a better error message as well.",
"Further translation updates - Big thanks to all contributors!",
],
},
{
version: "1.1.10",
date: "12.06.2020",
entries: [
"There are now linux builds on steam! Please report any issues in the Discord!",
"Steam cloud saves are now available!",
"Added and update more translations (Big thank you to all translators!)",
"Prevent invalid connection if existing underground tunnel entrance exists (by jaysc)",
],
},
{
version: "1.1.9",
date: "11.06.2020",
entries: [
"Support for translations! Interested in helping out? Check out the <a target='_blank' href='https://github.com/tobspr/shapez.io/tree/master/translations'>translation guide</a>!",
"Update stacker artwork to clarify how it works",
"Update keybinding hints on the top left to be more accurate",
"Make it more clear when blueprints are unlocked when trying to use them",
"Fix pinned shape icons not being visible in dark mode",
"Fix being able to select buildings via hotkeys in map overview mode",
"Make shapes unpinnable in the upgrades tab (By hexy)",
],
},
{
version: "1.1.8",
date: "07.06.2020",
entries: [
"You can now purchase the standalone on steam! <a href='https://steam.shapez.io' target='blank'>View steam page</a>",
"Added ability to create markers in the demo, but only two.",
"Contest #01 has ended! I'll now work through the entries, select the 5 I like most and present them to the community to vote for!",
],
},
{
version: "1.1.7",
date: "04.06.2020",
entries: ["HOTFIX: Fix savegames not showing up on the standalone version"],
},
{
version: "1.1.6",
date: "04.06.2020",
entries: [
"The steam release will happen on the <strong>7th of June</strong> - Be sure to add it to your wishlist! <a href='https://steam.shapez.io' target='blank'>View on steam</a>",
"Fixed level complete dialog being blurred when the shop was opened before",
"Standalone: Increased icon visibility for windows builds",
"Web version: Fixed firefox not loading the game when browsing in private mode",
],
},
{
version: "1.1.5",
date: "03.06.2020",
entries: ["Added weekly contests!"],
},
{
version: "1.1.4",
date: "01.06.2020",
entries: ["Add 'interactive' tutorial for the first level to improve onboarding experience"],
},
{
version: "1.1.3",
date: "01.06.2020",
entries: [
"Added setting to configure zoom / mouse wheel / touchpad sensitivity",
"Fix belts being too slow when copied via blueprint (by Dimava)",
"Allow binding mouse buttons to actions (by Dimava)",
"Increase readability of certain HUD elements",
],
},
{
version: "1.1.2",
date: "30.05.2020",
entries: [
"The official trailer is now ready! Check it out <a href='https://www.youtube.com/watch?v=KyorY1uIqiQ' target='_blank'>here</a>!",
"The <a href='https://steam.shapez.io' target='_blank'>steam page</a> is now live!",
"Experimental linux builds are now available! Please give me feedback on them in the Discord",
"Allow hovering pinned shapes to enlarge them",
"Allow deselecting blueprints with right click and 'Q'",
"Move default key for deleting from 'X' to 'DEL'",
"Show confirmation when deleting more than 100 buildings",
"Reintroduce 'SPACE' keybinding to center on map",
"Improved keybinding hints",
"Fixed some keybindings showing as 'undefined'",
],
},
{
version: "1.1.1",
date: "28.05.2020",
entries: ["Fix crash when 'Show Hints' setting was turned off"],
},
{
version: "1.1.0",
date: "28.05.2020",
entries: [
"BLUEPRINTS! They are unlocked at level 12 and cost a special shape to build.",
"MAP MARKERS! Press 'M' to create a waypoint and be able to jump to it",
"Savegame levels are now shown in the main menu. For existing games, save them again to make the level show up.",
"Allow holding SHIFT to rotate counter clockwise",
"Added confirmation when deleting more than 500 buildings at a time",
"Added background to toolbar to increase contrast",
"Further decerase requirements of first levels",
"Pinned shapes now are saved",
"Allow placing extractors anywhere again, but they don't work at all if not placed on a resource",
"Show dialog explaining some keybindings after completing level 4",
"Fix keys being stuck when opening a dialog",
"Swapped shape order for painting upgrades",
"Allow changing all keybindings, including CTRL, ALT and SHIFT (by Dimava)",
"Fix cycling through keybindings selecting locked buildings as well (by Dimava)",
"There is now a github action, checking all pull requests with eslint. (by mrHedgehog)",
],
},
{
version: "1.0.4",
date: "26.05.2020",
entries: [
"Reduce cost of first painting upgrade, and change 'Shape Processing' to 'Cutting, Rotating & Stacking'",
"Add dialog after completing level 2 to check out the upgrades tab.",
"Allow changing the keybindings in the demo version",
],
},
{
version: "1.0.3",
date: "24.05.2020",
entries: [
"Reduced the amount of shapes required for the first 5 levels to make it easier to get into the game.",
],
},
{
version: "1.0.2",
date: "23.05.2020",
entries: [
"Introduced changelog",
"Removed 'early access' label because the game isn't actually early access - its in a pretty good state already! (No worries, a lot more updates will follow!)",
"Added a 'Show hint' button which shows a small video for almost all levels to help out",
"Now showing proper descriptions when completing levels, with instructions on what the gained reward does.",
"Show a landing page on mobile devices about the game not being ready to be played on mobile yet",
"Fix painters and mixers being affected by the shape processors upgrade and not the painter one",
"Added 'multiplace' setting which is equivalent to holding SHIFT all the time",
"Added keybindings to zoom in / zoom out",
"Tunnels now also show connection lines to tunnel exits, instead of just tunnel entries",
"Lots of minor fixes and improvements",
],
},
{
version: "1.0.1",
date: "21.05.2020",
entries: ["Initial release!"],
},
];

@ -0,0 +1,49 @@
import { Signal } from "./signal";
// @ts-ignore
import BackgroundAnimationFrameEmitterWorker from "../webworkers/background_animation_frame_emittter.worker";
import { createLogger } from "./logging";
const logger: any = createLogger("animation_frame");
const maxDtMs: any = 1000;
const resetDtMs: any = 16;
export class AnimationFrame {
public frameEmitted = new Signal();
public bgFrameEmitted = new Signal();
public lastTime = performance.now();
public bgLastTime = performance.now();
public boundMethod = this.handleAnimationFrame.bind(this);
public backgroundWorker = new BackgroundAnimationFrameEmitterWorker();
constructor() {
this.backgroundWorker.addEventListener("error", (err: any): any => {
logger.error("Error in background fps worker:", err);
});
this.backgroundWorker.addEventListener("message", this.handleBackgroundTick.bind(this));
}
handleBackgroundTick(): any {
const time: any = performance.now();
let dt: any = time - this.bgLastTime;
if (dt > maxDtMs) {
dt = resetDtMs;
}
this.bgFrameEmitted.dispatch(dt);
this.bgLastTime = time;
}
start(): any {
assertAlways(window.requestAnimationFrame, "requestAnimationFrame is not supported!");
this.handleAnimationFrame();
}
handleAnimationFrame(time: any): any {
let dt: any = time - this.lastTime;
if (dt > maxDtMs) {
dt = resetDtMs;
}
try {
this.frameEmitted.dispatch(dt);
}
catch (ex: any) {
console.error(ex);
}
this.lastTime = time;
window.requestAnimationFrame(this.boundMethod);
}
}

@ -0,0 +1,21 @@
import { createLogger } from "./logging";
const logger: any = createLogger("assert");
let assertionErrorShown: any = false;
function initAssert(): any {
/**
* Expects a given condition to be true
* @param {} failureMessage
*/
// @ts-ignore
window.assert = function (condition: Boolean, ...failureMessage: ...String): any {
if (!condition) {
logger.error("assertion failed:", ...failureMessage);
if (!assertionErrorShown) {
// alert("Assertion failed (the game will try to continue to run): \n\n" + failureMessage);
assertionErrorShown = true;
}
throw new Error("AssertionError: " + failureMessage.join(" "));
}
};
}
initAssert();

@ -0,0 +1,95 @@
// @ts-ignore
import CompressionWorker from "../webworkers/compression.worker";
import { createLogger } from "./logging";
import { round2Digits } from "./utils";
const logger: any = createLogger("async_compression");
export let compressionPrefix: any = String.fromCodePoint(1);
function checkCryptPrefix(prefix: any): any {
try {
window.localStorage.setItem("prefix_test", prefix);
window.localStorage.removeItem("prefix_test");
return true;
}
catch (ex: any) {
logger.warn("Prefix '" + prefix + "' not available");
return false;
}
}
if (!checkCryptPrefix(compressionPrefix)) {
logger.warn("Switching to basic prefix");
compressionPrefix = " ";
if (!checkCryptPrefix(compressionPrefix)) {
logger.warn("Prefix not available, ls seems to be unavailable");
}
}
export type JobEntry = {
errorHandler: function(: void):void;
resolver: function(: void):void;
startTime: number;
};
class AsynCompression {
public worker = new CompressionWorker();
public currentJobId = 1000;
public currentJobs: {
[idx: number]: JobEntry;
} = {};
constructor() {
this.worker.addEventListener("message", (event: any): any => {
const { jobId, result }: any = event.data;
const jobData: any = this.currentJobs[jobId];
if (!jobData) {
logger.error("Failed to resolve job result, job id", jobId, "is not known");
return;
}
const duration: any = performance.now() - jobData.startTime;
logger.log("Got job", jobId, "response within", round2Digits(duration), "ms: ", result.length, "bytes");
const resolver: any = jobData.resolver;
delete this.currentJobs[jobId];
resolver(result);
});
this.worker.addEventListener("error", (err: any): any => {
logger.error("Got error from webworker:", err, "aborting all jobs");
const failureCalls: any = [];
for (const jobId: any in this.currentJobs) {
failureCalls.push(this.currentJobs[jobId].errorHandler);
}
this.currentJobs = {};
for (let i: any = 0; i < failureCalls.length; ++i) {
failureCalls[i](err);
}
});
}
/**
* Compresses any object
*/
compressObjectAsync(obj: any): any {
logger.log("Compressing object async (optimized)");
return this.internalQueueJob("compressObject", {
obj,
compressionPrefix,
});
}
/**
* Queues a new job
* {}
*/
internalQueueJob(job: string, data: any): Promise<any> {
const jobId: any = ++this.currentJobId;
return new Promise((resolve: any, reject: any): any => {
const errorHandler: any = (err: any): any => {
logger.error("Failed to compress job", jobId, ":", err);
reject(err);
};
this.currentJobs[jobId] = {
errorHandler,
resolver: resolve,
startTime: performance.now(),
};
logger.log("Posting job", job, "/", jobId);
this.worker.postMessage({ jobId, job, data });
});
}
}
export const asyncCompressor: any = new AsynCompression();

@ -0,0 +1,47 @@
export type Size = {
w: number;
h: number;
};
export type Position = {
x: number;
y: number;
};
export type SpriteDefinition = {
frame: Position & Size;
rotated: boolean;
spriteSourceSize: Position & Size;
sourceSize: Size;
trimmed: boolean;
};
export type AtlasMeta = {
app: string;
version: string;
image: string;
format: string;
size: Size;
scale: string;
smartupdate: string;
};
export type SourceData = {
frames: Object<string, SpriteDefinition>;
meta: AtlasMeta;
};
export class AtlasDefinition {
public meta = meta;
public sourceData = frames;
public sourceFileName = meta.image;
constructor({ frames, meta }) {
}
getFullSourcePath(): any {
return this.sourceFileName;
}
}
export const atlasFiles: AtlasDefinition[] = require
// @ts-ignore
.context("../../../res_built/atlas/", false, /.*\.json/i)
.keys()
.map((f: any): any => f.replace(/^\.\//gi, ""))
.map((f: any): any => require("../../../res_built/atlas/" + f))
.map((data: any): any => new AtlasDefinition(data));

@ -0,0 +1,192 @@
/* typehints:start */
import type { Application } from "../application";
/* typehints:end */
import { initSpriteCache } from "../game/meta_building_registry";
import { MUSIC, SOUNDS } from "../platform/sound";
import { T } from "../translations";
import { AtlasDefinition, atlasFiles } from "./atlas_definitions";
import { cachebust } from "./cachebust";
import { Loader } from "./loader";
import { createLogger } from "./logging";
import { Signal } from "./signal";
import { clamp, getLogoSprite, timeoutPromise } from "./utils";
const logger: any = createLogger("background_loader");
const MAIN_MENU_ASSETS: any = {
sprites: [getLogoSprite()],
sounds: [SOUNDS.uiClick, SOUNDS.uiError, SOUNDS.dialogError, SOUNDS.dialogOk],
atlas: [],
css: [],
};
const INGAME_ASSETS: any = {
sprites: [],
sounds: [
...Array.from(Object.values(MUSIC)),
...Array.from(Object.values(SOUNDS)).filter((sound: any): any => !MAIN_MENU_ASSETS.sounds.includes(sound)),
],
atlas: atlasFiles,
css: ["async-resources.css"],
};
if (G_IS_STANDALONE) {
MAIN_MENU_ASSETS.sounds = [...Array.from(Object.values(MUSIC)), ...Array.from(Object.values(SOUNDS))];
INGAME_ASSETS.sounds = [];
}
const LOADER_TIMEOUT_PER_RESOURCE: any = 180000;
// Cloudflare does not send content-length headers with brotli compression,
// so store the actual (compressed) file sizes so we can show a progress bar.
const HARDCODED_FILE_SIZES: any = {
"async-resources.css": 2216145,
};
export class BackgroundResourcesLoader {
public app = app;
public mainMenuPromise = null;
public ingamePromise = null;
public resourceStateChangedSignal = new Signal();
constructor(app) {
}
getMainMenuPromise(): any {
if (this.mainMenuPromise) {
return this.mainMenuPromise;
}
logger.log("⏰ Loading main menu assets");
return (this.mainMenuPromise = this.loadAssets(MAIN_MENU_ASSETS));
}
getIngamePromise(): any {
if (this.ingamePromise) {
return this.ingamePromise;
}
logger.log("⏰ Loading ingame assets");
const promise: any = this.loadAssets(INGAME_ASSETS).then((): any => initSpriteCache());
return (this.ingamePromise = promise);
}
async loadAssets({ sprites, sounds, atlas, css }: {
sprites: string[];
sounds: string[];
atlas: AtlasDefinition[];
css: string[];
}): any {
let promiseFunctions: ((progressHandler: (progress: number) => void) => Promise<void>)[] = [];
// CSS
for (let i: any = 0; i < css.length; ++i) {
promiseFunctions.push((progress: any): any => timeoutPromise(this.internalPreloadCss(css[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch((err: any): any => {
logger.error("Failed to load css:", css[i], err);
throw new Error("HUD Stylesheet " + css[i] + " failed to load: " + err);
}));
}
// ATLAS FILES
for (let i: any = 0; i < atlas.length; ++i) {
promiseFunctions.push((progress: any): any => timeoutPromise(Loader.preloadAtlas(atlas[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch((err: any): any => {
logger.error("Failed to load atlas:", atlas[i].sourceFileName, err);
throw new Error("Atlas " + atlas[i].sourceFileName + " failed to load: " + err);
}));
}
// HUD Sprites
for (let i: any = 0; i < sprites.length; ++i) {
promiseFunctions.push((progress: any): any => timeoutPromise(Loader.preloadCSSSprite(sprites[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch((err: any): any => {
logger.error("Failed to load css sprite:", sprites[i], err);
throw new Error("HUD Sprite " + sprites[i] + " failed to load: " + err);
}));
}
// SFX & Music
for (let i: any = 0; i < sounds.length; ++i) {
promiseFunctions.push((progress: any): any => timeoutPromise(this.app.sound.loadSound(sounds[i]), LOADER_TIMEOUT_PER_RESOURCE).catch((err: any): any => {
logger.warn("Failed to load sound, will not be available:", sounds[i], err);
}));
}
const originalAmount: any = promiseFunctions.length;
const start: any = performance.now();
logger.log("⏰ Preloading", originalAmount, "assets");
let progress: any = 0;
this.resourceStateChangedSignal.dispatch({ progress });
let promises: any = [];
for (let i: any = 0; i < promiseFunctions.length; i++) {
let lastIndividualProgress: any = 0;
const progressHandler: any = (individualProgress: any): any => {
const delta: any = clamp(individualProgress) - lastIndividualProgress;
lastIndividualProgress = clamp(individualProgress);
progress += delta / originalAmount;
this.resourceStateChangedSignal.dispatch({ progress });
};
promises.push(promiseFunctions[i](progressHandler).then((): any => {
progressHandler(1);
}));
}
await Promise.all(promises);
logger.log("⏰ Preloaded assets in", Math.round(performance.now() - start), "ms");
}
/**
* Shows an error when a resource failed to load and allows to reload the game
*/
showLoaderError(dialogs: any, err: any): any {
if (G_IS_STANDALONE) {
dialogs
.showWarning(T.dialogs.resourceLoadFailed.title, T.dialogs.resourceLoadFailed.descSteamDemo + "<br>" + err, ["retry"])
.retry.add((): any => window.location.reload());
}
else {
dialogs
.showWarning(T.dialogs.resourceLoadFailed.title, T.dialogs.resourceLoadFailed.descWeb.replace("<demoOnSteamLinkText>", `<a href="https://get.shapez.io/resource_timeout" target="_blank">${T.dialogs.resourceLoadFailed.demoLinkText}</a>`) +
"<br>" +
err, ["retry"])
.retry.add((): any => window.location.reload());
}
}
preloadWithProgress(src: any, progressHandler: any): any {
return new Promise((resolve: any, reject: any): any => {
const xhr: any = new XMLHttpRequest();
let notifiedNotComputable: any = false;
const fullUrl: any = cachebust(src);
xhr.open("GET", fullUrl, true);
xhr.responseType = "arraybuffer";
xhr.onprogress = function (ev: any): any {
if (ev.lengthComputable) {
progressHandler(ev.loaded / ev.total);
}
else {
if (window.location.search.includes("alwaysLogFileSize")) {
console.warn("Progress:", src, ev.loaded);
}
if (HARDCODED_FILE_SIZES[src]) {
progressHandler(clamp(ev.loaded / HARDCODED_FILE_SIZES[src]));
}
else {
if (!notifiedNotComputable) {
notifiedNotComputable = true;
console.warn("Progress not computable:", src, ev.loaded);
progressHandler(0);
}
}
}
};
xhr.onloadend = function (): any {
if (!xhr.status.toString().match(/^2/)) {
reject(fullUrl + ": " + xhr.status + " " + xhr.statusText);
}
else {
if (!notifiedNotComputable) {
progressHandler(1);
}
const options: any = {};
const headers: any = xhr.getAllResponseHeaders();
const contentType: any = headers.match(/^Content-Type:\s*(.*?)$/im);
if (contentType && contentType[1]) {
options.type = contentType[1].split(";")[0];
}
const blob: any = new Blob([this.response], options);
resolve(window.URL.createObjectURL(blob));
}
};
xhr.send();
});
}
internalPreloadCss(src: any, progressHandler: any): any {
return this.preloadWithProgress(src, progressHandler).then((blobSrc: any): any => {
var styleElement: any = document.createElement("link");
styleElement.href = blobSrc;
styleElement.rel = "stylesheet";
styleElement.setAttribute("media", "all");
styleElement.type = "text/css";
document.head.appendChild(styleElement);
});
}
}

@ -0,0 +1,161 @@
import { GameRoot } from "../game/root";
import { clearBufferBacklog, freeCanvas, getBufferStats, makeOffscreenBuffer } from "./buffer_utils";
import { createLogger } from "./logging";
import { round1Digit } from "./utils";
export type CacheEntry = {
canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D;
lastUse: number;
};
const logger: any = createLogger("buffers");
const bufferGcDurationSeconds: any = 0.5;
export class BufferMaintainer {
public root = root;
public cache: Map<string, Map<string, CacheEntry>> = new Map();
public iterationIndex = 1;
public lastIteration = 0;
constructor(root) {
this.root.signals.gameFrameStarted.add(this.update, this);
}
/**
* Returns the buffer stats
*/
getStats(): any {
let stats: any = {
rootKeys: 0,
subKeys: 0,
vramBytes: 0,
};
this.cache.forEach((subCache: any, key: any): any => {
++stats.rootKeys;
subCache.forEach((cacheEntry: any, subKey: any): any => {
++stats.subKeys;
const canvas: any = cacheEntry.canvas;
stats.vramBytes += canvas.width * canvas.height * 4;
});
});
return stats;
}
/**
* Goes to the next buffer iteration, clearing all buffers which were not used
* for a few iterations
*/
garbargeCollect(): any {
let totalKeys: any = 0;
let deletedKeys: any = 0;
const minIteration: any = this.iterationIndex;
this.cache.forEach((subCache: any, key: any): any => {
let unusedSubKeys: any = [];
// Filter sub cache
subCache.forEach((cacheEntry: any, subKey: any): any => {
if (cacheEntry.lastUse < minIteration ||
// @ts-ignore
cacheEntry.canvas._contextLost) {
unusedSubKeys.push(subKey);
freeCanvas(cacheEntry.canvas);
++deletedKeys;
}
else {
++totalKeys;
}
});
// Delete unused sub keys
for (let i: any = 0; i < unusedSubKeys.length; ++i) {
subCache.delete(unusedSubKeys[i]);
}
});
// Make sure our backlog never gets too big
clearBufferBacklog();
// if (G_IS_DEV) {
// const bufferStats = getBufferStats();
// const mbUsed = round1Digit(bufferStats.vramUsage / (1024 * 1024));
// logger.log(
// "GC: Remove",
// (deletedKeys + "").padStart(4),
// ", Remain",
// (totalKeys + "").padStart(4),
// "(",
// (bufferStats.bufferCount + "").padStart(4),
// "total",
// ")",
// "(",
// (bufferStats.backlogSize + "").padStart(4),
// "backlog",
// ")",
// "VRAM:",
// mbUsed,
// "MB"
// );
// }
++this.iterationIndex;
}
update(): any {
const now: any = this.root.time.realtimeNow();
if (now - this.lastIteration > bufferGcDurationSeconds) {
this.lastIteration = now;
this.garbargeCollect();
}
}
/**
* {}
*
*/
getForKey({ key, subKey, w, h, dpi, redrawMethod, additionalParams }: {
key: string;
subKey: string;
w: number;
h: number;
dpi: number;
redrawMethod: function(: void, : void, : void, : void, : void, : void):void;
additionalParams: object=;
}): HTMLCanvasElement {
// First, create parent key
let parent: any = this.cache.get(key);
if (!parent) {
parent = new Map();
this.cache.set(key, parent);
}
// Now search for sub key
const cacheHit: any = parent.get(subKey);
if (cacheHit) {
cacheHit.lastUse = this.iterationIndex;
return cacheHit.canvas;
}
// Need to generate new buffer
const effectiveWidth: any = w * dpi;
const effectiveHeight: any = h * dpi;
const [canvas, context]: any = makeOffscreenBuffer(effectiveWidth, effectiveHeight, {
reusable: true,
label: "buffer-" + key + "/" + subKey,
smooth: true,
});
redrawMethod(canvas, context, w, h, dpi, additionalParams);
parent.set(subKey, {
canvas,
context,
lastUse: this.iterationIndex,
});
return canvas;
}
/**
* {}
*
*/
getForKeyOrNullNoUpdate({ key, subKey }: {
key: string;
subKey: string;
}): ?HTMLCanvasElement {
let parent: any = this.cache.get(key);
if (!parent) {
return null;
}
// Now search for sub key
const cacheHit: any = parent.get(subKey);
if (cacheHit) {
return cacheHit.canvas;
}
return null;
}
}

@ -0,0 +1,195 @@
import { globalConfig } from "./config";
import { fastArrayDelete } from "./utils";
import { createLogger } from "./logging";
const logger: any = createLogger("buffer_utils");
/**
* Enables images smoothing on a context
*/
export function enableImageSmoothing(context: CanvasRenderingContext2D): any {
context.imageSmoothingEnabled = true;
context.webkitImageSmoothingEnabled = true;
// @ts-ignore
context.imageSmoothingQuality = globalConfig.smoothing.quality;
}
/**
* Disables image smoothing on a context
*/
export function disableImageSmoothing(context: CanvasRenderingContext2D): any {
context.imageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
}
export type CanvasCacheEntry = {
canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D;
};
const registeredCanvas: Array<CanvasCacheEntry> = [];
/**
* Buckets for each width * height combination
*/
const freeCanvasBuckets: Map<number, Array<CanvasCacheEntry>> = new Map();
/**
* Track statistics
*/
const stats: any = {
vramUsage: 0,
backlogVramUsage: 0,
bufferCount: 0,
numReused: 0,
numCreated: 0,
};
export function getBufferVramUsageBytes(canvas: HTMLCanvasElement): any {
assert(canvas, "no canvas given");
assert(Number.isFinite(canvas.width), "bad canvas width: " + canvas.width);
assert(Number.isFinite(canvas.height), "bad canvas height" + canvas.height);
return canvas.width * canvas.height * 4;
}
/**
* Returns stats on the allocated buffers
*/
export function getBufferStats(): any {
let numBuffersFree: any = 0;
freeCanvasBuckets.forEach((bucket: any): any => {
numBuffersFree += bucket.length;
});
return {
...stats,
backlogKeys: freeCanvasBuckets.size,
backlogSize: numBuffersFree,
};
}
/**
* Clears the backlog buffers if they grew too much
*/
export function clearBufferBacklog(): any {
freeCanvasBuckets.forEach((bucket: any): any => {
while (bucket.length > 500) {
const entry: any = bucket[bucket.length - 1];
stats.backlogVramUsage -= getBufferVramUsageBytes(entry.canvas);
delete entry.canvas;
delete entry.context;
bucket.pop();
}
});
}
/**
* Creates a new offscreen buffer
* {}
*/
export function makeOffscreenBuffer(w: Number, h: Number, { smooth = true, reusable = true, label = "buffer" }: any): [
HTMLCanvasElement,
CanvasRenderingContext2D
] {
assert(w > 0 && h > 0, "W or H < 0");
if (w % 1 !== 0 || h % 1 !== 0) {
// console.warn("Subpixel offscreen buffer size:", w, h);
}
if (w < 1 || h < 1) {
logger.error("Offscreen buffer size < 0:", w, "x", h);
w = Math.max(1, w);
h = Math.max(1, h);
}
const recommendedSize: any = 1024 * 1024;
if (w * h > recommendedSize) {
logger.warn("Creating huge buffer:", w, "x", h, "with label", label);
}
w = Math.floor(w);
h = Math.floor(h);
let canvas: any = null;
let context: any = null;
// Ok, search in cache first
const bucket: any = freeCanvasBuckets.get(w * h) || [];
for (let i: any = 0; i < bucket.length; ++i) {
const { canvas: useableCanvas, context: useableContext }: any = bucket[i];
if (useableCanvas.width === w && useableCanvas.height === h) {
// Ok we found one
canvas = useableCanvas;
context = useableContext;
// Restore past state
context.restore();
context.save();
context.clearRect(0, 0, canvas.width, canvas.height);
delete canvas.style.width;
delete canvas.style.height;
stats.numReused++;
stats.backlogVramUsage -= getBufferVramUsageBytes(canvas);
fastArrayDelete(bucket, i);
break;
}
}
// None found , create new one
if (!canvas) {
canvas = document.createElement("canvas");
context = canvas.getContext("2d" /*, { alpha } */);
stats.numCreated++;
canvas.width = w;
canvas.height = h;
// Initial state
context.save();
canvas.addEventListener("webglcontextlost", (): any => {
console.warn("canvas::webglcontextlost", canvas);
// @ts-ignore
canvas._contextLost = true;
});
canvas.addEventListener("contextlost", (): any => {
console.warn("canvas::contextlost", canvas);
// @ts-ignore
canvas._contextLost = true;
});
}
// @ts-ignore
canvas._contextLost = false;
// @ts-ignore
canvas.label = label;
if (smooth) {
enableImageSmoothing(context);
}
else {
disableImageSmoothing(context);
}
if (reusable) {
registerCanvas(canvas, context);
}
return [canvas, context];
}
/**
* Frees a canvas
*/
export function registerCanvas(canvas: HTMLCanvasElement, context: any): any {
registeredCanvas.push({ canvas, context });
stats.bufferCount += 1;
const bytesUsed: any = getBufferVramUsageBytes(canvas);
stats.vramUsage += bytesUsed;
}
/**
* Frees a canvas
*/
export function freeCanvas(canvas: HTMLCanvasElement): any {
assert(canvas, "Canvas is empty");
let index: any = -1;
let data: any = null;
for (let i: any = 0; i < registeredCanvas.length; ++i) {
if (registeredCanvas[i].canvas === canvas) {
index = i;
data = registeredCanvas[i];
break;
}
}
if (index < 0) {
logger.error("Tried to free unregistered canvas of size", canvas.width, canvas.height);
return;
}
fastArrayDelete(registeredCanvas, index);
const key: any = canvas.width * canvas.height;
const bucket: any = freeCanvasBuckets.get(key);
if (bucket) {
bucket.push(data);
}
else {
freeCanvasBuckets.set(key, [data]);
}
stats.bufferCount -= 1;
const bytesUsed: any = getBufferVramUsageBytes(canvas);
stats.vramUsage -= bytesUsed;
stats.backlogVramUsage += bytesUsed;
}

@ -0,0 +1,9 @@
/**
* Generates a cachebuster string. This only modifies the path in the browser version
*/
export function cachebust(path: string): any {
if (G_IS_BROWSER && !G_IS_STANDALONE && !G_IS_DEV) {
return "/v/" + G_BUILD_COMMIT_HASH + "/" + path;
}
return path;
}

@ -0,0 +1,351 @@
import { createLogger } from "./logging";
import { Signal } from "./signal";
import { fastArrayDelete, fastArrayDeleteValueIfContained } from "./utils";
import { Vector } from "./vector";
import { IS_MOBILE, SUPPORT_TOUCH } from "./config";
import { SOUNDS } from "../platform/sound";
import { GLOBAL_APP } from "./globals";
const logger: any = createLogger("click_detector");
export const MAX_MOVE_DISTANCE_PX: any = IS_MOBILE ? 20 : 80;
// For debugging
const registerClickDetectors: any = G_IS_DEV && true;
if (registerClickDetectors) {
window.activeClickDetectors = [];
}
// Store active click detectors so we can cancel them
const ongoingClickDetectors: Array<ClickDetector> = [];
// Store when the last touch event was registered, to avoid accepting a touch *and* a click event
export let clickDetectorGlobals: any = {
lastTouchTime: -1000,
};
export type ClickDetectorConstructorArgs = {
consumeEvents?: boolean;
preventDefault?: boolean;
applyCssClass?: string;
captureTouchmove?: boolean;
targetOnly?: boolean;
maxDistance?: number;
clickSound?: string;
preventClick?: boolean;
};
// Detects clicks
export class ClickDetector {
public clickDownPosition = null;
public consumeEvents = consumeEvents;
public preventDefault = preventDefault;
public applyCssClass = applyCssClass;
public captureTouchmove = captureTouchmove;
public targetOnly = targetOnly;
public clickSound = clickSound;
public maxDistance = maxDistance;
public preventClick = preventClick;
public click = new Signal();
public rightClick = new Signal();
public touchstart = new Signal();
public touchmove = new Signal();
public touchend = new Signal();
public touchcancel = new Signal();
public touchstartSimple = new Signal();
public touchmoveSimple = new Signal();
public touchendSimple = new Signal();
public clickStartTime = null;
public cancelled = false;
constructor(element, { consumeEvents = false, preventDefault = true, applyCssClass = "pressed", captureTouchmove = false, targetOnly = false, maxDistance = MAX_MOVE_DISTANCE_PX, clickSound = SOUNDS.uiClick, preventClick = false, }) {
assert(element, "No element given!");
this.internalBindTo(element as HTMLElement));
}
/**
* Cleans up all event listeners of this detector
*/
cleanup(): any {
if (this.element) {
if (registerClickDetectors) {
const index: any = window.activeClickDetectors.indexOf(this);
if (index < 0) {
logger.error("Click detector cleanup but is not active");
}
else {
window.activeClickDetectors.splice(index, 1);
}
}
const options: any = this.internalGetEventListenerOptions();
if (SUPPORT_TOUCH) {
this.element.removeEventListener("touchstart", this.handlerTouchStart, options);
this.element.removeEventListener("touchend", this.handlerTouchEnd, options);
this.element.removeEventListener("touchcancel", this.handlerTouchCancel, options);
}
this.element.removeEventListener("mouseup", this.handlerTouchStart, options);
this.element.removeEventListener("mousedown", this.handlerTouchEnd, options);
this.element.removeEventListener("mouseout", this.handlerTouchCancel, options);
if (this.captureTouchmove) {
if (SUPPORT_TOUCH) {
this.element.removeEventListener("touchmove", this.handlerTouchMove, options);
}
this.element.removeEventListener("mousemove", this.handlerTouchMove, options);
}
if (this.preventClick) {
this.element.removeEventListener("click", this.handlerPreventClick, options);
}
this.click.removeAll();
this.touchstart.removeAll();
this.touchmove.removeAll();
this.touchend.removeAll();
this.touchcancel.removeAll();
this.element = null;
}
}
// INTERNAL METHODS
internalPreventClick(event: Event): any {
window.focus();
event.preventDefault();
}
/**
* Internal method to get the options to pass to an event listener
*/
internalGetEventListenerOptions(): any {
return {
capture: this.consumeEvents,
passive: !this.preventDefault,
};
}
/**
* Binds the click detector to an element
*/
internalBindTo(element: HTMLElement): any {
const options: any = this.internalGetEventListenerOptions();
this.handlerTouchStart = this.internalOnPointerDown.bind(this);
this.handlerTouchEnd = this.internalOnPointerEnd.bind(this);
this.handlerTouchMove = this.internalOnPointerMove.bind(this);
this.handlerTouchCancel = this.internalOnTouchCancel.bind(this);
if (this.preventClick) {
this.handlerPreventClick = this.internalPreventClick.bind(this);
element.addEventListener("click", this.handlerPreventClick, options);
}
if (SUPPORT_TOUCH) {
element.addEventListener("touchstart", this.handlerTouchStart, options);
element.addEventListener("touchend", this.handlerTouchEnd, options);
element.addEventListener("touchcancel", this.handlerTouchCancel, options);
}
element.addEventListener("mousedown", this.handlerTouchStart, options);
element.addEventListener("mouseup", this.handlerTouchEnd, options);
element.addEventListener("mouseout", this.handlerTouchCancel, options);
if (this.captureTouchmove) {
if (SUPPORT_TOUCH) {
element.addEventListener("touchmove", this.handlerTouchMove, options);
}
element.addEventListener("mousemove", this.handlerTouchMove, options);
}
if (registerClickDetectors) {
window.activeClickDetectors.push(this);
}
this.element = element;
}
/**
* Returns if the bound element is currently in the DOM.
*/
internalIsDomElementAttached(): any {
return this.element && document.documentElement.contains(this.element);
}
/**
* Checks if the given event is relevant for this detector
*/
internalEventPreHandler(event: TouchEvent | MouseEvent, expectedRemainingTouches: any = 1): any {
if (!this.element) {
// Already cleaned up
return false;
}
if (this.targetOnly && event.target !== this.element) {
// Clicked a child element
return false;
}
// Stop any propagation and defaults if configured
if (this.consumeEvents && event.cancelable) {
event.stopPropagation();
}
if (this.preventDefault && event.cancelable) {
event.preventDefault();
}
if (window.TouchEvent && event instanceof TouchEvent) {
clickDetectorGlobals.lastTouchTime = performance.now();
// console.log("Got touches", event.targetTouches.length, "vs", expectedRemainingTouches);
if (event.targetTouches.length !== expectedRemainingTouches) {
return false;
}
}
if (event instanceof MouseEvent) {
if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) {
return false;
}
}
return true;
}
/**
* Extracts the mous position from an event
* {} The client space position
*/
static extractPointerPosition(event: TouchEvent | MouseEvent): Vector {
if (window.TouchEvent && event instanceof TouchEvent) {
if (event.changedTouches.length !== 1) {
logger.warn("Got unexpected target touches:", event.targetTouches.length, "->", event.targetTouches);
return new Vector(0, 0);
}
const touch: any = event.changedTouches[0];
return new Vector(touch.clientX, touch.clientY);
}
if (event instanceof MouseEvent) {
return new Vector(event.clientX, event.clientY);
}
assertAlways(false, "Got unknown event: " + event);
return new Vector(0, 0);
}
/**
* Cacnels all ongoing events on this detector
*/
cancelOngoingEvents(): any {
if (this.applyCssClass && this.element) {
this.element.classList.remove(this.applyCssClass);
}
this.clickDownPosition = null;
this.clickStartTime = null;
this.cancelled = true;
fastArrayDeleteValueIfContained(ongoingClickDetectors, this);
}
/**
* Internal pointer down handler
*/
internalOnPointerDown(event: TouchEvent | MouseEvent): any {
window.focus();
if (!this.internalEventPreHandler(event, 1)) {
return false;
}
const position: any = (this.constructor as typeof ClickDetector).extractPointerPosition(event);
if (event instanceof MouseEvent) {
const isRightClick: any = event.button === 2;
if (isRightClick) {
// Ignore right clicks
this.rightClick.dispatch(position, event);
this.cancelled = true;
this.clickDownPosition = null;
return;
}
}
if (this.clickDownPosition) {
logger.warn("Ignoring double click");
return false;
}
this.cancelled = false;
this.touchstart.dispatch(event);
// Store where the touch started
this.clickDownPosition = position;
this.clickStartTime = performance.now();
this.touchstartSimple.dispatch(this.clickDownPosition.x, this.clickDownPosition.y);
// If we are not currently within a click, register it
if (ongoingClickDetectors.indexOf(this) < 0) {
ongoingClickDetectors.push(this);
}
else {
logger.warn("Click detector got pointer down of active pointer twice");
}
// If we should apply any classes, do this now
if (this.applyCssClass) {
this.element.classList.add(this.applyCssClass);
}
// If we should play any sound, do this
if (this.clickSound) {
GLOBAL_APP.sound.playUiSound(this.clickSound);
}
return false;
}
/**
* Internal pointer move handler
*/
internalOnPointerMove(event: TouchEvent | MouseEvent): any {
if (!this.internalEventPreHandler(event, 1)) {
return false;
}
this.touchmove.dispatch(event);
const pos: any = (this.constructor as typeof ClickDetector).extractPointerPosition(event);
this.touchmoveSimple.dispatch(pos.x, pos.y);
return false;
}
/**
* Internal pointer end handler
*/
internalOnPointerEnd(event: TouchEvent | MouseEvent): any {
window.focus();
if (!this.internalEventPreHandler(event, 0)) {
return false;
}
if (this.cancelled) {
// warn(this, "Not dispatching touchend on cancelled listener");
return false;
}
if (event instanceof MouseEvent) {
const isRightClick: any = event.button === 2;
if (isRightClick) {
return;
}
}
const index: any = ongoingClickDetectors.indexOf(this);
if (index < 0) {
logger.warn("Got pointer end but click detector is not in pressed state");
}
else {
fastArrayDelete(ongoingClickDetectors, index);
}
let dispatchClick: any = false;
let dispatchClickPos: any = null;
// Check for correct down position, otherwise must have pinched or so
if (this.clickDownPosition) {
const pos: any = (this.constructor as typeof ClickDetector).extractPointerPosition(event);
const distance: any = pos.distance(this.clickDownPosition);
if (!IS_MOBILE || distance <= this.maxDistance) {
dispatchClick = true;
dispatchClickPos = pos;
}
else {
console.warn("[ClickDetector] Touch does not count as click:", "(was", distance, ")");
}
}
this.clickDownPosition = null;
this.clickStartTime = null;
if (this.applyCssClass) {
this.element.classList.remove(this.applyCssClass);
}
// Dispatch in the end to avoid the element getting invalidated
// Also make sure that the element is still in the dom
if (this.internalIsDomElementAttached()) {
this.touchend.dispatch(event);
this.touchendSimple.dispatch();
if (dispatchClick) {
const detectors: any = ongoingClickDetectors.slice();
for (let i: any = 0; i < detectors.length; ++i) {
detectors[i].cancelOngoingEvents();
}
this.click.dispatch(dispatchClickPos, event);
}
}
return false;
}
/**
* Internal touch cancel handler
*/
internalOnTouchCancel(event: TouchEvent | MouseEvent): any {
if (!this.internalEventPreHandler(event, 0)) {
return false;
}
if (this.cancelled) {
// warn(this, "Not dispatching touchcancel on cancelled listener");
return false;
}
this.cancelOngoingEvents();
this.touchcancel.dispatch(event);
this.touchendSimple.dispatch(event);
return false;
}
}

@ -0,0 +1,126 @@
export default {
// You can set any debug options here!
/* dev:start */
// -----------------------------------------------------------------------------------
// Quickly enters the game and skips the main menu - good for fast iterating
// fastGameEnter: true,
// -----------------------------------------------------------------------------------
// Skips any delays like transitions between states and such
// noArtificialDelays: true,
// -----------------------------------------------------------------------------------
// Disables writing of savegames, useful for testing the same savegame over and over
// disableSavegameWrite: true,
// -----------------------------------------------------------------------------------
// Shows bounds of all entities
// showEntityBounds: true,
// -----------------------------------------------------------------------------------
// Shows arrows for every ejector / acceptor
// showAcceptorEjectors: true,
// -----------------------------------------------------------------------------------
// Disables the music (Overrides any setting, can cause weird behaviour)
// disableMusic: true,
// -----------------------------------------------------------------------------------
// Do not render static map entities (=most buildings)
// doNotRenderStatics: true,
// -----------------------------------------------------------------------------------
// Allow to zoom freely without limits
// disableZoomLimits: true,
// -----------------------------------------------------------------------------------
// All rewards can be unlocked by passing just 1 of any shape
// rewardsInstant: true,
// -----------------------------------------------------------------------------------
// Unlocks all buildings
// allBuildingsUnlocked: true,
// -----------------------------------------------------------------------------------
// Disables cost of blueprints
// blueprintsNoCost: true,
// -----------------------------------------------------------------------------------
// Disables cost of upgrades
// upgradesNoCost: true,
// -----------------------------------------------------------------------------------
// Disables the dialog when completing a level
// disableUnlockDialog: true,
// -----------------------------------------------------------------------------------
// Disables the simulation - This effectively pauses the game.
// disableLogicTicks: true,
// -----------------------------------------------------------------------------------
// Test the rendering if everything is clipped out properly
// testClipping: true,
// -----------------------------------------------------------------------------------
// Allows to render slower, useful for recording at half speed to avoid stuttering
// framePausesBetweenTicks: 250,
// -----------------------------------------------------------------------------------
// Replace all translations with emojis to see which texts are translateable
// testTranslations: true,
// -----------------------------------------------------------------------------------
// Enables an inspector which shows information about the entity below the cursor
// enableEntityInspector: true,
// -----------------------------------------------------------------------------------
// Enables ads in the local build (normally they are deactivated there)
// testAds: true,
// -----------------------------------------------------------------------------------
// Allows unlocked achievements to be logged to console in the local build
// testAchievements: true,
// -----------------------------------------------------------------------------------
// Enables use of (some) existing flags within the puzzle mode context
// testPuzzleMode: true,
// -----------------------------------------------------------------------------------
// Disables the automatic switch to an overview when zooming out
// disableMapOverview: true,
// -----------------------------------------------------------------------------------
// Disables the notification when there are new entries in the changelog since last played
// disableUpgradeNotification: true,
// -----------------------------------------------------------------------------------
// Makes belts almost infinitely fast
// instantBelts: true,
// -----------------------------------------------------------------------------------
// Makes item processors almost infinitely fast
// instantProcessors: true,
// -----------------------------------------------------------------------------------
// Makes miners almost infinitely fast
// instantMiners: true,
// -----------------------------------------------------------------------------------
// When using fastGameEnter, controls whether a new game is started or the last one is resumed
// resumeGameOnFastEnter: true,
// -----------------------------------------------------------------------------------
// Special option used to render the trailer
// renderForTrailer: true,
// -----------------------------------------------------------------------------------
// Whether to render changes
// renderChanges: true,
// -----------------------------------------------------------------------------------
// Whether to render belt paths
// renderBeltPaths: true,
// -----------------------------------------------------------------------------------
// Whether to check belt paths
// checkBeltPaths: true,
// -----------------------------------------------------------------------------------
// Whether to items / s instead of items / m in stats
// detailedStatistics: true,
// -----------------------------------------------------------------------------------
// Shows detailed information about which atlas is used
// showAtlasInfo: true,
// -----------------------------------------------------------------------------------
// Renders the rotation of all wires
// renderWireRotations: true,
// -----------------------------------------------------------------------------------
// Renders information about wire networks
// renderWireNetworkInfos: true,
// -----------------------------------------------------------------------------------
// Disables ejector animations and processing
// disableEjectorProcessing: true,
// -----------------------------------------------------------------------------------
// Allows manual ticking
// manualTickOnly: true,
// -----------------------------------------------------------------------------------
// Disables slow asserts, useful for debugging performance
// disableSlowAsserts: true,
// -----------------------------------------------------------------------------------
// Allows to load a mod from an external source for developing it
// externalModUrl: "http://localhost:3005/combined.js",
// -----------------------------------------------------------------------------------
// Visualizes the shape grouping on belts
// showShapeGrouping: true
// -----------------------------------------------------------------------------------
/* dev:end */
};

@ -0,0 +1,133 @@
/* typehints:start */
import type { Application } from "../application";
/* typehints:end */
export const IS_DEBUG: any = G_IS_DEV &&
typeof window !== "undefined" &&
window.location.port === "3005" &&
(window.location.host.indexOf("localhost:") >= 0 || window.location.host.indexOf("192.168.0.") >= 0) &&
window.location.search.indexOf("nodebug") < 0;
export const SUPPORT_TOUCH: any = false;
const smoothCanvas: any = true;
export const THIRDPARTY_URLS: any = {
discord: "https://discord.gg/HN7EVzV",
github: "https://github.com/tobspr-games/shapez.io",
reddit: "https://www.reddit.com/r/shapezio",
shapeViewer: "https://viewer.shapez.io",
twitter: "https://twitter.com/tobspr",
patreon: "https://www.patreon.com/tobsprgames",
privacyPolicy: "https://tobspr.io/privacy.html",
standaloneCampaignLink: "https://get.shapez.io/bundle/$campaign",
puzzleDlcStorePage: "https://get.shapez.io/mm_puzzle_dlc?target=dlc",
levelTutorialVideos: {
21: "https://www.youtube.com/watch?v=0nUfRLMCcgo&",
25: "https://www.youtube.com/watch?v=7OCV1g40Iew&",
26: "https://www.youtube.com/watch?v=gfm6dS1dCoY",
},
modBrowser: "https://shapez.mod.io/",
};
export function openStandaloneLink(app: Application, campaign: string): any {
const discount: any = globalConfig.currentDiscount > 0 ? "_discount" + globalConfig.currentDiscount : "";
const event: any = campaign + discount;
app.platformWrapper.openExternalLink(THIRDPARTY_URLS.standaloneCampaignLink.replace("$campaign", event));
app.gameAnalytics.noteMinor("g.stdlink." + event);
}
export const globalConfig: any = {
// Size of a single tile in Pixels.
// NOTICE: Update webpack.production.config too!
tileSize: 32,
halfTileSize: 16,
// Which dpi the assets have
assetsDpi: 192 / 32,
assetsSharpness: 1.5,
shapesSharpness: 1.3,
// Achievements
achievementSliceDuration: 10,
// Production analytics
statisticsGraphDpi: 2.5,
statisticsGraphSlices: 100,
analyticsSliceDurationSeconds: G_IS_DEV ? 1 : 10,
minimumTickRate: 25,
maximumTickRate: 500,
// Map
mapChunkSize: 16,
chunkAggregateSize: 4,
mapChunkOverviewMinZoom: 0.9,
mapChunkWorldSize: null,
maxBeltShapeBundleSize: 20,
// Belt speeds
// NOTICE: Update webpack.production.config too!
beltSpeedItemsPerSecond: 2,
minerSpeedItemsPerSecond: 0,
defaultItemDiameter: 20,
itemSpacingOnBelts: 0.63,
wiresSpeedItemsPerSecond: 6,
undergroundBeltMaxTilesByTier: [5, 9],
readerAnalyzeIntervalSeconds: 10,
goalAcceptorItemsRequired: 12,
goalAcceptorsPerProducer: 5,
puzzleModeSpeed: 3,
puzzleMinBoundsSize: 2,
puzzleMaxBoundsSize: 20,
puzzleValidationDurationSeconds: 30,
buildingSpeeds: {
cutter: 1 / 4,
cutterQuad: 1 / 4,
rotater: 1 / 1,
rotaterCCW: 1 / 1,
rotater180: 1 / 1,
painter: 1 / 6,
painterDouble: 1 / 8,
painterQuad: 1 / 2,
mixer: 1 / 5,
stacker: 1 / 8,
},
// Zooming
initialZoom: 1.9,
minZoomLevel: 0.1,
maxZoomLevel: 3,
// Global game speed
gameSpeed: 1,
warmupTimeSecondsFast: 0.25,
warmupTimeSecondsRegular: 0.25,
smoothing: {
smoothMainCanvas: smoothCanvas && true,
quality: "low", // Low is CRUCIAL for mobile performance!
},
rendering: {},
debug: require("./config.local").default,
currentDiscount: 0,
// Secret vars
info: {
// Binary file salt
file: "Ec'])@^+*9zMevK3uMV4432x9%iK'=",
// Savegame salt
sgSalt: "}95Q3%8/.837Lqym_BJx%q7)pAHJbF",
// Analytics key
analyticsApiKey: "baf6a50f0cc7dfdec5a0e21c88a1c69a4b34bc4a",
},
};
export const IS_MOBILE: any = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
// Automatic calculations
globalConfig.minerSpeedItemsPerSecond = globalConfig.beltSpeedItemsPerSecond / 5;
globalConfig.mapChunkWorldSize = globalConfig.mapChunkSize * globalConfig.tileSize;
// Dynamic calculations
if (globalConfig.debug.disableMapOverview) {
globalConfig.mapChunkOverviewMinZoom = 0;
}
// Stuff for making the trailer
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
globalConfig.debug.framePausesBetweenTicks = 32;
// globalConfig.mapChunkOverviewMinZoom = 0.0;
// globalConfig.debug.instantBelts = true;
// globalConfig.debug.instantProcessors = true;
// globalConfig.debug.instantMiners = true;
globalConfig.debug.disableSavegameWrite = true;
// globalConfig.beltSpeedItemsPerSecond *= 2;
}
if (globalConfig.debug.fastGameEnter) {
globalConfig.debug.noArtificialDelays = true;
}
if (G_IS_DEV && globalConfig.debug.noArtificialDelays) {
globalConfig.warmupTimeSecondsFast = 0;
globalConfig.warmupTimeSecondsRegular = 0;
}

@ -0,0 +1,100 @@
import { globalConfig } from "./config";
import { round1Digit, round2Digits } from "./utils";
/**
* Returns the current dpi
* {}
*/
export function getDeviceDPI(): number {
return window.devicePixelRatio || 1;
}
/**
*
* {} Smoothed dpi
*/
export function smoothenDpi(dpi: number): number {
if (dpi < 0.05) {
return 0.05;
}
else if (dpi < 0.2) {
return round2Digits(Math.round(dpi / 0.04) * 0.04);
}
else if (dpi < 1) {
return round1Digit(Math.round(dpi / 0.1) * 0.1);
}
else if (dpi < 4) {
return round1Digit(Math.round(dpi / 0.5) * 0.5);
}
else {
return 4;
}
}
// Initial dpi
// setDPIMultiplicator(1);
/**
* Prepares a context for hihg dpi rendering
*/
export function prepareHighDPIContext(context: CanvasRenderingContext2D, smooth: any = true): any {
const dpi: any = getDeviceDPI();
context.scale(dpi, dpi);
if (smooth) {
context.imageSmoothingEnabled = true;
context.webkitImageSmoothingEnabled = true;
// @ts-ignore
context.imageSmoothingQuality = globalConfig.smoothing.quality;
}
else {
context.imageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
}
}
/**
* Resizes a high dpi canvas
*/
export function resizeHighDPICanvas(canvas: HTMLCanvasElement, w: number, h: number, smooth: any = true): any {
const dpi: any = getDeviceDPI();
const wNumber: any = Math.floor(w);
const hNumber: any = Math.floor(h);
const targetW: any = Math.floor(wNumber * dpi);
const targetH: any = Math.floor(hNumber * dpi);
if (targetW !== canvas.width || targetH !== canvas.height) {
// console.log("Resize Canvas from", canvas.width, canvas.height, "to", targetW, targetH)
canvas.width = targetW;
canvas.height = targetH;
canvas.style.width = wNumber + "px";
canvas.style.height = hNumber + "px";
prepareHighDPIContext(canvas.getContext("2d"), smooth);
}
}
/**
* Resizes a canvas
*/
export function resizeCanvas(canvas: HTMLCanvasElement, w: number, h: number, setStyle: any = true): any {
const actualW: any = Math.ceil(w);
const actualH: any = Math.ceil(h);
if (actualW !== canvas.width || actualH !== canvas.height) {
canvas.width = actualW;
canvas.height = actualH;
if (setStyle) {
canvas.style.width = actualW + "px";
canvas.style.height = actualH + "px";
}
// console.log("Resizing canvas to", actualW, "x", actualH);
}
}
/**
* Resizes a canvas and makes sure its cleared
*/
export function resizeCanvasAndClear(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, w: number, h: number): any {
const actualW: any = Math.ceil(w);
const actualH: any = Math.ceil(h);
if (actualW !== canvas.width || actualH !== canvas.height) {
canvas.width = actualW;
canvas.height = actualH;
canvas.style.width = actualW + "px";
canvas.style.height = actualH + "px";
// console.log("Resizing canvas to", actualW, "x", actualH);
}
else {
context.clearRect(0, 0, actualW, actualH);
}
}

@ -0,0 +1,14 @@
import { globalConfig } from "./config";
export type GameRoot = import("../game/root").GameRoot;
export type Rectangle = import("./rectangle").Rectangle;
export class DrawParameters {
public context: CanvasRenderingContext2D = context;
public visibleRect: Rectangle = visibleRect;
public desiredAtlasScale: string = desiredAtlasScale;
public zoomLevel: number = zoomLevel;
public root: GameRoot = root;
constructor({ context, visibleRect, desiredAtlasScale, zoomLevel, root }) {
}
}

@ -0,0 +1,88 @@
export type AtlasSprite = import("./sprites").AtlasSprite;
export type DrawParameters = import("./draw_parameters").DrawParameters;
import { globalConfig } from "./config";
import { createLogger } from "./logging";
import { Rectangle } from "./rectangle";
const logger: any = createLogger("draw_utils");
export function initDrawUtils(): any {
CanvasRenderingContext2D.prototype.beginRoundedRect = function (x: any, y: any, w: any, h: any, r: any): any {
this.beginPath();
if (r < 0.05) {
this.rect(x, y, w, h);
return;
}
if (w < 2 * r) {
r = w / 2;
}
if (h < 2 * r) {
r = h / 2;
}
this.moveTo(x + r, y);
this.arcTo(x + w, y, x + w, y + h, r);
this.arcTo(x + w, y + h, x, y + h, r);
this.arcTo(x, y + h, x, y, r);
this.arcTo(x, y, x + w, y, r);
};
CanvasRenderingContext2D.prototype.beginCircle = function (x: any, y: any, r: any): any {
this.beginPath();
if (r < 0.05) {
this.rect(x, y, 1, 1);
return;
}
this.arc(x, y, r, 0, 2.0 * Math.PI);
};
}
export function drawRotatedSprite({ parameters, sprite, x, y, angle, size, offsetX = 0, offsetY = 0 }: {
parameters: DrawParameters;
sprite: AtlasSprite;
x: number;
y: number;
angle: number;
size: number;
offsetX: number=;
offsetY: number=;
}): any {
if (angle === 0) {
sprite.drawCachedCentered(parameters, x + offsetX, y + offsetY, size);
return;
}
parameters.context.translate(x, y);
parameters.context.rotate(angle);
sprite.drawCachedCentered(parameters, offsetX, offsetY, size, false);
parameters.context.rotate(-angle);
parameters.context.translate(-x, -y);
}
let warningsShown: any = 0;
/**
* Draws a sprite with clipping
*/
export function drawSpriteClipped({ parameters, sprite, x, y, w, h, originalW, originalH }: {
parameters: DrawParameters;
sprite: HTMLCanvasElement;
x: number;
y: number;
w: number;
h: number;
originalW: number;
originalH: number;
}): any {
const rect: any = new Rectangle(x, y, w, h);
const intersection: any = rect.getIntersection(parameters.visibleRect);
if (!intersection) {
// Clipped
if (++warningsShown % 200 === 1) {
logger.warn("Sprite drawn clipped but it's not on screen - perform culling before (", warningsShown, "warnings)");
}
if (G_IS_DEV && globalConfig.debug.testClipping) {
parameters.context.fillStyle = "yellow";
parameters.context.fillRect(x, y, w, h);
}
return;
}
parameters.context.drawImage(sprite,
// src pos and size
((intersection.x - x) / w) * originalW, ((intersection.y - y) / h) * originalH, (originalW * intersection.w) / w, (originalH * intersection.h) / h,
// dest pos and size
intersection.x, intersection.y, intersection.w, intersection.h);
}

@ -0,0 +1,32 @@
export class ExplainedResult {
public result: boolean = result;
public reason: string = reason;
constructor(result = true, reason = null, additionalProps = {}) {
// Copy additional props
for (const key: any in additionalProps) {
this[key] = additionalProps[key];
}
}
isGood(): any {
return !!this.result;
}
isBad(): any {
return !this.result;
}
static good(): any {
return new ExplainedResult(true);
}
static bad(reason: any, additionalProps: any): any {
return new ExplainedResult(false, reason, additionalProps);
}
static requireAll(...args: any): any {
for (let i: any = 0; i < args.length; ++i) {
const subResult: any = args[i].call();
if (!subResult.isGood()) {
return subResult;
}
}
return this.good();
}
}

@ -0,0 +1,67 @@
import { createLogger } from "./logging";
const logger: any = createLogger("factory");
// simple factory pattern
export class Factory {
public id = id;
public entries = [];
public entryIds = [];
public idToEntry = {};
constructor(id) {
}
getId(): any {
return this.id;
}
register(entry: any): any {
// Extract id
const id: any = entry.getId();
assert(id, "Factory: Invalid id for class: " + entry);
// Check duplicates
assert(!this.idToEntry[id], "Duplicate factory entry for " + id);
// Insert
this.entries.push(entry);
this.entryIds.push(id);
this.idToEntry[id] = entry;
}
/**
* Checks if a given id is registered
* {}
*/
hasId(id: string): boolean {
return !!this.idToEntry[id];
}
/**
* Finds an instance by a given id
* {}
*/
findById(id: string): object {
const entry: any = this.idToEntry[id];
if (!entry) {
logger.error("Object with id", id, "is not registered on factory", this.id, "!");
assert(false, "Factory: Object with id '" + id + "' is not registered!");
return null;
}
return entry;
}
/**
* Returns all entries
* {}
*/
getEntries(): Array<object> {
return this.entries;
}
/**
* Returns all registered ids
* {}
*/
getAllIds(): Array<string> {
return this.entryIds;
}
/**
* Returns amount of stored entries
* {}
*/
getNumEntries(): number {
return this.entries.length;
}
}

@ -0,0 +1,280 @@
/* typehints:start */
import type { Application } from "../application";
import type { StateManager } from "./state_manager";
/* typehints:end */
import { globalConfig } from "./config";
import { ClickDetector } from "./click_detector";
import { logSection, createLogger } from "./logging";
import { InputReceiver } from "./input_receiver";
import { waitNextFrame } from "./utils";
import { RequestChannel } from "./request_channel";
import { MUSIC } from "../platform/sound";
const logger: any = createLogger("game_state");
/**
* Basic state of the game state machine. This is the base of the whole game
*/
export class GameState {
public key = key;
public stateManager: StateManager = null;
public app: Application = null;
public fadingOut = false;
public clickDetectors: Array<ClickDetector> = [];
public inputReciever = new InputReceiver("state-" + key);
public asyncChannel = new RequestChannel();
/**
* Constructs a new state with the given id
*/
constructor(key) {
this.inputReciever.backButton.add(this.onBackButton, this);
}
//// GETTERS / HELPER METHODS ////
/**
* Returns the states key
* {}
*/
getKey(): string {
return this.key;
}
/**
* Returns the html element of the state
* {}
*/
getDivElement(): HTMLElement {
return document.getElementById("state_" + this.key);
}
/**
* Transfers to a new state
*/
moveToState(stateKey: string, payload: any = {}, skipFadeOut: any = false): any {
if (this.fadingOut) {
logger.warn("Skipping move to '" + stateKey + "' since already fading out");
return;
}
// Clean up event listeners
this.internalCleanUpClickDetectors();
// Fading
const fadeTime: any = this.internalGetFadeInOutTime();
const doFade: any = !skipFadeOut && this.getHasFadeOut() && fadeTime !== 0;
logger.log("Moving to", stateKey, "(fading=", doFade, ")");
if (doFade) {
this.htmlElement.classList.remove("arrived");
this.fadingOut = true;
setTimeout((): any => {
this.stateManager.moveToState(stateKey, payload);
}, fadeTime);
}
else {
this.stateManager.moveToState(stateKey, payload);
}
}
/**
* Tracks clicks on a given element and calls the given callback *on this state*.
* If you want to call another function wrap it inside a lambda.
*/
trackClicks(element: Element, handler: function():void, args: import("./click_detector").ClickDetectorConstructorArgs= = {}): any {
const detector: any = new ClickDetector(element, args);
detector.click.add(handler, this);
if (G_IS_DEV) {
// Append a source so we can check where the click detector is from
// @ts-ignore
detector._src = "state-" + this.key;
}
this.clickDetectors.push(detector);
}
/**
* Cancels all promises on the api as well as our async channel
*/
cancelAllAsyncOperations(): any {
this.asyncChannel.cancelAll();
}
//// CALLBACKS ////
/**
* Callback when entering the state, to be overriddemn
*/
onEnter(payload: any): any { }
/**
* Callback when leaving the state
*/
onLeave(): any { }
/**
* Callback when the app got paused (on android, this means in background)
*/
onAppPause(): any { }
/**
* Callback when the app got resumed (on android, this means in foreground again)
*/
onAppResume(): any { }
/**
* Render callback
*/
onRender(dt: number): any { }
/**
* Background tick callback, called while the game is inactiev
*/
onBackgroundTick(dt: number): any { }
/**
* Called when the screen resized
*/
onResized(w: number, h: number): any { }
/**
* Internal backbutton handler, called when the hardware back button is pressed or
* the escape key is pressed
*/
onBackButton(): any { }
//// INTERFACE ////
/**
* Should return how many mulliseconds to fade in / out the state. Not recommended to override!
* {} Time in milliseconds to fade out
*/
getInOutFadeTime(): number {
if (globalConfig.debug.noArtificialDelays) {
return 0;
}
return 200;
}
/**
* Should return whether to fade in the game state. This will then apply the right css classes
* for the fadein.
* {}
*/
getHasFadeIn(): boolean {
return true;
}
/**
* Should return whether to fade out the game state. This will then apply the right css classes
* for the fadeout and wait the delay before moving states
* {}
*/
getHasFadeOut(): boolean {
return true;
}
/**
* Returns if this state should get paused if it does not have focus
* {} true to pause the updating of the game
*/
getPauseOnFocusLost(): boolean {
return true;
}
/**
* Should return the html code of the state.
* {}
* @abstract
*/
getInnerHTML(): string {
abstract;
return "";
}
/**
* Returns if the state has an unload confirmation, this is the
* "Are you sure you want to leave the page" message.
*/
getHasUnloadConfirmation(): any {
return false;
}
/**
* Should return the theme music for this state
* {}
*/
getThemeMusic(): string | null {
return MUSIC.menu;
}
/**
* Should return true if the player is currently ingame
* {}
*/
getIsIngame(): boolean {
return false;
}
/**
* Should return whether to clear the whole body content before entering the state.
* {}
*/
getRemovePreviousContent(): boolean {
return true;
}
////////////////////
//// INTERNAL ////
/**
* Internal callback from the manager. Do not override!
*/
internalRegisterCallback(stateManager: StateManager, app: any): any {
assert(stateManager, "No state manager");
assert(app, "No app");
this.stateManager = stateManager;
this.app = app;
}
/**
* Internal callback when entering the state. Do not override!
*/
internalEnterCallback(payload: any, callCallback: boolean = true): any {
logSection(this.key, "#26a69a");
this.app.inputMgr.pushReciever(this.inputReciever);
this.htmlElement = this.getDivElement();
this.htmlElement.classList.add("active");
// Apply classes in the next frame so the css transition keeps up
waitNextFrame().then((): any => {
if (this.htmlElement) {
this.htmlElement.classList.remove("fadingOut");
this.htmlElement.classList.remove("fadingIn");
}
});
// Call handler
if (callCallback) {
this.onEnter(payload);
}
}
/**
* Internal callback when the state is left. Do not override!
*/
internalLeaveCallback(): any {
this.onLeave();
this.htmlElement.classList.remove("active");
this.app.inputMgr.popReciever(this.inputReciever);
this.internalCleanUpClickDetectors();
this.asyncChannel.cancelAll();
}
/**
* Internal app pause callback
*/
internalOnAppPauseCallback(): any {
this.onAppPause();
}
/**
* Internal app resume callback
*/
internalOnAppResumeCallback(): any {
this.onAppResume();
}
/**
* Cleans up all click detectors
*/
internalCleanUpClickDetectors(): any {
if (this.clickDetectors) {
for (let i: any = 0; i < this.clickDetectors.length; ++i) {
this.clickDetectors[i].cleanup();
}
this.clickDetectors = [];
}
}
/**
* Internal method to get the HTML of the game state.
* {}
*/
internalGetFullHtml(): string {
return this.getInnerHTML();
}
/**
* Internal method to compute the time to fade in / out
* {} time to fade in / out in ms
*/
internalGetFadeInOutTime(): number {
if (G_IS_DEV && globalConfig.debug.fastGameEnter) {
return 1;
}
if (G_IS_DEV && globalConfig.debug.noArtificialDelays) {
return 1;
}
return this.getInOutFadeTime();
}
}

@ -0,0 +1,22 @@
import { SingletonFactory } from "./singleton_factory";
import { Factory } from "./factory";
export type BaseGameSpeed = import("../game/time/base_game_speed").BaseGameSpeed;
export type Component = import("../game/component").Component;
export type BaseItem = import("../game/base_item").BaseItem;
export type GameMode = import("../game/game_mode").GameMode;
export type MetaBuilding = import("../game/meta_building").MetaBuilding;
export let gMetaBuildingRegistry: SingletonFactoryTemplate<MetaBuilding> = new SingletonFactory();
export let gBuildingsByCategory: {
[idx: string]: Array<Class<MetaBuilding>>;
} = null;
export let gComponentRegistry: FactoryTemplate<Component> = new Factory("component");
export let gGameModeRegistry: FactoryTemplate<GameMode> = new Factory("gameMode");
export let gGameSpeedRegistry: FactoryTemplate<BaseGameSpeed> = new Factory("gamespeed");
export let gItemRegistry: FactoryTemplate<BaseItem> = new Factory("item");
// Helpers
export function initBuildingsByCategory(buildings: {
[idx: string]: Array<Class<MetaBuilding>>;
}): any {
gBuildingsByCategory = buildings;
}

@ -0,0 +1,24 @@
/* typehints:start */
import type { Application } from "../application";
/* typehints:end */
/**
* Used for the bug reporter, and the click detector which both have no handles to this.
* It would be nicer to have no globals, but this is the only one. I promise!
*/
export let GLOBAL_APP: Application = null;
export function setGlobalApp(app: Application): any {
assert(!GLOBAL_APP, "Create application twice!");
GLOBAL_APP = app;
}
export const BUILD_OPTIONS: any = {
HAVE_ASSERT: G_HAVE_ASSERT,
APP_ENVIRONMENT: G_APP_ENVIRONMENT,
IS_DEV: G_IS_DEV,
IS_RELEASE: G_IS_RELEASE,
IS_BROWSER: G_IS_BROWSER,
IS_STANDALONE: G_IS_STANDALONE,
BUILD_TIME: G_BUILD_TIME,
BUILD_COMMIT_HASH: G_BUILD_COMMIT_HASH,
BUILD_VERSION: G_BUILD_VERSION,
ALL_UI_IMAGES: G_ALL_UI_IMAGES,
};

@ -0,0 +1,161 @@
/* typehints:start */
import type { Application } from "../application";
import type { InputReceiver } from "./input_receiver";
/* typehints:end */
import { Signal, STOP_PROPAGATION } from "./signal";
import { createLogger } from "./logging";
import { arrayDeleteValue, fastArrayDeleteValue } from "./utils";
const logger: any = createLogger("input_distributor");
export class InputDistributor {
public app = app;
public recieverStack: Array<InputReceiver> = [];
public filters: Array<function(: boolean):boolean> = [];
public keysDown = new Set();
constructor(app) {
this.bindToEvents();
}
/**
* Attaches a new filter which can filter and reject events
*/
installFilter(filter: function(: boolean):boolean): any {
this.filters.push(filter);
}
/**
* Removes an attached filter
*/
dismountFilter(filter: function(: boolean):boolean): any {
fastArrayDeleteValue(this.filters, filter);
}
pushReciever(reciever: InputReceiver): any {
if (this.isRecieverAttached(reciever)) {
assert(false, "Can not add reciever " + reciever.context + " twice");
logger.error("Can not add reciever", reciever.context, "twice");
return;
}
this.recieverStack.push(reciever);
if (this.recieverStack.length > 10) {
logger.error("Reciever stack is huge, probably some dead receivers arround:", this.recieverStack.map((x: any): any => x.context));
}
}
popReciever(reciever: InputReceiver): any {
if (this.recieverStack.indexOf(reciever) < 0) {
assert(false, "Can not pop reciever " + reciever.context + " since its not contained");
logger.error("Can not pop reciever", reciever.context, "since its not contained");
return;
}
if (this.recieverStack[this.recieverStack.length - 1] !== reciever) {
logger.warn("Popping reciever", reciever.context, "which is not on top of the stack. Stack is: ", this.recieverStack.map((x: any): any => x.context));
}
arrayDeleteValue(this.recieverStack, reciever);
}
isRecieverAttached(reciever: InputReceiver): any {
return this.recieverStack.indexOf(reciever) >= 0;
}
isRecieverOnTop(reciever: InputReceiver): any {
return (this.isRecieverAttached(reciever) &&
this.recieverStack[this.recieverStack.length - 1] === reciever);
}
makeSureAttachedAndOnTop(reciever: InputReceiver): any {
this.makeSureDetached(reciever);
this.pushReciever(reciever);
}
makeSureDetached(reciever: InputReceiver): any {
if (this.isRecieverAttached(reciever)) {
arrayDeleteValue(this.recieverStack, reciever);
}
}
destroyReceiver(reciever: InputReceiver): any {
this.makeSureDetached(reciever);
reciever.cleanup();
}
// Internal
getTopReciever(): any {
if (this.recieverStack.length > 0) {
return this.recieverStack[this.recieverStack.length - 1];
}
return null;
}
bindToEvents(): any {
window.addEventListener("popstate", this.handleBackButton.bind(this), false);
document.addEventListener("backbutton", this.handleBackButton.bind(this), false);
window.addEventListener("keydown", this.handleKeyMouseDown.bind(this));
window.addEventListener("keyup", this.handleKeyMouseUp.bind(this));
window.addEventListener("mousedown", this.handleKeyMouseDown.bind(this));
window.addEventListener("mouseup", this.handleKeyMouseUp.bind(this));
window.addEventListener("blur", this.handleBlur.bind(this));
document.addEventListener("paste", this.handlePaste.bind(this));
}
forwardToReceiver(eventId: any, payload: any = null): any {
// Check filters
for (let i: any = 0; i < this.filters.length; ++i) {
if (!this.filters[i](eventId)) {
return STOP_PROPAGATION;
}
}
const reciever: any = this.getTopReciever();
if (!reciever) {
logger.warn("Dismissing event because not reciever was found:", eventId);
return;
}
const signal: any = reciever[eventId];
assert(signal instanceof Signal, "Not a valid event id");
return signal.dispatch(payload);
}
handleBackButton(event: Event): any {
event.preventDefault();
event.stopPropagation();
this.forwardToReceiver("backButton");
}
/**
* Handles when the page got blurred
*/
handleBlur(): any {
this.forwardToReceiver("pageBlur", {});
this.keysDown.clear();
}
handlePaste(ev: any): any {
this.forwardToReceiver("paste", ev);
}
handleKeyMouseDown(event: KeyboardEvent | MouseEvent): any {
const keyCode: any = event instanceof MouseEvent ? event.button + 1 : event.keyCode;
if (keyCode === 4 || // MB4
keyCode === 5 || // MB5
keyCode === 9 || // TAB
keyCode === 16 || // SHIFT
keyCode === 17 || // CTRL
keyCode === 18 || // ALT
(keyCode >= 112 && keyCode < 122) // F1 - F10
) {
event.preventDefault();
}
const isInitial: any = !this.keysDown.has(keyCode);
this.keysDown.add(keyCode);
if (this.forwardToReceiver("keydown", {
keyCode: keyCode,
shift: event.shiftKey,
alt: event.altKey,
ctrl: event.ctrlKey,
initial: isInitial,
event,
}) === STOP_PROPAGATION) {
return;
}
if (keyCode === 27) {
// Escape key
event.preventDefault();
event.stopPropagation();
return this.forwardToReceiver("backButton");
}
}
handleKeyMouseUp(event: KeyboardEvent | MouseEvent): any {
const keyCode: any = event instanceof MouseEvent ? event.button + 1 : event.keyCode;
this.keysDown.delete(keyCode);
this.forwardToReceiver("keyup", {
keyCode: keyCode,
shift: event.shiftKey,
alt: event.altKey,
});
}
}

@ -0,0 +1,20 @@
import { Signal } from "./signal";
export class InputReceiver {
public context = context;
public backButton = new Signal();
public keydown = new Signal();
public keyup = new Signal();
public pageBlur = new Signal();
public destroyed = new Signal();
public paste = new Signal();
constructor(context = "unknown") {
}
cleanup(): any {
this.backButton.removeAll();
this.keydown.removeAll();
this.keyup.removeAll();
this.paste.removeAll();
this.destroyed.dispatch();
}
}

@ -0,0 +1,159 @@
import { makeOffscreenBuffer } from "./buffer_utils";
import { AtlasSprite, BaseSprite, RegularSprite, SpriteAtlasLink } from "./sprites";
import { cachebust } from "./cachebust";
import { createLogger } from "./logging";
export type Application = import("../application").Application;
export type AtlasDefinition = import("./atlas_definitions").AtlasDefinition;
const logger: any = createLogger("loader");
const missingSpriteIds: any = {};
class LoaderImpl {
public app = null;
public sprites: Map<string, BaseSprite> = new Map();
public rawImages = [];
constructor() {
}
linkAppAfterBoot(app: Application): any {
this.app = app;
this.makeSpriteNotFoundCanvas();
}
/**
* Fetches a given sprite from the cache
* {}
*/
getSpriteInternal(key: string): BaseSprite {
const sprite: any = 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
* {}
*/
getSprite(key: string): AtlasSprite {
const sprite: any = this.getSpriteInternal(key);
assert(sprite instanceof AtlasSprite || sprite === this.spriteNotFoundSprite, "Not an atlas sprite");
return sprite as AtlasSprite);
}
/**
* Returns a regular sprite from the cache
* {}
*/
getRegularSprite(key: string): RegularSprite {
const sprite: any = this.getSpriteInternal(key);
assert(sprite instanceof RegularSprite || sprite === this.spriteNotFoundSprite, "Not a regular sprite");
return sprite as RegularSprite);
}
/**
*
* {}
*/
internalPreloadImage(key: string, progressHandler: (progress: number) => void): Promise<HTMLImageElement | null> {
return this.app.backgroundResourceLoader
.preloadWithProgress("res/" + key, (progress: any): any => {
progressHandler(progress);
})
.then((url: any): any => {
return new Promise((resolve: any, reject: any): any => {
const image: any = new Image();
image.addEventListener("load", (): any => resolve(image));
image.addEventListener("error", (err: any): any => reject("Failed to load sprite " + key + ": " + err));
image.src = url;
});
});
}
/**
* Preloads a sprite
* {}
*/
preloadCSSSprite(key: string, progressHandler: (progress: number) => void): Promise<void> {
return this.internalPreloadImage(key, progressHandler).then((image: any): any => {
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
* {}
*/
preloadAtlas(atlas: AtlasDefinition, progressHandler: (progress: number) => void): Promise<void> {
return this.internalPreloadImage(atlas.getFullSourcePath(), progressHandler).then((image: any): any => {
// @ts-ignore
image.label = atlas.sourceFileName;
return this.internalParseAtlas(atlas, image);
});
}
internalParseAtlas({ meta: { scale }, sourceData }: AtlasDefinition, loadedImage: HTMLImageElement): any {
this.rawImages.push(loadedImage);
for (const spriteName: any in sourceData) {
const { frame, sourceSize, spriteSourceSize }: any = sourceData[spriteName];
let sprite: any = (this.sprites.get(spriteName) as AtlasSprite);
if (!sprite) {
sprite = new AtlasSprite(spriteName);
this.sprites.set(spriteName, sprite);
}
if (sprite.frozen) {
continue;
}
const link: any = 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(): any {
const dims: any = 128;
const [canvas, context]: any = 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: any = new AtlasSprite("not-found");
["0.1", "0.25", "0.5", "0.75", "1"].forEach((resolution: any): any => {
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: any = new LoaderImpl();

@ -0,0 +1,212 @@
import { globalConfig } from "./config";
const circularJson: any = require("circular-json");
/*
Logging functions
- To be extended
*/
/**
* Base logger class
*/
class Logger {
public context = context;
constructor(context) {
}
debug(...args: any): any {
globalDebug(this.context, ...args);
}
log(...args: any): any {
globalLog(this.context, ...args);
}
warn(...args: any): any {
globalWarn(this.context, ...args);
}
error(...args: any): any {
globalError(this.context, ...args);
}
}
export function createLogger(context: any): any {
return new Logger(context);
}
function prepareObjectForLogging(obj: any, maxDepth: any = 1): any {
if (!window.Sentry) {
// Not required without sentry
return obj;
}
if (typeof obj !== "object" && !Array.isArray(obj)) {
return obj;
}
const result: any = {};
for (const key: any in obj) {
const val: any = obj[key];
if (typeof val === "object") {
if (maxDepth > 0) {
result[key] = prepareObjectForLogging(val, maxDepth - 1);
}
else {
result[key] = "[object]";
}
}
else {
result[key] = val;
}
}
return result;
}
/**
* Serializes an error
*/
export function serializeError(err: Error | ErrorEvent): any {
if (!err) {
return null;
}
const result: any = {
type: err.constructor.name,
};
if (err instanceof Error) {
result.message = err.message;
result.name = err.name;
result.stack = err.stack;
result.type = "{type.Error}";
}
else if (err instanceof ErrorEvent) {
result.filename = err.filename;
result.message = err.message;
result.lineno = err.lineno;
result.colno = err.colno;
result.type = "{type.ErrorEvent}";
if (err.error) {
result.error = serializeError(err.error);
}
else {
result.error = "{not-provided}";
}
}
else {
result.type = "{unkown-type:" + typeof err + "}";
}
return result;
}
/**
* Serializes an event
*/
function serializeEvent(event: Event): any {
let result: any = {
type: "{type.Event:" + typeof event + "}",
};
result.eventType = event.type;
return result;
}
/**
* Prepares a json payload
*/
function preparePayload(key: string, value: any): any {
if (value instanceof Error || value instanceof ErrorEvent) {
return serializeError(value);
}
if (value instanceof Event) {
return serializeEvent(value);
}
if (typeof value === "undefined") {
return null;
}
return value;
}
/**
* Stringifies an object containing circular references and errors
*/
export function stringifyObjectContainingErrors(payload: any): any {
return circularJson.stringify(payload, preparePayload);
}
export function globalDebug(context: any, ...args: any): any {
if (G_IS_DEV) {
logInternal(context, console.log, prepareArgsForLogging(args));
}
}
export function globalLog(context: any, ...args: any): any {
// eslint-disable-next-line no-console
logInternal(context, console.log, prepareArgsForLogging(args));
}
export function globalWarn(context: any, ...args: any): any {
// eslint-disable-next-line no-console
logInternal(context, console.warn, prepareArgsForLogging(args));
}
export function globalError(context: any, ...args: any): any {
args = prepareArgsForLogging(args);
// eslint-disable-next-line no-console
logInternal(context, console.error, args);
if (window.Sentry) {
window.Sentry.withScope((scope: any): any => {
scope.setExtra("args", args);
window.Sentry.captureMessage(internalBuildStringFromArgs(args), "error");
});
}
}
function prepareArgsForLogging(args: any): any {
let result: any = [];
for (let i: any = 0; i < args.length; ++i) {
result.push(prepareObjectForLogging(args[i]));
}
return result;
}
function internalBuildStringFromArgs(args: Array<any>): any {
let result: any = [];
for (let i: any = 0; i < args.length; ++i) {
let arg: any = args[i];
if (typeof arg === "string" ||
typeof arg === "number" ||
typeof arg === "boolean" ||
arg === null ||
arg === undefined) {
result.push("" + arg);
}
else if (arg instanceof Error) {
result.push(arg.message);
}
else {
result.push("[object]");
}
}
return result.join(" ");
}
export function logSection(name: any, color: any): any {
while (name.length <= 14) {
name = " " + name + " ";
}
name = name.padEnd(19, " ");
const lineCss: any = "letter-spacing: -3px; color: " + color + "; font-size: 6px; background: #eee; color: #eee;";
const line: any = "%c----------------------------";
console.log("\n" + line + " %c" + name + " " + line + "\n", lineCss, "color: " + color, lineCss);
}
function extractHandleContext(handle: any): any {
let context: any = handle || "unknown";
if (handle && handle.constructor && handle.constructor.name) {
context = handle.constructor.name;
if (context === "String") {
context = handle;
}
}
if (handle && handle.name) {
context = handle.name;
}
return context + "";
}
function logInternal(handle: any, consoleMethod: any, args: any): any {
const context: any = extractHandleContext(handle).padEnd(20, " ");
const labelColor: any = handle && handle.LOG_LABEL_COLOR ? handle.LOG_LABEL_COLOR : "#aaa";
if (G_IS_DEV && globalConfig.debug.logTimestamps) {
const timestamp: any = "⏱ %c" + (Math.floor(performance.now()) + "").padEnd(6, " ") + "";
consoleMethod.call(console, timestamp + " %c" + context, "color: #7f7;", "color: " + labelColor + ";", ...args);
}
else {
// if (G_IS_DEV && !globalConfig.debug.disableLoggingLogSources) {
consoleMethod.call(console, "%c" + context, "color: " + labelColor, ...args);
// } else {
// consoleMethod.call(console, ...args);
// }
}
}

@ -0,0 +1,453 @@
// Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
// This work is free. You can redistribute it and/or modify it
// under the terms of the WTFPL, Version 2
// For more information see LICENSE.txt or http://www.wtfpl.net/
//
// For more information, the home page:
// http://pieroxy.net/blog/pages/lz-string/testing.html
//
// LZ-based compression algorithm, version 1.4.4
const fromCharCode: any = String.fromCharCode;
const hasOwnProperty: any = Object.prototype.hasOwnProperty;
const keyStrUriSafe: any = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
const baseReverseDic: any = {};
function getBaseValue(alphabet: any, character: any): any {
if (!baseReverseDic[alphabet]) {
baseReverseDic[alphabet] = {};
for (let i: any = 0; i < alphabet.length; i++) {
baseReverseDic[alphabet][alphabet.charAt(i)] = i;
}
}
return baseReverseDic[alphabet][character];
}
//compress into uint8array (UCS-2 big endian format)
export function compressU8(uncompressed: any): any {
let compressed: any = compress(uncompressed);
let buf: any = new Uint8Array(compressed.length * 2); // 2 bytes per character
for (let i: any = 0, TotalLen: any = compressed.length; i < TotalLen; i++) {
let current_value: any = compressed.charCodeAt(i);
buf[i * 2] = current_value >>> 8;
buf[i * 2 + 1] = current_value % 256;
}
return buf;
}
// Compreses with header
export function compressU8WHeader(uncompressed: string, header: number): any {
let compressed: any = compress(uncompressed);
let buf: any = new Uint8Array(2 + compressed.length * 2); // 2 bytes per character
buf[0] = header >>> 8;
buf[1] = header % 256;
for (let i: any = 0, TotalLen: any = compressed.length; i < TotalLen; i++) {
let current_value: any = compressed.charCodeAt(i);
buf[2 + i * 2] = current_value >>> 8;
buf[2 + i * 2 + 1] = current_value % 256;
}
return buf;
}
//decompress from uint8array (UCS-2 big endian format)
export function decompressU8WHeader(compressed: Uint8Array): any {
// let buf = new Array(compressed.length / 2); // 2 bytes per character
// for (let i = 0, TotalLen = buf.length; i < TotalLen; i++) {
// buf[i] = compressed[i * 2] * 256 + compressed[i * 2 + 1];
// }
// let result = [];
// buf.forEach(function (c) {
// result.push(fromCharCode(c));
// });
let result: any = [];
for (let i: any = 2, n: any = compressed.length; i < n; i += 2) {
const code: any = compressed[i] * 256 + compressed[i + 1];
result.push(fromCharCode(code));
}
return decompress(result.join(""));
}
//compress into a string that is already URI encoded
export function compressX64(input: any): any {
if (input == null)
return "";
return _compress(input, 6, function (a: any): any {
return keyStrUriSafe.charAt(a);
});
}
//decompress from an output of compressToEncodedURIComponent
export function decompressX64(input: any): any {
if (input == null)
return "";
if (input == "")
return null;
input = input.replace(/ /g, "+");
return _decompress(input.length, 32, function (index: any): any {
return getBaseValue(keyStrUriSafe, input.charAt(index));
});
}
function compress(uncompressed: any): any {
return _compress(uncompressed, 16, function (a: any): any {
return fromCharCode(a);
});
}
function _compress(uncompressed: any, bitsPerChar: any, getCharFromInt: any): any {
if (uncompressed == null)
return "";
let i: any, value: any, context_dictionary: any = {}, context_dictionaryToCreate: any = {}, context_c: any = "", context_wc: any = "", context_w: any = "", context_enlargeIn: any = 2, // Compensate for the first entry which should not count
context_dictSize: any = 3, context_numBits: any = 2, context_data: any = [], context_data_val: any = 0, context_data_position: any = 0, ii: any;
for (ii = 0; ii < uncompressed.length; ii += 1) {
context_c = uncompressed.charAt(ii);
if (!hasOwnProperty.call(context_dictionary, context_c)) {
context_dictionary[context_c] = context_dictSize++;
context_dictionaryToCreate[context_c] = true;
}
context_wc = context_w + context_c;
if (hasOwnProperty.call(context_dictionary, context_wc)) {
context_w = context_wc;
}
else {
if (hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
if (context_w.charCodeAt(0) < 256) {
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
}
else {
context_data_position++;
}
}
value = context_w.charCodeAt(0);
for (i = 0; i < 8; i++) {
context_data_val = (context_data_val << 1) | (value & 1);
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
}
else {
context_data_position++;
}
value = value >> 1;
}
}
else {
value = 1;
for (i = 0; i < context_numBits; i++) {
context_data_val = (context_data_val << 1) | value;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
}
else {
context_data_position++;
}
value = 0;
}
value = context_w.charCodeAt(0);
for (i = 0; i < 16; i++) {
context_data_val = (context_data_val << 1) | (value & 1);
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
}
else {
context_data_position++;
}
value = value >> 1;
}
}
context_enlargeIn--;
if (context_enlargeIn == 0) {
context_enlargeIn = Math.pow(2, context_numBits);
context_numBits++;
}
delete context_dictionaryToCreate[context_w];
}
else {
value = context_dictionary[context_w];
for (i = 0; i < context_numBits; i++) {
context_data_val = (context_data_val << 1) | (value & 1);
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
}
else {
context_data_position++;
}
value = value >> 1;
}
}
context_enlargeIn--;
if (context_enlargeIn == 0) {
context_enlargeIn = Math.pow(2, context_numBits);
context_numBits++;
}
// Add wc to the dictionary.
context_dictionary[context_wc] = context_dictSize++;
context_w = String(context_c);
}
}
// Output the code for w.
if (context_w !== "") {
if (hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
if (context_w.charCodeAt(0) < 256) {
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
}
else {
context_data_position++;
}
}
value = context_w.charCodeAt(0);
for (i = 0; i < 8; i++) {
context_data_val = (context_data_val << 1) | (value & 1);
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
}
else {
context_data_position++;
}
value = value >> 1;
}
}
else {
value = 1;
for (i = 0; i < context_numBits; i++) {
context_data_val = (context_data_val << 1) | value;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
}
else {
context_data_position++;
}
value = 0;
}
value = context_w.charCodeAt(0);
for (i = 0; i < 16; i++) {
context_data_val = (context_data_val << 1) | (value & 1);
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
}
else {
context_data_position++;
}
value = value >> 1;
}
}
context_enlargeIn--;
if (context_enlargeIn == 0) {
context_enlargeIn = Math.pow(2, context_numBits);
context_numBits++;
}
delete context_dictionaryToCreate[context_w];
}
else {
value = context_dictionary[context_w];
for (i = 0; i < context_numBits; i++) {
context_data_val = (context_data_val << 1) | (value & 1);
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
}
else {
context_data_position++;
}
value = value >> 1;
}
}
context_enlargeIn--;
if (context_enlargeIn == 0) {
context_enlargeIn = Math.pow(2, context_numBits);
context_numBits++;
}
}
// Mark the end of the stream
value = 2;
for (i = 0; i < context_numBits; i++) {
context_data_val = (context_data_val << 1) | (value & 1);
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
}
else {
context_data_position++;
}
value = value >> 1;
}
// Flush the last char
// eslint-disable-next-line no-constant-condition
while (true) {
context_data_val = context_data_val << 1;
if (context_data_position == bitsPerChar - 1) {
context_data.push(getCharFromInt(context_data_val));
break;
}
else
context_data_position++;
}
return context_data.join("");
}
function decompress(compressed: any): any {
if (compressed == null)
return "";
if (compressed == "")
return null;
return _decompress(compressed.length, 32768, function (index: any): any {
return compressed.charCodeAt(index);
});
}
function _decompress(length: any, resetValue: any, getNextValue: any): any {
let dictionary: any = [], next: any, enlargeIn: any = 4, dictSize: any = 4, numBits: any = 3, entry: any = "", result: any = [], i: any, w: any, bits: any, resb: any, maxpower: any, power: any, c: any, data: any = { val: getNextValue(0), position: resetValue, index: 1 };
for (i = 0; i < 3; i += 1) {
dictionary[i] = i;
}
bits = 0;
maxpower = Math.pow(2, 2);
power = 1;
while (power != maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position == 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
switch ((next = bits)) {
case 0:
bits = 0;
maxpower = Math.pow(2, 8);
power = 1;
while (power != maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position == 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
c = fromCharCode(bits);
break;
case 1:
bits = 0;
maxpower = Math.pow(2, 16);
power = 1;
while (power != maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position == 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
c = fromCharCode(bits);
break;
case 2:
return "";
}
dictionary[3] = c;
w = c;
result.push(c);
// eslint-disable-next-line no-constant-condition
while (true) {
if (data.index > length) {
return "";
}
bits = 0;
maxpower = Math.pow(2, numBits);
power = 1;
while (power != maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position == 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
switch ((c = bits)) {
case 0:
bits = 0;
maxpower = Math.pow(2, 8);
power = 1;
while (power != maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position == 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
dictionary[dictSize++] = fromCharCode(bits);
c = dictSize - 1;
enlargeIn--;
break;
case 1:
bits = 0;
maxpower = Math.pow(2, 16);
power = 1;
while (power != maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position == 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
dictionary[dictSize++] = fromCharCode(bits);
c = dictSize - 1;
enlargeIn--;
break;
case 2:
return result.join("");
}
if (enlargeIn == 0) {
enlargeIn = Math.pow(2, numBits);
numBits++;
}
if (dictionary[c]) {
// @ts-ignore
entry = dictionary[c];
}
else {
if (c === dictSize) {
entry = w + w.charAt(0);
}
else {
return null;
}
}
result.push(entry);
// Add w+entry[0] to the dictionary.
dictionary[dictSize++] = w + entry.charAt(0);
enlargeIn--;
w = entry;
if (enlargeIn == 0) {
enlargeIn = Math.pow(2, numBits);
numBits++;
}
}
}

@ -0,0 +1,362 @@
/* typehints:start */
import type { Application } from "../application";
/* typehints:end */
import { Signal, STOP_PROPAGATION } from "./signal";
import { arrayDeleteValue, waitNextFrame } from "./utils";
import { ClickDetector } from "./click_detector";
import { SOUNDS } from "../platform/sound";
import { InputReceiver } from "./input_receiver";
import { FormElement } from "./modal_dialog_forms";
import { globalConfig } from "./config";
import { getStringForKeyCode } from "../game/key_action_mapper";
import { createLogger } from "./logging";
import { T } from "../translations";
/*
* ***************************************************
*
* LEGACY CODE WARNING
*
* This is old code from yorg3.io and needs to be refactored
* @TODO
*
* ***************************************************
*/
const kbEnter: any = 13;
const kbCancel: any = 27;
const logger: any = createLogger("dialogs");
/**
* Basic text based dialog
*/
export class Dialog {
public app = app;
public title = title;
public contentHTML = contentHTML;
public type = type;
public buttonIds = buttons;
public closeButton = closeButton;
public closeRequested = new Signal();
public buttonSignals = {};
public valueChosen = new Signal();
public timeouts = [];
public clickDetectors = [];
public inputReciever = new InputReceiver("dialog-" + this.title);
public enterHandler = null;
public escapeHandler = null;
/**
*
* Constructs a new dialog with the given options
*/
constructor({ app, title, contentHTML, buttons, type = "info", closeButton = false }) {
for (let i: any = 0; i < buttons.length; ++i) {
if (G_IS_DEV && globalConfig.debug.disableTimedButtons) {
this.buttonIds[i] = this.buttonIds[i].replace(":timeout", "");
}
const buttonId: any = this.buttonIds[i].split(":")[0];
this.buttonSignals[buttonId] = new Signal();
}
this.inputReciever.keydown.add(this.handleKeydown, this);
}
/**
* Internal keydown handler
*/
handleKeydown({ keyCode, shift, alt, ctrl }: {
keyCode: number;
shift: boolean;
alt: boolean;
ctrl: boolean;
}): any {
if (keyCode === kbEnter && this.enterHandler) {
this.internalButtonHandler(this.enterHandler);
return STOP_PROPAGATION;
}
if (keyCode === kbCancel && this.escapeHandler) {
this.internalButtonHandler(this.escapeHandler);
return STOP_PROPAGATION;
}
}
internalButtonHandler(id: any, ...payload: any): any {
this.app.inputMgr.popReciever(this.inputReciever);
if (id !== "close-button") {
this.buttonSignals[id].dispatch(...payload);
}
this.closeRequested.dispatch();
}
createElement(): any {
const elem: any = document.createElement("div");
elem.classList.add("ingameDialog");
this.dialogElem = document.createElement("div");
this.dialogElem.classList.add("dialogInner");
if (this.type) {
this.dialogElem.classList.add(this.type);
}
elem.appendChild(this.dialogElem);
const title: any = document.createElement("h1");
title.innerText = this.title;
title.classList.add("title");
this.dialogElem.appendChild(title);
if (this.closeButton) {
this.dialogElem.classList.add("hasCloseButton");
const closeBtn: any = document.createElement("button");
closeBtn.classList.add("closeButton");
this.trackClicks(closeBtn, (): any => this.internalButtonHandler("close-button"), {
applyCssClass: "pressedSmallElement",
});
title.appendChild(closeBtn);
this.inputReciever.backButton.add((): any => this.internalButtonHandler("close-button"));
}
const content: any = document.createElement("div");
content.classList.add("content");
content.innerHTML = this.contentHTML;
this.dialogElem.appendChild(content);
if (this.buttonIds.length > 0) {
const buttons: any = document.createElement("div");
buttons.classList.add("buttons");
// Create buttons
for (let i: any = 0; i < this.buttonIds.length; ++i) {
const [buttonId, buttonStyle, rawParams]: any = this.buttonIds[i].split(":");
const button: any = document.createElement("button");
button.classList.add("button");
button.classList.add("styledButton");
button.classList.add(buttonStyle);
button.innerText = T.dialogs.buttons[buttonId];
const params: any = (rawParams || "").split("/");
const useTimeout: any = params.indexOf("timeout") >= 0;
const isEnter: any = params.indexOf("enter") >= 0;
const isEscape: any = params.indexOf("escape") >= 0;
if (isEscape && this.closeButton) {
logger.warn("Showing dialog with close button, and additional cancel button");
}
if (useTimeout) {
button.classList.add("timedButton");
const timeout: any = setTimeout((): any => {
button.classList.remove("timedButton");
arrayDeleteValue(this.timeouts, timeout);
}, 1000);
this.timeouts.push(timeout);
}
if (isEnter || isEscape) {
// if (this.app.settings.getShowKeyboardShortcuts()) {
// Show keybinding
const spacer: any = document.createElement("code");
spacer.classList.add("keybinding");
spacer.innerHTML = getStringForKeyCode(isEnter ? kbEnter : kbCancel);
button.appendChild(spacer);
// }
if (isEnter) {
this.enterHandler = buttonId;
}
if (isEscape) {
this.escapeHandler = buttonId;
}
}
this.trackClicks(button, (): any => this.internalButtonHandler(buttonId));
buttons.appendChild(button);
}
this.dialogElem.appendChild(buttons);
}
else {
this.dialogElem.classList.add("buttonless");
}
this.element = elem;
this.app.inputMgr.pushReciever(this.inputReciever);
return this.element;
}
setIndex(index: any): any {
this.element.style.zIndex = index;
}
destroy(): any {
if (!this.element) {
assert(false, "Tried to destroy dialog twice");
return;
}
// We need to do this here, because if the backbutton event gets
// dispatched to the modal dialogs, it will not call the internalButtonHandler,
// and thus our receiver stays attached the whole time
this.app.inputMgr.destroyReceiver(this.inputReciever);
for (let i: any = 0; i < this.clickDetectors.length; ++i) {
this.clickDetectors[i].cleanup();
}
this.clickDetectors = [];
this.element.remove();
this.element = null;
for (let i: any = 0; i < this.timeouts.length; ++i) {
clearTimeout(this.timeouts[i]);
}
this.timeouts = [];
}
hide(): any {
this.element.classList.remove("visible");
}
show(): any {
this.element.classList.add("visible");
}
/**
* Helper method to track clicks on an element
* {}
*/
trackClicks(elem: Element, handler: function():void, args: import("./click_detector").ClickDetectorConstructorArgs= = {}): ClickDetector {
const detector: any = new ClickDetector(elem, args);
detector.click.add(handler, this);
this.clickDetectors.push(detector);
return detector;
}
}
/**
* Dialog which simply shows a loading spinner
*/
export class DialogLoading extends Dialog {
public text = text;
constructor(app, text = "") {
super({
app,
title: "",
contentHTML: "",
buttons: [],
type: "loading",
});
// Loading dialog can not get closed with back button
this.inputReciever.backButton.removeAll();
this.inputReciever.context = "dialog-loading";
}
createElement(): any {
const elem: any = document.createElement("div");
elem.classList.add("ingameDialog");
elem.classList.add("loadingDialog");
this.element = elem;
if (this.text) {
const text: any = document.createElement("div");
text.classList.add("text");
text.innerText = this.text;
elem.appendChild(text);
}
const loader: any = document.createElement("div");
loader.classList.add("prefab_LoadingTextWithAnim");
loader.classList.add("loadingIndicator");
elem.appendChild(loader);
this.app.inputMgr.pushReciever(this.inputReciever);
return elem;
}
}
export class DialogOptionChooser extends Dialog {
public options = options;
public initialOption = options.active;
constructor({ app, title, options }) {
let html: any = "<div class='optionParent'>";
options.options.forEach(({ value, text, desc = null, iconPrefix = null }: any): any => {
const descHtml: any = desc ? `<span class="desc">${desc}</span>` : "";
let iconHtml: any = iconPrefix ? `<span class="icon icon-${iconPrefix}-${value}"></span>` : "";
html += `
<div class='option ${value === options.active ? "active" : ""} ${iconPrefix ? "hasIcon" : ""}' data-optionvalue='${value}'>
${iconHtml}
<span class='title'>${text}</span>
${descHtml}
</div>
`;
});
html += "</div>";
super({
app,
title,
contentHTML: html,
buttons: [],
type: "info",
closeButton: true,
});
this.buttonSignals.optionSelected = new Signal();
}
createElement(): any {
const div: any = super.createElement();
this.dialogElem.classList.add("optionChooserDialog");
div.querySelectorAll("[data-optionvalue]").forEach((handle: any): any => {
const value: any = handle.getAttribute("data-optionvalue");
if (!handle) {
logger.error("Failed to bind option value in dialog:", value);
return;
}
// Need click detector here to forward elements, otherwise scrolling does not work
const detector: any = new ClickDetector(handle, {
consumeEvents: false,
preventDefault: false,
clickSound: null,
applyCssClass: "pressedOption",
targetOnly: true,
});
this.clickDetectors.push(detector);
if (value !== this.initialOption) {
detector.click.add((): any => {
const selected: any = div.querySelector(".option.active");
if (selected) {
selected.classList.remove("active");
}
else {
logger.warn("No selected option");
}
handle.classList.add("active");
this.app.sound.playUiSound(SOUNDS.uiClick);
this.internalButtonHandler("optionSelected", value);
});
}
});
return div;
}
}
export class DialogWithForm extends Dialog {
public confirmButtonId = confirmButtonId;
public formElements = formElements;
public enterHandler = confirmButtonId;
constructor({ app, title, desc, formElements, buttons = ["cancel", "ok:good"], confirmButtonId = "ok", closeButton = true, }) {
let html: any = "";
html += desc + "<br>";
for (let i: any = 0; i < formElements.length; ++i) {
html += formElements[i].getHtml();
}
super({
app,
title: title,
contentHTML: html,
buttons: buttons,
type: "info",
closeButton,
});
}
internalButtonHandler(id: any, ...payload: any): any {
if (id === this.confirmButtonId) {
if (this.hasAnyInvalid()) {
this.dialogElem.classList.remove("errorShake");
waitNextFrame().then((): any => {
if (this.dialogElem) {
this.dialogElem.classList.add("errorShake");
}
});
this.app.sound.playUiSound(SOUNDS.uiError);
return;
}
}
super.internalButtonHandler(id, payload);
}
hasAnyInvalid(): any {
for (let i: any = 0; i < this.formElements.length; ++i) {
if (!this.formElements[i].isValid()) {
return true;
}
}
return false;
}
createElement(): any {
const div: any = super.createElement();
for (let i: any = 0; i < this.formElements.length; ++i) {
const elem: any = this.formElements[i];
elem.bindEvents(div, this.clickDetectors);
// elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested);
elem.valueChosen.add(this.valueChosen.dispatch, this.valueChosen);
}
waitNextFrame().then((): any => {
this.formElements[this.formElements.length - 1].focus();
});
return div;
}
}

@ -0,0 +1,189 @@
import { BaseItem } from "../game/base_item";
import { ClickDetector } from "./click_detector";
import { Signal } from "./signal";
/*
* ***************************************************
*
* LEGACY CODE WARNING
*
* This is old code from yorg3.io and needs to be refactored
* @TODO
*
* ***************************************************
*/
export class FormElement {
public id = id;
public label = label;
public valueChosen = new Signal();
constructor(id, label) {
}
getHtml(): any {
abstract;
return "";
}
getFormElement(parent: any): any {
return parent.querySelector("[data-formId='" + this.id + "']");
}
bindEvents(parent: any, clickTrackers: any): any {
abstract;
}
focus(): any { }
isValid(): any {
return true;
}
/** {} */
getValue(): any {
abstract;
}
}
export class FormElementInput extends FormElement {
public placeholder = placeholder;
public defaultValue = defaultValue;
public inputType = inputType;
public validator = validator;
public element = null;
constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) {
super(id, label);
}
getHtml(): any {
let classes: any = [];
let inputType: any = "text";
let maxlength: any = 256;
switch (this.inputType) {
case "text": {
classes.push("input-text");
break;
}
case "email": {
classes.push("input-email");
inputType = "email";
break;
}
case "token": {
classes.push("input-token");
inputType = "text";
maxlength = 4;
break;
}
}
return `
<div class="formElement input">
${this.label ? `<label>${this.label}</label>` : ""}
<input
type="${inputType}"
value="${this.defaultValue.replace(/["\\]+/gi, "")}"
maxlength="${maxlength}"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
class="${classes.join(" ")}"
placeholder="${this.placeholder.replace(/["\\]+/gi, "")}"
data-formId="${this.id}">
</div>
`;
}
bindEvents(parent: any, clickTrackers: any): any {
this.element = this.getFormElement(parent);
this.element.addEventListener("input", (event: any): any => this.updateErrorState());
this.updateErrorState();
}
updateErrorState(): any {
this.element.classList.toggle("errored", !this.isValid());
}
isValid(): any {
return !this.validator || this.validator(this.element.value);
}
getValue(): any {
return this.element.value;
}
setValue(value: any): any {
this.element.value = value;
this.updateErrorState();
}
focus(): any {
this.element.focus();
this.element.select();
}
}
export class FormElementCheckbox extends FormElement {
public defaultValue = defaultValue;
public value = this.defaultValue;
public element = null;
constructor({ id, label, defaultValue = true }) {
super(id, label);
}
getHtml(): any {
return `
<div class="formElement checkBoxFormElem">
${this.label ? `<label>${this.label}</label>` : ""}
<div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'>
<span class="knob"></span >
</div >
</div>
`;
}
bindEvents(parent: any, clickTrackers: any): any {
this.element = this.getFormElement(parent);
const detector: any = new ClickDetector(this.element, {
consumeEvents: false,
preventDefault: false,
});
clickTrackers.push(detector);
detector.click.add(this.toggle, this);
}
getValue(): any {
return this.value;
}
toggle(): any {
this.value = !this.value;
this.element.classList.toggle("checked", this.value);
}
focus(parent: any): any { }
}
export class FormElementItemChooser extends FormElement {
public items = items;
public element = null;
public chosenItem: BaseItem = null;
constructor({ id, label, items = [] }) {
super(id, label);
}
getHtml(): any {
let classes: any = [];
return `
<div class="formElement">
${this.label ? `<label>${this.label}</label>` : ""}
<div class="ingameItemChooser input" data-formId="${this.id}"></div>
</div>
`;
}
bindEvents(parent: HTMLElement, clickTrackers: Array<ClickDetector>): any {
this.element = this.getFormElement(parent);
for (let i: any = 0; i < this.items.length; ++i) {
const item: any = this.items[i];
const canvas: any = document.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
const context: any = canvas.getContext("2d");
item.drawFullSizeOnCanvas(context, 128);
this.element.appendChild(canvas);
const detector: any = new ClickDetector(canvas, {});
clickTrackers.push(detector);
detector.click.add((): any => {
this.chosenItem = item;
this.valueChosen.dispatch(item);
});
}
}
isValid(): any {
return true;
}
getValue(): any {
return null;
}
focus(): any { }
}

@ -0,0 +1,106 @@
function mathPolyfills(): any {
// Converts from degrees to radians.
Math.radians = function (degrees: any): any {
return (degrees * Math.PI) / 180.0;
};
// Converts from radians to degrees.
Math.degrees = function (radians: any): any {
return (radians * 180.0) / Math.PI;
};
}
function stringPolyfills(): any {
// https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
if (!String.prototype.padStart) {
String.prototype.padStart = function padStart(targetLength: any, padString: any): any {
targetLength = targetLength >> 0; //truncate if number, or convert non-number to 0;
padString = String(typeof padString !== "undefined" ? padString : " ");
if (this.length >= targetLength) {
return String(this);
}
else {
targetLength = targetLength - this.length;
if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
}
return padString.slice(0, targetLength) + String(this);
}
};
}
// https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd
if (!String.prototype.padEnd) {
String.prototype.padEnd = function padEnd(targetLength: any, padString: any): any {
targetLength = targetLength >> 0; //floor if number or convert non-number to 0;
padString = String(typeof padString !== "undefined" ? padString : " ");
if (this.length > targetLength) {
return String(this);
}
else {
targetLength = targetLength - this.length;
if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
}
return String(this) + padString.slice(0, targetLength);
}
};
}
}
function objectPolyfills(): any {
// https://github.com/tc39/proposal-object-values-entries/blob/master/polyfill.js
// @ts-ignore
const reduce: any = Function.bind.call(Function.call, Array.prototype.reduce);
// @ts-ignore
const isEnumerable: any = Function.bind.call(Function.call, Object.prototype.propertyIsEnumerable);
// @ts-ignore
const concat: any = Function.bind.call(Function.call, Array.prototype.concat);
const keys: any = Reflect.ownKeys;
// @ts-ignore
if (!Object.values) {
// @ts-ignore
Object.values = function values(O: any): any {
return reduce(keys(O), (v: any, k: any): any => concat(v, typeof k === "string" && isEnumerable(O, k) ? [O[k]] : []), []);
};
}
if (!Object.entries) {
// @ts-ignore
Object.entries = function entries(O: any): any {
return reduce(keys(O), (e: any, k: any): any => concat(e, typeof k === "string" && isEnumerable(O, k) ? [[k, O[k]]] : []), []);
};
}
}
function domPolyfills(): any {
// from:https://github.com/jserz/js_piece/blob/master/DOM/ChildNode/remove()/remove().md
(function (arr: any): any {
arr.forEach(function (item: any): any {
if (item.hasOwnProperty("remove")) {
return;
}
Object.defineProperty(item, "remove", {
configurable: true,
enumerable: true,
writable: true,
value: function remove(): any {
this.parentNode.removeChild(this);
},
});
});
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
}
function initPolyfills(): any {
mathPolyfills();
stringPolyfills();
objectPolyfills();
domPolyfills();
}
function initExtensions(): any {
String.prototype.replaceAll = function (search: any, replacement: any): any {
var target: any = this;
return target.split(search).join(replacement);
};
}
// Fetch polyfill
import "whatwg-fetch";
// Other polyfills
initPolyfills();
initExtensions();

@ -0,0 +1,24 @@
const queryString: any = require("query-string");
const options: any = queryString.parse(location.search);
export let queryParamOptions: any = {
embedProvider: null,
abtVariant: null,
campaign: null,
fbclid: null,
gclid: null,
};
if (options.embed) {
queryParamOptions.embedProvider = options.embed;
}
if (options.abtVariant) {
queryParamOptions.abtVariant = options.abtVariant;
}
if (options.fbclid) {
queryParamOptions.fbclid = options.fbclid;
}
if (options.gclid) {
queryParamOptions.gclid = options.gclid;
}
if (options.utm_campaign) {
queryParamOptions.campaign = options.utm_campaign;
}

@ -0,0 +1,253 @@
/* typehints:start */
import type { Application } from "../application";
/* typehints:end */
import { sha1, CRC_PREFIX, computeCrc } from "./sensitive_utils.encrypt";
import { createLogger } from "./logging";
import { FILE_NOT_FOUND } from "../platform/storage";
import { accessNestedPropertyReverse } from "./utils";
import { IS_DEBUG, globalConfig } from "./config";
import { ExplainedResult } from "./explained_result";
import { decompressX64, compressX64 } from "./lzstring";
import { asyncCompressor, compressionPrefix } from "./async_compression";
import { compressObject, decompressObject } from "../savegame/savegame_compressor";
const debounce: any = require("debounce-promise");
const logger: any = createLogger("read_write_proxy");
const salt: any = accessNestedPropertyReverse(globalConfig, ["file", "info"]);
// Helper which only writes / reads if verify() works. Also performs migration
export class ReadWriteProxy {
public app: Application = app;
public filename = filename;
public currentData: object = null;
public debouncedWrite = debounce(this.doWriteAsync.bind(this), 50);
constructor(app, filename) {
// TODO: EXTREMELY HACKY! To verify we need to do this a step later
if (G_IS_DEV && IS_DEBUG) {
setTimeout((): any => {
assert(this.verify(this.getDefaultData()).result, "Verify() failed for default data: " + this.verify(this.getDefaultData()).reason);
});
}
}
// -- Methods to override
/** {} */
verify(data: any): ExplainedResult {
abstract;
return ExplainedResult.bad();
}
// Should return the default data
getDefaultData(): any {
abstract;
return {};
}
// Should return the current version as an integer
getCurrentVersion(): any {
abstract;
return 0;
}
// Should migrate the data (Modify in place)
/** {} */
migrate(data: any): ExplainedResult {
abstract;
return ExplainedResult.bad();
}
// -- / Methods
// Resets whole data, returns promise
resetEverythingAsync(): any {
logger.warn("Reset data to default");
this.currentData = this.getDefaultData();
return this.writeAsync();
}
static serializeObject(obj: object): any {
const jsonString: any = JSON.stringify(compressObject(obj));
const checksum: any = computeCrc(jsonString + salt);
return compressionPrefix + compressX64(checksum + jsonString);
}
static deserializeObject(text: object): any {
const decompressed: any = decompressX64(text.substr(compressionPrefix.length));
if (!decompressed) {
// LZ string decompression failure
throw new Error("bad-content / decompression-failed");
}
if (decompressed.length < 40) {
// String too short
throw new Error("bad-content / payload-too-small");
}
// Compare stored checksum with actual checksum
const checksum: any = decompressed.substring(0, 40);
const jsonString: any = decompressed.substr(40);
const desiredChecksum: any = checksum.startsWith(CRC_PREFIX)
? computeCrc(jsonString + salt)
: sha1(jsonString + salt);
if (desiredChecksum !== checksum) {
// Checksum mismatch
throw new Error("bad-content / checksum-mismatch");
}
const parsed: any = JSON.parse(jsonString);
const decoded: any = decompressObject(parsed);
return decoded;
}
/**
* Writes the data asychronously, fails if verify() fails.
* Debounces the operation by up to 50ms
* {}
*/
writeAsync(): Promise<void> {
const verifyResult: any = this.internalVerifyEntry(this.currentData);
if (!verifyResult.result) {
logger.error("Tried to write invalid data to", this.filename, "reason:", verifyResult.reason);
return Promise.reject(verifyResult.reason);
}
return this.debouncedWrite();
}
/**
* Actually writes the data asychronously
* {}
*/
doWriteAsync(): Promise<void> {
return asyncCompressor
.compressObjectAsync(this.currentData)
.then((compressed: any): any => {
return this.app.storage.writeFileAsync(this.filename, compressed);
})
.then((): any => {
logger.log("📄 Wrote", this.filename);
})
.catch((err: any): any => {
logger.error("Failed to write", this.filename, ":", err);
throw err;
});
}
// Reads the data asynchronously, fails if verify() fails
readAsync(): any {
// Start read request
return (this.app.storage
.readFileAsync(this.filename)
// Check for errors during read
.catch((err: any): any => {
if (err === FILE_NOT_FOUND) {
logger.log("File not found, using default data");
// File not found or unreadable, assume default file
return Promise.resolve(null);
}
return Promise.reject("file-error: " + err);
})
// Decrypt data (if its encrypted)
// @ts-ignore
.then((rawData: any): any => {
if (rawData == null) {
// So, the file has not been found, use default data
return JSON.stringify(compressObject(this.getDefaultData()));
}
if (rawData.startsWith(compressionPrefix)) {
const decompressed: any = decompressX64(rawData.substr(compressionPrefix.length));
if (!decompressed) {
// LZ string decompression failure
return Promise.reject("bad-content / decompression-failed");
}
if (decompressed.length < 40) {
// String too short
return Promise.reject("bad-content / payload-too-small");
}
// Compare stored checksum with actual checksum
const checksum: any = decompressed.substring(0, 40);
const jsonString: any = decompressed.substr(40);
const desiredChecksum: any = checksum.startsWith(CRC_PREFIX)
? computeCrc(jsonString + salt)
: sha1(jsonString + salt);
if (desiredChecksum !== checksum) {
// Checksum mismatch
return Promise.reject("bad-content / checksum-mismatch: " + desiredChecksum + " vs " + checksum);
}
return jsonString;
}
else {
if (!G_IS_DEV) {
return Promise.reject("bad-content / missing-compression");
}
}
return rawData;
})
// Parse JSON, this could throw but that's fine
.then((res: any): any => {
try {
return JSON.parse(res);
}
catch (ex: any) {
logger.error("Failed to parse file content of", this.filename, ":", ex, "(content was:", res, ")");
throw new Error("invalid-serialized-data");
}
})
// Decompress
.then((compressed: any): any => decompressObject(compressed))
// Verify basic structure
.then((contents: any): any => {
const result: any = this.internalVerifyBasicStructure(contents);
if (!result.isGood()) {
return Promise.reject("verify-failed: " + result.reason);
}
return contents;
})
// Check version and migrate if required
.then((contents: any): any => {
if (contents.version > this.getCurrentVersion()) {
return Promise.reject("stored-data-is-newer");
}
if (contents.version < this.getCurrentVersion()) {
logger.log("Trying to migrate data object from version", contents.version, "to", this.getCurrentVersion());
const migrationResult: any = this.migrate(contents); // modify in place
if (migrationResult.isBad()) {
return Promise.reject("migration-failed: " + migrationResult.reason);
}
}
return contents;
})
// Verify
.then((contents: any): any => {
const verifyResult: any = this.internalVerifyEntry(contents);
if (!verifyResult.result) {
logger.error("Read invalid data from", this.filename, "reason:", verifyResult.reason, "contents:", contents);
return Promise.reject("invalid-data: " + verifyResult.reason);
}
return contents;
})
// Store
.then((contents: any): any => {
this.currentData = contents;
logger.log("📄 Read data with version", this.currentData.version, "from", this.filename);
return contents;
})
// Catchall
.catch((err: any): any => {
return Promise.reject("Failed to read " + this.filename + ": " + err);
}));
}
/**
* Deletes the file
* {}
*/
deleteAsync(): Promise<void> {
return this.app.storage.deleteFileAsync(this.filename);
}
// Internal
/** {} */
internalVerifyBasicStructure(data: any): ExplainedResult {
if (!data) {
return ExplainedResult.bad("Data is empty");
}
if (!Number.isInteger(data.version) || data.version < 0) {
return ExplainedResult.bad(`Data has invalid version: ${data.version} (expected ${this.getCurrentVersion()})`);
}
return ExplainedResult.good();
}
/** {} */
internalVerifyEntry(data: any): ExplainedResult {
if (data.version !== this.getCurrentVersion()) {
return ExplainedResult.bad("Version mismatch, got " + data.version + " and expected " + this.getCurrentVersion());
}
const verifyStructureError: any = this.internalVerifyBasicStructure(data);
if (!verifyStructureError.isGood()) {
return verifyStructureError;
}
return this.verify(data);
}
}

@ -0,0 +1,283 @@
import { globalConfig } from "./config";
import { epsilonCompare, round2Digits } from "./utils";
import { Vector } from "./vector";
export class Rectangle {
public x = x;
public y = y;
public w = w;
public h = h;
constructor(x = 0, y = 0, w = 0, h = 0) {
}
/**
* Creates a rectangle from top right bottom and left offsets
*/
static fromTRBL(top: number, right: number, bottom: number, left: number): any {
return new Rectangle(left, top, right - left, bottom - top);
}
/**
* Constructs a new square rectangle
*/
static fromSquare(x: number, y: number, size: number): any {
return new Rectangle(x, y, size, size);
}
static fromTwoPoints(p1: Vector, p2: Vector): any {
const left: any = Math.min(p1.x, p2.x);
const top: any = Math.min(p1.y, p2.y);
const right: any = Math.max(p1.x, p2.x);
const bottom: any = Math.max(p1.y, p2.y);
return new Rectangle(left, top, right - left, bottom - top);
}
static centered(width: number, height: number): any {
return new Rectangle(-Math.ceil(width / 2), -Math.ceil(height / 2), width, height);
}
/**
* Returns if a intersects b
*/
static intersects(a: Rectangle, b: Rectangle): any {
return a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom;
}
/**
* Copies this instance
* {}
*/
clone(): Rectangle {
return new Rectangle(this.x, this.y, this.w, this.h);
}
/**
* Returns if this rectangle is empty
* {}
*/
isEmpty(): boolean {
return epsilonCompare(this.w * this.h, 0);
}
/**
* Returns if this rectangle is equal to the other while taking an epsilon into account
*/
equalsEpsilon(other: Rectangle, epsilon: number): any {
return (epsilonCompare(this.x, other.x, epsilon) &&
epsilonCompare(this.y, other.y, epsilon) &&
epsilonCompare(this.w, other.w, epsilon) &&
epsilonCompare(this.h, other.h, epsilon));
}
/**
* {}
*/
left(): number {
return this.x;
}
/**
* {}
*/
right(): number {
return this.x + this.w;
}
/**
* {}
*/
top(): number {
return this.y;
}
/**
* {}
*/
bottom(): number {
return this.y + this.h;
}
/**
* Returns Top, Right, Bottom, Left
* {}
*/
trbl(): [
number,
number,
number,
number
] {
return [this.y, this.right(), this.bottom(), this.x];
}
/**
* Returns the center of the rect
* {}
*/
getCenter(): Vector {
return new Vector(this.x + this.w / 2, this.y + this.h / 2);
}
/**
* Sets the right side of the rect without moving it
*/
setRight(right: number): any {
this.w = right - this.x;
}
/**
* Sets the bottom side of the rect without moving it
*/
setBottom(bottom: number): any {
this.h = bottom - this.y;
}
/**
* Sets the top side of the rect without scaling it
*/
setTop(top: number): any {
const bottom: any = this.bottom();
this.y = top;
this.setBottom(bottom);
}
/**
* Sets the left side of the rect without scaling it
*/
setLeft(left: number): any {
const right: any = this.right();
this.x = left;
this.setRight(right);
}
/**
* Returns the top left point
* {}
*/
topLeft(): Vector {
return new Vector(this.x, this.y);
}
/**
* Returns the bottom left point
* {}
*/
bottomRight(): Vector {
return new Vector(this.right(), this.bottom());
}
/**
* Moves the rectangle by the given parameters
*/
moveBy(x: number, y: number): any {
this.x += x;
this.y += y;
}
/**
* Moves the rectangle by the given vector
*/
moveByVector(vec: Vector): any {
this.x += vec.x;
this.y += vec.y;
}
/**
* Scales every parameter (w, h, x, y) by the given factor. Useful to transform from world to
* tile space and vice versa
*/
allScaled(factor: number): any {
return new Rectangle(this.x * factor, this.y * factor, this.w * factor, this.h * factor);
}
/**
* Expands the rectangle in all directions
* {} new rectangle
*/
expandedInAllDirections(amount: number): Rectangle {
return new Rectangle(this.x - amount, this.y - amount, this.w + 2 * amount, this.h + 2 * amount);
}
/**
* Returns if the given rectangle is contained
* {}
*/
containsRect(rect: Rectangle): boolean {
return (this.x <= rect.right() &&
rect.x <= this.right() &&
this.y <= rect.bottom() &&
rect.y <= this.bottom());
}
/**
* Returns if this rectangle contains the other rectangle specified by the parameters
* {}
*/
containsRect4Params(x: number, y: number, w: number, h: number): boolean {
return this.x <= x + w && x <= this.right() && this.y <= y + h && y <= this.bottom();
}
/**
* Returns if the rectangle contains the given circle at (x, y) with the radius (radius)
* {}
*/
containsCircle(x: number, y: number, radius: number): boolean {
return (this.x <= x + radius &&
x - radius <= this.right() &&
this.y <= y + radius &&
y - radius <= this.bottom());
}
/**
* Returns if the rectangle contains the given point
* {}
*/
containsPoint(x: number, y: number): boolean {
return x >= this.x && x < this.right() && y >= this.y && y < this.bottom();
}
/**
* Returns the shared area with another rectangle, or null if there is no intersection
* {}
*/
getIntersection(rect: Rectangle): Rectangle | null {
const left: any = Math.max(this.x, rect.x);
const top: any = Math.max(this.y, rect.y);
const right: any = Math.min(this.x + this.w, rect.x + rect.w);
const bottom: any = Math.min(this.y + this.h, rect.y + rect.h);
if (right <= left || bottom <= top) {
return null;
}
return Rectangle.fromTRBL(top, right, bottom, left);
}
/**
* Returns whether the rectangle fully intersects the given rectangle
*/
intersectsFully(rect: Rectangle): any {
const intersection: any = this.getIntersection(rect);
return intersection && Math.abs(intersection.w * intersection.h - rect.w * rect.h) < 0.001;
}
/**
* Returns the union of this rectangle with another
*/
getUnion(rect: Rectangle): any {
if (this.isEmpty()) {
// If this is rect is empty, return the other one
return rect.clone();
}
if (rect.isEmpty()) {
// If the other is empty, return this one
return this.clone();
}
// Find contained area
const left: any = Math.min(this.x, rect.x);
const top: any = Math.min(this.y, rect.y);
const right: any = Math.max(this.right(), rect.right());
const bottom: any = Math.max(this.bottom(), rect.bottom());
return Rectangle.fromTRBL(top, right, bottom, left);
}
/**
* Good for caching stuff
*/
toCompareableString(): any {
return (round2Digits(this.x) +
"/" +
round2Digits(this.y) +
"/" +
round2Digits(this.w) +
"/" +
round2Digits(this.h));
}
/**
* Good for printing stuff
*/
toString(): any {
return ("[x:" +
round2Digits(this.x) +
"| y:" +
round2Digits(this.y) +
"| w:" +
round2Digits(this.w) +
"| h:" +
round2Digits(this.h) +
"]");
}
/**
* Returns a new rectangle in tile space which includes all tiles which are visible in this rect
* {}
*/
toTileCullRectangle(): Rectangle {
return new Rectangle(Math.floor(this.x / globalConfig.tileSize), Math.floor(this.y / globalConfig.tileSize), Math.ceil(this.w / globalConfig.tileSize), Math.ceil(this.h / globalConfig.tileSize));
}
}

@ -0,0 +1,61 @@
import { createLogger } from "./logging";
import { fastArrayDeleteValueIfContained } from "./utils";
const logger: any = createLogger("request_channel");
// Thrown when a request is aborted
export const PROMISE_ABORTED: any = "promise-aborted";
export class RequestChannel {
public pendingPromises: Array<Promise> = [];
constructor() {
}
/**
*
* {}
*/
watch(promise: Promise<any>): Promise<any> {
// log(this, "Added new promise:", promise, "(pending =", this.pendingPromises.length, ")");
let cancelled: any = false;
const wrappedPromise: any = new Promise((resolve: any, reject: any): any => {
promise.then((result: any): any => {
// Remove from pending promises
fastArrayDeleteValueIfContained(this.pendingPromises, wrappedPromise);
// If not cancelled, resolve promise with same payload
if (!cancelled) {
resolve.call(this, result);
}
else {
logger.warn("Not resolving because promise got cancelled");
// reject.call(this, PROMISE_ABORTED);
}
}, (err: any): any => {
// Remove from pending promises
fastArrayDeleteValueIfContained(this.pendingPromises, wrappedPromise);
// If not cancelled, reject promise with same payload
if (!cancelled) {
reject.call(this, err);
}
else {
logger.warn("Not rejecting because promise got cancelled");
// reject.call(this, PROMISE_ABORTED);
}
});
});
// Add cancel handler
// @ts-ignore
wrappedPromise.cancel = function (): any {
cancelled = true;
};
this.pendingPromises.push(wrappedPromise);
return wrappedPromise;
}
cancelAll(): any {
if (this.pendingPromises.length > 0) {
logger.log("Cancel all pending promises (", this.pendingPromises.length, ")");
}
for (let i: any = 0; i < this.pendingPromises.length; ++i) {
// @ts-ignore
this.pendingPromises[i].cancel();
}
this.pendingPromises = [];
}
}

@ -0,0 +1,100 @@
/* typehints:start */
import type { Application } from "../application";
/* typehints:end */
import { ExplainedResult } from "./explained_result";
import { ReadWriteProxy } from "./read_write_proxy";
import { WEB_STEAM_SSO_AUTHENTICATED } from "./steam_sso";
export class RestrictionManager extends ReadWriteProxy {
public currentData = this.getDefaultData();
constructor(app) {
super(app, "restriction-flags.bin");
}
// -- RW Proxy Impl
verify(data: any): any {
return ExplainedResult.good();
}
getDefaultData(): any {
return {
version: this.getCurrentVersion(),
};
}
getCurrentVersion(): any {
return 1;
}
migrate(data: any): any {
return ExplainedResult.good();
}
initialize(): any {
return this.readAsync();
}
// -- End RW Proxy Impl
/**
* Returns if the app is currently running as the limited version
* {}
*/
isLimitedVersion(): boolean {
if (G_IS_STANDALONE) {
// Standalone is never limited
return false;
}
if (WEB_STEAM_SSO_AUTHENTICATED) {
return false;
}
if (G_IS_DEV) {
return typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0;
}
return true;
}
/**
* Returns if the app markets the standalone version on steam
* {}
*/
getIsStandaloneMarketingActive(): boolean {
return this.isLimitedVersion();
}
/**
* Returns if exporting the base as a screenshot is possible
* {}
*/
getIsExportingScreenshotsPossible(): boolean {
return !this.isLimitedVersion();
}
/**
* Returns the maximum number of supported waypoints
* {}
*/
getMaximumWaypoints(): number {
return this.isLimitedVersion() ? 2 : 1e20;
}
/**
* Returns if the user has unlimited savegames
* {}
*/
getHasUnlimitedSavegames(): boolean {
return !this.isLimitedVersion();
}
/**
* Returns if the app has all settings available
* {}
*/
getHasExtendedSettings(): boolean {
return !this.isLimitedVersion();
}
/**
* Returns if all upgrades are available
* {}
*/
getHasExtendedUpgrades(): boolean {
return !this.isLimitedVersion();
}
/**
* Returns if all levels & freeplay is available
* {}
*/
getHasExtendedLevelsAndFreeplay(): boolean {
return !this.isLimitedVersion();
}
}

@ -0,0 +1,102 @@
// ALEA RNG
function Mash(): any {
var n: any = 0xefc8249d;
return function (data: any): any {
data = data.toString();
for (var i: any = 0; i < data.length; i++) {
n += data.charCodeAt(i);
var h: any = 0.02519603282416938 * n;
n = h >>> 0;
h -= n;
h *= n;
n = h >>> 0;
h -= n;
n += h * 0x100000000; // 2^32
}
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
};
}
function makeNewRng(seed: number | string): any {
// Johannes Baagøe <baagoe@baagoe.com>, 2010
var c: any = 1;
var mash: any = Mash();
let s0: any = mash(" ");
let s1: any = mash(" ");
let s2: any = mash(" ");
s0 -= mash(seed);
if (s0 < 0) {
s0 += 1;
}
s1 -= mash(seed);
if (s1 < 0) {
s1 += 1;
}
s2 -= mash(seed);
if (s2 < 0) {
s2 += 1;
}
mash = null;
var random: any = function (): any {
var t: any = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32
s0 = s1;
s1 = s2;
return (s2 = t - (c = t | 0));
};
random.exportState = function (): any {
return [s0, s1, s2, c];
};
random.importState = function (i: any): any {
s0 = +i[0] || 0;
s1 = +i[1] || 0;
s2 = +i[2] || 0;
c = +i[3] || 0;
};
return random;
}
export class RandomNumberGenerator {
public internalRng = makeNewRng(seed || Math.random());
constructor(seed) {
}
/**
* Re-seeds the generator
*/
reseed(seed: number | string): any {
this.internalRng = makeNewRng(seed || Math.random());
}
/**
* {} between 0 and 1
*/
next(): number {
return this.internalRng();
}
/**
* Random choice of an array
*/
choice(array: array): any {
const index: any = this.nextIntRange(0, array.length);
return array[index];
}
/**
* {} Integer in range [min, max[
*/
nextIntRange(min: number, max: number): number {
assert(Number.isFinite(min), "Minimum is no integer");
assert(Number.isFinite(max), "Maximum is no integer");
assert(max > min, "rng: max <= min");
return Math.floor(this.next() * (max - min) + min);
}
/**
* {} Number in range [min, max[
*/
nextRange(min: number, max: number): number {
assert(max > min, "rng: max <= min");
return this.next() * (max - min) + min;
}
/**
* Updates the seed
*/
setSeed(seed: number): any {
this.internalRng = makeNewRng(seed);
}
}

@ -0,0 +1,18 @@
import { createHash } from "rusha";
import crc32 from "crc/crc32";
import { decompressX64 } from "./lzstring";
export function sha1(str: any): any {
return createHash().update(str).digest("hex");
}
// Window.location.host
export function getNameOfProvider(): any {
return window[decompressX64("DYewxghgLgliB2Q")][decompressX64("BYewzgLgdghgtgUyA")];
}
// Distinguish legacy crc prefixes
export const CRC_PREFIX: any = "crc32".padEnd(32, "-");
/**
* Computes the crc for a given string
*/
export function computeCrc(str: string): any {
return CRC_PREFIX + crc32(str).toString(16).padStart(8, "0");
}

@ -0,0 +1,65 @@
export const STOP_PROPAGATION: any = "stop_propagation";
export class Signal {
public receivers = [];
public modifyCount = 0;
constructor() {
}
/**
* Adds a new signal listener
*/
add(receiver: function, scope: object = null): any {
assert(receiver, "receiver is null");
this.receivers.push({ receiver, scope });
++this.modifyCount;
}
/**
* Adds a new signal listener
*/
addToTop(receiver: function, scope: object = null): any {
assert(receiver, "receiver is null");
this.receivers.unshift({ receiver, scope });
++this.modifyCount;
}
/**
* Dispatches the signal
* @param {} payload
*/
dispatch(): any {
const modifyState: any = this.modifyCount;
const n: any = this.receivers.length;
for (let i: any = 0; i < n; ++i) {
const { receiver, scope }: any = this.receivers[i];
if (receiver.apply(scope, arguments) === STOP_PROPAGATION) {
return STOP_PROPAGATION;
}
if (modifyState !== this.modifyCount) {
// Signal got modified during iteration
return STOP_PROPAGATION;
}
}
}
/**
* Removes a receiver
*/
remove(receiver: function): any {
let index: any = null;
const n: any = this.receivers.length;
for (let i: any = 0; i < n; ++i) {
if (this.receivers[i].receiver === receiver) {
index = i;
break;
}
}
assert(index !== null, "Receiver not found in list");
this.receivers.splice(index, 1);
++this.modifyCount;
}
/**
* Removes all receivers
*/
removeAll(): any {
this.receivers = [];
++this.modifyCount;
}
}

@ -0,0 +1,81 @@
import { createLogger } from "./logging";
const logger: any = createLogger("singleton_factory");
// simple factory pattern
export class SingletonFactory {
public id = id;
public entries = [];
public idToEntry = {};
constructor(id) {
}
getId(): any {
return this.id;
}
register(classHandle: any): any {
// First, construct instance
const instance: any = new classHandle();
// Extract id
const id: any = instance.getId();
assert(id, "Factory: Invalid id for class " + classHandle.name + ": " + id);
// Check duplicates
assert(!this.idToEntry[id], "Duplicate factory entry for " + id);
// Insert
this.entries.push(instance);
this.idToEntry[id] = instance;
}
/**
* Checks if a given id is registered
* {}
*/
hasId(id: string): boolean {
return !!this.idToEntry[id];
}
/**
* Finds an instance by a given id
* {}
*/
findById(id: string): object {
const entry: any = this.idToEntry[id];
if (!entry) {
logger.error("Object with id", id, "is not registered!");
assert(false, "Factory: Object with id '" + id + "' is not registered!");
return null;
}
return entry;
}
/**
* Finds an instance by its constructor (The class handle)
* {}
*/
findByClass(classHandle: object): object {
for (let i: any = 0; i < this.entries.length; ++i) {
if (this.entries[i] instanceof classHandle) {
return this.entries[i];
}
}
assert(false, "Factory: Object not found by classHandle (classid: " + classHandle.name + ")");
return null;
}
/**
* Returns all entries
* {}
*/
getEntries(): Array<object> {
return this.entries;
}
/**
* Returns all registered ids
* {}
*/
getAllIds(): Array<string> {
return Object.keys(this.idToEntry);
}
/**
* Returns amount of stored entries
* {}
*/
getNumEntries(): number {
return this.entries.length;
}
}

@ -0,0 +1,273 @@
import { DrawParameters } from "./draw_parameters";
import { Rectangle } from "./rectangle";
import { round3Digits } from "./utils";
export const ORIGINAL_SPRITE_SCALE: any = "0.75";
export const FULL_CLIP_RECT: any = new Rectangle(0, 0, 1, 1);
const EXTRUDE: any = 0.1;
export class BaseSprite {
/**
* Returns the raw handle
* {}
* @abstract
*/
getRawTexture(): HTMLImageElement | HTMLCanvasElement {
abstract;
return null;
}
/**
* Draws the sprite
*/
draw(context: CanvasRenderingContext2D, x: number, y: number, w: number, h: number): any {
// eslint-disable-line no-unused-vars
abstract;
}
}
/**
* Position of a sprite within an atlas
*/
export class SpriteAtlasLink {
public packedX = packedX;
public packedY = packedY;
public packedW = packedW;
public packedH = packedH;
public packOffsetX = packOffsetX;
public packOffsetY = packOffsetY;
public atlas = atlas;
public w = w;
public h = h;
constructor({ w, h, packedX, packedY, packOffsetX, packOffsetY, packedW, packedH, atlas }) {
}
}
export class AtlasSprite extends BaseSprite {
public linksByResolution: {
[idx: string]: SpriteAtlasLink;
} = {};
public spriteName = spriteName;
public frozen = false;
constructor(spriteName = "sprite") {
super();
}
getRawTexture(): any {
return this.linksByResolution[ORIGINAL_SPRITE_SCALE].atlas;
}
/**
* Draws the sprite onto a regular context using no contexts
* @see {BaseSprite.draw}
*/
draw(context: any, x: any, y: any, w: any, h: any): any {
if (G_IS_DEV) {
assert(context instanceof CanvasRenderingContext2D, "Not a valid context");
}
const link: any = this.linksByResolution[ORIGINAL_SPRITE_SCALE];
if (!link) {
throw new Error("draw: Link for " +
this.spriteName +
" not known: " +
ORIGINAL_SPRITE_SCALE +
" (having " +
Object.keys(this.linksByResolution) +
")");
}
const width: any = w || link.w;
const height: any = h || link.h;
const scaleW: any = width / link.w;
const scaleH: any = height / link.h;
context.drawImage(link.atlas, link.packedX, link.packedY, link.packedW, link.packedH, x + link.packOffsetX * scaleW, y + link.packOffsetY * scaleH, link.packedW * scaleW, link.packedH * scaleH);
}
drawCachedCentered(parameters: DrawParameters, x: number, y: number, size: number, clipping: boolean= = true): any {
this.drawCached(parameters, x - size / 2, y - size / 2, size, size, clipping);
}
drawCentered(context: CanvasRenderingContext2D, x: number, y: number, size: number): any {
this.draw(context, x - size / 2, y - size / 2, size, size);
}
/**
* Draws the sprite
*/
drawCached(parameters: DrawParameters, x: number, y: number, w: number = null, h: number = null, clipping: boolean= = true): any {
if (G_IS_DEV) {
assert(parameters instanceof DrawParameters, "Not a valid context");
assert(!!w && w > 0, "Not a valid width:" + w);
assert(!!h && h > 0, "Not a valid height:" + h);
}
const visibleRect: any = parameters.visibleRect;
const scale: any = parameters.desiredAtlasScale;
const link: any = this.linksByResolution[scale];
if (!link) {
throw new Error("drawCached: Link for " +
this.spriteName +
" at scale " +
scale +
" not known (having " +
Object.keys(this.linksByResolution) +
")");
}
const scaleW: any = w / link.w;
const scaleH: any = h / link.h;
let destX: any = x + link.packOffsetX * scaleW;
let destY: any = y + link.packOffsetY * scaleH;
let destW: any = link.packedW * scaleW;
let destH: any = link.packedH * scaleH;
let srcX: any = link.packedX;
let srcY: any = link.packedY;
let srcW: any = link.packedW;
let srcH: any = link.packedH;
let intersection: any = null;
if (clipping) {
const rect: any = new Rectangle(destX, destY, destW, destH);
intersection = rect.getIntersection(visibleRect);
if (!intersection) {
return;
}
srcX += (intersection.x - destX) / scaleW;
srcY += (intersection.y - destY) / scaleH;
srcW *= intersection.w / destW;
srcH *= intersection.h / destH;
destX = intersection.x;
destY = intersection.y;
destW = intersection.w;
destH = intersection.h;
}
parameters.context.drawImage(link.atlas,
// atlas src pos
srcX, srcY,
// atlas src size
srcW, srcH,
// dest pos and size
destX - EXTRUDE, destY - EXTRUDE, destW + 2 * EXTRUDE, destH + 2 * EXTRUDE);
}
/**
* Draws a subset of the sprite. Does NO culling
*/
drawCachedWithClipRect(parameters: DrawParameters, x: number, y: number, w: number = null, h: number = null, clipRect: Rectangle= = FULL_CLIP_RECT): any {
if (G_IS_DEV) {
assert(parameters instanceof DrawParameters, "Not a valid context");
assert(!!w && w > 0, "Not a valid width:" + w);
assert(!!h && h > 0, "Not a valid height:" + h);
assert(clipRect, "No clip rect given!");
}
const scale: any = parameters.desiredAtlasScale;
const link: any = this.linksByResolution[scale];
if (!link) {
throw new Error("drawCachedWithClipRect: Link for " +
this.spriteName +
" at scale " +
scale +
" not known (having " +
Object.keys(this.linksByResolution) +
")");
}
const scaleW: any = w / link.w;
const scaleH: any = h / link.h;
let destX: any = x + link.packOffsetX * scaleW + clipRect.x * w;
let destY: any = y + link.packOffsetY * scaleH + clipRect.y * h;
let destW: any = link.packedW * scaleW * clipRect.w;
let destH: any = link.packedH * scaleH * clipRect.h;
let srcX: any = link.packedX + clipRect.x * link.packedW;
let srcY: any = link.packedY + clipRect.y * link.packedH;
let srcW: any = link.packedW * clipRect.w;
let srcH: any = link.packedH * clipRect.h;
parameters.context.drawImage(link.atlas,
// atlas src pos
srcX, srcY,
// atlas src siize
srcW, srcH,
// dest pos and size
destX - EXTRUDE, destY - EXTRUDE, destW + 2 * EXTRUDE, destH + 2 * EXTRUDE);
}
/**
* Renders into an html element
*/
renderToHTMLElement(element: HTMLElement, w: number = 1, h: number = 1): any {
element.style.position = "relative";
element.innerHTML = this.getAsHTML(w, h);
}
/**
* Returns the html to render as icon
*/
getAsHTML(w: number, h: number): any {
const link: any = this.linksByResolution["0.5"];
if (!link) {
throw new Error("getAsHTML: Link for " +
this.spriteName +
" at scale 0.5" +
" not known (having " +
Object.keys(this.linksByResolution) +
")");
}
// Find out how much we have to scale it so that it fits
const scaleX: any = w / link.w;
const scaleY: any = h / link.h;
// Find out how big the scaled atlas is
const atlasW: any = link.atlas.width * scaleX;
const atlasH: any = link.atlas.height * scaleY;
// @ts-ignore
const srcSafe: any = link.atlas.src.replaceAll("\\", "/");
// Find out how big we render the sprite
const widthAbsolute: any = scaleX * link.packedW;
const heightAbsolute: any = scaleY * link.packedH;
// Compute the position in the relative container
const leftRelative: any = (link.packOffsetX * scaleX) / w;
const topRelative: any = (link.packOffsetY * scaleY) / h;
const widthRelative: any = widthAbsolute / w;
const heightRelative: any = heightAbsolute / h;
// Scale the atlas relative to the width and height of the element
const bgW: any = atlasW / widthAbsolute;
const bgH: any = atlasH / heightAbsolute;
// Figure out what the position of the atlas is
const bgX: any = link.packedX * scaleX;
const bgY: any = link.packedY * scaleY;
// Fuck you, whoever thought its a good idea to make background-position work like it does now
const bgXRelative: any = -bgX / (widthAbsolute - atlasW);
const bgYRelative: any = -bgY / (heightAbsolute - atlasH);
return `
<span class="spritesheetImage" style="
background-image: url('${srcSafe}');
left: ${round3Digits(leftRelative * 100.0)}%;
top: ${round3Digits(topRelative * 100.0)}%;
width: ${round3Digits(widthRelative * 100.0)}%;
height: ${round3Digits(heightRelative * 100.0)}%;
background-repeat: repeat;
background-position: ${round3Digits(bgXRelative * 100.0)}% ${round3Digits(bgYRelative * 100.0)}%;
background-size: ${round3Digits(bgW * 100.0)}% ${round3Digits(bgH * 100.0)}%;
"></span>
`;
}
}
export class RegularSprite extends BaseSprite {
public w = w;
public h = h;
public sprite = sprite;
constructor(sprite, w, h) {
super();
}
getRawTexture(): any {
return this.sprite;
}
/**
* Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing
* images into buffers
*/
draw(context: CanvasRenderingContext2D, x: number, y: number, w: number, h: number): any {
assert(context, "No context given");
assert(x !== undefined, "No x given");
assert(y !== undefined, "No y given");
assert(w !== undefined, "No width given");
assert(h !== undefined, "No height given");
context.drawImage(this.sprite, x, y, w, h);
}
/**
* Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing
* images into buffers
*/
drawCentered(context: CanvasRenderingContext2D, x: number, y: number, w: number, h: number): any {
assert(context, "No context given");
assert(x !== undefined, "No x given");
assert(y !== undefined, "No y given");
assert(w !== undefined, "No width given");
assert(h !== undefined, "No height given");
context.drawImage(this.sprite, x - w / 2, y - h / 2, w, h);
}
}

@ -0,0 +1,69 @@
import { Component } from "../game/component";
import { Entity } from "../game/entity";
import { globalConfig } from "./config";
import { createLogger } from "./logging";
import { Rectangle } from "./rectangle";
const logger: any = createLogger("stale_areas");
export class StaleAreaDetector {
public root = root;
public name = name;
public recomputeMethod = recomputeMethod;
public staleArea: Rectangle = null;
constructor({ root, name, recomputeMethod }) {
}
/**
* Invalidates the given area
*/
invalidate(area: Rectangle): any {
// logger.log(this.name, "invalidated", area.toString());
if (this.staleArea) {
this.staleArea = this.staleArea.getUnion(area);
}
else {
this.staleArea = area.clone();
}
}
/**
* Makes this detector recompute the area of an entity whenever
* it changes in any way
*/
recomputeOnComponentsChanged(components: Array<typeof Component>, tilesAround: number): any {
const componentIds: any = components.map((component: any): any => component.getId());
/**
* Internal checker method
*/
const checker: any = (entity: Entity): any => {
if (!this.root.gameInitialized) {
return;
}
// Check for all components
for (let i: any = 0; i < componentIds.length; ++i) {
if (entity.components[componentIds[i]]) {
// Entity is relevant, compute affected area
const area: any = entity.components.StaticMapEntity.getTileSpaceBounds().expandedInAllDirections(tilesAround);
this.invalidate(area);
return;
}
}
};
this.root.signals.entityAdded.add(checker);
this.root.signals.entityChanged.add(checker);
this.root.signals.entityComponentRemoved.add(checker);
this.root.signals.entityGotNewComponent.add(checker);
this.root.signals.entityDestroyed.add(checker);
}
/**
* Updates the stale area
*/
update(): any {
if (this.staleArea) {
if (G_IS_DEV && globalConfig.debug.renderChanges) {
logger.log(this.name, "is recomputing", this.staleArea.toString());
this.root.hud.parts.changesDebugger.renderChange(this.name, this.staleArea, "#fd145b");
}
this.recomputeMethod(this.staleArea);
this.staleArea = null;
}
}
}

@ -0,0 +1,103 @@
/* typehints:start*/
import type { Application } from "../application";
/* typehints:end*/
import { GameState } from "./game_state";
import { createLogger } from "./logging";
import { waitNextFrame, removeAllChildren } from "./utils";
import { MOD_SIGNALS } from "../mods/mod_signals";
const logger: any = createLogger("state_manager");
/**
* This is the main state machine which drives the game states.
*/
export class StateManager {
public app = app;
public currentState: GameState = null;
public stateClasses: {
[idx: string]: new () => GameState;
} = {};
constructor(app) {
}
/**
* Registers a new state class, should be a GameState derived class
*/
register(stateClass: object): any {
// Create a dummy to retrieve the key
const dummy: any = new stateClass();
assert(dummy instanceof GameState, "Not a state!");
const key: any = dummy.getKey();
assert(!this.stateClasses[key], `State '${key}' is already registered!`);
this.stateClasses[key] = stateClass;
}
/**
* Constructs a new state or returns the instance from the cache
*/
constructState(key: string): any {
if (this.stateClasses[key]) {
return new this.stateClasses[key]();
}
assert(false, `State '${key}' is not known!`);
}
/**
* Moves to a given state
*/
moveToState(key: string, payload: any = {}): any {
if (window.APP_ERROR_OCCURED) {
console.warn("Skipping state transition because of application crash");
return;
}
if (this.currentState) {
if (key === this.currentState.getKey()) {
logger.error(`State '${key}' is already active!`);
return false;
}
this.currentState.internalLeaveCallback();
// Remove all references
for (const stateKey: any in this.currentState) {
if (this.currentState.hasOwnProperty(stateKey)) {
delete this.currentState[stateKey];
}
}
this.currentState = null;
}
this.currentState = this.constructState(key);
this.currentState.internalRegisterCallback(this, this.app);
// Clean up old elements
if (this.currentState.getRemovePreviousContent()) {
removeAllChildren(document.body);
}
document.body.className = "gameState " + (this.currentState.getHasFadeIn() ? "" : "arrived");
document.body.id = "state_" + key;
if (this.currentState.getRemovePreviousContent()) {
document.body.innerHTML = this.currentState.internalGetFullHtml();
}
const dialogParent: any = document.createElement("div");
dialogParent.classList.add("modalDialogParent");
document.body.appendChild(dialogParent);
try {
this.currentState.internalEnterCallback(payload);
}
catch (ex: any) {
console.error(ex);
throw ex;
}
this.app.sound.playThemeMusic(this.currentState.getThemeMusic());
this.currentState.onResized(this.app.screenWidth, this.app.screenHeight);
this.app.analytics.trackStateEnter(key);
window.history.pushState({
key,
}, key);
MOD_SIGNALS.stateEntered.dispatch(this.currentState);
waitNextFrame().then((): any => {
document.body.classList.add("arrived");
});
return true;
}
/**
* Returns the current state
* {}
*/
getCurrentState(): GameState {
return this.currentState;
}
}

@ -0,0 +1,74 @@
import { T } from "../translations";
import { openStandaloneLink } from "./config";
export let WEB_STEAM_SSO_AUTHENTICATED: any = false;
export async function authorizeViaSSOToken(app: any, dialogs: any): any {
if (G_IS_STANDALONE) {
return;
}
if (window.location.search.includes("sso_logout_silent")) {
window.localStorage.setItem("steam_sso_auth_token", "");
window.location.replace("/");
return new Promise((): any => null);
}
if (window.location.search.includes("sso_logout")) {
const { ok }: any = dialogs.showWarning(T.dialogs.steamSsoError.title, T.dialogs.steamSsoError.desc);
window.localStorage.setItem("steam_sso_auth_token", "");
ok.add((): any => window.location.replace("/"));
return new Promise((): any => null);
}
if (window.location.search.includes("steam_sso_no_ownership")) {
const { ok, getStandalone }: any = dialogs.showWarning(T.dialogs.steamSsoNoOwnership.title, T.dialogs.steamSsoNoOwnership.desc, ["ok", "getStandalone:good"]);
window.localStorage.setItem("steam_sso_auth_token", "");
getStandalone.add((): any => {
openStandaloneLink(app, "sso_ownership");
window.location.replace("/");
});
ok.add((): any => window.location.replace("/"));
return new Promise((): any => null);
}
const token: any = window.localStorage.getItem("steam_sso_auth_token");
if (!token) {
return Promise.resolve();
}
const apiUrl: any = app.clientApi.getEndpoint();
console.warn("Authorizing via token:", token);
const verify: any = async (): any => {
const token: any = window.localStorage.getItem("steam_sso_auth_token");
if (!token) {
window.location.replace("?sso_logout");
return;
}
try {
const response: any = await Promise.race([
fetch(apiUrl + "/v1/sso/refresh", {
method: "POST",
body: token,
headers: {
"x-api-key": "d5c54aaa491f200709afff082c153ef2",
},
}),
new Promise((resolve: any, reject: any): any => {
setTimeout((): any => reject("timeout exceeded"), 20000);
}),
]);
const responseText: any = await response.json();
if (!responseText.token) {
console.warn("Failed to register");
window.localStorage.setItem("steam_sso_auth_token", "");
window.location.replace("?sso_logout");
return;
}
window.localStorage.setItem("steam_sso_auth_token", responseText.token);
app.clientApi.token = responseText.token;
WEB_STEAM_SSO_AUTHENTICATED = true;
}
catch (ex: any) {
console.warn("Auth failure", ex);
window.localStorage.setItem("steam_sso_auth_token", "");
window.location.replace("/");
return new Promise((): any => null);
}
};
await verify();
setInterval(verify, 120000);
}

@ -0,0 +1,133 @@
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { GameState } from "./game_state";
import { T } from "../translations";
/**
* Baseclass for all game states which are structured similary: A header with back button + some
* scrollable content.
*/
export class TextualGameState extends GameState {
///// INTERFACE ////
/**
* Should return the states inner html. If not overriden, will create a scrollable container
* with the content of getMainContentHTML()
* {}
*/
getInnerHTML(): string {
return `
<div class="content mainContent">
${this.getMainContentHTML()}
</div>
`;
}
/**
* Should return the states HTML content.
*/
getMainContentHTML(): any {
return "";
}
/**
* Should return the title of the game state. If null, no title and back button will
* get created
* {}
*/
getStateHeaderTitle(): string | null {
return null;
}
/////////////
/**
* Back button handler, can be overridden. Per default it goes back to the main menu,
* or if coming from the game it moves back to the game again.
*/
onBackButton(): any {
if (this.backToStateId) {
this.moveToState(this.backToStateId, this.backToStatePayload);
}
else {
this.moveToState(this.getDefaultPreviousState());
}
}
/**
* Returns the default state to go back to
*/
getDefaultPreviousState(): any {
return "MainMenuState";
}
/**
* Goes to a new state, telling him to go back to this state later
*/
moveToStateAddGoBack(stateId: string): any {
this.moveToState(stateId, {
backToStateId: this.key,
backToStatePayload: {
backToStateId: this.backToStateId,
backToStatePayload: this.backToStatePayload,
},
});
}
/**
* Removes all click detectors, except the one on the back button. Useful when regenerating
* content.
*/
clearClickDetectorsExceptHeader(): any {
for (let i: any = 0; i < this.clickDetectors.length; ++i) {
const detector: any = this.clickDetectors[i];
if (detector.element === this.headerElement) {
continue;
}
detector.cleanup();
this.clickDetectors.splice(i, 1);
i -= 1;
}
}
/**
* Overrides the GameState implementation to provide our own html
*/
internalGetFullHtml(): any {
let headerHtml: any = "";
if (this.getStateHeaderTitle()) {
headerHtml = `
<div class="headerBar">
<h1><button class="backButton"></button> ${this.getStateHeaderTitle()}</h1>
</div>`;
}
return `
${headerHtml}
<div class="container">
${this.getInnerHTML()}
</div>
`;
}
//// INTERNALS /////
/**
* Overrides the GameState leave callback to cleanup stuff
*/
internalLeaveCallback(): any {
super.internalLeaveCallback();
this.dialogs.cleanup();
}
/**
* Overrides the GameState enter callback to setup required stuff
*/
internalEnterCallback(payload: any): any {
super.internalEnterCallback(payload, false);
if (payload.backToStateId) {
this.backToStateId = payload.backToStateId;
this.backToStatePayload = payload.backToStatePayload;
}
this.htmlElement.classList.add("textualState");
if (this.getStateHeaderTitle()) {
this.htmlElement.classList.add("hasTitle");
}
this.containerElement = this.htmlElement.querySelector(".widthKeeper .container");
this.headerElement = this.htmlElement.querySelector(".headerBar > h1");
if (this.headerElement) {
this.trackClicks(this.headerElement, this.onBackButton);
}
this.dialogs = new HUDModalDialogs(null, this.app);
const dialogsElement: any = document.body.querySelector(".modalDialogParent");
this.dialogs.initializeToElement(dialogsElement);
this.onEnter(payload);
}
}

@ -0,0 +1,39 @@
export class TrackedState {
public lastSeenValue = null;
constructor(callbackMethod = null, callbackScope = null) {
if (callbackMethod) {
this.callback = callbackMethod;
if (callbackScope) {
this.callback = this.callback.bind(callbackScope);
}
}
}
set(value: any, changeHandler: any = null, changeScope: any = null): any {
if (value !== this.lastSeenValue) {
// Copy value since the changeHandler call could actually modify our lastSeenValue
const valueCopy: any = value;
this.lastSeenValue = value;
if (changeHandler) {
if (changeScope) {
changeHandler.call(changeScope, valueCopy);
}
else {
changeHandler(valueCopy);
}
}
else if (this.callback) {
this.callback(value);
}
else {
assert(false, "No callback specified");
}
}
}
setSilent(value: any): any {
this.lastSeenValue = value;
}
get(): any {
return this.lastSeenValue;
}
}

@ -0,0 +1,628 @@
import { T } from "../translations";
import { rando } from "@nastyox/rando.js";
import { WEB_STEAM_SSO_AUTHENTICATED } from "./steam_sso";
const bigNumberSuffixTranslationKeys: any = ["thousands", "millions", "billions", "trillions"];
/**
* Returns a platform name
* {}
*/
export function getPlatformName(): "android" | "browser" | "ios" | "standalone" | "unknown" {
if (G_IS_STANDALONE) {
return "standalone";
}
else if (G_IS_BROWSER) {
return "browser";
}
return "unknown";
}
/**
* Makes a new 2D array with undefined contents
* {}
*/
export function make2DUndefinedArray(w: number, h: number): Array<Array<any>> {
const result: any = new Array(w);
for (let x: any = 0; x < w; ++x) {
result[x] = new Array(h);
}
return result;
}
/**
* Creates a new map (an empty object without any props)
*/
export function newEmptyMap(): any {
return Object.create(null);
}
/**
* Returns a random integer in the range [start,end]
*/
export function randomInt(start: number, end: number): any {
return rando(start, end);
}
/**
* Access an object in a very annoying way, used for obsfuscation.
*/
export function accessNestedPropertyReverse(obj: any, keys: Array<string>): any {
let result: any = obj;
for (let i: any = keys.length - 1; i >= 0; --i) {
result = result[keys[i]];
}
return result;
}
/**
* Chooses a random entry of an array
* @template T
* {}
*/
export function randomChoice(arr: T[]): T {
return arr[Math.floor(Math.random() * arr.length)];
}
/**
* Deletes from an array by swapping with the last element
*/
export function fastArrayDelete(array: Array<any>, index: number): any {
if (index < 0 || index >= array.length) {
throw new Error("Out of bounds");
}
// When the element is not the last element
if (index !== array.length - 1) {
// Get the last element, and swap it with the one we want to delete
const last: any = array[array.length - 1];
array[index] = last;
}
// Finally remove the last element
array.length -= 1;
}
/**
* Deletes from an array by swapping with the last element. Searches
* for the value in the array first
*/
export function fastArrayDeleteValue(array: Array<any>, value: any): any {
if (array == null) {
throw new Error("Tried to delete from non array!");
}
const index: any = array.indexOf(value);
if (index < 0) {
console.error("Value", value, "not contained in array:", array, "!");
return value;
}
return fastArrayDelete(array, index);
}
/**
* @see fastArrayDeleteValue
*/
export function fastArrayDeleteValueIfContained(array: Array<any>, value: any): any {
if (array == null) {
throw new Error("Tried to delete from non array!");
}
const index: any = array.indexOf(value);
if (index < 0) {
return value;
}
return fastArrayDelete(array, index);
}
/**
* Deletes from an array at the given index
*/
export function arrayDelete(array: Array<any>, index: number): any {
if (index < 0 || index >= array.length) {
throw new Error("Out of bounds");
}
array.splice(index, 1);
}
/**
* Deletes the given value from an array
*/
export function arrayDeleteValue(array: Array<any>, value: any): any {
if (array == null) {
throw new Error("Tried to delete from non array!");
}
const index: any = array.indexOf(value);
if (index < 0) {
console.error("Value", value, "not contained in array:", array, "!");
return value;
}
return arrayDelete(array, index);
}
/**
* Compare two floats for epsilon equality
* {}
*/
export function epsilonCompare(a: number, b: number, epsilon: any = 1e-5): boolean {
return Math.abs(a - b) < epsilon;
}
/**
* Interpolates two numbers
*/
export function lerp(a: number, b: number, x: number): any {
return a * (1 - x) + b * x;
}
/**
* Finds a value which is nice to display, e.g. 15669 -> 15000. Also handles fractional stuff
*/
export function findNiceValue(num: number): any {
if (num > 1e8) {
return num;
}
if (num < 0.00001) {
return 0;
}
let roundAmount: any = 1;
if (num > 50000) {
roundAmount = 10000;
}
else if (num > 20000) {
roundAmount = 5000;
}
else if (num > 5000) {
roundAmount = 1000;
}
else if (num > 2000) {
roundAmount = 500;
}
else if (num > 1000) {
roundAmount = 100;
}
else if (num > 100) {
roundAmount = 20;
}
else if (num > 20) {
roundAmount = 5;
}
const niceValue: any = Math.floor(num / roundAmount) * roundAmount;
if (num >= 10) {
return Math.round(niceValue);
}
if (num >= 1) {
return Math.round(niceValue * 10) / 10;
}
return Math.round(niceValue * 100) / 100;
}
/**
* Finds a nice integer value
* @see findNiceValue
*/
export function findNiceIntegerValue(num: number): any {
return Math.ceil(findNiceValue(num));
}
/**
* Formats a big number
* {}
*/
export function formatBigNumber(num: number, separator: string= = T.global.decimalSeparator): string {
const sign: any = num < 0 ? "-" : "";
num = Math.abs(num);
if (num > 1e54) {
return sign + T.global.infinite;
}
if (num < 10 && !Number.isInteger(num)) {
return sign + num.toFixed(2);
}
if (num < 50 && !Number.isInteger(num)) {
return sign + num.toFixed(1);
}
num = Math.floor(num);
if (num < 1000) {
return sign + "" + num;
}
else {
let leadingDigits: any = num;
let suffix: any = "";
for (let suffixIndex: any = 0; suffixIndex < bigNumberSuffixTranslationKeys.length; ++suffixIndex) {
leadingDigits = leadingDigits / 1000;
suffix = T.global.suffix[bigNumberSuffixTranslationKeys[suffixIndex]];
if (leadingDigits < 1000) {
break;
}
}
const leadingDigitsRounded: any = round1Digit(leadingDigits);
const leadingDigitsNoTrailingDecimal: any = leadingDigitsRounded
.toString()
.replace(".0", "")
.replace(".", separator);
return sign + leadingDigitsNoTrailingDecimal + suffix;
}
}
/**
* Formats a big number, but does not add any suffix and instead uses its full representation
* {}
*/
export function formatBigNumberFull(num: number, divider: string= = T.global.thousandsDivider): string {
if (num < 1000) {
return num + "";
}
if (num > 1e54) {
return T.global.infinite;
}
let rest: any = num;
let out: any = "";
while (rest >= 1000) {
out = (rest % 1000).toString().padStart(3, "0") + divider + out;
rest = Math.floor(rest / 1000);
}
out = rest + divider + out;
return out.substring(0, out.length - 1);
}
/**
* Waits two frames so the ui is updated
* {}
*/
export function waitNextFrame(): Promise<void> {
return new Promise(function (resolve: any): any {
window.requestAnimationFrame(function (): any {
window.requestAnimationFrame(function (): any {
resolve();
});
});
});
}
/**
* Rounds 1 digit
* {}
*/
export function round1Digit(n: number): number {
return Math.floor(n * 10.0) / 10.0;
}
/**
* Rounds 2 digits
* {}
*/
export function round2Digits(n: number): number {
return Math.floor(n * 100.0) / 100.0;
}
/**
* Rounds 3 digits
* {}
*/
export function round3Digits(n: number): number {
return Math.floor(n * 1000.0) / 1000.0;
}
/**
* Rounds 4 digits
* {}
*/
export function round4Digits(n: number): number {
return Math.floor(n * 10000.0) / 10000.0;
}
/**
* Clamps a value between [min, max]
*/
export function clamp(v: number, minimum: number= = 0, maximum: number= = 1): any {
return Math.max(minimum, Math.min(maximum, v));
}
/**
* Helper method to create a new div element
*/
export function makeDivElement(id: string= = null, classes: Array<string>= = [], innerHTML: string= = ""): any {
const div: any = document.createElement("div");
if (id) {
div.id = id;
}
for (let i: any = 0; i < classes.length; ++i) {
div.classList.add(classes[i]);
}
div.innerHTML = innerHTML;
return div;
}
/**
* Helper method to create a new div
*/
export function makeDiv(parent: Element, id: string= = null, classes: Array<string>= = [], innerHTML: string= = ""): any {
const div: any = makeDivElement(id, classes, innerHTML);
parent.appendChild(div);
return div;
}
/**
* Helper method to create a new button element
*/
export function makeButtonElement(classes: Array<string>= = [], innerHTML: string= = ""): any {
const element: any = document.createElement("button");
for (let i: any = 0; i < classes.length; ++i) {
element.classList.add(classes[i]);
}
element.classList.add("styledButton");
element.innerHTML = innerHTML;
return element;
}
/**
* Helper method to create a new button
*/
export function makeButton(parent: Element, classes: Array<string>= = [], innerHTML: string= = ""): any {
const element: any = makeButtonElement(classes, innerHTML);
parent.appendChild(element);
return element;
}
/**
* Removes all children of the given element
*/
export function removeAllChildren(elem: Element): any {
if (elem) {
var range: any = document.createRange();
range.selectNodeContents(elem);
range.deleteContents();
}
}
/**
* Returns if the game supports this browser
*/
export function isSupportedBrowser(): any {
// please note,
// that IE11 now returns undefined again for window.chrome
// and new Opera 30 outputs true for window.chrome
// but needs to check if window.opr is not undefined
// and new IE Edge outputs to true now for window.chrome
// and if not iOS Chrome check
// so use the below updated condition
if (G_IS_STANDALONE) {
return true;
}
// @ts-ignore
var isChromium: any = window.chrome;
var winNav: any = window.navigator;
var vendorName: any = winNav.vendor;
// @ts-ignore
var isIEedge: any = winNav.userAgent.indexOf("Edge") > -1;
var isIOSChrome: any = winNav.userAgent.match("CriOS");
if (isIOSChrome) {
// is Google Chrome on IOS
return false;
}
else if (isChromium !== null &&
typeof isChromium !== "undefined" &&
vendorName === "Google Inc." &&
isIEedge === false) {
// is Google Chrome
return true;
}
else {
// not Google Chrome
return false;
}
}
/**
* Formats an amount of seconds into something like "5s ago"
* {}
*/
export function formatSecondsToTimeAgo(secs: number): string {
const seconds: any = Math.floor(secs);
const minutes: any = Math.floor(seconds / 60);
const hours: any = Math.floor(minutes / 60);
const days: any = Math.floor(hours / 24);
if (seconds < 60) {
if (seconds === 1) {
return T.global.time.oneSecondAgo;
}
return T.global.time.xSecondsAgo.replace("<x>", "" + seconds);
}
else if (minutes < 60) {
if (minutes === 1) {
return T.global.time.oneMinuteAgo;
}
return T.global.time.xMinutesAgo.replace("<x>", "" + minutes);
}
else if (hours < 24) {
if (hours === 1) {
return T.global.time.oneHourAgo;
}
return T.global.time.xHoursAgo.replace("<x>", "" + hours);
}
else {
if (days === 1) {
return T.global.time.oneDayAgo;
}
return T.global.time.xDaysAgo.replace("<x>", "" + days);
}
}
/**
* Formats seconds into a readable string like "5h 23m"
* {}
*/
export function formatSeconds(secs: number): string {
const trans: any = T.global.time;
secs = Math.ceil(secs);
if (secs < 60) {
return trans.secondsShort.replace("<seconds>", "" + secs);
}
else if (secs < 60 * 60) {
const minutes: any = Math.floor(secs / 60);
const seconds: any = secs % 60;
return trans.minutesAndSecondsShort
.replace("<seconds>", "" + seconds)
.replace("<minutes>", "" + minutes);
}
else {
const hours: any = Math.floor(secs / 3600);
const minutes: any = Math.floor(secs / 60) % 60;
return trans.hoursAndMinutesShort.replace("<minutes>", "" + minutes).replace("<hours>", "" + hours);
}
}
/**
* Formats a number like 2.51 to "2.5"
*/
export function round1DigitLocalized(speed: number, separator: string= = T.global.decimalSeparator): any {
return round1Digit(speed).toString().replace(".", separator);
}
/**
* Formats a number like 2.51 to "2.51 items / s"
*/
export function formatItemsPerSecond(speed: number, double: boolean= = false, separator: string= = T.global.decimalSeparator): any {
return ((speed === 1.0
? T.ingame.buildingPlacement.infoTexts.oneItemPerSecond
: T.ingame.buildingPlacement.infoTexts.itemsPerSecond.replace("<x>", round2Digits(speed).toString().replace(".", separator))) + (double ? " " + T.ingame.buildingPlacement.infoTexts.itemsPerSecondDouble : ""));
}
/**
* Rotates a flat 3x3 matrix clockwise
* Entries:
* 0 lo
* 1 mo
* 2 ro
* 3 lm
* 4 mm
* 5 rm
* 6 lu
* 7 mu
* 8 ru
*/
export function rotateFlatMatrix3x3(flatMatrix: Array<number>): any {
return [
flatMatrix[6],
flatMatrix[3],
flatMatrix[0],
flatMatrix[7],
flatMatrix[4],
flatMatrix[1],
flatMatrix[8],
flatMatrix[5],
flatMatrix[2],
];
}
/**
* Generates rotated variants of the matrix
* {}
*/
export function generateMatrixRotations(originalMatrix: Array<number>): Object<number, Array<number>> {
const result: any = {
0: originalMatrix,
};
originalMatrix = rotateFlatMatrix3x3(originalMatrix);
result[90] = originalMatrix;
originalMatrix = rotateFlatMatrix3x3(originalMatrix);
result[180] = originalMatrix;
originalMatrix = rotateFlatMatrix3x3(originalMatrix);
result[270] = originalMatrix;
return result;
}
/**
* Rotates a directional object
* {}
*/
export function rotateDirectionalObject(obj: DirectionalObject, rotation: any): DirectionalObject {
const queue: any = [obj.top, obj.right, obj.bottom, obj.left];
while (rotation !== 0) {
rotation -= 90;
queue.push(queue.shift());
}
return {
top: queue[0],
right: queue[1],
bottom: queue[2],
left: queue[3],
};
}
/**
* Modulo which works for negative numbers
*/
export function safeModulo(n: number, m: number): any {
return ((n % m) + m) % m;
}
/**
* Returns a smooth pulse between 0 and 1
* {}
*/
export function smoothPulse(time: number): number {
return Math.sin(time * 4) * 0.5 + 0.5;
}
/**
* Fills in a <link> tag
*/
export function fillInLinkIntoTranslation(translation: string, link: string): any {
return translation
.replace("<link>", "<a href='" + link + "' target='_blank'>")
.replace("</link>", "</a>");
}
/**
* Generates a file download
*/
export function generateFileDownload(filename: string, text: string): any {
var element: any = document.createElement("a");
element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text));
element.setAttribute("download", filename);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
/**
* Starts a file chooser
*/
export function startFileChoose(acceptedType: string = ".bin"): any {
var input: any = document.createElement("input");
input.type = "file";
input.accept = acceptedType;
return new Promise((resolve: any): any => {
input.onchange = (_: any): any => resolve(input.files[0]);
input.click();
});
}
const MAX_ROMAN_NUMBER: any = 49;
const romanLiteralsCache: any = ["0"];
/**
*
* {}
*/
export function getRomanNumber(number: number): string {
number = Math.max(0, Math.round(number));
if (romanLiteralsCache[number]) {
return romanLiteralsCache[number];
}
if (number > MAX_ROMAN_NUMBER) {
return String(number);
}
function formatDigit(digit: any, unit: any, quintuple: any, decuple: any): any {
switch (digit) {
case 0:
return "";
case 1: // I
return unit;
case 2: // II
return unit + unit;
case 3: // III
return unit + unit + unit;
case 4: // IV
return unit + quintuple;
case 9: // IX
return unit + decuple;
default:
// V, VI, VII, VIII
return quintuple + formatDigit(digit - 5, unit, quintuple, decuple);
}
}
let thousands: any = Math.floor(number / 1000);
let thousandsPart: any = "";
while (thousands > 0) {
thousandsPart += "M";
thousands -= 1;
}
const hundreds: any = Math.floor((number % 1000) / 100);
const hundredsPart: any = formatDigit(hundreds, "C", "D", "M");
const tens: any = Math.floor((number % 100) / 10);
const tensPart: any = formatDigit(tens, "X", "L", "C");
const units: any = number % 10;
const unitsPart: any = formatDigit(units, "I", "V", "X");
const formatted: any = thousandsPart + hundredsPart + tensPart + unitsPart;
romanLiteralsCache[number] = formatted;
return formatted;
}
/**
* Returns the appropriate logo sprite path
*/
export function getLogoSprite(): any {
if (G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED) {
return "logo.png";
}
if (G_IS_BROWSER) {
return "logo_demo.png";
}
return "logo.png";
}
/**
* Rejects a promise after X ms
*/
export function timeoutPromise(promise: Promise, timeout: any = 30000): any {
return Promise.race([
new Promise((resolve: any, reject: any): any => {
setTimeout((): any => reject("timeout of " + timeout + " ms exceeded"), timeout);
}),
promise,
]);
}

@ -0,0 +1,577 @@
import { globalConfig } from "./config";
import { safeModulo } from "./utils";
const tileSize: any = globalConfig.tileSize;
const halfTileSize: any = globalConfig.halfTileSize;
/**
* @enum {string}
*/
export const enumDirection: any = {
top: "top",
right: "right",
bottom: "bottom",
left: "left",
};
/**
* @enum {string}
*/
export const enumInvertedDirections: any = {
[enumDirection.top]: enumDirection.bottom,
[enumDirection.right]: enumDirection.left,
[enumDirection.bottom]: enumDirection.top,
[enumDirection.left]: enumDirection.right,
};
/**
* @enum {number}
*/
export const enumDirectionToAngle: any = {
[enumDirection.top]: 0,
[enumDirection.right]: 90,
[enumDirection.bottom]: 180,
[enumDirection.left]: 270,
};
/**
* @enum {enumDirection}
*/
export const enumAngleToDirection: any = {
0: enumDirection.top,
90: enumDirection.right,
180: enumDirection.bottom,
270: enumDirection.left,
};
export const arrayAllDirections: Array<enumDirection> = [
enumDirection.top,
enumDirection.right,
enumDirection.bottom,
enumDirection.left,
];
export class Vector {
public x = x || 0;
public y = y || 0;
constructor(x, y) {
}
/**
* return a copy of the vector
* {}
*/
copy(): Vector {
return new Vector(this.x, this.y);
}
/**
* Adds a vector and return a new vector
* {}
*/
add(other: Vector): Vector {
return new Vector(this.x + other.x, this.y + other.y);
}
/**
* Adds a vector
* {}
*/
addInplace(other: Vector): Vector {
this.x += other.x;
this.y += other.y;
return this;
}
/**
* Substracts a vector and return a new vector
* {}
*/
sub(other: Vector): Vector {
return new Vector(this.x - other.x, this.y - other.y);
}
/**
* Subs a vector
* {}
*/
subInplace(other: Vector): Vector {
this.x -= other.x;
this.y -= other.y;
return this;
}
/**
* Multiplies with a vector and return a new vector
* {}
*/
mul(other: Vector): Vector {
return new Vector(this.x * other.x, this.y * other.y);
}
/**
* Adds two scalars and return a new vector
* {}
*/
addScalars(x: number, y: number): Vector {
return new Vector(this.x + x, this.y + y);
}
/**
* Substracts a scalar and return a new vector
* {}
*/
subScalar(f: number): Vector {
return new Vector(this.x - f, this.y - f);
}
/**
* Substracts two scalars and return a new vector
* {}
*/
subScalars(x: number, y: number): Vector {
return new Vector(this.x - x, this.y - y);
}
/**
* Returns the euclidian length
* {}
*/
length(): number {
return Math.hypot(this.x, this.y);
}
/**
* Returns the square length
* {}
*/
lengthSquare(): number {
return this.x * this.x + this.y * this.y;
}
/**
* Divides both components by a scalar and return a new vector
* {}
*/
divideScalar(f: number): Vector {
return new Vector(this.x / f, this.y / f);
}
/**
* Divides both components by the given scalars and return a new vector
* {}
*/
divideScalars(a: number, b: number): Vector {
return new Vector(this.x / a, this.y / b);
}
/**
* Divides both components by a scalar
* {}
*/
divideScalarInplace(f: number): Vector {
this.x /= f;
this.y /= f;
return this;
}
/**
* Multiplies both components with a scalar and return a new vector
* {}
*/
multiplyScalar(f: number): Vector {
return new Vector(this.x * f, this.y * f);
}
/**
* Multiplies both components with two scalars and returns a new vector
* {}
*/
multiplyScalars(a: number, b: number): Vector {
return new Vector(this.x * a, this.y * b);
}
/**
* For both components, compute the maximum of each component and the given scalar, and return a new vector.
* For example:
* - new Vector(-1, 5).maxScalar(0) -> Vector(0, 5)
* {}
*/
maxScalar(f: number): Vector {
return new Vector(Math.max(f, this.x), Math.max(f, this.y));
}
/**
* Adds a scalar to both components and return a new vector
* {}
*/
addScalar(f: number): Vector {
return new Vector(this.x + f, this.y + f);
}
/**
* Computes the component wise minimum and return a new vector
* {}
*/
min(v: Vector): Vector {
return new Vector(Math.min(v.x, this.x), Math.min(v.y, this.y));
}
/**
* Computes the component wise maximum and return a new vector
* {}
*/
max(v: Vector): Vector {
return new Vector(Math.max(v.x, this.x), Math.max(v.y, this.y));
}
/**
* Computes the component wise absolute
* {}
*/
abs(): Vector {
return new Vector(Math.abs(this.x), Math.abs(this.y));
}
/**
* Computes the scalar product
* {}
*/
dot(v: Vector): number {
return this.x * v.x + this.y * v.y;
}
/**
* Computes the distance to a given vector
* {}
*/
distance(v: Vector): number {
return Math.hypot(this.x - v.x, this.y - v.y);
}
/**
* Computes the square distance to a given vectort
* {}
*/
distanceSquare(v: Vector): number {
const dx: any = this.x - v.x;
const dy: any = this.y - v.y;
return dx * dx + dy * dy;
}
/**
* Returns x % f, y % f
* {} new vector
*/
modScalar(f: number): Vector {
return new Vector(safeModulo(this.x, f), safeModulo(this.y, f));
}
/**
* Computes and returns the center between both points
* {}
*/
centerPoint(v: Vector): Vector {
const cx: any = this.x + v.x;
const cy: any = this.y + v.y;
return new Vector(cx / 2, cy / 2);
}
/**
* Computes componentwise floor and returns a new vector
* {}
*/
floor(): Vector {
return new Vector(Math.floor(this.x), Math.floor(this.y));
}
/**
* Computes componentwise ceil and returns a new vector
* {}
*/
ceil(): Vector {
return new Vector(Math.ceil(this.x), Math.ceil(this.y));
}
/**
* Computes componentwise round and return a new vector
* {}
*/
round(): Vector {
return new Vector(Math.round(this.x), Math.round(this.y));
}
/**
* Converts this vector from world to tile space and return a new vector
* {}
*/
toTileSpace(): Vector {
return new Vector(Math.floor(this.x / tileSize), Math.floor(this.y / tileSize));
}
/**
* Converts this vector from world to street space and return a new vector
* {}
*/
toStreetSpace(): Vector {
return new Vector(Math.floor(this.x / halfTileSize + 0.25), Math.floor(this.y / halfTileSize + 0.25));
}
/**
* Converts this vector to world space and return a new vector
* {}
*/
toWorldSpace(): Vector {
return new Vector(this.x * tileSize, this.y * tileSize);
}
/**
* Converts this vector to world space and return a new vector
* {}
*/
toWorldSpaceCenterOfTile(): Vector {
return new Vector(this.x * tileSize + halfTileSize, this.y * tileSize + halfTileSize);
}
/**
* Converts the top left tile position of this vector
* {}
*/
snapWorldToTile(): Vector {
return new Vector(Math.floor(this.x / tileSize) * tileSize, Math.floor(this.y / tileSize) * tileSize);
}
/**
* Normalizes the vector, dividing by the length(), and return a new vector
* {}
*/
normalize(): Vector {
const len: any = Math.max(1e-5, Math.hypot(this.x, this.y));
return new Vector(this.x / len, this.y / len);
}
/**
* Normalizes the vector, dividing by the length(), and return a new vector
* {}
*/
normalizeIfGreaterOne(): Vector {
const len: any = Math.max(1, Math.hypot(this.x, this.y));
return new Vector(this.x / len, this.y / len);
}
/**
* Returns the normalized vector to the other point
* {}
*/
normalizedDirection(v: Vector): Vector {
const dx: any = v.x - this.x;
const dy: any = v.y - this.y;
const len: any = Math.max(1e-5, Math.hypot(dx, dy));
return new Vector(dx / len, dy / len);
}
/**
* Returns a perpendicular vector
* {}
*/
findPerpendicular(): Vector {
return new Vector(-this.y, this.x);
}
/**
* Returns the unnormalized direction to the other point
* {}
*/
direction(v: Vector): Vector {
return new Vector(v.x - this.x, v.y - this.y);
}
/**
* Returns a string representation of the vector
* {}
*/
toString(): string {
return this.x + "," + this.y;
}
/**
* Compares both vectors for exact equality. Does not do an epsilon compare
* {}
*/
equals(v: Vector): Boolean {
return this.x === v.x && this.y === v.y;
}
/**
* Rotates this vector
* {} new vector
*/
rotated(angle: number): Vector {
const sin: any = Math.sin(angle);
const cos: any = Math.cos(angle);
return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
}
/**
* Rotates this vector
* {} this vector
*/
rotateInplaceFastMultipleOf90(angle: number): Vector {
// const sin = Math.sin(angle);
// const cos = Math.cos(angle);
// let sin = 0, cos = 1;
assert(angle >= 0 && angle <= 360, "Invalid angle, please clamp first: " + angle);
switch (angle) {
case 0:
case 360: {
return this;
}
case 90: {
// sin = 1;
// cos = 0;
const x: any = this.x;
this.x = -this.y;
this.y = x;
return this;
}
case 180: {
// sin = 0
// cos = -1
this.x = -this.x;
this.y = -this.y;
return this;
}
case 270: {
// sin = -1
// cos = 0
const x: any = this.x;
this.x = this.y;
this.y = -x;
return this;
}
default: {
assertAlways(false, "Invalid fast inplace rotation: " + angle);
return this;
}
}
// return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
}
/**
* Rotates this vector
* {} new vector
*/
rotateFastMultipleOf90(angle: number): Vector {
assert(angle >= 0 && angle <= 360, "Invalid angle, please clamp first: " + angle);
switch (angle) {
case 360:
case 0: {
return new Vector(this.x, this.y);
}
case 90: {
return new Vector(-this.y, this.x);
}
case 180: {
return new Vector(-this.x, -this.y);
}
case 270: {
return new Vector(this.y, -this.x);
}
default: {
assertAlways(false, "Invalid fast inplace rotation: " + angle);
return new Vector();
}
}
}
/**
* Helper method to rotate a direction
* {}
*/
static transformDirectionFromMultipleOf90(direction: enumDirection, angle: number): enumDirection {
if (angle === 0 || angle === 360) {
return direction;
}
assert(angle >= 0 && angle <= 360, "Invalid angle: " + angle);
switch (direction) {
case enumDirection.top: {
switch (angle) {
case 90:
return enumDirection.right;
case 180:
return enumDirection.bottom;
case 270:
return enumDirection.left;
default:
assertAlways(false, "Invalid angle: " + angle);
return;
}
}
case enumDirection.right: {
switch (angle) {
case 90:
return enumDirection.bottom;
case 180:
return enumDirection.left;
case 270:
return enumDirection.top;
default:
assertAlways(false, "Invalid angle: " + angle);
return;
}
}
case enumDirection.bottom: {
switch (angle) {
case 90:
return enumDirection.left;
case 180:
return enumDirection.top;
case 270:
return enumDirection.right;
default:
assertAlways(false, "Invalid angle: " + angle);
return;
}
}
case enumDirection.left: {
switch (angle) {
case 90:
return enumDirection.top;
case 180:
return enumDirection.right;
case 270:
return enumDirection.bottom;
default:
assertAlways(false, "Invalid angle: " + angle);
return;
}
}
default:
assertAlways(false, "Invalid angle: " + angle);
return;
}
}
/**
* Compares both vectors for epsilon equality
* {}
*/
equalsEpsilon(v: Vector, epsilon: any = 1e-5): Boolean {
return Math.abs(this.x - v.x) < 1e-5 && Math.abs(this.y - v.y) < epsilon;
}
/**
* Returns the angle
* {} 0 .. 2 PI
*/
angle(): number {
return Math.atan2(this.y, this.x) + Math.PI / 2;
}
/**
* Serializes the vector to a string
* {}
*/
serializeTile(): string {
return String.fromCharCode(33 + this.x) + String.fromCharCode(33 + this.y);
}
/**
* Creates a simple representation of the vector
*/
serializeSimple(): any {
return { x: this.x, y: this.y };
}
/**
* {}
*/
serializeTileToInt(): number {
return this.x + this.y * 256;
}
/**
*
* {}
*/
static deserializeTileFromInt(i: number): Vector {
const x: any = i % 256;
const y: any = Math.floor(i / 256);
return new Vector(x, y);
}
/**
* Deserializes a vector from a string
* {}
*/
static deserializeTile(s: string): Vector {
return new Vector(s.charCodeAt(0) - 33, s.charCodeAt(1) - 33);
}
/**
* Deserializes a vector from a serialized json object
* {}
*/
static fromSerializedObject(obj: object): Vector {
if (obj) {
return new Vector(obj.x || 0, obj.y || 0);
}
}
}
/**
* Interpolates two vectors, for a = 0, returns v1 and for a = 1 return v2, otherwise interpolate
*/
export function mixVector(v1: Vector, v2: Vector, a: number): any {
return new Vector(v1.x * (1 - a) + v2.x * a, v1.y * (1 - a) + v2.y * a);
}
/**
* Mapping from string direction to actual vector
* @enum {Vector}
*/
export const enumDirectionToVector: any = {
top: new Vector(0, -1),
right: new Vector(1, 0),
bottom: new Vector(0, 1),
left: new Vector(-1, 0),
};

@ -0,0 +1,104 @@
/* typehints:start */
import type { Entity } from "./entity";
import type { GameRoot } from "./root";
/* typehints:end */
import { globalConfig } from "../core/config";
import { createLogger } from "../core/logging";
import { ACHIEVEMENTS } from "../platform/achievement_provider";
import { getBuildingDataFromCode } from "./building_codes";
const logger: any = createLogger("achievement_proxy");
const ROTATER: any = "rotater";
const DEFAULT: any = "default";
export class AchievementProxy {
public root = root;
public provider = this.root.app.achievementProvider;
public disabled = true;
public sliceTime = 0;
constructor(root) {
if (G_IS_DEV && globalConfig.debug.testAchievements) {
// still enable the proxy
}
else if (!this.provider.hasAchievements()) {
return;
}
this.root.signals.postLoadHook.add(this.onLoad, this);
}
onLoad(): any {
if (!this.root.gameMode.hasAchievements()) {
logger.log("Disabling achievements because game mode does not have achievements");
this.disabled = true;
return;
}
this.provider
.onLoad(this.root)
.then((): any => {
this.disabled = false;
logger.log("Recieving achievement signals");
this.initialize();
})
.catch((err: any): any => {
this.disabled = true;
logger.error("Ignoring achievement signals", err);
});
}
initialize(): any {
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.darkMode, null);
if (this.has(ACHIEVEMENTS.mam)) {
this.root.signals.entityAdded.add(this.onMamFailure, this);
this.root.signals.entityDestroyed.add(this.onMamFailure, this);
this.root.signals.storyGoalCompleted.add(this.onStoryGoalCompleted, this);
}
if (this.has(ACHIEVEMENTS.noInverseRotater)) {
this.root.signals.entityAdded.add(this.onEntityAdded, this);
}
this.startSlice();
}
startSlice(): any {
this.sliceTime = this.root.time.now();
this.root.signals.bulkAchievementCheck.dispatch(ACHIEVEMENTS.storeShape, this.sliceTime, ACHIEVEMENTS.throughputBp25, this.sliceTime, ACHIEVEMENTS.throughputBp50, this.sliceTime, ACHIEVEMENTS.throughputLogo25, this.sliceTime, ACHIEVEMENTS.throughputLogo50, this.sliceTime, ACHIEVEMENTS.throughputRocket10, this.sliceTime, ACHIEVEMENTS.throughputRocket20, this.sliceTime, ACHIEVEMENTS.play1h, this.sliceTime, ACHIEVEMENTS.play10h, this.sliceTime, ACHIEVEMENTS.play20h, this.sliceTime);
}
update(): any {
if (this.disabled) {
return;
}
if (this.root.time.now() - this.sliceTime > globalConfig.achievementSliceDuration) {
this.startSlice();
}
}
/**
* {}
*/
has(key: string): boolean {
if (!this.provider.collection) {
return false;
}
return this.provider.collection.map.has(key);
}
onEntityAdded(entity: Entity): any {
if (!entity.components.StaticMapEntity) {
return;
}
const building: any = getBuildingDataFromCode(entity.components.StaticMapEntity.code);
if (building.metaInstance.id !== ROTATER) {
return;
}
if (building.variant === DEFAULT) {
return;
}
this.root.savegame.currentData.stats.usedInverseRotater = true;
this.root.signals.entityAdded.remove(this.onEntityAdded);
}
onStoryGoalCompleted(level: number): any {
if (level > 26) {
this.root.signals.entityAdded.add(this.onMamFailure, this);
this.root.signals.entityDestroyed.add(this.onMamFailure, this);
}
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.mam, null);
// reset on every level
this.root.savegame.currentData.stats.failedMam = false;
}
onMamFailure(): any {
this.root.savegame.currentData.stats.failedMam = true;
}
}

@ -0,0 +1,63 @@
import { globalConfig } from "../core/config";
import { createLogger } from "../core/logging";
import { GameRoot } from "./root";
// How important it is that a savegame is created
/**
* @enum {number}
*/
export const enumSavePriority: any = {
regular: 2,
asap: 100,
};
const logger: any = createLogger("autosave");
export class AutomaticSave {
public root: GameRoot = root;
public saveImportance = enumSavePriority.regular;
public lastSaveAttempt = -1000;
constructor(root) {
}
setSaveImportance(importance: any): any {
this.saveImportance = Math.max(this.saveImportance, importance);
}
doSave(): any {
if (G_IS_DEV && globalConfig.debug.disableSavegameWrite) {
return;
}
this.root.gameState.doSave();
this.saveImportance = enumSavePriority.regular;
}
update(): any {
if (!this.root.gameInitialized) {
// Bad idea
return;
}
const saveInterval: any = this.root.app.settings.getAutosaveIntervalSeconds();
if (!saveInterval) {
// Disabled
return;
}
// Check when the last save was, but make sure that if it fails, we don't spam
const lastSaveTime: any = Math.max(this.lastSaveAttempt, this.root.savegame.getRealLastUpdate());
const secondsSinceLastSave: any = (Date.now() - lastSaveTime) / 1000.0;
let shouldSave: any = false;
switch (this.saveImportance) {
case enumSavePriority.asap:
// High always should save
shouldSave = true;
break;
case enumSavePriority.regular:
// Could determine if there is a good / bad point here
shouldSave = secondsSinceLastSave > saveInterval;
break;
default:
assert(false, "Unknown save prio: " + this.saveImportance);
break;
}
if (shouldSave) {
logger.log("Saving automatically");
this.lastSaveAttempt = Date.now();
this.doSave();
}
}
}

@ -0,0 +1,79 @@
import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters";
import { BasicSerializableObject } from "../savegame/serialization";
/**
* Class for items on belts etc. Not an entity for performance reasons
*/
export class BaseItem extends BasicSerializableObject {
public _type = this.getItemType();
constructor() {
super();
}
static getId(): any {
return "base_item";
}
/** {} */
static getSchema(): import("../savegame/serialization").Schema {
return {};
}
/** {} **/
getItemType(): ItemType {
abstract;
return "shape";
}
/**
* Returns a string id of the item
* {}
* @abstract
*/
getAsCopyableKey(): string {
abstract;
return "";
}
/**
* Returns if the item equals the other itme
* {}
*/
equals(other: BaseItem): boolean {
if (this.getItemType() !== other.getItemType()) {
return false;
}
return this.equalsImpl(other);
}
/**
* Override for custom comparison
* {}
* @abstract
*/
equalsImpl(other: BaseItem): boolean {
abstract;
return false;
}
/**
* Draws the item to a canvas
* @abstract
*/
drawFullSizeOnCanvas(context: CanvasRenderingContext2D, size: number): any {
abstract;
}
/**
* Draws the item at the given position
*/
drawItemCenteredClipped(x: number, y: number, parameters: DrawParameters, diameter: number= = globalConfig.defaultItemDiameter): any {
if (parameters.visibleRect.containsCircle(x, y, diameter / 2)) {
this.drawItemCenteredImpl(x, y, parameters, diameter);
}
}
/**
* INTERNAL
* @abstract
*/
drawItemCenteredImpl(x: number, y: number, parameters: DrawParameters, diameter: number= = globalConfig.defaultItemDiameter): any {
abstract;
}
getBackgroundColorAsResource(): any {
abstract;
return "";
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,143 @@
import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters";
import { findNiceIntegerValue } from "../core/utils";
import { Vector } from "../core/vector";
import { Entity } from "./entity";
import { ACHIEVEMENTS } from "../platform/achievement_provider";
import { GameRoot } from "./root";
export class Blueprint {
public entities = entities;
constructor(entities) {
}
/**
* Returns the layer of this blueprint
* {}
*/
get layer() {
if (this.entities.length === 0) {
return "regular";
}
return this.entities[0].layer;
}
/**
* Creates a new blueprint from the given entity uids
*/
static fromUids(root: GameRoot, uids: Array<number>): any {
const newEntities: any = [];
let averagePosition: any = new Vector();
// First, create a copy
for (let i: any = 0; i < uids.length; ++i) {
const entity: any = root.entityMgr.findByUid(uids[i]);
assert(entity, "Entity for blueprint not found:" + uids[i]);
const clone: any = entity.clone();
newEntities.push(clone);
const pos: any = entity.components.StaticMapEntity.getTileSpaceBounds().getCenter();
averagePosition.addInplace(pos);
}
averagePosition.divideScalarInplace(uids.length);
const blueprintOrigin: any = averagePosition.subScalars(0.5, 0.5).floor();
for (let i: any = 0; i < uids.length; ++i) {
newEntities[i].components.StaticMapEntity.origin.subInplace(blueprintOrigin);
}
// Now, make sure the origin is 0,0
return new Blueprint(newEntities);
}
/**
* Returns the cost of this blueprint in shapes
*/
getCost(): any {
if (G_IS_DEV && globalConfig.debug.blueprintsNoCost) {
return 0;
}
return findNiceIntegerValue(4 * Math.pow(this.entities.length, 1.1));
}
/**
* Draws the blueprint at the given origin
*/
draw(parameters: DrawParameters, tile: any): any {
parameters.context.globalAlpha = 0.8;
for (let i: any = 0; i < this.entities.length; ++i) {
const entity: any = this.entities[i];
const staticComp: any = entity.components.StaticMapEntity;
const newPos: any = staticComp.origin.add(tile);
const rect: any = staticComp.getTileSpaceBounds();
rect.moveBy(tile.x, tile.y);
if (!parameters.root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
parameters.context.globalAlpha = 0.3;
}
else {
parameters.context.globalAlpha = 1;
}
staticComp.drawSpriteOnBoundsClipped(parameters, staticComp.getBlueprintSprite(), 0, newPos);
}
parameters.context.globalAlpha = 1;
}
/**
* Rotates the blueprint clockwise
*/
rotateCw(): any {
for (let i: any = 0; i < this.entities.length; ++i) {
const entity: any = this.entities[i];
const staticComp: any = entity.components.StaticMapEntity;
// Actually keeping this in as an easter egg to rotate the trash can
// if (staticComp.getMetaBuilding().getIsRotateable()) {
staticComp.rotation = (staticComp.rotation + 90) % 360;
staticComp.originalRotation = (staticComp.originalRotation + 90) % 360;
// }
staticComp.origin = staticComp.origin.rotateFastMultipleOf90(90);
}
}
/**
* Rotates the blueprint counter clock wise
*/
rotateCcw(): any {
// Well ...
for (let i: any = 0; i < 3; ++i) {
this.rotateCw();
}
}
/**
* Checks if the blueprint can be placed at the given tile
*/
canPlace(root: GameRoot, tile: Vector): any {
let anyPlaceable: any = false;
for (let i: any = 0; i < this.entities.length; ++i) {
const entity: any = this.entities[i];
if (root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
anyPlaceable = true;
}
}
return anyPlaceable;
}
canAfford(root: GameRoot): any {
if (root.gameMode.getHasFreeCopyPaste()) {
return true;
}
return root.hubGoals.getShapesStoredByKey(root.gameMode.getBlueprintShapeKey()) >= this.getCost();
}
/**
* Attempts to place the blueprint at the given tile
*/
tryPlace(root: GameRoot, tile: Vector): any {
return root.logic.performBulkOperation((): any => {
return root.logic.performImmutableOperation((): any => {
let count: any = 0;
for (let i: any = 0; i < this.entities.length; ++i) {
const entity: any = this.entities[i];
if (!root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
continue;
}
const clone: any = entity.clone();
clone.components.StaticMapEntity.origin.addInplace(tile);
root.logic.freeEntityAreaBeforeBuild(clone);
root.map.placeStaticEntity(clone);
root.entityMgr.registerEntity(clone);
count++;
}
root.signals.bulkAchievementCheck.dispatch(ACHIEVEMENTS.placeBlueprint, count, ACHIEVEMENTS.placeBp1000, count);
return count !== 0;
});
});
}
}

@ -0,0 +1,204 @@
/* typehints:start */
import type { MetaBuilding } from "./meta_building";
import type { AtlasSprite } from "../core/sprites";
import type { Vector } from "../core/vector";
/* typehints:end */
import { gMetaBuildingRegistry } from "../core/global_registries";
export type BuildingVariantIdentifier = {
metaClass: typeof MetaBuilding;
metaInstance?: MetaBuilding;
variant?: string;
rotationVariant?: number;
tileSize?: Vector;
sprite?: AtlasSprite;
blueprintSprite?: AtlasSprite;
silhouetteColor?: string;
};
/**
* Stores a lookup table for all building variants (for better performance)
*/
export const gBuildingVariants: {
[idx: number|string]: BuildingVariantIdentifier;
} = {
// Set later
};
/**
* Mapping from 'metaBuildingId/variant/rotationVariant' to building code
*/
const variantsCache: Map<string, number | string> = new Map();
/**
* Registers a new variant
*/
export function registerBuildingVariant(code: number | string, meta: typeof MetaBuilding, variant: string = "default" /* @TODO: Circular dependency, actually its defaultBuildingVariant */, rotationVariant: number = 0): any {
assert(!gBuildingVariants[code], "Duplicate id: " + code);
gBuildingVariants[code] = {
metaClass: meta,
metaInstance: gMetaBuildingRegistry.findByClass(meta),
variant,
rotationVariant,
// @ts-ignore
tileSize: new meta().getDimensions(variant),
};
}
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
* @param {} buildingId
* @param {} variant
* @param {} rotat * @
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
* @param {string} buildingId
* @param {string} variant
* @param {number} rotat * @returns
*/
functioildingHash(build striniant: strin
/**
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
* @param {} buildingId
* @param {} variant
* @param {} rotationVar * @
/**
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
* @param {string} buildingId
* @param {string} variant
* @param {number} rotationVar * @returns
*/
functiorateBuildingHash(build string, variant: strin
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
* @param {} buildingId
* @param {} variant
* @param {} rotationVar * @
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
* @param {string} buildingId
* @param {string} variant
* @param {number} rotationVar * @returns
*/
functiorateBuildingHash(build string, variant: strin
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
* @param {} buildingId
* @param {} variant
* @param {} rotationVariant
* @
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
*
/**
* Hashes the combination of buildng, variant and rotation variant
* @param {string} buildingId
* @param {string} variant
* @param {number} rotationVariant
* @returns
*/
function generateBuildingHash(buildingId: string, variant: string, rotationVariant: number): any {
return buildingId + "/" + variant + "/" + rotationVariant;
}
/**
*
* {}
*/
export function getBuildingDataFromCode(code: string | number): BuildingVariantIdentifier {
assert(gBuildingVariants[code], "Invalid building code: " + code);
return gBuildingVariants[code];
}
/**
* Builds the cache for the codes
*/
export function buildBuildingCodeCache(): any {
for (const code: any in gBuildingVariants) {
const data: any = gBuildingVariants[code];
const hash: any = generateBuildingHash(data.metaInstance.getId(), data.variant, data.rotationVariant);
variantsCache.set(hash, isNaN(+code) ? code : +code);
}
}
/**
* Finds the code for a given variant
* {}
*/
export function getCodeFromBuildingData(metaBuilding: MetaBuilding, variant: string, rotationVariant: number): number | string {
const hash: any = generateBuildingHash(metaBuilding.getId(), variant, rotationVariant);
const result: any = variantsCache.get(hash);
if (G_IS_DEV) {
if (!result) {
console.warn("Known hashes:", Array.from(variantsCache.keys()));
assertAlways(false, "Building not found by data: " + hash);
}
}
return result;
}

@ -0,0 +1,70 @@
import { generateMatrixRotations } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
const overlayMatrix: any = generateMatrixRotations([1, 1, 0, 1, 1, 1, 0, 1, 0]);
export class MetaAnalyzerBuilding extends MetaBuilding {
constructor() {
super("analyzer");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 43,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor(): any {
return "#3a52bc";
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_virtual_processing);
}
/** {} **/
getLayer(): "wires" {
return "wires";
}
getDimensions(): any {
return new Vector(1, 1);
}
getRenderPins(): any {
// We already have it included
return false;
}
getSpecialOverlayRenderMatrix(rotation: any, rotationVariant: any, variant: any): any {
return overlayMatrix[rotation];
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new WiredPinsComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.left,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 0),
direction: enumDirection.right,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
],
}));
entity.addComponent(new LogicGateComponent({
type: enumLogicGateType.analyzer,
}));
}
}

@ -0,0 +1,207 @@
import { enumDirection, Vector } from "../../core/vector";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { MetaBuilding, defaultBuildingVariant } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
import { T } from "../../translations";
import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils";
import { BeltUnderlaysComponent } from "../components/belt_underlays";
/** @enum {string} */
export const enumBalancerVariants: any = {
merger: "merger",
mergerInverse: "merger-inverse",
splitter: "splitter",
splitterInverse: "splitter-inverse",
};
const overlayMatrices: any = {
[defaultBuildingVariant]: null,
[enumBalancerVariants.merger]: generateMatrixRotations([0, 1, 0, 0, 1, 1, 0, 1, 0]),
[enumBalancerVariants.mergerInverse]: generateMatrixRotations([0, 1, 0, 1, 1, 0, 0, 1, 0]),
[enumBalancerVariants.splitter]: generateMatrixRotations([0, 1, 0, 0, 1, 1, 0, 1, 0]),
[enumBalancerVariants.splitterInverse]: generateMatrixRotations([0, 1, 0, 1, 1, 0, 0, 1, 0]),
};
export class MetaBalancerBuilding extends MetaBuilding {
constructor() {
super("balancer");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 4,
variant: defaultBuildingVariant,
},
{
internalId: 5,
variant: enumBalancerVariants.merger,
},
{
internalId: 6,
variant: enumBalancerVariants.mergerInverse,
},
{
internalId: 47,
variant: enumBalancerVariants.splitter,
},
{
internalId: 48,
variant: enumBalancerVariants.splitterInverse,
},
];
}
getDimensions(variant: any): any {
switch (variant) {
case defaultBuildingVariant:
return new Vector(2, 1);
case enumBalancerVariants.merger:
case enumBalancerVariants.mergerInverse:
case enumBalancerVariants.splitter:
case enumBalancerVariants.splitterInverse:
return new Vector(1, 1);
default:
assertAlways(false, "Unknown balancer variant: " + variant);
}
}
/**
* {}
*/
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): Array<number> | null {
const matrix: any = overlayMatrices[variant];
if (matrix) {
return matrix[rotation];
}
return null;
}
/**
* {}
*/
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
string,
string
]> {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
let speedMultiplier: any = 2;
switch (variant) {
case enumBalancerVariants.merger:
case enumBalancerVariants.mergerInverse:
case enumBalancerVariants.splitter:
case enumBalancerVariants.splitterInverse:
speedMultiplier = 1;
}
const speed: any = (root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.balancer) / 2) * speedMultiplier;
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
}
getSilhouetteColor(): any {
return "#555759";
}
getAvailableVariants(root: GameRoot): any {
const deterministic: any = root.gameMode.getIsDeterministic();
let available: any = deterministic ? [] : [defaultBuildingVariant];
if (!deterministic && root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_merger)) {
available.push(enumBalancerVariants.merger, enumBalancerVariants.mergerInverse);
}
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_splitter)) {
available.push(enumBalancerVariants.splitter, enumBalancerVariants.splitterInverse);
}
return available;
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_balancer);
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new ItemAcceptorComponent({
slots: [], // set later
}));
entity.addComponent(new ItemProcessorComponent({
inputsPerCharge: 1,
processorType: enumItemProcessorTypes.balancer,
}));
entity.addComponent(new ItemEjectorComponent({
slots: [],
renderFloatingItems: false,
}));
entity.addComponent(new BeltUnderlaysComponent({ underlays: [] }));
}
updateVariants(entity: Entity, rotationVariant: number, variant: string): any {
switch (variant) {
case defaultBuildingVariant: {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
},
{
pos: new Vector(1, 0),
direction: enumDirection.bottom,
},
]);
entity.components.ItemEjector.setSlots([
{ pos: new Vector(0, 0), direction: enumDirection.top },
{ pos: new Vector(1, 0), direction: enumDirection.top },
]);
entity.components.BeltUnderlays.underlays = [
{ pos: new Vector(0, 0), direction: enumDirection.top },
{ pos: new Vector(1, 0), direction: enumDirection.top },
];
break;
}
case enumBalancerVariants.merger:
case enumBalancerVariants.mergerInverse: {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
},
{
pos: new Vector(0, 0),
direction: variant === enumBalancerVariants.mergerInverse
? enumDirection.left
: enumDirection.right,
},
]);
entity.components.ItemEjector.setSlots([
{ pos: new Vector(0, 0), direction: enumDirection.top },
]);
entity.components.BeltUnderlays.underlays = [
{ pos: new Vector(0, 0), direction: enumDirection.top },
];
break;
}
case enumBalancerVariants.splitter:
case enumBalancerVariants.splitterInverse: {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
},
]);
entity.components.ItemEjector.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.top,
},
{
pos: new Vector(0, 0),
direction: variant === enumBalancerVariants.splitterInverse
? enumDirection.left
: enumDirection.right,
},
]);
entity.components.BeltUnderlays.underlays = [
{ pos: new Vector(0, 0), direction: enumDirection.top },
];
break;
}
default:
assertAlways(false, "Unknown balancer variant: " + variant);
}
}
}

@ -0,0 +1,218 @@
import { Loader } from "../../core/loader";
import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils";
import { enumAngleToDirection, enumDirection, Vector } from "../../core/vector";
import { SOUNDS } from "../../platform/sound";
import { T } from "../../translations";
import { BeltComponent } from "../components/belt";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { THEME } from "../theme";
export const arrayBeltVariantToRotation: any = [enumDirection.top, enumDirection.left, enumDirection.right];
export const beltOverlayMatrices: any = {
[enumDirection.top]: generateMatrixRotations([0, 1, 0, 0, 1, 0, 0, 1, 0]),
[enumDirection.left]: generateMatrixRotations([0, 0, 0, 1, 1, 0, 0, 1, 0]),
[enumDirection.right]: generateMatrixRotations([0, 0, 0, 0, 1, 1, 0, 1, 0]),
};
export class MetaBeltBuilding extends MetaBuilding {
constructor() {
super("belt");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 1,
variant: defaultBuildingVariant,
rotationVariant: 0,
},
{
internalId: 2,
variant: defaultBuildingVariant,
rotationVariant: 1,
},
{
internalId: 3,
variant: defaultBuildingVariant,
rotationVariant: 2,
},
];
}
getSilhouetteColor(): any {
return THEME.map.chunkOverview.beltColor;
}
getPlacementSound(): any {
return SOUNDS.placeBelt;
}
getHasDirectionLockAvailable(): any {
return true;
}
getStayInPlacementMode(): any {
return true;
}
getRotateAutomaticallyWhilePlacing(): any {
return true;
}
getSprite(): any {
return null;
}
getIsReplaceable(): any {
return true;
}
/**
* {}
*/
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
string,
string
]> {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
const beltSpeed: any = root.hubGoals.getBeltBaseSpeed();
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]];
}
getPreviewSprite(rotationVariant: any): any {
switch (arrayBeltVariantToRotation[rotationVariant]) {
case enumDirection.top: {
return Loader.getSprite("sprites/buildings/belt_top.png");
}
case enumDirection.left: {
return Loader.getSprite("sprites/buildings/belt_left.png");
}
case enumDirection.right: {
return Loader.getSprite("sprites/buildings/belt_right.png");
}
default: {
assertAlways(false, "Invalid belt rotation variant");
}
}
}
getBlueprintSprite(rotationVariant: any): any {
switch (arrayBeltVariantToRotation[rotationVariant]) {
case enumDirection.top: {
return Loader.getSprite("sprites/blueprints/belt_top.png");
}
case enumDirection.left: {
return Loader.getSprite("sprites/blueprints/belt_left.png");
}
case enumDirection.right: {
return Loader.getSprite("sprites/blueprints/belt_right.png");
}
default: {
assertAlways(false, "Invalid belt rotation variant");
}
}
}
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): any {
return beltOverlayMatrices[entity.components.Belt.direction][rotation];
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new BeltComponent({
direction: enumDirection.top, // updated later
}));
}
updateVariants(entity: Entity, rotationVariant: number): any {
entity.components.Belt.direction = arrayBeltVariantToRotation[rotationVariant];
}
/**
* Should compute the optimal rotation variant on the given tile
* @return {{ rotation: number, rotationVariant: number, connectedEntities?: Array<Entity> }}
*/
computeOptimalDirectionAndRotationVariantAtTile({ root, tile, rotation, variant, layer }: {
root: GameRoot;
tile: Vector;
rotation: number;
variant: string;
layer: Layer;
}): {
rotation: number;
rotationVariant: number;
connectedEntities?: Array<Entity>;
} {
const topDirection: any = enumAngleToDirection[rotation];
const rightDirection: any = enumAngleToDirection[(rotation + 90) % 360];
const bottomDirection: any = enumAngleToDirection[(rotation + 180) % 360];
const leftDirection: any = enumAngleToDirection[(rotation + 270) % 360];
const { ejectors, acceptors }: any = root.logic.getEjectorsAndAcceptorsAtTile(tile);
let hasBottomEjector: any = false;
let hasRightEjector: any = false;
let hasLeftEjector: any = false;
let hasTopAcceptor: any = false;
let hasLeftAcceptor: any = false;
let hasRightAcceptor: any = false;
// Check all ejectors
for (let i: any = 0; i < ejectors.length; ++i) {
const ejector: any = ejectors[i];
if (ejector.toDirection === topDirection) {
hasBottomEjector = true;
}
else if (ejector.toDirection === leftDirection) {
hasRightEjector = true;
}
else if (ejector.toDirection === rightDirection) {
hasLeftEjector = true;
}
}
// Check all acceptors
for (let i: any = 0; i < acceptors.length; ++i) {
const acceptor: any = acceptors[i];
if (acceptor.fromDirection === bottomDirection) {
hasTopAcceptor = true;
}
else if (acceptor.fromDirection === rightDirection) {
hasLeftAcceptor = true;
}
else if (acceptor.fromDirection === leftDirection) {
hasRightAcceptor = true;
}
}
// Soo .. if there is any ejector below us we always prioritize
// this ejector
if (!hasBottomEjector) {
// When something ejects to us from the left and nothing from the right,
// do a curve from the left to the top
if (hasRightEjector && !hasLeftEjector) {
return {
rotation: (rotation + 270) % 360,
rotationVariant: 2,
};
}
// When something ejects to us from the right and nothing from the left,
// do a curve from the right to the top
if (hasLeftEjector && !hasRightEjector) {
return {
rotation: (rotation + 90) % 360,
rotationVariant: 1,
};
}
}
// When there is a top acceptor, ignore sides
// NOTICE: This makes the belt prefer side turns *way* too much!
if (!hasTopAcceptor) {
// When there is an acceptor to the right but no acceptor to the left,
// do a turn to the right
if (hasRightAcceptor && !hasLeftAcceptor) {
return {
rotation,
rotationVariant: 2,
};
}
// When there is an acceptor to the left but no acceptor to the right,
// do a turn to the left
if (hasLeftAcceptor && !hasRightAcceptor) {
return {
rotation,
rotationVariant: 1,
};
}
}
return {
rotation,
rotationVariant: 0,
};
}
}

@ -0,0 +1,148 @@
/* typehints:start */
import type { Entity } from "../entity";
/* typehints:end */
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
export class MetaBlockBuilding extends MetaBuilding {
constructor() {
super("block");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 64,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor(): any {
return "#333";
}
/**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param {import("../../savegame/
*/
g /**
*
* @ /**
*
* @ /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @ /**
*
* @ /**
*
* @param {import("../../savegame/
*/
g /**
*
* @ /**
*
* @ /**
*
* @ /**
*
* @param {} root
* @ /**
*
* @ /**
*
* @ /**
*
* @ /**
*
* @param {import("../../savegame/savegame_serializer").GameRoot} root
* @returns
*/
getIsRemovable(root: import("../../savegame/savegame_serializer").GameRoot): any {
return root.gameMode.getIsEditor();
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any { }
}

@ -0,0 +1,65 @@
import { enumDirection, Vector } from "../../core/vector";
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
export class MetaComparatorBuilding extends MetaBuilding {
constructor() {
super("comparator");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 46,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor(): any {
return "#823cab";
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_virtual_processing);
}
/** {} **/
getLayer(): "wires" {
return "wires";
}
getDimensions(): any {
return new Vector(1, 1);
}
getRenderPins(): any {
// We already have it included
return false;
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new WiredPinsComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.top,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 0),
direction: enumDirection.left,
type: enumPinSlotType.logicalAcceptor,
},
{
pos: new Vector(0, 0),
direction: enumDirection.right,
type: enumPinSlotType.logicalAcceptor,
},
],
}));
entity.addComponent(new LogicGateComponent({
type: enumLogicGateType.compare,
}));
}
}

@ -0,0 +1,158 @@
/* typehints:start */
import type { Entity } from "../entity";
/* typehints:end */
import { enumDirection, Vector } from "../../core/vector";
import { ConstantSignalComponent } from "../components/constant_signal";
import { ItemEjectorComponent } from "../components/item_ejector";
import { ItemProducerComponent } from "../components/item_producer";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
export class MetaConstantProducerBuilding extends MetaBuilding {
constructor() {
super("constant_producer");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 62,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor(): any {
return "#bfd630";
}
/**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param {import("../../savegame/
*/
g /**
*
* @ /**
*
* @ /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @ /**
*
* @ /**
*
* @param {import("../../savegame/
*/
g /**
*
* @ /**
*
* @ /**
*
* @ /**
*
* @param {} root
* @ /**
*
* @ /**
*
* @ /**
*
* @ /**
*
* @param {import("../../savegame/savegame_serializer").GameRoot} root
* @returns
*/
getIsRemovable(root: import("../../savegame/savegame_serializer").GameRoot): any {
return root.gameMode.getIsEditor();
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new ItemEjectorComponent({
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
}));
entity.addComponent(new ItemProducerComponent({}));
entity.addComponent(new ConstantSignalComponent({}));
}
}

@ -0,0 +1,57 @@
import { enumDirection, Vector } from "../../core/vector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { ConstantSignalComponent } from "../components/constant_signal";
import { generateMatrixRotations } from "../../core/utils";
import { enumHubGoalRewards } from "../tutorial_goals";
const overlayMatrix: any = generateMatrixRotations([0, 1, 0, 1, 1, 1, 1, 1, 1]);
export class MetaConstantSignalBuilding extends MetaBuilding {
constructor() {
super("constant_signal");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 31,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor(): any {
return "#2b84fd";
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_constant_signal);
}
/** {} **/
getLayer(): "wires" {
return "wires";
}
getDimensions(): any {
return new Vector(1, 1);
}
getRenderPins(): any {
return false;
}
getSpecialOverlayRenderMatrix(rotation: any): any {
return overlayMatrix[rotation];
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new WiredPinsComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.top,
type: enumPinSlotType.logicalEjector,
},
],
}));
entity.addComponent(new ConstantSignalComponent({}));
}
}

@ -0,0 +1,110 @@
import { formatItemsPerSecond } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { T } from "../../translations";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
/** @enum {string} */
export const enumCutterVariants: any = { quad: "quad" };
export class MetaCutterBuilding extends MetaBuilding {
constructor() {
super("cutter");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 9,
variant: defaultBuildingVariant,
},
{
internalId: 10,
variant: enumCutterVariants.quad,
},
];
}
getSilhouetteColor(): any {
return "#7dcda2";
}
getDimensions(variant: any): any {
switch (variant) {
case defaultBuildingVariant:
return new Vector(2, 1);
case enumCutterVariants.quad:
return new Vector(4, 1);
default:
assertAlways(false, "Unknown cutter variant: " + variant);
}
}
/**
* {}
*/
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
string,
string
]> {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
const speed: any = root.hubGoals.getProcessorBaseSpeed(variant === enumCutterVariants.quad
? enumItemProcessorTypes.cutterQuad
: enumItemProcessorTypes.cutter);
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
}
getAvailableVariants(root: GameRoot): any {
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_cutter_quad)) {
return [defaultBuildingVariant, enumCutterVariants.quad];
}
return super.getAvailableVariants(root);
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_cutter_and_trash);
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new ItemProcessorComponent({
inputsPerCharge: 1,
processorType: enumItemProcessorTypes.cutter,
}));
entity.addComponent(new ItemEjectorComponent({}));
entity.addComponent(new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
filter: "shape",
},
],
}));
}
updateVariants(entity: Entity, rotationVariant: number, variant: string): any {
switch (variant) {
case defaultBuildingVariant: {
entity.components.ItemEjector.setSlots([
{ pos: new Vector(0, 0), direction: enumDirection.top },
{ pos: new Vector(1, 0), direction: enumDirection.top },
]);
entity.components.ItemProcessor.type = enumItemProcessorTypes.cutter;
break;
}
case enumCutterVariants.quad: {
entity.components.ItemEjector.setSlots([
{ pos: new Vector(0, 0), direction: enumDirection.top },
{ pos: new Vector(1, 0), direction: enumDirection.top },
{ pos: new Vector(2, 0), direction: enumDirection.top },
{ pos: new Vector(3, 0), direction: enumDirection.top },
]);
entity.components.ItemProcessor.type = enumItemProcessorTypes.cutterQuad;
break;
}
default:
assertAlways(false, "Unknown painter variant: " + variant);
}
}
}

@ -0,0 +1,48 @@
import { enumDirection, Vector } from "../../core/vector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { DisplayComponent } from "../components/display";
import { enumHubGoalRewards } from "../tutorial_goals";
export class MetaDisplayBuilding extends MetaBuilding {
constructor() {
super("display");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 40,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor(): any {
return "#aaaaaa";
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_display);
}
getDimensions(): any {
return new Vector(1, 1);
}
getShowWiresLayerPreview(): any {
return true;
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new WiredPinsComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
],
}));
entity.addComponent(new DisplayComponent());
}
}

@ -0,0 +1,85 @@
import { formatItemsPerSecond } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { T } from "../../translations";
import { FilterComponent } from "../components/filter";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
export class MetaFilterBuilding extends MetaBuilding {
constructor() {
super("filter");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 37,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor(): any {
return "#c45c2e";
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_filter);
}
getDimensions(): any {
return new Vector(2, 1);
}
getShowWiresLayerPreview(): any {
return true;
}
/**
* {}
*/
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
string,
string
]> {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
const beltSpeed: any = root.hubGoals.getBeltBaseSpeed();
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]];
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new WiredPinsComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.left,
type: enumPinSlotType.logicalAcceptor,
},
],
}));
entity.addComponent(new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
},
],
}));
entity.addComponent(new ItemEjectorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.top,
},
{
pos: new Vector(1, 0),
direction: enumDirection.right,
},
],
}));
entity.addComponent(new FilterComponent());
}
}

@ -0,0 +1,166 @@
/* typehints:start */
import type { Entity } from "../entity";
/* typehints:end */
import { enumDirection, Vector } from "../../core/vector";
import { GoalAcceptorComponent } from "../components/goal_acceptor";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
export class MetaGoalAcceptorBuilding extends MetaBuilding {
constructor() {
super("goal_acceptor");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 63,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor(): any {
return "#ce418a";
}
/**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @param {import("../../savegame/
*/
g /**
*
* @ /**
*
* @ /**
*
* @ /**
*
* @param { /**
*
* @ /**
*
* @ /**
*
* @ /**
*
* @param {import("../../savegame/
*/
g /**
*
* @ /**
*
* @ /**
*
* @ /**
*
* @param {} root
* @ /**
*
* @ /**
*
* @ /**
*
* @ /**
*
* @param {import("../../savegame/savegame_serializer").GameRoot} root
* @returns
*/
getIsRemovable(root: import("../../savegame/savegame_serializer").GameRoot): any {
return root.gameMode.getIsEditor();
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
filter: "shape",
},
],
}));
entity.addComponent(new ItemProcessorComponent({
processorType: enumItemProcessorTypes.goal,
}));
entity.addComponent(new GoalAcceptorComponent({}));
}
}

@ -0,0 +1,66 @@
import { enumDirection, Vector } from "../../core/vector";
import { HubComponent } from "../components/hub";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { WiredPinsComponent, enumPinSlotType } from "../components/wired_pins";
export class MetaHubBuilding extends MetaBuilding {
constructor() {
super("hub");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 26,
variant: defaultBuildingVariant,
},
];
}
getDimensions(): any {
return new Vector(4, 4);
}
getSilhouetteColor(): any {
return "#eb5555";
}
getIsRotateable(): any {
return false;
}
getBlueprintSprite(): any {
return null;
}
getSprite(): any {
// We render it ourself
return null;
}
getIsRemovable(): any {
return false;
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new HubComponent());
entity.addComponent(new ItemProcessorComponent({
inputsPerCharge: 1,
processorType: enumItemProcessorTypes.hub,
}));
entity.addComponent(new WiredPinsComponent({
slots: [
{
pos: new Vector(0, 2),
type: enumPinSlotType.logicalEjector,
direction: enumDirection.left,
},
],
}));
const slots: Array<import("../components/item_acceptor").ItemAcceptorSlotConfig> = [];
for (let i: any = 0; i < 4; ++i) {
slots.push({ pos: new Vector(i, 0), direction: enumDirection.top, filter: "shape" }, { pos: new Vector(i, 3), direction: enumDirection.bottom, filter: "shape" }, { pos: new Vector(0, i), direction: enumDirection.left, filter: "shape" }, { pos: new Vector(3, i), direction: enumDirection.right, filter: "shape" });
}
entity.addComponent(new ItemAcceptorComponent({
slots,
}));
}
}

@ -0,0 +1,44 @@
import { enumDirection, Vector } from "../../core/vector";
import { ItemEjectorComponent } from "../components/item_ejector";
import { ItemProducerComponent } from "../components/item_producer";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
export class MetaItemProducerBuilding extends MetaBuilding {
constructor() {
super("item_producer");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 61,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor(): any {
return "#b37dcd";
}
getShowWiresLayerPreview(): any {
return true;
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new ItemEjectorComponent({
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
}));
entity.addComponent(new WiredPinsComponent({
slots: [
{
pos: new Vector(0, 0),
type: enumPinSlotType.logicalAcceptor,
direction: enumDirection.bottom,
},
],
}));
entity.addComponent(new ItemProducerComponent({}));
}
}

@ -0,0 +1,52 @@
import { enumDirection, Vector } from "../../core/vector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { LeverComponent } from "../components/lever";
import { enumHubGoalRewards } from "../tutorial_goals";
export class MetaLeverBuilding extends MetaBuilding {
constructor() {
super("lever");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 33,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor(): any {
// @todo: Render differently based on if its activated or not
return "#1a678b";
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers);
}
getDimensions(): any {
return new Vector(1, 1);
}
getSprite(): any {
return null;
}
getShowWiresLayerPreview(): any {
return true;
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new WiredPinsComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.top,
type: enumPinSlotType.logicalEjector,
},
],
}));
entity.addComponent(new LeverComponent({}));
}
}

@ -0,0 +1,142 @@
import { enumDirection, Vector } from "../../core/vector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding, defaultBuildingVariant } from "../meta_building";
import { GameRoot } from "../root";
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
import { generateMatrixRotations } from "../../core/utils";
import { enumHubGoalRewards } from "../tutorial_goals";
/** @enum {string} */
export const enumLogicGateVariants: any = {
not: "not",
xor: "xor",
or: "or",
};
/** @enum {string} */
const enumVariantToGate: any = {
[defaultBuildingVariant]: enumLogicGateType.and,
[enumLogicGateVariants.not]: enumLogicGateType.not,
[enumLogicGateVariants.xor]: enumLogicGateType.xor,
[enumLogicGateVariants.or]: enumLogicGateType.or,
};
const overlayMatrices: any = {
[defaultBuildingVariant]: generateMatrixRotations([0, 1, 0, 1, 1, 1, 0, 1, 1]),
[enumLogicGateVariants.xor]: generateMatrixRotations([0, 1, 0, 1, 1, 1, 0, 1, 1]),
[enumLogicGateVariants.or]: generateMatrixRotations([0, 1, 0, 1, 1, 1, 0, 1, 1]),
[enumLogicGateVariants.not]: generateMatrixRotations([0, 1, 0, 0, 1, 0, 0, 1, 0]),
};
const colors: any = {
[defaultBuildingVariant]: "#f48d41",
[enumLogicGateVariants.xor]: "#f4a241",
[enumLogicGateVariants.or]: "#f4d041",
[enumLogicGateVariants.not]: "#f44184",
};
export class MetaLogicGateBuilding extends MetaBuilding {
constructor() {
super("logic_gate");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 32,
variant: defaultBuildingVariant,
},
{
internalId: 34,
variant: enumLogicGateVariants.not,
},
{
internalId: 35,
variant: enumLogicGateVariants.xor,
},
{
internalId: 36,
variant: enumLogicGateVariants.or,
},
];
}
getSilhouetteColor(variant: any): any {
return colors[variant];
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_logic_gates);
}
/** {} **/
getLayer(): "wires" {
return "wires";
}
getDimensions(): any {
return new Vector(1, 1);
}
getSpecialOverlayRenderMatrix(rotation: any, rotationVariant: any, variant: any): any {
return overlayMatrices[variant][rotation];
}
getAvailableVariants(): any {
return [
defaultBuildingVariant,
enumLogicGateVariants.or,
enumLogicGateVariants.not,
enumLogicGateVariants.xor,
];
}
getRenderPins(): any {
// We already have it included
return false;
}
updateVariants(entity: Entity, rotationVariant: number, variant: any): any {
const gateType: any = enumVariantToGate[variant];
entity.components.LogicGate.type = gateType;
const pinComp: any = entity.components.WiredPins;
switch (gateType) {
case enumLogicGateType.and:
case enumLogicGateType.xor:
case enumLogicGateType.or: {
pinComp.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.top,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 0),
direction: enumDirection.left,
type: enumPinSlotType.logicalAcceptor,
},
{
pos: new Vector(0, 0),
direction: enumDirection.right,
type: enumPinSlotType.logicalAcceptor,
},
]);
break;
}
case enumLogicGateType.not: {
pinComp.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.top,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
]);
break;
}
default:
assertAlways("unknown logic gate type: " + gateType);
}
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new WiredPinsComponent({
slots: [],
}));
entity.addComponent(new LogicGateComponent({}));
}
}

@ -0,0 +1,70 @@
import { enumDirection, Vector } from "../../core/vector";
import { ItemEjectorComponent } from "../components/item_ejector";
import { MinerComponent } from "../components/miner";
import { Entity } from "../entity";
import { MetaBuilding, defaultBuildingVariant } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
import { T } from "../../translations";
import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils";
/** @enum {string} */
export const enumMinerVariants: any = { chainable: "chainable" };
const overlayMatrix: any = {
[defaultBuildingVariant]: generateMatrixRotations([1, 1, 1, 1, 0, 1, 1, 1, 1]),
[enumMinerVariants.chainable]: generateMatrixRotations([0, 1, 0, 1, 1, 1, 1, 1, 1]),
};
export class MetaMinerBuilding extends MetaBuilding {
constructor() {
super("miner");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 7,
variant: defaultBuildingVariant,
},
{
internalId: 8,
variant: enumMinerVariants.chainable,
},
];
}
getSilhouetteColor(): any {
return "#b37dcd";
}
/**
* {}
*/
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
string,
string
]> {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
const speed: any = root.hubGoals.getMinerBaseSpeed();
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
}
getAvailableVariants(root: GameRoot): any {
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_miner_chainable)) {
return [enumMinerVariants.chainable];
}
return super.getAvailableVariants(root);
}
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): any {
return overlayMatrix[variant][rotation];
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new MinerComponent({}));
entity.addComponent(new ItemEjectorComponent({
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
}));
}
updateVariants(entity: Entity, rotationVariant: number, variant: string): any {
entity.components.Miner.chainable = variant === enumMinerVariants.chainable;
}
}

@ -0,0 +1,72 @@
import { formatItemsPerSecond } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { T } from "../../translations";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
export class MetaMixerBuilding extends MetaBuilding {
constructor() {
super("mixer");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 15,
variant: defaultBuildingVariant,
},
];
}
getDimensions(): any {
return new Vector(2, 1);
}
getSilhouetteColor(): any {
return "#cdbb7d";
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_mixer);
}
/**
* {}
*/
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
string,
string
]> {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.mixer);
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new ItemProcessorComponent({
inputsPerCharge: 2,
processorType: enumItemProcessorTypes.mixer,
}));
entity.addComponent(new ItemEjectorComponent({
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
}));
entity.addComponent(new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
filter: "color",
},
{
pos: new Vector(1, 0),
direction: enumDirection.bottom,
filter: "color",
},
],
}));
}
}

@ -0,0 +1,243 @@
import { formatItemsPerSecond } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { T } from "../../translations";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent, enumItemProcessorRequirements, } from "../components/item_processor";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
import { WiredPinsComponent, enumPinSlotType } from "../components/wired_pins";
/** @enum {string} */
export const enumPainterVariants: any = { mirrored: "mirrored", double: "double", quad: "quad" };
export class MetaPainterBuilding extends MetaBuilding {
constructor() {
super("painter");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 16,
variant: defaultBuildingVariant,
},
{
internalId: 17,
variant: enumPainterVariants.mirrored,
},
{
internalId: 18,
variant: enumPainterVariants.double,
},
{
internalId: 19,
variant: enumPainterVariants.quad,
},
];
}
getDimensions(variant: any): any {
switch (variant) {
case defaultBuildingVariant:
case enumPainterVariants.mirrored:
return new Vector(2, 1);
case enumPainterVariants.double:
return new Vector(2, 2);
case enumPainterVariants.quad:
return new Vector(4, 1);
default:
assertAlways(false, "Unknown painter variant: " + variant);
}
}
getSilhouetteColor(): any {
return "#cd9b7d";
}
/**
* {}
*/
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
string,
string
]> {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
switch (variant) {
case defaultBuildingVariant:
case enumPainterVariants.mirrored: {
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.painter);
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
}
case enumPainterVariants.double: {
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.painterDouble);
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed, true)]];
}
case enumPainterVariants.quad: {
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.painterQuad);
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
}
}
}
getAvailableVariants(root: GameRoot): any {
let variants: any = [defaultBuildingVariant, enumPainterVariants.mirrored];
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter_double)) {
variants.push(enumPainterVariants.double);
}
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers) &&
root.gameMode.getSupportsWires()) {
variants.push(enumPainterVariants.quad);
}
return variants;
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter);
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new ItemProcessorComponent({}));
entity.addComponent(new ItemEjectorComponent({
slots: [{ pos: new Vector(1, 0), direction: enumDirection.right }],
}));
entity.addComponent(new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(1, 0),
direction: enumDirection.top,
filter: "color",
},
],
}));
}
updateVariants(entity: Entity, rotationVariant: number, variant: string): any {
switch (variant) {
case defaultBuildingVariant:
case enumPainterVariants.mirrored: {
// REGULAR PAINTER
if (entity.components.WiredPins) {
entity.removeComponent(WiredPinsComponent);
}
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(1, 0),
direction: variant === defaultBuildingVariant ? enumDirection.top : enumDirection.bottom,
filter: "color",
},
]);
entity.components.ItemEjector.setSlots([
{ pos: new Vector(1, 0), direction: enumDirection.right },
]);
entity.components.ItemProcessor.type = enumItemProcessorTypes.painter;
entity.components.ItemProcessor.processingRequirement = null;
entity.components.ItemProcessor.inputsPerCharge = 2;
break;
}
case enumPainterVariants.double: {
// DOUBLE PAINTER
if (entity.components.WiredPins) {
entity.removeComponent(WiredPinsComponent);
}
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(0, 1),
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(1, 0),
direction: enumDirection.top,
filter: "color",
},
]);
entity.components.ItemEjector.setSlots([
{ pos: new Vector(1, 0), direction: enumDirection.right },
]);
entity.components.ItemProcessor.type = enumItemProcessorTypes.painterDouble;
entity.components.ItemProcessor.processingRequirement = null;
entity.components.ItemProcessor.inputsPerCharge = 3;
break;
}
case enumPainterVariants.quad: {
// QUAD PAINTER
if (!entity.components.WiredPins) {
entity.addComponent(new WiredPinsComponent({ slots: [] }));
}
entity.components.WiredPins.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
{
pos: new Vector(1, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
{
pos: new Vector(2, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
{
pos: new Vector(3, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
]);
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
filter: "color",
},
{
pos: new Vector(1, 0),
direction: enumDirection.bottom,
filter: "color",
},
{
pos: new Vector(2, 0),
direction: enumDirection.bottom,
filter: "color",
},
{
pos: new Vector(3, 0),
direction: enumDirection.bottom,
filter: "color",
},
]);
entity.components.ItemEjector.setSlots([
{ pos: new Vector(0, 0), direction: enumDirection.top },
]);
entity.components.ItemProcessor.type = enumItemProcessorTypes.painterQuad;
entity.components.ItemProcessor.processingRequirement =
enumItemProcessorRequirements.painterQuad;
entity.components.ItemProcessor.inputsPerCharge = 5;
break;
}
default:
assertAlways(false, "Unknown painter variant: " + variant);
}
}
}

@ -0,0 +1,93 @@
import { enumDirection, Vector } from "../../core/vector";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { BeltUnderlaysComponent } from "../components/belt_underlays";
import { BeltReaderComponent } from "../components/belt_reader";
import { enumHubGoalRewards } from "../tutorial_goals";
import { generateMatrixRotations } from "../../core/utils";
const overlayMatrix: any = generateMatrixRotations([0, 1, 0, 0, 1, 0, 0, 1, 0]);
export class MetaReaderBuilding extends MetaBuilding {
constructor() {
super("reader");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 49,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor(): any {
return "#25fff2";
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_belt_reader);
}
getDimensions(): any {
return new Vector(1, 1);
}
getShowWiresLayerPreview(): any {
return true;
}
/**
* {}
*/
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): Array<number> | null {
return overlayMatrix[rotation];
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new WiredPinsComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.right,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 0),
direction: enumDirection.left,
type: enumPinSlotType.logicalEjector,
},
],
}));
entity.addComponent(new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
},
],
}));
entity.addComponent(new ItemEjectorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.top,
},
],
}));
entity.addComponent(new ItemProcessorComponent({
processorType: enumItemProcessorTypes.reader,
inputsPerCharge: 1,
}));
entity.addComponent(new BeltUnderlaysComponent({
underlays: [
{
pos: new Vector(0, 0),
direction: enumDirection.top,
},
],
}));
entity.addComponent(new BeltReaderComponent());
}
}

@ -0,0 +1,129 @@
import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { T } from "../../translations";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
/** @enum {string} */
export const enumRotaterVariants: any = { ccw: "ccw", rotate180: "rotate180" };
const overlayMatrices: any = {
[defaultBuildingVariant]: generateMatrixRotations([0, 1, 1, 1, 1, 0, 0, 1, 1]),
[enumRotaterVariants.ccw]: generateMatrixRotations([1, 1, 0, 0, 1, 1, 1, 1, 0]),
[enumRotaterVariants.rotate180]: generateMatrixRotations([1, 1, 0, 1, 1, 1, 0, 1, 1]),
};
export class MetaRotaterBuilding extends MetaBuilding {
constructor() {
super("rotater");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 11,
variant: defaultBuildingVariant,
},
{
internalId: 12,
variant: enumRotaterVariants.ccw,
},
{
internalId: 13,
variant: enumRotaterVariants.rotate180,
},
];
}
getSilhouetteColor(): any {
return "#7dc6cd";
}
/**
* {}
*/
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): Array<number> | null {
const matrix: any = overlayMatrices[variant];
if (matrix) {
return matrix[rotation];
}
return null;
}
/**
* {}
*/
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
string,
string
]> {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
switch (variant) {
case defaultBuildingVariant: {
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotater);
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
}
case enumRotaterVariants.ccw: {
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotaterCCW);
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
}
case enumRotaterVariants.rotate180: {
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotater180);
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
}
}
}
getAvailableVariants(root: GameRoot): any {
let variants: any = [defaultBuildingVariant];
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_rotater_ccw)) {
variants.push(enumRotaterVariants.ccw);
}
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_rotater_180)) {
variants.push(enumRotaterVariants.rotate180);
}
return variants;
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_rotater);
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new ItemProcessorComponent({
inputsPerCharge: 1,
processorType: enumItemProcessorTypes.rotater,
}));
entity.addComponent(new ItemEjectorComponent({
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
}));
entity.addComponent(new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
filter: "shape",
},
],
}));
}
updateVariants(entity: Entity, rotationVariant: number, variant: string): any {
switch (variant) {
case defaultBuildingVariant: {
entity.components.ItemProcessor.type = enumItemProcessorTypes.rotater;
break;
}
case enumRotaterVariants.ccw: {
entity.components.ItemProcessor.type = enumItemProcessorTypes.rotaterCCW;
break;
}
case enumRotaterVariants.rotate180: {
entity.components.ItemProcessor.type = enumItemProcessorTypes.rotater180;
break;
}
default:
assertAlways(false, "Unknown rotater variant: " + variant);
}
}
}

@ -0,0 +1,72 @@
import { formatItemsPerSecond } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { T } from "../../translations";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
export class MetaStackerBuilding extends MetaBuilding {
constructor() {
super("stacker");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 14,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor(): any {
return "#9fcd7d";
}
getDimensions(): any {
return new Vector(2, 1);
}
/**
* {}
*/
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
string,
string
]> {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.stacker);
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_stacker);
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new ItemProcessorComponent({
inputsPerCharge: 2,
processorType: enumItemProcessorTypes.stacker,
}));
entity.addComponent(new ItemEjectorComponent({
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
}));
entity.addComponent(new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
filter: "shape",
},
{
pos: new Vector(1, 0),
direction: enumDirection.bottom,
filter: "shape",
},
],
}));
}
}

@ -0,0 +1,91 @@
import { formatBigNumber } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { T } from "../../translations";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { StorageComponent } from "../components/storage";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
const storageSize: any = 5000;
export class MetaStorageBuilding extends MetaBuilding {
constructor() {
super("storage");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 21,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor(): any {
return "#bbdf6d";
}
/**
* {}
*/
getAdditionalStatistics(root: any, variant: any): Array<[
string,
string
]> {
return [[T.ingame.buildingPlacement.infoTexts.storage, formatBigNumber(storageSize)]];
}
getDimensions(): any {
return new Vector(2, 2);
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_storage);
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
// Required, since the item processor needs this.
entity.addComponent(new ItemEjectorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.top,
},
{
pos: new Vector(1, 0),
direction: enumDirection.top,
},
],
}));
entity.addComponent(new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 1),
direction: enumDirection.bottom,
},
{
pos: new Vector(1, 1),
direction: enumDirection.bottom,
},
],
}));
entity.addComponent(new StorageComponent({
maximumStorage: storageSize,
}));
entity.addComponent(new WiredPinsComponent({
slots: [
{
pos: new Vector(1, 1),
direction: enumDirection.right,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 1),
direction: enumDirection.left,
type: enumPinSlotType.logicalEjector,
},
],
}));
}
}

@ -0,0 +1,88 @@
import { generateMatrixRotations } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
/** @enum {string} */
export const enumTransistorVariants: any = {
mirrored: "mirrored",
};
const overlayMatrices: any = {
[defaultBuildingVariant]: generateMatrixRotations([0, 1, 0, 1, 1, 0, 0, 1, 0]),
[enumTransistorVariants.mirrored]: generateMatrixRotations([0, 1, 0, 0, 1, 1, 0, 1, 0]),
};
export class MetaTransistorBuilding extends MetaBuilding {
constructor() {
super("transistor");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 38,
variant: defaultBuildingVariant,
},
{
internalId: 60,
variant: enumTransistorVariants.mirrored,
},
];
}
getSilhouetteColor(): any {
return "#bc3a61";
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_logic_gates);
}
/** {} **/
getLayer(): "wires" {
return "wires";
}
getDimensions(): any {
return new Vector(1, 1);
}
getAvailableVariants(): any {
return [defaultBuildingVariant, enumTransistorVariants.mirrored];
}
getSpecialOverlayRenderMatrix(rotation: any, rotationVariant: any, variant: any): any {
return overlayMatrices[variant][rotation];
}
getRenderPins(): any {
// We already have it included
return false;
}
updateVariants(entity: Entity, rotationVariant: number, variant: any): any {
entity.components.WiredPins.slots[1].direction =
variant === enumTransistorVariants.mirrored ? enumDirection.right : enumDirection.left;
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new WiredPinsComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.top,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 0),
direction: enumDirection.left,
type: enumPinSlotType.logicalAcceptor,
},
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
],
}));
entity.addComponent(new LogicGateComponent({
type: enumLogicGateType.transistor,
}));
}
}

@ -0,0 +1,83 @@
import { generateMatrixRotations } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { ACHIEVEMENTS } from "../../platform/achievement_provider";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
const overlayMatrix: any = generateMatrixRotations([1, 1, 0, 1, 1, 1, 0, 1, 1]);
export class MetaTrashBuilding extends MetaBuilding {
constructor() {
super("trash");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 20,
variant: defaultBuildingVariant,
},
];
}
getIsRotateable(): any {
return false;
}
getSilhouetteColor(): any {
return "#ed1d5d";
}
getDimensions(): any {
return new Vector(1, 1);
}
getSpecialOverlayRenderMatrix(rotation: any): any {
return overlayMatrix[rotation];
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_cutter_and_trash);
}
addAchievementReceiver(entity: any): any {
if (!entity.root) {
return;
}
const itemProcessor: any = entity.components.ItemProcessor;
const tryTakeItem: any = itemProcessor.tryTakeItem.bind(itemProcessor);
itemProcessor.tryTakeItem = (): any => {
const taken: any = tryTakeItem(...arguments);
if (taken) {
entity.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.trash1000, 1);
}
return taken;
};
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.top,
},
{
pos: new Vector(0, 0),
direction: enumDirection.right,
},
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
},
{
pos: new Vector(0, 0),
direction: enumDirection.left,
},
],
}));
entity.addComponent(new ItemProcessorComponent({
inputsPerCharge: 1,
processorType: enumItemProcessorTypes.trash,
}));
this.addAchievementReceiver(entity);
}
}

@ -0,0 +1,237 @@
import { Loader } from "../../core/loader";
import { enumDirection, Vector, enumAngleToDirection, enumDirectionToVector } from "../../core/vector";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumUndergroundBeltMode, UndergroundBeltComponent } from "../components/underground_belt";
import { Entity } from "../entity";
import { MetaBuilding, defaultBuildingVariant } from "../meta_building";
import { GameRoot } from "../root";
import { globalConfig } from "../../core/config";
import { enumHubGoalRewards } from "../tutorial_goals";
import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils";
import { T } from "../../translations";
/** @enum {string} */
export const arrayUndergroundRotationVariantToMode: any = [
enumUndergroundBeltMode.sender,
enumUndergroundBeltMode.receiver,
];
/** @enum {string} */
export const enumUndergroundBeltVariants: any = { tier2: "tier2" };
export const enumUndergroundBeltVariantToTier: any = {
[defaultBuildingVariant]: 0,
[enumUndergroundBeltVariants.tier2]: 1,
};
const colorsByRotationVariant: any = ["#6d9dff", "#71ff9c"];
const overlayMatrices: any = [
// Sender
generateMatrixRotations([1, 1, 1, 0, 1, 0, 0, 1, 0]),
// Receiver
generateMatrixRotations([0, 1, 0, 0, 1, 0, 1, 1, 1]),
];
export class MetaUndergroundBeltBuilding extends MetaBuilding {
constructor() {
super("underground_belt");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 22,
variant: defaultBuildingVariant,
rotationVariant: 0,
},
{
internalId: 23,
variant: defaultBuildingVariant,
rotationVariant: 1,
},
{
internalId: 24,
variant: enumUndergroundBeltVariants.tier2,
rotationVariant: 0,
},
{
internalId: 25,
variant: enumUndergroundBeltVariants.tier2,
rotationVariant: 1,
},
];
}
getSilhouetteColor(variant: any, rotationVariant: any): any {
return colorsByRotationVariant[rotationVariant];
}
getFlipOrientationAfterPlacement(): any {
return true;
}
getStayInPlacementMode(): any {
return true;
}
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): any {
return overlayMatrices[rotationVariant][rotation];
}
/**
* {}
*/
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
string,
string
]> {
const rangeTiles: any = globalConfig.undergroundBeltMaxTilesByTier[enumUndergroundBeltVariantToTier[variant]];
const beltSpeed: any = root.hubGoals.getUndergroundBeltBaseSpeed();
const stats: Array<[
string,
string
]> = [
[
T.ingame.buildingPlacement.infoTexts.range,
T.ingame.buildingPlacement.infoTexts.tiles.replace("<x>", "" + rangeTiles),
],
];
if (root.gameMode.throughputDoesNotMatter()) {
return stats;
}
stats.push([T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]);
return stats;
}
getAvailableVariants(root: GameRoot): any {
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_underground_belt_tier_2)) {
return [defaultBuildingVariant, enumUndergroundBeltVariants.tier2];
}
return super.getAvailableVariants(root);
}
getPreviewSprite(rotationVariant: number, variant: string): any {
let suffix: any = "";
if (variant !== defaultBuildingVariant) {
suffix = "-" + variant;
}
switch (arrayUndergroundRotationVariantToMode[rotationVariant]) {
case enumUndergroundBeltMode.sender:
return Loader.getSprite("sprites/buildings/underground_belt_entry" + suffix + ".png");
case enumUndergroundBeltMode.receiver:
return Loader.getSprite("sprites/buildings/underground_belt_exit" + suffix + ".png");
default:
assertAlways(false, "Invalid rotation variant");
}
}
getBlueprintSprite(rotationVariant: number, variant: string): any {
let suffix: any = "";
if (variant !== defaultBuildingVariant) {
suffix = "-" + variant;
}
switch (arrayUndergroundRotationVariantToMode[rotationVariant]) {
case enumUndergroundBeltMode.sender:
return Loader.getSprite("sprites/blueprints/underground_belt_entry" + suffix + ".png");
case enumUndergroundBeltMode.receiver:
return Loader.getSprite("sprites/blueprints/underground_belt_exit" + suffix + ".png");
default:
assertAlways(false, "Invalid rotation variant");
}
}
getSprite(rotationVariant: number, variant: string): any {
return this.getPreviewSprite(rotationVariant, variant);
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_tunnel);
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
// Required, since the item processor needs this.
entity.addComponent(new ItemEjectorComponent({
slots: [],
}));
entity.addComponent(new UndergroundBeltComponent({}));
entity.addComponent(new ItemAcceptorComponent({
slots: [],
}));
}
/**
* Should compute the optimal rotation variant on the given tile
* @return {{ rotation: number, rotationVariant: number, connectedEntities?: Array<Entity> }}
*/
computeOptimalDirectionAndRotationVariantAtTile({ root, tile, rotation, variant, layer }: {
root: GameRoot;
tile: Vector;
rotation: number;
variant: string;
layer: Layer;
}): {
rotation: number;
rotationVariant: number;
connectedEntities?: Array<Entity>;
} {
const searchDirection: any = enumAngleToDirection[rotation];
const searchVector: any = enumDirectionToVector[searchDirection];
const tier: any = enumUndergroundBeltVariantToTier[variant];
const targetRotation: any = (rotation + 180) % 360;
const targetSenderRotation: any = rotation;
for (let searchOffset: any = 1; searchOffset <= globalConfig.undergroundBeltMaxTilesByTier[tier]; ++searchOffset) {
tile = tile.addScalars(searchVector.x, searchVector.y);
const contents: any = root.map.getTileContent(tile, "regular");
if (contents) {
const undergroundComp: any = contents.components.UndergroundBelt;
if (undergroundComp && undergroundComp.tier === tier) {
const staticComp: any = contents.components.StaticMapEntity;
if (staticComp.rotation === targetRotation) {
if (undergroundComp.mode !== enumUndergroundBeltMode.sender) {
// If we encounter an underground receiver on our way which is also faced in our direction, we don't accept that
break;
}
return {
rotation: targetRotation,
rotationVariant: 1,
connectedEntities: [contents],
};
}
else if (staticComp.rotation === targetSenderRotation) {
// Draw connections to receivers
if (undergroundComp.mode === enumUndergroundBeltMode.receiver) {
return {
rotation: rotation,
rotationVariant: 0,
connectedEntities: [contents],
};
}
else {
break;
}
}
}
}
}
return {
rotation,
rotationVariant: 0,
};
}
updateVariants(entity: Entity, rotationVariant: number, variant: string): any {
entity.components.UndergroundBelt.tier = enumUndergroundBeltVariantToTier[variant];
switch (arrayUndergroundRotationVariantToMode[rotationVariant]) {
case enumUndergroundBeltMode.sender: {
entity.components.UndergroundBelt.mode = enumUndergroundBeltMode.sender;
entity.components.ItemEjector.setSlots([]);
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
},
]);
return;
}
case enumUndergroundBeltMode.receiver: {
entity.components.UndergroundBelt.mode = enumUndergroundBeltMode.receiver;
entity.components.ItemAcceptor.setSlots([]);
entity.components.ItemEjector.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.top,
},
]);
return;
}
default:
assertAlways(false, "Invalid rotation variant");
}
}
}

@ -0,0 +1,164 @@
import { Vector, enumDirection } from "../../core/vector";
import { LogicGateComponent, enumLogicGateType } from "../components/logic_gate";
import { WiredPinsComponent, enumPinSlotType } from "../components/wired_pins";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
import { MetaCutterBuilding } from "./cutter";
import { MetaPainterBuilding } from "./painter";
import { MetaRotaterBuilding } from "./rotater";
import { MetaStackerBuilding } from "./stacker";
/** @enum {string} */
export const enumVirtualProcessorVariants: any = {
rotater: "rotater",
unstacker: "unstacker",
stacker: "stacker",
painter: "painter",
};
/** @enum {string} */
const enumVariantToGate: any = {
[defaultBuildingVariant]: enumLogicGateType.cutter,
[enumVirtualProcessorVariants.rotater]: enumLogicGateType.rotater,
[enumVirtualProcessorVariants.unstacker]: enumLogicGateType.unstacker,
[enumVirtualProcessorVariants.stacker]: enumLogicGateType.stacker,
[enumVirtualProcessorVariants.painter]: enumLogicGateType.painter,
};
const colors: any = {
[defaultBuildingVariant]: new MetaCutterBuilding().getSilhouetteColor(),
[enumVirtualProcessorVariants.rotater]: new MetaRotaterBuilding().getSilhouetteColor(),
[enumVirtualProcessorVariants.unstacker]: new MetaStackerBuilding().getSilhouetteColor(),
[enumVirtualProcessorVariants.stacker]: new MetaStackerBuilding().getSilhouetteColor(),
[enumVirtualProcessorVariants.painter]: new MetaPainterBuilding().getSilhouetteColor(),
};
export class MetaVirtualProcessorBuilding extends MetaBuilding {
constructor() {
super("virtual_processor");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 42,
variant: defaultBuildingVariant,
},
{
internalId: 44,
variant: enumVirtualProcessorVariants.rotater,
},
{
internalId: 45,
variant: enumVirtualProcessorVariants.unstacker,
},
{
internalId: 50,
variant: enumVirtualProcessorVariants.stacker,
},
{
internalId: 51,
variant: enumVirtualProcessorVariants.painter,
},
];
}
getSilhouetteColor(variant: any): any {
return colors[variant];
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_virtual_processing);
}
/** {} **/
getLayer(): "wires" {
return "wires";
}
getDimensions(): any {
return new Vector(1, 1);
}
getAvailableVariants(): any {
return [
defaultBuildingVariant,
enumVirtualProcessorVariants.rotater,
enumVirtualProcessorVariants.stacker,
enumVirtualProcessorVariants.painter,
enumVirtualProcessorVariants.unstacker,
];
}
getRenderPins(): any {
// We already have it included
return false;
}
updateVariants(entity: Entity, rotationVariant: number, variant: any): any {
const gateType: any = enumVariantToGate[variant];
entity.components.LogicGate.type = gateType;
const pinComp: any = entity.components.WiredPins;
switch (gateType) {
case enumLogicGateType.cutter:
case enumLogicGateType.unstacker: {
pinComp.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.left,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 0),
direction: enumDirection.right,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
]);
break;
}
case enumLogicGateType.rotater: {
pinComp.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.top,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
]);
break;
}
case enumLogicGateType.stacker:
case enumLogicGateType.painter: {
pinComp.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.top,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
{
pos: new Vector(0, 0),
direction: enumDirection.right,
type: enumPinSlotType.logicalAcceptor,
},
]);
break;
}
default:
assertAlways("unknown logic gate type: " + gateType);
}
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new WiredPinsComponent({
slots: [],
}));
entity.addComponent(new LogicGateComponent({}));
}
}

@ -0,0 +1,258 @@
import { Loader } from "../../core/loader";
import { generateMatrixRotations } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { SOUNDS } from "../../platform/sound";
import { enumWireType, enumWireVariant, WireComponent } from "../components/wire";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
export const arrayWireRotationVariantToType: any = [
enumWireType.forward,
enumWireType.turn,
enumWireType.split,
enumWireType.cross,
];
export const wireOverlayMatrices: any = {
[enumWireType.forward]: generateMatrixRotations([0, 1, 0, 0, 1, 0, 0, 1, 0]),
[enumWireType.split]: generateMatrixRotations([0, 0, 0, 1, 1, 1, 0, 1, 0]),
[enumWireType.turn]: generateMatrixRotations([0, 0, 0, 0, 1, 1, 0, 1, 0]),
[enumWireType.cross]: generateMatrixRotations([0, 1, 0, 1, 1, 1, 0, 1, 0]),
};
/** @enum {string} */
export const wireVariants: any = {
second: "second",
};
const enumWireVariantToVariant: any = {
[defaultBuildingVariant]: enumWireVariant.first,
[wireVariants.second]: enumWireVariant.second,
};
export class MetaWireBuilding extends MetaBuilding {
constructor() {
super("wire");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 27,
variant: defaultBuildingVariant,
rotationVariant: 0,
},
{
internalId: 28,
variant: defaultBuildingVariant,
rotationVariant: 1,
},
{
internalId: 29,
variant: defaultBuildingVariant,
rotationVariant: 2,
},
{
internalId: 30,
variant: defaultBuildingVariant,
rotationVariant: 3,
},
{
internalId: 52,
variant: enumWireVariant.second,
rotationVariant: 0,
},
{
internalId: 53,
variant: enumWireVariant.second,
rotationVariant: 1,
},
{
internalId: 54,
variant: enumWireVariant.second,
rotationVariant: 2,
},
{
internalId: 55,
variant: enumWireVariant.second,
rotationVariant: 3,
},
];
}
getHasDirectionLockAvailable(): any {
return true;
}
getSilhouetteColor(): any {
return "#61ef6f";
}
getAvailableVariants(): any {
return [defaultBuildingVariant, wireVariants.second];
}
getDimensions(): any {
return new Vector(1, 1);
}
getStayInPlacementMode(): any {
return true;
}
getPlacementSound(): any {
return SOUNDS.placeBelt;
}
getRotateAutomaticallyWhilePlacing(): any {
return true;
}
/** {} **/
getLayer(): "wires" {
return "wires";
}
getSprite(): any {
return null;
}
getIsReplaceable(): any {
return true;
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers);
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new WireComponent({}));
}
updateVariants(entity: Entity, rotationVariant: number, variant: string): any {
entity.components.Wire.type = arrayWireRotationVariantToType[rotationVariant];
entity.components.Wire.variant = enumWireVariantToVariant[variant];
}
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): any {
return wireOverlayMatrices[entity.components.Wire.type][rotation];
}
/**
*
* {}
*/
getPreviewSprite(rotationVariant: number, variant: string): import("../../core/draw_utils").AtlasSprite {
const wireVariant: any = enumWireVariantToVariant[variant];
switch (arrayWireRotationVariantToType[rotationVariant]) {
case enumWireType.forward: {
return Loader.getSprite("sprites/wires/sets/" + wireVariant + "_forward.png");
}
case enumWireType.turn: {
return Loader.getSprite("sprites/wires/sets/" + wireVariant + "_turn.png");
}
case enumWireType.split: {
return Loader.getSprite("sprites/wires/sets/" + wireVariant + "_split.png");
}
case enumWireType.cross: {
return Loader.getSprite("sprites/wires/sets/" + wireVariant + "_cross.png");
}
default: {
assertAlways(false, "Invalid wire rotation variant");
}
}
}
getBlueprintSprite(rotationVariant: any, variant: any): any {
return this.getPreviewSprite(rotationVariant, variant);
}
/**
* Should compute the optimal rotation variant on the given tile
* @return {{ rotation: number, rotationVariant: number, connectedEntities?: Array<Entity> }}
*/
computeOptimalDirectionAndRotationVariantAtTile({ root, tile, rotation, variant, layer }: {
root: GameRoot;
tile: Vector;
rotation: number;
variant: string;
layer: string;
}): {
rotation: number;
rotationVariant: number;
connectedEntities?: Array<Entity>;
} {
const wireVariant: any = enumWireVariantToVariant[variant];
const connections: any = {
top: root.logic.computeWireEdgeStatus({ tile, wireVariant, edge: enumDirection.top }),
right: root.logic.computeWireEdgeStatus({ tile, wireVariant, edge: enumDirection.right }),
bottom: root.logic.computeWireEdgeStatus({ tile, wireVariant, edge: enumDirection.bottom }),
left: root.logic.computeWireEdgeStatus({ tile, wireVariant, edge: enumDirection.left }),
};
let flag: any = 0;
flag |= connections.top ? 0x1000 : 0;
flag |= connections.right ? 0x100 : 0;
flag |= connections.bottom ? 0x10 : 0;
flag |= connections.left ? 0x1 : 0;
let targetType: any = enumWireType.forward;
// First, reset rotation
rotation = 0;
switch (flag) {
case 0x0000:
// Nothing
break;
case 0x0001:
// Left
rotation += 90;
break;
case 0x0010:
// Bottom
// END
break;
case 0x0011:
// Bottom | Left
targetType = enumWireType.turn;
rotation += 90;
break;
case 0x0100:
// Right
rotation += 90;
break;
case 0x0101:
// Right | Left
rotation += 90;
break;
case 0x0110:
// Right | Bottom
targetType = enumWireType.turn;
break;
case 0x0111:
// Right | Bottom | Left
targetType = enumWireType.split;
break;
case 0x1000:
// Top
break;
case 0x1001:
// Top | Left
targetType = enumWireType.turn;
rotation += 180;
break;
case 0x1010:
// Top | Bottom
break;
case 0x1011:
// Top | Bottom | Left
targetType = enumWireType.split;
rotation += 90;
break;
case 0x1100:
// Top | Right
targetType = enumWireType.turn;
rotation -= 90;
break;
case 0x1101:
// Top | Right | Left
targetType = enumWireType.split;
rotation += 180;
break;
case 0x1110:
// Top | Right | Bottom
targetType = enumWireType.split;
rotation -= 90;
break;
case 0x1111:
// Top | Right | Bottom | Left
targetType = enumWireType.cross;
break;
}
return {
// Clamp rotation
rotation: (rotation + 360 * 10) % 360,
rotationVariant: arrayWireRotationVariantToType.indexOf(targetType),
};
}
}

@ -0,0 +1,47 @@
import { generateMatrixRotations } from "../../core/utils";
import { Vector } from "../../core/vector";
import { WireTunnelComponent } from "../components/wire_tunnel";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
const wireTunnelOverlayMatrix: any = generateMatrixRotations([0, 1, 0, 1, 1, 1, 0, 1, 0]);
export class MetaWireTunnelBuilding extends MetaBuilding {
constructor() {
super("wire_tunnel");
}
static getAllVariantCombinations(): any {
return [
{
internalId: 39,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor(): any {
return "#777a86";
}
getIsUnlocked(root: GameRoot): any {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers);
}
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): any {
return wireTunnelOverlayMatrix[rotation];
}
getIsRotateable(): any {
return false;
}
getDimensions(): any {
return new Vector(1, 1);
}
/** {} **/
getLayer(): "wires" {
return "wires";
}
/**
* Creates the entity at the given location
*/
setupEntityComponents(entity: Entity): any {
entity.addComponent(new WireTunnelComponent());
}
}

@ -0,0 +1,825 @@
import { clickDetectorGlobals } from "../core/click_detector";
import { globalConfig, SUPPORT_TOUCH } from "../core/config";
import { createLogger } from "../core/logging";
import { Rectangle } from "../core/rectangle";
import { Signal, STOP_PROPAGATION } from "../core/signal";
import { clamp } from "../core/utils";
import { mixVector, Vector } from "../core/vector";
import { BasicSerializableObject, types } from "../savegame/serialization";
import { KEYMAPPINGS } from "./key_action_mapper";
import { GameRoot } from "./root";
const logger: any = createLogger("camera");
export const USER_INTERACT_MOVE: any = "move";
export const USER_INTERACT_ZOOM: any = "zoom";
export const USER_INTERACT_TOUCHEND: any = "touchend";
const velocitySmoothing: any = 0.5;
const velocityFade: any = 0.98;
const velocityStrength: any = 0.4;
const velocityMax: any = 20;
const ticksBeforeErasingVelocity: any = 10;
/**
* @enum {string}
*/
export const enumMouseButton: any = {
left: "left",
middle: "middle",
right: "right",
};
export class Camera extends BasicSerializableObject {
public root: GameRoot = root;
public zoomLevel = this.findInitialZoom();
public center: Vector = new Vector(0, 0);
public currentlyMoving = false;
public lastMovingPosition = null;
public lastMovingPositionLastTick = null;
public numTicksStandingStill = null;
public cameraUpdateTimeBucket = 0.0;
public didMoveSinceTouchStart = false;
public currentlyPinching = false;
public lastPinchPositions = null;
public keyboardForce = new Vector();
public userInteraction = new Signal();
public currentShake: Vector = new Vector(0, 0);
public currentPan: Vector = new Vector(0, 0);
public desiredPan: Vector = new Vector(0, 0);
public desiredCenter: Vector = null;
public desiredZoom: number = null;
public touchPostMoveVelocity: Vector = new Vector(0, 0);
public downPreHandler = (new Signal() as TypedSignal<[
Vector,
enumMouseButton
]>);
public movePreHandler = (new Signal() as TypedSignal<[
Vector
]>);
public upPostHandler = (new Signal() as TypedSignal<[
Vector
]>);
constructor(root) {
super();
this.clampZoomLevel();
this.internalInitEvents();
this.clampZoomLevel();
this.bindKeys();
if (G_IS_DEV) {
window.addEventListener("keydown", (ev: any): any => {
if (ev.key === "i") {
this.zoomLevel = 3;
}
});
}
}
// Serialization
static getId(): any {
return "Camera";
}
static getSchema(): any {
return {
zoomLevel: types.float,
center: types.vector,
};
}
deserialize(data: any): any {
const errorCode: any = super.deserialize(data);
if (errorCode) {
return errorCode;
}
// Safety
this.clampZoomLevel();
}
// Simple getters & setters
addScreenShake(amount: any): any {
const currentShakeAmount: any = this.currentShake.length();
const scale: any = 1 / (1 + 3 * currentShakeAmount);
this.currentShake.x = this.currentShake.x + 2 * (Math.random() - 0.5) * scale * amount;
this.currentShake.y = this.currentShake.y + 2 * (Math.random() - 0.5) * scale * amount;
}
/**
* Sets a point in world space to focus on
*/
setDesiredCenter(center: Vector): any {
this.desiredCenter = center.copy();
this.currentlyMoving = false;
}
/**
* Sets a desired zoom level
*/
setDesiredZoom(zoom: number): any {
this.desiredZoom = zoom;
}
/**
* Returns if this camera is currently moving by a non-user interaction
*/
isCurrentlyMovingToDesiredCenter(): any {
return this.desiredCenter !== null;
}
/**
* Sets the camera pan, every frame the camera will move by this amount
*/
setPan(pan: Vector): any {
this.desiredPan = pan.copy();
}
/**
* Finds a good initial zoom level
*/
findInitialZoom(): any {
let desiredWorldSpaceWidth: any = 18 * globalConfig.tileSize;
if (window.innerWidth < 1000) {
desiredWorldSpaceWidth = 12 * globalConfig.tileSize;
}
const zoomLevelX: any = this.root.gameWidth / desiredWorldSpaceWidth;
const zoomLevelY: any = this.root.gameHeight / desiredWorldSpaceWidth;
const finalLevel: any = Math.min(zoomLevelX, zoomLevelY);
assert(Number.isFinite(finalLevel) && finalLevel > 0, "Invalid zoom level computed for initial zoom: " + finalLevel);
return finalLevel;
}
/**
* Clears all animations
*/
clearAnimations(): any {
this.touchPostMoveVelocity.x = 0;
this.touchPostMoveVelocity.y = 0;
this.desiredCenter = null;
this.desiredPan.x = 0;
this.desiredPan.y = 0;
this.currentPan.x = 0;
this.currentPan.y = 0;
this.currentlyPinching = false;
this.currentlyMoving = false;
this.lastMovingPosition = null;
this.didMoveSinceTouchStart = false;
this.desiredZoom = null;
}
/**
* Returns if the user is currently interacting with the camera
* {} true if the user interacts
*/
isCurrentlyInteracting(): boolean {
if (this.currentlyPinching) {
return true;
}
if (this.currentlyMoving) {
// Only interacting if moved at least once
return this.didMoveSinceTouchStart;
}
if (this.touchPostMoveVelocity.lengthSquare() > 1) {
return true;
}
return false;
}
/**
* Returns if in the next frame the viewport will change
* {} true if it willchange
*/
viewportWillChange(): boolean {
return this.desiredCenter !== null || this.desiredZoom !== null || this.isCurrentlyInteracting();
}
/**
* Cancels all interactions, that is user interaction and non user interaction
*/
cancelAllInteractions(): any {
this.touchPostMoveVelocity = new Vector(0, 0);
this.desiredCenter = null;
this.currentlyMoving = false;
this.currentlyPinching = false;
this.desiredZoom = null;
}
/**
* Returns effective viewport width
*/
getViewportWidth(): any {
return this.root.gameWidth / this.zoomLevel;
}
/**
* Returns effective viewport height
*/
getViewportHeight(): any {
return this.root.gameHeight / this.zoomLevel;
}
/**
* Returns effective world space viewport left
*/
getViewportLeft(): any {
return this.center.x - this.getViewportWidth() / 2 + (this.currentShake.x * 10) / this.zoomLevel;
}
/**
* Returns effective world space viewport right
*/
getViewportRight(): any {
return this.center.x + this.getViewportWidth() / 2 + (this.currentShake.x * 10) / this.zoomLevel;
}
/**
* Returns effective world space viewport top
*/
getViewportTop(): any {
return this.center.y - this.getViewportHeight() / 2 + (this.currentShake.x * 10) / this.zoomLevel;
}
/**
* Returns effective world space viewport bottom
*/
getViewportBottom(): any {
return this.center.y + this.getViewportHeight() / 2 + (this.currentShake.x * 10) / this.zoomLevel;
}
/**
* Returns the visible world space rect
* {}
*/
getVisibleRect(): Rectangle {
return Rectangle.fromTRBL(Math.floor(this.getViewportTop()), Math.ceil(this.getViewportRight()), Math.ceil(this.getViewportBottom()), Math.floor(this.getViewportLeft()));
}
getIsMapOverlayActive(): any {
return this.zoomLevel < globalConfig.mapChunkOverviewMinZoom;
}
/**
* Attaches all event listeners
*/
internalInitEvents(): any {
this.eventListenerTouchStart = this.onTouchStart.bind(this);
this.eventListenerTouchEnd = this.onTouchEnd.bind(this);
this.eventListenerTouchMove = this.onTouchMove.bind(this);
this.eventListenerMousewheel = this.onMouseWheel.bind(this);
this.eventListenerMouseDown = this.onMouseDown.bind(this);
this.eventListenerMouseMove = this.onMouseMove.bind(this);
this.eventListenerMouseUp = this.onMouseUp.bind(this);
if (SUPPORT_TOUCH) {
this.root.canvas.addEventListener("touchstart", this.eventListenerTouchStart);
this.root.canvas.addEventListener("touchend", this.eventListenerTouchEnd);
this.root.canvas.addEventListener("touchcancel", this.eventListenerTouchEnd);
this.root.canvas.addEventListener("touchmove", this.eventListenerTouchMove);
}
this.root.canvas.addEventListener("wheel", this.eventListenerMousewheel);
this.root.canvas.addEventListener("mousedown", this.eventListenerMouseDown);
this.root.canvas.addEventListener("mousemove", this.eventListenerMouseMove);
window.addEventListener("mouseup", this.eventListenerMouseUp);
// this.root.canvas.addEventListener("mouseout", this.eventListenerMouseUp);
}
/**
* Cleans up all event listeners
*/
cleanup(): any {
if (SUPPORT_TOUCH) {
this.root.canvas.removeEventListener("touchstart", this.eventListenerTouchStart);
this.root.canvas.removeEventListener("touchend", this.eventListenerTouchEnd);
this.root.canvas.removeEventListener("touchcancel", this.eventListenerTouchEnd);
this.root.canvas.removeEventListener("touchmove", this.eventListenerTouchMove);
}
this.root.canvas.removeEventListener("wheel", this.eventListenerMousewheel);
this.root.canvas.removeEventListener("mousedown", this.eventListenerMouseDown);
this.root.canvas.removeEventListener("mousemove", this.eventListenerMouseMove);
window.removeEventListener("mouseup", this.eventListenerMouseUp);
// this.root.canvas.removeEventListener("mouseout", this.eventListenerMouseUp);
}
/**
* Binds the arrow keys
*/
bindKeys(): any {
const mapper: any = this.root.keyMapper;
mapper.getBinding(KEYMAPPINGS.navigation.mapMoveUp).add((): any => (this.keyboardForce.y = -1));
mapper.getBinding(KEYMAPPINGS.navigation.mapMoveDown).add((): any => (this.keyboardForce.y = 1));
mapper.getBinding(KEYMAPPINGS.navigation.mapMoveRight).add((): any => (this.keyboardForce.x = 1));
mapper.getBinding(KEYMAPPINGS.navigation.mapMoveLeft).add((): any => (this.keyboardForce.x = -1));
mapper
.getBinding(KEYMAPPINGS.navigation.mapZoomIn)
.add((): any => (this.desiredZoom = this.zoomLevel * 1.2));
mapper
.getBinding(KEYMAPPINGS.navigation.mapZoomOut)
.add((): any => (this.desiredZoom = this.zoomLevel / 1.2));
mapper.getBinding(KEYMAPPINGS.navigation.centerMap).add((): any => this.centerOnMap());
}
centerOnMap(): any {
this.desiredCenter = new Vector(0, 0);
}
/**
* Converts from screen to world space
* {} world space
*/
screenToWorld(screen: Vector): Vector {
const centerSpace: any = screen.subScalars(this.root.gameWidth / 2, this.root.gameHeight / 2);
return centerSpace.divideScalar(this.zoomLevel).add(this.center);
}
/**
* Converts from world to screen space
* {} screen space
*/
worldToScreen(world: Vector): Vector {
const screenSpace: any = world.sub(this.center).multiplyScalar(this.zoomLevel);
return screenSpace.addScalars(this.root.gameWidth / 2, this.root.gameHeight / 2);
}
/**
* Returns if a point is on screen
* {} true if its on screen
*/
isWorldPointOnScreen(point: Vector): boolean {
const rect: any = this.getVisibleRect();
return rect.containsPoint(point.x, point.y);
}
getMaximumZoom(): any {
return this.root.gameMode.getMaximumZoom();
}
getMinimumZoom(): any {
return this.root.gameMode.getMinimumZoom();
}
/**
* Returns if we can further zoom in
* {}
*/
canZoomIn(): boolean {
return this.zoomLevel <= this.getMaximumZoom() - 0.01;
}
/**
* Returns if we can further zoom out
* {}
*/
canZoomOut(): boolean {
return this.zoomLevel >= this.getMinimumZoom() + 0.01;
}
// EVENTS
/**
* Checks if the mouse event is too close after a touch event and thus
* should get ignored
*/
checkPreventDoubleMouse(): any {
if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) {
return false;
}
return true;
}
/**
* Mousedown handler
*/
onMouseDown(event: MouseEvent): any {
if (event.cancelable) {
event.preventDefault();
// event.stopPropagation();
}
if (!this.checkPreventDoubleMouse()) {
return;
}
this.touchPostMoveVelocity = new Vector(0, 0);
if (event.button === 0) {
this.combinedSingleTouchStartHandler(event.clientX, event.clientY);
}
else if (event.button === 1) {
this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.middle);
}
else if (event.button === 2) {
this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.right);
}
return false;
}
/**
* Mousemove handler
*/
onMouseMove(event: MouseEvent): any {
if (event.cancelable) {
event.preventDefault();
// event.stopPropagation();
}
if (!this.checkPreventDoubleMouse()) {
return;
}
if (event.button === 0) {
this.combinedSingleTouchMoveHandler(event.clientX, event.clientY);
}
// Clamp everything afterwards
this.clampZoomLevel();
this.clampToBounds();
return false;
}
/**
* Mouseup handler
*/
onMouseUp(event: MouseEvent=): any {
if (event) {
if (event.cancelable) {
event.preventDefault();
// event.stopPropagation();
}
}
if (!this.checkPreventDoubleMouse()) {
return;
}
this.combinedSingleTouchStopHandler(event.clientX, event.clientY);
return false;
}
/**
* Mousewheel event
*/
onMouseWheel(event: WheelEvent): any {
if (event.cancelable) {
event.preventDefault();
// event.stopPropagation();
}
const prevZoom: any = this.zoomLevel;
const scale: any = 1 + 0.15 * this.root.app.settings.getScrollWheelSensitivity();
assert(Number.isFinite(scale), "Got invalid scale in mouse wheel event: " + event.deltaY);
assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *before* wheel: " + this.zoomLevel);
this.zoomLevel *= event.deltaY < 0 ? scale : 1 / scale;
assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *after* wheel: " + this.zoomLevel);
this.clampZoomLevel();
this.desiredZoom = null;
let mousePosition: any = this.root.app.mousePosition;
if (!this.root.app.settings.getAllSettings().zoomToCursor) {
mousePosition = new Vector(this.root.gameWidth / 2, this.root.gameHeight / 2);
}
if (mousePosition) {
const worldPos: any = this.root.camera.screenToWorld(mousePosition);
const worldDelta: any = worldPos.sub(this.center);
const actualDelta: any = this.zoomLevel / prevZoom - 1;
this.center = this.center.add(worldDelta.multiplyScalar(actualDelta));
this.desiredCenter = null;
}
return false;
}
/**
* Touch start handler
*/
onTouchStart(event: TouchEvent): any {
if (event.cancelable) {
event.preventDefault();
// event.stopPropagation();
}
clickDetectorGlobals.lastTouchTime = performance.now();
this.touchPostMoveVelocity = new Vector(0, 0);
if (event.touches.length === 1) {
const touch: any = event.touches[0];
this.combinedSingleTouchStartHandler(touch.clientX, touch.clientY);
}
else if (event.touches.length === 2) {
// if (this.pinchPreHandler.dispatch() === STOP_PROPAGATION) {
// // Something prevented pinching
// return false;
// }
const touch1: any = event.touches[0];
const touch2: any = event.touches[1];
this.currentlyMoving = false;
this.currentlyPinching = true;
this.lastPinchPositions = [
new Vector(touch1.clientX, touch1.clientY),
new Vector(touch2.clientX, touch2.clientY),
];
}
return false;
}
/**
* Touch move handler
*/
onTouchMove(event: TouchEvent): any {
if (event.cancelable) {
event.preventDefault();
// event.stopPropagation();
}
clickDetectorGlobals.lastTouchTime = performance.now();
if (event.touches.length === 1) {
const touch: any = event.touches[0];
this.combinedSingleTouchMoveHandler(touch.clientX, touch.clientY);
}
else if (event.touches.length === 2) {
if (this.currentlyPinching) {
const touch1: any = event.touches[0];
const touch2: any = event.touches[1];
const newPinchPositions: any = [
new Vector(touch1.clientX, touch1.clientY),
new Vector(touch2.clientX, touch2.clientY),
];
// Get distance of taps last time and now
const lastDistance: any = this.lastPinchPositions[0].distance(this.lastPinchPositions[1]);
const thisDistance: any = newPinchPositions[0].distance(newPinchPositions[1]);
// IMPORTANT to do math max here to avoid NaN and causing an invalid zoom level
const difference: any = thisDistance / Math.max(0.001, lastDistance);
// Find old center of zoom
let oldCenter: any = this.lastPinchPositions[0].centerPoint(this.lastPinchPositions[1]);
// Find new center of zoom
let center: any = newPinchPositions[0].centerPoint(newPinchPositions[1]);
// Compute movement
let movement: any = oldCenter.sub(center);
this.center.x += movement.x / this.zoomLevel;
this.center.y += movement.y / this.zoomLevel;
// Compute zoom
center = center.sub(new Vector(this.root.gameWidth / 2, this.root.gameHeight / 2));
// Apply zoom
assert(Number.isFinite(difference), "Invalid pinch difference: " +
difference +
"(last=" +
lastDistance +
", new = " +
thisDistance +
")");
this.zoomLevel *= difference;
// Stick to pivot point
const correcture: any = center.multiplyScalar(difference - 1).divideScalar(this.zoomLevel);
this.center = this.center.add(correcture);
this.lastPinchPositions = newPinchPositions;
this.userInteraction.dispatch(USER_INTERACT_MOVE);
// Since we zoomed, abort any programmed zooming
if (this.desiredZoom) {
this.desiredZoom = null;
}
}
}
// Clamp everything afterwards
this.clampZoomLevel();
return false;
}
/**
* Touch end and cancel handler
*/
onTouchEnd(event: TouchEvent=): any {
if (event) {
if (event.cancelable) {
event.preventDefault();
// event.stopPropagation();
}
}
clickDetectorGlobals.lastTouchTime = performance.now();
if (event.changedTouches.length === 0) {
logger.warn("Touch end without changed touches");
}
const touch: any = event.changedTouches[0];
this.combinedSingleTouchStopHandler(touch.clientX, touch.clientY);
return false;
}
/**
* Internal touch start handler
*/
combinedSingleTouchStartHandler(x: number, y: number): any {
const pos: any = new Vector(x, y);
if (this.downPreHandler.dispatch(pos, enumMouseButton.left) === STOP_PROPAGATION) {
// Somebody else captured it
return;
}
this.touchPostMoveVelocity = new Vector(0, 0);
this.currentlyMoving = true;
this.lastMovingPosition = pos;
this.lastMovingPositionLastTick = null;
this.numTicksStandingStill = 0;
this.didMoveSinceTouchStart = false;
}
/**
* Internal touch move handler
*/
combinedSingleTouchMoveHandler(x: number, y: number): any {
const pos: any = new Vector(x, y);
if (this.movePreHandler.dispatch(pos) === STOP_PROPAGATION) {
// Somebody else captured it
return;
}
if (!this.currentlyMoving) {
return false;
}
let delta: any = this.lastMovingPosition.sub(pos).divideScalar(this.zoomLevel);
if (G_IS_DEV && globalConfig.debug.testCulling) {
// When testing culling, we see everything from the same distance
delta = delta.multiplyScalar(this.zoomLevel * -2);
}
this.didMoveSinceTouchStart = this.didMoveSinceTouchStart || delta.length() > 0;
this.center = this.center.add(delta);
this.touchPostMoveVelocity = this.touchPostMoveVelocity
.multiplyScalar(velocitySmoothing)
.add(delta.multiplyScalar(1 - velocitySmoothing));
this.lastMovingPosition = pos;
this.userInteraction.dispatch(USER_INTERACT_MOVE);
// Since we moved, abort any programmed moving
if (this.desiredCenter) {
this.desiredCenter = null;
}
}
/**
* Internal touch stop handler
*/
combinedSingleTouchStopHandler(x: any, y: any): any {
if (this.currentlyMoving || this.currentlyPinching) {
this.currentlyMoving = false;
this.currentlyPinching = false;
this.lastMovingPosition = null;
this.lastMovingPositionLastTick = null;
this.numTicksStandingStill = 0;
this.lastPinchPositions = null;
this.userInteraction.dispatch(USER_INTERACT_TOUCHEND);
this.didMoveSinceTouchStart = false;
}
this.upPostHandler.dispatch(new Vector(x, y));
}
/**
* Clamps the camera zoom level within the allowed range
*/
clampZoomLevel(): any {
if (G_IS_DEV && globalConfig.debug.disableZoomLimits) {
return;
}
assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *before* clamp: " + this.zoomLevel);
this.zoomLevel = clamp(this.zoomLevel, this.getMinimumZoom(), this.getMaximumZoom());
assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *after* clamp: " + this.zoomLevel);
if (this.desiredZoom) {
this.desiredZoom = clamp(this.desiredZoom, this.getMinimumZoom(), this.getMaximumZoom());
}
}
/**
* Clamps the center within set boundaries
*/
clampToBounds(): any {
const bounds: any = this.root.gameMode.getCameraBounds();
if (!bounds) {
return;
}
const tileScaleBounds: any = this.root.gameMode.getCameraBounds().allScaled(globalConfig.tileSize);
this.center.x = clamp(this.center.x, tileScaleBounds.x, tileScaleBounds.x + tileScaleBounds.w);
this.center.y = clamp(this.center.y, tileScaleBounds.y, tileScaleBounds.y + tileScaleBounds.h);
}
/**
* Updates the camera
*/
update(dt: number): any {
dt = Math.min(dt, 33);
this.cameraUpdateTimeBucket += dt;
// Simulate movement of N FPS
const updatesPerFrame: any = 4;
const physicsStepSizeMs: any = 1000.0 / (60.0 * updatesPerFrame);
let now: any = this.root.time.systemNow() - 3 * physicsStepSizeMs;
while (this.cameraUpdateTimeBucket > physicsStepSizeMs) {
now += physicsStepSizeMs;
this.cameraUpdateTimeBucket -= physicsStepSizeMs;
this.internalUpdatePanning(now, physicsStepSizeMs);
this.internalUpdateMousePanning(now, physicsStepSizeMs);
this.internalUpdateZooming(now, physicsStepSizeMs);
this.internalUpdateCentering(now, physicsStepSizeMs);
this.internalUpdateShake(now, physicsStepSizeMs);
this.internalUpdateKeyboardForce(now, physicsStepSizeMs);
}
this.clampZoomLevel();
}
/**
* Prepares a context to transform it
*/
transform(context: CanvasRenderingContext2D): any {
if (G_IS_DEV && globalConfig.debug.testCulling) {
context.transform(1, 0, 0, 1, 100, 100);
return;
}
this.clampZoomLevel();
const zoom: any = this.zoomLevel;
context.transform(
// Scale, skew, rotate
zoom, 0, 0, zoom,
// Translate
-zoom * this.getViewportLeft(), -zoom * this.getViewportTop());
}
/**
* Internal shake handler
*/
internalUpdateShake(now: number, dt: number): any {
this.currentShake = this.currentShake.multiplyScalar(0.92);
}
/**
* Internal pan handler
*/
internalUpdatePanning(now: number, dt: number): any {
const baseStrength: any = velocityStrength * this.root.app.platformWrapper.getTouchPanStrength();
this.touchPostMoveVelocity = this.touchPostMoveVelocity.multiplyScalar(velocityFade);
// Check if the camera is being dragged but standing still: if not, zero out `touchPostMoveVelocity`.
if (this.currentlyMoving && this.desiredCenter === null) {
if (this.lastMovingPositionLastTick !== null &&
this.lastMovingPositionLastTick.equalsEpsilon(this.lastMovingPosition)) {
this.numTicksStandingStill++;
}
else {
this.numTicksStandingStill = 0;
}
this.lastMovingPositionLastTick = this.lastMovingPosition.copy();
if (this.numTicksStandingStill >= ticksBeforeErasingVelocity) {
this.touchPostMoveVelocity.x = 0;
this.touchPostMoveVelocity.y = 0;
}
}
// Check influence of past points
if (!this.currentlyMoving && !this.currentlyPinching) {
const len: any = this.touchPostMoveVelocity.length();
if (len >= velocityMax) {
this.touchPostMoveVelocity.x = (this.touchPostMoveVelocity.x * velocityMax) / len;
this.touchPostMoveVelocity.y = (this.touchPostMoveVelocity.y * velocityMax) / len;
}
this.center = this.center.add(this.touchPostMoveVelocity.multiplyScalar(baseStrength));
// Panning
this.currentPan = mixVector(this.currentPan, this.desiredPan, 0.06);
this.center = this.center.add(this.currentPan.multiplyScalar((0.5 * dt) / this.zoomLevel));
this.clampToBounds();
}
}
/**
* Internal screen panning handler
*/
internalUpdateMousePanning(now: number, dt: number): any {
if (!this.root.app.focused) {
return;
}
if (!this.root.app.settings.getAllSettings().enableMousePan) {
// Not enabled
return;
}
const mousePos: any = this.root.app.mousePosition;
if (!mousePos) {
return;
}
if (this.root.hud.shouldPauseGame() || this.root.hud.hasBlockingOverlayOpen()) {
return;
}
if (this.desiredCenter || this.desiredZoom || this.currentlyMoving || this.currentlyPinching) {
// Performing another method of movement right now
return;
}
if (mousePos.x < 0 ||
mousePos.y < 0 ||
mousePos.x > this.root.gameWidth ||
mousePos.y > this.root.gameHeight) {
// Out of screen
return;
}
const panAreaPixels: any = 2;
const panVelocity: any = new Vector();
if (mousePos.x < panAreaPixels) {
panVelocity.x -= 1;
}
if (mousePos.x > this.root.gameWidth - panAreaPixels) {
panVelocity.x += 1;
}
if (mousePos.y < panAreaPixels) {
panVelocity.y -= 1;
}
if (mousePos.y > this.root.gameHeight - panAreaPixels) {
panVelocity.y += 1;
}
this.center = this.center.add(panVelocity.multiplyScalar(((0.5 * dt) / this.zoomLevel) * this.root.app.settings.getMovementSpeed()));
this.clampToBounds();
}
/**
* Updates the non user interaction zooming
*/
internalUpdateZooming(now: number, dt: number): any {
if (!this.currentlyPinching && this.desiredZoom !== null) {
const diff: any = this.zoomLevel - this.desiredZoom;
if (Math.abs(diff) > 0.0001) {
let fade: any = 0.94;
if (diff > 0) {
// Zoom out faster than in
fade = 0.9;
}
assert(Number.isFinite(this.desiredZoom), "Desired zoom is NaN: " + this.desiredZoom);
assert(Number.isFinite(fade), "Zoom fade is NaN: " + fade);
this.zoomLevel = this.zoomLevel * fade + this.desiredZoom * (1 - fade);
assert(Number.isFinite(this.zoomLevel), "Zoom level is NaN after fade: " + this.zoomLevel);
}
else {
this.zoomLevel = this.desiredZoom;
this.desiredZoom = null;
}
}
}
/**
* Updates the non user interaction centering
*/
internalUpdateCentering(now: number, dt: number): any {
if (!this.currentlyMoving && this.desiredCenter !== null) {
const diff: any = this.center.direction(this.desiredCenter);
const length: any = diff.length();
const tolerance: any = 1 / this.zoomLevel;
if (length > tolerance) {
const movement: any = diff.multiplyScalar(Math.min(1, dt * 0.008));
this.center.x += movement.x;
this.center.y += movement.y;
}
else {
this.desiredCenter = null;
}
}
}
/**
* Updates the keyboard forces
*/
internalUpdateKeyboardForce(now: number, dt: number): any {
if (!this.currentlyMoving && this.desiredCenter == null) {
const limitingDimension: any = Math.min(this.root.gameWidth, this.root.gameHeight);
const moveAmount: any = ((limitingDimension / 2048) * dt) / this.zoomLevel;
let forceX: any = 0;
let forceY: any = 0;
const actionMapper: any = this.root.keyMapper;
if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveUp).pressed) {
forceY -= 1;
}
if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveDown).pressed) {
forceY += 1;
}
if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveLeft).pressed) {
forceX -= 1;
}
if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveRight).pressed) {
forceX += 1;
}
let movementSpeed: any = this.root.app.settings.getMovementSpeed() *
(actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveFaster).pressed ? 4 : 1);
this.center.x += moveAmount * forceX * movementSpeed;
this.center.y += moveAmount * forceY * movementSpeed;
this.clampToBounds();
}
}
}

@ -0,0 +1,61 @@
/** @enum {string} */
export const enumColors: any = {
red: "red",
green: "green",
blue: "blue",
yellow: "yellow",
purple: "purple",
cyan: "cyan",
white: "white",
uncolored: "uncolored",
};
const c: any = enumColors;
/** @enum {string} */
export const enumColorToShortcode: any = {
[c.red]: "r",
[c.green]: "g",
[c.blue]: "b",
[c.yellow]: "y",
[c.purple]: "p",
[c.cyan]: "c",
[c.white]: "w",
[c.uncolored]: "u",
};
/** @enum {enumColors} */
export const enumShortcodeToColor: any = {};
for (const key: any in enumColorToShortcode) {
enumShortcodeToColor[enumColorToShortcode[key]] = key;
}
/** @enum {string} */
export const enumColorsToHexCode: any = {
[c.red]: "#ff666a",
[c.green]: "#78ff66",
[c.blue]: "#66a7ff",
// red + green
[c.yellow]: "#fcf52a",
// red + blue
[c.purple]: "#dd66ff",
// blue + green
[c.cyan]: "#00fcff",
// blue + green + red
[c.white]: "#ffffff",
[c.uncolored]: "#aaaaaa",
};
/** @enum {Object.<string, string>} */
export const enumColorMixingResults: any = {};
const bitfieldToColor: any = [
/* 000 */ c.uncolored,
/* 001 */ c.red,
/* 010 */ c.green,
/* 011 */ c.yellow,
/* 100 */ c.blue,
/* 101 */ c.purple,
/* 110 */ c.cyan,
/* 111 */ c.white,
];
for (let i: any = 0; i < 1 << 3; ++i) {
enumColorMixingResults[bitfieldToColor[i]] = {};
for (let j: any = 0; j < 1 << 3; ++j) {
enumColorMixingResults[bitfieldToColor[i]][bitfieldToColor[j]] = bitfieldToColor[i | j];
}
}

@ -0,0 +1,43 @@
import { BasicSerializableObject } from "../savegame/serialization";
export class Component extends BasicSerializableObject {
/**
* Returns the components unique id
* {}
* @abstract
*/
static getId(): string {
abstract;
return "unknown-component";
}
/**
* Should return the schema used for serialization
*/
static getSchema(): any {
return {};
}
/**
* Copy the current state to another component
*/
copyAdditionalStateTo(otherComponent: Component): any { }
/**
* Clears all items and state
*/
clear(): any { }
/* dev:start */
/**
* Fixes typeof DerivedComponent is not assignable to typeof Component, compiled out
* in non-dev builds
*/
constructor(...args) {
super();
}
/**
* Returns a string representing the components data, only in dev builds
* {}
*/
getDebugString(): string {
return null;
}
}

@ -0,0 +1,55 @@
import { gComponentRegistry } from "../core/global_registries";
import { StaticMapEntityComponent } from "./components/static_map_entity";
import { BeltComponent } from "./components/belt";
import { ItemEjectorComponent } from "./components/item_ejector";
import { ItemAcceptorComponent } from "./components/item_acceptor";
import { MinerComponent } from "./components/miner";
import { ItemProcessorComponent } from "./components/item_processor";
import { UndergroundBeltComponent } from "./components/underground_belt";
import { HubComponent } from "./components/hub";
import { StorageComponent } from "./components/storage";
import { WiredPinsComponent } from "./components/wired_pins";
import { BeltUnderlaysComponent } from "./components/belt_underlays";
import { WireComponent } from "./components/wire";
import { ConstantSignalComponent } from "./components/constant_signal";
import { LogicGateComponent } from "./components/logic_gate";
import { LeverComponent } from "./components/lever";
import { WireTunnelComponent } from "./components/wire_tunnel";
import { DisplayComponent } from "./components/display";
import { BeltReaderComponent } from "./components/belt_reader";
import { FilterComponent } from "./components/filter";
import { ItemProducerComponent } from "./components/item_producer";
import { GoalAcceptorComponent } from "./components/goal_acceptor";
export function initComponentRegistry(): any {
const components: any = [
StaticMapEntityComponent,
BeltComponent,
ItemEjectorComponent,
ItemAcceptorComponent,
MinerComponent,
ItemProcessorComponent,
UndergroundBeltComponent,
HubComponent,
StorageComponent,
WiredPinsComponent,
BeltUnderlaysComponent,
WireComponent,
ConstantSignalComponent,
LogicGateComponent,
LeverComponent,
WireTunnelComponent,
DisplayComponent,
BeltReaderComponent,
FilterComponent,
ItemProducerComponent,
GoalAcceptorComponent,
];
components.forEach((component: any): any => gComponentRegistry.register(component));
// IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS
// Sanity check - If this is thrown, you forgot to add a new component here
assert(
// @ts-ignore
require.context("./components", false, /.*\.js/i).keys().length ===
gComponentRegistry.getNumEntries(), "Not all components are registered");
console.log("📦 There are", gComponentRegistry.getNumEntries(), "components");
}

@ -0,0 +1,95 @@
import { enumDirection, Vector } from "../../core/vector";
import { types } from "../../savegame/serialization";
import { BeltPath } from "../belt_path";
import { Component } from "../component";
export const curvedBeltLength: any = 0.78;
export const FAKE_BELT_ACCEPTOR_SLOT: import("./item_acceptor").ItemAcceptorSlot = {
pos: new Vector(0, 0),
direction: enumDirection.bottom,
};
export const FAKE_BELT_EJECTOR_SLOT_BY_DIRECTION: {
[idx: enumDirection]: import("./item_ejector").ItemEjectorSlot;
} = {
[enumDirection.top]: {
pos: new Vector(0, 0),
direction: enumDirection.top,
item: null,
progress: 0,
},
[enumDirection.right]: {
pos: new Vector(0, 0),
direction: enumDirection.right,
item: null,
progress: 0,
},
[enumDirection.left]: {
pos: new Vector(0, 0),
direction: enumDirection.left,
item: null,
progress: 0,
},
};
export class BeltComponent extends Component {
static getId(): any {
return "Belt";
}
public direction = direction;
public assignedPath: BeltPath = null;
constructor({ direction = enumDirection.top }) {
super();
}
clear(): any {
if (this.assignedPath) {
this.assignedPath.clearAllItems();
}
}
/**
* Returns the effective length of this belt in tile space
* {}
*/
getEffectiveLengthTiles(): number {
return this.direction === enumDirection.top ? 1.0 : curvedBeltLength;
}
/**
* Returns fake acceptor slot used for matching
* {}
*/
getFakeAcceptorSlot(): import("./item_acceptor").ItemAcceptorSlot {
return FAKE_BELT_ACCEPTOR_SLOT;
}
/**
* Returns fake acceptor slot used for matching
* {}
*/
getFakeEjectorSlot(): import("./item_ejector").ItemEjectorSlot {
assert(FAKE_BELT_EJECTOR_SLOT_BY_DIRECTION[this.direction], "Invalid belt direction: ", this.direction);
return FAKE_BELT_EJECTOR_SLOT_BY_DIRECTION[this.direction];
}
/**
* Converts from belt space (0 = start of belt ... 1 = end of belt) to the local
* belt coordinates (-0.5|-0.5 to 0.5|0.5)
* {}
*/
transformBeltToLocalSpace(progress: number): Vector {
assert(progress >= 0.0, "Invalid progress ( < 0): " + progress);
switch (this.direction) {
case enumDirection.top:
assert(progress <= 1.02, "Invalid progress: " + progress);
return new Vector(0, 0.5 - progress);
case enumDirection.right: {
assert(progress <= curvedBeltLength + 0.02, "Invalid progress 2: " + progress);
const arcProgress: any = (progress / curvedBeltLength) * 0.5 * Math.PI;
return new Vector(0.5 - 0.5 * Math.cos(arcProgress), 0.5 - 0.5 * Math.sin(arcProgress));
}
case enumDirection.left: {
assert(progress <= curvedBeltLength + 0.02, "Invalid progress 3: " + progress);
const arcProgress: any = (progress / curvedBeltLength) * 0.5 * Math.PI;
return new Vector(-0.5 + 0.5 * Math.cos(arcProgress), 0.5 - 0.5 * Math.sin(arcProgress));
}
default:
assertAlways(false, "Invalid belt direction: " + this.direction);
return new Vector(0, 0);
}
}
}

@ -0,0 +1,42 @@
import { Component } from "../component";
import { BaseItem } from "../base_item";
import { typeItemSingleton } from "../item_resolver";
import { types } from "../../savegame/serialization";
/** @enum {string} */
export const enumBeltReaderType: any = {
wired: "wired",
wireless: "wireless",
};
export class BeltReaderComponent extends Component {
static getId(): any {
return "BeltReader";
}
static getSchema(): any {
return {
lastItem: types.nullable(typeItemSingleton),
};
}
constructor() {
super();
this.clear();
}
clear(): any {
/**
* Which items went through the reader, we only store the time
*/
this.lastItemTimes = [];
/**
* Which item passed the reader last
*/
this.lastItem = null;
/**
* Stores the last throughput we computed
*/
this.lastThroughput = 0;
/**
* Stores when we last computed the throughput
*/
this.lastThroughputComputation = 0;
}
}

@ -0,0 +1,33 @@
import { enumDirection, Vector } from "../../core/vector";
import { Component } from "../component";
/**
* Store which type an underlay is, this is cached so we can easily
* render it.
*
* Full: Render underlay at top and bottom of tile
* Bottom Only: Only render underlay at the bottom half
* Top Only:
* @enum {string}
*/
export const enumClippedBeltUnderlayType: any = {
full: "full",
bottomOnly: "bottomOnly",
topOnly: "topOnly",
none: "none",
};
export type BeltUnderlayTile = {
pos: Vector;
direction: enumDirection;
cachedType?: enumClippedBeltUnderlayType;
};
export class BeltUnderlaysComponent extends Component {
static getId(): any {
return "BeltUnderlays";
}
public underlays = underlays;
constructor({ underlays = [] }) {
super();
}
}

@ -0,0 +1,25 @@
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { Component } from "../component";
import { typeItemSingleton } from "../item_resolver";
export class ConstantSignalComponent extends Component {
static getId(): any {
return "ConstantSignal";
}
static getSchema(): any {
return {
signal: types.nullable(typeItemSingleton),
};
}
/**
* Copy the current state to another component
*/
copyAdditionalStateTo(otherComponent: ConstantSignalComponent): any {
otherComponent.signal = this.signal;
}
public signal = signal;
constructor({ signal = null }) {
super();
}
}

@ -0,0 +1,6 @@
import { Component } from "../component";
export class DisplayComponent extends Component {
static getId(): any {
return "Display";
}
}

@ -0,0 +1,44 @@
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { Component } from "../component";
import { typeItemSingleton } from "../item_resolver";
export type PendingFilterItem = {
item: BaseItem;
progress: number;
};
export class FilterComponent extends Component {
static getId(): any {
return "Filter";
}
duplicateWithoutContents(): any {
return new FilterComponent();
}
static getSchema(): any {
return {
pendingItemsToLeaveThrough: types.array(types.structured({
item: typeItemSingleton,
progress: types.ufloat,
})),
pendingItemsToReject: types.array(types.structured({
item: typeItemSingleton,
progress: types.ufloat,
})),
};
}
constructor() {
super();
this.clear();
}
clear(): any {
/**
* Items in queue to leave through
*/
this.pendingItemsToLeaveThrough = [];
/**
* Items in queue to reject
*/
this.pendingItemsToReject = [];
}
}

@ -0,0 +1,47 @@
import { globalConfig } from "../../core/config";
import { BaseItem } from "../base_item";
import { Component } from "../component";
import { typeItemSingleton } from "../item_resolver";
export class GoalAcceptorComponent extends Component {
static getId(): any {
return "GoalAcceptor";
}
static getSchema(): any {
return {
item: typeItemSingleton,
};
}
public item: BaseItem | undefined = item;
constructor({ item = null, rate = null }) {
super();
this.clear();
}
clear(): any {
/**
* The last item we delivered
*/
this.lastDelivery = null;
// The amount of items we delivered so far
this.currentDeliveredItems = 0;
// Used for animations
this.displayPercentage = 0;
}
/**
* Clears items but doesn't instantly reset the progress bar
*/
clearItems(): any {
this.lastDelivery = null;
this.currentDeliveredItems = 0;
}
getRequiredSecondsPerItem(): any {
return (globalConfig.goalAcceptorsPerProducer /
(globalConfig.puzzleModeSpeed * globalConfig.beltSpeedItemsPerSecond));
}
/**
* Copy the current state to another component
*/
copyAdditionalStateTo(otherComponent: GoalAcceptorComponent): any {
otherComponent.item = this.item;
}
}

@ -0,0 +1,6 @@
import { Component } from "../component";
export class HubComponent extends Component {
static getId(): any {
return "Hub";
}
}

@ -0,0 +1,96 @@
import { enumDirection, enumInvertedDirections, Vector } from "../../core/vector";
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { Component } from "../component";
export type ItemAcceptorSlot = {
pos: Vector;
direction: enumDirection;
filter?: ItemType;
};
export type ItemAcceptorLocatedSlot = {
slot: ItemAcceptorSlot;
index: number;
};
export type ItemAcceptorSlotConfig = {
pos: Vector;
direction: enumDirection;
filter?: ItemType;
};
export class ItemAcceptorComponent extends Component {
static getId(): any {
return "ItemAcceptor";
}
constructor({ slots = [] }) {
super();
this.setSlots(slots);
this.clear();
}
clear(): any {
/**
* Fixes belt animations
*/
this.itemConsumptionAnimations = [];
}
setSlots(slots: Array<ItemAcceptorSlotConfig>): any {
this.slots = [];
for (let i: any = 0; i < slots.length; ++i) {
const slot: any = slots[i];
this.slots.push({
pos: slot.pos,
direction: slot.direction,
// Which type of item to accept (shape | color | all) @see ItemType
filter: slot.filter,
});
}
}
/**
* Returns if this acceptor can accept a new item at slot N
*
* NOTICE: The belt path ignores this for performance reasons and does his own check
*/
canAcceptItem(slotIndex: number, item: BaseItem=): any {
const slot: any = this.slots[slotIndex];
return !slot.filter || slot.filter === item.getItemType();
}
/**
* Called when an item has been accepted so that
*/
onItemAccepted(slotIndex: number, direction: enumDirection, item: BaseItem, remainingProgress: number = 0.0): any {
this.itemConsumptionAnimations.push({
item,
slotIndex,
direction,
animProgress: Math.min(1, remainingProgress * 2),
});
}
/**
* Tries to find a slot which accepts the current item
* {}
*/
findMatchingSlot(targetLocalTile: Vector, fromLocalDirection: enumDirection): ItemAcceptorLocatedSlot | null {
// We need to invert our direction since the acceptor specifies *from* which direction
// it accepts items, but the ejector specifies *into* which direction it ejects items.
// E.g.: Ejector ejects into "right" direction but acceptor accepts from "left" direction.
const desiredDirection: any = enumInvertedDirections[fromLocalDirection];
// Go over all slots and try to find a target slot
for (let slotIndex: any = 0; slotIndex < this.slots.length; ++slotIndex) {
const slot: any = this.slots[slotIndex];
// Make sure the acceptor slot is on the right position
if (!slot.pos.equals(targetLocalTile)) {
continue;
}
// Check if the acceptor slot accepts items from our direction
if (desiredDirection === slot.direction) {
return {
slot,
index: slotIndex,
};
}
}
return null;
}
}

@ -0,0 +1,126 @@
import { enumDirection, enumDirectionToVector, Vector } from "../../core/vector";
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { BeltPath } from "../belt_path";
import { Component } from "../component";
import { Entity } from "../entity";
import { typeItemSingleton } from "../item_resolver";
export type ItemEjectorSlot = {
pos: Vector;
direction: enumDirection;
item: BaseItem;
lastItem: BaseItem;
progress: ?number;
cachedDestSlot?: import("./item_acceptor").ItemAcceptorLocatedSlot;
cachedBeltPath?: BeltPath;
cachedTargetEntity?: Entity;
};
export class ItemEjectorComponent extends Component {
static getId(): any {
return "ItemEjector";
}
static getSchema(): any {
// The cachedDestSlot, cachedTargetEntity fields are not serialized.
return {
slots: types.fixedSizeArray(types.structured({
item: types.nullable(typeItemSingleton),
progress: types.float,
})),
};
}
public renderFloatingItems = renderFloatingItems;
constructor({ slots = [], renderFloatingItems = true }) {
super();
this.setSlots(slots);
}
clear(): any {
for (const slot: any of this.slots) {
slot.item = null;
slot.lastItem = null;
slot.progress = 0;
}
}
setSlots(slots: Array<{
pos: Vector;
direction: enumDirection;
}>): any {
this.slots = [];
for (let i: any = 0; i < slots.length; ++i) {
const slot: any = slots[i];
this.slots.push({
pos: slot.pos,
direction: slot.direction,
item: null,
lastItem: null,
progress: 0,
cachedDestSlot: null,
cachedTargetEntity: null,
});
}
}
/**
* Returns where this slot ejects to
* {}
*/
getSlotTargetLocalTile(slot: ItemEjectorSlot): Vector {
const directionVector: any = enumDirectionToVector[slot.direction];
return slot.pos.add(directionVector);
}
/**
* Returns whether any slot ejects to the given local tile
*/
anySlotEjectsToLocalTile(tile: Vector): any {
for (let i: any = 0; i < this.slots.length; ++i) {
if (this.getSlotTargetLocalTile(this.slots[i]).equals(tile)) {
return true;
}
}
return false;
}
/**
* Returns if we can eject on a given slot
* {}
*/
canEjectOnSlot(slotIndex: number): boolean {
assert(slotIndex >= 0 && slotIndex < this.slots.length, "Invalid ejector slot: " + slotIndex);
return !this.slots[slotIndex].item;
}
/**
* Returns the first free slot on this ejector or null if there is none
* {}
*/
getFirstFreeSlot(): ?number {
for (let i: any = 0; i < this.slots.length; ++i) {
if (this.canEjectOnSlot(i)) {
return i;
}
}
return null;
}
/**
* Tries to eject a given item
* {}
*/
tryEject(slotIndex: number, item: BaseItem): boolean {
if (!this.canEjectOnSlot(slotIndex)) {
return false;
}
this.slots[slotIndex].item = item;
this.slots[slotIndex].lastItem = item;
this.slots[slotIndex].progress = 0;
return true;
}
/**
* Clears the given slot and returns the item it had
* {}
*/
takeSlotItem(slotIndex: number): BaseItem | null {
const slot: any = this.slots[slotIndex];
const item: any = slot.item;
slot.item = null;
slot.progress = 0.0;
return item;
}
}

@ -0,0 +1,98 @@
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { Component } from "../component";
/** @enum {string} */
export const enumItemProcessorTypes: any = {
balancer: "balancer",
cutter: "cutter",
cutterQuad: "cutterQuad",
rotater: "rotater",
rotaterCCW: "rotaterCCW",
rotater180: "rotater180",
stacker: "stacker",
trash: "trash",
mixer: "mixer",
painter: "painter",
painterDouble: "painterDouble",
painterQuad: "painterQuad",
hub: "hub",
filter: "filter",
reader: "reader",
goal: "goal",
};
/** @enum {string} */
export const enumItemProcessorRequirements: any = {
painterQuad: "painterQuad",
};
export type EjectorItemToEject = {
item: BaseItem;
requiredSlot?: number;
preferredSlot?: number;
};
export type EjectorCharge = {
remainingTime: number;
items: Array<EjectorItemToEject>;
};
export class ItemProcessorComponent extends Component {
static getId(): any {
return "ItemProcessor";
}
static getSchema(): any {
return {
nextOutputSlot: types.uint,
};
}
public inputsPerCharge = inputsPerCharge;
public type = processorType;
public processingRequirement = processingRequirement;
public inputSlots: Map<number, BaseItem> = new Map();
constructor({ processorType = enumItemProcessorTypes.balancer, processingRequirement = null, inputsPerCharge = 1, }) {
super();
this.clear();
}
clear(): any {
// Which slot to emit next, this is only a preference and if it can't emit
// it will take the other one. Some machines ignore this (e.g. the balancer) to make
// sure the outputs always match
this.nextOutputSlot = 0;
this.inputSlots.clear();
/**
* Current input count
*/
this.inputCount = 0;
/**
* What we are currently processing, empty if we don't produce anything rn
* requiredSlot: Item *must* be ejected on this slot
* preferredSlot: Item *can* be ejected on this slot, but others are fine too if the one is not usable
*/
this.ongoingCharges = [];
/**
* How much processing time we have left from the last tick
*/
this.bonusTime = 0;
this.queuedEjects = [];
}
/**
* Tries to take the item
*/
tryTakeItem(item: BaseItem, sourceSlot: number): any {
if (this.type === enumItemProcessorTypes.hub ||
this.type === enumItemProcessorTypes.trash ||
this.type === enumItemProcessorTypes.goal) {
// Hub has special logic .. not really nice but efficient.
this.inputSlots.set(this.inputCount, item);
this.inputCount++;
return true;
}
// Check that we only take one item per slot
if (this.inputSlots.has(sourceSlot)) {
return false;
}
this.inputSlots.set(sourceSlot, item);
this.inputCount++;
return true;
}
}

@ -0,0 +1,6 @@
import { Component } from "../component";
export class ItemProducerComponent extends Component {
static getId(): any {
return "ItemProducer";
}
}

@ -0,0 +1,23 @@
import { Component } from "../component";
import { types } from "../../savegame/serialization";
export class LeverComponent extends Component {
static getId(): any {
return "Lever";
}
static getSchema(): any {
return {
toggled: types.bool,
};
}
/**
* Copy the current state to another component
*/
copyAdditionalStateTo(otherComponent: LeverComponent): any {
otherComponent.toggled = this.toggled;
}
public toggled = toggled;
constructor({ toggled = false }) {
super();
}
}

@ -0,0 +1,26 @@
import { Component } from "../component";
/** @enum {string} */
export const enumLogicGateType: any = {
and: "and",
not: "not",
xor: "xor",
or: "or",
transistor: "transistor",
analyzer: "analyzer",
rotater: "rotater",
unstacker: "unstacker",
cutter: "cutter",
compare: "compare",
stacker: "stacker",
painter: "painter",
};
export class LogicGateComponent extends Component {
static getId(): any {
return "LogicGate";
}
public type = type;
constructor({ type = enumLogicGateType.and }) {
super();
}
}

@ -0,0 +1,42 @@
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { Component } from "../component";
import { Entity } from "../entity";
import { typeItemSingleton } from "../item_resolver";
const chainBufferSize: any = 6;
export class MinerComponent extends Component {
static getId(): any {
return "Miner";
}
static getSchema(): any {
// cachedMinedItem is not serialized.
return {
lastMiningTime: types.ufloat,
itemChainBuffer: types.array(typeItemSingleton),
};
}
public lastMiningTime = 0;
public chainable = chainable;
public cachedMinedItem: BaseItem = null;
public cachedChainedMiner: Entity | null | false = null;
constructor({ chainable = false }) {
super();
this.clear();
}
clear(): any {
/**
* Stores items from other miners which were chained to this
* miner.
*/
this.itemChainBuffer = [];
}
tryAcceptChainedItem(item: BaseItem): any {
if (this.itemChainBuffer.length > chainBufferSize) {
// Well, this one is full
return false;
}
this.itemChainBuffer.push(item);
return true;
}
}

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

Loading…
Cancel
Save