import { CacheableFunction } from "@/typings/cacheableFunction"; import { Achievement } from "@/typings/features/achievement"; import { Board } from "@/typings/features/board"; import { Buyable } from "@/typings/features/buyable"; import { Challenge } from "@/typings/features/challenge"; import { Clickable } from "@/typings/features/clickable"; import { Feature, Features, GridFeatures, RawFeature, RawFeatures, RawGridFeatures } from "@/typings/features/feature"; import { Grid } from "@/typings/features/grid"; import { Hotkey } from "@/typings/features/hotkey"; import { Milestone } from "@/typings/features/milestone"; import { Microtab, Subtab } from "@/typings/features/subtab"; import { Upgrade } from "@/typings/features/upgrade"; import { Layer, RawLayer } from "@/typings/layer"; import { PlayerData } from "@/typings/player"; import { State } from "@/typings/state"; import Decimal, { DecimalSource } from "@/util/bignum"; import { isFunction } from "@/util/common"; import { defaultLayerProperties, getStartingBoards, getStartingBuyables, getStartingChallenges, getStartingClickables, noCache } from "@/util/layers"; import { createGridProxy, createLayerProxy } from "@/util/proxies"; import { applyPlayerData } from "@/util/save"; import clone from "lodash.clonedeep"; import { isRef } from "vue"; import { ProgressDisplay } from "./enums"; import { default as playerProxy } from "./player"; export const layers: Record> = {}; export const hotkeys: Hotkey[] = []; window.layers = layers; export function addLayer(layer: RawLayer, player?: Partial): void { player = player || playerProxy; // Check for required properties if (!("id" in layer)) { console.error(`Cannot add layer without a "id" property!`, layer); return; } if (layer.type === "static" || layer.type === "normal") { const missingProperty = ["baseAmount", "requires"].find(prop => !(prop in layer)); if (missingProperty) { console.error(`Cannot add layer without a "${missingProperty}" property!`, layer); return; } } // Clone object to prevent modifying the original layer = clone(layer); setDefault(player, "layers", {}); player.layers[layer.id] = applyPlayerData( { points: new Decimal(0), unlocked: false, resetTime: new Decimal(0), upgrades: [], achievements: [], milestones: [], infoboxes: {}, buyables: getStartingBuyables(layer.buyables?.data), clickables: getStartingClickables(layer.clickables?.data), challenges: getStartingChallenges(layer.challenges?.data), boards: player.layers[layer.id]?.boards || getStartingBoards(layer.boards?.data), grids: {}, confirmRespecBuyables: false, ...(layer.startData?.() || {}) }, player.layers[layer.id] ); // Set default property values layer = Object.assign({}, defaultLayerProperties, layer); layer.layer = layer.id; if (layer.type === "static" && layer.base == undefined) { layer.base = 2; } // Process each feature const uncachedProperties = ["startData", "click", "update", "reset", "hardReset"]; for (const property of uncachedProperties) { if (layer[property] && !isRef(layer.property) && isFunction(layer[property])) { (layer[property] as CacheableFunction).forceCached = false; } } if (layer.upgrades) { setupFeatures, Upgrade>(layer.id, layer.upgrades); setRowCol(layer.upgrades); for (const id in layer.upgrades.data) { layer.upgrades.data[id].bought = function() { return ( !layers[this.layer].deactivated && playerProxy.layers[this.layer].upgrades.some( (upgrade: string | number) => upgrade == id ) ); }; setDefault(layer.upgrades.data[id], "canAfford", function() { if (this.currencyInternalName) { const name = this.currencyInternalName; if (this.currencyLocation) { return !Decimal.lt(this.currencyLocation[name], this.cost); } else if (this.currencyLayer) { return !Decimal.lt( playerProxy.layers[this.currencyLayer][name] as DecimalSource, this.cost ); } else { return !Decimal.lt(playerProxy[name] as DecimalSource, this.cost); } } else { return !playerProxy.layers[this.layer].points.lt(this.cost); } }); setDefault( layer.upgrades.data[id], "pay", function() { if (this.bought || !this.canAfford) { return; } if (this.currencyInternalName) { const name = this.currencyInternalName; if (this.currencyLocation) { if (Decimal.lt(this.currencyLocation[name], this.cost)) { return; } this.currencyLocation[name] = Decimal.sub( this.currencyLocation[name], this.cost ); } else if (this.currencyLayer) { const lr = this.currencyLayer; if ( Decimal.lt(playerProxy.layers[lr][name] as DecimalSource, this.cost) ) { return; } playerProxy.layers[lr][name] = Decimal.sub( playerProxy.layers[lr][name] as DecimalSource, this.cost ); } else { if (Decimal.lt(playerProxy[name] as DecimalSource, this.cost)) { return; } playerProxy[name] = Decimal.sub( playerProxy[name] as DecimalSource, this.cost ); } } else { if (playerProxy.layers[this.layer].points.lt(this.cost)) { return; } playerProxy.layers[this.layer].points = playerProxy.layers[ this.layer ].points.sub(this.cost); } }, false ); setDefault( layer.upgrades.data[id], "buy", function() { if (this.bought || !this.canAfford) { return; } this.pay(); playerProxy.layers[this.layer].upgrades.push(this.id); this.onPurchase?.(); }, false ); setDefault(layer.upgrades.data[id], "onPurchase", undefined, false); } } if (layer.achievements) { setupFeatures, Achievement>( layer.id, layer.achievements ); setRowCol(layer.achievements); for (const id in layer.achievements.data) { layer.achievements.data[id].earned = function() { return ( !layers[this.layer].deactivated && playerProxy.layers[this.layer].achievements.some( (achievement: string | number) => achievement == id ) ); }; setDefault(layer.achievements.data[id], "onComplete", undefined, false); } } if (layer.challenges) { setupFeatures, Challenge>(layer.id, layer.challenges); setRowCol(layer.challenges); layer.activeChallenge = function() { return Object.values(this.challenges!.data).find( (challenge: Challenge) => challenge.active ); }; for (const id in layer.challenges.data) { layer.challenges.data[id].shown = function() { return ( this.unlocked !== false && (playerProxy.hideChallenges === false || !this.maxed) ); }; layer.challenges.data[id].completed = function() { return ( !layers[this.layer].deactivated && playerProxy.layers[this.layer].challenges[id]?.gt(0) ); }; layer.challenges.data[id].completions = function() { return playerProxy.layers[this.layer].challenges[id]; }; layer.challenges.data[id].maxed = function() { return ( !layers[this.layer].deactivated && Decimal.gte(playerProxy.layers[this.layer].challenges[id], this.completionLimit) ); }; layer.challenges.data[id].active = function() { return ( !layers[this.layer].deactivated && playerProxy.layers[this.layer].activeChallenge === id ); }; layer.challenges.data[id].toggle = noCache(function(this: Challenge) { const exiting = playerProxy.layers[this.layer].activeChallenge === id; if (exiting) { if (this.canComplete && !this.maxed) { let completions: boolean | DecimalSource = this.canComplete; if (completions === true) { completions = 1; } playerProxy.layers[this.layer].challenges[id] = Decimal.min( playerProxy.layers[this.layer].challenges[id].add(completions), this.completionLimit ); this.onComplete?.(); } playerProxy.layers[this.layer].activeChallenge = null; this.onExit?.(); layers[this.layer].reset(true); } else if (!exiting && this.canStart) { layers[this.layer].reset(true); playerProxy.layers[this.layer].activeChallenge = id; this.onEnter?.(); } }); setDefault(layer.challenges.data[id], "onComplete", undefined, false); setDefault(layer.challenges.data[id], "onEnter", undefined, false); setDefault(layer.challenges.data[id], "onExit", undefined, false); setDefault(layer.challenges.data[id], "canStart", true); setDefault(layer.challenges.data[id], "completionLimit", new Decimal(1)); setDefault(layer.challenges.data[id], "mark", function() { return Decimal.gt(this.completionLimit, 1) && this.maxed; }); setDefault(layer.challenges.data[id], "canComplete", function() { if (!this.active) { return false; } if (this.currencyInternalName) { const name = this.currencyInternalName; if (this.currencyLocation) { return !Decimal.lt(this.currencyLocation[name], this.goal); } else if (this.currencyLayer) { const lr = this.currencyLayer; return !Decimal.lt( playerProxy.layers[lr][name] as DecimalSource, this.goal ); } else { return !Decimal.lt(playerProxy[name] as DecimalSource, this.goal); } } else { return !playerProxy.points.lt(this.goal); } }); } } if (layer.buyables) { setupFeatures, Buyable>(layer.id, layer.buyables); setRowCol(layer.buyables); setDefault(layer.buyables, "respec", undefined, false); setDefault( layer.buyables, "reset", function(this: NonNullable) { playerProxy.layers[this.layer].buyables = getStartingBuyables(layer.buyables?.data); }, false ); for (const id in layer.buyables.data) { layer.buyables.data[id].amount = function() { return playerProxy.layers[this.layer].buyables[id]; }; layer.buyables.data[id].amountSet = function(amount: Decimal) { playerProxy.layers[this.layer].buyables[id] = amount; }; layer.buyables.data[id].canBuy = function() { return ( !layers[this.layer].deactivated && this.unlocked !== false && this.canAfford !== false && Decimal.lt(playerProxy.layers[this.layer].buyables[id], this.purchaseLimit) ); }; setDefault(layer.buyables.data[id], "purchaseLimit", new Decimal(Infinity)); setDefault(layer.buyables.data[id], "sellOne", undefined, false); setDefault(layer.buyables.data[id], "sellAll", undefined, false); if (layer.buyables.data[id].cost != undefined) { setDefault( layer.buyables.data[id], "buy", function() { if (this.canBuy) { playerProxy.layers[this.layer].points = playerProxy.layers[ this.layer ].points.sub(this.cost!); this.amount = this.amount.add(1); } }, false ); } } } if (layer.clickables) { setupFeatures, Clickable>(layer.id, layer.clickables); setRowCol(layer.clickables); setDefault(layer.clickables, "masterButtonClick", undefined, false); if (layer.clickables.masterButtonDisplay != undefined) { setDefault(layer.clickables, "showMasterButton", true); } for (const id in layer.clickables.data) { layer.clickables.data[id].state = function() { return playerProxy.layers[this.layer].clickables[id]; }; layer.clickables.data[id].stateSet = function(state: State) { playerProxy.layers[this.layer].clickables[id] = state; }; setDefault(layer.clickables.data[id], "canClick", true); setDefault(layer.clickables.data[id], "click", undefined, false); setDefault(layer.clickables.data[id], "hold", undefined, false); } } if (layer.milestones) { setupFeatures, Milestone>(layer.id, layer.milestones); for (const id in layer.milestones.data) { layer.milestones.data[id].earned = function() { return ( !layer.deactivated && playerProxy.layers[this.layer].milestones.some( (milestone: string | number) => milestone == id ) ); }; layer.milestones.data[id].shown = function() { if (!this.unlocked) { return false; } switch (playerProxy.msDisplay) { default: case "all": return true; case "last": return ( this.optionsDisplay || !this.earned || playerProxy.layers[this.layer].milestones[ playerProxy.layers[this.layer].milestones.length - 1 ] === this.id ); case "configurable": return this.optionsDisplay || !this.earned; case "incomplete": return !this.earned; case "none": return false; } }; setDefault(layer.milestones.data[id], "done", false); } } if (layer.grids) { setupFeatures, Grid>(layer.id, layer.grids); for (const id in layer.grids.data) { setDefault(player.layers[layer.id].grids, id, {}); layer.grids.data[id].getData = function(cell): State { if (playerProxy.layers[this.layer].grids[id][cell] != undefined) { return playerProxy.layers[this.layer].grids[id][cell]; } if (isFunction(this.getStartData)) { return (this.getStartData as (this: Grid, cell: string | number) => State)( cell ); } return this.getStartData; }; layer.grids.data[id].setData = function(cell, data) { playerProxy.layers[this.layer].grids[id][cell] = data; }; setDefault(layer.grids.data[id], "getUnlocked", true, false); setDefault(layer.grids.data[id], "getCanClick", true, false); setDefault(layer.grids.data[id], "getStartData", "", false); setDefault(layer.grids.data[id], "getStyle", undefined, false); setDefault(layer.grids.data[id], "click", undefined, false); setDefault(layer.grids.data[id], "hold", undefined, false); setDefault(layer.grids.data[id], "getTitle", undefined, false); layer.grids.data[id] = createGridProxy(layer.grids.data[id]) as Grid; } } if (layer.boards) { setupFeatures, Board>(layer.id, layer.boards); for (const id in layer.boards.data) { setDefault(layer.boards.data[id], "width", "100%"); setDefault(layer.boards.data[id], "height", "400px"); for (const nodeType in layer.boards.data[id].types) { layer.boards.data[id].types[nodeType].layer = layer.id; layer.boards.data[id].types[nodeType].id = id; layer.boards.data[id].types[nodeType].type = nodeType; setDefault(layer.boards.data[id].types[nodeType], "size", 50); setDefault(layer.boards.data[id].types[nodeType], "draggable", false); setDefault(layer.boards.data[id].types[nodeType], "canAccept", false); setDefault( layer.boards.data[id].types[nodeType], "progressDisplay", ProgressDisplay.Fill ); setDefault(layer.boards.data[id].types[nodeType], "nodes", function() { return playerProxy.layers[this.layer].boards[this.id].filter( node => node.type === this.type ); }); } } } if (layer.subtabs) { layer.activeSubtab = function() { if ( layers[this.layer].subtabs![playerProxy.subtabs[this.layer].mainTabs!] && layers[this.layer].subtabs![playerProxy.subtabs[this.layer].mainTabs!].unlocked !== false ) { return layers[this.layer].subtabs![playerProxy.subtabs[this.layer].mainTabs!]; } // Default to first unlocked tab return Object.values(layers[this.layer].subtabs!).find( (subtab: Subtab) => subtab.unlocked !== false ); }; setDefault(player, "subtabs", {}); setDefault(player.subtabs, layer.id, {}); setDefault(player.subtabs[layer.id], "mainTabs", Object.keys(layer.subtabs)[0]); for (const id in layer.subtabs) { layer.subtabs[id].active = function() { return playerProxy.subtabs[this.layer].mainTabs === this.id; }; } } if (layer.microtabs) { setDefault(player, "subtabs", {}); setDefault(player.subtabs, layer.id, {}); for (const family in layer.microtabs) { if (Object.keys(layer.microtabs[family]).length === 0) { console.warn( "Cannot create microtab family with 0 tabs", layer.id, family, layer.microtabs[family] ); continue; } layer.microtabs[family].activeMicrotab = function() { if ( this.data[playerProxy.subtabs[this.layer as string][family]] && this.data[playerProxy.subtabs[this.layer as string][family]].unlocked !== false ) { return this[playerProxy.subtabs[this.layer as string][family]]; } // Default to first unlocked tab const firstUnlocked: string | undefined = Object.keys(this).find( microtab => microtab !== "activeMicrotab" && this.data[microtab].unlocked !== false ); return firstUnlocked != undefined ? this[firstUnlocked] : undefined; }; setDefault( player.subtabs[layer.id], family, Object.keys(layer.microtabs[family]).find(tab => tab !== "activeMicrotab")! ); layer.microtabs[family].layer = layer.id; layer.microtabs[family].family = family; for (const id in layer.microtabs[family].data) { const microtab: RawFeature = layer.microtabs[family].data[id]; microtab.layer = layer.id; microtab.family = family; microtab.id = id; microtab.active = function() { return playerProxy.subtabs[this.layer][this.family] === this.id; }; } } } if (layer.hotkeys) { for (const id in layer.hotkeys) { setDefault(layer.hotkeys[id], "press", undefined, false); setDefault(layer.hotkeys[id], "unlocked", function() { return layers[this.layer].unlocked; }); } } // Create layer proxy layer = createLayerProxy(layer) as Layer; // Register layer layers[layer.id] = layer as Layer; // Register hotkeys if (layers[layer.id].hotkeys) { for (const hotkey of layers[layer.id].hotkeys!) { hotkeys.push(hotkey); } } } export function removeLayer(layer: string): void { // Un-set hotkeys if (layers[layer].hotkeys) { for (const hotkey of Object.values(layers[layer].hotkeys!)) { const index = hotkeys.indexOf(hotkey); if (index >= 0) { hotkeys.splice(index, 1); } } } delete layers[layer]; } export function reloadLayer(layer: Layer): void { removeLayer(layer.id); // Re-create layer addLayer(layer); } function setRowCol, S extends Feature>(features: RawGridFeatures) { if (features.rows && features.cols) { return; } let maxRow = 0; let maxCol = 0; for (const id in features) { const index = Number(id); if (!isNaN(index)) { if (Math.floor(index / 10) > maxRow) { maxRow = Math.floor(index / 10); } if (index % 10 > maxCol) { maxCol = index % 10; } } } features.rows = maxRow; features.cols = maxCol; } function setupFeatures< T extends RawFeatures, S extends Feature, R extends Features = Features >(layer: string, features: T) { features.layer = layer; for (const id in features.data) { const feature = features.data[id]; (feature as S).id = id; (feature as S).layer = layer; if ((feature as S).unlocked == undefined) { (feature as S).unlocked = true; } } } function setDefault( object: T, key: K, value: T[K], forceCached?: boolean ): asserts object is Exclude & Required> { if (object[key] === undefined && value != undefined) { object[key] = value; } if (object[key] != undefined && isFunction(object[key]) && forceCached != undefined) { Object.assign(object[key], { forceCached }); } }