diff --git a/app/world-6/summoning/content.tsx b/app/world-6/summoning/content.tsx index 4a573d86..dbc07e4d 100644 --- a/app/world-6/summoning/content.tsx +++ b/app/world-6/summoning/content.tsx @@ -17,6 +17,8 @@ import { SummoningUpgrades } from '../../../components/world-6/summoning/summoni import { SummoningBattles } from '../../../components/world-6/summoning/summoningBattles'; import { useAppDataStore } from '../../../lib/providers/appDataStoreProvider'; import { useShallow } from 'zustand/react/shallow'; +import TipDisplay from '../../../components/base/TipDisplay'; +import { CircleInformation } from 'grommet-icons'; function Summoning() { const { theData } = useAppDataStore(useShallow( @@ -38,7 +40,7 @@ function Summoning() { { summoning.summonEssences.filter(essence => essence.displayEssence == true).map((essence) => ( - + } /> + + {essence.stoneBossVictories} Stone kills + + You can find the stone to start the boss battle here : + {summoning.getEssenceSummoningStoneBossmap(essence.color)} + + } + > + + + )) } diff --git a/components/world-6/summoning/summoningUpgrades.tsx b/components/world-6/summoning/summoningUpgrades.tsx index 2b81f0e2..8b4329e0 100644 --- a/components/world-6/summoning/summoningUpgrades.tsx +++ b/components/world-6/summoning/summoningUpgrades.tsx @@ -1,4 +1,5 @@ import { Box, Grid, Stack, Text } from "grommet"; +import { Star } from 'grommet-icons'; import ShadowBox from "../../base/ShadowBox"; import IconImage from "../../base/IconImage"; import TextAndLabel, { ComponentAndLabel } from "../../base/TextAndLabel"; @@ -6,6 +7,7 @@ import { nFormatter } from "../../../data/utility"; import { Summoning as SummoningDomain, SummonUpgrade, SummonEssence } from '../../../data/domain/world-6/summoning'; import TabButton from "../../base/TabButton"; import { useState } from "react"; +import TipDisplay from "../../base/TipDisplay"; const ColorSection = ({ colorUpgrades, allUpgrades, essence }: { colorUpgrades: SummonUpgrade[], allUpgrades: SummonUpgrade[], essence: SummonEssence | undefined }) => { if (!essence || colorUpgrades.length == 0) { @@ -43,6 +45,14 @@ const ColorSection = ({ colorUpgrades, allUpgrades, essence }: { colorUpgrades: {upgrade.data.name} {upgrade.getLevelDisplay()} + {upgrade.isDoubled && + + + } diff --git a/data/domain/idleonData.tsx b/data/domain/idleonData.tsx index 36888d8d..594c9e38 100644 --- a/data/domain/idleonData.tsx +++ b/data/domain/idleonData.tsx @@ -48,7 +48,7 @@ import { Rift } from "./world-4/rift"; import { Equinox, updateEquinoxBar } from './world-3/equinox'; import { POExtra } from './world-2/postoffice'; import { Sneaking, updateSneaking } from './world-6/sneaking'; -import { Summoning, updateSummoningLevelAndBonusesFromIt, updateSummoningWinnerBonusBoost, updateSummoningWinnerImpact } from './world-6/summoning'; +import { Summoning, updateSummoningUpgrades, updateSummoningWinnerBonusBoost, updateSummoningWinnerImpact } from './world-6/summoning'; import { Farming, updateFarmingCropScientistBonuses, updateFarmingDisplayData, updateFarmingLevel } from './world-6/farming'; import { StarSigns, updateInfinityStarSigns, updateStarSignsUnlocked } from './starsigns'; import { IslandExpeditions } from './world-2/islandExpedition'; @@ -236,6 +236,7 @@ export const initAccountDataKeys = (allItems: Item[]) => { // ORDER IS IMPORTANT, the keys are not relevant as data doesn't get persisted. // This allows for multiple calls that touch the same data to happen in the same map (artifacts + sailing for example) const postProcessingMap: Record = { + "updateCompanionImpact": (doc: Cloudsave, accountData: Map) => updateCompanionImpact(accountData), "updateOrionGlobalBonus": (doc: Cloudsave, accountData: Map) => updateOrionGlobalBonus(accountData), "updatePoppyGlobalBonus": (doc: Cloudsave, accountData: Map) => updatePoppyGlobalBonus(accountData), "updateBubba": (doc: Cloudsave, accountData: Map) => updateBubba(accountData), @@ -244,18 +245,16 @@ const postProcessingMap: Record = { "updateLegendTalents": (doc: Cloudsave, accountData: Map) => updateLegendTalents(accountData), "updatePlayerDeathnote": (doc: Cloudsave, accountData: Map) => updatePlayerDeathnote(accountData), "updatePlayerSpecialTalents": (doc: Cloudsave, accountData: Map) => updatePlayerSpecialTalents(accountData), - "summoningLevel": (doc: Cloudsave, accountData: Map) => updateSummoningLevelAndBonusesFromIt(accountData), + "emperorBonuses": (doc: Cloudsave, accountData: Map) => updateEmperorBonuses(accountData), "summoningWinnerBonus": (doc: Cloudsave, accountData: Map) => updateSummoningWinnerBonusBoost(accountData), "summoningWinnerImpact": (doc: Cloudsave, accountData: Map) => updateSummoningWinnerImpact(accountData), "starSignsUnlocked": (doc: Cloudsave, accountData: Map) => updateStarSignsUnlocked(accountData), "farmingLevel": (doc: Cloudsave, accountData: Map) => updateFarmingLevel(accountData), - "updateCompanionImpact": (doc: Cloudsave, accountData: Map) => updateCompanionImpact(accountData), "divinity": (doc: Cloudsave, accountData: Map) => updateDivinity(accountData), "updatePlayerTalentLevelWithoutESBonus": (doc: Cloudsave, accountData: Map) => updatePlayerTalentLevelExceptESBonus(accountData), "family": (doc: Cloudsave, accountData: Map) => calculateFamily(accountData), "updatePlayerTalentLevelESBonus": (doc: Cloudsave, accountData: Map) => updatePlayerTalentLevelESBonus(accountData), "arcaneCultistImpact": (doc: Cloudsave, accountData: Map) => updateArcaneCultistImpact(accountData), - "emperorBonuses": (doc: Cloudsave, accountData: Map) => updateEmperorBonuses(accountData), "updateAllShinies": (doc: Cloudsave, accountData: Map) => updateAllShinyEffects(accountData), "updateInfinityStarSigns": (doc: Cloudsave, accountData: Map) => updateInfinityStarSigns(accountData), "updateSuperbitImpcats": (doc: Cloudsave, accountData: Map) => updateSuperbitImpacts(accountData), @@ -299,6 +298,7 @@ const postProcessingMap: Record = { // I really really hate this. const postPostProcessingMap: Record = { + "updateSummoningUpgrades": (doc: Cloudsave, accountData: Map) => updateSummoningUpgrades(accountData), "updateWorshipTotemsSoulGainBonuses": (doc: Cloudsave, accountData: Map) => updateTotemsBonuses(accountData), "updateOrionFeatherRate": (doc: Cloudsave, accountData: Map) => updateOrionFeatherRate(accountData), "updatePoppyFishRate": (doc: Cloudsave, accountData: Map) => updatePoppyFishRate(accountData), diff --git a/data/domain/world-2/arcade.tsx b/data/domain/world-2/arcade.tsx index 1d32c522..d6240d4e 100644 --- a/data/domain/world-2/arcade.tsx +++ b/data/domain/world-2/arcade.tsx @@ -1,4 +1,4 @@ -import { lavaFunc, range } from "../../utility" +import { lavaFunc, nFormatter, range } from "../../utility" import { Achievement } from "../achievements"; import { Alchemy } from "./alchemy/alchemy"; import { Domain, RawData } from "../base/domain"; @@ -76,7 +76,7 @@ export class ArcadeBonus { } const finalBonus = baseBonus * multiplier; - return round ? Math.round(finalBonus) : finalBonus; + return round ? parseFloat(nFormatter(finalBonus)) : finalBonus; } getBonusText = (_: boolean = true) => { diff --git a/data/domain/world-4/tome.tsx b/data/domain/world-4/tome.tsx index d34261b5..5d00fc5d 100644 --- a/data/domain/world-4/tome.tsx +++ b/data/domain/world-4/tome.tsx @@ -45,6 +45,7 @@ import { TomeEpilogueBonusModel } from '../model/tomeEpilogueBonusModel'; import { EquipmentSets } from '../misc/equipmentSets'; import { Grimoire } from '../grimoire'; import { Bubba } from '../world-3/bubba'; +import { CoralReef } from '../world-7/coralReef'; export enum TomeScoreColors { Platinum = "#6EE3FF", @@ -364,6 +365,7 @@ export const updateTomeScores = (data: Map) => { const equipmentSet = data.get("equipmentSets") as EquipmentSets; const grimoire = data.get("grimoire") as Grimoire; const bubba = data.get("bubba") as Bubba; + const coralReef = data.get("coralReef") as CoralReef; // Calculate how many trophy and obols have been found const slab = data.get("slab") as Slab; @@ -524,7 +526,6 @@ export const updateTomeScores = (data: Map) => { const totalSummoningUpgradeLevels = summoning.summonUpgrades.reduce((sum, upgrade) => sum + upgrade.level, 0); // Sum of summoning victories - // TODO : update summoning to include the new color battles and upgrades const summoningVictories = summoning.summonBattles.getTotalVictories(); // Number of Ninja floors unlocked @@ -545,8 +546,7 @@ export const updateTomeScores = (data: Map) => { } // Total of all summoning boss victories - // TODO once implemented - const totalSummoningBossesVictories = 0; + const totalSummoningBossesVictories = summoning.summonEssences.reduce((sum, essence) => sum += essence.stoneBossVictories, 0); // Number of Jade Emporium upgrades bought const jadeEmporiumUpgradesBought = sneaking.jadeUpgrades.filter(upgrade => upgrade.display && upgrade.purchased).length; @@ -983,8 +983,7 @@ export const updateTomeScores = (data: Map) => { break; case 97: // Total coral reef upgrades - // TODO : get the values once implemented - line.updateAllPlayersCurrentValue(0); + line.updateAllPlayersCurrentValue(coralReef.upgrades.reduce((sum, upgrade) => sum += upgrade.level, 0)); break; case 98: // Deepest delve depth reach in a single run diff --git a/data/domain/world-6/emperor.tsx b/data/domain/world-6/emperor.tsx index 7f18069a..d5e2bce7 100644 --- a/data/domain/world-6/emperor.tsx +++ b/data/domain/world-6/emperor.tsx @@ -74,8 +74,6 @@ export class Emperor extends Domain { return 135e13 * Math.pow(1.7, this.emperorKills); } - - /** * Calculate daily emperor tries */ diff --git a/data/domain/world-6/farming.tsx b/data/domain/world-6/farming.tsx index 2402dbfe..b9b9432d 100644 --- a/data/domain/world-6/farming.tsx +++ b/data/domain/world-6/farming.tsx @@ -834,7 +834,7 @@ export const updateFarmingDisplayData = (data: Map) => { const skillMastery = rift.bonuses.find(bonus => bonus.name == "Skill Mastery") as SkillMastery; // Update Min and Max possible quantity to collect from one fully grown crop - const gemInstagrowPurchase = gemStore.purchases.find(purchase => purchase.index == 140)?.pucrhased ?? 0; + const gemInstagrowPurchase = gemStore.purchases.find(purchase => purchase.no == 139)?.pucrhased ?? 0; farming.updatePossibleQuantityToCollect(farming.getMarketUpgradeBonusValue(1), gemInstagrowPurchase); // Update growth speed for displayng when crops will be ready diff --git a/data/domain/world-6/summoning.tsx b/data/domain/world-6/summoning.tsx index 25ccd4e0..adaf5c79 100644 --- a/data/domain/world-6/summoning.tsx +++ b/data/domain/world-6/summoning.tsx @@ -17,8 +17,10 @@ import { TaskBoard } from "../tasks"; import { Achievement } from "../achievements"; import { Equinox } from "../world-3/equinox"; import { Cooking } from "../world-4/cooking"; -import { Emperor } from "../world-6/emperor"; +import { Emperor } from "./emperor"; import { EquipmentSets } from "../misc/equipmentSets"; +import { Tesseract } from "../tesseract"; +import { GemStore } from "../gemPurchases"; const WhiteBattleOrder = [ "Pet1", "Pet2", "Pet3", "Pet0", "Pet4", "Pet6", "Pet5", "Pet10", "Pet11" @@ -35,7 +37,7 @@ const EndlessModeBonusIndexes = [ // engine.GameAttribute.h.CustomList.h.SummonEnemies[10] const EndlessModeBonusIncrease = [ - 1,3,1,12,1,7,2,4,15,10,1,4,18,2,4,3,20,25,2,20,5,30,24,4,1,2,2,35,9,26,5,5,40,1,45,50,2,6,30,3 + 1,3,1,12,1,7,2,4,15,3,1,4,18,2,4,3,2,2,2,20,5,4,24,4,1,2,3,5,9,26,5,5,3,1,5,8,2,6,30,3 ] // engine.GameAttribute.h.CustomList.h.SummonEnemies[11] @@ -66,7 +68,7 @@ export enum SummonEssenceColor { Purple = 4, Red = 5, Cyan = 6, - FutureContent3 = 7, + Teal = 7, FutureContent4 = 8, Endless = 9 } @@ -76,18 +78,23 @@ export class SummonUpgrade { level: number = 0; shouldBeDisplayed: boolean = true; bonusMultiplyer: number = 1; + isDoubled: boolean = false; + doublerBonus: number = 1; + stoneBossBonus: number = 1; + totalCostReduction: number = 1; - constructor(public index: number, public data: SummonUpgradeModel, level: number = 0) { + constructor(public index: number, public data: SummonUpgradeModel, level: number = 0, doubled: boolean = false) { this.shouldBeDisplayed = (data.name != "Name"); this.level = level; + this.isDoubled = doubled; } nextLevelCost = (): number => { - return this.data.cost * Math.pow(this.data.costExponent, this.level); + return this.totalCostReduction * this.data.cost * Math.pow(this.data.costExponent, this.level); } getBaseBonus = (level: number = this.level): number => { - return level * this.data.bonusQty + return level * this.data.bonusQty * this.stoneBossBonus * this.doublerBonus; } getFullBonus = (level: number = this.level): number => { @@ -123,11 +130,14 @@ export class SummonUpgrade { } getBonusText = (level: number = this.level): string => { - // This bonus is special so we make a special case if (this.index == 2) { + // This bonus is special so we make a special case return this.data.bonus.slice(0, this.data.bonus.indexOf('@')) + "Cost (and level) reset by cycle of 4 days (but you keep summoned familiars)"; + } else if ([49, 57, 72, 75].indexOf(this.index) >= 0) { + // Those are the cost reduction upgrades, so we need to format the bonus to get the same display as in-game + return this.data.bonus.replace(/@/, '\r\n').replace(/{/, nFormatter(this.getBaseBonus(level), "CommaNotation")).replace(/}/, nFormatter((1 - 1 / (1 + this.getFullBonus(level) / 100)) * 100, "MultiplierInfo")); } else { - return this.data.bonus.replace(/@/, '\r\n').replace(/{/, this.getBaseBonus(level).toString()).replace(/}/, this.getFullBonus(level).toString()); + return this.data.bonus.replace(/@/, '\r\n').replace(/{/, nFormatter(this.getBaseBonus(level), "CommaNotation")).replace(/}/, nFormatter(this.getFullBonus(level), "CommaNotation")); } } @@ -136,7 +146,6 @@ export class SummonUpgrade { } } - export class SummonBonus { bonusValue: number = 0; pristineCharmBonus: number = 1; @@ -144,11 +153,10 @@ export class SummonBonus { taskBoardBonus: number = 0; achievement379Bonus: number = 0; achievement373Bonus: number = 0; - summoning32Bonus: number = 0; - - // More bonuses + summoning31Bonus: number = 0; godshardSetBonus: number = 0; emperorBonus: number = 0; + gemItemPurchased: number = 0; constructor(public index: number, public data: SummonEnemyBonusModel, bonusValue: number = 0) { this.bonusValue = bonusValue; @@ -157,18 +165,17 @@ export class SummonBonus { getBonus = (): number => { switch (true) { - // +1 from in-game indexes as it starts from 1 instead of 0 - case this.data.bonusId == 21: - case this.data.bonusId == 23: - case this.data.bonusId == 25: - case this.data.bonusId == 32: + case this.index == 20: + case this.index == 22: + case this.index == 24: + case this.index == 31: return this.bonusValue; - case this.data.bonusId == 20: - return 3.5 * this.bonusValue * this.pristineCharmBonus * (1 + (this.artifactBonus + Math.min(10, this.taskBoardBonus) + this.achievement379Bonus + this.achievement373Bonus + this.godshardSetBonus) / 100); - case 21 <= this.data.bonusId && 34 >= this.data.bonusId: - return this.bonusValue * this.pristineCharmBonus * (1 + (this.artifactBonus + Math.min(10, this.taskBoardBonus) + this.achievement379Bonus + this.achievement373Bonus + this.summoning32Bonus + this.godshardSetBonus + this.emperorBonus) / 100); + case this.index == 19: + return 3.5 * this.bonusValue * this.pristineCharmBonus * (1 + 10 * this.gemItemPurchased / 100) * (1 + (this.artifactBonus + Math.min(10, this.taskBoardBonus) + this.achievement379Bonus + this.achievement373Bonus + this.godshardSetBonus) / 100); + case 20 <= this.index && 33 >= this.index: + return this.bonusValue * this.pristineCharmBonus * (1 + 10 * this.gemItemPurchased / 100) * (1 + (this.artifactBonus + Math.min(10, this.taskBoardBonus) + this.achievement379Bonus + this.achievement373Bonus + this.summoning31Bonus + this.godshardSetBonus + this.emperorBonus) / 100); default: - return 3.5 * this.bonusValue * this.pristineCharmBonus * (1 + (this.artifactBonus + Math.min(10, this.taskBoardBonus) + this.achievement379Bonus + this.achievement373Bonus + this.summoning32Bonus + this.godshardSetBonus + this.emperorBonus) / 100); + return 3.5 * this.bonusValue * this.pristineCharmBonus * (1 + 10 * this.gemItemPurchased / 100) * (1 + (this.artifactBonus + Math.min(10, this.taskBoardBonus) + this.achievement379Bonus + this.achievement373Bonus + this.summoning31Bonus + this.godshardSetBonus + this.emperorBonus) / 100); } } @@ -192,7 +199,7 @@ export class SummonBonus { getBonusText = (): string => { // Can't have the two at the same time, so no worries with displaying two times the bonus - return this.data.bonus.replace(/{/, nFormatter(this.getBonus())).replace(/ essence.color == SummonEssenceColor.Cyan)?.victories ?? 0); break; + case 73: + // Multiply bonus by teal victory + upgrade.bonusMultiplyer = (this.summonEssences?.find(essence => essence.color == SummonEssenceColor.Teal)?.victories ?? 0); + break; case 62: case 63: case 64: @@ -341,13 +353,15 @@ export class Summoning extends Domain { case 65: case 66: case 67: + case 80: // Multiply bonus by summoning level upgrade.bonusMultiplyer = this.summoningLevel; break; case 60: case 61: + case 75: // Multiply bonus for every 100 total summoning upgrades purchased - upgrade.bonusMultiplyer = Math.floor(this.summonUpgrades.reduce((sum, upgrade) => sum + upgrade.level, 0)/100); + upgrade.bonusMultiplyer = Math.max(0, Math.floor(this.summonUpgrades.reduce((sum, upgrade) => sum + upgrade.level, 0)/100)); break; default: upgrade.bonusMultiplyer = 1; @@ -358,16 +372,16 @@ export class Summoning extends Domain { updatePlayersUnitStats = () => { const healthFlatBonus = [1,10,35,37]; - const healthFlat = (1 + this.summonUpgrades.filter(upgrade => healthFlatBonus.indexOf(upgrade.index) > -1)?.reduce((sum, upgrade) => sum + upgrade.getFullBonus(), 0)); - this.summonBattles.playerUnitsHP = healthFlat * (1 + (this.summonUpgrades.find(upgrade => upgrade.index == 20)?.getFullBonus() ?? 0) / 100 ) - * (1 + ((this.summonUpgrades.find(upgrade => upgrade.index == 50)?.getFullBonus() ?? 0) + (this.summonUpgrades.find(upgrade => upgrade.index == 59)?.getFullBonus() ?? 0) + (this.summonUpgrades.find(upgrade => upgrade.index == 63)?.getFullBonus() ?? 0)) / 100 ) - * (1 + (this.summonUpgrades.find(upgrade => upgrade.index == 61)?.getFullBonus() ?? 0) / 100); + const healthFlat = 2 + (1 + this.summonUpgrades.filter(upgrade => healthFlatBonus.indexOf(upgrade.index) > -1)?.reduce((sum, upgrade) => sum + upgrade.getFullBonus(), 0)); + this.summonBattles.playerUnitsHP = healthFlat * (1 + (this.getUpgradeBonusFromIndex(20) + this.getUpgradeBonusFromIndex(81)) / 100) + * (1 + (this.getUpgradeBonusFromIndex(50) + this.getUpgradeBonusFromIndex(59) + this.getUpgradeBonusFromIndex(63)) / 100) + * (1 + this.getUpgradeBonusFromIndex(61) / 100); const attackFlatBonus = [3,12,21,31]; const attackFlat = (1 + this.summonUpgrades.filter(upgrade => attackFlatBonus.indexOf(upgrade.index) > -1)?.reduce((sum, upgrade) => sum + upgrade.getFullBonus(), 0)); - this.summonBattles.playerUnitsAtk = attackFlat * (1 + (this.summonUpgrades.find(upgrade => upgrade.index == 43)?.getFullBonus() ?? 0) / 100 ) - * (1 + ((this.summonUpgrades.find(upgrade => upgrade.index == 51)?.getFullBonus() ?? 0) + (this.summonUpgrades.find(upgrade => upgrade.index == 56)?.getFullBonus() ?? 0) + (this.summonUpgrades.find(upgrade => upgrade.index == 64)?.getFullBonus() ?? 0)) / 100 ) - * (1 + (this.summonUpgrades.find(upgrade => upgrade.index == 60)?.getFullBonus() ?? 0) / 100); + this.summonBattles.playerUnitsAtk = attackFlat * (1 + (this.getUpgradeBonusFromIndex(43) + this.getUpgradeBonusFromIndex(74)) / 100) + * (1 + (this.getUpgradeBonusFromIndex(51) + this.getUpgradeBonusFromIndex(56) + this.getUpgradeBonusFromIndex(64)) / 100) + * (1 + this.getUpgradeBonusFromIndex(60) / 100); // This bonus is based on attack damage of units, so can't update it before const sharpenedSpikeUpgrade = this.summonUpgrades.find(upgrade => upgrade.index == 68); @@ -379,7 +393,9 @@ export class Summoning extends Domain { getRawKeys(): RawData[] { return [ { key: "Summon", perPlayer: false, default: [] }, - { key: "OptLacc", perPlayer: false, default: [] } + { key: "OptLacc", perPlayer: false, default: [] }, + { key: "Holes", perPlayer: false, default:[] }, + { key: "KRbest", perPlayer: false, default:[] }, ] } @@ -391,20 +407,22 @@ export class Summoning extends Domain { const summoning = data.get(this.dataKey) as Summoning; const summoningData = data.get("Summon") as any[]; const optionList = data.get("OptLacc") as number[]; + const holeData = data.get("Holes") as any[]; + const killroyKillsData = data.get("KRbest") as Record; // Defend against old accounts and people without any summoning data. if (summoningData.length == 0) { return; } + const doublerData = (holeData[28] || []) as number[]; + summoning.summonUpgrades = []; - summoning.summonUpgrades = initSummonUpgradeRepo() - .map( - base => new SummonUpgrade(base.index, base.data, summoningData[0][base.index] ?? 0) + summoning.summonUpgrades = initSummonUpgradeRepo().map( + base => new SummonUpgrade(base.index, base.data, summoningData[0][base.index] ?? 0, doublerData.includes(base.index)) ); - summoning.summonBonuses = initSummonEnemyBonusRepo() - .map( + summoning.summonBonuses = initSummonEnemyBonusRepo().map( base => new SummonBonus(base.index, base.data) ); @@ -479,6 +497,7 @@ export class Summoning extends Domain { let unlocked: boolean = false; let displayBattle: boolean = false; let displayEssence: boolean = false; + const stoneBossVictories = killroyKillsData[`SummzTrz${index}`] ?? 0; const essenceOwned: number = index < essences.length ? essences[index] : 0; switch(index) { case SummonEssenceColor.White: @@ -516,6 +535,12 @@ export class Summoning extends Domain { displayBattle = true; displayEssence = true; break; + case SummonEssenceColor.Teal: + // TODO : update this once Spelunking is correctly implemented + unlocked = false; + displayBattle = true; + displayEssence = true; + break; case SummonEssenceColor.Endless: // Need to update this once we know which upgrade unlock this unlocked = (this.summonUpgrades?.find(upgrade => upgrade.index == 70)?.level ?? 0) > 0; @@ -523,7 +548,6 @@ export class Summoning extends Domain { // There's no real Endless essence, so never display it displayEssence = false; break; - case SummonEssenceColor.FutureContent3: case SummonEssenceColor.FutureContent4: default: unlocked = false; @@ -542,7 +566,7 @@ export class Summoning extends Domain { colorBattles = summoning.summonBattles.allBattles[index]; } - summoning.summonEssences.push({ color: index, quantity: essenceOwned, unlocked: unlocked, displayBattles: displayBattle, displayEssence: displayEssence, victories: colorVictories, battles: colorBattles }); + summoning.summonEssences.push({ color: index, quantity: essenceOwned, unlocked: unlocked, displayBattles: displayBattle, displayEssence: displayEssence, victories: colorVictories, battles: colorBattles, stoneBossVictories: stoneBossVictories }); } summoning.updateUnlockedUpgrades(); @@ -553,6 +577,40 @@ export class Summoning extends Domain { summoning.summonFamiliarRaw = summoningData[4]; } + getUpgradeBonusFromIndex(index: number): number { + return this.summonUpgrades.find(upgrade => upgrade.index == index)?.getFullBonus() || 0; + } + + getSummoningWinnerBonusFromIndex(index: number): number { + return this.summonBonuses.find(bonus => bonus.index == index)?.getBonus() || 0; + } + + getEssenceSummoningStoneBossmap(index: number): string { + switch (index) { + case SummonEssenceColor.White: return "Bamboo Laboredge (W6)"; + case SummonEssenceColor.Green: return "Lightway Path (W6)"; + case SummonEssenceColor.Yellow: return "Yolrock Basin (W6)"; + case SummonEssenceColor.Blue: return "Equinox Valley (W3)"; + case SummonEssenceColor.Purple: return "Jelly Cube Bridge (W4)"; + case SummonEssenceColor.Red: return "Crawly Catacombs (W5)"; + case SummonEssenceColor.Cyan: return "Emperor's Castle Doorstep (W6)"; + default: return "We don't know yet"; + } + } + + getEssenceSummoningStoneBossName(index: number): string { + switch (index) { + case SummonEssenceColor.White: return "The Aetherdoot"; + case SummonEssenceColor.Green: return "The Grover Glunko"; + case SummonEssenceColor.Yellow: return "The Shimmerlord"; + case SummonEssenceColor.Blue: return "The Freezerslush"; + case SummonEssenceColor.Purple: return "The Hexermush"; + case SummonEssenceColor.Red: return "The Cinderdomeo"; + case SummonEssenceColor.Cyan: return "The Zephyeror"; + default: return "No boss yet"; + } + } + static getSummoningStoneIcon(color: SummonEssenceColor): ImageData { if (color == SummonEssenceColor.Endless) { return { @@ -594,20 +652,6 @@ export class Summoning extends Domain { } } -export const updateSummoningLevelAndBonusesFromIt = (data: Map) => { - const summoning = data.get("summoning") as Summoning; - const players = data.get("players") as Player[]; - - const levels: number[] = []; - players.forEach(player => { - levels.push(player.skills.get(SkillsIndex.Summoning)?.level ?? 0); - }) - summoning.summoningLevel = Math.max(...levels); - - summoning.updateSecondaryBonus(); - summoning.updatePlayersUnitStats(); -} - export const updateSummoningWinnerBonusBoost = (data: Map) => { const summoning = data.get("summoning") as Summoning; const sneaking = data.get("sneaking") as Sneaking; @@ -616,6 +660,7 @@ export const updateSummoningWinnerBonusBoost = (data: Map) => { const achievements = data.get("achievements") as Achievement[]; const emperor = data.get("emperor") as Emperor; const equipmentSets = data.get("equipmentSets") as EquipmentSets; + const gemStore = data.get("gems") as GemStore; const crystalComb = sneaking.pristineCharms?.find(charm => charm.data.itemId == 8); const charmBonus = (crystalComb && crystalComb.unlocked) ? (1 + crystalComb.data.x1 / 100) : 1; @@ -626,9 +671,10 @@ export const updateSummoningWinnerBonusBoost = (data: Map) => { const taskBonus = taskboard.merits[44].getBonus(); const emperorBonus = emperor.emperorBonuses[8].getBonus(); const godshardSetBonus = equipmentSets.getSetBonus("GODSHARD_SET", undefined, true); + const gemItemPurchased = gemStore.purchases.find(purchase => purchase.no == 11)?.pucrhased || 0; // this bonus isn't affected by any boost, so we can already calculate it here - const summonBonus = (summoning.summonBonuses.find(bonus => bonus.data.bonusId == 32)?.getBonus() ?? 0); + const summonBonus = summoning.getSummoningWinnerBonusFromIndex(31); summoning.summonBonuses.forEach(bonus => { bonus.pristineCharmBonus = charmBonus; @@ -636,10 +682,13 @@ export const updateSummoningWinnerBonusBoost = (data: Map) => { bonus.taskBoardBonus = taskBonus; bonus.achievement373Bonus = achiev373; bonus.achievement379Bonus = achiev379; - bonus.summoning32Bonus = summonBonus; + bonus.summoning31Bonus = summonBonus; bonus.godshardSetBonus = godshardSetBonus; bonus.emperorBonus = emperorBonus; + bonus.gemItemPurchased = gemItemPurchased; }); + + return summoning; } // Kinda like what have been done for breeding Shiny with updateAllShinyEffects(), easier to manage this way @@ -649,7 +698,7 @@ export const updateSummoningWinnerImpact = (data: Map) => { const cooking = data.get("cooking") as Cooking; // Equinox Max Level - const bonusEquinoxLevel = (summoning.summonBonuses.find(bonus => bonus.data.bonusId == 25)?.getBonus() ?? 0); + const bonusEquinoxLevel = (summoning.summonBonuses.find(bonus => bonus.index == 24)?.getBonus() ?? 0); // Don't bother if == 0 if (bonusEquinoxLevel > 0) { equinox.upgrades.forEach((upgrade) => { @@ -659,9 +708,69 @@ export const updateSummoningWinnerImpact = (data: Map) => { } // Meal Bonus - const mealBonus = (summoning.summonBonuses.find(bonus => bonus.data.bonusId == 27)?.getBonus() ?? 0); + const mealBonus = (summoning.summonBonuses.find(bonus => bonus.index == 26)?.getBonus() ?? 0); cooking.meals.forEach(meal => { meal.winnerBonus = mealBonus; }); + + return summoning; +} + +export const updateSummoningUpgrades= (data: Map) => { + const summoning = data.get("summoning") as Summoning; + const players = data.get("players") as Player[]; + const tesseract = data.get("tesseract") as Tesseract; + + // Update upgrade bonuses + const levels: number[] = []; + players.forEach(player => { + levels.push(player.skills.get(SkillsIndex.Summoning)?.level ?? 0); + }) + summoning.summoningLevel = Math.max(...levels); + + const talentBonus597 = players.reduce((value, player) => { + const talentBonus = player.getTalentBonus(597); + + if (talentBonus > value) { + return talentBonus; + } else { + return value; + } + }, 0); + const doublerBonus = 2 + Math.max(0, talentBonus597 / 100 - 1) + summoning.getUpgradeBonusFromIndex(78) / 100; + + summoning.summonUpgrades.forEach(upgrade => { + upgrade.doublerBonus = upgrade.isDoubled ? doublerBonus : 1; + upgrade.stoneBossBonus = 1 + (parseInt(upgrade.data.filler) == 1 ? summoning.summonEssences.find(essence => essence.color == upgrade.data.colour)?.stoneBossVictories || 0 : 0); + }); + summoning.updateSecondaryBonus(); + summoning.updatePlayersUnitStats(); + + // Update cost reduction for upgrades + const talentBonus595 = players.reduce((value, player) => { + const talentBonus = player.getTalentBonus(595); + + if (talentBonus > value) { + return talentBonus; + } else { + return value; + } + }, 0); + + const costReductionFromTalent595 = 1 / (1 + Math.max(0, Math.floor(summoning.summonUpgrades.reduce((sum, upgrade) => sum + upgrade.level, 0)/100)) * talentBonus595 / 100); + const costReductionFromTesseract54 = 1 / (1 + tesseract.getUpgradeBonus(54) * (summoning.summonEssences?.find(essence => essence.color == SummonEssenceColor.Endless)?.victories ?? 0) / 100); + const costReductionFromUpgrade49 = 1 / (1 + summoning.getUpgradeBonusFromIndex(49) / 100); + const costReductionFromUpgrade57 = 1 / (1 + summoning.getUpgradeBonusFromIndex(57) / 100); + const costReductionFromUpgrade72 = 1 / (1 + summoning.getUpgradeBonusFromIndex(72) / 100); + const costReductionFromUpgrade75 = 1 / (1 + summoning.getUpgradeBonusFromIndex(75) / 100); + + const totalCostReduction = costReductionFromTalent595 * costReductionFromTesseract54 * costReductionFromUpgrade49 * costReductionFromUpgrade57 + * costReductionFromUpgrade72 * costReductionFromUpgrade75; + + summoning.summonUpgrades.forEach(upgrade => { + upgrade.totalCostReduction = totalCostReduction; + }); + + return summoning; } diff --git a/docs/TESTING_IMPLEMENTATION.md b/docs/TESTING_IMPLEMENTATION.md index 25c2e161..c7c8ace7 100644 --- a/docs/TESTING_IMPLEMENTATION.md +++ b/docs/TESTING_IMPLEMENTATION.md @@ -306,13 +306,19 @@ boat.getSpeedValue({withCaptain: false}) ### Precision Handling -Use appropriate precision for floating-point comparisons: +99% of cases will use a tolerance of 0. + +We can use a few percentage of tolerance for very big numbers of +unstable calculations. + +This will only be done on a case by case basis and very rarely. + ```typescript -// For percentages and most calculations -expect(calculated).toBeCloseTo(extracted, 2); // 2 decimal places +// For most calculations +expect(domainValue).toMatchLiveGame(liveValue, 0); -// For very precise calculations -expect(calculated).toBeCloseTo(extracted, 5); // 5 decimal places +// For very rare inconsistent calculations +expect(domainValue).toMatchLiveGame(liveValue, 0.01); ``` ## Troubleshooting diff --git a/tests/configs/summoning-win-bonus.json b/tests/configs/summoning-win-bonus.json new file mode 100644 index 00000000..4c05b97f --- /dev/null +++ b/tests/configs/summoning-win-bonus.json @@ -0,0 +1,81 @@ +{ + "description": "Extract data for summoning winner bonus (WinBonus) calculations", + "target": "Summoning WinBonus", + "extractions": [ + { + "label": "pristine_charm_bonus_8", + "expression": "idleon.callFunction(\"Ninja\", \"PristineBon\", 8, 0)", + "description": "Pristine charm bonus for summoning (index 8 - Crystal Comb)" + }, + { + "label": "gem_item_purchased_11", + "expression": "idleon.engine.getGameAttribute(\"GemItemsPurchased\")[11]", + "description": "Gem shop purchase 11 (Winner Bonuses)" + }, + { + "label": "artifact_bonus_32", + "expression": "idleon.callFunction(\"Sailing\", \"ArtifactBonus\", 32, 0)", + "description": "Sailing artifact 32 bonus (summoning winner bonus)" + }, + { + "label": "task_board_bonus_2_5_4", + "expression": "idleon.engine.getGameAttribute(\"Tasks\")[2][5][4]", + "description": "Task board merit bonus (max 10) for summoning winners" + }, + { + "label": "achievement_379_status", + "expression": "idleon.callFunction(\"AchieveStatus\", 379)", + "description": "Achievement 379 completion status (winner bonus +1%)" + }, + { + "label": "achievement_373_status", + "expression": "idleon.callFunction(\"AchieveStatus\", 373)", + "description": "Achievement 373 completion status (winner bonus +1%)" + }, + { + "label": "godshard_set_bonus", + "expression": "idleon.callFunction(\"GetSetBonus\", \"GODSHARD_SET\", \"Bonus\", 0, 0)", + "description": "Godshard equipment set bonus for summoning winners" + }, + { + "label": "emperor_bonus_8", + "expression": "idleon.callFunction(\"Thingies\", \"EmperorBon\", 8, 0)", + "description": "Emperor bonus 8 (summoning winner bonus)" + }, + { + "label": "win_bonus_19", + "expression": "idleon.callFunction(\"Summoning\", \"WinBonus\", 19, 0)", + "description": "Winner bonus 19 (uses 3.5x multiplier, no recursive bonus)" + }, + { + "label": "win_bonus_20", + "expression": "idleon.callFunction(\"Summoning\", \"WinBonus\", 20, 0)", + "description": "Winner bonus 20 (raw value only, no multipliers)" + }, + { + "label": "win_bonus_22", + "expression": "idleon.callFunction(\"Summoning\", \"WinBonus\", 22, 0)", + "description": "Winner bonus 22 (raw value only, no multipliers)" + }, + { + "label": "win_bonus_24", + "expression": "idleon.callFunction(\"Summoning\", \"WinBonus\", 24, 0)", + "description": "Winner bonus 24 (raw value only, no multipliers)" + }, + { + "label": "win_bonus_26", + "expression": "idleon.callFunction(\"Summoning\", \"WinBonus\", 26, 0)", + "description": "Winner bonus 26 (Meal bonus - uses all multipliers + bonus 31)" + }, + { + "label": "win_bonus_31", + "expression": "idleon.callFunction(\"Summoning\", \"WinBonus\", 31, 0)", + "description": "Winner bonus 31" + }, + { + "label": "win_bonus_5", + "expression": "idleon.callFunction(\"Summoning\", \"WinBonus\", 5, 0)", + "description": "Winner bonus 5 (uses 3.5x multiplier and all bonuses including recursive bonus 31)" + } + ] +} diff --git a/tests/domains/alchemy/diamondchef.test.ts b/tests/domains/alchemy/diamondchef.test.ts index b5349fd1..314c723e 100644 --- a/tests/domains/alchemy/diamondchef.test.ts +++ b/tests/domains/alchemy/diamondchef.test.ts @@ -7,7 +7,7 @@ import { loadExtractionResults, validateExtractionHealth, getExtractedValue } from '../../utils/live-game-data-loader'; import { loadGameDataFromSave } from '../../utils/cloudsave-loader'; -import { Alchemy, CauldronIndex, DiamonChefBubble } from '../../../data/domain/alchemy'; +import { Alchemy, CauldronIndex, DiamonChefBubble } from '../../../data/domain/world-2/alchemy/alchemy'; import { lavaFunc } from '../../../data/utility'; const saveName = 'latest'; diff --git a/tests/domains/cooking/meal-bonus-parameters.test.ts b/tests/domains/cooking/meal-bonus-parameters.test.ts index a14ba7fe..cd5f8d20 100644 --- a/tests/domains/cooking/meal-bonus-parameters.test.ts +++ b/tests/domains/cooking/meal-bonus-parameters.test.ts @@ -15,10 +15,10 @@ import { loadExtractionResults, validateExtractionHealth, getExtractedValue } from '../../utils/live-game-data-loader'; import { loadGameDataFromSave } from '../../utils/cloudsave-loader'; -import { Lab } from '../../../data/domain/lab'; -import { Breeding } from '../../../data/domain/breeding'; +import { Lab } from '../../../data/domain/world-4/lab'; +import { Breeding } from '../../../data/domain/world-4/breeding'; import { Summoning } from '../../../data/domain/world-6/summoning'; -import { Cooking } from '../../../data/domain/cooking'; +import { Cooking } from '../../../data/domain/world-4/cooking'; const saveName = 'latest'; const extractionResultsName = 'cooking-meal-bonus-data.json'; @@ -47,7 +47,7 @@ const parameterSpecs = { extractionKey: 'win_bonus_26', domainExtractor: (gameData: Map) => { const summoning = gameData.get("summoning") as Summoning; - return summoning.summonBonuses.find(bonus => bonus.data.bonusId === 26)?.getBonus() ?? 0; + return summoning.summonBonuses.find(bonus => bonus.index === 26)?.getBonus() ?? 0; } }, diff --git a/tests/domains/cooking/meal-bonus.test.ts b/tests/domains/cooking/meal-bonus.test.ts index 94cee2be..1124bdf9 100644 --- a/tests/domains/cooking/meal-bonus.test.ts +++ b/tests/domains/cooking/meal-bonus.test.ts @@ -7,7 +7,7 @@ import { loadExtractionResults, validateExtractionHealth } from '../../utils/live-game-data-loader'; import { loadGameDataFromSave } from '../../utils/cloudsave-loader'; -import { Cooking } from '../../../data/domain/cooking'; +import { Cooking } from '../../../data/domain/world-4/cooking'; const saveName = 'latest'; const extractionResultsName = 'cooking-meal-bonus-data.json'; diff --git a/tests/domains/cooking/meal-speed-parameters.test.ts b/tests/domains/cooking/meal-speed-parameters.test.ts index c909beef..ef0fce27 100644 --- a/tests/domains/cooking/meal-speed-parameters.test.ts +++ b/tests/domains/cooking/meal-speed-parameters.test.ts @@ -8,19 +8,19 @@ import { loadExtractionResults, validateExtractionHealth, getExtractedValue } from '../../utils/live-game-data-loader'; import { loadGameDataFromSave } from '../../utils/cloudsave-loader'; import { ParameterTestSpec } from '../../utils/parameter-test-config'; -import { Cooking } from '../../../data/domain/cooking'; +import { Cooking } from '../../../data/domain/world-4/cooking'; import { StarSigns } from '../../../data/domain/starsigns'; import { CropScientistBonusText, Farming } from '../../../data/domain/world-6/farming'; import { Player } from '../../../data/domain/player'; import { Votes } from '../../../data/domain/world-2/votes'; -import { Alchemy } from '../../../data/domain/alchemy'; +import { Alchemy } from '../../../data/domain/world-2/alchemy/alchemy'; import { UpgradeVault } from '../../../data/domain/upgradeVault'; -import { AtomCollider } from '../../../data/domain/atomCollider'; -import { TotalizerBonus, Worship } from '../../../data/domain/worship'; -import { Sailing } from '../../../data/domain/sailing'; -import { Arcade } from '../../../data/domain/arcade'; +import { AtomCollider } from '../../../data/domain/world-3/construction/atomCollider'; +import { TotalizerBonus, Worship } from '../../../data/domain/world-3/worship'; +import { Sailing } from '../../../data/domain/world-5/sailing/sailing'; +import { Arcade } from '../../../data/domain/world-2/arcade'; import { Stamp } from '../../../data/domain/world-1/stamps'; -import { Lab } from '../../../data/domain/lab'; +import { Lab } from '../../../data/domain/world-4/lab'; import { Summoning } from '../../../data/domain/world-6/summoning'; import { Hole } from '../../../data/domain/world-5/hole/hole'; import { Card } from '../../../data/domain/cards'; @@ -235,8 +235,7 @@ const cookingParameterSpecs: Record = { extractionKey: 'summoning_win_bonus_15', domainExtractor: (gameData) => { const summoning = gameData.get("summoning") as Summoning; - // Our bonus index starts at 1, so we get 16 instead of 15. - const winnerBonus = summoning.summonBonuses.find(bonus => bonus.data.bonusId == 16)?.getBonus() ?? 0; + const winnerBonus = summoning.summonBonuses.find(bonus => bonus.index == 15)?.getBonus() ?? 0; return winnerBonus; } }, diff --git a/tests/domains/cooking/meal-speed.test.ts b/tests/domains/cooking/meal-speed.test.ts index 24ed8396..1c44bf65 100644 --- a/tests/domains/cooking/meal-speed.test.ts +++ b/tests/domains/cooking/meal-speed.test.ts @@ -7,7 +7,7 @@ import { loadExtractionResults, getExtractedValue, validateExtractionHealth } from '../../utils/live-game-data-loader'; import { loadGameDataFromSave } from '../../utils/cloudsave-loader'; -import { Cooking } from '../../../data/domain/cooking'; +import { Cooking } from '../../../data/domain/world-4/cooking'; // TODO: Make it possible to test multiple save / extraction results. const saveName = 'latest'; diff --git a/tests/domains/sailing/speed-parameters.test.ts b/tests/domains/sailing/speed-parameters.test.ts index bf048ed8..592f6d4b 100644 --- a/tests/domains/sailing/speed-parameters.test.ts +++ b/tests/domains/sailing/speed-parameters.test.ts @@ -1,19 +1,19 @@ import { loadExtractionResults, validateExtractionHealth, getExtractedValue } from '../../utils/live-game-data-loader'; import { loadGameDataFromSave } from '../../utils/cloudsave-loader'; import { ParameterTestSpec } from '../../utils/parameter-test-config'; -import { Sailing } from '../../../data/domain/sailing'; -import { Divinity } from '../../../data/domain/divinity'; +import { Sailing } from '../../../data/domain/world-5/sailing/sailing'; +import { Divinity } from '../../../data/domain/world-5/divinity'; import { Card } from '../../../data/domain/cards'; -import { Alchemy } from '../../../data/domain/alchemy'; +import { Alchemy } from '../../../data/domain/world-2/alchemy/alchemy'; import { Votes } from '../../../data/domain/world-2/votes'; import { Stamp } from '../../../data/domain/world-1/stamps'; -import { PlayerStatues } from '../../../data/domain/statues'; -import { Cooking } from '../../../data/domain/cooking'; -import { Rift, SkillMastery } from '../../../data/domain/rift'; -import { Worship, TotalizerBonus } from '../../../data/domain/worship'; +import { PlayerStatues } from '../../../data/domain/world-1/statues'; +import { Cooking } from '../../../data/domain/world-4/cooking'; +import { Rift, SkillMastery } from '../../../data/domain/world-4/rift'; +import { Worship, TotalizerBonus } from '../../../data/domain/world-3/worship'; import { StarSigns } from '../../../data/domain/starsigns'; import { SkillsIndex } from '../../../data/domain/SkillsIndex'; -import { SlabInfluencedArtifact } from '../../../data/domain/sailing/artifacts'; +import { SlabInfluencedArtifact } from '../../../data/domain/world-5/sailing/artifacts'; // TODO: Make it possible to test multiple save / extraction results. const saveName = 'latest'; diff --git a/tests/domains/summoning/win-bonus-parameters.test.ts b/tests/domains/summoning/win-bonus-parameters.test.ts new file mode 100644 index 00000000..78b0e6c8 --- /dev/null +++ b/tests/domains/summoning/win-bonus-parameters.test.ts @@ -0,0 +1,140 @@ +/** + * Summoning Winner Bonus Parameter Validation + * + * Tests our summoning winner bonus (WinBonus) calculations against live game data. + * + * Formula (from game code): + * WinBonus calculation has multiple branches based on bonus index: + * + * 1. Indices 20, 22, 24, 31: Return raw bonus value only + * bonusValue + * + * 2. Index 19: Uses 3.5x multiplier without recursive bonus 31 + * 3.5 * bonusValue * pristineCharm * (1 + gemPurchase/100) * + * (1 + (artifact + min(10, taskBonus) + achiev379 + achiev373 + godshardSet) / 100) + * + * 3. Indices 20-33 (excluding the raw-only ones): Uses all multipliers including recursive bonus 31 + * bonusValue * pristineCharm * (1 + gemPurchase/100) * + * (1 + (artifact + min(10, taskBonus) + achiev379 + achiev373 + winBonus31 + emperorBonus + godshardSet) / 100) + * + * 4. Default (all others): Uses 3.5x multiplier with all bonuses including recursive bonus 31 + * 3.5 * bonusValue * pristineCharm * (1 + gemPurchase/100) * + * (1 + (artifact + min(10, taskBonus) + achiev379 + achiev373 + winBonus31 + emperorBonus + godshardSet) / 100) + * + * Cross-domain dependencies: + * - Sneaking: Pristine charm bonus (Crystal Comb) + * - Sailing: Artifact 32 bonus + * - TaskBoard: Merit bonus (capped at 10) + * - Achievements: Completion status for 373 and 379 + * - EquipmentSets: Godshard set bonus + * - Emperor: Emperor bonus 8 + * - GemStore: Gem purchase 11 + */ + +import { loadExtractionResults, validateExtractionHealth, getExtractedValue } from '../../utils/live-game-data-loader'; +import { loadGameDataFromSave } from '../../utils/cloudsave-loader'; +import type { Sneaking } from '../../../data/domain/world-6/sneaking'; +import type { Sailing } from '../../../data/domain/world-5/sailing/sailing'; +import type { TaskBoard } from '../../../data/domain/tasks'; +import type { Achievement } from '../../../data/domain/achievements'; +import type { EquipmentSets } from '../../../data/domain/misc/equipmentSets'; +import type { Emperor } from '../../../data/domain/world-6/emperor'; +import type { GemStore } from '../../../data/domain/gemPurchases'; + +const saveName = 'latest'; +const extractionResultsName = 'summoning-win-bonus-data.json'; + +const parameterSpecs = { + pristine_charm_bonus_8: { + description: 'Pristine charm bonus 8 (Crystal Comb - summoning winner bonus)', + extractionKey: 'pristine_charm_bonus_8', + domainExtractor: (gameData: Map) => { + const sneaking = gameData.get("sneaking") as Sneaking; + const crystalComb = sneaking.pristineCharms?.find(charm => charm.data.itemId === 8); + return (crystalComb && crystalComb.unlocked) ? crystalComb.data.x1 : 0; + } + }, + + gem_item_purchased_11: { + description: 'Gem shop purchase 11 (Winner Bonuses)', + extractionKey: 'gem_item_purchased_11', + domainExtractor: (gameData: Map) => { + const gemStore = gameData.get("gems") as GemStore; + return gemStore.purchases.find(purchase => purchase.no === 11)?.pucrhased ?? 0; + } + }, + + artifact_bonus_32: { + description: 'Sailing artifact 32 bonus (summoning winner bonus)', + extractionKey: 'artifact_bonus_32', + domainExtractor: (gameData: Map) => { + const sailing = gameData.get("sailing") as Sailing; + return sailing.artifacts[32]?.getBonus() ?? 0; + } + }, + + task_board_bonus_2_5_4: { + description: 'Task board merit bonus for summoning winners (capped at 10 in formula)', + extractionKey: 'task_board_bonus_2_5_4', + domainExtractor: (gameData: Map) => { + const taskboard = gameData.get("taskboard") as TaskBoard; + // Merit 44 is index [2][5] in the game data structure + return taskboard.merits[44]?.getBonus() ?? 0; + } + }, + + achievement_379_status: { + description: 'Achievement 379 completion status (adds +1% if completed)', + extractionKey: 'achievement_379_status', + domainExtractor: (gameData: Map) => { + const achievements = gameData.get("achievements") as Achievement[]; + return achievements[379]?.completed ? 1 : 0; + } + }, + + achievement_373_status: { + description: 'Achievement 373 completion status (adds +1% if completed)', + extractionKey: 'achievement_373_status', + domainExtractor: (gameData: Map) => { + const achievements = gameData.get("achievements") as Achievement[]; + return achievements[373]?.completed ? 1 : 0; + } + }, + + godshard_set_bonus: { + description: 'Godshard equipment set bonus for summoning winners', + extractionKey: 'godshard_set_bonus', + domainExtractor: (gameData: Map) => { + const equipmentSets = gameData.get("equipmentSets") as EquipmentSets; + return equipmentSets.getSetBonus("GODSHARD_SET", undefined, true); + } + }, + + emperor_bonus_8: { + description: 'Emperor bonus 8 (summoning winner bonus)', + extractionKey: 'emperor_bonus_8', + domainExtractor: (gameData: Map) => { + const emperor = gameData.get("emperor") as Emperor; + return emperor.emperorBonuses[8]?.getBonus() ?? 0; + } + } +}; + +describe('Summoning Domain - Winner Bonus - Parameters', () => { + let extractionResults: any; + let gameData: Map; + + beforeAll(() => { + extractionResults = loadExtractionResults(extractionResultsName); + validateExtractionHealth(extractionResults); + gameData = loadGameDataFromSave(saveName); + }); + + Object.entries(parameterSpecs).forEach(([_, spec]) => { + it(`validates ${spec.description}`, () => { + const liveValue = getExtractedValue(extractionResults, spec.extractionKey); + const domainValue = spec.domainExtractor(gameData); + expect(domainValue).toMatchLiveGame(liveValue, 0); + }); + }); +}); diff --git a/tests/domains/summoning/win-bonus.test.ts b/tests/domains/summoning/win-bonus.test.ts new file mode 100644 index 00000000..f2ebb104 --- /dev/null +++ b/tests/domains/summoning/win-bonus.test.ts @@ -0,0 +1,74 @@ +/** + * Summoning Winner Bonus Calculation Validation + * + * Tests our Summoning winner bonus calculations against live game data. + */ + +import { loadExtractionResults, validateExtractionHealth, getExtractedValue } from '../../utils/live-game-data-loader'; +import { loadGameDataFromSave } from '../../utils/cloudsave-loader'; +import type { Summoning } from '../../../data/domain/world-6/summoning'; + +const saveName = 'latest'; +const extractionResultsName = 'summoning-win-bonus-data.json'; + +describe('Summoning Domain - Winner Bonus Calculations', () => { + let extractionResults: any; + let gameData: Map; + let summoning: Summoning; + + beforeAll(() => { + extractionResults = loadExtractionResults(extractionResultsName); + validateExtractionHealth(extractionResults); + gameData = loadGameDataFromSave(saveName); + summoning = gameData.get("summoning") as Summoning; + }); + + it('validates winner bonus 19 (3.5x multiplier, no recursive bonus)', () => { + const liveValue = getExtractedValue(extractionResults, 'win_bonus_19'); + const domainValue = summoning.summonBonuses.find(bonus => bonus.index === 19)?.getBonus() ?? 0; + + expect(domainValue).toMatchLiveGame(liveValue, 0); + }); + + it('validates winner bonus 20 (raw value only, no multipliers)', () => { + const liveValue = getExtractedValue(extractionResults, 'win_bonus_20'); + const domainValue = summoning.summonBonuses.find(bonus => bonus.index === 20)?.getBonus() ?? 0; + + expect(domainValue).toMatchLiveGame(liveValue, 0); + }); + + it('validates winner bonus 22 (raw value only, no multipliers)', () => { + const liveValue = getExtractedValue(extractionResults, 'win_bonus_22'); + const domainValue = summoning.summonBonuses.find(bonus => bonus.index === 22)?.getBonus() ?? 0; + + expect(domainValue).toMatchLiveGame(liveValue, 0); + }); + + it('validates winner bonus 24 (raw value only, no multipliers)', () => { + const liveValue = getExtractedValue(extractionResults, 'win_bonus_24'); + const domainValue = summoning.summonBonuses.find(bonus => bonus.index === 24)?.getBonus() ?? 0; + + expect(domainValue).toMatchLiveGame(liveValue, 0); + }); + + it('validates winner bonus 26 (Meal bonus - uses all multipliers + bonus 31)', () => { + const liveValue = getExtractedValue(extractionResults, 'win_bonus_26'); + const domainValue = summoning.summonBonuses.find(bonus => bonus.index === 26)?.getBonus() ?? 0; + + expect(domainValue).toMatchLiveGame(liveValue, 0); + }); + + it('validates winner bonus 31 (raw value only - used in other calculations)', () => { + const liveValue = getExtractedValue(extractionResults, 'win_bonus_31'); + const domainValue = summoning.summonBonuses.find(bonus => bonus.index === 31)?.getBonus() ?? 0; + + expect(domainValue).toMatchLiveGame(liveValue, 0); + }); + + it('validates winner bonus 5 (3.5x multiplier with all bonuses including recursive bonus 31)', () => { + const liveValue = getExtractedValue(extractionResults, 'win_bonus_5'); + const domainValue = summoning.summonBonuses.find(bonus => bonus.index === 5)?.getBonus() ?? 0; + + expect(domainValue).toMatchLiveGame(liveValue, 0); + }); +}); diff --git a/tests/results/summoning-win-bonus-data.json b/tests/results/summoning-win-bonus-data.json new file mode 100644 index 00000000..c1055284 --- /dev/null +++ b/tests/results/summoning-win-bonus-data.json @@ -0,0 +1,104 @@ +{ + "timestamp": "2026-02-12T12:42:13.696Z", + "extractions": { + "pristine_charm_bonus_8": { + "expression": "idleon.callFunction(\"Ninja\", \"PristineBon\", 8, 0)", + "result": 30, + "description": "Pristine charm bonus for summoning (index 8 - Crystal Comb)", + "extractedAt": "2026-02-12T12:42:13.744Z" + }, + "gem_item_purchased_11": { + "expression": "idleon.engine.getGameAttribute(\"GemItemsPurchased\")[11]", + "result": 5, + "description": "Gem shop purchase 11 (Winner Bonuses)", + "extractedAt": "2026-02-12T12:42:13.748Z" + }, + "artifact_bonus_32": { + "expression": "idleon.callFunction(\"Sailing\", \"ArtifactBonus\", 32, 0)", + "result": 100, + "description": "Sailing artifact 32 bonus (summoning winner bonus)", + "extractedAt": "2026-02-12T12:42:13.755Z" + }, + "task_board_bonus_2_5_4": { + "expression": "idleon.engine.getGameAttribute(\"Tasks\")[2][5][4]", + "result": 10, + "description": "Task board merit bonus (max 10) for summoning winners", + "extractedAt": "2026-02-12T12:42:13.759Z" + }, + "achievement_379_status": { + "expression": "idleon.callFunction(\"AchieveStatus\", 379)", + "result": 1, + "description": "Achievement 379 completion status (winner bonus +1%)", + "extractedAt": "2026-02-12T12:42:13.762Z" + }, + "achievement_373_status": { + "expression": "idleon.callFunction(\"AchieveStatus\", 373)", + "result": 1, + "description": "Achievement 373 completion status (winner bonus +1%)", + "extractedAt": "2026-02-12T12:42:13.765Z" + }, + "godshard_set_bonus": { + "expression": "idleon.callFunction(\"GetSetBonus\", \"GODSHARD_SET\", \"Bonus\", 0, 0)", + "result": 0, + "description": "Godshard equipment set bonus for summoning winners", + "extractedAt": "2026-02-12T12:42:13.773Z" + }, + "emperor_bonus_8": { + "expression": "idleon.callFunction(\"Thingies\", \"EmperorBon\", 8, 0)", + "result": 1, + "description": "Emperor bonus 8 (summoning winner bonus)", + "extractedAt": "2026-02-12T12:42:13.776Z" + }, + "win_bonus_19": { + "expression": "idleon.callFunction(\"Summoning\", \"WinBonus\", 19, 0)", + "result": 43.407000000000004, + "description": "Winner bonus 19 (uses 3.5x multiplier, no recursive bonus)", + "extractedAt": "2026-02-12T12:42:13.779Z" + }, + "win_bonus_20": { + "expression": "idleon.callFunction(\"Summoning\", \"WinBonus\", 20, 0)", + "result": 3, + "description": "Winner bonus 20 (raw value only, no multipliers)", + "extractedAt": "2026-02-12T12:42:13.791Z" + }, + "win_bonus_22": { + "expression": "idleon.callFunction(\"Summoning\", \"WinBonus\", 22, 0)", + "result": 10, + "description": "Winner bonus 22 (raw value only, no multipliers)", + "extractedAt": "2026-02-12T12:42:13.794Z" + }, + "win_bonus_24": { + "expression": "idleon.callFunction(\"Summoning\", \"WinBonus\", 24, 0)", + "result": 6, + "description": "Winner bonus 24 (raw value only, no multipliers)", + "extractedAt": "2026-02-12T12:42:13.796Z" + }, + "win_bonus_26": { + "expression": "idleon.callFunction(\"Summoning\", \"WinBonus\", 26, 0)", + "result": 98.2215, + "description": "Winner bonus 26 (Meal bonus - uses all multipliers + bonus 31)", + "extractedAt": "2026-02-12T12:42:13.806Z" + }, + "win_bonus_31": { + "expression": "idleon.callFunction(\"Summoning\", \"WinBonus\", 31, 0)", + "result": 6, + "description": "Winner bonus 31", + "extractedAt": "2026-02-12T12:42:13.809Z" + }, + "win_bonus_5": { + "expression": "idleon.callFunction(\"Summoning\", \"WinBonus\", 5, 0)", + "result": 1972.9710000000002, + "description": "Winner bonus 5 (uses 3.5x multiplier and all bonuses including recursive bonus 31)", + "extractedAt": "2026-02-12T12:42:13.811Z" + } + }, + "errors": {}, + "metadata": { + "serverStatus": { + "server": "running", + "cdpConnected": true, + "injected": true, + "gameReady": true + } + } +} \ No newline at end of file