diff --git a/Pictures/Default/patchnotes.png b/Pictures/Default/patchnotes.png index 6a733a0de..7bbec33d5 100644 Binary files a/Pictures/Default/patchnotes.png and b/Pictures/Default/patchnotes.png differ diff --git a/Pictures/Jar of PseudoCoins.png b/Pictures/Jar of PseudoCoins.png new file mode 100644 index 000000000..b12578790 Binary files /dev/null and b/Pictures/Jar of PseudoCoins.png differ diff --git a/Pictures/Jug of PseudoCoins.png b/Pictures/Jug of PseudoCoins.png new file mode 100644 index 000000000..c58dadc82 Binary files /dev/null and b/Pictures/Jug of PseudoCoins.png differ diff --git a/Pictures/Legacy/patchnotes.png b/Pictures/Legacy/patchnotes.png index 6a733a0de..7bbec33d5 100644 Binary files a/Pictures/Legacy/patchnotes.png and b/Pictures/Legacy/patchnotes.png differ diff --git a/Pictures/Magic Pot of PseudoCoins.png b/Pictures/Magic Pot of PseudoCoins.png new file mode 100644 index 000000000..edcac92aa Binary files /dev/null and b/Pictures/Magic Pot of PseudoCoins.png differ diff --git a/Pictures/Monotonous/patchnotes.png b/Pictures/Monotonous/patchnotes.png index 6a733a0de..7bbec33d5 100644 Binary files a/Pictures/Monotonous/patchnotes.png and b/Pictures/Monotonous/patchnotes.png differ diff --git a/Pictures/Piggy Bank of PseudoCoins.png b/Pictures/Piggy Bank of PseudoCoins.png new file mode 100644 index 000000000..244438465 Binary files /dev/null and b/Pictures/Piggy Bank of PseudoCoins.png differ diff --git a/Pictures/PseudoCoin border.png b/Pictures/PseudoCoin border.png new file mode 100644 index 000000000..ad46d140a Binary files /dev/null and b/Pictures/PseudoCoin border.png differ diff --git a/Pictures/PseudoCoinAscentUnlock.png b/Pictures/PseudoCoinAscentUnlock.png new file mode 100644 index 000000000..cd4b63eb4 Binary files /dev/null and b/Pictures/PseudoCoinAscentUnlock.png differ diff --git a/Pictures/PseudoCoinCalculator.png b/Pictures/PseudoCoinCalculator.png new file mode 100644 index 000000000..def00b666 Binary files /dev/null and b/Pictures/PseudoCoinCalculator.png differ diff --git a/Pictures/PseudoCoinCubes.png b/Pictures/PseudoCoinCubes.png new file mode 100644 index 000000000..54bc48a74 Binary files /dev/null and b/Pictures/PseudoCoinCubes.png differ diff --git a/Pictures/PseudoCoinExtraOffline.png b/Pictures/PseudoCoinExtraOffline.png new file mode 100644 index 000000000..0ca731a49 Binary files /dev/null and b/Pictures/PseudoCoinExtraOffline.png differ diff --git a/Pictures/PseudoCoinFreeDailyImprovement.png b/Pictures/PseudoCoinFreeDailyImprovement.png new file mode 100644 index 000000000..20ae9c4cd Binary files /dev/null and b/Pictures/PseudoCoinFreeDailyImprovement.png differ diff --git a/Pictures/PseudoCoinGeneration.png b/Pictures/PseudoCoinGeneration.png new file mode 100644 index 000000000..6837442ab Binary files /dev/null and b/Pictures/PseudoCoinGeneration.png differ diff --git a/Pictures/PseudoCoinGoldenQuark.png b/Pictures/PseudoCoinGoldenQuark.png new file mode 100644 index 000000000..5f8e9e809 Binary files /dev/null and b/Pictures/PseudoCoinGoldenQuark.png differ diff --git a/Pictures/PseudoCoinLuck.png b/Pictures/PseudoCoinLuck.png new file mode 100644 index 000000000..d254cb4e7 Binary files /dev/null and b/Pictures/PseudoCoinLuck.png differ diff --git a/Pictures/PseudoCoinPotion.png b/Pictures/PseudoCoinPotion.png new file mode 100644 index 000000000..e81d4035b Binary files /dev/null and b/Pictures/PseudoCoinPotion.png differ diff --git a/Pictures/PseudoCoinTalismanUnlock.png b/Pictures/PseudoCoinTalismanUnlock.png new file mode 100644 index 000000000..9224ae2a6 Binary files /dev/null and b/Pictures/PseudoCoinTalismanUnlock.png differ diff --git a/Pictures/PseudoShop/ADD_CODE_CAP_BUFF.png b/Pictures/PseudoShop/ADD_CODE_CAP_BUFF.png new file mode 100644 index 000000000..def00b666 Binary files /dev/null and b/Pictures/PseudoShop/ADD_CODE_CAP_BUFF.png differ diff --git a/Pictures/PseudoShop/AMBROSIA_GENERATION_BUFF.png b/Pictures/PseudoShop/AMBROSIA_GENERATION_BUFF.png new file mode 100644 index 000000000..6837442ab Binary files /dev/null and b/Pictures/PseudoShop/AMBROSIA_GENERATION_BUFF.png differ diff --git a/Pictures/PseudoShop/AMBROSIA_LOADOUT_SLOT_QOL.png b/Pictures/PseudoShop/AMBROSIA_LOADOUT_SLOT_QOL.png new file mode 100644 index 000000000..d633b0b9f Binary files /dev/null and b/Pictures/PseudoShop/AMBROSIA_LOADOUT_SLOT_QOL.png differ diff --git a/Pictures/PseudoShop/AMBROSIA_LUCK_BUFF.png b/Pictures/PseudoShop/AMBROSIA_LUCK_BUFF.png new file mode 100644 index 000000000..d254cb4e7 Binary files /dev/null and b/Pictures/PseudoShop/AMBROSIA_LUCK_BUFF.png differ diff --git a/Pictures/PseudoShop/AUTO_POTION_FREE_POTIONS_QOL.png b/Pictures/PseudoShop/AUTO_POTION_FREE_POTIONS_QOL.png new file mode 100644 index 000000000..e81d4035b Binary files /dev/null and b/Pictures/PseudoShop/AUTO_POTION_FREE_POTIONS_QOL.png differ diff --git a/Pictures/PseudoShop/CORRUPTION_LOADOUT_SLOT_QOL.png b/Pictures/PseudoShop/CORRUPTION_LOADOUT_SLOT_QOL.png new file mode 100644 index 000000000..f0f997555 Binary files /dev/null and b/Pictures/PseudoShop/CORRUPTION_LOADOUT_SLOT_QOL.png differ diff --git a/Pictures/PseudoShop/CUBE_BUFF.png b/Pictures/PseudoShop/CUBE_BUFF.png new file mode 100644 index 000000000..54bc48a74 Binary files /dev/null and b/Pictures/PseudoShop/CUBE_BUFF.png differ diff --git a/Pictures/PseudoShop/FREE_UPGRADE_PROMOCODE_BUFF.png b/Pictures/PseudoShop/FREE_UPGRADE_PROMOCODE_BUFF.png new file mode 100644 index 000000000..20ae9c4cd Binary files /dev/null and b/Pictures/PseudoShop/FREE_UPGRADE_PROMOCODE_BUFF.png differ diff --git a/Pictures/PseudoShop/GOLDEN_QUARK_BUFF.png b/Pictures/PseudoShop/GOLDEN_QUARK_BUFF.png new file mode 100644 index 000000000..5f8e9e809 Binary files /dev/null and b/Pictures/PseudoShop/GOLDEN_QUARK_BUFF.png differ diff --git a/Pictures/PseudoShop/INSTANT_UNLOCK_1.png b/Pictures/PseudoShop/INSTANT_UNLOCK_1.png new file mode 100644 index 000000000..9224ae2a6 Binary files /dev/null and b/Pictures/PseudoShop/INSTANT_UNLOCK_1.png differ diff --git a/Pictures/PseudoShop/INSTANT_UNLOCK_2.png b/Pictures/PseudoShop/INSTANT_UNLOCK_2.png new file mode 100644 index 000000000..cd4b63eb4 Binary files /dev/null and b/Pictures/PseudoShop/INSTANT_UNLOCK_2.png differ diff --git a/Pictures/PseudoShop/OFFLINE_TIMER_CAP_BUFF.png b/Pictures/PseudoShop/OFFLINE_TIMER_CAP_BUFF.png new file mode 100644 index 000000000..61b5383c1 Binary files /dev/null and b/Pictures/PseudoShop/OFFLINE_TIMER_CAP_BUFF.png differ diff --git a/Pictures/PseudoShop/PseudoCoin border.png b/Pictures/PseudoShop/PseudoCoin border.png new file mode 100644 index 000000000..ad46d140a Binary files /dev/null and b/Pictures/PseudoShop/PseudoCoin border.png differ diff --git a/Pictures/PseudoShop/PseudoCoins.png b/Pictures/PseudoShop/PseudoCoins.png new file mode 100644 index 000000000..852eb489f Binary files /dev/null and b/Pictures/PseudoShop/PseudoCoins.png differ diff --git a/Pictures/Roll of PseudoCoins.png b/Pictures/Roll of PseudoCoins.png new file mode 100644 index 000000000..464e0d620 Binary files /dev/null and b/Pictures/Roll of PseudoCoins.png differ diff --git a/Pictures/Simplified/patchnotes.png b/Pictures/Simplified/patchnotes.png index 6a733a0de..7bbec33d5 100644 Binary files a/Pictures/Simplified/patchnotes.png and b/Pictures/Simplified/patchnotes.png differ diff --git a/Pictures/cart.png b/Pictures/cart.png new file mode 100644 index 000000000..f1dae0d05 Binary files /dev/null and b/Pictures/cart.png differ diff --git a/Synergism.css b/Synergism.css index fac2fd532..3094e93f9 100644 --- a/Synergism.css +++ b/Synergism.css @@ -93,6 +93,7 @@ body { --buildings-hover-color: #666; --blessings-canbuy-color: #222; --blessings-hover-color: #444; + --darkgold-color: #b59410; --tab-color: #171717; --singtab-color: black; --buttonimg-color: black; @@ -201,8 +202,8 @@ button { outline: none; } -body button:hover, -body button:active { +body button:hover:not(:disabled), +body button:active:not(:disabled) { background-color: var(--hover-color); } @@ -477,7 +478,8 @@ body button:active { font-size: min(1em, 20px); } -.offlineStats { +.offlineStats, +.cartList { display: flex; flex-direction: column; z-index: 3; @@ -491,7 +493,8 @@ body button:active { margin-bottom: 2px; } -.offlineStatElement { +.offlineStatElement, +.cartListElement { margin-left: 10px; } @@ -507,7 +510,9 @@ body button:active { margin: 5px 0; } -#exitOffline { +#exitOffline, +#closeCart, +#checkout { width: 18em; min-height: auto; min-width: 220px; @@ -519,7 +524,9 @@ body button:active { color: white; } -#exitOffline:hover { +#exitOffline:hover, +#closeCart:hover, +#checkout:not(:disabled):hover { background-color: var(--purplehover-color); } @@ -2662,6 +2669,7 @@ p#ascendHotKeys { background-color: #fff4b3; } */ +.subtabSwitcher > * > button.buttonActive, .subtabSwitcher > button.buttonActive { background-color: crimson; } @@ -3936,6 +3944,13 @@ img#singularityPerksIcon { font-weight: bold; } +#bbLoadoutContainer { + display: flex; + flex-direction: row; + align-items: center; + margin-top: 10px; +} + form input:hover { background-color: var(--hover-color); cursor: pointer; @@ -3949,3 +3964,239 @@ form input:hover { margin-right: auto; margin-top: 5px; } + +.pseudoUpgradeContainer { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + justify-items: center; + width: 70%; + margin: 0 auto; +} + +.pseudoCoinContainer { + margin: 10px 10px 0 0; + border: 1px solid #ccc; + border: 2px solid orchid; + text-align: center; +} + +#pseudoCoins > #productContainer > section > * > .pseudoCoinImage { + margin-top: 8px; +} + +.pseudoCoinText { + width: 90%; + height: 80px; + padding: 10px; + box-sizing: border-box; + text-align: center; + box-shadow: 0 0 10px rgb(0 0 0 / 10%); + border: 1px solid #ccc; + margin: 5px auto; +} + +.pseudoCoinButton { + width: 50%; + padding: 10px; + background-color: black; + color: white; + cursor: pointer; + border: 2px solid white; + font-size: 1.2em; + word-wrap: break-word; + margin-bottom: 5px; +} + +.pseudoCoinButton:hover { + background-color: white; + color: black; +} + +#cart { + width: fit-content; + margin: 5px auto 0; + border: 2px solid white; + border-radius: 5px; +} + +#cart > img { + width: 24px; + height: 24px; +} + +#totalCost { + margin: 0 auto; +} + +#closeCart, +#checkout { + margin: 2px auto 4px; +} + +.cartListElementContainer > * { + width: 5vh; +} + +.cartListElementContainer > button { + min-height: unset; + height: 50%; + border: 2px solid green; + border-radius: 2px; +} + +#itemList { + display: flex; + flex-wrap: wrap; + flex-direction: column; +} + +.cartListElementContainer { + display: flex; +} + +.cartListElement { + width: 30vh; +} + +.cartListElementContainer > span:first-of-type { + width: 80%; +} + +#upgradesContainer { + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +#cartContainer { + display: flex; + width: 70vh; + justify-content: center; + margin: 0 auto; + margin-top: 10px; +} + +#upgradeGrid { + display: grid; + grid-template-columns: repeat(6, 15%); + grid-template-rows: repeat(2, 15%); + column-gap: 10px; + row-gap: 64px; + margin: 0; + width: 38%; + padding-top: 15px; + max-height: 192px; +} + +#upgradeGrid > div { + position: relative; + height: 64px; + width: 64px; + background: #222; + place-self: center; +} + +#upgradeGrid > div > p { + margin: 0; + position: absolute; + bottom: 0; +} + +#upgradeGrid > div > p:nth-child(odd) { + margin: 0; + position: absolute; + top: 0; + right: 0; +} + +#upgradeGrid > div > img { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + border-radius: 50%; + width: 70%; +} + +#upgradeGrid > div[data-id].active { + outline: 2px solid white; +} + +#pseudoCoinAmounts { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + margin: 10px; +} + +#pseudoCoinAmounts > img { + max-height: 32px; + max-width: 32px; +} + +#upgradeInfo { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin: 10px auto; + width: 50%; + border: 3px solid var(--darkgold-color); +} + +#pseudoCoinUpgradeName { + background-color: var(--darkgold-color); + width: 100%; + text-align: center; + font-size: 0.8em; +} + +#pseudoCoinUpgradeDescription { + display: grid; + height: 128px; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: 128px; + width: 100%; +} + +#pseudoCoinUpgradeIcon { + grid-column: 1; + margin: auto; +} + +#pseudoCoinUpgradeTexts { + display: grid; + grid-template-rows: repeat(3, 1fr); + grid-column: 2 / 4; + margin: auto; + width: 100%; + height: 100%; + align-items: center; +} + +#pCoinUpgradeDesc { + grid-row: 1 / 3; + text-align: center; + margin: auto; + width: 100%; +} + +#pCoinUpgradeDesc > p { + margin: 0; +} + +#pCoinButton { + grid-row: 3; + margin: 0 auto; + width: 100%; +} + +#buy { + width: 100%; + border: 2px solid gold; + font-size: 1.2em; +} diff --git a/index.html b/index.html index 2ee94b0b6..916cfc3db 100644 --- a/index.html +++ b/index.html @@ -3024,7 +3024,7 @@

Current Bonus: 0%

- + Patch Notes Icon
@@ -3372,6 +3372,7 @@

Artists

--

--

--

+

--

TOTAL GLOBAL CUBE MULTIPLIER: 0

@@ -3483,6 +3484,7 @@

Artists

Golden Revolution II:0

Immaculate Alchemy:0

Total Quarks Coefficient:0

+

Total Quarks Coefficient:0

TOTAL GOLDEN QUARK MULTIPLIER: 0

@@ -3562,6 +3564,7 @@

Artists

PL-AT _:0

Singularity factor:0

+

Singularity factor:0

 

@@ -3587,13 +3590,14 @@

Artists

a0

a0

a0

+

a0

Multipliers (Additive)0

TOTAL AMBROSIA LUCK: 0

-

Ambrosia Generation Chance

+

Ambrosia Generation Speed

a0

a0

a0

@@ -3604,6 +3608,7 @@

Artists

a0

a0

a0

+

a0

BLUEBERRY TIME /S: 0

@@ -4592,6 +4597,14 @@

Artists

+ + + + + + + +
@@ -4606,5 +4619,84 @@

Artists

+ +
+
+ + + + + + +
+
+
+
+
+ PseudoCoins +

+
+
+
+
+
+

Welcome to the PseudoShop!

+
+
+
+ Upgrade Icon +
+
+
+

Buy PseudoCoin upgrades, get exclusive bonuses and support development! ❤

+

Click on an icon to see what you can get!

+

Note: you must be logged in to buy or use these!

+
+
+ +
+
+
+
+
+
+
+
+
+ +

+ +
+ + +
+ + + +
+
+
diff --git a/package.json b/package.json index 919689051..ccebdfed0 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,8 @@ }, "scripts": { "lint": "npx @biomejs/biome lint .", - "lint:fix": "npx @biomejs/biome lint --apply .", - "lint:fix-unsafe": "npx @biomejs/biome lint --apply-unsafe .", + "lint:fix": "npx @biomejs/biome lint --write .", + "lint:fix-unsafe": "npx @biomejs/biome lint --write --unsafe .", "fmt": "npx dprint fmt", "format": "npx dprint fmt", "csslint": "npx stylelint Synergism.css", diff --git a/src/BlueberryUpgrades.ts b/src/BlueberryUpgrades.ts index 56c889bdd..bb18c0bfc 100644 --- a/src/BlueberryUpgrades.ts +++ b/src/BlueberryUpgrades.ts @@ -4,6 +4,7 @@ import { calculateAmbrosiaGenerationSpeed, calculateAmbrosiaLuck } from './Calcu import { DynamicUpgrade } from './DynamicUpgrade' import type { IUpgradeData } from './DynamicUpgrade' import { exportData, saveFilename } from './ImportExport' +import { PCoinUpgradeEffects } from './PseudoCoinUpgrades' import { getQuarkBonus } from './Quark' import { format, player } from './Synergism' import type { Player } from './types/Synergism' @@ -759,6 +760,20 @@ export const blueberryUpgradeData: Record< } } +export const displayProperLoadoutCount = () => { + const loadoutCount = 8 + PCoinUpgradeEffects.AMBROSIA_LOADOUT_SLOT_QOL + if (loadoutCount < 16) { + for (let i = 1; i <= 16; i++) { + const elm = DOMCacheGetOrSet(`blueberryLoadout${i}`) + if (i <= loadoutCount) { + elm.style.display = 'flex' + } else { + elm.style.display = 'none' + } + } + } +} + export const resetBlueberryTree = async (giveAlert = true) => { for (const upgrade of Object.keys(player.blueberryUpgrades)) { const k = upgrade as keyof Player['blueberryUpgrades'] @@ -990,3 +1005,14 @@ export const createLoadoutDescription = ( DOMCacheGetOrSet('singularityAmbrosiaMultiline').innerHTML = ` ${loadoutTitle} ${str}` } + +export const updateBlueberryLoadoutCount = () => { + const maxLoadouts = 16 + const loadoutCount = Object.keys(player.blueberryLoadouts).length + + if (loadoutCount < maxLoadouts) { + for (let i = loadoutCount + 1; i <= maxLoadouts; i++) { + player.blueberryLoadouts[i] = {} + } + } +} diff --git a/src/Cache/DOM.ts b/src/Cache/DOM.ts index 14d667b78..b15f013e0 100644 --- a/src/Cache/DOM.ts +++ b/src/Cache/DOM.ts @@ -18,3 +18,5 @@ export const DOMCacheGetOrSet = (id: string) => { DOMCache.set(id, el) return el } + +export const DOMCacheHas = (id: string) => DOMCache.has(id) diff --git a/src/Calculate.ts b/src/Calculate.ts index 646c30970..7db3c0485 100644 --- a/src/Calculate.ts +++ b/src/Calculate.ts @@ -7,6 +7,7 @@ import { BuffType, calculateEventSourceBuff } from './Event' import { addTimers, automaticTools } from './Helper' import { hepteractEffective } from './Hepteracts' import { disableHotkeys, enableHotkeys } from './Hotkeys' +import { PCoinUpgradeEffects } from './PseudoCoinUpgrades' import { getQuarkBonus, quarkHandler } from './Quark' import { reset } from './Reset' import { calculateSingularityDebuff } from './singularity' @@ -342,10 +343,12 @@ export const calculateMaxRunes = (i: number) => { } export const calculateEffectiveIALevel = () => { + const bonus = PCoinUpgradeEffects.INSTANT_UNLOCK_2 ? 6 : 0 return ( player.runelevels[5] + Math.max(0, player.runelevels[5] - 74) + Math.max(0, player.runelevels[5] - 98) + + bonus ) } @@ -1362,9 +1365,11 @@ export const calculateOffline = async (forceTime = 0) => { G.timeWarp = true // Variable Declarations i guess - const maximumTimer = 86400 * 3 + const maximumTimer = (86400 * 3 + 7200 * 2 * player.researches[31] - + 7200 * 2 * player.researches[32] + + 7200 * 2 * player.researches[32]) + * PCoinUpgradeEffects.OFFLINE_TIMER_CAP_BUFF + const updatedTime = Date.now() const timeAdd = Math.min( maximumTimer, @@ -1705,6 +1710,8 @@ export const calculateAllCubeMultiplier = () => { exaltPenalty = calculateExalt6Penalty(comps, time) } const arr = [ + // Pseudocoin Multiplier + PCoinUpgradeEffects.CUBE_BUFF, // Ascension Time Multiplier to cubes Math.pow(Math.min(1, player.ascensionCounter / 10), 2) * (1 @@ -2084,6 +2091,7 @@ export const calculateHepteractMultiplier = (score = -1) => { export const getOcteractValueMultipliers = () => { const corruptionLevelSum = sumContents(player.usedCorruptions.slice(2, 10)) return [ + PCoinUpgradeEffects.CUBE_BUFF, 1 + (1.5 * player.shopUpgrades.seasonPass3) / 100, 1 + (0.75 * player.shopUpgrades.seasonPassY) / 100, 1 + (player.shopUpgrades.seasonPassZ * player.singularityCount) / 100, @@ -2424,8 +2432,8 @@ export const calculateQuarkMultiplier = () => { // Challenge 15: Exceed 1e11 exponent reward multiplier += G.challenge15Rewards.quarks - 1 } - if (player.shopUpgrades.infiniteAscent) { - // Purchased Infinite Ascent Rune + if (isIARuneUnlocked()) { + // Purchased Infinite Ascent Rune (or corresponding PCoin Upgrade) multiplier *= 1.1 + (0.15 / 75) * calculateEffectiveIALevel() } if (player.challenge15Exponent >= 1e15) { @@ -2521,6 +2529,7 @@ export const calculateGoldenQuarkMultiplier = (computeMultiplier = false) => { } const arr = [ + PCoinUpgradeEffects.GOLDEN_QUARK_BUFF, // Golden Quark Buff from PseudoCoins 1 + Math.max(0, Math.log10(player.challenge15Exponent + 1) - 20) / 2, // Challenge 15 Exponent 1 + getQuarkBonus() / 100, // Patreon Bonus +player.singularityUpgrades.goldenQuarks1.getEffect().bonus, // Golden Quarks I @@ -3349,6 +3358,7 @@ export const calculateAdditiveLuckMult = () => { export const calculateAmbrosiaLuck = () => { const arr = [ 100, // Base + PCoinUpgradeEffects.AMBROSIA_LUCK_BUFF, // Platonic Coin Upgrade calculateSingularityAmbrosiaLuckMilestoneBonus(), // Ambrosia Luck Milestones calculateAmbrosiaLuckShopUpgrade(), // Ambrosia Luck from Shop Upgrades (I-IV) calculateAmbrosiaLuckSingularityUpgrade(), // Ambrosia Luck from Singularity Upgrades (I-IV) @@ -3393,6 +3403,7 @@ export const calculateBlueberryInventory = () => { export const calculateAmbrosiaGenerationSpeed = () => { const arr = [ +(player.visitedAmbrosiaSubtab), + PCoinUpgradeEffects.AMBROSIA_GENERATION_BUFF, calculateBlueberryInventory().value, calculateAmbrosiaGenerationShopUpgrade(), calculateAmbrosiaGenerationSingularityUpgrade(), @@ -3498,6 +3509,14 @@ export const derpsmithCornucopiaBonus = () => { return 1 + (counter * player.highestSingularityCount) / 100 } +export const isIARuneUnlocked = () => { + return player.shopUpgrades.infiniteAscent > 0 || PCoinUpgradeEffects.INSTANT_UNLOCK_2 +} + +export const isShopTalismanUnlocked = () => { + return player.shopUpgrades.shopTalisman > 0 || PCoinUpgradeEffects.INSTANT_UNLOCK_1 +} + export const sing6Mult = () => { if (player.singularityCount <= 200) { return 1 diff --git a/src/Config.ts b/src/Config.ts index 26042807e..2980aa70a 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -1,4 +1,4 @@ -export const version = '3.0.0 pt 6: December 21, 2024: Rise of the Singular Entities, pt 1.0.4' +export const version = '3.1.0 January 3, 2025: The PseudoCoin Update' /** * PSEUDO DO NOT CHANGE THIS LINE diff --git a/src/Corruptions.ts b/src/Corruptions.ts index 64247efbd..8c524a201 100644 --- a/src/Corruptions.ts +++ b/src/Corruptions.ts @@ -1,5 +1,6 @@ import i18next from 'i18next' import { DOMCacheGetOrSet } from './Cache/DOM' +import { PCoinUpgradeEffects } from './PseudoCoinUpgrades' import { format, player } from './Synergism' import { IconSets } from './Themes' import { toggleCorruptionLevel } from './Toggles' @@ -213,7 +214,8 @@ export const corruptionLoadoutTableCreate = () => { table.deleteRow(i) } - for (let i = 0; i < Object.keys(player.corruptionLoadouts).length + 1; i++) { + const totalSlots = 8 + PCoinUpgradeEffects.CORRUPTION_LOADOUT_SLOT_QOL + for (let i = 0; i < totalSlots + 1; i++) { const row = table.insertRow() for (let j = 0; j <= corrCount; j++) { const cell = row.insertCell() @@ -349,14 +351,15 @@ async function corruptionLoadoutGetNewName (loadout = 0) { player.corruptionLoadoutNames[loadout] = renamePrompt updateCorruptionLoadoutNames() if (renamePrompt === 'crazy') { - return Alert(i18next.t('corruptions.loadoutPrompt.errors.crazyJoke')) + return Alert(i18next.t('corruptions.corruptionLoadoutName.errors.crazyJoke')) } } } export const updateCorruptionLoadoutNames = () => { const rows = getElementById('corruptionLoadoutTable').rows - for (let i = 0; i < Object.keys(player.corruptionLoadouts).length; i++) { + const totalSlots = 8 + PCoinUpgradeEffects.CORRUPTION_LOADOUT_SLOT_QOL + for (let i = 0; i < totalSlots; i++) { const cells = rows[i + 2].cells // start changes on 2nd row if (cells[0].textContent!.length === 0) { // first time setup cells[0].addEventListener('click', () => void corruptionLoadoutGetNewName(i)) // get name function handles -1 for array @@ -376,6 +379,21 @@ const corruptionLoadoutGetExport = async () => { } } +export const updateUndefinedLoadouts = () => { + // Sanity checks that you have 16 loadouts and 16 loadout names in the Player object + // The monetization update adds more loadouts, so this is to ensure that the player object is up to date + // And because the validation schema does not take into account length of object + + const maxLoadoutCount = 16 // Update if more loadouts are added + const currLoadoutCount = Object.keys(player.corruptionLoadouts).length + if (currLoadoutCount < maxLoadoutCount) { + for (let i = currLoadoutCount + 1; i <= maxLoadoutCount; i++) { + player.corruptionLoadouts[i] = Array(13).fill(0) + player.corruptionLoadoutNames.push(`Loadout ${i}`) + } + } +} + export const corruptionCleanseConfirm = () => { const corrupt = DOMCacheGetOrSet('corruptionCleanseConfirm') corrupt.style.visibility = 'visible' diff --git a/src/ImportExport.ts b/src/ImportExport.ts index 5f5ed751f..caf204332 100644 --- a/src/ImportExport.ts +++ b/src/ImportExport.ts @@ -8,6 +8,7 @@ import { octeractGainPerSecond } from './Calculate' import { testing, version } from './Config' import { Synergism } from './Events' import { addTimers } from './Helper' +import { PCoinUpgradeEffects } from './PseudoCoinUpgrades' import { getQuarkBonus, quarkHandler } from './Quark' import { playerJsonSchema } from './saves/PlayerJsonSchema' import { shopData } from './Shop' @@ -589,6 +590,8 @@ export const promocodes = async (input: string | null, amount?: number) => { rolls *= 2 } + rolls *= PCoinUpgradeEffects.FREE_UPGRADE_PROMOCODE_BUFF + rolls = Math.floor(rolls) const keys = Object.keys(player.singularityUpgrades).filter( @@ -1039,6 +1042,7 @@ export const addCodeMaxUses = () => { ] let maxUses = sumContents(arr) + maxUses *= PCoinUpgradeEffects.ADD_CODE_CAP_BUFF arr.push(addCodeSingularityPerkBonus()) maxUses *= addCodeSingularityPerkBonus() diff --git a/src/Login.ts b/src/Login.ts index 7488759ea..538d6a76f 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -25,6 +25,9 @@ const SMITH_GOD = '1045562390995009606' const GOLDEN_SMITH_GOD = '1178125584061173800' const DIAMOND_SMITH_MESSIAH = '1311165096378105906' +let loggedIn = false +export const isLoggedIn = () => loggedIn + /** * @see https://discord.com/developers/docs/resources/user#user-object */ @@ -110,6 +113,7 @@ export async function handleLogin () { setQuarkBonus(100 * (1 + globalBonus / 100) * (1 + personalBonus / 100) - 100) player.worlds = new QuarkHandler(Number(player.worlds)) + loggedIn = member !== null currentBonus.textContent = `Generous patrons give you a bonus of ${globalBonus}% more Quarks!` diff --git a/src/PseudoCoinUpgrades.ts b/src/PseudoCoinUpgrades.ts new file mode 100644 index 000000000..ebb4f6efc --- /dev/null +++ b/src/PseudoCoinUpgrades.ts @@ -0,0 +1,190 @@ +import i18next from 'i18next' +import { displayProperLoadoutCount } from './BlueberryUpgrades' +import { corruptionLoadoutTableCreate, updateCorruptionLoadoutNames } from './Corruptions' +import { format } from './Synergism' + +export type PseudoCoinUpgradeNames = + | 'INSTANT_UNLOCK_1' + | 'INSTANT_UNLOCK_2' + | 'CUBE_BUFF' + | 'AMBROSIA_LUCK_BUFF' + | 'AMBROSIA_GENERATION_BUFF' + | 'GOLDEN_QUARK_BUFF' + | 'FREE_UPGRADE_PROMOCODE_BUFF' + | 'CORRUPTION_LOADOUT_SLOT_QOL' + | 'AMBROSIA_LOADOUT_SLOT_QOL' + | 'AUTO_POTION_FREE_POTIONS_QOL' + | 'OFFLINE_TIMER_CAP_BUFF' + | 'ADD_CODE_CAP_BUFF' + +export type PseudoCoinUpgrades = Record +export type PseudoCoinUpgradeEffects = Record + +interface Upgrades { + upgradeId: number + maxLevel: number + name: string + description: string + internalName: string + level: number + cost: number +} + +interface PlayerUpgrades { + level: number + upgradeId: number + internalName: PseudoCoinUpgradeNames +} + +interface UpgradesResponse { + coins: number + upgrades: Upgrades[] + playerUpgrades: PlayerUpgrades[] +} + +// TODO?: Something more robust to injections? + +export const PCoinUpgrades: PseudoCoinUpgrades = { + 'INSTANT_UNLOCK_1': 0, + 'INSTANT_UNLOCK_2': 0, + 'CUBE_BUFF': 0, + 'AMBROSIA_LUCK_BUFF': 0, + 'AMBROSIA_GENERATION_BUFF': 0, + 'GOLDEN_QUARK_BUFF': 0, + 'FREE_UPGRADE_PROMOCODE_BUFF': 0, + 'CORRUPTION_LOADOUT_SLOT_QOL': 0, + 'AMBROSIA_LOADOUT_SLOT_QOL': 0, + 'AUTO_POTION_FREE_POTIONS_QOL': 0, + 'OFFLINE_TIMER_CAP_BUFF': 0, + 'ADD_CODE_CAP_BUFF': 0 +} + +export const PCoinUpgradeEffects: PseudoCoinUpgradeEffects = { + INSTANT_UNLOCK_1: 0, + INSTANT_UNLOCK_2: 0, + CUBE_BUFF: 1, + AMBROSIA_LUCK_BUFF: 0, + AMBROSIA_GENERATION_BUFF: 1, + GOLDEN_QUARK_BUFF: 1, + FREE_UPGRADE_PROMOCODE_BUFF: 1, + CORRUPTION_LOADOUT_SLOT_QOL: 0, + AMBROSIA_LOADOUT_SLOT_QOL: 0, + AUTO_POTION_FREE_POTIONS_QOL: 0, + OFFLINE_TIMER_CAP_BUFF: 1, + ADD_CODE_CAP_BUFF: 1 +} + +export const initializePCoinCache = async () => { + const response = await fetch('https://synergism.cc/stripe/upgrades') + const upgradesList = await response.json() as UpgradesResponse + + // Reset Cache + for (const key of Object.keys(PCoinUpgrades)) { + PCoinUpgrades[key as PseudoCoinUpgradeNames] = 0 + updatePCoinEffects(key as PseudoCoinUpgradeNames, 0) + } + + // Update Cache only for the upgrades that the player has + for (const upgrade of upgradesList.playerUpgrades) { + PCoinUpgrades[upgrade.internalName] = upgrade.level + updatePCoinEffects(upgrade.internalName, upgrade.level) + } +} + +export const updatePCoinCache = async (name: PseudoCoinUpgradeNames, level: number) => { + PCoinUpgrades[name] = level + updatePCoinEffects(name, level) +} + +export const updatePCoinEffects = (name: PseudoCoinUpgradeNames, level: number) => { + switch (name) { + case 'INSTANT_UNLOCK_1': + PCoinUpgradeEffects.INSTANT_UNLOCK_1 = level > 0 ? 1 : 0 + break + case 'INSTANT_UNLOCK_2': + PCoinUpgradeEffects.INSTANT_UNLOCK_2 = level > 0 ? 1 : 0 + break + case 'CUBE_BUFF': + PCoinUpgradeEffects.CUBE_BUFF = 1 + level * 0.06 + break + case 'AMBROSIA_LUCK_BUFF': + PCoinUpgradeEffects.AMBROSIA_LUCK_BUFF = level * 20 + break + case 'AMBROSIA_GENERATION_BUFF': + PCoinUpgradeEffects.AMBROSIA_GENERATION_BUFF = 1 + level * 0.05 + break + case 'GOLDEN_QUARK_BUFF': + PCoinUpgradeEffects.GOLDEN_QUARK_BUFF = 1 + level * 0.04 + break + case 'FREE_UPGRADE_PROMOCODE_BUFF': + PCoinUpgradeEffects.FREE_UPGRADE_PROMOCODE_BUFF = 1 + level * 0.02 + break + case 'CORRUPTION_LOADOUT_SLOT_QOL': + PCoinUpgradeEffects.CORRUPTION_LOADOUT_SLOT_QOL = level + corruptionLoadoutTableCreate() + updateCorruptionLoadoutNames() + break + case 'AMBROSIA_LOADOUT_SLOT_QOL': + PCoinUpgradeEffects.AMBROSIA_LOADOUT_SLOT_QOL = level + displayProperLoadoutCount() + break + case 'AUTO_POTION_FREE_POTIONS_QOL': + PCoinUpgradeEffects.AUTO_POTION_FREE_POTIONS_QOL = level > 0 ? 1 : 0 + break + case 'OFFLINE_TIMER_CAP_BUFF': + PCoinUpgradeEffects.OFFLINE_TIMER_CAP_BUFF = 1 + level + break + case 'ADD_CODE_CAP_BUFF': + PCoinUpgradeEffects.ADD_CODE_CAP_BUFF = 1 + level + break + } +} + +export const displayPCoinEffect = (name: PseudoCoinUpgradeNames, level: number) => { + switch (name) { + case 'INSTANT_UNLOCK_1': + return String( + i18next.t('pseudoCoins.upgradeEffects.INSTANT_UNLOCK_1', { + descriptor: level > 0 ? '' : 'NOT', + amount: 10 * level + }) + ) + case 'INSTANT_UNLOCK_2': + return String( + i18next.t('pseudoCoins.upgradeEffects.INSTANT_UNLOCK_2', { + descriptor: level > 0 ? '' : 'NOT', + amount: 6 * level + }) + ) + case 'CUBE_BUFF': + return String(i18next.t('pseudoCoins.upgradeEffects.CUBE_BUFF', { amount: format(1 + 0.06 * level, 2, true) })) + case 'AMBROSIA_LUCK_BUFF': + return String(i18next.t('pseudoCoins.upgradeEffects.AMBROSIA_LUCK_BUFF', { amount: 20 * level })) + case 'AMBROSIA_GENERATION_BUFF': + return String( + i18next.t('pseudoCoins.upgradeEffects.AMBROSIA_GENERATION_BUFF', { amount: format(1 + 0.05 * level, 2, true) }) + ) + case 'GOLDEN_QUARK_BUFF': + return String( + i18next.t('pseudoCoins.upgradeEffects.GOLDEN_QUARK_BUFF', { amount: format(1 + 0.04 * level, 2, true) }) + ) + case 'FREE_UPGRADE_PROMOCODE_BUFF': + return String( + i18next.t('pseudoCoins.upgradeEffects.FREE_UPGRADE_PROMOCODE_BUFF', { + amount: format(1 + 0.02 * level, 2, true) + }) + ) + case 'CORRUPTION_LOADOUT_SLOT_QOL': + return String(i18next.t('pseudoCoins.upgradeEffects.CORRUPTION_LOADOUT_SLOT_QOL', { amount: level })) + case 'AMBROSIA_LOADOUT_SLOT_QOL': + return String(i18next.t('pseudoCoins.upgradeEffects.AMBROSIA_LOADOUT_SLOT_QOL', { amount: level })) + case 'AUTO_POTION_FREE_POTIONS_QOL': + return String( + i18next.t('pseudoCoins.upgradeEffects.AUTO_POTION_FREE_POTIONS_QOL', { descriptor: level > 0 ? '' : 'NOT' }) + ) + case 'OFFLINE_TIMER_CAP_BUFF': + return String(i18next.t('pseudoCoins.upgradeEffects.OFFLINE_TIMER_CAP_BUFF', { amount: level + 1 })) + case 'ADD_CODE_CAP_BUFF': + return String(i18next.t('pseudoCoins.upgradeEffects.ADD_CODE_CAP_BUFF', { amount: level + 1 })) + } +} diff --git a/src/Runes.ts b/src/Runes.ts index 96276c6b9..bfc47ecf6 100644 --- a/src/Runes.ts +++ b/src/Runes.ts @@ -5,7 +5,8 @@ import { calculateOfferings, calculateRuneExpGiven, calculateRuneExpToLevel, - calculateRuneLevels + calculateRuneLevels, + isIARuneUnlocked } from './Calculate' import { format, player } from './Synergism' import { Globals as G } from './Variables' @@ -98,7 +99,7 @@ export const unlockedRune = (runeIndexPlusOne: number) => { player.achievements[44] > 0.5, player.achievements[102] > 0.5, player.researches[82] > 0.5, - player.shopUpgrades.infiniteAscent, + isIARuneUnlocked(), player.platonicUpgrades[20] > 0 ] return unlockedRune[runeIndexPlusOne] diff --git a/src/Shop.ts b/src/Shop.ts index 88069da9d..71512c36d 100644 --- a/src/Shop.ts +++ b/src/Shop.ts @@ -13,6 +13,7 @@ import { sumOfExaltCompletions } from './Calculate' import type { IMultiBuy } from './Cubes' +import { PCoinUpgradeEffects } from './PseudoCoinUpgrades' import { format, player } from './Synergism' import type { Player } from './types/Synergism' import { Alert, Confirm, Prompt, revealStuff } from './UpdateHTML' @@ -1634,6 +1635,7 @@ export const useConsumable = async ( used = 1, spend = true ) => { + const infiniteAutoBrew = PCoinUpgradeEffects.AUTO_POTION_FREE_POTIONS_QOL const p = player.shopConfirmationToggle && !automatic ? await Confirm('Would you like to use some of this potion?') : true @@ -1646,7 +1648,15 @@ export const useConsumable = async ( * used if (input === 'offeringPotion') { - if (player.shopUpgrades.offeringPotion >= used || !spend) { + if (infiniteAutoBrew && automatic) { + player.runeshards += Math.floor( + 7200 + * player.offeringpersecond + * calculateTimeAcceleration().mult + * multiplier + ) + player.runeshards = Math.min(1e300, player.runeshards) + } else if (player.shopUpgrades.offeringPotion >= used || !spend) { player.shopUpgrades.offeringPotion -= spend ? used : 0 player.runeshards += Math.floor( 7200 @@ -1657,7 +1667,15 @@ export const useConsumable = async ( player.runeshards = Math.min(1e300, player.runeshards) } } else if (input === 'obtainiumPotion') { - if (player.shopUpgrades.obtainiumPotion >= used || !spend) { + if (infiniteAutoBrew && automatic) { + player.researchPoints += Math.floor( + 7200 + * player.maxobtainiumpersecond + * calculateTimeAcceleration().mult + * multiplier + ) + player.researchPoints = Math.min(1e300, player.researchPoints) + } else if (player.shopUpgrades.obtainiumPotion >= used || !spend) { player.shopUpgrades.obtainiumPotion -= spend ? used : 0 player.researchPoints += Math.floor( 7200 diff --git a/src/Statistics.ts b/src/Statistics.ts index a3d010c46..855f52857 100644 --- a/src/Statistics.ts +++ b/src/Statistics.ts @@ -721,46 +721,50 @@ export const loadGlobalSpeedMultiplier = () => { export const loadStatisticsCubeMultipliers = () => { const arr0 = calculateAllCubeMultiplier().list - const map0: Record = { - 1: { acc: 2, desc: 'Ascension Time Multiplier:' }, - 2: { acc: 2, desc: 'Sun and Moon Achievements:' }, - 3: { acc: 2, desc: 'Speed Achievement:' }, - 4: { acc: 2, desc: 'Challenge 15 All Cube Bonus:' }, - 5: { acc: 2, desc: 'Rune 6 - Infinite Ascent:' }, - 6: { acc: 2, desc: 'Platonic Beta:' }, - 7: { acc: 2, desc: 'Platonic Omega:' }, - 8: { acc: 2, desc: 'Overflux Powder:' }, - 9: { acc: 2, desc: 'Event:' }, - 10: { acc: 2, desc: 'Singularity Factor:' }, - 11: { acc: 2, desc: 'Wow Pass Y' }, - 12: { acc: 2, desc: 'Starter Pack:' }, - 13: { acc: 2, desc: 'Cube Flame [GQ]:' }, - 14: { acc: 2, desc: 'Cube Blaze [GQ]:' }, - 15: { acc: 2, desc: 'Cube Inferno [GQ]:' }, - 16: { acc: 2, desc: 'Wow Pass Z:' }, - 17: { acc: 2, desc: 'Cookie Upgrade 16:' }, - 18: { acc: 2, desc: 'Cookie Upgrade 8:' }, - 19: { acc: 2, desc: 'Total Octeract Bonus:' }, - 20: { acc: 2, desc: 'No Singularity Upgrades Challenge:' }, - 21: { acc: 2, desc: 'Citadel [GQ]' }, - 22: { acc: 2, desc: 'Citadel 2 [GQ]' }, - 23: { acc: 4, desc: 'Platonic DELTA' }, - 24: { acc: 2, desc: 'Wow Pass ∞' }, - 25: { acc: 2, desc: 'Unspent Ambrosia Bonus' }, - 26: { acc: 2, desc: 'Module- Tutorial' }, - 27: { acc: 2, desc: 'Module- Cubes 1' }, - 28: { acc: 2, desc: 'Module- Luck-Cube 1' }, - 29: { acc: 2, desc: 'Module- Quark-Cube 1' }, - 30: { acc: 2, desc: 'Module- Cubes 2' }, - 31: { acc: 2, desc: 'Module- Hyperflux' }, - 32: { acc: 2, desc: '20 Ascensions X20 Bonus [EXALT ONLY]' }, - 33: { acc: 2, desc: 'Cash Grab ULTIMATE' }, - 34: { acc: 2, desc: 'Shop EX ULTIMATE' }, - 35: { acc: 2, desc: 'Exalt 6 Penalty (for being too slow!)' } + const map0: Record = { + 1: { acc: 2, desc: 'PseudoCoin Upgrade:', color: 'gold' }, + 2: { acc: 2, desc: 'Ascension Time Multiplier:' }, + 3: { acc: 2, desc: 'Sun and Moon Achievements:' }, + 4: { acc: 2, desc: 'Speed Achievement:' }, + 5: { acc: 2, desc: 'Challenge 15 All Cube Bonus:' }, + 6: { acc: 2, desc: 'Rune 6 - Infinite Ascent:' }, + 7: { acc: 2, desc: 'Platonic Beta:' }, + 8: { acc: 2, desc: 'Platonic Omega:' }, + 9: { acc: 2, desc: 'Overflux Powder:' }, + 10: { acc: 2, desc: 'Event:' }, + 11: { acc: 2, desc: 'Singularity Factor:' }, + 12: { acc: 2, desc: 'Wow Pass Y' }, + 13: { acc: 2, desc: 'Starter Pack:' }, + 14: { acc: 2, desc: 'Cube Flame [GQ]:' }, + 15: { acc: 2, desc: 'Cube Blaze [GQ]:' }, + 16: { acc: 2, desc: 'Cube Inferno [GQ]:' }, + 17: { acc: 2, desc: 'Wow Pass Z:' }, + 18: { acc: 2, desc: 'Cookie Upgrade 16:' }, + 19: { acc: 2, desc: 'Cookie Upgrade 8:' }, + 20: { acc: 2, desc: 'Total Octeract Bonus:' }, + 21: { acc: 2, desc: 'No Singularity Upgrades Challenge:' }, + 22: { acc: 2, desc: 'Citadel [GQ]' }, + 23: { acc: 2, desc: 'Citadel 2 [GQ]' }, + 24: { acc: 4, desc: 'Platonic DELTA' }, + 25: { acc: 2, desc: 'Wow Pass ∞' }, + 26: { acc: 2, desc: 'Unspent Ambrosia Bonus' }, + 27: { acc: 2, desc: 'Module- Tutorial' }, + 28: { acc: 2, desc: 'Module- Cubes 1' }, + 29: { acc: 2, desc: 'Module- Luck-Cube 1' }, + 30: { acc: 2, desc: 'Module- Quark-Cube 1' }, + 31: { acc: 2, desc: 'Module- Cubes 2' }, + 32: { acc: 2, desc: 'Module- Hyperflux' }, + 33: { acc: 2, desc: '20 Ascensions X20 Bonus [EXALT ONLY]' }, + 34: { acc: 2, desc: 'Cash Grab ULTIMATE' }, + 35: { acc: 2, desc: 'Shop EX ULTIMATE' }, + 36: { acc: 2, desc: 'Exalt 6 Penalty (for being too slow!)' } } for (let i = 0; i < arr0.length; i++) { const statGCMi = DOMCacheGetOrSet(`statGCM${i + 1}`) statGCMi.childNodes[0].textContent = map0[i + 1].desc + if (map0[i + 1].color) { + statGCMi.style.color = map0[i + 1].color ?? 'white' + } DOMCacheGetOrSet(`sGCM${i + 1}`).textContent = `x${ format( arr0[i], @@ -947,45 +951,49 @@ export const loadStatisticsCubeMultipliers = () => { .bonus ? 'One Mind Multiplier' : 'Ascension Speed Multiplier' - const map6: Record = { + const map6: Record = { 1: { acc: 2, desc: 'Ascension Score Multiplier:' }, - 2: { acc: 2, desc: 'Season Pass 3:' }, - 3: { acc: 2, desc: 'Season Pass Y:' }, - 4: { acc: 2, desc: 'Season Pass Z:' }, - 5: { acc: 2, desc: 'Season Pass Lost:' }, - 6: { acc: 2, desc: 'Cookie Upgrade 20:' }, - 7: { acc: 2, desc: 'Divine Pack:' }, - 8: { acc: 2, desc: 'Cube Flame:' }, - 9: { acc: 2, desc: 'Cube Blaze:' }, - 10: { acc: 2, desc: 'Cube Inferno:' }, - 11: { acc: 2, desc: 'Octeract Absinthe' }, - 12: { acc: 2, desc: 'Pieces of Eight' }, - 13: { acc: 2, desc: 'Obelisk Shaped Like an Octagon' }, - 14: { acc: 2, desc: 'Octahedral Synthesis' }, - 15: { acc: 2, desc: 'Eighth Wonder of the World' }, - 16: { acc: 2, desc: 'Platonic is a fat sellout' }, - 17: { acc: 2, desc: 'Octeracts for Dummies' }, - 18: { acc: 2, desc: 'Octeract Cogenesis' }, - 19: { acc: 2, desc: 'Octeract Trigenesis' }, - 20: { acc: 2, desc: 'Singularity Factor' }, - 21: { acc: 2, desc: 'Digital Octeract Accumulator' }, - 22: { acc: 2, desc: 'Event Buff' }, - 23: { acc: 2, desc: 'Platonic DELTA' }, - 24: { acc: 2, desc: 'No Singularity Upgrades Challenge' }, - 25: { acc: 2, desc: 'Wow Pass ∞' }, - 26: { acc: 2, desc: 'Unspent Ambrosia Bonus' }, - 27: { acc: 2, desc: 'Module- Tutorial' }, - 28: { acc: 2, desc: 'Module- Cubes 1' }, - 29: { acc: 2, desc: 'Module- Luck-Cube 1' }, - 30: { acc: 2, desc: 'Module- Quark-Cube 1' }, - 31: { acc: 2, desc: 'Module- Cubes 2' }, - 32: { acc: 2, desc: 'Cash Grab ULTIMATE' }, - 33: { acc: 2, desc: 'Shop EX ULTIMATE' }, - 34: { acc: 2, desc: ascensionSpeedDesc } + 2: { acc: 2, desc: 'PseudoCoin Upgrade:', color: 'gold' }, + 3: { acc: 2, desc: 'Season Pass 3:' }, + 4: { acc: 2, desc: 'Season Pass Y:' }, + 5: { acc: 2, desc: 'Season Pass Z:' }, + 6: { acc: 2, desc: 'Season Pass Lost:' }, + 7: { acc: 2, desc: 'Cookie Upgrade 20:' }, + 8: { acc: 2, desc: 'Divine Pack:' }, + 9: { acc: 2, desc: 'Cube Flame:' }, + 10: { acc: 2, desc: 'Cube Blaze:' }, + 11: { acc: 2, desc: 'Cube Inferno:' }, + 12: { acc: 2, desc: 'Octeract Absinthe' }, + 13: { acc: 2, desc: 'Pieces of Eight' }, + 14: { acc: 2, desc: 'Obelisk Shaped Like an Octagon' }, + 15: { acc: 2, desc: 'Octahedral Synthesis' }, + 16: { acc: 2, desc: 'Eighth Wonder of the World' }, + 17: { acc: 2, desc: 'Platonic is a fat sellout' }, + 18: { acc: 2, desc: 'Octeracts for Dummies' }, + 19: { acc: 2, desc: 'Octeract Cogenesis' }, + 20: { acc: 2, desc: 'Octeract Trigenesis' }, + 21: { acc: 2, desc: 'Singularity Factor' }, + 22: { acc: 2, desc: 'Digital Octeract Accumulator' }, + 23: { acc: 2, desc: 'Event Buff' }, + 24: { acc: 2, desc: 'Platonic DELTA' }, + 25: { acc: 2, desc: 'No Singularity Upgrades Challenge' }, + 26: { acc: 2, desc: 'Wow Pass ∞' }, + 27: { acc: 2, desc: 'Unspent Ambrosia Bonus' }, + 28: { acc: 2, desc: 'Module- Tutorial' }, + 29: { acc: 2, desc: 'Module- Cubes 1' }, + 30: { acc: 2, desc: 'Module- Luck-Cube 1' }, + 31: { acc: 2, desc: 'Module- Quark-Cube 1' }, + 32: { acc: 2, desc: 'Module- Cubes 2' }, + 33: { acc: 2, desc: 'Cash Grab ULTIMATE' }, + 34: { acc: 2, desc: 'Shop EX ULTIMATE' }, + 35: { acc: 2, desc: ascensionSpeedDesc } } for (let i = 0; i < octMults.list.length; i++) { const statOcMi = DOMCacheGetOrSet(`statOcM${i + 1}`) statOcMi.childNodes[0].textContent = map6[i + 1].desc + if (map6[i + 1].color) { + statOcMi.style.color = map6[i + 1].color ?? 'white' + } DOMCacheGetOrSet(`sOcM${i + 1}`).textContent = `x${ format( octMults.list[i], @@ -1534,20 +1542,24 @@ export const loadStatisticsAscensionSpeedMultipliers = () => { export const loadStatisticsGoldenQuarkMultipliers = () => { const arr = calculateGoldenQuarkMultiplier() - const map: Record = { - 1: { acc: 2, desc: 'Challenge 15 Exponent:' }, - 2: { acc: 2, desc: 'Patreon Bonus:' }, - 3: { acc: 2, desc: 'Golden Quarks I:' }, - 4: { acc: 2, desc: 'Cookie Upgrade 19:' }, - 5: { acc: 2, desc: 'No Singularity Upgrades:' }, - 6: { acc: 2, desc: 'Event:' }, - 7: { acc: 2, desc: 'Singularity Fast Forwards:' }, - 8: { acc: 2, desc: 'Golden Revolution II:' }, - 9: { acc: 2, desc: 'Immaculate Alchemy:' }, - 10: { acc: 2, desc: 'Total Quarks Coefficient:' } + const map: Record = { + 1: { acc: 2, desc: 'PseudoCoin Bonus:', color: 'gold' }, + 2: { acc: 2, desc: 'Challenge 15 Exponent:' }, + 3: { acc: 2, desc: 'Patreon Bonus:' }, + 4: { acc: 2, desc: 'Golden Quarks I:' }, + 5: { acc: 2, desc: 'Cookie Upgrade 19:' }, + 6: { acc: 2, desc: 'No Singularity Upgrades:' }, + 7: { acc: 2, desc: 'Event:' }, + 8: { acc: 2, desc: 'Singularity Fast Forwards:' }, + 9: { acc: 2, desc: 'Golden Revolution II:' }, + 10: { acc: 2, desc: 'Immaculate Alchemy:' }, + 11: { acc: 2, desc: 'Total Quarks Coefficient:' } } for (let i = 0; i < arr.list.length; i++) { const statGQMi = DOMCacheGetOrSet(`statGQMS${i + 1}`) + if (map[i + 1].color) { + statGQMi.style.color = map[i + 1].color ?? 'white' + } statGQMi.childNodes[0].textContent = map[i + 1].desc DOMCacheGetOrSet(`sGQMS${i + 1}`).textContent = `x${ format( @@ -1617,20 +1629,24 @@ export const loadAddCodeModifiersAndEffects = () => { } // Add capacity stats - const capacityMap: Record = { + const capacityMap: Record = { 1: { acc: 0, desc: 'Base:' }, 2: { acc: 0, desc: 'PL-AT X:' }, 3: { acc: 0, desc: 'PL-AT δ:' }, 4: { acc: 0, desc: 'PL-AT Γ:' }, 5: { acc: 0, desc: 'PL-AT _:' }, 6: { acc: 0, desc: 'PL-AT ΩΩ' }, - 7: { acc: 3, desc: 'Singularity factor:' } + 7: { acc: 3, desc: 'Singularity factor:' }, + 8: { acc: 0, desc: 'Plat-P:', color: 'gold' } } for (let i = 0; i < capacityStats.list.length; i++) { const statAddIntervalI = DOMCacheGetOrSet(`stat+cap${i + 1}`) + if (capacityMap[i + 1].color) { + statAddIntervalI.style.color = capacityMap[i + 1].color ?? 'white' + } statAddIntervalI.childNodes[0].textContent = capacityMap[i + 1].desc - const prefix = i === 0 ? '' : i === 5 ? 'x' : '+' + const prefix = i === 0 ? '' : (i === 5 || i === 7) ? 'x' : '+' DOMCacheGetOrSet(`s+cap${i + 1}`).textContent = `${prefix}${ format( capacityStats.list[i], @@ -1700,25 +1716,29 @@ export const loadAddCodeModifiersAndEffects = () => { export const loadStatisticsAmbrosiaLuck = () => { const stats = calculateAmbrosiaLuck() const arr = stats.array - const map: Record = { + const map: Record = { 1: { acc: 0, desc: 'Base Value' }, - 2: { acc: 0, desc: 'Irish Ants Singularity Perk' }, - 3: { acc: 1, desc: 'Shop Upgrade Bonus' }, - 4: { acc: 0, desc: 'Singularity Ambrosia Luck Upgrades' }, - 5: { acc: 0, desc: 'Octeract Ambrosia Luck Upgrades' }, - 6: { acc: 0, desc: 'Ambrosia Luck Module I' }, - 7: { acc: 1, desc: 'Ambrosia Luck Module II' }, - 8: { acc: 2, desc: 'Ambrosia Cube-Luck Hybrid Module I' }, - 9: { acc: 2, desc: 'Ambrosia Quark-Luck Hybrid Module I' }, - 10: { acc: 0, desc: 'Perk: One Hundred Thirty One!' }, - 11: { acc: 0, desc: 'Perk: Two Hundred Sixty Nine!' }, - 12: { acc: 0, desc: 'Shop: Octeract-Based Ambrosia Luck' }, - 13: { acc: 0, desc: 'No Ambrosia Upgrades EXALT' }, - 14: { acc: 0, desc: 'ULTRA Upgrade: Ambrosia Exalter' } + 2: { acc: 0, desc: 'PseudoCoin Upgrade', color: 'gold' }, + 3: { acc: 0, desc: 'Irish Ants Singularity Perk' }, + 4: { acc: 1, desc: 'Shop Upgrade Bonus' }, + 5: { acc: 0, desc: 'Singularity Ambrosia Luck Upgrades' }, + 6: { acc: 0, desc: 'Octeract Ambrosia Luck Upgrades' }, + 7: { acc: 0, desc: 'Ambrosia Luck Module I' }, + 8: { acc: 1, desc: 'Ambrosia Luck Module II' }, + 9: { acc: 2, desc: 'Ambrosia Cube-Luck Hybrid Module I' }, + 10: { acc: 2, desc: 'Ambrosia Quark-Luck Hybrid Module I' }, + 11: { acc: 0, desc: 'Perk: One Hundred Thirty One!' }, + 12: { acc: 0, desc: 'Perk: Two Hundred Sixty Nine!' }, + 13: { acc: 0, desc: 'Shop: Octeract-Based Ambrosia Luck' }, + 14: { acc: 0, desc: 'No Ambrosia Upgrades EXALT' }, + 15: { acc: 0, desc: 'ULTRA Upgrade: Ambrosia Exalter' } } for (let i = 0; i < arr.length - 1; i++) { const statALuckMi = DOMCacheGetOrSet(`statALuckM${i + 1}`) statALuckMi.childNodes[0].textContent = map[i + 1].desc + if (map[i + 1].color) { + statALuckMi.style.color = map[i + 1].color ?? 'white' + } DOMCacheGetOrSet(`sALuckM${i + 1}`).textContent = `+${ format( arr[i], @@ -1737,21 +1757,25 @@ export const loadStatisticsAmbrosiaLuck = () => { export const loadStatisticsAmbrosiaGeneration = () => { const stats = calculateAmbrosiaGenerationSpeed() const arr = stats.array - const map: Record = { + const map: Record = { 1: { acc: 4, desc: 'Visited Ambrosia Subtab' }, - 2: { acc: 4, desc: 'Number of Blueberries' }, - 3: { acc: 4, desc: 'Shop Upgrade Bonus' }, - 4: { acc: 4, desc: 'Singularity Ambrosia Generation Upgrades' }, - 5: { acc: 4, desc: 'Octeract Ambrosia Generation Upgrades' }, - 6: { acc: 4, desc: 'Patreon Bonus' }, - 7: { acc: 4, desc: 'One Ascension Challenge EXALT' }, - 8: { acc: 4, desc: 'No Ambrosia Upgrades EXALT' }, - 9: { acc: 4, desc: 'Cash-Grab ULTIMATE' }, - 10: { acc: 4, desc: 'Event Bonus' } + 2: { acc: 4, desc: 'PseudoCoin Upgrade', color: 'gold' }, + 3: { acc: 4, desc: 'Number of Blueberries' }, + 4: { acc: 4, desc: 'Shop Upgrade Bonus' }, + 5: { acc: 4, desc: 'Singularity Ambrosia Generation Upgrades' }, + 6: { acc: 4, desc: 'Octeract Ambrosia Generation Upgrades' }, + 7: { acc: 4, desc: 'Patreon Bonus' }, + 8: { acc: 4, desc: 'One Ascension Challenge EXALT' }, + 9: { acc: 4, desc: 'No Ambrosia Upgrades EXALT' }, + 10: { acc: 4, desc: 'Cash-Grab ULTIMATE' }, + 11: { acc: 4, desc: 'Event Bonus' } } for (let i = 0; i < arr.length; i++) { const statAGenMi = DOMCacheGetOrSet(`statAGenM${i + 1}`) statAGenMi.childNodes[0].textContent = map[i + 1].desc + if (map[i + 1].color) { + statAGenMi.style.color = map[i + 1].color ?? 'white' + } DOMCacheGetOrSet(`sAGenM${i + 1}`).textContent = `x${format(arr[i], map[i + 1].acc, true)}` } diff --git a/src/Summary.ts b/src/Summary.ts index 0425a1e49..4052c0346 100644 --- a/src/Summary.ts +++ b/src/Summary.ts @@ -10,6 +10,7 @@ import { calculateTimeAcceleration, calculateTotalOcteractCubeBonus, calculateTotalOcteractQuarkBonus, + isIARuneUnlocked, octeractGainPerSecond } from './Calculate' import { getMaxChallenges } from './Challenges' @@ -292,7 +293,7 @@ export const generateExportSummary = async (): Promise => { format(calculateMaxRunes(5)) } [Bonus: ${format(G.rune5level - player.runelevels[4], 0, true)}]\n` } - if (player.shopUpgrades.infiniteAscent > 0 || player.highestSingularityCount > 0) { + if (isIARuneUnlocked() || player.highestSingularityCount > 0) { prestige = `${prestige}Infinite Ascent: Level ${format(player.runelevels[5], 0, true)}/${ format(calculateMaxRunes(6)) }\n` diff --git a/src/Synergism.ts b/src/Synergism.ts index 699b47dad..ee62abd9b 100644 --- a/src/Synergism.ts +++ b/src/Synergism.ts @@ -55,7 +55,8 @@ import { calculateTotalAcceleratorBoost, calculateTotalCoinOwned, dailyResetCheck, - exitOffline + exitOffline, + isShopTalismanUnlocked } from './Calculate' import { corrChallengeMinimum, @@ -65,7 +66,8 @@ import { corruptionLoadoutTableUpdate, corruptionStatsUpdate, maxCorruptionLevel, - updateCorruptionLoadoutNames + updateCorruptionLoadoutNames, + updateUndefinedLoadouts } from './Corruptions' import { updateCubeUpgradeBG } from './Cubes' import { generateEventHandlers } from './EventListeners' @@ -136,7 +138,13 @@ import { import i18next from 'i18next' import localforage from 'localforage' -import { BlueberryUpgrade, blueberryUpgradeData, updateLoadoutHoverClasses } from './BlueberryUpgrades' +import { + BlueberryUpgrade, + blueberryUpgradeData, + displayProperLoadoutCount, + updateBlueberryLoadoutCount, + updateLoadoutHoverClasses +} from './BlueberryUpgrades' import { DOMCacheGetOrSet } from './Cache/DOM' import { lastUpdated, prod, testing, version } from './Config' import { WowCubes, WowHypercubes, WowPlatonicCubes, WowTesseracts } from './CubeExperimental' @@ -158,6 +166,7 @@ import { init as i18nInit } from './i18n' import { handleLogin } from './Login' import { octeractData, OcteractUpgrade } from './Octeracts' import { updatePlatonicUpgradeBG } from './Platonic' +import { initializePCoinCache, PCoinUpgradeEffects } from './PseudoCoinUpgrades' import { getQuarkBonus, QuarkHandler } from './Quark' import { playerJsonSchema } from './saves/PlayerJsonSchema' import { playerSchema } from './saves/PlayerSchema' @@ -874,7 +883,15 @@ export const player: Player = { 5: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 6: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 7: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - 8: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + 8: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 9: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 10: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 11: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 12: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 13: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 14: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 15: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 16: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }, corruptionLoadoutNames: [ 'Loadout 1', @@ -884,7 +901,15 @@ export const player: Player = { 'Loadout 5', 'Loadout 6', 'Loadout 7', - 'Loadout 8' + 'Loadout 8', + 'Loadout 9', + 'Loadout 10', + 'Loadout 11', + 'Loadout 12', + 'Loadout 13', + 'Loadout 14', + 'Loadout 15', + 'Loadout 16' ], corruptionShowStats: true, @@ -1492,7 +1517,15 @@ export const player: Player = { 5: {}, 6: {}, 7: {}, - 8: {} + 8: {}, + 9: {}, + 10: {}, + 11: {}, + 12: {}, + 13: {}, + 14: {}, + 15: {}, + 16: {} }, blueberryLoadoutMode: 'saveTree', @@ -2253,13 +2286,20 @@ const loadSynergy = async () => { } corruptionStatsUpdate() - const corrs = Math.min(8, Object.keys(player.corruptionLoadouts).length) + 1 + updateUndefinedLoadouts() // Monetization update added more corruption loadout slots + updateBlueberryLoadoutCount() // Monetization update also added more Blueberry loadout slots + + const corrs = 1 + 8 + PCoinUpgradeEffects.CORRUPTION_LOADOUT_SLOT_QOL + // const corrs = Math.min(8, Object.keys(player.corruptionLoadouts).length) + 1 for (let i = 0; i < corrs; i++) { corruptionLoadoutTableUpdate(i) } showCorruptionStatsLoadouts() updateCorruptionLoadoutNames() + // For blueberry upgrades! + displayProperLoadoutCount() + DOMCacheGetOrSet('researchrunebonus').textContent = i18next.t( 'runes.thanksResearches', { @@ -5514,7 +5554,7 @@ export const updateAll = (): void => { player.achievements[140] > 0, player.achievements[147] > 0, player.antUpgrades[11]! > 0 || player.ascensionCount > 0, - player.shopUpgrades.shopTalisman > 0 + isShopTalismanUnlocked() ] let upgradedTalisman = false @@ -6337,6 +6377,19 @@ function playerNeedsReminderToExport () { window.addEventListener('load', async () => { await i18nInit() + handleLogin().catch(console.error) + + try { + await initializePCoinCache() + } catch (e) { + console.error(e) + const response = await Confirm( + 'PseudoCoin bonuses weren\'t fetched, if you have purchased upgrades they will not take effect. ' + + 'Press OK to continue to the game without upgrades.' + ) + + if (!response) return + } const ver = DOMCacheGetOrSet('versionnumber') const addZero = (n: number) => `${n}`.padStart(2, '0') @@ -6365,8 +6418,6 @@ window.addEventListener('load', async () => { corruptionButtonsAdd() corruptionLoadoutTableCreate() - - handleLogin().catch(console.error) }) window.addEventListener('unload', () => { diff --git a/src/Tabs.ts b/src/Tabs.ts index fb88814b1..ac48a7c14 100644 --- a/src/Tabs.ts +++ b/src/Tabs.ts @@ -1,6 +1,8 @@ -import { DOMCacheGetOrSet } from './Cache/DOM' +import { DOMCacheGetOrSet, DOMCacheHas } from './Cache/DOM' import { calculateAmbrosiaGenerationSpeed } from './Calculate' import { pressedKeys } from './Hotkeys' +import { isLoggedIn } from './Login' +import { initializeCart } from './purchases/CartTab' import { player } from './Synergism' import { setActiveSettingScreen, @@ -28,7 +30,8 @@ export enum Tabs { Singularity = 9, Settings = 10, Shop = 11, - Event = 12 + Event = 12, + Purchase = 13 } /** @@ -267,7 +270,31 @@ const subtabInfo: Record = { } ] }, - [Tabs.Event]: { subTabList: [] } + [Tabs.Event]: { subTabList: [] }, + [Tabs.Purchase]: { + tabSwitcher: () => initializeCart, + subTabList: [ + { + subTabID: 'productContainer', + get unlocked () { + return isLoggedIn() + }, + buttonID: 'cartSubTab1' + }, + { + subTabID: 'upgradesContainer', + unlocked: true, + buttonID: 'cartSubTab2' + }, + { + subTabID: 'cartContainer', + get unlocked () { + return isLoggedIn() + }, + buttonID: 'cartSubTab3' + } + ] + } } class TabRow extends HTMLDivElement { @@ -552,7 +579,10 @@ tabRow.appendButton( .setUnlockedState(() => G.isEvent) .setType(Tabs.Event) .makeDraggable() - .makeRemoveable() + .makeRemoveable(), + new $Tab({ id: 'pseudoCoinstab', i18n: 'tabs.main.purchase' }) + .setType(Tabs.Purchase) + .makeDraggable() ) /** @@ -603,7 +633,7 @@ export const changeTab = (tabs: Tabs, step?: number) => { if (G.currentTab !== Tabs.Settings) { for (let i = 0; i < subTabList.length; i++) { const id = subTabList[i].buttonID - if (id) { + if (id && DOMCacheHas(id)) { const button = DOMCacheGetOrSet(id) if (button.style.backgroundColor === 'crimson') { // handles every tab except settings and corruptions diff --git a/src/Talismans.ts b/src/Talismans.ts index 1f978f6d5..5b8ce5a6a 100644 --- a/src/Talismans.ts +++ b/src/Talismans.ts @@ -3,6 +3,7 @@ import { achievementaward } from './Achievements' import { DOMCacheGetOrSet } from './Cache/DOM' import { calculateRuneLevels } from './Calculate' import { CalcECC } from './Challenges' +import { PCoinUpgradeEffects } from './PseudoCoinUpgrades' import { format, player } from './Synergism' import { Globals as G } from './Variables' @@ -44,6 +45,10 @@ export const calculateMaxTalismanLevel = (i: number) => { maxLevel += 6 * CalcECC('ascension', player.challengecompletions[13]) maxLevel += Math.floor(player.researches[200] / 400) + if (i === 6) { + maxLevel += PCoinUpgradeEffects.INSTANT_UNLOCK_1 ? 10 : 0 + } + if (player.cubeUpgrades[67] > 0 && i === 3) { maxLevel += 1337 } diff --git a/src/UpdateHTML.ts b/src/UpdateHTML.ts index 3b16c0959..1379497af 100644 --- a/src/UpdateHTML.ts +++ b/src/UpdateHTML.ts @@ -2,9 +2,16 @@ import Decimal from 'break_infinity.js' import i18next from 'i18next' import { achievementaward, totalachievementpoints } from './Achievements' import { DOMCacheGetOrSet } from './Cache/DOM' -import { CalcCorruptionStuff, calculateAscensionAcceleration, calculateTimeAcceleration } from './Calculate' +import { + CalcCorruptionStuff, + calculateAscensionAcceleration, + calculateTimeAcceleration, + isIARuneUnlocked, + isShopTalismanUnlocked +} from './Calculate' import { getMaxChallenges } from './Challenges' import { revealCorruptions } from './Corruptions' +import { initializeCart } from './purchases/CartTab' import { autoResearchEnabled } from './Research' import { displayRuneInformation } from './Runes' import { updateSingularityPenalties, updateSingularityPerks } from './singularity' @@ -19,6 +26,7 @@ import { visualUpdateCorruptions, visualUpdateCubes, visualUpdateEvent, + visualUpdatePurchase, visualUpdateResearch, visualUpdateRunes, visualUpdateSettings, @@ -408,7 +416,7 @@ export const revealStuff = () => { ? 'block' : 'none' - player.shopUpgrades.shopTalisman > 0 // Plastic Talisman Shop Purchase + isShopTalismanUnlocked() // Plastic Talisman Shop Purchase ? DOMCacheGetOrSet('talisman7area').style.display = 'flex' : DOMCacheGetOrSet('talisman7area').style.display = 'none' @@ -416,7 +424,7 @@ export const revealStuff = () => { ? DOMCacheGetOrSet('reincarnateAutoUpgrade').style.display = 'block' : DOMCacheGetOrSet('reincarnateAutoUpgrade').style.display = 'none' - if (player.shopUpgrades.infiniteAscent) { + if (isIARuneUnlocked()) { DOMCacheGetOrSet('rune6area').style.display = 'flex' DOMCacheGetOrSet('runeshowpower6').style.display = 'block' } else { @@ -608,6 +616,8 @@ export const hideStuff = () => { DOMCacheGetOrSet('singularitytab').style.backgroundColor = '' DOMCacheGetOrSet('event').style.display = 'none' DOMCacheGetOrSet('eventtab').style.backgroundColor = '' + document.getElementById('pseudoCoins')?.style.setProperty('display', 'none') + DOMCacheGetOrSet('pseudoCoinstab').style.backgroundColor = '' const tab = DOMCacheGetOrSet('settingstab')! tab.style.backgroundColor = '' @@ -688,6 +698,13 @@ export const hideStuff = () => { DOMCacheGetOrSet('event').style.display = 'block' DOMCacheGetOrSet('eventtab').style.backgroundColor = 'gold' } + + if (G.currentTab === Tabs.Purchase) { + initializeCart() + + document.getElementById('pseudoCoins')?.style.setProperty('display', 'unset') + DOMCacheGetOrSet('pseudoCoinstab').style.backgroundColor = 'orange' + } } const visualTab: Record void> = { @@ -703,7 +720,8 @@ const visualTab: Record void> = { [Tabs.WowCubes]: visualUpdateCubes, [Tabs.Corruption]: visualUpdateCorruptions, [Tabs.Singularity]: visualUpdateSingularity, - [Tabs.Event]: visualUpdateEvent + [Tabs.Event]: visualUpdateEvent, + [Tabs.Purchase]: visualUpdatePurchase } export const htmlInserts = () => { @@ -1154,149 +1172,191 @@ export const changeTabColor = () => { tab.style.backgroundColor = color } -export const Confirm = (text: string): Promise => { - const conf = DOMCacheGetOrSet('confirmationBox') - const confWrap = DOMCacheGetOrSet('confirmWrapper') - const popup = DOMCacheGetOrSet('confirm') - const overlay = DOMCacheGetOrSet('transparentBG') - const ok = DOMCacheGetOrSet('ok_confirm') - const cancel = DOMCacheGetOrSet('cancel_confirm') - - DOMCacheGetOrSet('alertWrapper').style.display = 'none' - DOMCacheGetOrSet('promptWrapper').style.display = 'none' - - conf.style.display = 'block' - confWrap.style.display = 'block' - overlay.style.display = 'block' - popup.querySelector('p')!.textContent = text - popup.focus() - - const p = createDeferredPromise() - - // IF you clean up the typing here also clean up PromptCB - const listener = ({ target }: MouseEvent | { target: HTMLElement }) => { - const targetEl = target as HTMLButtonElement - ok.removeEventListener('click', listener) - cancel.removeEventListener('click', listener) - popup.removeEventListener('keyup', kbListener) - - conf.style.display = 'none' - confWrap.style.display = 'none' - overlay.style.display = 'none' +class AsyncQueue { + #items: { + action: () => Promise + resolve: (value: unknown) => void + reject: (err: Error) => void + }[] = [] + #pending = false + + enqueue (action: () => Promise): Promise { + return new Promise((resolve: (value: unknown) => void, reject) => { + this.#items.push({ action, resolve, reject }) + this.dequeue() + }) as Promise + } + + async dequeue () { + if (this.#pending) return false + + const item = this.#items.shift() + if (!item) return false + + try { + this.#pending = true + + const payload = await item.action() + item.resolve(payload) + } catch (e) { + item.reject(e as Error) + } finally { + this.#pending = false + this.dequeue() + } - p.resolve(targetEl === ok) + return true } +} - const kbListener = (e: KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - return listener({ target: ok }) - } else if (e.key === 'Escape') { - return listener({ target: cancel }) - } +const queue = new AsyncQueue() - return e.preventDefault() - } +export const Confirm = async (text: string) => + queue.enqueue(() => { + const conf = DOMCacheGetOrSet('confirmationBox') + const confWrap = DOMCacheGetOrSet('confirmWrapper') + const popup = DOMCacheGetOrSet('confirm') + const overlay = DOMCacheGetOrSet('transparentBG') + const ok = DOMCacheGetOrSet('ok_confirm') + const cancel = DOMCacheGetOrSet('cancel_confirm') - ok.addEventListener('click', listener, { once: true }) - cancel.addEventListener('click', listener, { once: true }) - popup.addEventListener('keyup', kbListener) + DOMCacheGetOrSet('alertWrapper').style.display = 'none' + DOMCacheGetOrSet('promptWrapper').style.display = 'none' - return p.promise -} + conf.style.display = 'block' + confWrap.style.display = 'block' + overlay.style.display = 'block' + popup.querySelector('p')!.textContent = text + popup.focus() -export const Alert = (text: string): Promise => { - const conf = DOMCacheGetOrSet('confirmationBox') - const alertWrap = DOMCacheGetOrSet('alertWrapper') - const overlay = DOMCacheGetOrSet('transparentBG') - const popup = DOMCacheGetOrSet('alert') - const ok = DOMCacheGetOrSet('ok_alert') + const p = createDeferredPromise() - DOMCacheGetOrSet('confirmWrapper').style.display = 'none' - DOMCacheGetOrSet('promptWrapper').style.display = 'none' + // IF you clean up the typing here also clean up PromptCB + const listener = ({ target }: MouseEvent | { target: HTMLElement }) => { + const targetEl = target as HTMLButtonElement + ok.removeEventListener('click', listener) + cancel.removeEventListener('click', listener) + popup.removeEventListener('keyup', kbListener) - conf.style.display = 'block' - alertWrap.style.display = 'block' - overlay.style.display = 'block' - popup.querySelector('p')!.textContent = text - popup.focus() + conf.style.display = 'none' + confWrap.style.display = 'none' + overlay.style.display = 'none' - const p = createDeferredPromise() + p.resolve(targetEl === ok) + } - const listener = () => { - ok.removeEventListener('click', listener) - popup.removeEventListener('keyup', kbListener) + const kbListener = (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + return listener({ target: ok }) + } else if (e.key === 'Escape') { + return listener({ target: cancel }) + } - conf.style.display = 'none' - alertWrap.style.display = 'none' - overlay.style.display = 'none' - p.resolve() - } + return e.preventDefault() + } - const kbListener = (e: KeyboardEvent) => (e.key === 'Enter' || e.key === ' ') && listener() + ok.addEventListener('click', listener, { once: true }) + cancel.addEventListener('click', listener, { once: true }) + popup.addEventListener('keyup', kbListener) - ok.addEventListener('click', listener, { once: true }) - popup.addEventListener('keyup', kbListener) + return p.promise + }) - return p.promise -} +export const Alert = (text: string): Promise => + queue.enqueue(() => { + const conf = DOMCacheGetOrSet('confirmationBox') + const alertWrap = DOMCacheGetOrSet('alertWrapper') + const overlay = DOMCacheGetOrSet('transparentBG') + const popup = DOMCacheGetOrSet('alert') + const ok = DOMCacheGetOrSet('ok_alert') + + DOMCacheGetOrSet('confirmWrapper').style.display = 'none' + DOMCacheGetOrSet('promptWrapper').style.display = 'none' + + conf.style.display = 'block' + alertWrap.style.display = 'block' + overlay.style.display = 'block' + popup.querySelector('p')!.textContent = text + popup.focus() + + const p = createDeferredPromise() + + const listener = () => { + ok.removeEventListener('click', listener) + popup.removeEventListener('keyup', kbListener) + + conf.style.display = 'none' + alertWrap.style.display = 'none' + overlay.style.display = 'none' + p.resolve() + } -export const Prompt = (text: string, defaultValue?: string): Promise => { - const conf = DOMCacheGetOrSet('confirmationBox') - const confWrap = DOMCacheGetOrSet('promptWrapper') - const overlay = DOMCacheGetOrSet('transparentBG') - const popup = DOMCacheGetOrSet('prompt') - const ok = DOMCacheGetOrSet('ok_prompt') - const cancel = DOMCacheGetOrSet('cancel_prompt') + const kbListener = (e: KeyboardEvent) => (e.key === 'Enter' || e.key === ' ') && listener() - DOMCacheGetOrSet('alertWrapper').style.display = 'none' - DOMCacheGetOrSet('confirmWrapper').style.display = 'none' + ok.addEventListener('click', listener, { once: true }) + popup.addEventListener('keyup', kbListener) - conf.style.display = 'block' - confWrap.style.display = 'block' - overlay.style.display = 'block' - popup.querySelector('label')!.textContent = text - if (defaultValue) { - popup.querySelector('input')!.placeholder = defaultValue - } - popup.querySelector('input')!.focus() + return p.promise + }) - const p = createDeferredPromise() +export const Prompt = (text: string, defaultValue?: string): Promise => + queue.enqueue(() => { + const conf = DOMCacheGetOrSet('confirmationBox') + const confWrap = DOMCacheGetOrSet('promptWrapper') + const overlay = DOMCacheGetOrSet('transparentBG') + const popup = DOMCacheGetOrSet('prompt') + const ok = DOMCacheGetOrSet('ok_prompt') + const cancel = DOMCacheGetOrSet('cancel_prompt') + + DOMCacheGetOrSet('alertWrapper').style.display = 'none' + DOMCacheGetOrSet('confirmWrapper').style.display = 'none' + + conf.style.display = 'block' + confWrap.style.display = 'block' + overlay.style.display = 'block' + popup.querySelector('label')!.textContent = text + if (defaultValue) { + popup.querySelector('input')!.placeholder = defaultValue + } + popup.querySelector('input')!.focus() - // kinda disgusting types but whatever - const listener = ({ target }: MouseEvent | { target: HTMLElement }) => { - const targetEl = target as HTMLButtonElement - const el = targetEl.parentNode!.querySelector('input')! + const p = createDeferredPromise() - ok.removeEventListener('click', listener) - cancel.removeEventListener('click', listener) - popup.querySelector('input')!.removeEventListener('keyup', kbListener) + // kinda disgusting types but whatever + const listener = ({ target }: MouseEvent | { target: HTMLElement }) => { + const targetEl = target as HTMLButtonElement + const el = targetEl.parentNode!.querySelector('input')! - conf.style.display = 'none' - confWrap.style.display = 'none' - overlay.style.display = 'none' + ok.removeEventListener('click', listener) + cancel.removeEventListener('click', listener) + popup.querySelector('input')!.removeEventListener('keyup', kbListener) - p.resolve(targetEl.id === ok.id ? el.value || el.placeholder : null) + conf.style.display = 'none' + confWrap.style.display = 'none' + overlay.style.display = 'none' - el.value = el.textContent = el.placeholder = '' - popup.querySelector('input')!.blur() - } + p.resolve(targetEl.id === ok.id ? el.value || el.placeholder : null) - const kbListener = (e: KeyboardEvent) => { - if (e.key === 'Enter') { - return listener({ target: ok }) - } else if (e.key === 'Escape') { - return listener({ target: cancel }) + el.value = el.textContent = el.placeholder = '' + popup.querySelector('input')!.blur() } - return e.preventDefault() - } + const kbListener = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + return listener({ target: ok }) + } else if (e.key === 'Escape') { + return listener({ target: cancel }) + } - ok.addEventListener('click', listener, { once: true }) - cancel.addEventListener('click', listener, { once: true }) - popup.querySelector('input')!.addEventListener('keyup', kbListener) + return e.preventDefault() + } - return p.promise -} + ok.addEventListener('click', listener, { once: true }) + cancel.addEventListener('click', listener, { once: true }) + popup.querySelector('input')!.addEventListener('keyup', kbListener) + + return p.promise + }) let closeNotification: ReturnType let closedNotification: ReturnType diff --git a/src/UpdateVisuals.ts b/src/UpdateVisuals.ts index 4ca4f2784..0156b7b56 100644 --- a/src/UpdateVisuals.ts +++ b/src/UpdateVisuals.ts @@ -29,6 +29,7 @@ import { version } from './Config' import type { IMultiBuy } from './Cubes' import type { hepteractTypes } from './Hepteracts' import { hepteractTypeList } from './Hepteracts' +import { PCoinUpgradeEffects } from './PseudoCoinUpgrades' import { getQuarkBonus, quarkHandler } from './Quark' import { displayRuneInformation } from './Runes' import { getShopCosts, isShopUpgradeUnlocked, shopData, shopUpgradeTypes } from './Shop' @@ -593,6 +594,10 @@ export const visualUpdateRunes = () => { ) } ) + } else if (i === 6) { + DOMCacheGetOrSet(`bonusrune${i}`).textContent = i18next.t('runes.bonusAmount', { + x: PCoinUpgradeEffects.INSTANT_UNLOCK_2 ? 6 : 0 + }) } else { DOMCacheGetOrSet(`bonusrune${i}`).textContent = i18next.t('runes.bonusNope') } @@ -1776,3 +1781,5 @@ export const visualUpdateShop = () => { } export const visualUpdateEvent = () => {} + +export const visualUpdatePurchase = () => {} diff --git a/src/Utility.ts b/src/Utility.ts index 61a828c58..aebb3236b 100644 --- a/src/Utility.ts +++ b/src/Utility.ts @@ -171,7 +171,13 @@ export function limitRange (number: number, min: number, max: number): number { return number } -export const createDeferredPromise = () => { +export interface DeferredPromise { + promise: Promise + resolve: (value: T) => void + reject: (err: Error) => void +} + +export const createDeferredPromise = (): DeferredPromise => { let resolve!: (unknown: T) => void let reject!: (err: Error) => void @@ -192,3 +198,17 @@ export const deepClone = (value: unknown) => { return value }) } + +export function memoize (fn: (...args: Args) => Ret) { + let ran = false + let ret: Ret + + return (...args: Args): Ret => { + if (!ran) { + ran = true + ret = fn(...args) + } + + return ret + } +} diff --git a/src/purchases/CartTab.ts b/src/purchases/CartTab.ts new file mode 100644 index 000000000..b5cd58d4f --- /dev/null +++ b/src/purchases/CartTab.ts @@ -0,0 +1,114 @@ +import { isLoggedIn } from '../Login' +import { player } from '../Synergism' +import { changeSubTab, Tabs } from '../Tabs' +import { Alert } from '../UpdateHTML' +import { createDeferredPromise, type DeferredPromise, memoize } from '../Utility' +import { setEmptyProductMap } from './CartUtil' +import { clearCheckoutTab, toggleCheckoutTab } from './CheckoutTab' +import { clearProductPage, initializeProductPage } from './ProductSubtab' +import { clearUpgradeSubtab, toggleUpgradeSubtab } from './UpgradesSubtab' + +export type Product = { + name: string + id: number + price: number + coins: number +} + +const cartSubTabs = { + Coins: 0, + Upgrades: 1, + Checkout: 2 +} as const + +const tab = document.getElementById('pseudoCoins')! + +function* yieldQuerySelectorAll (selector: string) { + const elements = tab.querySelectorAll(selector) + + for (let i = 0; i < elements.length; i++) { + yield [i, elements.item(i)] as const + } +} + +class CartTab { + static #productsFetch: DeferredPromise | undefined + static #products: Product[] = [] + + constructor () { + this.#updateSubtabs() + } + + static fetchProducts () { + if (CartTab.#productsFetch) { + return CartTab.#productsFetch.promise + } + + CartTab.#productsFetch = createDeferredPromise() + + // TODO: move this fetch to the products page. + fetch('https://synergism.cc/stripe/products') + .then((response) => response.json() as Promise) + .then((products) => { + CartTab.#products.push(...products) + setEmptyProductMap(products) + CartTab.#productsFetch?.resolve(undefined) + }, CartTab.#productsFetch.reject) + + return CartTab.#productsFetch.promise + } + + static applySubtabListeners () { + for (const [index, element] of yieldQuerySelectorAll('.subtabSwitcher button')) { + element.addEventListener('click', () => { + if (isLoggedIn()) { + changeSubTab(Tabs.Purchase, { page: index }) + } else { + Alert('Note: you must be logged in to view this tab!') + } + }) + } + } + + #updateSubtabs () { + for (const [index, element] of yieldQuerySelectorAll('.subtabSwitcher button')) { + if (player.subtabNumber === index) { + element.classList.add('buttonActive') + } else { + element.classList.remove('buttonActive') + } + } + + clearProductPage() + clearUpgradeSubtab() + clearCheckoutTab() + + switch (player.subtabNumber) { + case cartSubTabs.Coins: + CartTab.fetchProducts().then(() => initializeProductPage(CartTab.#products)) + break + case cartSubTabs.Upgrades: + toggleUpgradeSubtab() + break + case cartSubTabs.Checkout: + CartTab.fetchProducts().then(() => toggleCheckoutTab(CartTab.#products)) + break + } + } +} + +const onInit = memoize(() => { + CartTab.fetchProducts() + CartTab.applySubtabListeners() + + // Switch to the upgrades tab if not logged in + if (!isLoggedIn()) { + changeSubTab(Tabs.Purchase, { step: 1 }) + } +}) + +export const initializeCart = () => { + onInit() + + new CartTab() +} diff --git a/src/purchases/CartUtil.ts b/src/purchases/CartUtil.ts new file mode 100644 index 000000000..8e9de05c4 --- /dev/null +++ b/src/purchases/CartUtil.ts @@ -0,0 +1,84 @@ +import type { Product } from './CartTab' + +/** A map of all products in the cart. */ +const cartMap = new Map }>() + +/** Total number of items in the cart */ +let inCart = 0 + +export const decrementInCart = () => { + inCart = Math.max(0, inCart - 1) + updateInCartCount() +} + +export const incrementInCart = () => { + inCart++ + updateInCartCount() +} + +export const getInCartTotal = () => inCart + +const updateInCartCount = () => { + const counter = document.getElementById('notification-count')! + + if (inCart === 0) { + counter.style.display = 'none' + } else { + counter.style.display = 'unset' + } + + counter.textContent = `${inCart}` +} + +export const setEmptyProductMap = (products: Product[]) => { + for (const { id, price, ...rest } of products) { + cartMap.set(id, { quantity: 0, price, rest }) + } +} + +export const addToCart = (id: number) => { + const itemInCart = cartMap.get(id)! + itemInCart.quantity++ + inCart++ + + updateInCartCount() +} + +export const removeFromCart = (id: number) => { + const itemInCart = cartMap.get(id)! + itemInCart.quantity-- + inCart-- + + updateInCartCount() +} + +/** + * Returns the price of everything in the cart. + */ +export const getPrice = () => { + let totalPrice = 0 + + for (const { price, quantity } of cartMap.values()) { + if (quantity > 0) { + totalPrice += price * quantity + } + } + + return totalPrice +} + +export const getQuantity = (id: number) => { + return cartMap.get(id)!.quantity +} + +export const getProductsInCart = () => { + const temp: (Product & { quantity: number })[] = [] + + for (const [id, { quantity, price, rest }] of cartMap) { + if (quantity > 0) { + temp.push({ id, quantity, price, ...rest }) + } + } + + return temp +} diff --git a/src/purchases/CheckoutTab.ts b/src/purchases/CheckoutTab.ts new file mode 100644 index 000000000..880b95ac1 --- /dev/null +++ b/src/purchases/CheckoutTab.ts @@ -0,0 +1,150 @@ +import { changeSubTab, Tabs } from '../Tabs' +import { Alert, Notification } from '../UpdateHTML' +import { memoize } from '../Utility' +import type { Product } from './CartTab' +import { addToCart, getPrice, getProductsInCart, getQuantity, removeFromCart } from './CartUtil' + +const tab = document.querySelector('#pseudoCoins > #cartContainer')! +const form = tab.querySelector('div.cartList')! + +const checkout = form.querySelector('button#checkout') +const closeCart = form.querySelector('button#closeCart') +const radioTOSAgree = form.querySelector('section > input[type="radio"]')! +const totalCost = form.querySelector('p#totalCost') +const itemList = form.querySelector('#itemList')! + +let tosAgreed = false +const formatter = Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' +}) + +export const initializeCheckoutTab = memoize((products: Product[]) => { + closeCart?.addEventListener('click', () => { + changeSubTab(Tabs.Purchase, { page: 0 }) + }) + + radioTOSAgree.addEventListener('click', () => { + tosAgreed = !tosAgreed + radioTOSAgree.checked = tosAgreed + }) + + itemList.insertAdjacentHTML( + 'afterend', + products.map((product) => (` +
+ +
+ `)).join('') + ) + + checkout?.addEventListener('click', () => { + if (!tosAgreed) { + Notification('You must accept the terms of service first!') + return + } + + const fd = new FormData() + + for (const product of getProductsInCart()) { + fd.set(`product-${product.id}`, `${product.quantity}`) + } + + fd.set('tosAgree', tosAgreed ? 'on' : 'off') + + checkout.setAttribute('disabled', '') + + fetch('https://synergism.cc/stripe/create-checkout-session', { + method: 'POST', + body: fd + }).then((response) => response.json()) + .then((json: { redirect: string; error: string }) => { + if (json.redirect) { + window.location.href = json.redirect + } else { + Notification(json.error) + } + }) + .finally(() => { + checkout.removeAttribute('disabled') + }) + }) +}) + +function addItem (e: MouseEvent) { + e.preventDefault() + + const key = Number((e.target as HTMLButtonElement).closest('div[key]')?.getAttribute('key')) + + if (Number.isNaN(key) || !Number.isSafeInteger(key)) { + Alert('Stop fucking touching the html! We do server-side validation!') + return + } + + addToCart(key) + updateItemList() + updateTotalPriceInCart() +} + +function removeItem (e: MouseEvent) { + e.preventDefault() + + const key = Number((e.target as HTMLButtonElement).closest('div[key]')?.getAttribute('key')) + + if (Number.isNaN(key) || !Number.isSafeInteger(key)) { + Alert('Stop fucking touching the html! We do server-side validation!') + return + } + + removeFromCart(key) + updateItemList() + updateTotalPriceInCart() +} + +function updateItemList () { + itemList.querySelectorAll('.cartListElementContainer > button').forEach((button) => { + button.removeEventListener('click', button.id === 'add' ? addItem : removeItem) + }) + + itemList.innerHTML = getProductsInCart().map((product) => (` +
+ Backed Quark + ${product.name} + + ${product.quantity > 0 ? `x${product.quantity}` : ''} + + + +
+ `)).join('') + + itemList.querySelectorAll('.cartListElementContainer > button').forEach((button) => { + button.addEventListener('click', button.id === 'add' ? addItem : removeItem) + }) +} + +export const toggleCheckoutTab = (products: Product[]) => { + initializeCheckoutTab(products) + + updateTotalPriceInCart() + updateItemList() + + tab.style.display = 'flex' +} + +export const clearCheckoutTab = () => { + tab.style.display = 'none' + + itemList.querySelectorAll('.cartListElementContainer > button').forEach((button) => { + button.removeEventListener('click', button.id === 'add' ? addItem : removeItem) + }) +} + +const updateTotalPriceInCart = () => { + totalCost!.textContent = `${formatter.format(getPrice() / 100)} USD` +} diff --git a/src/purchases/ProductSubtab.ts b/src/purchases/ProductSubtab.ts new file mode 100644 index 000000000..fe5e3aa94 --- /dev/null +++ b/src/purchases/ProductSubtab.ts @@ -0,0 +1,55 @@ +import { format } from '../Synergism' +import { Alert, Notification } from '../UpdateHTML' +import type { Product } from './CartTab' +import { addToCart } from './CartUtil' + +const productContainer = document.querySelector('#pseudoCoins > #productContainer') + +const formatter = Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' +}) + +const clickHandler = (e: HTMLElementEventMap['click']) => { + const productId = Number((e.target as HTMLButtonElement).getAttribute('data-id')) + const productName = (e.target as HTMLButtonElement).getAttribute('data-name') + + if (Number.isNaN(productId) || !Number.isSafeInteger(productId)) { + Alert('Stop fucking touching the html! We do server-side validation!') + return + } + + addToCart(productId) + Notification(`Added ${productName} to the cart!`) +} + +export const initializeProductPage = (products: Product[]) => { + productContainer!.innerHTML = products.map((product) => (` +
+
+ ${product.name} +

+ ${product.name} [${format(product.coins)} PseudoCoins] +

+ +
+
+ `)).join('') + + productContainer!.style.display = 'grid' + + document.querySelectorAll('.pseudoCoinContainer > div > button[data-id]').forEach((element) => { + element.addEventListener('click', clickHandler) + }) +} + +export const clearProductPage = () => { + productContainer!.innerHTML = '' + productContainer!.style.display = 'none' + + document.querySelectorAll('.pseudoCoinContainer > div > button[data-id]').forEach((element) => { + element.removeEventListener('click', clickHandler) + }) +} diff --git a/src/purchases/UpgradesSubtab.ts b/src/purchases/UpgradesSubtab.ts new file mode 100644 index 000000000..cefb35dca --- /dev/null +++ b/src/purchases/UpgradesSubtab.ts @@ -0,0 +1,200 @@ +import i18next from 'i18next' +import { z } from 'zod' +import { DOMCacheGetOrSet } from '../Cache/DOM' +import { displayPCoinEffect, type PseudoCoinUpgradeNames, updatePCoinCache } from '../PseudoCoinUpgrades' +import { Alert } from '../UpdateHTML' +import { memoize } from '../Utility' + +interface Upgrades { + upgradeId: number + maxLevel: number + name: string + description: string + internalName: PseudoCoinUpgradeNames + level: number + cost: number +} + +interface PlayerUpgrades { + level: number + upgradeId: number + internalName: PseudoCoinUpgradeNames +} + +type UpgradesList = Omit & { + level: number[] + cost: number[] + playerLevel: number +} + +interface UpgradesResponse { + coins: number + upgrades: Upgrades[] + playerUpgrades: PlayerUpgrades[] +} + +interface CoinsResponse { + coins: number +} + +const tab = document.querySelector('#pseudoCoins > #upgradesContainer')! +let activeUpgrade: UpgradesList | undefined + +const buyUpgradeSchema = z.object({ + upgradeId: z.number(), + level: z.number() +}) + +function setActiveUpgrade (upgrade: UpgradesList | undefined) { + activeUpgrade = upgrade + + const name = i18next.t(`pseudoCoins.upgradeNames.${upgrade?.internalName}`) + + DOMCacheGetOrSet('pCoinUpgradeName').textContent = `${name ?? '???'}` + DOMCacheGetOrSet('description').textContent = `${upgrade?.description ?? '???'}` + DOMCacheGetOrSet('pCoinUpgradeIcon').setAttribute( + 'src', + `Pictures/PseudoShop/${upgrade?.internalName ?? 'PseudoCoins'}.png` + ) + + const buy = DOMCacheGetOrSet('buy') + const currEffect = DOMCacheGetOrSet('pCoinEffectCurr') + const nextEffect = DOMCacheGetOrSet('pCoinEffectNext') + + currEffect.innerHTML = `${i18next.t('pseudoCoins.currEffect')} ${ + i18next.t(displayPCoinEffect(upgrade!.internalName, upgrade!.playerLevel)) + }` + nextEffect.innerHTML = `${i18next.t('pseudoCoins.nextEffect')} ${ + i18next.t(displayPCoinEffect(upgrade!.internalName, upgrade!.playerLevel + 1)) + }` + + if (upgrade && upgrade.playerLevel === upgrade.maxLevel) { + buy?.setAttribute('disabled', '') + buy!.setAttribute('style', 'display: none') + nextEffect.setAttribute('style', 'display: none') + } else { + buy?.removeAttribute('disabled') + buy!.removeAttribute('style') + nextEffect.removeAttribute('style') + buy!.innerHTML = upgrade + ? `${ + i18next.t('pseudoCoins.buyButton', { amount: Intl.NumberFormat().format(upgrade.cost[upgrade.playerLevel]) }) + }` + : 'Cannot buy. Sorry!' + } +} + +async function purchaseUpgrade (upgrades: Map) { + if (!activeUpgrade) { + Alert('Click on an upgrade to buy it.') + return + } + + const response = await fetch(`https://synergism.cc/stripe/buy-upgrade/${activeUpgrade.upgradeId}`, { + method: 'PUT' + }) + const json = await response.json() + const parsed = buyUpgradeSchema.safeParse(json) + + if (!parsed.success) { + Alert(`Didn't buy the upgrade... try again? ${JSON.stringify(json)}`) + return + } + + const upgrade = upgrades?.get(parsed.data.upgradeId) + + if (upgrade) { + upgrade.playerLevel = parsed.data.level + Alert(`Upgraded ${upgrade.name} (${upgrade.description}) to ${parsed.data.level}!`) + + tab.querySelector('#upgradeGrid > .active > p#a')!.textContent = `${upgrade.playerLevel}/${upgrade.maxLevel}` + tab.querySelector('#upgradeGrid > .active > p#b')!.textContent = upgrade.playerLevel === upgrade.maxLevel ? '✔️' : '' + + setActiveUpgrade(upgrade) + + const response = await fetch('https://synergism.cc/stripe/coins') + const coins = await response.json() as CoinsResponse + + tab!.querySelector('#pseudoCoinAmounts > #currentCoinBalance')!.innerHTML = `${ + i18next.t('pseudoCoins.coinCount', { amount: Intl.NumberFormat().format(coins.coins) }) + }` + + updatePCoinCache(upgrade.internalName, parsed.data.level) + } else { + Alert('Upgrades did not load. Please refresh the page.') + } +} + +export const initializeUpgradeSubtab = memoize(() => { + ;(async () => { + const response = await fetch('https://synergism.cc/stripe/upgrades') + const upgradesList = await response.json() as UpgradesResponse + + DOMCacheGetOrSet('currentCoinBalance').innerHTML = `${ + i18next.t('pseudoCoins.coinCount', { amount: Intl.NumberFormat().format(upgradesList.coins) }) + }` + const grouped = upgradesList.upgrades.reduce((map, upgrade) => { + const current = map.get(upgrade.upgradeId) + const playerUpgrade = upgradesList.playerUpgrades.find((v) => v.upgradeId === upgrade.upgradeId) + + if (!current) { + map.set(upgrade.upgradeId, { + ...upgrade, + cost: [upgrade.cost], + level: [upgrade.level], + playerLevel: playerUpgrade?.level ?? 0 + }) + } else { + current.maxLevel = Math.max(current.maxLevel, upgrade.maxLevel) + current.cost.push(upgrade.cost) + current.level.push(upgrade.level) + } + + return map + }, new Map()) + + tab.querySelector('#upgradeGrid')!.innerHTML = [...grouped.values()].map((u) => ` +
+ ${u.internalName} +

${u.playerLevel}/${u.maxLevel}

+ ${u.playerLevel === u.maxLevel ? '

✔️

' : '

'} +
+ `).join('') + + const upgradesInGrid = tab.querySelectorAll('#upgradeGrid > div[data-id]') + upgradesInGrid.forEach((element) => { + element.addEventListener('click', (e) => { + const upgradeId = Number((e.target as HTMLElement).closest('div')?.getAttribute('data-id')) + + if (Number.isNaN(upgradeId) || !Number.isSafeInteger(upgradeId)) { + Alert('Stop touching the fucking html! We do server-side validations!') + return + } + + setActiveUpgrade([...grouped.values()].find((u) => u.upgradeId === upgradeId)) + + // Setting an active class here turns the border white due to a CSS rule + upgradesInGrid.forEach((u) => u.classList.remove('active')) + element.classList.add('active') + }) + }) + + DOMCacheGetOrSet('buy').addEventListener('click', () => { + purchaseUpgrade(grouped) + }) + })() +}) + +export const toggleUpgradeSubtab = () => { + initializeUpgradeSubtab() + + tab.style.display = 'flex' +} + +export const clearUpgradeSubtab = () => { + tab.style.display = 'none' +} diff --git a/src/types/Synergism.d.ts b/src/types/Synergism.d.ts index 187d132e6..ac0e4bba8 100644 --- a/src/types/Synergism.d.ts +++ b/src/types/Synergism.d.ts @@ -1060,3 +1060,19 @@ export type FirstToFifth = GlobalVariables['ordinals'][ZeroToFour] export type FirstToEighth = GlobalVariables['ordinals'][ZeroToSeven] export type SaveSupplier = Map Player[K]> + +export type PseudoCoinUpgradeNames = + | 'INSTANT_UNLOCK_1' + | 'INSTANT_UNLOCK_2' + | 'CUBE_BUFF' + | 'AMBROSIA_LUCK_BUFF' + | 'AMBROSIA_GENERATION_BUFF' + | 'GOLDEN_QUARK_BUFF' + | 'FREE_UPGRADE_PROMOCODE_BUFF' + | 'CORRUPTION_LOADOUT_SLOT_QOL' + | 'AMBROSIA_LOADOUT_SLOT_QOL' + | 'AUTO_POTION_FREE_POTIONS_QOL' + | 'OFFLINE_TIMER_CAP_BUFF' + | 'ADD_CODE_CAP_BUFF' + +export type PseudoCoinUpgrades = Record diff --git a/translations/en.json b/translations/en.json index d000d0893..fa474fcc4 100644 --- a/translations/en.json +++ b/translations/en.json @@ -2436,7 +2436,8 @@ "singularity": "Singularity", "settings": "Settings", "shop": "Shop", - "unsmith": "UNSMITH" + "unsmith": "UNSMITH", + "purchase": "PseudoCoins" }, "buildings": { "coin": "Coin Buildings", @@ -2478,6 +2479,10 @@ "singularityHistory": "Singularity History", "hotkeys": "Hotkeys", "account": "Account" + }, + "pseudocoins": { + "buy": "Purchase Coins", + "upgrades": "Buy Upgrades" } }, "offlineProgress": { @@ -3547,5 +3552,41 @@ }, "account": { "logout": "The page will now reload. You are logged out! Goodbye!!" + }, + "pseudoCoins": { + "coinCount": "You have <>!", + "cost": "Cost: <>", + "maxxed": "<>", + "buyButton": "Buy! <>", + "currEffect": "Current Level:", + "nextEffect": "Next Level:", + "upgradeNames": { + "INSTANT_UNLOCK_1": "The Talisman Activator-inator", + "INSTANT_UNLOCK_2": "The Rune Discoverer-inator", + "CUBE_BUFF": "Gold-Infused Cubes", + "AMBROSIA_LUCK_BUFF": "The Gold Leaf Clovers", + "AMBROSIA_GENERATION_BUFF": "Alchemized Ambrosia Speedup", + "GOLDEN_QUARK_BUFF": "Even Golder Quarks", + "FREE_UPGRADE_PROMOCODE_BUFF": "Bribing the Daily Code", + "CORRUPTION_LOADOUT_SLOT_QOL": "More Corruption Loadouts!", + "AMBROSIA_LOADOUT_SLOT_QOL": "More Ambrosia Loadouts!", + "AUTO_POTION_FREE_POTIONS_QOL": "Potion Duplication Glitch", + "OFFLINE_TIMER_CAP_BUFF": "Cozier Beds Enshrined in Gold", + "ADD_CODE_CAP_BUFF": "The Limited-Edition PLAT-P Calculator" + }, + "upgradeEffects": { + "INSTANT_UNLOCK_1": "The Plastic Talisman is <> always unlocked! <>", + "INSTANT_UNLOCK_2": "The Rune of Infinite Ascent is <> always unlocked! <>", + "CUBE_BUFF": "Cube Gain is multiplied by <>", + "AMBROSIA_LUCK_BUFF": "Ambrosia Luck is increased by <>", + "AMBROSIA_GENERATION_BUFF": "Blueberry Generation Speed is multiplied by <>", + "GOLDEN_QUARK_BUFF": "Golden Quark Gain is multiplied by <>", + "FREE_UPGRADE_PROMOCODE_BUFF": "Code 'Daily' gives <> the Free Upgrades!", + "CORRUPTION_LOADOUT_SLOT_QOL": "You have <> additional Corruption Loadout slots!", + "AMBROSIA_LOADOUT_SLOT_QOL": "You have <> additional Ambrosia Loadout slots!", + "AUTO_POTION_FREE_POTIONS_QOL": "Auto-Potion is <> FREE!", + "OFFLINE_TIMER_CAP_BUFF": "Offline Time Capacity is multiplied by <>", + "ADD_CODE_CAP_BUFF": "Code 'add' Capacity is multiplied by <>" + } } } diff --git a/translations/source.json b/translations/source.json index 653988c54..0290e144e 100644 --- a/translations/source.json +++ b/translations/source.json @@ -2436,7 +2436,8 @@ "singularity": "Singularity", "settings": "Settings", "shop": "Shop", - "unsmith": "UNSMITH" + "unsmith": "UNSMITH", + "purchase": "PseudoCoins" }, "buildings": { "coin": "Coin Buildings", @@ -2478,6 +2479,10 @@ "singularityHistory": "Singularity History", "hotkeys": "Hotkeys", "account": "Account" + }, + "pseudocoins": { + "buy": "Purchase Coins", + "upgrades": "Buy Upgrades" } }, "offlineProgress": { @@ -3547,5 +3552,41 @@ }, "account": { "logout": "The page will now reload. You are logged out! Goodbye!!" + }, + "pseudoCoins": { + "coinCount": "You have <>!", + "cost": "Cost: <>", + "maxxed": "<>", + "buyButton": "Buy! <>", + "currEffect": "Current Level:", + "nextEffect": "Next Level:", + "upgradeNames": { + "INSTANT_UNLOCK_1": "The Talisman Activator-inator", + "INSTANT_UNLOCK_2": "The Rune Discoverer-inator", + "CUBE_BUFF": "Gold-Infused Cubes", + "AMBROSIA_LUCK_BUFF": "The Gold Leaf Clovers", + "AMBROSIA_GENERATION_BUFF": "Alchemized Ambrosia Speedup", + "GOLDEN_QUARK_BUFF": "Even Golder Quarks", + "FREE_UPGRADE_PROMOCODE_BUFF": "Bribing the Daily Code", + "CORRUPTION_LOADOUT_SLOT_QOL": "More Corruption Loadouts!", + "AMBROSIA_LOADOUT_SLOT_QOL": "More Ambrosia Loadouts!", + "AUTO_POTION_FREE_POTIONS_QOL": "Potion Duplication Glitch", + "OFFLINE_TIMER_CAP_BUFF": "Cozier Beds Enshrined in Gold", + "ADD_CODE_CAP_BUFF": "The Limited-Edition PLAT-P Calculator" + }, + "upgradeEffects": { + "INSTANT_UNLOCK_1": "The Plastic Talisman is <> always unlocked! <>", + "INSTANT_UNLOCK_2": "The Rune of Infinite Ascent is <> always unlocked! <>", + "CUBE_BUFF": "Cube Gain is multiplied by <>", + "AMBROSIA_LUCK_BUFF": "Ambrosia Luck is increased by <>", + "AMBROSIA_GENERATION_BUFF": "Blueberry Generation Speed is multiplied by <>", + "GOLDEN_QUARK_BUFF": "Golden Quark Gain is multiplied by <>", + "FREE_UPGRADE_PROMOCODE_BUFF": "Code 'Daily' gives <> the Free Upgrades!", + "CORRUPTION_LOADOUT_SLOT_QOL": "You have <> additional Corruption Loadout slots!", + "AMBROSIA_LOADOUT_SLOT_QOL": "You have <> additional Ambrosia Loadout slots!", + "AUTO_POTION_FREE_POTIONS_QOL": "Auto-Potion is <> FREE!", + "OFFLINE_TIMER_CAP_BUFF": "Offline Time Capacity is multiplied by <>", + "ADD_CODE_CAP_BUFF": "Code 'add' Capacity is multiplied by <>" + } } } diff --git a/tsconfig.json b/tsconfig.json index 7eb862540..b4f1a09d0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,9 @@ ], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment", // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */