diff --git a/Pictures/PseudoShop/HAPPY_HOUR_BELL.png b/Pictures/PseudoShop/HAPPY_HOUR_BELL.png new file mode 100644 index 000000000..ea031d036 Binary files /dev/null and b/Pictures/PseudoShop/HAPPY_HOUR_BELL.png differ diff --git a/Synergism.css b/Synergism.css index e018a391a..472d0f34a 100644 --- a/Synergism.css +++ b/Synergism.css @@ -3789,8 +3789,78 @@ img#singularityPerksIcon { #event { position: relative; - padding: 0; + padding: 20px; text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + width: 100%; +} + +.event-container { + border-radius: 8px; + padding: 15px; +} + +.event-title { + margin: 0 0 10px; + color: #ffd700; +} + +.event-bonus, +.event-timer { + margin: 5px 0; +} + +#globalEventBonus, +#consumableEventBonus, +#globalEventTimer, +#consumableEventTimer { + color: #8f8; +} + +#eventBuffContainer, +#consumableBuffContainer { + display: flex; + flex-direction: row; + justify-content: center; +} + +.eventBuffVal, +.consumableBuffVal { + display: flex; + align-items: center; +} + +.eventBuffVal img, +.consumableBuffVal img { + margin: 0; + padding: 0; + height: 32px; + width: 32px; +} + +.eventBuffVal p, +.consumableBuffVall p { + margin: 0; + padding: 0; +} + +.consumableButton, +#apply-tips { + border: 2px solid #ffd700; + background-color: #ffd700; + color: black; + cursor: pointer; + padding: 5px; + margin: 5px; + width: 150px; +} + +#apply-tips:not(:hover) { + border: 2px solid var(--hovershop-color); + background-color: var(--hovershop-color); } @keyframes rotation { @@ -3803,23 +3873,7 @@ img#singularityPerksIcon { } } -#unsmith { - margin: 15px; - gap: 10px 15px; - height: auto; - width: 200px; - max-width: 500%; - animation: rotation 2s infinite linear; -} - -.hoveredBlueberryLoadout1 div.bbPurchasedLoadout1, -.hoveredBlueberryLoadout2 div.bbPurchasedLoadout2, -.hoveredBlueberryLoadout3 div.bbPurchasedLoadout3, -.hoveredBlueberryLoadout4 div.bbPurchasedLoadout4, -.hoveredBlueberryLoadout5 div.bbPurchasedLoadout5, -.hoveredBlueberryLoadout6 div.bbPurchasedLoadout6, -.hoveredBlueberryLoadout7 div.bbPurchasedLoadout7, -.hoveredBlueberryLoadout8 div.bbPurchasedLoadout8 { +*[class^="hoveredBlueberryLoadout"] div[class^="bbPurchasedLoadout"] { background-color: green; } @@ -4146,6 +4200,40 @@ form input:hover { margin-top: 10px; } +#consumablesGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(1, 1fr); + gap: 10px; + width: 70%; + margin: 0 auto; +} + +#consumablesGrid > div { + border: 2px solid var(--amber-text-color); + padding: 5px; + box-sizing: border-box; +} + +#consumablesGrid > div > button { + display: flex; + align-items: center; + white-space: pre; + padding: 0 5px; + box-sizing: border-box; + border: 2px solid var(--amber-text-color); + margin: 5px auto 0; +} + +/* Cost text */ +#consumablesGrid > div > button > :nth-child(2) { + color: var(--amber-text-color); +} + +#consumablesGrid > div > * { + margin: 0; +} + #upgradeGrid { display: grid; grid-template-columns: repeat(6, 15%); diff --git a/index.html b/index.html index 0b18b22cd..80d4dd2b5 100644 --- a/index.html +++ b/index.html @@ -110,7 +110,7 @@ - +
@@ -3045,8 +3045,6 @@
- -
Buffs:
@@ -3283,6 +3281,7 @@

Artists

Forbidden Clock of Time:0

EXALT 6: The Great Singularity Speedrun0

Shop Chronometer S0

+

Event0

TOTAL GLOBAL SPEED MULTIPLIER:0

@@ -4682,9 +4681,135 @@

Artists

- -

- +
+

Global Event

+
+
+ Quarks +

+10%

+
+
+ Golden Quarks +

+10%

+
+
+ Cube Bonus +

+10%

+
+
+ Octeract Bonus +

+10%

+
+
+ Ascension Speed +

+10%

+
+
+ Global Speed +

+10%

+
+
+ Obtainium +

+10%

+
+
+ Offering +

+10%

+
+
+ Ascension Score +

+10%

+
+
+ Blueberry Generation Speed +

+10%

+
+
+ Ambrosia Luck +

+10%

+
+
+ Powder Conversion +

+10%

+
+
+ Ant Sacrifice +

+10%

+
+
+ One Mind Bonus +

+10%

+
+
+
Time Remaining: --:--:--
+
+ +
+

+
Current Amount: No active consumable
+
Ends At: --:--:--
+
+
+ +

+10%

+
+
+ +

+10%

+
+
+ +

+10%

+
+
+ +

+10%

+
+
+ +

+10%

+
+
+ +

+10%

+
+
+ +

+10%

+
+
+ +

+10%

+
+
+ +

+10%

+
+
+ +

+10%

+
+
+ +

+10%

+
+
+ +

+10%

+
+
+ +

+10%

+
+
+ +

+10%

+
+
+
+ + +
@@ -4704,9 +4829,14 @@

Artists

style="border: 2px solid cyan" i18n="tabs.pseudocoins.upgrades" > + @@ -4758,6 +4888,8 @@

Welcome to the PseudoShop!

+
+
{ } DOMCacheGetOrSet(`ach${num}`).style.backgroundColor = 'Green' - Synergism.emit('achievement', num) } diff --git a/src/Ants.ts b/src/Ants.ts index 3b025319f..8438c2b1b 100644 --- a/src/Ants.ts +++ b/src/Ants.ts @@ -14,8 +14,7 @@ import Decimal from 'break_infinity.js' import i18next from 'i18next' import { achievementaward } from './Achievements' import { DOMCacheGetOrSet } from './Cache/DOM' -import { Synergism } from './Events' -import type { ResetHistoryEntryAntSacrifice } from './History' +import { resetHistoryAdd, type ResetHistoryEntryAntSacrifice } from './History' import { buyResearch } from './Research' import { resetAnts } from './Reset' import { Tabs } from './Tabs' @@ -409,7 +408,7 @@ export const sacrificeAnts = async (auto = false) => { } calculateAntSacrificeELO() - Synergism.emit('historyAdd', 'ants', historyEntry) + resetHistoryAdd('ants', historyEntry) } } diff --git a/src/Calculate.ts b/src/Calculate.ts index 0fed84a29..15165d7aa 100644 --- a/src/Calculate.ts +++ b/src/Calculate.ts @@ -517,9 +517,6 @@ export function calculateOfferings ( return productContents(arr) } - if (G.eventClicked && G.isEvent) { - q *= 1.05 - } q /= calculateSingularityDebuff('Offering') if (player.currentChallenge.ascension === 15) { q *= 1 + 7 * player.cubeUpgrades[62] @@ -673,10 +670,6 @@ export const calculateObtainium = () => { G.obtainiumGain *= Math.pow(1.02, player.shopUpgrades.obtainiumEX3) G.obtainiumGain *= calculateTotalOcteractObtainiumBonus() - if (G.eventClicked && G.isEvent) { - G.obtainiumGain *= 1.05 - } - if (player.currentChallenge.ascension === 15) { G.obtainiumGain += 1 G.obtainiumGain *= 1 + 7 * player.cubeUpgrades[62] @@ -1363,7 +1356,10 @@ export const timeWarp = async () => { calculateOffline(timeUse) } -export const calculateOffline = async (forceTime = 0) => { +/** + * @param forceTime The number of SECONDS to warp. Why the fuck is it in seconds? + */ +export const calculateOffline = (forceTime = 0) => { disableHotkeys() G.timeWarp = true @@ -1818,9 +1814,8 @@ export const calculateAllCubeMultiplier = () => { // Total Global Cube Multipliers: 34 ] - const extraMult = G.isEvent && G.eventClicked ? 1.05 : 1 return { - mult: productContents(arr) * extraMult, + mult: productContents(arr), list: arr } } @@ -2181,13 +2176,11 @@ export const octeractGainPerSecond = () => { +player.octeractUpgrades.octeractOneMindImprover.getEffect().bonus ) : 1 - const extraMult = G.isEvent && G.eventClicked ? 1.05 : 1 const perSecond = (1 / (24 * 3600 * 365 * 1e15)) * baseMultiplier * productContents(valueMultipliers) * ascensionSpeed * oneMindModifier - * extraMult return perSecond } @@ -2277,7 +2270,8 @@ export const calculateTimeAcceleration = () => { + +player.octeractUpgrades.octeractImprovedGlobalSpeed.getEffect().bonus * player.singularityCount, 1 + +player.singularityChallenges.limitedTime.rewards.globalSpeed, // Limited Time Challenge - Math.max(Math.pow(1.01, (player.singularityCount - 200) * player.shopUpgrades.shopChronometerS), 1) // Limited Time Upg Accels + Math.max(Math.pow(1.01, (player.singularityCount - 200) * player.shopUpgrades.shopChronometerS), 1), // Limited Time Upg Accels + 1 + calculateEventBuff(BuffType.GlobalSpeed) // Event ] const timeMult = productContents(preCorruptionArr) @@ -3496,9 +3490,9 @@ export const forcedDailyReset = (rewards = false) => { } export const calculateEventBuff = (buff: BuffType) => { - if (!G.isEvent) { - return 0 - } + // if (!G.isEvent) { + // return 0 + // } return calculateEventSourceBuff(buff) } diff --git a/src/Event.ts b/src/Event.ts index 420012bec..ffb485060 100644 --- a/src/Event.ts +++ b/src/Event.ts @@ -1,8 +1,9 @@ -import i18next from 'i18next' import { DOMCacheGetOrSet } from './Cache/DOM' import { calculateAdditiveLuckMult, calculateAmbrosiaGenerationSpeed, calculateAmbrosiaLuck } from './Calculate' -import { format, getTimePinnedToLoadDate, player } from './Synergism' -import { Alert, revealStuff } from './UpdateHTML' +import { activeConsumables, type PseudoCoinConsumableNames } from './Login' +import { getTimePinnedToLoadDate, player } from './Synergism' +import { revealStuff } from './UpdateHTML' +import { timeReminingHours } from './Utility' import { Globals as G } from './Variables' export enum BuffType { @@ -45,7 +46,6 @@ interface GameEvent { } let nowEvent: GameEvent | null = null - export const getEvent = () => nowEvent export const eventCheck = async () => { @@ -64,52 +64,16 @@ export const eventCheck = async () => { nowEvent = null const now = new Date(getTimePinnedToLoadDate()).getTime() - if (now >= apiEvents.start && now <= apiEvents.end && apiEvents.name.length) { nowEvent = apiEvents } - const happyHolidays = DOMCacheGetOrSet('happyHolidays') as HTMLAnchorElement - const eventBuffs = DOMCacheGetOrSet('eventBuffs') + const eventNowEndDate = new Date(nowEvent?.end ?? 0) + DOMCacheGetOrSet('globalEventTimer').textContent = timeReminingHours(eventNowEndDate) + const updateIsEventCheck = G.isEvent - if (nowEvent) { - G.isEvent = true - const buffs: string[] = [] - - for (let i = 0; i < eventBuffType.length; i++) { - const eventBuff = calculateEventSourceBuff(BuffType[eventBuffType[i]]) - - if (eventBuff !== 0) { - if (eventBuffType[i] === 'OneMind' && player.singularityUpgrades.oneMind.level > 0) { - buffs.push( - `${eventBuff >= 0 ? '+' : '-'}${format(100 * eventBuff, 3, true)}% ${ - eventBuffName[i] - }` - ) - } else if (eventBuffType[i] !== 'OneMind' || player.singularityUpgrades.oneMind.level === 0) { - buffs.push(`${eventBuff >= 0 ? '+' : '-'}${format(100 * eventBuff, 2, true)}% ${eventBuffName[i]}`) - } - } - } - - DOMCacheGetOrSet('eventCurrent').textContent = i18next.t('settings.events.activeUntil', { - x: new Date(nowEvent.end) - }) - - eventBuffs.innerHTML = G.isEvent && buffs.length ? `Current Buffs: ${buffs.join(', ')}` : '' - // eventBuffs.style.color = 'lime'; - happyHolidays.innerHTML = `(${nowEvent.name.length}) ${nowEvent.name.join(', ')}` - happyHolidays.style.color = nowEvent.color[Math.floor(Math.random() * nowEvent.color.length)] - happyHolidays.href = nowEvent.url.length > 0 ? nowEvent.url[Math.floor(Math.random() * nowEvent.url.length)] : '#' - } else { - G.isEvent = false - DOMCacheGetOrSet('eventCurrent').innerHTML = i18next.t('settings.events.inactive') - eventBuffs.textContent = '' - eventBuffs.style.color = 'var(--red-text-color)' - happyHolidays.innerHTML = '' - happyHolidays.href = '' - } + updateGlobalsIsEvent() if (G.isEvent !== updateIsEventCheck) { revealStuff() @@ -119,7 +83,7 @@ export const eventCheck = async () => { } } -const eventBuffType: (keyof typeof BuffType)[] = [ +export const eventBuffType: (keyof typeof BuffType)[] = [ 'Quark', 'GoldenQuark', 'Cubes', @@ -135,24 +99,12 @@ const eventBuffType: (keyof typeof BuffType)[] = [ 'AmbrosiaLuck', 'OneMind' ] -const eventBuffName = [ - 'Quarks', - 'Golden Quarks', - 'Cubes from all type', - 'Powder Conversion', - 'Ascension Speed', - 'Global Speed', - 'Ascension Score', - 'Ant Sacrifice rewards', - 'Offering', - 'Obtainium', - 'Eight Dimensional Hypercubes', - 'Blueberry Time Generation', - 'Ambrosia Luck (Additive Mult)', - 'One Mind Quark Bonus' -] export const calculateEventSourceBuff = (buff: BuffType): number => { + return getEventBuff(buff) + consumableEventBuff(buff) +} + +export const getEventBuff = (buff: BuffType): number => { const event = getEvent() if (event === null) { @@ -161,38 +113,86 @@ export const calculateEventSourceBuff = (buff: BuffType): number => { switch (buff) { case BuffType.Quark: - return event.quark ?? 0 + return event.quark case BuffType.GoldenQuark: - return event.goldenQuark ?? 0 + return event.goldenQuark case BuffType.Cubes: - return event.cubes ?? 0 + return event.cubes case BuffType.PowderConversion: - return event.powderConversion ?? 0 + return event.powderConversion case BuffType.AscensionSpeed: - return event.ascensionSpeed ?? 0 + return event.ascensionSpeed case BuffType.GlobalSpeed: - return event.globalSpeed ?? 0 + return event.globalSpeed case BuffType.AscensionScore: - return event.ascensionScore ?? 0 + return event.ascensionScore case BuffType.AntSacrifice: - return event.antSacrifice ?? 0 + return event.antSacrifice case BuffType.Offering: - return event.offering ?? 0 + return event.offering case BuffType.Obtainium: - return event.obtainium ?? 0 + return event.obtainium case BuffType.Octeract: - return event.octeract ?? 0 + return event.octeract case BuffType.OneMind: return player.singularityUpgrades.oneMind.level > 0 ? event.oneMind : 0 case BuffType.BlueberryTime: - return event.blueberryTime ?? 0 + return event.blueberryTime case BuffType.AmbrosiaLuck: - return event.ambrosiaLuck ?? 0 + return event.ambrosiaLuck } } -export const clickSmith = (): Promise => { - G.eventClicked = true - DOMCacheGetOrSet('eventClicked').style.display = 'block' - return Alert(i18next.t('event.aprilFools.clicked')) +export const consumableEventBuff = (buff: BuffType) => { + const { HAPPY_HOUR_BELL } = activeConsumables + // The interval is the number of events queued excluding the first. + const happyHourInterval = HAPPY_HOUR_BELL - 1 + + // If no consumable is active, early return + if (HAPPY_HOUR_BELL === 0) { + return 0 + } + + switch (buff) { + case BuffType.Quark: + return HAPPY_HOUR_BELL ? 0.25 + 0.025 * happyHourInterval : 0 + case BuffType.GoldenQuark: + return 0 + case BuffType.Cubes: + return HAPPY_HOUR_BELL ? 0.5 + 0.05 * happyHourInterval : 0 + case BuffType.PowderConversion: + return 0 + case BuffType.AscensionSpeed: + return 0 + case BuffType.GlobalSpeed: + return 0 + case BuffType.AscensionScore: + return 0 + case BuffType.AntSacrifice: + return 0 + case BuffType.Offering: + return HAPPY_HOUR_BELL ? 0.5 + 0.05 * happyHourInterval : 0 + case BuffType.Obtainium: + return HAPPY_HOUR_BELL ? 0.5 + 0.05 * happyHourInterval : 0 + case BuffType.Octeract: + return 0 + case BuffType.OneMind: + return 0 + case BuffType.BlueberryTime: + return HAPPY_HOUR_BELL ? 0.1 + 0.01 * happyHourInterval : 0 + case BuffType.AmbrosiaLuck: + return HAPPY_HOUR_BELL ? 0.1 + 0.01 * happyHourInterval : 0 + } +} + +const isConsumableActive = (name?: PseudoCoinConsumableNames) => { + if (typeof name === 'string') { + return activeConsumables[name] > 0 + } + + return activeConsumables.HAPPY_HOUR_BELL !== 0 +} + +export const updateGlobalsIsEvent = () => { + return G.isEvent = getEvent() !== null || isConsumableActive() } diff --git a/src/EventListeners.ts b/src/EventListeners.ts index be56349ae..194ebf90f 100644 --- a/src/EventListeners.ts +++ b/src/EventListeners.ts @@ -32,7 +32,6 @@ import { challengeDisplay, toggleRetryChallenges } from './Challenges' import { testing } from './Config' import { corruptionCleanseConfirm, corruptionDisplay } from './Corruptions' import { buyCubeUpgrades, cubeUpgradeDesc } from './Cubes' -import { clickSmith } from './Event' import { hepteractDescriptions, hepteractToOverfluxOrbDescription, @@ -55,6 +54,7 @@ import { resetGame, updateSaveString } from './ImportExport' +import { getTips, sendToWebsocket, setTips } from './Login' import { buyPlatonicUpgrades, createPlatonicDescription } from './Platonic' import { buyResearch, researchDescriptions } from './Research' import { resetrepeat, updateAutoCubesOpens, updateAutoReset, updateTesseractAutoBuyAmount } from './Reset' @@ -64,7 +64,7 @@ import { buyGoldenQuarks, getLastUpgradeInfo, singularityPerks } from './singula import { displayStats } from './Statistics' import { generateExportSummary } from './Summary' import { player, resetCheck, saveSynergy } from './Synergism' -import { changeSubTab, Tabs } from './Tabs' +import { changeSubTab, changeTab, Tabs } from './Tabs' import { buyAllTalismanResources, buyTalismanEnhance, @@ -117,7 +117,7 @@ import { updateRuneBlessingBuyAmount } from './Toggles' import type { OneToFive, Player } from './types/Synergism' -import { Confirm } from './UpdateHTML' +import { Confirm, Prompt } from './UpdateHTML' import { shopMouseover } from './UpdateVisuals' import { buyConstantUpgrades, @@ -1094,8 +1094,25 @@ TODO: Fix this entire tab it's utter shit ) } - // EVENT TAB (Replace as events are created) - DOMCacheGetOrSet('unsmith').addEventListener('click', () => clickSmith()) + // EVENT TAB + document.querySelector('.consumableButton')?.addEventListener('click', () => { + changeTab(Tabs.Purchase) + changeSubTab(Tabs.Purchase, { page: 3 }) + }) + + document.getElementById('apply-tips')?.addEventListener('click', () => { + Prompt(i18next.t('pseudoCoins.consumables.applyTipsPrompt', { tips: getTips() })) + .then((amount) => { + const n = Number(amount) + + if (Number.isNaN(n) || !Number.isSafeInteger(n) || n <= 0 || n > getTips()) { + return + } + + sendToWebsocket(JSON.stringify({ type: 'applied-tip', amount: n })) + setTips(getTips() - n) + }) + }) // Import button DOMCacheGetOrSet('importfile').addEventListener('change', (e) => importData(e, importSynergism)) diff --git a/src/Events.ts b/src/Events.ts deleted file mode 100644 index 983bd54f2..000000000 --- a/src/Events.ts +++ /dev/null @@ -1,5 +0,0 @@ -// EventTarget is lacking. -import EventEmitter from 'eventemitter3' -import type { SynergismEvents } from './types/Synergism' - -export const Synergism = new EventEmitter() diff --git a/src/History.ts b/src/History.ts index 865342cb2..abf1fb887 100644 --- a/src/History.ts +++ b/src/History.ts @@ -4,7 +4,6 @@ import i18next from 'i18next' import { antSacrificePointsToMultiplier } from './Ants' import { DOMCacheGetOrSet } from './Cache/DOM' import { applyCorruptions } from './Corruptions' -import { Synergism } from './Events' import { format, formatTimeShort, player } from './Synergism' import { IconSets } from './Themes' import { Notification } from './UpdateHTML' @@ -351,7 +350,7 @@ const extractStringExponent = (str: string) => { } // Add an entry to the history. This can be called via the event system. -const resetHistoryAdd = (category: Category, data: ResetHistoryEntryUnion) => { +export const resetHistoryAdd = (category: Category, data: ResetHistoryEntryUnion) => { while (player.history[category].length > (G.historyCountMax - 1)) { player.history[category].shift() } @@ -360,8 +359,6 @@ const resetHistoryAdd = (category: Category, data: ResetHistoryEntryUnion) => { resetHistoryPushNewRow(category, data) } -Synergism.on('historyAdd', resetHistoryAdd) - // Add a row to the table, shifting out old ones as required. const resetHistoryPushNewRow = (category: Category, data: ResetHistoryEntryUnion) => { const row = resetHistoryRenderRow(category, data) diff --git a/src/ImportExport.ts b/src/ImportExport.ts index 6ae29aea5..f3e55d396 100644 --- a/src/ImportExport.ts +++ b/src/ImportExport.ts @@ -5,7 +5,6 @@ import { achievementaward } from './Achievements' import { DOMCacheGetOrSet } from './Cache/DOM' 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' @@ -969,8 +968,6 @@ export const promocodes = async (input: string | null, amount?: number) => { return } - Synergism.emit('promocode', input) - setTimeout(() => (el.textContent = ''), 15000) } diff --git a/src/Login.ts b/src/Login.ts index d4c77a7b8..7cea539cd 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -1,11 +1,17 @@ /// import i18next from 'i18next' +import { z } from 'zod' import { DOMCacheGetOrSet } from './Cache/DOM' +import { calculateOffline } from './Calculate' +import { updateGlobalsIsEvent } from './Event' import { importSynergism } from './ImportExport' import { QuarkHandler, setQuarkBonus } from './Quark' import { format, player } from './Synergism' -import { Alert } from './UpdateHTML' +import { Alert, Notification } from './UpdateHTML' +import { assert } from './Utility' + +export type PseudoCoinConsumableNames = 'HAPPY_HOUR_BELL' // Consts for Patreon Supporter Roles. const TRANSCENDED_BALLER = '756419583941804072' @@ -26,8 +32,58 @@ const SMITH_GOD = '1045562390995009606' const GOLDEN_SMITH_GOD = '1178125584061173800' const DIAMOND_SMITH_MESSIAH = '1311165096378105906' +let ws: WebSocket | undefined let loggedIn = false +let tips = 0 + export const isLoggedIn = () => loggedIn +export const getTips = () => tips +export const setTips = (newTips: number) => tips = newTips + +export const activeConsumables: Record = { + HAPPY_HOUR_BELL: 0 +} + +export let happyHourEndTime = 0 + +const messageSchema = z.preprocess( + (data, ctx) => { + if (typeof data === 'string') { + try { + return JSON.parse(data) + } catch {} + } + + ctx.addIssue({ code: 'custom', message: 'Invalid message received.' }) + }, + z.union([ + /** Received after the user connects to the websocket */ + z.object({ type: z.literal('join') }), + z.object({ type: z.literal('error'), message: z.string() }), + /** Received after a consumable is redeemed (broadcasted to everyone) */ + z.object({ type: z.literal('consumed'), consumable: z.string(), startedAt: z.number().int() }), + /** Received after a consumable ends (broadcasted to everyone) */ + z.object({ type: z.literal('consumable-ended'), consumable: z.string(), endedAt: z.number().int() }), + /** Information about currently active consumables */ + z.object({ + type: z.literal('info'), + active: z.object({ + name: z.string(), + internalName: z.string(), + amount: z.number().int(), + endsAt: z.number().int() + }).array(), + tips: z.number().int().nonnegative() + }), + /** Received after the *user* successfully redeems a consumable. */ + z.object({ type: z.literal('thanks') }), + /** Received when a user is tipped */ + z.object({ type: z.literal('tips'), tips: z.number().int() }), + /** Received when a user reconnects, if there are unclaimed tips */ + z.object({ type: z.literal('tip-backlog'), tips: z.number().int() }), + z.object({ type: z.literal('applied-tip'), amount: z.number(), remaining: z.number() }) + ]) +) /** * @see https://discord.com/developers/docs/resources/user#user-object @@ -259,6 +315,111 @@ export async function handleLogin () { renderCaptcha() }) } + + if (loggedIn) { + handleWebSocket() + } +} + +const queue: string[] = [] + +/** + * Delays before attempting to re-establish the connection after the socket closes. + * The delay is reset after a successful connection. + */ +const exponentialBackoff = [5000, 15000, 30000, 60000] +let tries = 0 + +function handleWebSocket () { + assert(!ws || ws.readyState === WebSocket.CLOSED, 'WebSocket has been set and is not closed') + + ws = new WebSocket('wss://synergism.cc/consumables/connect') + + ws.addEventListener('close', () => { + const delay = exponentialBackoff[++tries] + + if (delay !== undefined) { + setTimeout(() => handleWebSocket(), delay) + } else { + Notification( + 'Could not re-establish your connection. Consumables and events related to Consumables will not work.' + ) + } + }) + + ws.addEventListener('open', () => { + tries = 0 + + for (const message of queue) { + ws?.send(message) + } + + queue.length = 0 + }) + + ws.addEventListener('message', (ev) => { + const data = messageSchema.parse(ev.data) + console.log(data) + + if (data.type === 'error') { + Notification(data.message, 5_000) + } else if (data.type === 'consumed') { + activeConsumables[data.consumable as PseudoCoinConsumableNames]++ + Notification(`Someone redeemed a(n) ${data.consumable}!`) + } else if (data.type === 'consumable-ended') { + activeConsumables[data.consumable as PseudoCoinConsumableNames]-- + Notification(`A(n) ${data.consumable} ended!`) + } else if (data.type === 'join') { + Notification('Connection was established!') + } else if (data.type === 'info') { + if (data.active.length !== 0) { + let message = 'The following consumables are active:\n' + let ends = 0 + + for (const { amount, internalName, name, endsAt } of data.active) { + activeConsumables[internalName as PseudoCoinConsumableNames] = amount + message += `${name} (x${amount})` + ends = Math.max(ends, endsAt) + } + + happyHourEndTime = ends + + Notification(message) + updateEventsPage(ends) + } + + tips = data.tips + } else if (data.type === 'thanks') { + Alert(i18next.t('pseudoCoins.consumables.thanks')) + } else if (data.type === 'tip-backlog' || data.type === 'tips') { + tips += data.tips + + Notification(i18next.t('pseudoCoins.consumables.tipReceived', { offlineTime: data.tips })) + } else if (data.type === 'applied-tip') { + tips = data.remaining + calculateOffline(data.amount * 60) + DOMCacheGetOrSet('exitOffline').style.visibility = 'unset' + } + + updateGlobalsIsEvent() + }) +} + +function updateEventsPage (endsAt: number) { + const amount = document.getElementById('consumableEventBonus')! + const timer = document.getElementById('consumableEventTimer')! + + timer.textContent = new Date(endsAt).toLocaleString() + amount.textContent = `${Object.values(activeConsumables).reduce((a, b) => a + b, 0)}` +} + +export function sendToWebsocket (message: string) { + if (ws?.readyState !== WebSocket.OPEN) { + queue.push(message) + return + } + + ws.send(message) } async function logout () { @@ -294,7 +455,9 @@ async function getCloudSave () { const response = await fetch('https://synergism.cc/api/v1/saves/get') const save = await response.json() as CloudSave - importSynergism(save?.save ?? null) + if (save !== null) { + importSynergism(save.save) + } } const hasCaptcha = new WeakSet() diff --git a/src/Platonic.ts b/src/Platonic.ts index 022a8b916..fcde6227e 100644 --- a/src/Platonic.ts +++ b/src/Platonic.ts @@ -1,5 +1,4 @@ import { DOMCacheGetOrSet } from './Cache/DOM' -import { Synergism } from './Events' import { calculateSingularityDebuff } from './singularity' import { format, player } from './Synergism' import { Alert, revealStuff } from './UpdateHTML' @@ -437,7 +436,6 @@ export const buyPlatonicUpgrades = (index: number, auto = false) => { player.wowPlatonicCubes.sub(Math.floor(platUpgradeBaseCosts[index].platonics * priceMultiplier)) player.hepteractCrafts.abyss.spend(Math.floor(platUpgradeBaseCosts[index].abyssals * priceMultiplier)) - Synergism.emit('boughtPlatonicUpgrade', platUpgradeBaseCosts[index]) if (index === 20 && !auto && player.singularityCount === 0) { void Alert( 'While I strongly recommended you not to buy this, you did it anyway. For that, you have unlocked the rune of Grandiloquence, for you are a richass.' diff --git a/src/Reset.ts b/src/Reset.ts index bb74a936d..dc804a821 100644 --- a/src/Reset.ts +++ b/src/Reset.ts @@ -22,14 +22,14 @@ import { challengeRequirement } from './Challenges' import { corrChallengeMinimum, corruptionStatsUpdate, maxCorruptionLevel } from './Corruptions' import { WowCubes } from './CubeExperimental' import { autoBuyCubeUpgrades, awardAutosCookieUpgrade, updateCubeUpgradeBG } from './Cubes' -import { Synergism } from './Events' import { getAutoHepteractCrafts } from './Hepteracts' -import type { - ResetHistoryEntryAscend, - ResetHistoryEntryPrestige, - ResetHistoryEntryReincarnate, - ResetHistoryEntrySingularity, - ResetHistoryEntryTranscend +import { + resetHistoryAdd, + type ResetHistoryEntryAscend, + type ResetHistoryEntryPrestige, + type ResetHistoryEntryReincarnate, + type ResetHistoryEntrySingularity, + type ResetHistoryEntryTranscend } from './History' import { calculateHypercubeBlessings } from './Hypercubes' import { importSynergism } from './ImportExport' @@ -263,7 +263,7 @@ const resetAddHistoryEntry = (input: resetNames, from = 'unknown') => { diamonds: G.prestigePointGain.toString() } - Synergism.emit('historyAdd', 'reset', historyEntry) + resetHistoryAdd('reset', historyEntry) } else if (input === 'transcension' || input === 'transcensionChallenge') { // Heuristics: transcend entries are not added when entering or leaving a challenge, // unless a meaningful gain in particles was made. This prevents spam when using the challenge automator. @@ -275,7 +275,7 @@ const resetAddHistoryEntry = (input: resetNames, from = 'unknown') => { mythos: G.transcendPointGain.toString() } - Synergism.emit('historyAdd', 'reset', historyEntry) + resetHistoryAdd('reset', historyEntry) } else if (input === 'reincarnation' || input === 'reincarnationChallenge') { // Heuristics: reincarnate entries are not added when entering or leaving a challenge, // unless a meaningful gain in particles was made. This prevents spam when using the challenge automator. @@ -289,7 +289,7 @@ const resetAddHistoryEntry = (input: resetNames, from = 'unknown') => { obtainium: G.obtainiumGain } - Synergism.emit('historyAdd', 'reset', historyEntry) + resetHistoryAdd('reset', historyEntry) } } else if (input === 'ascension' || input === 'ascensionChallenge') { // Ascension entries will only be logged if C10 was completed. @@ -314,7 +314,7 @@ const resetAddHistoryEntry = (input: resetNames, from = 'unknown') => { historyEntry.currentChallenge = player.currentChallenge.ascension } - Synergism.emit('historyAdd', 'ascend', historyEntry) + resetHistoryAdd('ascend', historyEntry) } } } @@ -1155,7 +1155,7 @@ export const singularity = async (setSingNumber = -1): Promise => { quarkHept: player.hepteractCrafts.quark.BAL, kind: 'singularity' } - Synergism.emit('historyAdd', 'singularity', historyEntry) + resetHistoryAdd('singularity', historyEntry) } // reset the rune instantly to hopefully prevent a double singularity player.runelevels[6] = 0 diff --git a/src/Tabs.ts b/src/Tabs.ts index e19d6a9f9..09b8ae303 100644 --- a/src/Tabs.ts +++ b/src/Tabs.ts @@ -297,17 +297,22 @@ const subtabInfo: Record = { unlocked: true, buttonID: 'cartSubTab3' }, + { + subTabID: 'consumablesGrid', + unlocked: true, + buttonID: 'cartSubTab4' + }, { subTabID: 'cartContainer', get unlocked () { return isLoggedIn() || !prod }, - buttonID: 'cartSubTab4' + buttonID: 'cartSubTab5' }, { subTabID: 'merchContainer', unlocked: true, - buttonID: 'cartSubTab5' + buttonID: 'cartSubTab6' } ] } @@ -592,7 +597,6 @@ tabRow.appendButton( .makeDraggable() .makeRemoveable(), new $Tab({ class: 'isEvent', id: 'eventtab', i18n: 'tabs.main.unsmith' }) - .setUnlockedState(() => G.isEvent) .setType(Tabs.Event) .makeDraggable() .makeRemoveable(), @@ -682,8 +686,7 @@ export const changeSubTab = (tabs: Tabs, { page, step }: SubTabSwitchOptions) => let subTabList = subTabs.subTabList[player.subtabNumber] while (!subTabList.unlocked) { - assert(page === undefined) - player.subtabNumber = limitRange(player.subtabNumber + step, 0, subTabs.subTabList.length - 1) + player.subtabNumber = limitRange(player.subtabNumber + (step ?? 1), 0, subTabs.subTabList.length - 1) subTabList = subTabs.subTabList[player.subtabNumber] } diff --git a/src/UpdateHTML.ts b/src/UpdateHTML.ts index 542a515df..61063a6e1 100644 --- a/src/UpdateHTML.ts +++ b/src/UpdateHTML.ts @@ -231,11 +231,6 @@ export const revealStuff = () => { HTML.style.display = player.highestSingularityCount >= count ? 'block' : 'none' } - const eventHTMLs = document.getElementsByClassName('isEvent') as HTMLCollectionOf - for (const HTML of Array.from(eventHTMLs)) { - HTML.style.display = G.isEvent ? 'block' : 'none' - } - visualUpdateShop() const hepts = DOMCacheGetOrSet('corruptionHepteracts') diff --git a/src/UpdateVisuals.ts b/src/UpdateVisuals.ts index 84d3b4142..45b126b74 100644 --- a/src/UpdateVisuals.ts +++ b/src/UpdateVisuals.ts @@ -28,8 +28,10 @@ import { import { CalcECC } from './Challenges' import { version } from './Config' import type { IMultiBuy } from './Cubes' +import { BuffType, calculateEventSourceBuff, consumableEventBuff, eventBuffType, getEvent } from './Event' import type { hepteractTypes } from './Hepteracts' import { hepteractTypeList } from './Hepteracts' +import { activeConsumables, happyHourEndTime } from './Login' import { PCoinUpgradeEffects } from './PseudoCoinUpgrades' import { getQuarkBonus, quarkHandler } from './Quark' import { displayRuneInformation } from './Runes' @@ -40,7 +42,7 @@ import { format, formatTimeShort, player } from './Synergism' import { Tabs } from './Tabs' import { calculateMaxTalismanLevel } from './Talismans' import type { Player, ZeroToFour } from './types/Synergism' -import { sumContents } from './Utility' +import { sumContents, timeReminingHours } from './Utility' import { Globals as G } from './Variables' export const visualUpdateBuildings = () => { @@ -1807,6 +1809,52 @@ export const visualUpdateShop = () => { } Quarks Each` } -export const visualUpdateEvent = () => {} +export const visualUpdateEvent = () => { + const event = getEvent() + if (event !== null) { + const eventEnd = new Date(event.end) + DOMCacheGetOrSet('globalEventTimer').textContent = timeReminingHours(eventEnd) + DOMCacheGetOrSet('globalEventName').textContent = `(${event.name.length}) - ${event.name.join(', ')}` + + for (let i = 0; i < eventBuffType.length; i++) { + const eventBuff = calculateEventSourceBuff(BuffType[eventBuffType[i]]) + + if (eventBuff !== 0) { + DOMCacheGetOrSet(`eventBuff${eventBuffType[i]}`).style.display = 'flex' + DOMCacheGetOrSet(`eventBuff${eventBuffType[i]}Value`).textContent = `+${format(100 * eventBuff, 0, true)}%` + } else { + DOMCacheGetOrSet(`eventBuff${eventBuffType[i]}`).style.display = 'none' + } + } + } else { + DOMCacheGetOrSet('globalEventTimer').textContent = '--:--:--' + DOMCacheGetOrSet('globalEventName').textContent = '' + for (let i = 0; i < eventBuffType.length; i++) { + DOMCacheGetOrSet(`eventBuff${eventBuffType[i]}`).style.display = 'none' + } + } + const { HAPPY_HOUR_BELL } = activeConsumables + if (HAPPY_HOUR_BELL > 0) { + DOMCacheGetOrSet('consumableEventTimer').textContent = timeReminingHours(new Date(happyHourEndTime)) + DOMCacheGetOrSet('consumableEventBonus').textContent = `${HAPPY_HOUR_BELL}` + + for (let i = 0; i < eventBuffType.length; i++) { + const eventBuff = consumableEventBuff(BuffType[eventBuffType[i]]) + + if (eventBuff !== 0) { + DOMCacheGetOrSet(`consumableBuff${eventBuffType[i]}`).style.display = 'flex' + DOMCacheGetOrSet(`consumableBuff${eventBuffType[i]}Value`).textContent = `+${format(100 * eventBuff, 1, true)}%` + } else { + DOMCacheGetOrSet(`consumableBuff${eventBuffType[i]}`).style.display = 'none' + } + } + } else { + DOMCacheGetOrSet('consumableEventBonus').textContent = 'No active consumable' + DOMCacheGetOrSet('consumableEventTimer').textContent = '--:--:--' + for (let i = 0; i < eventBuffType.length; i++) { + DOMCacheGetOrSet(`consumableBuff${eventBuffType[i]}`).style.display = 'none' + } + } +} export const visualUpdatePurchase = () => {} diff --git a/src/Utility.ts b/src/Utility.ts index b6d4fc679..ec449d6f6 100644 --- a/src/Utility.ts +++ b/src/Utility.ts @@ -143,6 +143,25 @@ export const formatS = (s: number) => { return formatMS(1000 * s) } +export const addLeadingZero = (n: number): string => { + return n < 10 ? `0${n}` : String(n) +} + +export const timeReminingHours = (targetDate: Date): string => { + const now = new Date() + const timeDifference = targetDate.getTime() - now.getTime() + + if (timeDifference < 0) { + return '--:--:--' + } + + const hours = addLeadingZero(Math.floor(timeDifference / (1000 * 60 * 60))) + const minutes = addLeadingZero(Math.floor((timeDifference % (1000 * 60 * 60)) / (1000 * 60))) + const seconds = addLeadingZero(Math.floor((timeDifference % (1000 * 60)) / 1000)) + + return `${hours}:${minutes}:${seconds}` +} + export const cleanString = (s: string): string => { let cleaned = '' diff --git a/src/Variables.ts b/src/Variables.ts index 492822538..39c5d0204 100644 --- a/src/Variables.ts +++ b/src/Variables.ts @@ -502,8 +502,6 @@ export const Globals: GlobalVariables = { // talismanResourceObtainiumCosts: [1e13, 1e14, 1e16, 1e18, 1e20, 1e22, 1e24] // talismanResourceOfferingCosts: [0, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9] - eventClicked: false, - ambrosiaTimer: 0, TIME_PER_AMBROSIA: 600, diff --git a/src/purchases/CartTab.ts b/src/purchases/CartTab.ts index 4d8ec962c..448273fc9 100644 --- a/src/purchases/CartTab.ts +++ b/src/purchases/CartTab.ts @@ -6,6 +6,7 @@ import { Alert } from '../UpdateHTML' import { createDeferredPromise, type DeferredPromise, memoize } from '../Utility' import { setEmptyProductMap } from './CartUtil' import { clearCheckoutTab, toggleCheckoutTab } from './CheckoutTab' +import { clearConsumablesTab, toggleConsumablesTab } from './ConsumablesTab' import { clearMerchSubtab, toggleMerchSubtab } from './MerchTab' import { clearProductPage, toggleProductPage } from './ProductSubtab' import { clearSubscriptionPage, toggleSubscriptionPage } from './SubscriptionsSubtab' @@ -29,8 +30,9 @@ const cartSubTabs = { Coins: 0, Subscriptions: 1, Upgrades: 2, - Checkout: 3, - Merch: 4 + Consumables: 3, + Checkout: 4, + Merch: 5 } as const const tab = document.getElementById('pseudoCoins')! @@ -66,8 +68,8 @@ export class CartTab { .then((productsList: Product[]) => { products.push(...productsList) setEmptyProductMap(productsList) - coinProducts = products.filter((product) => !product.subscription) - subscriptionProducts = products.filter((product) => product.subscription) + coinProducts = products.filter((product) => (!product.subscription)) + subscriptionProducts = products.filter((product) => (product.subscription)) // The Subscriptions do not naturally sort themselves by price subscriptionProducts.sort((a, b) => a.price - b.price) @@ -123,6 +125,7 @@ export class CartTab { clearUpgradeSubtab() clearCheckoutTab() clearMerchSubtab() + clearConsumablesTab() switch (player.subtabNumber) { case cartSubTabs.Coins: @@ -146,6 +149,9 @@ export class CartTab { } }) break + case cartSubTabs.Consumables: + toggleConsumablesTab() + break case cartSubTabs.Checkout: CartTab.fetchProducts().then(() => { if (player.subtabNumber === cartSubTabs.Checkout) { diff --git a/src/purchases/ConsumablesTab.ts b/src/purchases/ConsumablesTab.ts new file mode 100644 index 000000000..ea2313643 --- /dev/null +++ b/src/purchases/ConsumablesTab.ts @@ -0,0 +1,50 @@ +import { sendToWebsocket } from '../Login' +import { memoize } from '../Utility' + +interface ConsumableListItems { + name: string + description: string + internalName: string + length: string + cost: number +} + +const tab = document.querySelector('#pseudoCoins > #consumablesGrid')! + +const initializeConsumablesTab = memoize(() => { + fetch('https://synergism.cc/consumables/list') + .then((r) => r.json()) + .then((consumables: ConsumableListItems[]) => { + tab.innerHTML = consumables.map((u) => ` +
+ ${u.name} Consumable +

${u.name}

+

${u.description}

+ +
+ `).join('') + + tab.querySelectorAll('div > button').forEach((element) => { + const key = element.parentElement!.getAttribute('data-key')! + element.addEventListener('click', () => { + sendToWebsocket(JSON.stringify({ + type: 'consume', + consumable: key + })) + }) + }) + }) +}) + +export const toggleConsumablesTab = () => { + initializeConsumablesTab() + + tab.style.display = 'flex' +} + +export const clearConsumablesTab = () => { + tab.style.display = 'none' +} diff --git a/src/purchases/UpgradesSubtab.ts b/src/purchases/UpgradesSubtab.ts index be1e63ab6..d147a8968 100644 --- a/src/purchases/UpgradesSubtab.ts +++ b/src/purchases/UpgradesSubtab.ts @@ -158,7 +158,6 @@ const initializeUpgradeSubtab = memoize(() => { current.cost.push(upgrade.cost) current.level.push(upgrade.level) } - return map }, new Map()) diff --git a/src/types/Synergism.d.ts b/src/types/Synergism.d.ts index 3e6610520..58d96d371 100644 --- a/src/types/Synergism.d.ts +++ b/src/types/Synergism.d.ts @@ -1002,8 +1002,6 @@ export interface GlobalVariables { isEvent: boolean shopEnhanceVision: boolean - eventClicked: boolean - ambrosiaTimer: number TIME_PER_AMBROSIA: number diff --git a/translations/en.json b/translations/en.json index f42405c7f..8af431d8c 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1152,7 +1152,7 @@ } }, "calculate": { - "timePrompt": "How far in the future would you like to go into the future? Anything awaits when it is testing season.", + "timePrompt": "How far in the future (in seconds) would you like to go into the future? Anything awaits when it is testing season.", "timePromptError": "Hey! That's not a valid time!", "offlineTimer": "You have {{value}} seconds of offline progress!", "offlineEarnings": "While offline, the Idle Overlords earned:", @@ -2491,7 +2491,7 @@ "singularity": "Singularity", "settings": "Settings", "shop": "Shop", - "unsmith": "UNSMITH", + "unsmith": "Events", "purchase": "PseudoCoins" }, "buildings": { @@ -2539,7 +2539,8 @@ "buy": "Purchase Coins", "subscriptions": "Subscriptions", "upgrades": "Buy Upgrades", - "merch": "Purchase Merch" + "merch": "Purchase Merch", + "consumables": "Consumables & Events" } }, "offlineProgress": { @@ -3645,6 +3646,15 @@ "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 <>" + }, + "consumables": { + "tipReceived": "{{offlineTime}} people tipped you in the last minute!", + "thanks": "The booster is now in effect! Thank you for supporting the game!", + "eventTitle": "Active Consumables", + "consumableSpoiler": "Start an event for everyone, with the Happy Hour Bell!", + "consumableButton": "Buy at the PseudoShop!", + "applyTips": "Use Tips!", + "applyTipsPrompt": "How many tips would you like to use? Each tip gives 1 minute of Offline Time. You have {{tips}} tip(s)." } } } diff --git a/translations/source.json b/translations/source.json index 77523b961..8c562b9e5 100644 --- a/translations/source.json +++ b/translations/source.json @@ -2491,7 +2491,7 @@ "singularity": "Singularity", "settings": "Settings", "shop": "Shop", - "unsmith": "UNSMITH", + "unsmith": "Events", "purchase": "PseudoCoins" }, "buildings": {