const MutationTimeFeePerMinute = 3; const PlantKeysById = new Map(); const PlantsById = new Map(); const MutationsById = new Map(); let IndexedPlants = null; let IndexedMutations = null; function EnsureEconomyIndexes() { if ( IndexedPlants === Plants && IndexedMutations === MutationSets ) { return; } PlantKeysById.clear(); PlantsById.clear(); MutationsById.clear(); for ( const [PlantKey, Plant] of Object.entries(Plants) ) { if ( PlantsById.has( Plant.Id ) ) { throw new Error( "Duplicate plant ID: " + Plant.Id ); } PlantKeysById.set( Plant.Id, PlantKey ); PlantsById.set( Plant.Id, Plant ); } for ( const Mutation of Object.values( MutationSets ) ) { if ( MutationsById.has( Mutation.Id ) ) { throw new Error( "Duplicate mutation ID: " + Mutation.Id ); } MutationsById.set( Mutation.Id, Mutation ); } IndexedPlants = Plants; IndexedMutations = MutationSets; } function GetPlantKeyById( PlantId ) { EnsureEconomyIndexes(); return PlantKeysById.get( Number(PlantId) ) ?? null; } function GetPlantById( PlantId ) { EnsureEconomyIndexes(); return PlantsById.get( Number(PlantId) ) ?? null; } function GetMutationById( MutationId ) { EnsureEconomyIndexes(); return MutationsById.get( Number(MutationId) ) ?? null; } function HasDiscoveredPlant( SaveData, PlantId ) { return SaveData.Discoveries.Plants.includes( Number(PlantId) ); } function HasDiscoveredMutation( SaveData, MutationId ) { return SaveData.Discoveries.Mutations.includes( Number(MutationId) ); } function DiscoverPlant( SaveData, PlantId ) { PlantId = Number(PlantId); if ( !HasDiscoveredPlant( SaveData, PlantId ) ) { SaveData.Discoveries.Plants.push( PlantId ); } } function DiscoverMutation( SaveData, MutationId ) { MutationId = Number( MutationId ); if ( !HasDiscoveredMutation( SaveData, MutationId ) ) { SaveData.Discoveries.Mutations.push( MutationId ); } } function IsPlantAvailableInShop( SaveData, Plant ) { if (Plant.Archived === true) { return false; } if ( Plant.Shop?.ShopPlant === true ) { return true; } return HasDiscoveredPlant( SaveData, Plant.Id ); } function GetSeedCount( SaveData, PlantId ) { return Number( SaveData.Inventory.Seeds[ String(PlantId) ] ?? 0 ); } function AddSeed( SaveData, PlantId, Amount = 1 ) { const InventoryKey = String(PlantId); SaveData.Inventory.Seeds[ InventoryKey ] ??= 0; SaveData.Inventory.Seeds[ InventoryKey ] += Amount; } function TakeSeed( SaveData, PlantId, Amount = 1 ) { const CurrentAmount = GetSeedCount( SaveData, PlantId ); if (CurrentAmount < Amount) { return false; } SaveData.Inventory.Seeds[ String(PlantId) ] = CurrentAmount - Amount; return true; } function GetPlantShopCost( SaveData, PlantId, Cache = new Map(), Visiting = new Set() ) { PlantId = Number(PlantId); if (Cache.has(PlantId)) { return Cache.get(PlantId); } const Plant = GetPlantById( PlantId ); if (Plant === null) { return null; } if ( Plant.Shop?.ShopPlant === true ) { if ( Plant.Shop.BaseCost === null || Plant.Shop.BaseCost === undefined || Plant.Shop.BaseCost === "" ) { return null; } const BaseCost = Number( Plant.Shop.BaseCost ); if ( !Number.isFinite(BaseCost) || BaseCost < 0 ) { return null; } const Cost = Math.ceil(BaseCost); Cache.set( PlantId, Cost ); return Cost; } if ( !HasDiscoveredPlant( SaveData, PlantId ) ) { return null; } if (Visiting.has(PlantId)) { return null; } Visiting.add( PlantId ); let CheapestCost = null; for ( const Mutation of Object.values( MutationSets ) ) { if (Mutation.Archived === true) { continue; } if ( !HasDiscoveredMutation( SaveData, Mutation.Id ) ) { continue; } if ( !Mutation.Relations ?.PlantsCreated ?.includes( PlantId ) ) { continue; } const IngredientCost = GetMutationIngredientCost( SaveData, Mutation, Cache, Visiting ); if (IngredientCost === null) { continue; } const MutationCost = IngredientCost + GetMutationTimeFee( Mutation ); if ( CheapestCost === null || MutationCost < CheapestCost ) { CheapestCost = MutationCost; } } Visiting.delete( PlantId ); Cache.set( PlantId, CheapestCost ); return CheapestCost; } function GetMutationIngredientCost( SaveData, Mutation, Cache, Visiting ) { let TotalCost = 0; const ListReferenceCounts = GetEconomyMutationListReferenceCounts( Mutation.Pattern ); for ( const Row of Mutation.Pattern ?? [] ) { for ( const Matcher of Row ) { if ( Matcher === null || Matcher === "Empty" || Matcher === "Any" || GetEconomyMutationListReference( Matcher ) !== null ) { continue; } const MatcherCost = GetMatcherSeedCost( SaveData, Matcher, Cache, Visiting ); if (MatcherCost === null) { return null; } TotalCost += MatcherCost; } } for ( const [ ListNumber, ReferenceCount ] of ListReferenceCounts ) { const List = Mutation.Lists?.[ ListNumber - 1 ]; const ListCost = GetMutationListSeedCost( List, ReferenceCount, Item => GetMutationListItemSeedCost( Item, Plants, Plant => GetPlantShopCost( SaveData, Plant.Id, Cache, Visiting ), Plant => IsPlantAvailableInShop( SaveData, Plant ) ) ); if (ListCost === null) { return null; } TotalCost += ListCost; } return TotalCost; } function GetEconomyMutationListReference( Matcher ) { if (typeof Matcher !== "string") { return null; } const Match = /^List:([1-9][0-9]*)$/.exec( Matcher ); return Match === null ? null : Number(Match[1]); } function GetEconomyMutationListReferenceCounts( Pattern ) { const Counts = new Map(); for (const Row of Pattern ?? []) { if (!Array.isArray(Row)) { continue; } for (const Matcher of Row) { const ListNumber = GetEconomyMutationListReference( Matcher ); if (ListNumber === null) { continue; } Counts.set( ListNumber, (Counts.get(ListNumber) ?? 0) + 1 ); } } return Counts; } function GetMutationListSeedCost( List, ReferenceCount, GetItemCost ) { if ( List === null || typeof List !== "object" || !Array.isArray(List.Items) || List.Items.length === 0 || !Number.isInteger(ReferenceCount) || ReferenceCount <= 0 ) { return null; } const ItemCosts = []; for (const Item of List.Items) { const Cost = GetItemCost(Item); if ( Cost === null || !Number.isFinite(Cost) || Cost < 0 ) { continue; } ItemCosts.push(Cost); } if (ItemCosts.length === 0) { return null; } if (List.Mode !== "Once") { return ( Math.min(...ItemCosts) * ReferenceCount ); } if (ItemCosts.length < ReferenceCount) { return null; } ItemCosts.sort( (A, B) => A - B ); return ItemCosts .slice(0, ReferenceCount) .reduce( (Total, Cost) => Total + Cost, 0 ); } function GetMutationListItemSeedCost( Item, PlantCatalogue, GetPlantCost, IsPlantCandidateAvailable ) { if ( Item === null || typeof Item !== "object" || Array.isArray(Item) ) { return null; } if ( Item.Type === "Any" || Item.Type === "Empty" ) { return 0; } if (Item.Type === "Plant") { const Plant = PlantCatalogue[Item.Value]; if ( Plant === undefined || Plant.Archived === true ) { return null; } return GetPlantCost(Plant); } if (Item.Type !== "Tag") { return null; } let CheapestCost = null; for ( const Plant of Object.values( PlantCatalogue ) ) { if ( !IsPlantCandidateAvailable( Plant ) || !Array.isArray(Plant.Tags) || !Plant.Tags.includes( Item.Value ) ) { continue; } const Cost = GetPlantCost(Plant); if (Cost === null) { continue; } if ( CheapestCost === null || Cost < CheapestCost ) { CheapestCost = Cost; } } return CheapestCost; } function GetMatcherSeedCost( SaveData, Matcher, Cache, Visiting ) { if (typeof Matcher === "string") { const Plant = Plants[Matcher]; if ( Plant === undefined || Plant.Archived === true ) { return null; } return GetPlantShopCost( SaveData, Plant.Id, Cache, Visiting ); } if ( typeof Matcher !== "object" || Matcher === null ) { return null; } if ( typeof Matcher.Plant === "string" ) { const Plant = Plants[Matcher.Plant]; if ( Plant === undefined || Plant.Archived === true ) { return null; } return GetPlantShopCost( SaveData, Plant.Id, Cache, Visiting ); } let CheapestCost = null; for ( const Plant of Object.values(Plants) ) { if ( !IsPlantAvailableInShop( SaveData, Plant ) ) { continue; } if ( !DoesPlantMatchRecipeMatcher( Plant, Matcher ) ) { continue; } const Cost = GetPlantShopCost( SaveData, Plant.Id, Cache, Visiting ); if (Cost === null) { continue; } if ( CheapestCost === null || Cost < CheapestCost ) { CheapestCost = Cost; } } return CheapestCost; } function DoesPlantMatchRecipeMatcher( Plant, Matcher ) { const Tags = Plant.Tags ?? []; if ( Array.isArray( Matcher.Tags ) && !Matcher.Tags.every( Tag => Tags.includes(Tag) ) ) { return false; } if ( Array.isArray( Matcher.TagsAny ) && Matcher.TagsAny.length > 0 && !Matcher.TagsAny.some( Tag => Tags.includes(Tag) ) ) { return false; } if ( Array.isArray( Matcher.TagsNot ) && Matcher.TagsNot.some( Tag => Tags.includes(Tag) ) ) { return false; } return true; } function GetMutationTimeFee( Mutation ) { const MutationTime = Number( Mutation.MutationTime ?? Mutation.Cooldown ?? 0 ); if ( !Number.isFinite( MutationTime ) || MutationTime <= 0 ) { return 0; } return Math.ceil( MutationTime / 60000 * MutationTimeFeePerMinute ); } function GetPlantHarvestMultiplier( PlantId ) { const Plant = typeof PlantId === "object" && PlantId !== null ? PlantId : GetPlantById( PlantId ); if (Plant === null) { return 1.5; } const Multiplier = Number( Plant.HarvestMultiplier ?? 1.5 ); return ( Number.isFinite(Multiplier) && Multiplier >= 0 ) ? Multiplier : 1.5; } function GetPlantHarvestReward( SaveData, PlantId ) { const Cost = GetPlantShopCost( SaveData, PlantId ); if (Cost === null) { return null; } return Math.ceil( Cost * GetPlantHarvestMultiplier( PlantId ) ); } function GetPlantDewPerHour( SaveData, PlantId ) { const Plant = GetPlantById( PlantId ); if (Plant === null) { return null; } const GrowthTime = Number( Plant.GrowthTime ); if ( !Number.isFinite( GrowthTime ) || GrowthTime <= 0 ) { return null; } const Cost = GetPlantShopCost( SaveData, PlantId ); const Reward = GetPlantHarvestReward( SaveData, PlantId ); if ( Cost === null || Reward === null ) { return null; } const Profit = Reward - Cost; return ( Profit * 3600000 / GrowthTime ); } function GetGardenEconomySummary( SaveData, Garden = SaveData?.Garden ) { if ( Garden === null || Garden === undefined || !Array.isArray( Garden.Plots ) ) { return null; } let DewInvested = 0; let HarvestValue = 0; let NetProfit = 0; let DewPerHour = 0; for ( const Plot of Garden.Plots ) { if (Plot === null) { continue; } const Plant = Plants[Plot.Plant]; if (Plant === undefined) { return null; } const Cost = GetPlantShopCost( SaveData, Plant.Id ); const Reward = GetPlantHarvestReward( SaveData, Plant.Id ); const PlantDewPerHour = GetPlantDewPerHour( SaveData, Plant.Id ); if ( Cost === null || Reward === null || PlantDewPerHour === null ) { return null; } DewInvested += Cost; HarvestValue += Reward; NetProfit += Reward - Cost; DewPerHour += PlantDewPerHour; } return { DewInvested, HarvestValue, NetProfit, DewPerHour }; } function FormatDewPerHour( DewPerHour ) { if ( DewPerHour === null || !Number.isFinite( DewPerHour ) ) { return "Unavailable"; } return ( DewPerHour.toLocaleString( undefined, { maximumFractionDigits: 2 } ) + " Dew" ); } function GetCataloguePlantById( PlantId, PlantCatalogue = Plants ) { PlantId = Number(PlantId); for ( const Plant of Object.values( PlantCatalogue ) ) { if (Number(Plant.Id) === PlantId) { return Plant; } } return null; } function GetCataloguePlantCostInfo( PlantId, PlantCatalogue = Plants, MutationCatalogue = MutationSets, Cache = new Map(), Visiting = new Set() ) { PlantId = Number(PlantId); if (Cache.has(PlantId)) { return Cache.get(PlantId); } const Plant = GetCataloguePlantById( PlantId, PlantCatalogue ); if (Plant === null) { return null; } if (Plant.Shop?.ShopPlant === true) { if ( Plant.Shop.BaseCost === null || Plant.Shop.BaseCost === undefined || Plant.Shop.BaseCost === "" ) { return null; } const BaseCost = Number( Plant.Shop.BaseCost ); if ( !Number.isFinite(BaseCost) || BaseCost < 0 ) { return null; } const Info = { Cost: Math.ceil(BaseCost), SourceType: "Base", SourceMutation: null }; Cache.set(PlantId, Info); return Info; } if (Visiting.has(PlantId)) { return null; } Visiting.add(PlantId); let CheapestInfo = null; for ( const [MutationKey, Mutation] of Object.entries( MutationCatalogue ) ) { if (Mutation.Archived === true) { continue; } if ( !Mutation.Relations ?.PlantsCreated ?.includes(PlantId) ) { continue; } const IngredientCost = GetCatalogueMutationIngredientCost( Mutation, PlantCatalogue, MutationCatalogue, Cache, Visiting ); if (IngredientCost === null) { continue; } const Cost = IngredientCost + GetMutationTimeFee( Mutation ); if ( CheapestInfo === null || Cost < CheapestInfo.Cost ) { CheapestInfo = { Cost: Cost, SourceType: "Mutation", SourceMutation: { Id: Number( Mutation.Id ), MutationKey: MutationKey, Name: Mutation.Name ?? MutationKey } }; } } Visiting.delete(PlantId); Cache.set(PlantId, CheapestInfo); return CheapestInfo; } function GetCatalogueMutationIngredientCost( Mutation, PlantCatalogue, MutationCatalogue, Cache, Visiting ) { let TotalCost = 0; const ListReferenceCounts = GetEconomyMutationListReferenceCounts( Mutation.Pattern ); for (const Row of Mutation.Pattern ?? []) { for (const Matcher of Row) { if ( Matcher === null || Matcher === "Empty" || Matcher === "Any" || GetEconomyMutationListReference( Matcher ) !== null ) { continue; } const MatcherCost = GetCatalogueMatcherSeedCost( Matcher, PlantCatalogue, MutationCatalogue, Cache, Visiting ); if (MatcherCost === null) { return null; } TotalCost += MatcherCost; } } for ( const [ ListNumber, ReferenceCount ] of ListReferenceCounts ) { const List = Mutation.Lists?.[ ListNumber - 1 ]; const ListCost = GetMutationListSeedCost( List, ReferenceCount, Item => GetMutationListItemSeedCost( Item, PlantCatalogue, Plant => GetCataloguePlantCostInfo( Plant.Id, PlantCatalogue, MutationCatalogue, Cache, Visiting )?.Cost ?? null, Plant => Plant.Archived !== true ) ); if (ListCost === null) { return null; } TotalCost += ListCost; } return TotalCost; } function GetCatalogueMatcherSeedCost( Matcher, PlantCatalogue, MutationCatalogue, Cache, Visiting ) { if (typeof Matcher === "string") { const Plant = PlantCatalogue[Matcher]; if ( Plant === undefined || Plant.Archived === true ) { return null; } return GetCataloguePlantCostInfo( Plant.Id, PlantCatalogue, MutationCatalogue, Cache, Visiting )?.Cost ?? null; } if ( typeof Matcher !== "object" || Matcher === null ) { return null; } if (typeof Matcher.Plant === "string") { const Plant = PlantCatalogue[Matcher.Plant]; if ( Plant === undefined || Plant.Archived === true ) { return null; } return GetCataloguePlantCostInfo( Plant.Id, PlantCatalogue, MutationCatalogue, Cache, Visiting )?.Cost ?? null; } let CheapestCost = null; for ( const Plant of Object.values( PlantCatalogue ) ) { if (Plant.Archived === true) { continue; } if ( !DoesPlantMatchRecipeMatcher( Plant, Matcher ) ) { continue; } const Cost = GetCataloguePlantCostInfo( Plant.Id, PlantCatalogue, MutationCatalogue, Cache, Visiting )?.Cost ?? null; if (Cost === null) { continue; } if ( CheapestCost === null || Cost < CheapestCost ) { CheapestCost = Cost; } } return CheapestCost; }