Merge branch 'master' into Steam-Webpack

This commit is contained in:
ZackRhodes 2022-12-03 19:51:15 -05:00
commit 5757bc7736
172 changed files with 4837 additions and 1150 deletions

View File

@ -55,7 +55,6 @@ import GlyphSetSaveDeleteModal from "@/components/modals/GlyphSetSaveDeleteModal
import GlyphShowcasePanelModal from "@/components/modals/GlyphShowcasePanelModal";
import H2PModal from "@/components/modals/H2PModal";
import ImportAutomatorDataModal from "@/components/modals/ImportAutomatorDataModal";
import ImportFileWarningModal from "@/components/modals/ImportFileWarningModal";
import ImportSaveModal from "@/components/modals/ImportSaveModal";
import InformationModal from "@/components/modals/InformationModal";
import LoadGameModal from "@/components/modals/LoadGameModal";
@ -104,6 +103,7 @@ export class Modal {
this._uniqueID = nextModalID++;
this._props = Object.assign({}, modalConfig || {});
if (this._closeEvent) this.applyCloseListeners(this._closeEvent);
if (modalConfig?.closeEvent) this.applyCloseListeners(modalConfig.closeEvent);
const modalQueue = ui.view.modal.queue;
// Add this modal to the front of the queue and sort based on priority to ensure priority is maintained.
@ -238,7 +238,6 @@ Modal.changelog = new Modal(ChangelogModal, 1);
Modal.awayProgress = new Modal(AwayProgressModal);
Modal.loadGame = new Modal(LoadGameModal);
Modal.import = new Modal(ImportSaveModal);
Modal.importWarning = new Modal(ImportFileWarningModal);
Modal.importScriptData = new Modal(ImportAutomatorDataModal);
Modal.automatorScriptDelete = new Modal(DeleteAutomatorScriptModal);
Modal.automatorScriptTemplate = new Modal(AutomatorScriptTemplate);
@ -265,7 +264,11 @@ function getSaveInfo(save) {
imaginaryMachines: 0,
dilatedTime: new Decimal(0),
bestLevel: 0,
totalSTD: 0,
pelleAM: new Decimal(0),
remnants: 0,
realityShards: new Decimal(0),
// This is a slight workaround to hide DT/level once Doomed
pelleLore: 0,
saveName: "",
compositeProgress: 0,
};
@ -282,7 +285,10 @@ function getSaveInfo(save) {
resources.imaginaryMachines = save.reality?.iMCap ?? 0;
resources.dilatedTime.copyFrom(new Decimal(save.dilation.dilatedTime));
resources.bestLevel = save.records?.bestReality.glyphLevel ?? 0;
resources.totalSTD = save?.IAP?.totalSTD ?? 0;
resources.pelleAM.copyFrom(new Decimal(save.celestials?.pelle.records.totalAntimatter));
resources.remnants = save.celestials?.pelle.remnants ?? 0;
resources.realityShards.copyFrom(new Decimal(save.celestials?.pelle.realityShards));
resources.pelleLore = save.celestials?.pelle.quoteBits ?? 0;
resources.saveName = save.options.saveFileName ?? "";
resources.compositeProgress = ProgressChecker.getCompositeProgress(save);

View File

@ -15,15 +15,13 @@ export class GameOptions {
ui.view.newUI = player.options.newUI;
// This is needed because .s-base--dark is on newUI/normal but not on oldUI/normal
// So the classes on body need to be updated
Themes.find(player.options.theme).set();
Themes.find(Theme.currentName()).set();
SteamFunctions.UIZoom()
GameStorage.save(true);
}
static cloudSave() {
// This flag suppresses the modal on autosave, but this function is only called with manual saves
Cloud.hasSeenSavingConflict = false;
Cloud.saveCheck();
Cloud.saveCheck(true);
}
static cloudLoad() {

View File

@ -52,6 +52,11 @@ export class PlayerProgress {
return PlayerProgress.current.isRealityUnlocked;
}
static seenAlteredSpeed() {
const ec12 = EternityChallenge(12);
return this.realityUnlocked() || ec12.completions > 0 || ec12.isRunning;
}
static challengeCompleted() {
return NormalChallenges.all.slice(1).some(c => c.isCompleted);
}

View File

@ -44,7 +44,11 @@ export const Theme = function Theme(name, config) {
} else {
document.getElementById("background-animations").style.display = "none";
}
player.options.theme = name;
if (player.options.newUI) {
player.options.themeModern = name;
} else {
player.options.themeClassic = name;
}
ui.view.theme = name;
window.getSelection().removeAllRanges();
PerkNetwork.forceNetworkRemake();
@ -55,8 +59,14 @@ export const Theme = function Theme(name, config) {
};
};
Theme.currentName = function() {
return player.options.newUI
? player.options.themeModern
: player.options.themeClassic;
};
Theme.current = function() {
return Themes.find(player.options.theme);
return Themes.find(Theme.currentName());
};
Theme.set = function(name) {

View File

@ -32,7 +32,6 @@ export const state = {
automator: {
fullScreen: false,
editorScriptID: "",
// TODO: enum
lines: []
}
},

View File

@ -10,7 +10,7 @@ Autobuyer.annihilation = new class AnnihilationAutobuyerState extends AutobuyerS
}
get isUnlocked() {
return Laitela.darkMatterMult > 1 && !Pelle.isDoomed;
return SingularityMilestone.annihilationAutobuyer.canBeApplied;
}
get multiplier() {

View File

@ -11,7 +11,7 @@ new class DarkMatterDimensionAscensionAutobuyerState extends IntervaledAutobuyer
}
get isUnlocked() {
return SingularityMilestone.darkDimensionAutobuyers.canBeApplied;
return SingularityMilestone.ascensionAutobuyers.canBeApplied;
}
get interval() {
@ -24,7 +24,7 @@ new class DarkMatterDimensionAscensionAutobuyerState extends IntervaledAutobuyer
tick() {
super.tick();
for (let i = 1; i <= SingularityMilestone.darkDimensionAutobuyers.effectValue; i++) {
for (let i = 1; i <= SingularityMilestone.ascensionAutobuyers.effectValue; i++) {
DarkMatterDimension(i).ascend();
}
}

View File

@ -234,7 +234,7 @@ import { AutomatorLexer } from "./lexer";
// things like new Decimal("11,21,31") return 11 instead of something indicating an error.
return value.match(/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/u);
case AUTOMATOR_VAR_TYPES.STUDIES:
return new TimeStudyTree(value).purchasedStudies.length > 0;
return TimeStudyTree.isValidImportString(value);
case AUTOMATOR_VAR_TYPES.DURATION:
return !Number.isNaN(parseInt(1000 * value, 10));
default:

View File

@ -44,11 +44,23 @@ export function bigCrunchResetRequest(disableAnimation = false) {
}
}
export function bigCrunchReset() {
if (!Player.canCrunch) return;
export function bigCrunchReset(
forced = false,
enteringAntimatterChallenge = Player.isInAntimatterChallenge && player.options.retryChallenge
) {
if (!forced && !Player.canCrunch) return;
EventHub.dispatch(GAME_EVENT.BIG_CRUNCH_BEFORE);
if (Player.canCrunch) {
EventHub.dispatch(GAME_EVENT.BIG_CRUNCH_BEFORE);
bigCrunchGiveRewards();
if (Pelle.isDoomed) PelleStrikes.infinity.trigger();
}
bigCrunchResetValues(enteringAntimatterChallenge);
EventHub.dispatch(GAME_EVENT.BIG_CRUNCH_AFTER);
}
function bigCrunchGiveRewards() {
bigCrunchUpdateStatistics();
const infinityPoints = gainedInfinityPoints();
@ -56,12 +68,7 @@ export function bigCrunchReset() {
Currency.infinities.add(gainedInfinities().round());
bigCrunchTabChange(!PlayerProgress.infinityUnlocked());
bigCrunchResetValues();
bigCrunchCheckUnlocks();
if (Pelle.isDoomed) PelleStrikes.infinity.trigger();
EventHub.dispatch(GAME_EVENT.BIG_CRUNCH_AFTER);
}
function bigCrunchUpdateStatistics() {
@ -108,13 +115,13 @@ function bigCrunchTabChange(firstInfinity) {
}
}
export function bigCrunchResetValues() {
export function bigCrunchResetValues(enteringAntimatterChallenge) {
const currentReplicanti = Replicanti.amount;
const currentReplicantiGalaxies = player.replicanti.galaxies;
// For unknown reasons, everything but keeping of RGs (including resetting of RGs)
// is done in the function called below. For now, we're just trying to keep
// code structure similar to what it was before to avoid new bugs.
secondSoftReset(true);
secondSoftReset(enteringAntimatterChallenge);
let remainingGalaxies = 0;
if (Achievement(95).isUnlocked) {
@ -142,12 +149,12 @@ function bigCrunchCheckUnlocks() {
}
}
export function secondSoftReset(forcedNDReset = false) {
export function secondSoftReset(enteringAntimatterChallenge) {
player.dimensionBoosts = 0;
player.galaxies = 0;
player.records.thisInfinity.maxAM = DC.D0;
Currency.antimatter.reset();
softReset(0, forcedNDReset);
softReset(0, true, true, enteringAntimatterChallenge);
InfinityDimensions.resetAmount();
if (player.replicanti.unl) Replicanti.amount = DC.D1;
player.replicanti.galaxies = 0;

View File

@ -220,10 +220,13 @@ class BlackHoleState {
return this.isCharged && (this.id === 1 || BlackHole(this.id - 1).isActive) && !Pelle.isDisabled("blackhole");
}
// Proportion of active time, scaled 0 to 1
get dutyCycle() {
return this.duration / (this.rawInterval + this.duration);
}
get isPermanent() {
// If the black hole is active 99.99% of the time, the duration is exactly
// 9999 times longer than the interval.
return this.duration / this.rawInterval >= 9999;
return this.dutyCycle >= 0.9999;
}
/**
@ -351,6 +354,7 @@ export const BlackHoles = {
Currency.realityMachines.purchase(100);
SpeedrunMilestones(17).tryComplete();
Achievement(144).unlock();
player.records.timePlayedAtBHUnlock = player.records.totalTimePlayed;
EventHub.dispatch(GAME_EVENT.BLACK_HOLE_UNLOCKED);
},
@ -364,7 +368,7 @@ export const BlackHoles = {
// whether the player's using negative BH (i.e. BH inversion); if going paused -> unpaused,
// use "unpaused".
// eslint-disable-next-line no-nested-ternary
const pauseType = player.blackHolePause ? (BlackHoles.areNegative ? "inverted" : "paused") : 'unpaused';
const pauseType = player.blackHolePause ? (BlackHoles.areNegative ? "inverted" : "paused") : "unpaused";
const automaticString = automatic ? "automatically " : "";
GameUI.notify.blackHole(`${blackHoleString} ${automaticString}${pauseType}`);
},
@ -380,7 +384,7 @@ export const BlackHoles = {
},
get areNegative() {
return this.arePaused && player.blackHoleNegative < 1;
return this.arePaused && !Laitela.isRunning && player.blackHoleNegative < 1;
},
get arePermanent() {
@ -560,18 +564,18 @@ export const BlackHoles = {
timeToNextPause(bhNum, steps = 100) {
if (bhNum === 1) {
// This is a simple case that we can do mathematically.
const bh = BlackHole(1);
// If no blackhole gaps are as long as the warmup time, we never pause.
if (bh.interval <= BlackHoles.ACCELERATION_TIME) {
return null;
}
// Find the time until next activation.
const t = (bh.isCharged ? bh.duration : 0) + bh.interval - bh.phase;
// If the time until next activation is less than the acceleration time,
// we have to wait until the activation after that;
// otherwise, we can just use the next activation.
return (t < BlackHoles.ACCELERATION_TIME)
? t + bh.duration + bh.interval - BlackHoles.ACCELERATION_TIME : t - BlackHoles.ACCELERATION_TIME;
const bh = BlackHole(1);
// If no blackhole gaps are as long as the warmup time, we never pause.
if (bh.interval <= BlackHoles.ACCELERATION_TIME) {
return null;
}
// Find the time until next activation.
const t = (bh.isCharged ? bh.duration : 0) + bh.interval - bh.phase;
// If the time until next activation is less than the acceleration time,
// we have to wait until the activation after that;
// otherwise, we can just use the next activation.
return (t < BlackHoles.ACCELERATION_TIME)
? t + bh.duration + bh.interval - BlackHoles.ACCELERATION_TIME : t - BlackHoles.ACCELERATION_TIME;
}
// Look at the next 100 black hole transitions.
// This is called every tick if BH pause setting is set to BH2, so we try to optimize it.
@ -609,11 +613,11 @@ export const BlackHoles = {
const phaseBounds = phaseBoundList[current];
// Compute time until some phase reaches its bound.
const minTime = current > 0 ? Math.min(phaseBounds[0] - phases[0], phaseBounds[1] - phases[1])
: phaseBounds[0] - phases[0];
: phaseBounds[0] - phases[0];
if (current === 2) {
// Check if there was enough time before this activation to pause.
if (inactiveTime >= BlackHoles.ACCELERATION_TIME) {
return totalTime - BlackHoles.ACCELERATION_TIME;
return totalTime - BlackHoles.ACCELERATION_TIME;
}
// Not enough time, reset inactive time to 0.
inactiveTime = 0;

View File

@ -66,7 +66,7 @@ GameDatabase.celestials.descriptions = [
{
name: "Ra",
effects() {
return `You only have ${formatInt(4)} Dimension Boosts and can't gain any more.
return `You only have ${formatInt(4)} Dimension Boosts and can not gain any more.
The Tickspeed purchase multiplier is fixed at ${formatX(1.1245, 0, 3)}.`;
},
},
@ -98,7 +98,7 @@ GameDatabase.celestials.descriptions = [
return `Infinity Point and Eternity Point gain are Dilated.
Game speed is reduced to ${formatInt(1)} and gradually comes back over ${formatInt(10)} minutes.
Black Hole storing, discharging, and pulsing are disabled.
Black Hole storing, discharging, pulsing, and inversion are all disabled.
${disabledText}`;
},
description() {

View File

@ -83,15 +83,19 @@ export const Effarig = {
}
return 3 * (1 - c / (c + Math.sqrt(power.pLog10())));
},
get tickDilation() {
return 0.7 + 0.1 * this.nerfFactor(Currency.timeShards.value);
},
get multDilation() {
return 0.25 + 0.25 * this.nerfFactor(Currency.infinityPower.value);
},
get tickspeed() {
const base = 3 + Tickspeed.baseValue.reciprocal().log10();
const pow = 0.7 + 0.1 * this.nerfFactor(Currency.timeShards.value);
return Decimal.pow10(Math.pow(base, pow)).reciprocal();
return Decimal.pow10(Math.pow(base, this.tickDilation)).reciprocal();
},
multiplier(mult) {
const base = new Decimal(mult).pLog10();
const pow = 0.25 + 0.25 * this.nerfFactor(Currency.infinityPower.value);
return Decimal.pow10(Math.pow(base, pow));
return Decimal.pow10(Math.pow(base, this.multDilation));
},
get bonusRG() {
// Will return 0 if Effarig Infinity is uncompleted

View File

@ -183,10 +183,14 @@ export const Enslaved = {
return Math.pow(10, Math.pow(Math.log10(stored / 1e3), 0.55)) * 1e3;
},
feelEternity() {
if (!this.feltEternity) {
if (this.feltEternity) {
Modal.message.show(`You have already exposed this crack in the Reality. Time in this Eternity is being multiplied
by your Eternity count, up to a maximum of ${formatX(1e66)}.`,
{ closeEvent: GAME_EVENT.REALITY_RESET_AFTER }, 1);
} else {
EnslavedProgress.feelEternity.giveProgress();
this.feltEternity = true;
Modal.message.show(`Time in this Eternity will be multiplied by number of Eternities,
Modal.message.show(`Time in this Eternity will be multiplied by your Eternity count,
up to a maximum of ${formatX(1e66)}.`, { closeEvent: GAME_EVENT.REALITY_RESET_AFTER }, 1);
}
},

View File

@ -129,9 +129,10 @@ export const SingularityMilestones = {
case SINGULARITY_MILESTONE_SORT.CURRENT_COMPLETIONS:
// Also counts partial completion on the current step
sortFn = m => {
const currComp = Math.clampMax(Math.log(Currency.singularities.value / m.previousGoal) /
Math.log(m.nextGoal / m.previousGoal), 1);
return (m.completions + currComp) / 20;
// For never-completed repeatable milestones, this is zero and will cause NaN bugs if we don't set it to 1
const prev = Math.clampMin(m.previousGoal, 1);
const part = Math.clamp(Math.log(Currency.singularities.value / prev) / Math.log(m.nextGoal / prev), 0, 1);
return (m.completions + part) / 20;
};
break;
case SINGULARITY_MILESTONE_SORT.PERCENT_COMPLETIONS:

View File

@ -138,7 +138,7 @@ export const Pelle = {
get uselessPerks() {
return [10, 12, 13, 14, 15, 16, 17, 30, 40, 41, 42, 43, 44, 45, 46, 51, 53,
60, 61, 62, 80, 81, 82, 83, 100, 105, 106, 201, 202, 203, 204];
60, 61, 62, 80, 81, 82, 83, 100, 104, 105, 106, 201, 202, 203, 204];
},
get specialGlyphEffect() {

View File

@ -248,13 +248,6 @@ export const Ra = {
data.peakGamespeed = 1;
for (const pet of Ra.pets.all) pet.reset();
},
// Scans through all glyphs and fills base resources to the maximum allowed by the cap
// TODO update/delete this function when we get back to alchemy, it's outdated since it's not linear any more
fillAlchemyResources() {
for (const resource of AlchemyResources.base) {
resource.amount = Math.min(this.alchemyResourceCap, player.records.bestReality.glyphLevel);
}
},
memoryTick(realDiff, generateChunks) {
if (!this.isUnlocked) return;
for (const pet of Ra.pets.all) pet.tick(realDiff, generateChunks);
@ -284,12 +277,19 @@ export const Ra = {
const post15Scaling = Math.pow(1.5, Math.max(0, level - 15));
return Math.floor(Math.pow(adjustedLevel, 5.52) * post15Scaling * 1e6);
},
// Calculates the cumulative exp needed to REACH a level starting from nothing.
// TODO mathematically optimize this once Ra exp curves and balancing are finalized
totalExpForLevel(maxLevel) {
let runningTotal = 0;
for (let lv = 1; lv < maxLevel; lv++) runningTotal += this.requiredMemoriesForLevel(lv);
return runningTotal;
// Returns a string containing a time estimate for gaining a specific amount of exp (UI only)
timeToGoalString(pet, expToGain) {
// Quadratic formula for growth (uses constant growth for a = 0)
const a = Ra.productionPerMemoryChunk * pet.memoryUpgradeCurrentMult * pet.memoryChunksPerSecond / 2;
const b = Ra.productionPerMemoryChunk * pet.memoryUpgradeCurrentMult * pet.memoryChunks;
const c = -expToGain;
const estimate = a === 0
? -c / b
: (Math.sqrt(Math.pow(b, 2) - 4 * a * c) - b) / (2 * a);
if (Number.isFinite(estimate)) {
return `in ${TimeSpan.fromSeconds(estimate).toStringShort()}`;
}
return "";
},
get totalPetLevel() {
return this.pets.all.map(pet => (pet.isUnlocked ? pet.level : 0)).sum();

View File

@ -463,3 +463,5 @@ window.PROGRESS_STAGE = {
LAITELA: 16,
PELLE: 17,
};
window.STD_BACKEND_URL = "https://antimatterdimensionspayments.ew.r.appspot.com";

View File

@ -396,10 +396,11 @@ Currency.realities = new class extends NumberCurrency {
Currency.realityMachines = new class extends DecimalCurrency {
get value() { return player.reality.realityMachines; }
set value(value) {
const cappedValue = Decimal.min(value, MachineHandler.hardcapRM);
player.reality.realityMachines = cappedValue;
if (player.records.bestReality.RM.lt(cappedValue)) {
player.records.bestReality.RM = cappedValue;
const newValue = Decimal.min(value, MachineHandler.hardcapRM);
const addedThisReality = newValue.minus(player.reality.realityMachines);
player.reality.realityMachines = newValue;
if (player.records.bestReality.RM.lt(addedThisReality)) {
player.records.bestReality.RM = addedThisReality;
player.records.bestReality.RMSet = Glyphs.copyForRecords(Glyphs.active.filter(g => g !== null));
}
}

View File

@ -137,7 +137,7 @@ export function getDilationGainPerSecond() {
return dtRate;
}
function tachyonGainMultiplier() {
export function tachyonGainMultiplier() {
if (Pelle.isDisabled("tpMults")) return new Decimal(1);
const pow = Enslaved.isRunning ? Enslaved.tachyonNerf : 1;
return DC.D1.timesEffectsOf(
@ -151,7 +151,7 @@ function tachyonGainMultiplier() {
}
export function rewardTP() {
Currency.tachyonParticles.bumpTo(getTP(Currency.antimatter.value, true));
Currency.tachyonParticles.bumpTo(getTP(player.records.thisEternity.maxAM, true));
player.dilation.lastEP = Currency.eternityPoints.value;
}
@ -182,17 +182,32 @@ export function getTachyonGain(requireEternity) {
// Returns the minimum antimatter needed in order to gain more TP; used only for display purposes
export function getTachyonReq() {
let effectiveTP = Currency.tachyonParticles.value;
let effectiveTP = Currency.tachyonParticles.value.dividedBy(tachyonGainMultiplier());
if (Enslaved.isRunning) effectiveTP = effectiveTP.pow(1 / Enslaved.tachyonNerf);
return Decimal.pow10(
effectiveTP
.times(Math.pow(400, 1.5))
.dividedBy(tachyonGainMultiplier())
.pow(2 / 3)
.toNumber()
);
}
export function getDilationTimeEstimate(goal) {
const currentDTGain = getDilationGainPerSecond();
const rawDTGain = currentDTGain.times(getGameSpeedupForDisplay());
const currentDT = Currency.dilatedTime.value;
if (currentDTGain.eq(0)) return null;
if (PelleRifts.paradox.isActive) {
const drain = Pelle.riftDrainPercent;
const goalNetRate = rawDTGain.minus(Decimal.multiply(goal, drain));
const currNetRate = rawDTGain.minus(currentDT.times(drain));
if (goalNetRate.lt(0)) return "Never affordable due to Rift drain";
return TimeSpan.fromSeconds(currNetRate.div(goalNetRate).ln() / drain).toTimeEstimate();
}
return TimeSpan.fromSeconds(Decimal.sub(goal, currentDT)
.div(rawDTGain).toNumber()).toTimeEstimate();
}
export function dilatedValueOf(value) {
const log10 = value.log10();
const dilationPenalty = 0.75 * Effects.product(DilationUpgrade.dilationPenalty);

View File

@ -173,7 +173,8 @@ export class DimBoost {
}
}
export function softReset(tempBulk, forcedNDReset = false, forcedAMReset = false) {
// eslint-disable-next-line max-params
export function softReset(tempBulk, forcedADReset = false, forcedAMReset = false, enteringAntimatterChallenge = false) {
if (Currency.antimatter.gt(Player.infinityLimit)) return;
const bulk = Math.min(tempBulk, DimBoost.maxBoosts - player.dimensionBoosts);
EventHub.dispatch(GAME_EVENT.DIMBOOST_BEFORE, bulk);
@ -182,12 +183,12 @@ export function softReset(tempBulk, forcedNDReset = false, forcedAMReset = false
const canKeepDimensions = Pelle.isDoomed
? PelleUpgrade.dimBoostResetsNothing.canBeApplied
: Perk.antimatterNoReset.canBeApplied;
if (forcedNDReset || !canKeepDimensions) {
if (forcedADReset || !canKeepDimensions) {
AntimatterDimensions.reset();
player.sacrificed = DC.D0;
resetTickspeed();
}
skipResetsIfPossible();
skipResetsIfPossible(enteringAntimatterChallenge);
const canKeepAntimatter = Pelle.isDoomed
? PelleUpgrade.dimBoostResetsNothing.canBeApplied
: (Achievement(111).isUnlocked || Perk.antimatterNoReset.canBeApplied);
@ -199,8 +200,8 @@ export function softReset(tempBulk, forcedNDReset = false, forcedAMReset = false
EventHub.dispatch(GAME_EVENT.DIMBOOST_AFTER, bulk);
}
export function skipResetsIfPossible() {
if (Player.isInAntimatterChallenge) return;
export function skipResetsIfPossible(enteringAntimatterChallenge) {
if (enteringAntimatterChallenge || Player.isInAntimatterChallenge) return;
if (InfinityUpgrade.skipResetGalaxy.isBought && player.dimensionBoosts < 4) {
player.dimensionBoosts = 4;
if (player.galaxies === 0) player.galaxies = 1;

View File

@ -439,6 +439,19 @@ class AntimatterDimensionState extends DimensionState {
return toGain.times(10).dividedBy(this.amount.max(1)).times(getGameSpeedupForDisplay());
}
/**
* @returns {boolean}
*/
get isProducing() {
const tier = this.tier;
if ((EternityChallenge(3).isRunning && tier > 4) ||
(NormalChallenge(12).isRunning && tier > 6) ||
(Laitela.isRunning && tier > Laitela.maxAllowedDimension)) {
return false;
}
return this.totalAmount.gt(0);
}
/**
* @returns {Decimal}
*/

View File

@ -180,6 +180,16 @@ class InfinityDimensionState extends DimensionState {
return mult;
}
get isProducing() {
const tier = this.tier;
if (EternityChallenge(2).isRunning ||
EternityChallenge(10).isRunning ||
(Laitela.isRunning && tier > Laitela.maxAllowedDimension)) {
return false;
}
return this.amount.gt(0);
}
get baseCost() {
return this._baseCost;
}

View File

@ -233,6 +233,16 @@ class TimeDimensionState extends DimensionState {
return toGain.times(10).dividedBy(current).times(getGameSpeedupForDisplay());
}
get isProducing() {
const tier = this.tier;
if (EternityChallenge(1).isRunning ||
EternityChallenge(10).isRunning ||
(Laitela.isRunning && tier > Laitela.maxAllowedDimension)) {
return false;
}
return this.amount.gt(0);
}
get baseCost() {
return this._baseCost;
}

View File

@ -208,7 +208,9 @@ function askEternityConfirmation() {
export function gainedEternities() {
return Pelle.isDisabled("eternityMults")
? new Decimal(1)
: new Decimal(getAdjustedGlyphEffect("timeetermult")).timesEffectsOf(RealityUpgrade(3), Achievement(113));
: new Decimal(getAdjustedGlyphEffect("timeetermult"))
.timesEffectsOf(RealityUpgrade(3), Achievement(113))
.pow(AlchemyResource.eternity.effectValue);
}
export class EternityMilestoneState {

View File

@ -90,6 +90,7 @@ export class Galaxy {
if (this.canBeBought) return null;
if (EternityChallenge(6).isRunning) return "Locked (Eternity Challenge 6)";
if (InfinityChallenge(7).isRunning) return "Locked (Infinity Challenge 7)";
if (InfinityChallenge(1).isRunning) return "Locked (Infinity Challenge 1)";
if (NormalChallenge(8).isRunning) return "Locked (8th Antimatter Dimension Autobuyer Challenge)";
return null;
}

View File

@ -85,7 +85,7 @@ export * from "./black_hole";
export * from "./machines";
export * from "./devtools";
export * from "./news-ticker";
export * from "./kong";
export * from "./shop";
export * from "./ui/tabs";
export * from "./ui/tab-notifications";
export * from "./speedrun";

View File

@ -167,9 +167,10 @@ function getGlyphLevelSources() {
// Glyph levels are the product of 3 or 4 sources (eternities are enabled via upgrade).
// Once Effarig is unlocked, these contributions can be adjusted; the math is described in detail
// in getGlyphLevelInputs. These *Base values are the nominal inputs, as they would be multiplied without Effarig
const eternityPoints = Player.canEternity
let eternityPoints = Player.canEternity
? Currency.eternityPoints.value.plus(gainedEternityPoints())
: Currency.eternityPoints.value;
eternityPoints = Decimal.max(player.records.thisReality.maxEP, eternityPoints);
const epCoeff = 0.016;
const epBase = Math.pow(Math.max(1, eternityPoints.pLog10()), 0.5) * epCoeff;
const replPow = 0.4 + getAdjustedGlyphEffect("replicationglyphlevel");

View File

@ -232,13 +232,10 @@ export const Glyphs = {
return this.active[activeIndex];
},
equip(glyph, targetSlot) {
const pelleMaxGlyphs = 1;
const glyphsEquipped = Glyphs.active.filter(Boolean).length;
if (
Pelle.isDoomed &&
(
Pelle.isDisabled("glyphs") ||
glyphsEquipped >= pelleMaxGlyphs ||
["effarig", "reality", "cursed"].includes(glyph.type)
)
) return;
@ -619,7 +616,9 @@ export const Glyphs = {
Currency.tachyonParticles.value = new Decimal(undoData.tp);
Currency.dilatedTime.value = new Decimal(undoData.dt);
}
if (AutomatorBackend.state.forceRestart) AutomatorBackend.restart();
if (Player.automatorUnlocked && AutomatorBackend.state.forceRestart) {
AutomatorBackend.start(player.reality.automator.state.editorScript);
}
},
copyForRecords(glyphList) {
// Sorting by effect ensures consistent ordering by type, based on how the effect bitmasks are structured

View File

@ -49,11 +49,12 @@ class ImaginaryUpgradeState extends BitPurchasableMechanicState {
EventHub.dispatch(GAME_EVENT.REALITY_UPGRADE_BOUGHT);
if (this.id >= 15 && this.id <= 18) {
DarkMatterDimension(this.id - 14).amount = DC.D1;
Tab.celestials.laitela.show();
if (this.id === 17) Laitela.quotes.thirdDMD.show();
}
if (this.id === 19) {
Tab.celestials.laitela.show();
if (this.id >= 15 && this.id <= 19) {
// Need to clear before retriggering, or else it won't actually show up on subsequent upgrades
TabNotification.laitelaUnlock.clearTrigger();
TabNotification.laitelaUnlock.tryTrigger();
}
if (this.id === 21) {
Laitela.quotes.finalRowIM.show();
@ -62,7 +63,7 @@ class ImaginaryUpgradeState extends BitPurchasableMechanicState {
BASIC_GLYPH_TYPES.forEach(x => player.reality.glyphs.sac[x] = ImaginaryUpgrade(22).effectValue);
}
if (this.id === 25) {
Tab.celestials.pelle.show();
TabNotification.pelleUnlock.tryTrigger();
}
}
}

View File

@ -49,9 +49,10 @@ class InfinityChallengeState extends GameMechanicState {
start() {
if (!this.isUnlocked || this.isRunning) return;
// Forces big crunch reset but ensures IP gain, if any.
bigCrunchReset(true, true);
player.challenge.normal.current = 0;
player.challenge.infinity.current = this.id;
bigCrunchResetValues();
if (!Enslaved.isRunning) Tab.dimensions.antimatter.show();
player.break = true;
if (EternityChallenge.isRunning) Achievement(115).unlock();
@ -90,14 +91,13 @@ class InfinityChallengeState extends GameMechanicState {
if (bestTimes[this.id - 1] <= player.records.thisInfinity.time) {
return;
}
// TODO: remove splice once player.challenge.infinity.bestTimes is not reactive
bestTimes.splice(this.id - 1, 1, player.records.thisInfinity.time);
player.challenge.infinity.bestTimes[this.id - 1] = player.records.thisInfinity.time;
GameCache.infinityChallengeTimeSum.invalidate();
}
exit() {
player.challenge.infinity.current = 0;
bigCrunchResetValues();
bigCrunchReset(true, false);
if (!Enslaved.isRunning) Tab.dimensions.antimatter.show();
}
}

View File

@ -1,217 +0,0 @@
import { RebuyableMechanicState } from "./game-mechanics/index";
export const kong = {};
kong.enabled = false;
kong.init = function() {
if (document.referrer.indexOf("kongregate") === -1)
return;
kong.enabled = true;
try {
kongregateAPI.loadAPI(() => {
window.kongregate = kongregateAPI.getAPI();
kong.updatePurchases();
});
// eslint-disable-next-line no-console
} catch (err) { console.log("Couldn't load Kongregate API"); }
};
class ShopPurchaseState extends RebuyableMechanicState {
get currency() {
return player.IAP.totalSTD - player.IAP.spentSTD;
}
get isAffordable() {
return this.currency >= this.cost;
}
get description() {
return this.config.description;
}
get cost() {
return this.config.cost;
}
get purchases() {
return player.IAP[this.config.key];
}
set purchases(value) {
player.IAP[this.config.key] = value;
}
get shouldDisplayMult() {
return Boolean(this.config.multiplier);
}
get currentMult() {
if (!this.shouldDisplayMult) return "";
return this.config.multiplier(player.IAP.disabled ? 0 : this.purchases);
}
get nextMult() {
if (!this.shouldDisplayMult) return "";
return this.config.multiplier(player.IAP.disabled ? 0 : this.purchases + 1);
}
// We want to still display the correct value in the button, so we need separate getters for it
get currentMultForDisplay() {
if (!this.shouldDisplayMult) return "";
return this.config.multiplier(this.purchases);
}
get nextMultForDisplay() {
if (!this.shouldDisplayMult) return "";
return this.config.multiplier(this.purchases + 1);
}
formatEffect(effect) {
return this.config.formatEffect?.(effect) || formatX(effect, 2, 0);
}
purchase() {
if (!this.canBeBought) return false;
if (GameEnd.creditsEverClosed) return false;
if (this.config.singleUse && ui.$viewModel.modal.progressBar) return false;
player.IAP.spentSTD += this.cost;
if (this.config.singleUse) {
this.config.onPurchase();
} else {
this.purchases++;
}
GameUI.update();
return true;
}
}
export const ShopPurchase = mapGameDataToObject(
GameDatabase.shopPurchases,
config => new ShopPurchaseState(config)
);
ShopPurchase.respecAll = function() {
for (const purchase of ShopPurchase.all) {
if (purchase.config.singleUse) continue;
player.IAP.spentSTD -= purchase.purchases * purchase.cost;
purchase.purchases = 0;
}
};
ShopPurchase.respecRequest = function() {
if (player.options.confirmations.respecIAP) {
Modal.respecIAP.show();
} else {
ShopPurchase.respecAll();
}
};
kong.purchaseTimeSkip = function() {
simulateTime(3600 * 6, true);
};
kong.purchaseLongerTimeSkip = function() {
simulateTime(3600 * 24, true);
};
kong.updatePurchases = function() {
if (!kong.enabled) return;
try {
kongregate.mtx.requestUserItemList("", items);
// eslint-disable-next-line no-console
} catch (e) { console.error(e); }
function items(result) {
let totalSTD = player.IAP.totalSTD;
for (let i = 0; i < result.data.length; i++) {
const item = result.data[i];
switch (item.identifier) {
case "doublemult":
totalSTD += 30;
break;
case "doubleip":
totalSTD += 40;
break;
case "tripleep":
totalSTD += 50;
break;
case "alldimboost":
totalSTD += 60;
break;
case "20worthofstd":
totalSTD += 20;
break;
case "50worthofstd":
totalSTD += 60;
break;
case "100worthofstd":
totalSTD += 140;
break;
case "200worthofstd":
totalSTD += 300;
break;
case "500worthofstd":
totalSTD += 1000;
break;
}
}
if (player.IAP.totalSTD !== totalSTD) {
// eslint-disable-next-line no-console
console.warn(`STD amounts don't match! ${player.IAP.totalSTD} in save, ${totalSTD} in kong`);
}
}
};
kong.migratePurchases = function() {
if (!kong.enabled) return;
try {
kongregate.mtx.requestUserItemList("", items);
// eslint-disable-next-line no-console
} catch (e) { console.log(e); }
function items(result) {
let ipPurchases = 0;
let dimPurchases = 0;
let epPurchases = 0;
let alldimPurchases = 0;
for (const item of result.data) {
if (item.identifier === "doublemult") {
player.IAP.totalSTD += 30;
player.IAP.spentSTD += 30;
dimPurchases++;
}
if (item.identifier === "doubleip") {
player.IAP.totalSTD += 40;
player.IAP.spentSTD += 40;
ipPurchases++;
}
if (item.identifier === "tripleep") {
player.IAP.totalSTD += 50;
player.IAP.spentSTD += 50;
epPurchases++;
}
if (item.identifier === "alldimboost") {
player.IAP.totalSTD += 60;
player.IAP.spentSTD += 60;
alldimPurchases++;
}
}
player.IAP.dimPurchases = dimPurchases;
player.IAP.allDimPurchases = alldimPurchases;
player.IAP.IPPurchases = ipPurchases;
player.IAP.EPPurchases = epPurchases;
}
};

View File

@ -4,6 +4,9 @@ export const NG = {
startNewGame() {
GameEnd.creditsClosed = false;
GameEnd.creditsEverClosed = false;
// We set this ASAP so that the AD tab is immediately recreated without END formatting, and any lag which could
// happen is instead hidden by the overlay from the credits rollback
player.celestials.pelle.doomed = false;
const backUpOptions = JSON.stringify(player.options);
// This can't be JSONed as it contains sets
const secretUnlocks = player.secretUnlocks;
@ -23,7 +26,7 @@ export const NG = {
player.reality.automator.scripts = JSON.parse(automatorScripts);
ui.view.newUI = player.options.newUI;
ui.view.news = player.options.news.enabled;
Themes.find(player.options.theme).set();
Themes.find(Theme.currentName()).set();
Notations.all.find(n => n.name === player.options.notation).setAsCurrent();
ADNotations.Settings.exponentCommas.show = player.options.commas;
player.lastUpdate = Date.now();

View File

@ -61,7 +61,7 @@ class NormalChallengeState extends GameMechanicState {
}
get isDisabled() {
return this.config.isDisabledInDoomed && Pelle.isDoomed;
return Pelle.isDoomed;
}
get lockedAt() {
@ -81,9 +81,10 @@ class NormalChallengeState extends GameMechanicState {
start() {
if (this.id === 1 || this.isOnlyActiveChallenge) return;
if (!Tab.challenges.isUnlocked) return;
// Forces big crunch reset but ensures IP gain, if any.
bigCrunchReset(true, true);
player.challenge.normal.current = this.id;
player.challenge.infinity.current = 0;
bigCrunchResetValues();
if (Enslaved.isRunning && EternityChallenge(6).isRunning && this.id === 10) {
EnslavedProgress.challengeCombo.giveProgress();
Enslaved.quotes.ec6C10.show();
@ -102,6 +103,12 @@ class NormalChallengeState extends GameMechanicState {
// and thus unlocking an autobuyer.
Achievement(52).tryUnlock();
Achievement(53).tryUnlock();
// Completing a challenge unlocks an autobuyer even if not purchased with antimatter, but we still
// need to clear the notification because otherwise it sticks there forever. Any other methods of
// unlocking autobuyers (such as Existentially Prolong) should also go through this code path
TabNotification.newAutobuyer.clearTrigger();
GameCache.cheapestAntimatterAutobuyer.invalidate();
}
get goal() {
@ -116,15 +123,14 @@ class NormalChallengeState extends GameMechanicState {
if (bestTimes[this.id - 2] <= player.records.thisInfinity.time) {
return;
}
// TODO: remove splice once player.challenge.infinity.bestTimes is not reactive
bestTimes.splice(this.id - 2, 1, player.records.thisInfinity.time);
player.challenge.normal.bestTimes[this.id - 2] = player.records.thisInfinity.time;
GameCache.challengeTimeSum.invalidate();
GameCache.worstChallengeTime.invalidate();
}
exit() {
player.challenge.normal.current = 0;
bigCrunchResetValues();
bigCrunchReset(true, false);
if (!Enslaved.isRunning) Tab.dimensions.antimatter.show();
}
}

View File

@ -1,21 +1,32 @@
const Payments = {
interval: null,
windowReference: null,
// This is here to prevent notification spam; purchase canceling can be called multiple times before the first
// call's Promise is settled
hasCanceled: false,
init: () => {
// We have unfinished checkouts
// We have unfinished checkouts from when the page was last closed
if (player.IAP.checkoutSession.id) {
Payments.pollForPurchases();
}
},
// Only called from clicking the "Buy More" button in the Shop tab
buyMoreSTD: async STD => {
player.IAP.checkoutSession = { id: true };
const res = await fetch("https://antimatterdimensionspayments.ew.r.appspot.com/purchase", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ amount: STD })
});
let res;
try {
res = await fetch(`${STD_BACKEND_URL}/purchase`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ amount: STD, cloudID: Cloud.user.id })
});
} catch (e) {
GameUI.notify.error("Could not contact payment server!", 10000);
return;
}
const data = await res.json();
Payments.windowReference = window.open(
data.url,
@ -26,25 +37,35 @@ const Payments = {
GameStorage.save();
Payments.pollForPurchases();
},
// Starts a purchase-checking loop and adds a listener which cancels any ongoing purchases if the page is closed.
// Any unresolved purchases will be reopened when the page is opened again in init()
pollForPurchases: () => {
console.log("Polling for purchases...");
const { id, amount } = player.IAP.checkoutSession;
let pollAmount = 0;
window.onbeforeunload = async() => {
if (!Payments.interval) return;
Payments.windowReference?.close();
await Payments.cancelPurchase();
await Payments.cancelPurchase(false);
};
// This setInterval checks every 3 seconds for a response from the payment backend
Payments.interval = setInterval(async() => {
pollAmount++;
const statusRes = await fetch(
`https://antimatterdimensionspayments.ew.r.appspot.com/validate?sessionId=${id}`
);
let statusRes;
try {
statusRes = await fetch(`${STD_BACKEND_URL}/validate?sessionId=${id}`);
} catch (e) {
// Note: Not redundant with notification in buyMoreSTD above; will not be reached if exception is thrown there
GameUI.notify.error("Could not contact payment server!", 10000);
Payments.clearInterval();
return;
}
const { completed, failure } = await statusRes.json();
if (completed) {
Payments.windowReference?.close();
player.IAP.totalSTD += amount;
await ShopPurchaseData.syncSTD();
GameUI.notify.success(`Purchase of ${amount} STDs was successful, thank you for your support! ❤️`, 10000);
Payments.clearInterval();
player.IAP.checkoutSession = { id: false };
@ -52,7 +73,6 @@ const Payments = {
Modal.hide();
}
if (failure) {
Payments.windowReference?.close();
Payments.clearInterval();
@ -64,24 +84,59 @@ const Payments = {
// 30 minutes of polling is the maximum
if (!completed && (Payments.windowReference?.closed || pollAmount >= 20 * 30)) {
await Payments.cancelPurchase();
await Payments.cancelPurchase(true);
}
}, 3000);
},
async cancelPurchase() {
// Sends a request to purchase a STD upgrade, returning true if successful (and syncs data), false if not
async buyUpgrade(upgradeKey) {
if (!Cloud.loggedIn) return false;
let res;
try {
res = await fetch(`${STD_BACKEND_URL}/upgrade`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ user: Cloud.user.id, upgrade: upgradeKey })
});
} catch (e) {
GameUI.notify.error("Unable to spend STD coins on upgrade!", 10000);
return false;
}
const stdData = await res.json();
// The "not enough STDs" message should only show up if the player modifies costs on the frontend and forces the
// game to send a request despite not actually having enough STDs. The cost check is done again on the backend
if (stdData.success) GameUI.notify.info(`Successfully spent ${stdData.amountSpent} STD coins`, 10000);
else GameUI.notify.error("Not enough STDs to purchase upgrade!", 10000);
ShopPurchaseData.syncSTD(false, stdData.data);
return stdData.success;
},
// Explicitly cancels purchases if the player chooses to, they take too long to resolve, or the page is closed
async cancelPurchase(isTimeout) {
if (this.hasCanceled) return;
Payments.windowReference?.close();
Payments.clearInterval();
await fetch("https://antimatterdimensionspayments.ew.r.appspot.com/expire", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ sessionId: player.IAP.checkoutSession.id })
});
GameUI.notify.error(`Purchase failed!`, 10000);
try {
await fetch(`${STD_BACKEND_URL}/expire`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ sessionId: player.IAP.checkoutSession.id })
});
} catch (e) {
GameUI.notify.error("Could not contact payment server!", 10000);
}
if (isTimeout) GameUI.notify.error("Purchase took too long to resolve!", 10000);
player.IAP.checkoutSession = { id: false };
GameStorage.save();
this.hasCanceled = false;
},
// Removes the repeating checker and page-close listener for if payments have been resolved
clearInterval() {
clearInterval(Payments.interval);
window.onbeforeunload = null;

View File

@ -192,7 +192,7 @@ window.player = {
},
annihilation: {
isActive: false,
multiplier: 5
multiplier: 1.05,
},
singularity: { isActive: false },
ipMultBuyer: { isActive: false, },
@ -267,6 +267,7 @@ window.player = {
records: {
gameCreatedTime: Date.now(),
totalTimePlayed: 0,
timePlayedAtBHUnlock: Number.MAX_VALUE,
realTimePlayed: 0,
realTimeDoomed: 0,
totalAntimatter: DC.E1,
@ -334,8 +335,10 @@ window.player = {
isUnlocked: false,
isActive: false,
isSegmented: false,
usedSTD: false,
hasStarted: false,
hideInfo: false,
displayAllMilestones: false,
startDate: 0,
name: "",
offlineTimeUsed: 0,
@ -767,9 +770,12 @@ window.player = {
retryCelestial: false,
showAllChallenges: false,
cloudEnabled: true,
showCloudModal: true,
forceCloudOverwrite: false,
syncSaveIntervals: true,
hotkeys: true,
theme: "Normal",
themeClassic: "Normal",
themeModern: "Normal",
commas: true,
updateRate: 33,
newUI: true,
@ -863,6 +869,7 @@ window.player = {
hiddenSubtabBits: Array.repeat(0, 11),
lastOpenTab: 0,
lastOpenSubtab: Array.repeat(0, 11),
currentMultiplierSubtab: 0,
fixedPerkStartingPos: false,
perkPhysicsEnabled: true,
automatorEvents: {
@ -874,16 +881,7 @@ window.player = {
}
},
IAP: {
totalSTD: 0,
spentSTD: 0,
IPPurchases: 0,
EPPurchases: 0,
RMPurchases: 0,
dimPurchases: 0,
allDimPurchases: 0,
replicantiPurchases: 0,
dilatedTimePurchases: 0,
disabled: false,
enabled: false,
checkoutSession: {
id: false,
}
@ -1012,7 +1010,6 @@ export function guardFromNaNValues(obj) {
for (const key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
// TODO: rework autobuyer saving
if (key === "automator") continue;
let value = obj[key];

View File

@ -75,10 +75,13 @@ export const GlyphSelection = {
},
select(glyphID, sacrifice) {
const chosenGlyph = this.glyphs[glyphID];
if (sacrifice) {
GlyphSacrificeHandler.removeGlyph(this.glyphs[glyphID], true);
GlyphSacrificeHandler.removeGlyph(chosenGlyph, true);
} else if (GameCache.glyphInventorySpace.value > 0) {
Glyphs.addToInventory(chosenGlyph);
} else {
Glyphs.addToInventory(this.glyphs[glyphID]);
AutoGlyphProcessor.getRidOfGlyph(chosenGlyph);
}
this.glyphs = [];
this.realityProps = undefined;
@ -113,11 +116,20 @@ export function requestManualReality() {
return;
}
if (GameCache.glyphInventorySpace.value === 0) {
Modal.message.show("Inventory cannot hold new Glyphs. Delete/sacrifice (shift-click) some Glyphs.",
Modal.message.show("No available inventory space; free up space by shift-clicking Glyphs to get rid of them.",
{ closeEvent: GAME_EVENT.GLYPHS_CHANGED });
return;
}
processManualReality(false);
startManualReality(false);
}
export function startManualReality(sacrifice, glyphID) {
if (player.options.animations.reality) {
runRealityAnimation();
setTimeout(processManualReality, 3000, sacrifice, glyphID);
} else {
processManualReality(sacrifice, glyphID);
}
}
export function processManualReality(sacrifice, glyphID) {
@ -167,16 +179,8 @@ export function processManualReality(sacrifice, glyphID) {
// We've already gotten a glyph at this point, so the second value has to be true.
// If we haven't sacrificed, we need to sort and purge glyphs, as applicable.
triggerManualReality(getRealityProps(false, true));
}
beginProcessReality(getRealityProps(false, true));
function triggerManualReality(realityProps) {
if (player.options.animations.reality) {
runRealityAnimation();
setTimeout(beginProcessReality, 3000, realityProps);
} else {
beginProcessReality(realityProps);
}
// Should be here so that the perk graphics update even when we're on the perk subtab, while also keeping its
// relatively expensive operations off of the reality reset hot path for when realities are significantly faster
PerkNetwork.updatePerkColor();
@ -207,19 +211,20 @@ function processAutoGlyph(gainedLevel, rng) {
// Always generate a list of glyphs to avoid RNG diverging based on whether
// a reality is done automatically.
const glyphs = GlyphSelection.glyphList(GlyphSelection.choiceCount, gainedLevel, { rng });
let keepGlyph;
if (EffarigUnlock.glyphFilter.isUnlocked) {
newGlyph = AutoGlyphProcessor.pick(glyphs);
if (!AutoGlyphProcessor.wouldKeep(newGlyph) || GameCache.glyphInventorySpace.value === 0) {
AutoGlyphProcessor.getRidOfGlyph(newGlyph);
newGlyph = null;
}
keepGlyph = AutoGlyphProcessor.wouldKeep(newGlyph);
} else {
// It really doesn't matter which we pick since they're random,
// so we might as well take the first one.
newGlyph = glyphs[0];
keepGlyph = true;
}
if (newGlyph && GameCache.glyphInventorySpace.value > 0) {
if (keepGlyph && GameCache.glyphInventorySpace.value > 0) {
Glyphs.addToInventory(newGlyph);
} else {
AutoGlyphProcessor.getRidOfGlyph(newGlyph);
}
}
@ -541,7 +546,9 @@ export function finishProcessReality(realityProps) {
disChargeAll();
}
}
if (AutomatorBackend.state.forceRestart) AutomatorBackend.restart();
if (Player.automatorUnlocked && AutomatorBackend.state.forceRestart) {
AutomatorBackend.start(player.reality.automator.state.editorScript);
}
if (player.options.automatorEvents.clearOnReality) AutomatorData.clearEventLog();
const celestialRunState = clearCelestialRuns();
@ -627,6 +634,7 @@ export function finishProcessReality(realityProps) {
player.dilation.lastEP = DC.DM1;
Currency.antimatter.reset();
Enslaved.autoReleaseTick = 0;
player.celestials.enslaved.hasSecretStudy = false;
player.celestials.laitela.entropy = 0;
playerInfinityUpgradesOnReset();
@ -636,7 +644,7 @@ export function finishProcessReality(realityProps) {
fullResetTimeDimensions();
resetChallengeStuff();
AntimatterDimensions.reset();
secondSoftReset();
secondSoftReset(false);
player.celestials.ra.peakGamespeed = 1;
InfinityDimensions.resetAmount();

View File

@ -764,9 +764,10 @@ GameDatabase.achievements.normal = [
${format(Decimal.NUMBER_MAX_VALUE, 1, 0)} times higher Infinity Points than the previous one.`;
},
checkRequirement: () => {
if (player.records.lastTenInfinities.some(i => i[0] === Number.MAX_VALUE)) return false;
const infinities = player.records.lastTenInfinities.map(run => run[1]);
for (let i = 0; i < infinities.length - 1; i++) {
if (infinities[i].lt(infinities[i + 1].times(Decimal.NUMBER_MAX_VALUE)) || infinities[i].eq(0)) return false;
if (infinities[i].lt(infinities[i + 1].times(Decimal.NUMBER_MAX_VALUE))) return false;
}
return true;
},
@ -1039,9 +1040,10 @@ GameDatabase.achievements.normal = [
${format(Decimal.NUMBER_MAX_VALUE, 1, 0)} times higher Eternity Points than the previous one.`;
},
checkRequirement: () => {
if (player.records.lastTenEternities.some(i => i[0] === Number.MAX_VALUE)) return false;
const eternities = player.records.lastTenEternities.map(run => run[1]);
for (let i = 0; i < eternities.length - 1; i++) {
if (eternities[i].lt(eternities[i + 1].times(Decimal.NUMBER_MAX_VALUE)) || eternities[i].eq(0)) return false;
if (eternities[i].lt(eternities[i + 1].times(Decimal.NUMBER_MAX_VALUE))) return false;
}
return true;
},

View File

@ -1813,8 +1813,8 @@ GameDatabase.celestials.navigation = {
complete: () => {
if (Pelle.isUnlocked) return 1;
const imCost = Math.clampMax(emphasizeEnd(Math.log10(Currency.imaginaryMachines.value) / Math.log10(1.6e15)), 1);
let laitelaProgress = Laitela.isRunning ? Currency.eternityPoints.value.log10() / 4000 : 0;
if (Laitela.difficultyTier !== 8) laitelaProgress = 0;
let laitelaProgress = Laitela.isRunning ? Math.min(Currency.eternityPoints.value.log10() / 4000, 0.99) : 0;
if (Laitela.difficultyTier !== 8 || Glyphs.activeList.length > 1) laitelaProgress = 0;
else if (ImaginaryUpgrade(25).isAvailableForPurchase) laitelaProgress = 1;
return (imCost + laitelaProgress) / 2;
},
@ -1835,7 +1835,7 @@ GameDatabase.celestials.navigation = {
];
}
let laitelaString = `${format(Currency.eternityPoints.value)} / ${format("1e4000")} EP`;
if (!Laitela.isRunning || Laitela.difficultyTier !== 8) {
if (!Laitela.isRunning || Laitela.difficultyTier !== 8 || Glyphs.activeList.length > 1) {
laitelaString = "Lai'tela's Reality is still intact";
} else if (ImaginaryUpgrade(25).isAvailableForPurchase) {
laitelaString = "Lai'tela's Reality has been destroyed";
@ -1888,8 +1888,8 @@ GameDatabase.celestials.navigation = {
Achievements.prePelleRows.length];
const alchemy = [AlchemyResources.all.countWhere(r => r.capped), AlchemyResources.all.length];
return [
`Complete ${formatInt(achievements[0])} / ${formatInt(achievements[1])} rows of achievements`,
`Fill ${formatInt(alchemy[0])} / ${formatInt(alchemy[1])} alchemy resources`,
`Complete ${formatInt(achievements[0])} / ${formatInt(achievements[1])} rows of Achievements`,
`Fill ${formatInt(alchemy[0])} / ${formatInt(alchemy[1])} Alchemy Resources`,
];
},
angle: 290,

View File

@ -108,13 +108,22 @@ GameDatabase.celestials.singularityMilestones = {
effectFormat: x => ((x === 0) ? "No autobuyers" : `Autobuy up to the ${["1st", "2nd", "3rd", "4th"][x - 1]} DMD`),
upgradeDirection: LAITELA_UPGRADE_DIRECTION.SELF_BOOST,
},
ascensionAutobuyers: {
start: 1e8,
repeat: 140,
limit: 4,
description: "DMD Ascension Autobuyers",
effect: completions => completions,
effectFormat: x => ((x === 0) ? "No autobuyers" : `Ascend up to the ${["1st", "2nd", "3rd", "4th"][x - 1]} DMD`),
upgradeDirection: LAITELA_UPGRADE_DIRECTION.SELF_BOOST,
},
darkAutobuyerSpeed: {
start: 45,
repeat: 650,
limit: 8,
description: "All Dark Matter Dimension Autobuyers trigger faster",
description: "Autobuyer speed for all DMD Autobuyers",
effect: completions => [30, 20, 15, 10, 5, 3, 2, 1, 0][completions],
effectFormat: x => `${formatInt(x)}s`,
effectFormat: x => (x === 0 ? "Instant" : `${formatInt(x)}s`),
upgradeDirection: LAITELA_UPGRADE_DIRECTION.SELF_BOOST,
},
realityDEMultiplier: {
@ -204,7 +213,7 @@ GameDatabase.celestials.singularityMilestones = {
start: 5e11,
repeat: 0,
limit: 1,
description: "Annihilation multiplier generates 4th Dark Matter Dimensions when Annihilation is available",
description: "Annihilation mult. generates 4th DMD when Annihilation is available",
effect: () => Laitela.darkMatterMult,
effectFormat: x => `${format(x, 2, 1)}/s`,
upgradeDirection: LAITELA_UPGRADE_DIRECTION.SELF_BOOST,
@ -218,6 +227,15 @@ GameDatabase.celestials.singularityMilestones = {
effectFormat: x => formatX(x, 2, 2),
upgradeDirection: LAITELA_UPGRADE_DIRECTION.SELF_BOOST,
},
annihilationAutobuyer: {
start: 4e18,
repeat: 0,
limit: 1,
description: "Unlock an Autobuyer for Annihilation",
effect: completions => completions,
effectFormat: x => (x === 1 ? "Unlocked" : "Locked"),
upgradeDirection: LAITELA_UPGRADE_DIRECTION.SELF_BOOST,
},
theoremPowerFromSingularities: {
start: 3e21,
repeat: 0,

View File

@ -16,7 +16,9 @@ GameDatabase.challenges.eternity = [
effect: completions =>
Decimal.pow(Math.max(player.records.thisEternity.time / 10, 0.9), 0.3 + (completions * 0.05)),
formatEffect: value => formatX(value, 2, 1)
}
},
// These will get notation-formatted and scrambled between for the final goal
scrambleText: ["1e2600", "1e201600"],
},
{
id: 2,
@ -77,14 +79,9 @@ GameDatabase.challenges.eternity = [
},
{
id: 6,
// The asterisk, if present, will get replaced with strings generated from the scramble text
description: () => {
if (Enslaved.isRunning) {
return Notations.current === Notation.shi
? "yo̶u̶ ̶c̶a̶n̶not̶ ̶g̶a̶i̶n̶ ̶A̶n̶t̶i̶m̶a̶t̶t̶e̶r̶ ̶G̶a̶l̶a̶x̶i̶e̶s̶ ̶n̶o̶r̶m̶a̶l̶l̶y̶. ̶ ̶ ̶The " +
" cost of upgrading your max Replicanti Galaxies is massively reduced."
: "you c㏰'퐚 gai鸭 Ant꟢matterﶓa⁍axie㮾랜䂇rma㦂l the cost of upgrading your max Replicanti" +
" Galaxies is massively reduced";
}
if (Enslaved.isRunning) return "you *. The cost of upgrading your max Replicanti Galaxies is massively reduced.";
return "you cannot gain Antimatter Galaxies normally. The cost of upgrading your max Replicanti" +
" Galaxies is massively reduced.";
},
@ -98,7 +95,8 @@ GameDatabase.challenges.eternity = [
const total = Math.round(Player.dimensionMultDecrease + Effects.sum(EternityChallenge(6).reward)) - value;
return `-${format(value, 2, 1)} (${formatX(total, 2, 1)} total)`;
}
}
},
scrambleText: ["cannot gain Antimatter Galaxies normally", "c㏰'퐚 gai鸭 Anti꟢at랜erﶓa⁍axie㮾 䂇orma㦂l"],
},
{
id: 7,

View File

@ -19,7 +19,6 @@ GameDatabase.challenges.normal = [
name: "1st Antimatter Dimension Autobuyer",
reward: "Upgradeable 1st Antimatter Dimension Autobuyer",
lockedAt: DC.D0,
isDisabledInDoomed: true,
},
{
id: 2,
@ -31,7 +30,6 @@ GameDatabase.challenges.normal = [
name: "2nd Antimatter Dimension Autobuyer",
reward: "Upgradeable 2nd Antimatter Dimension Autobuyer",
lockedAt: DC.D0,
isDisabledInDoomed: true,
},
{
id: 3,
@ -43,7 +41,6 @@ GameDatabase.challenges.normal = [
name: "3rd Antimatter Dimension",
reward: "Upgradeable 3rd Antimatter Dimension Autobuyer",
lockedAt: DC.D0,
isDisabledInDoomed: true,
},
{
id: 4,
@ -54,7 +51,6 @@ GameDatabase.challenges.normal = [
name: "4th Antimatter Dimension Autobuyer",
reward: "Upgradeable 4th Antimatter Dimension Autobuyer",
lockedAt: DC.D0,
isDisabledInDoomed: true,
},
{
id: 5,
@ -65,7 +61,6 @@ GameDatabase.challenges.normal = [
name: "5th Antimatter Dimension Autobuyer",
reward: "Upgradeable 5th Antimatter Dimension Autobuyer",
lockedAt: DC.D0,
isDisabledInDoomed: true,
},
{
id: 6,
@ -76,7 +71,6 @@ GameDatabase.challenges.normal = [
name: "6th Antimatter Dimension Autobuyer",
reward: "Upgradeable 6th Antimatter Dimension Autobuyer",
lockedAt: DC.D0,
isDisabledInDoomed: true,
},
{
id: 7,
@ -88,7 +82,6 @@ GameDatabase.challenges.normal = [
name: "7th Antimatter Dimension Autobuyer",
reward: "Upgradeable 7th Antimatter Dimension Autobuyer",
lockedAt: DC.D0,
isDisabledInDoomed: true,
},
{
id: 8,
@ -99,7 +92,6 @@ GameDatabase.challenges.normal = [
name: "8th Antimatter Dimension Autobuyer",
reward: "Upgradeable 8th Antimatter Dimension Autobuyer",
lockedAt: DC.D0,
isDisabledInDoomed: true,
},
{
id: 9,
@ -110,7 +102,6 @@ GameDatabase.challenges.normal = [
name: "Tickspeed Autobuyer",
reward: "Upgradeable Tickspeed Autobuyer",
lockedAt: DC.D0,
isDisabledInDoomed: true,
},
{
id: 10,
@ -121,7 +112,6 @@ GameDatabase.challenges.normal = [
name: "Automated Dimension Boosts",
reward: "Dimension Boosts Autobuyer",
lockedAt: DC.D16,
isDisabledInDoomed: true,
},
{
id: 11,
@ -132,7 +122,6 @@ GameDatabase.challenges.normal = [
name: "Automated Antimatter Galaxies",
reward: "Antimatter Galaxies Autobuyer",
lockedAt: DC.D16,
isDisabledInDoomed: true,
},
{
id: 12,

View File

@ -88,7 +88,7 @@ GameDatabase.eternity.dilation = {
doubleGalaxies: {
id: 4,
cost: 5e6,
description: () => `Gain twice as many Tachyon Galaxies, up to ${formatInt(1000)}`,
description: () => `Gain twice as many Tachyon Galaxies, up to ${formatInt(500)} base Galaxies`,
effect: 2
},
tdMultReplicanti: {

View File

@ -27,10 +27,13 @@ GameDatabase.eternity.milestones = {
eternities: 6,
reward: () => {
const EPmin = getOfflineEPGain(TimeSpan.fromMinutes(1).totalMilliseconds);
const em200 = getEternitiedMilestoneReward(TimeSpan.fromHours(1).totalMilliseconds, true).gt(0);
const em1000 = getInfinitiedMilestoneReward(TimeSpan.fromHours(1).totalMilliseconds, true).gt(0);
if (!player.options.offlineProgress) return `This milestone would give offline EP generation, but offline progress
is currently disabled.`;
const effectText = (em200 || em1000) ? "Inactive" : `Currently ${format(EPmin, 2, 2)} EP/min`;
return `While offline, gain ${formatPercents(0.25)} of your best Eternity Points per minute from previous
Eternities. (Currently ${format(EPmin, 2, 2)} EP/min)`;
Eternities. (${effectText})`;
},
activeCondition: () => (player.options.offlineProgress
? `Active as long as neither of the other offline milestones
@ -139,10 +142,10 @@ GameDatabase.eternity.milestones = {
// which seems messy to say the least.
// eslint-disable-next-line prefer-template
return `While offline, gain Eternities at ${formatPercents(0.5)} the rate of your fastest Eternity. ` +
(eternities.gt(0) ? `(currently ${format(eternities, 2, 2)}/hour)` : "(disabled)");
(eternities.gt(0) ? `(Currently ${format(eternities, 2, 2)}/hour)` : "(Inactive)");
},
activeCondition: () => (player.options.offlineProgress
? `Must be outside of all Challenges and Dilation
? `Must be outside of all Challenges and Dilation,
and the Eternity Autobuyer must be turned on and set to zero EP.`
: ""),
},
@ -155,7 +158,7 @@ GameDatabase.eternity.milestones = {
// eslint-disable-next-line prefer-template
return `While offline, gain Infinities equal to ${formatPercents(0.5)}
your best Infinities/hour this Eternity. ` +
(infinities.gt(0) ? `(currently ${format(infinities, 2, 2)}/hour)` : "(disabled)");
(infinities.gt(0) ? `(Currently ${format(infinities, 2, 2)}/hour)` : "(Inactive)");
},
activeCondition: () => (player.options.offlineProgress
? `Must be outside of Normal/Infinity Challenges and outside of EC4 and EC12,

View File

@ -119,7 +119,7 @@ autobuyers - in this situation autobuyers will effectively only trigger once eve
may have a strong impact depending on the part of the game.
<br>
<br>
${BlackHole(1).isUnlocked
${player.blackHole[0].unlocked
? `<b>Offline Black Hole behavior:</b> Once the Black Hole has been unlocked, the offline progress simulation will
attempt to run the game in a way where each tick contains roughly the same amount of <i>game</i> time. This may
give the appearance of the Black Hole(s) being active for a much larger fraction of time than normal while
@ -1048,8 +1048,8 @@ Duration - How long each speed burst lasts before going back to normal speed,
increased by ${formatPercents(0.3)} per upgrade.
<br>
<br>
Once you have ${formatInt(1)} year of <i>game time</i> on your save, you unlock a Reality Upgrade that allows
you to have a second Black Hole.
${formatInt(100)} days of <i>game time</i> after unlocking the Black Hole, you unlock the ability to purchase
a Reality Upgrade that allows you to have a second Black Hole.
The timer on the second Black Hole only advances when the first Black Hole is active. So, for example, if the first
Black Hole has a duration of ${formatInt(4)} minutes and the second has an interval of ${formatInt(8)} minutes, the
second Black Hole will only activate once every two cycles of the first Black Hole regardless of how short the
@ -1081,7 +1081,7 @@ the Black Hole tab.
<br>
<b>Hotkey: B</b> will pause/unpause the Black Holes.
`,
isUnlocked: () => BlackHole(1).isUnlocked,
isUnlocked: () => player.blackHole[0].unlocked,
tags: ["reality", "time", "speed", "duration", "interval", "rm", "endgame", "lategame"],
tab: "reality/hole"
}, {
@ -1292,7 +1292,7 @@ The Nameless Ones won't directly unlock the next Celestial.
// TODO Add the rest of the testers here too before release; this is all only pre wave 1
tags: ["reality", "time", "blackhole", "lategame", "endgame", "testers", "celestial",
"ikerstream", "realrapidjazz", "saturnus", "earth", "garnet", "pichusuperlover"],
tab: "celestials/nameless"
tab: "celestials/enslaved"
}, {
name: "Tesseracts",
info: () => `

View File

@ -48,6 +48,8 @@ export * from "./celestials/singularity-milestones";
import "./script-templates";
import "./speedrun-milestones";
import "./multiplier-tab/index";
import "./celestials/quotes/index";
import "./h2p";

View File

@ -0,0 +1,416 @@
import { DC } from "../../constants";
import { GameDatabase } from "../game-database";
import { PlayerProgress } from "../../app/player-progress";
import { MultiplierTabHelper } from "./helper-functions";
import { MultiplierTabIcons } from "./icons";
// See index.js for documentation
GameDatabase.multiplierTabValues.AD = {
total: {
name: dim => (dim ? `AD ${dim} Multiplier` : "Base AD Production"),
displayOverride: dim => {
if (dim) return formatX(AntimatterDimension(dim).multiplier, 2, 2);
const highestDim = AntimatterDimension(
EternityChallenge(7).isRunning ? 7 : MultiplierTabHelper.activeDimCount("AD")).totalAmount;
return `${format(AntimatterDimensions.all
.filter(ad => ad.isProducing)
.map(ad => ad.multiplier)
.reduce((x, y) => x.times(y), DC.D1)
.times(highestDim), 2)}/sec`;
},
multValue: dim => {
const mult = dim
? AntimatterDimension(dim).multiplier
: AntimatterDimensions.all
.filter(ad => ad.isProducing)
.map(ad => ad.multiplier)
.reduce((x, y) => x.times(y), DC.D1);
const highestDim = AntimatterDimension(
EternityChallenge(7).isRunning ? 7 : MultiplierTabHelper.activeDimCount("AD")).totalAmount;
return mult.times(highestDim).clampMin(1);
},
isActive: dim => (dim ? dim <= MultiplierTabHelper.activeDimCount("AD") : true),
dilationEffect: () => {
const baseEff = (player.dilation.active || Enslaved.isRunning)
? 0.75 * Effects.product(DilationUpgrade.dilationPenalty)
: 1;
return baseEff * (Effarig.isRunning ? Effarig.multDilation : 1);
},
isDilated: true,
overlay: ["Ω", "<i class='fas fa-cube' />"],
icon: dim => MultiplierTabIcons.DIMENSION("AD", dim),
},
purchase: {
name: dim => (dim ? `Purchased AD ${dim}` : "Purchases"),
multValue: dim => {
const getPurchases = ad => (Laitela.continuumActive
? AntimatterDimension(ad).continuumValue
: Math.floor(AntimatterDimension(ad).bought / 10)
);
if (dim) return Decimal.pow(AntimatterDimensions.buyTenMultiplier, getPurchases(dim));
return AntimatterDimensions.all
.filter(ad => ad.isProducing)
.map(ad => Decimal.pow(AntimatterDimensions.buyTenMultiplier, getPurchases(ad.tier)))
.reduce((x, y) => x.times(y), DC.D1);
},
isActive: () => !EternityChallenge(11).isRunning,
icon: dim => MultiplierTabIcons.PURCHASE("AD", dim),
},
highestDim: {
name: () => `Amount of highest Dimension`,
displayOverride: () => {
const dim = EternityChallenge(7).isRunning ? 7 : MultiplierTabHelper.activeDimCount("AD");
return `AD ${dim}, ${format(AntimatterDimension(dim).totalAmount, 2)}`;
},
multValue: () => {
const dim = EternityChallenge(7).isRunning ? 7 : MultiplierTabHelper.activeDimCount("AD");
return AntimatterDimension(dim).totalAmount;
},
isActive: () => AntimatterDimension(1).isProducing,
icon: MultiplierTabIcons.DIMENSION("AD"),
},
dimboost: {
name: dim => (dim ? `Dimboosts on AD ${dim}` : "Dimboosts"),
multValue: dim => (dim
? DimBoost.multiplierToNDTier(dim)
: AntimatterDimensions.all
.filter(ad => ad.isProducing)
.map(ad => DimBoost.multiplierToNDTier(ad.tier))
.reduce((x, y) => x.times(y), DC.D1)),
isActive: true,
icon: MultiplierTabIcons.DIMBOOST,
},
sacrifice: {
name: "Sacrifice Multiplier",
multValue: dim => ((!dim || dim === 8) ? Sacrifice.totalBoost : DC.D1),
isActive: dim => (!dim || dim === 8) && Sacrifice.totalBoost.gt(1) && !EternityChallenge(11).isRunning,
icon: MultiplierTabIcons.SACRIFICE("antimatter"),
},
achievementMult: {
name: "Achievement Multiplier",
multValue: dim => Decimal.pow(Achievements.power, dim ? 1 : MultiplierTabHelper.activeDimCount("AD")),
isActive: () => !Pelle.isDoomed && !EternityChallenge(11).isRunning,
icon: MultiplierTabIcons.ACHIEVEMENT,
},
achievement: {
name: "Achievement Rewards",
multValue: dim => {
const allMult = DC.D1.timesEffectsOf(
Achievement(48),
Achievement(56),
Achievement(65),
Achievement(72),
Achievement(73),
Achievement(74),
Achievement(76),
Achievement(84),
Achievement(91),
Achievement(92)
);
const dimMults = Array.repeat(DC.D1, 9);
for (let tier = 1; tier <= 8; tier++) {
if (tier === 1) {
dimMults[tier] = dimMults[tier].timesEffectsOf(
Achievement(28),
Achievement(31),
Achievement(68),
Achievement(71),
);
}
dimMults[tier] = dimMults[tier].timesEffectsOf(
tier === 8 ? Achievement(23) : null,
tier < 8 ? Achievement(34) : null,
tier <= 4 ? Achievement(64) : null,
);
if (Achievement(43).isUnlocked) {
dimMults[tier] = dimMults[tier].times(1 + tier / 100);
}
}
if (dim) return allMult.times(dimMults[dim]);
let totalMult = DC.D1;
for (let tier = 1; tier <= MultiplierTabHelper.activeDimCount("AD"); tier++) {
totalMult = totalMult.times(dimMults[tier]).times(allMult);
}
return totalMult;
},
powValue: () => Achievement(183).effectOrDefault(1),
isActive: () => !EternityChallenge(11).isRunning,
icon: MultiplierTabIcons.ACHIEVEMENT,
},
infinityUpgrade: {
name: dim => (dim ? `Infinity Upgrades (AD ${dim})` : "Infinity Upgrades"),
multValue: dim => {
const allMult = DC.D1.timesEffectsOf(
InfinityUpgrade.totalTimeMult,
InfinityUpgrade.thisInfinityTimeMult,
);
const dimMults = Array.repeat(DC.D1, 9);
for (let tier = 1; tier <= 8; tier++) {
if (tier === 1) {
dimMults[tier] = dimMults[tier].timesEffectsOf(
InfinityUpgrade.unspentIPMult,
InfinityUpgrade.unspentIPMult.chargedEffect,
);
}
dimMults[tier] = dimMults[tier].timesEffectsOf(
AntimatterDimension(tier).infinityUpgrade,
);
}
if (dim) return allMult.times(dimMults[dim]);
let totalMult = DC.D1;
for (let tier = 1; tier <= MultiplierTabHelper.activeDimCount("AD"); tier++) {
totalMult = totalMult.times(dimMults[tier]).times(allMult);
}
return totalMult;
},
powValue: dim => {
const allPow = InfinityUpgrade.totalTimeMult.chargedEffect.effectOrDefault(1) *
InfinityUpgrade.thisInfinityTimeMult.chargedEffect.effectOrDefault(1);
const dimPow = Array.repeat(1, 9);
for (let tier = 1; tier <= 8; tier++) {
dimPow[tier] = AntimatterDimension(tier).infinityUpgrade.chargedEffect.effectOrDefault(1);
}
if (dim) return allPow * dimPow[dim];
// This isn't entirely accurate because you can't return a power for all ADs if only some of them actually have
// it, so we cheat somewhat by returning the geometric mean of all actively producing dimensions (this should
// be close to the same value if all the base multipliers are similar in magnitude)
return allPow * Math.exp(dimPow.slice(1)
.map(n => Math.log(n)).sum() / MultiplierTabHelper.activeDimCount("AD"));
},
isActive: () => PlayerProgress.infinityUnlocked() && !EternityChallenge(11).isRunning,
icon: MultiplierTabIcons.UPGRADE("infinity"),
},
breakInfinityUpgrade: {
name: "Break Infinity Upgrades",
multValue: dim => {
const mult = DC.D1.timesEffectsOf(
BreakInfinityUpgrade.totalAMMult,
BreakInfinityUpgrade.currentAMMult,
BreakInfinityUpgrade.achievementMult,
BreakInfinityUpgrade.slowestChallengeMult,
BreakInfinityUpgrade.infinitiedMult
);
return Decimal.pow(mult, dim ? 1 : MultiplierTabHelper.activeDimCount("AD"));
},
isActive: () => player.break && !EternityChallenge(11).isRunning,
icon: MultiplierTabIcons.BREAK_INFINITY,
},
infinityPower: {
name: "Infinity Power",
fakeValue: () => Currency.infinityPower.value.pow(InfinityDimensions.powerConversionRate),
multValue: dim => {
const mult = Currency.infinityPower.value.pow(InfinityDimensions.powerConversionRate).max(1);
return Decimal.pow(mult, dim ? 1 : MultiplierTabHelper.activeDimCount("AD"));
},
isActive: () => Currency.infinityPower.value.gt(1) && !EternityChallenge(9).isRunning,
icon: MultiplierTabIcons.INFINITY_POWER,
},
infinityChallenge: {
name: dim => (dim ? `Infinity Challenges (AD ${dim})` : "Infinity Challenges"),
multValue: dim => {
const allMult = DC.D1.timesEffectsOf(
InfinityChallenge(3),
InfinityChallenge(3).reward,
);
const dimMults = Array.repeat(DC.D1, 9);
for (let tier = 1; tier <= 8; tier++) {
dimMults[tier] = dimMults[tier].timesEffectsOf(
tier > 1 && tier < 8 ? InfinityChallenge(8).reward : null
);
}
if (dim) return allMult.times(dimMults[dim]);
let totalMult = DC.D1;
for (let tier = 1; tier <= MultiplierTabHelper.activeDimCount("AD"); tier++) {
totalMult = totalMult.times(dimMults[tier]).times(allMult);
}
return totalMult;
},
powValue: () => InfinityChallenge(4).reward.effectOrDefault(1),
isActive: () => player.break && !EternityChallenge(11).isRunning,
icon: MultiplierTabIcons.CHALLENGE("infinity"),
},
timeStudy: {
name: dim => (dim ? `Time Studies (AD ${dim})` : "Time Studies"),
multValue: dim => {
const allMult = DC.D1.timesEffectsOf(
TimeStudy(91),
TimeStudy(101),
TimeStudy(161),
TimeStudy(193),
);
const dimMults = Array.repeat(DC.D1, 9);
for (let tier = 1; tier <= 8; tier++) {
// We don't want to double-count the base effect that TS31 boosts
const infinitiedMult = DC.D1.timesEffectsOf(
AntimatterDimension(tier).infinityUpgrade,
BreakInfinityUpgrade.infinitiedMult
);
dimMults[tier] = dimMults[tier].times(infinitiedMult.pow(TimeStudy(31).effectOrDefault(1) - 1));
dimMults[tier] = dimMults[tier].timesEffectsOf(
tier < 8 ? TimeStudy(71) : null,
tier === 8 ? TimeStudy(214) : null,
tier === 1 ? TimeStudy(234) : null,
);
}
if (dim) return allMult.times(dimMults[dim]);
let totalMult = DC.D1;
for (let tier = 1; tier <= MultiplierTabHelper.activeDimCount("AD"); tier++) {
totalMult = totalMult.times(dimMults[tier]).times(allMult);
}
return totalMult;
},
isActive: () => PlayerProgress.eternityUnlocked() && !EternityChallenge(11).isRunning,
icon: MultiplierTabIcons.TIME_STUDY,
},
eternityChallenge: {
name: "Eternity Challenges",
multValue: dim => Decimal.pow(EternityChallenge(10).effectValue,
dim ? 1 : MultiplierTabHelper.activeDimCount("AD")),
isActive: () => EternityChallenge(10).isRunning,
icon: MultiplierTabIcons.CHALLENGE("eternity"),
},
glyph: {
name: "Glyph Effects",
multValue: dim => {
const mult = getAdjustedGlyphEffect("powermult");
return Decimal.pow(mult, dim ? 1 : MultiplierTabHelper.activeDimCount("AD"));
},
powValue: () => {
const totalPow = getAdjustedGlyphEffect("powerpow") * getAdjustedGlyphEffect("effarigdimensions");
return totalPow * (player.dilation.active ? getAdjustedGlyphEffect("dilationpow") : 1);
},
isActive: () => PlayerProgress.realityUnlocked() && !EternityChallenge(11).isRunning,
icon: MultiplierTabIcons.GENERIC_GLYPH,
},
v: {
name: "V-Achievements",
powValue: () => VUnlocks.adPow.effectOrDefault(1),
isActive: () => PlayerProgress.realityUnlocked() && !EternityChallenge(11).isRunning,
icon: MultiplierTabIcons.ACHIEVEMENT,
},
alchemy: {
name: "Glyph Alchemy",
multValue: dim => {
const mult = AlchemyResource.dimensionality.effectOrDefault(1)
.times(Currency.realityMachines.value.powEffectOf(AlchemyResource.force));
return Decimal.pow(mult, dim ? 1 : MultiplierTabHelper.activeDimCount("AD"));
},
powValue: dim => {
const basePow = AlchemyResource.power.effectOrDefault(1) * Ra.momentumValue;
// Not entirely accurate, but returns the geometric mean of all producing dimensions (which should be close)
let inflationPow;
if (AlchemyResource.inflation.isUnlocked) {
if (dim) {
inflationPow = AntimatterDimension(dim).multiplier.gte(AlchemyResource.inflation.effectValue) ? 1.05 : 1;
} else {
const inflated = AntimatterDimensions.all
.countWhere(ad => ad.isProducing && ad.multiplier.gte(AlchemyResource.inflation.effectValue));
inflationPow = Math.pow(1.05, inflated / AntimatterDimensions.all.countWhere(ad => ad.isProducing));
}
}
return basePow * inflationPow;
},
isActive: () => Ra.unlocks.unlockGlyphAlchemy.canBeApplied && !EternityChallenge(11).isRunning,
icon: MultiplierTabIcons.ALCHEMY,
},
pelle: {
name: "Pelle Effects",
multValue: dim => Decimal.pow(PelleUpgrade.antimatterDimensionMult.effectOrDefault(1),
dim ? 1 : MultiplierTabHelper.activeDimCount("AD")),
powValue: () => PelleRifts.paradox.effectOrDefault(DC.D1).toNumber(),
isActive: () => Pelle.isDoomed && !EternityChallenge(11).isRunning,
icon: MultiplierTabIcons.PELLE,
},
iap: {
name: "Shop Tab Purchases",
multValue: dim => {
const mult = ShopPurchase.dimPurchases.currentMult * ShopPurchase.allDimPurchases.currentMult;
return Decimal.pow(mult, dim ? 1 : MultiplierTabHelper.activeDimCount("AD"));
},
isActive: () => ShopPurchaseData.totalSTD > 0 && !EternityChallenge(11).isRunning,
icon: MultiplierTabIcons.IAP,
},
effectNC: {
name: dim => (dim ? `Normal Challenge Effect (AD ${dim})` : "Normal Challenge Effects"),
multValue: dim => {
let dimMults = Array.repeat(DC.D1, 9);
if (NormalChallenge(2).isRunning) {
dimMults = Array.repeat(new Decimal(player.chall2Pow), 9);
} else if (NormalChallenge(3).isRunning) {
dimMults[1] = new Decimal(player.chall3Pow);
} else if (NormalChallenge(12).isRunning) {
dimMults[2] = AntimatterDimension(2).totalAmount.pow(0.6);
dimMults[4] = AntimatterDimension(4).totalAmount.pow(0.4);
dimMults[6] = AntimatterDimension(6).totalAmount.pow(0.2);
}
if (dim) return dimMults[dim];
let totalMult = DC.D1;
for (let tier = 1; tier <= MultiplierTabHelper.activeDimCount("AD"); tier++) {
totalMult = totalMult.times(dimMults[tier]);
}
return totalMult;
},
isActive: () => [2, 3, 12].some(c => NormalChallenge(c).isRunning),
icon: MultiplierTabIcons.CHALLENGE("infinity"),
},
nerfIC: {
name: dim => (dim ? `Infinity Challenge Nerf (AD ${dim})` : "Infinity Challenge Nerf"),
multValue: dim => {
let dimMults = Array.repeat(DC.D1, 9);
if (InfinityChallenge(4).isRunning) {
for (let tier = 1; tier <= 8; tier++) {
if (player.postC4Tier !== tier) {
dimMults[tier] = dimMults[tier].pow(1 - InfinityChallenge(4).effectValue).reciprocal();
}
}
} else if (InfinityChallenge(6).isRunning) {
dimMults = Array.repeat(DC.D1.dividedByEffectOf(InfinityChallenge(6)), 9);
} else if (InfinityChallenge(8).isRunning) {
dimMults = Array.repeat(DC.D1.timesEffectsOf(InfinityChallenge(8)), 9);
}
if (dim) return dimMults[dim];
let totalMult = DC.D1;
for (let tier = 1; tier <= MultiplierTabHelper.activeDimCount("AD"); tier++) {
totalMult = totalMult.times(dimMults[tier]);
}
return totalMult;
},
isActive: () => [4, 6, 8].some(ic => InfinityChallenge(ic).isRunning),
icon: MultiplierTabIcons.CHALLENGE("infinity"),
},
nerfV: {
name: "V's Reality",
powValue: () => 0.5,
isActive: () => V.isRunning,
icon: MultiplierTabIcons.GENERIC_V,
},
nerfCursed: {
name: "Cursed Glyphs",
powValue: () => getAdjustedGlyphEffect("curseddimensions"),
isActive: () => getAdjustedGlyphEffect("curseddimensions") !== 1,
icon: MultiplierTabIcons.SPECIFIC_GLYPH("cursed"),
},
nerfPelle: {
name: "Doomed Nerfs",
multValue: 0.1,
powValue: () => (PelleStrikes.infinity.hasStrike ? 0.5 : 1),
isActive: () => Pelle.isDoomed,
icon: MultiplierTabIcons.PELLE,
}
};

View File

@ -0,0 +1,24 @@
import { GameDatabase } from "../game-database";
import { MultiplierTabIcons } from "./icons";
// See index.js for documentation
GameDatabase.multiplierTabValues.AM = {
total: {
name: "Antimatter Production",
displayOverride: () => `${format(Currency.antimatter.productionPerSecond, 2, 2)}/sec`,
multValue: () => new Decimal(Currency.antimatter.productionPerSecond).clampMin(1),
isActive: true,
overlay: ["<i class='fas fa-atom' />"],
},
effarigAM: {
name: "Glyph Effect - Effarig Antimatter Production",
powValue: () => {
const ad1 = AntimatterDimension(1);
const baseProd = ad1.totalAmount.times(ad1.multiplier).times(Tickspeed.perSecond);
return Math.pow(baseProd.log10(), getAdjustedGlyphEffect("effarigantimatter") - 1);
},
isActive: () => getAdjustedGlyphEffect("effarigantimatter") > 1 && AntimatterDimension(1).isProducing,
icon: MultiplierTabIcons.SPECIFIC_GLYPH("effarig"),
}
};

View File

@ -0,0 +1,93 @@
import { DC } from "../../constants";
import { GameDatabase } from "../game-database";
import { PlayerProgress } from "../../app/player-progress";
import { MultiplierTabIcons } from "./icons";
// See index.js for documentation
GameDatabase.multiplierTabValues.DT = {
total: {
name: "Dilated Time gain",
isBase: true,
displayOverride: () => `${format(getDilationGainPerSecond().times(getGameSpeedupForDisplay()), 2, 2)}/sec`,
multValue: () => getDilationGainPerSecond().times(getGameSpeedupForDisplay()),
isActive: () => getDilationGainPerSecond().gt(0),
dilationEffect: () => (Enslaved.isRunning ? 0.85 : 1),
isDilated: true,
overlay: ["Ψ"],
},
achievement: {
name: "Achievements",
multValue: () => Achievement(132).effectOrDefault(1) * Achievement(137).effectOrDefault(1),
isActive: () => Achievement(132).canBeApplied || Achievement(137).canBeApplied,
icon: MultiplierTabIcons.ACHIEVEMENT,
},
dilation: {
name: "Repeatable Dilation Upgrades",
multValue: () => DC.D1.timesEffectsOf(
DilationUpgrade.dtGain,
DilationUpgrade.dtGainPelle,
DilationUpgrade.flatDilationMult
),
isActive: () => DC.D1.timesEffectsOf(
DilationUpgrade.dtGain,
DilationUpgrade.dtGainPelle,
DilationUpgrade.flatDilationMult
).gt(1),
icon: MultiplierTabIcons.UPGRADE("dilation"),
},
gamespeed: {
name: "Current Game speed",
multValue: () => getGameSpeedupForDisplay(),
isActive: () => getGameSpeedupForDisplay() > 1,
icon: MultiplierTabIcons.GAMESPEED,
},
realityUpgrade: {
name: "Temporal Amplifier",
multValue: () => RealityUpgrade(1).effectOrDefault(1),
isActive: () => RealityUpgrade(1).canBeApplied && !Pelle.isDoomed,
icon: MultiplierTabIcons.UPGRADE("reality"),
},
glyph: {
name: "Glyph Effects",
multValue: () => Decimal.times(getAdjustedGlyphEffect("dilationDT"),
Math.clampMin(Decimal.log10(Replicanti.amount) * getAdjustedGlyphEffect("replicationdtgain"), 1))
.times(Pelle.specialGlyphEffect.dilation),
isActive: () => PlayerProgress.realityUnlocked(),
icon: MultiplierTabIcons.GENERIC_GLYPH
},
ra: {
name: "Ra Upgrades",
multValue: () => DC.D1.timesEffectsOf(
Ra.unlocks.continuousTTBoost.effects.dilatedTime,
Ra.unlocks.peakGamespeedDT
),
isActive: () => Ra.unlocks.autoTP.canBeApplied,
icon: MultiplierTabIcons.GENERIC_RA,
},
alchemy: {
name: "Glyph Alchemy",
multValue: () => AlchemyResource.dilation.effectOrDefault(1),
isActive: () => Ra.unlocks.unlockGlyphAlchemy.canBeApplied,
icon: MultiplierTabIcons.ALCHEMY,
},
iap: {
name: "Shop Tab Purchases",
multValue: () => new Decimal(ShopPurchase.dilatedTimePurchases.currentMult ** (Pelle.isDoomed ? 0.5 : 1)),
isActive: () => ShopPurchaseData.totalSTD > 0,
icon: MultiplierTabIcons.IAP,
},
nerfV: {
name: "V's Reality",
powValue: () => 0.5,
isActive: () => V.isRunning,
icon: MultiplierTabIcons.GENERIC_V,
},
nerfPelle: {
name: "Doomed Nerfs",
multValue: 1e-5,
isActive: () => Pelle.isDoomed,
icon: MultiplierTabIcons.PELLE,
}
};

View File

@ -0,0 +1,38 @@
import { GameDatabase } from "../game-database";
import { MultiplierTabIcons } from "./icons";
// See index.js for documentation
GameDatabase.multiplierTabValues.eternities = {
total: {
name: "Eternities gained per Eternity",
isBase: true,
multValue: () => gainedEternities(),
isActive: () => (PlayerProgress.realityUnlocked() || Achievement(113).isUnlocked) && !Pelle.isDoomed,
overlay: ["Δ", "<i class='fa-solid fa-arrows-rotate' />"],
},
achievement: {
name: "Achievement 113",
multValue: () => Achievement(113).effectOrDefault(1),
isActive: () => Achievement(113).canBeApplied,
icon: MultiplierTabIcons.ACHIEVEMENT,
},
realityUpgrades: {
name: "Eternal Amplifier",
multValue: () => RealityUpgrade(3).effectOrDefault(1),
isActive: () => RealityUpgrade(3).canBeApplied,
icon: MultiplierTabIcons.UPGRADE("reality"),
},
glyph: {
name: "Equipped Glyphs",
multValue: () => getAdjustedGlyphEffect("timeetermult"),
isActive: () => PlayerProgress.realityUnlocked(),
icon: MultiplierTabIcons.GENERIC_GLYPH,
},
alchemy: {
name: "Eternity Alchemy Resource",
powValue: () => AlchemyResource.eternity.effectOrDefault(1),
isActive: () => AlchemyResource.eternity.canBeApplied,
icon: MultiplierTabIcons.ALCHEMY,
},
};

View File

@ -0,0 +1,100 @@
import { DC } from "../../constants";
import { GameDatabase } from "../game-database";
import { PlayerProgress } from "../../app/player-progress";
import { MultiplierTabIcons } from "./icons";
// See index.js for documentation
GameDatabase.multiplierTabValues.EP = {
total: {
name: "Total EP Gained on Eternity",
isBase: true,
multValue: () => gainedEternityPoints(),
isActive: () => new Decimal(Currency.eternities.value).gt(0) || gainedEternityPoints().gt(0),
dilationEffect: () => (Laitela.isRunning ? 0.75 * Effects.product(DilationUpgrade.dilationPenalty) : 1),
isDilated: true,
overlay: ["Δ", "<i class='fa-solid fa-layer-group' />"],
},
base: {
name: "Base Eternity Points",
isBase: true,
fakeValue: DC.D5,
multValue: () => DC.D5.pow(player.records.thisEternity.maxIP.plus(
gainedInfinityPoints()).log10() / (308 - PelleRifts.recursion.effectValue.toNumber()) - 0.7),
isActive: () => PlayerProgress.eternityUnlocked(),
icon: MultiplierTabIcons.CONVERT_FROM("IP"),
},
IP: {
name: "Eternity Points from Infinity Points",
displayOverride: () => `${format(player.records.thisEternity.maxIP.plus(gainedInfinityPoints()), 2, 2)} IP`,
// Just needs to match the value in base and be larger than 1
multValue: DC.D5,
isActive: () => PlayerProgress.eternityUnlocked(),
icon: MultiplierTabIcons.SPECIFIC_GLYPH("infinity"),
},
divisor: {
name: "Formula Improvement",
displayOverride: () => {
const div = 308 - PelleRifts.recursion.effectValue.toNumber();
return `log(IP)/${formatInt(308)} ➜ log(IP)/${format(div, 2, 2)}`;
},
powValue: () => 308 / (308 - PelleRifts.recursion.effectValue.toNumber()),
isActive: () => PelleRifts.recursion.canBeApplied,
icon: MultiplierTabIcons.DIVISOR("EP"),
},
eternityUpgrade: {
name: () => `Repeatable ${formatX(5)} Eternity Upgrade`,
multValue: () => EternityUpgrade.epMult.effectOrDefault(1),
isActive: () => PlayerProgress.eternityUnlocked() && !Pelle.isDoomed,
icon: MultiplierTabIcons.UPGRADE("eternity"),
},
timeStudy: {
name: "Time Studies",
multValue: () => DC.D1.timesEffectsOf(
TimeStudy(61),
TimeStudy(121),
TimeStudy(122),
TimeStudy(123),
),
isActive: () => PlayerProgress.eternityUnlocked() && !Pelle.isDoomed,
icon: MultiplierTabIcons.TIME_STUDY,
},
glyph: {
name: "Equipped Glyphs",
multValue: () => DC.D1.timesEffectsOf(GlyphEffect.epMult).times(Pelle.specialGlyphEffect.time),
powValue: () => (GlyphAlteration.isAdded("time") ? getSecondaryGlyphEffect("timeEP") : 1),
isActive: () => PlayerProgress.realityUnlocked(),
icon: MultiplierTabIcons.GENERIC_GLYPH,
},
realityUpgrade: {
name: "The Knowing Existence",
multValue: () => RealityUpgrade(12).effectOrDefault(1),
isActive: () => RealityUpgrade(12).canBeApplied && !Pelle.isDoomed,
icon: MultiplierTabIcons.UPGRADE("reality"),
},
pelle: {
name: "Pelle Rift Effects",
multValue: () => PelleRifts.vacuum.milestones[2].effectOrDefault(1),
isActive: () => PelleRifts.vacuum.milestones[2].canBeApplied,
icon: MultiplierTabIcons.PELLE,
},
iap: {
name: "Shop Tab Purchases",
multValue: () => ShopPurchase.EPPurchases.currentMult,
isActive: () => ShopPurchaseData.totalSTD > 0,
icon: MultiplierTabIcons.IAP,
},
nerfTeresa: {
name: "Teresa's Reality",
powValue: () => 0.55,
isActive: () => Teresa.isRunning,
icon: MultiplierTabIcons.GENERIC_TERESA,
},
nerfV: {
name: "V's Reality",
powValue: () => 0.5,
isActive: () => V.isRunning,
icon: MultiplierTabIcons.GENERIC_V,
},
};

View File

@ -0,0 +1,60 @@
import { GameDatabase } from "../game-database";
import { MultiplierTabHelper } from "./helper-functions";
import { MultiplierTabIcons } from "./icons";
// See index.js for documentation
GameDatabase.multiplierTabValues.galaxies = {
// Note: none of the galaxy types use the global multiplier that applies to all of them within multValue, which
// very slightly reduces performance impact and is okay because it's applied consistently
antimatter: {
name: "Antimatter Galaxies",
displayOverride: () => {
const num = player.galaxies + GalaxyGenerator.galaxies;
const mult = MultiplierTabHelper.globalGalaxyMult();
return `${formatInt(num)}, ${formatX(mult, 2, 2)} strength`;
},
multValue: () => Decimal.pow10(player.galaxies + GalaxyGenerator.galaxies),
isActive: true,
icon: MultiplierTabIcons.ANTIMATTER,
},
replicanti: {
name: "Replicanti Galaxies",
displayOverride: () => {
const num = Replicanti.galaxies.total;
let rg = Replicanti.galaxies.bought;
rg *= (1 + Effects.sum(TimeStudy(132), TimeStudy(133)));
rg += Replicanti.galaxies.extra;
rg += Math.min(Replicanti.galaxies.bought, ReplicantiUpgrade.galaxies.value) *
Effects.sum(EternityChallenge(8).reward);
const mult = rg / Math.clampMin(num, 1) * MultiplierTabHelper.globalGalaxyMult();
return `${formatInt(num)}, ${formatX(mult, 2, 2)} strength`;
},
multValue: () => {
let rg = Replicanti.galaxies.bought;
rg *= (1 + Effects.sum(TimeStudy(132), TimeStudy(133)));
rg += Replicanti.galaxies.extra;
rg += Math.min(Replicanti.galaxies.bought, ReplicantiUpgrade.galaxies.value) *
Effects.sum(EternityChallenge(8).reward);
return Decimal.pow10(rg);
},
isActive: () => Replicanti.areUnlocked,
icon: MultiplierTabIcons.SPECIFIC_GLYPH("replication"),
},
tachyon: {
name: "Tachyon Galaxies",
displayOverride: () => {
const num = player.dilation.totalTachyonGalaxies;
const mult = MultiplierTabHelper.globalGalaxyMult() *
(1 + Math.max(0, Replicanti.amount.log10() / 1e6) * AlchemyResource.alternation.effectValue);
return `${formatInt(num)}, ${formatX(mult, 2, 2)} strength`;
},
multValue: () => {
const num = player.dilation.totalTachyonGalaxies;
const mult = 1 + Math.max(0, Replicanti.amount.log10() / 1e6) * AlchemyResource.alternation.effectValue;
return Decimal.pow10(num * mult);
},
isActive: () => player.dilation.totalTachyonGalaxies > 0,
icon: MultiplierTabIcons.SPECIFIC_GLYPH("dilation"),
},
};

View File

@ -0,0 +1,103 @@
import { GameDatabase } from "../game-database";
import { MultiplierTabHelper } from "./helper-functions";
import { MultiplierTabIcons } from "./icons";
// See index.js for documentation
GameDatabase.multiplierTabValues.gamespeed = {
total: {
name: "Game speed",
isBase: true,
displayOverride: () => {
if (Enslaved.isStoringRealTime) return `Set to ${format(0)} (storing real time)`;
if (EternityChallenge(12).isRunning) return `${formatX(1)}/${formatInt(1000)} (fixed)`;
const curr = getGameSpeedupFactor();
const bh = MultiplierTabHelper.blackHoleSpeeds();
const currBH = bh.current;
const avgBH = bh.average;
const avgSpeed = Enslaved.isAutoReleasing
? getGameSpeedupForDisplay()
: curr / currBH * avgBH;
const avgString = ` (current) | ${formatX(avgSpeed, 2, 2)} (average)`;
return `${formatX(curr, 2, 2)}${curr === avgSpeed ? "" : avgString}`;
},
multValue: () => getGameSpeedupForDisplay(),
isActive: () => PlayerProgress.seenAlteredSpeed(),
dilationEffect: () => (Effarig.isRunning ? Effarig.multDilation : 1),
isDilated: true,
overlay: ["Δ", `<i class="fas fa-clock" />`, `<i class="fas fa-circle" />`],
},
glyph: {
name: "Equipped Glyphs",
multValue: () => getAdjustedGlyphEffect("timespeed"),
powValue: () => getAdjustedGlyphEffect("effarigblackhole"),
isActive: () => PlayerProgress.realityUnlocked() && !EternityChallenge(12).isRunning,
icon: MultiplierTabIcons.GENERIC_GLYPH,
},
blackHoleCurr: {
name: "Current Black Hole Speedup",
multValue: () => MultiplierTabHelper.blackHoleSpeeds().current,
isActive: () => BlackHole(1).isUnlocked && !BlackHoles.arePaused && !EternityChallenge(12).isRunning,
icon: MultiplierTabIcons.BLACK_HOLE,
},
blackHoleAvg: {
name: "Average Black Hole Speedup",
multValue: () => MultiplierTabHelper.blackHoleSpeeds().average,
isActive: () => BlackHole(1).isUnlocked && !BlackHoles.arePaused && !EternityChallenge(12).isRunning,
icon: MultiplierTabIcons.BLACK_HOLE,
},
achievementMult: {
name: "Achievement Multiplier",
multValue: () => Math.pow(VUnlocks.achievementBH.effectOrDefault(1),
BlackHoles.list.countWhere(bh => bh.isUnlocked)),
isActive: () => !BlackHoles.arePaused && VUnlocks.achievementBH.canBeApplied && !EternityChallenge(12).isRunning,
icon: MultiplierTabIcons.ACHIEVEMENT,
},
pulsing: {
name: "Auto-releasing Stored Time",
multValue: () => (Enslaved.isAutoReleasing
? Math.max(Enslaved.autoReleaseSpeed / getGameSpeedupFactor(), 1)
: getGameSpeedupFactor()),
isActive: () => Enslaved.canRelease() && Enslaved.isAutoReleasing && !EternityChallenge(12).isRunning,
icon: MultiplierTabIcons.BH_PULSE,
},
singularity: {
name: "Singularity Milestones",
multValue: () => SingularityMilestone.gamespeedFromSingularities.effectOrDefault(1),
isActive: () => SingularityMilestone.gamespeedFromSingularities.canBeApplied && !EternityChallenge(12).isRunning,
icon: MultiplierTabIcons.SINGULARITY,
},
pelle: {
name: "Pelle game speed Upgrade",
multValue: () => PelleUpgrade.timeSpeedMult.effectValue.toNumber(),
isActive: () => Pelle.isDoomed && !EternityChallenge(12).isRunning,
icon: MultiplierTabIcons.PELLE,
},
ec12: {
name: "Eternity Challenge 12",
multValue: () => 0.001 / getGameSpeedupForDisplay(),
isActive: () => EternityChallenge(12).isRunning,
icon: MultiplierTabIcons.CHALLENGE("eternity"),
},
chargingBH: {
name: "Black Hole Charging",
multValue: () => 1 - player.celestials.enslaved.storedFraction,
isActive: () => Enslaved.isStoringGameTime,
icon: MultiplierTabIcons.BLACK_HOLE,
},
invertedBH: {
name: "Inverted Black Hole",
multValue: () => player.blackHoleNegative,
isActive: () => BlackHoles.areNegative,
icon: MultiplierTabIcons.CHALLENGE("eternity"),
},
nerfLaitela: {
name: "Lai'tela's Reality",
powValue: () => Math.clampMax(Time.thisRealityRealTime.totalMinutes / 10, 1),
isActive: () => Laitela.isRunning,
icon: MultiplierTabIcons.GENERIC_LAITELA,
}
};

View File

@ -0,0 +1,130 @@
import { DC } from "../../constants";
import { GameDatabase } from "../game-database";
import { MultiplierTabHelper } from "./helper-functions";
import { MultiplierTabIcons } from "./icons";
// See index.js for documentation
GameDatabase.multiplierTabValues.general = {
achievement: {
name: (ach, dim) => (dim?.length === 2
? `Achievement ${ach} (${dim})`
: `Achievement ${ach}`),
multValue: (ach, dim) => {
// There is also a buy10 effect, but we don't track that in the multiplier tab
if (ach === 141) return Achievement(141).canBeApplied ? Achievement(141).effects.ipGain.effectOrDefault(1) : 1;
if (!dim) return Achievement(ach).canBeApplied ? Achievement(ach).effectOrDefault(1) : 1;
if (dim?.length === 2) {
let totalEffect = DC.D1;
for (let tier = 1; tier <= MultiplierTabHelper.activeDimCount(dim); tier++) {
let singleEffect;
if (ach === 43) singleEffect = Achievement(43).canBeApplied ? (1 + tier / 100) : 1;
else singleEffect = (MultiplierTabHelper.achievementDimCheck(ach, `${dim}${tier}`) &&
Achievement(ach).canBeApplied) ? Achievement(ach).effectOrDefault(1) : 1;
totalEffect = totalEffect.times(singleEffect);
}
return totalEffect;
}
if (ach === 43) return Achievement(43).canBeApplied ? (1 + Number(dim.charAt(2)) / 100) : 1;
return (MultiplierTabHelper.achievementDimCheck(ach, dim) && Achievement(ach).canBeApplied)
? Achievement(ach).effectOrDefault(1) : 1;
},
isActive: ach => Achievement(ach).canBeApplied,
icon: ach => {
const base = MultiplierTabIcons.ACHIEVEMENT;
return {
color: base.color,
symbol: `${base.symbol}${ach}`,
};
},
},
timeStudy: {
name: (ts, dim) => (dim?.length === 2
? `Time Study ${ts} (${dim})`
: `Time Study ${ts}`),
multValue: (ts, dim) => {
if (!dim) return TimeStudy(ts).canBeApplied ? TimeStudy(ts).effectOrDefault(1) : 1;
if (dim?.length === 2) {
let totalEffect = DC.D1;
for (let tier = 1; tier <= MultiplierTabHelper.activeDimCount(dim); tier++) {
totalEffect = totalEffect.times((MultiplierTabHelper.timeStudyDimCheck(ts, `${dim}${tier}`) &&
TimeStudy(ts).isBought) ? TimeStudy(ts).effectOrDefault(1) : 1);
}
return totalEffect;
}
// The new Decimal() wrapper is necessary because, for some inexplicable reason, replicanti becomes
// reactive through TS101 if that isn't there
return (MultiplierTabHelper.timeStudyDimCheck(ts, dim) && TimeStudy(ts).isBought)
? new Decimal(TimeStudy(ts).effectOrDefault(1)) : 1;
},
isActive: ts => TimeStudy(ts).isBought,
icon: ts => {
const base = MultiplierTabIcons.TIME_STUDY;
return {
color: base.color,
symbol: `${base.symbol}${ts}`,
};
},
},
infinityChallenge: {
name: ic => `Infinity Challenge ${ic}`,
displayOverride: ic => (ic === 4 ? formatPow(InfinityChallenge(4).reward.effectValue, 0, 3) : ""),
multValue: (ic, dim) => {
// We cheat here by actually giving IC4 a multiplier of a value equal to its effect on the final
// value in order to represent its proportion accurately. It's hidden by displayOverride
if (ic === 4) {
const ic4Pow = InfinityChallenge(4).reward.effectValue;
const mults = AntimatterDimensions.all.map(ad => ad.multiplier.pow((ic4Pow - 1) / ic4Pow));
if (dim?.length === 2) return mults.reduce((x, y) => x.times(y), DC.D1);
return mults[Number(dim.charAt(2)) - 1];
}
if (dim?.length === 2) {
let totalEffect = DC.D1;
for (let tier = 1; tier <= MultiplierTabHelper.activeDimCount(dim); tier++) {
totalEffect = totalEffect.times((MultiplierTabHelper.ICDimCheck(ic, `${dim}${tier}`) &&
InfinityChallenge(ic).isCompleted) ? InfinityChallenge(ic).reward.effectOrDefault(1) : 1);
}
return totalEffect;
}
const num = Number(dim.charAt(2));
if (ic === 8) return (num > 1 && num < 8) ? InfinityChallenge(ic).reward.effectValue : DC.D1;
return InfinityChallenge(ic).reward.effectValue;
},
isActive: ic => InfinityChallenge(ic).isCompleted,
icon: ic => {
const base = MultiplierTabIcons.CHALLENGE("infinity");
return {
color: base.color,
symbol: `${base.symbol}${ic}`,
};
},
},
eternityChallenge: {
name: ec => `Eternity Challenge ${ec}`,
multValue: (ec, dim) => {
if (dim?.length === 2) {
let totalEffect = DC.D1;
for (let tier = 1; tier <= MultiplierTabHelper.activeDimCount(dim); tier++) {
totalEffect = totalEffect.times(
(MultiplierTabHelper.ECDimCheck(ec, `${dim}${tier}`) && EternityChallenge(ec).reward.canBeApplied)
? EternityChallenge(ec).reward.effectOrDefault(1).clampMin(1)
: 1);
}
return totalEffect;
}
if (ec === 2) return dim === "ID1" ? EternityChallenge(ec).reward.effectValue : DC.D1;
return EternityChallenge(ec).reward.effectOrDefault(1);
},
isActive: ec => EternityChallenge(ec).reward.canBeApplied,
icon: ec => {
const base = MultiplierTabIcons.CHALLENGE("eternity");
return {
color: base.color,
symbol: `${base.symbol}${ec}`,
};
},
},
};

View File

@ -0,0 +1,206 @@
import { DC } from "../../../core/constants";
export const MultiplierTabHelper = {
// Helper method for counting enabled dimensions
activeDimCount(type) {
switch (type) {
case "AD":
// Technically not 100% correct, but within EC7 any AD8 production is going to be irrelevant compared to AD7
// and making the UI behave as if it's inactive produces a better look overall
return Math.clamp(AntimatterDimensions.all.filter(ad => ad.isProducing).length,
1, EternityChallenge(7).isRunning ? 7 : 8);
case "ID":
return InfinityDimensions.all.filter(id => id.isProducing).length;
case "TD":
return TimeDimensions.all.filter(td => td.isProducing).length;
default:
throw new Error("Unrecognized Dimension type in Multiplier tab GameDB entry");
}
},
// Helper method for galaxy strength multipliers affecting all galaxy types (this is used a large number of times)
globalGalaxyMult() {
return Effects.product(
InfinityUpgrade.galaxyBoost,
InfinityUpgrade.galaxyBoost.chargedEffect,
BreakInfinityUpgrade.galaxyBoost,
TimeStudy(212),
TimeStudy(232),
Achievement(86),
Achievement(178),
InfinityChallenge(5).reward,
PelleUpgrade.galaxyPower,
PelleRifts.decay.milestones[1]
);
},
// Helper method for galaxies and tickspeed, broken up as contributions of tickspeed*log(perGalaxy) and galaxyCount to
// their product, which is proportional to log(tickspeed)
decomposeTickspeed() {
let effectiveCount = effectiveBaseGalaxies();
const effects = this.globalGalaxyMult();
let galFrac, tickFrac;
if (effectiveCount < 3) {
let baseMult = 1.1245;
if (player.galaxies === 1) baseMult = 1.11888888;
if (player.galaxies === 2) baseMult = 1.11267177;
if (NormalChallenge(5).isRunning) {
baseMult = 1.08;
if (player.galaxies === 1) baseMult = 1.07632;
if (player.galaxies === 2) baseMult = 1.072;
}
// This is needed for numerical consistency with the other conditional case
baseMult /= 0.965 ** 2;
const logBase = Math.log10(baseMult);
const perGalaxy = 0.02 * effects;
effectiveCount *= Pelle.specialGlyphEffect.power;
tickFrac = Tickspeed.totalUpgrades * logBase;
galFrac = -Math.log10(Math.max(0.01, 1 / baseMult - (effectiveCount * perGalaxy))) / logBase;
} else {
effectiveCount -= 2;
effectiveCount *= effects;
effectiveCount *= getAdjustedGlyphEffect("realitygalaxies") * (1 + ImaginaryUpgrade(9).effectOrDefault(0));
effectiveCount *= Pelle.specialGlyphEffect.power;
// These all need to be framed as INCREASING x/sec tick rate (ie. all multipliers > 1, all logs > 0)
const baseMult = 0.965 ** 2 / (NormalChallenge(5).isRunning ? 0.83 : 0.8);
const logBase = Math.log10(baseMult);
const logPerGalaxy = -DC.D0_965.log10();
tickFrac = Tickspeed.totalUpgrades * logBase;
galFrac = (1 + effectiveCount / logBase * logPerGalaxy);
}
// Artificially inflate the galaxy portion in order to make the breakdown closer to 50/50 in common situations
galFrac *= 3;
// Calculate what proportion base tickspeed takes out of the entire tickspeed multiplier
const base = DC.D1.dividedByEffectsOf(
Achievement(36),
Achievement(45),
Achievement(66),
Achievement(83)
);
let baseFrac = base.log10() / Tickspeed.perSecond.log10();
// We want to make sure to zero out components in some edge cases
if (base.eq(1)) baseFrac = 0;
if (effectiveCount === 0) galFrac = 0;
// Normalize the sum by splitting tickspeed and galaxies across what's leftover besides the base value. These three
// values must be scaled so that they sum to 1 and none are negative
let factor = (1 - baseFrac) / (tickFrac + galFrac);
// The actual base tickspeed calculation multiplies things in a different order, which can lead to precision issues
// when no tickspeed upgrades have been bought if we don't explicitly set this to zero
if (Tickspeed.totalUpgrades === 0) factor = 0;
return {
base: baseFrac,
tickspeed: tickFrac * factor,
galaxies: galFrac * factor,
};
},
// Helper method to check for whether an achievement affects a particular dimension or not. Format of dimStr is
// expected to be a three-character string "XXN", eg. "AD3" or "TD2"
achievementDimCheck(ach, dimStr) {
switch (ach) {
case 23:
return dimStr === "AD8";
case 28:
case 31:
case 68:
case 71:
return dimStr === "AD1";
case 94:
return dimStr === "ID1";
case 34:
return dimStr.substr(0, 2) === "AD" && Number(dimStr.charAt(2)) !== 8;
case 64:
return dimStr.substr(0, 2) === "AD" && Number(dimStr.charAt(2)) <= 4;
default:
return true;
}
},
// Helper method to check for whether a time study affects a particular dimension or not, see achievementDimCheck()
timeStudyDimCheck(ts, dimStr) {
switch (ts) {
case 11:
return dimStr === "TD1";
case 71:
return dimStr.substr(0, 2) === "AD" && Number(dimStr.charAt(2)) !== 8;
case 72:
return dimStr === "ID4";
case 73:
return dimStr === "TD3";
case 214:
return dimStr === "AD8";
case 227:
return dimStr === "TD4";
case 234:
return dimStr === "AD1";
default:
return true;
}
},
// Helper method to check for whether an IC reward affects a particular dimension or not, see achievementDimCheck()
ICDimCheck(ic, dimStr) {
switch (ic) {
case 1:
case 6:
return dimStr.substr(0, 2) === "ID";
case 3:
case 4:
return dimStr.substr(0, 2) === "AD";
case 8:
return dimStr.substr(0, 2) === "AD" && Number(dimStr.charAt(2)) > 1 && Number(dimStr.charAt(2)) < 8;
default:
return false;
}
},
// Helper method to check for whether an EC reward affects a particular dimension or not, see achievementDimCheck()
ECDimCheck(ec, dimStr) {
switch (ec) {
case 1:
case 10:
return dimStr.substr(0, 2) === "TD";
case 2:
return dimStr === "ID1";
case 4:
case 9:
return dimStr.substr(0, 2) === "ID";
case 7:
return dimStr === "ID8";
default:
return false;
}
},
blackHoleSpeeds() {
const currBH = BlackHoles.list
.filter(bh => bh.isUnlocked)
.map(bh => (bh.isActive ? bh.power : 1))
.reduce((x, y) => x * y, 1);
// Calculate an average black hole speedup factor
const bh1 = BlackHole(1);
const bh2 = BlackHole(2);
const avgBH = 1 + (bh1.isUnlocked ? bh1.dutyCycle * (bh1.power - 1) : 0) +
(bh2.isUnlocked ? bh1.dutyCycle * bh2.dutyCycle * bh1.power * (bh2.power - 1) : 0);
return {
current: currBH,
average: avgBH
};
}
};
// All the resource files in this GameDB folder set props of multiplierTabValues, but it needs to be initialized.
// This file comes first in the import order and thus will make sure that nothing else attempts to define a prop
// on an undefined object
GameDatabase.multiplierTabValues = {};

View File

@ -0,0 +1,218 @@
/**
* Every entry in this object is a styling specification for bars within the multiplier tab.
* {
* @property {String} text String specifying the color to render the background of the bar (often a CSS var)
* @property {String} symbol String to show as text on the bar, may be HTML (allows for font awesome icons)
* @property {String} textColor A text color to override the default --color-text for better contrast
* }
*/
export const MultiplierTabIcons = {
DIMENSION(type, tier) {
const tierText = tier ?? "";
switch (type) {
case "AD":
return { symbol: `<b>Ω${tierText}</b>`, color: "var(--color-antimatter)" };
case "ID":
return { symbol: `<b>∞${tierText}</b>`, color: "var(--color-infinity)" };
case "TD":
return { symbol: `<b>Δ${tierText}</b>`, color: "var(--color-eternity)" };
default:
throw new Error("Unrecognized dimension type in multiplier tab icons");
}
},
PURCHASE(type, tier) {
const symbol = `<i class="fas fa-arrow-up-right-dots" />${tier ?? ""}`;
switch (type) {
case "AD":
return { symbol, color: "var(--color-antimatter)" };
case "ID":
return { symbol, color: "var(--color-infinity)" };
case "TD":
return { symbol, color: "var(--color-eternity)" };
case "baseID":
return { symbol: `<i class="fas fa-arrows-up-to-line" />`, color: "var(--color-infinity)" };
case "tesseractID":
return {
symbol: `<i class="fas fa-up-right-and-down-left-from-center" />`,
color: "var(--color-enslaved--base)"
};
default:
throw new Error("Unrecognized purchase type in multiplier tab icons");
}
},
CHALLENGE(type, tier) {
const tierText = `<i class="fas fa-arrow-down-wide-short" />${tier ?? ""}`;
switch (type) {
case "infinity":
return { symbol: `<b>∞</b>${tierText}`, color: "var(--color-infinity)" };
case "eternity":
return { symbol: `<b>Δ</b>${tierText}`, color: "var(--color-eternity)" };
default:
throw new Error("Unrecognized challenge type in multiplier tab icons");
}
},
// Regular sacrifice and glyph sacrifice
SACRIFICE(type) {
const icon = `<i class="fas fa-turn-down" />`;
switch (type) {
case "antimatter":
return { symbol: `<b>Ω</b>${icon}`, color: "var(--color-antimatter)" };
case "infinity":
return { symbol: `<b>∞</b>${icon}`, color: "var(--color-infinity)" };
case "time":
return { symbol: `<b>Δ</b>${icon}`, color: "var(--color-eternity)" };
case "dilation":
return { symbol: `<b>Ψ</b>${icon}`, color: "var(--color-dilation)", textColor: "black" };
default:
throw new Error("Unrecognized sacrifice type in multiplier tab icons");
}
},
UPGRADE(type) {
const icon = `<i class="fas fa-arrow-up" />`;
switch (type) {
case "infinity":
return { symbol: `<b>∞</b>${icon}`, color: "var(--color-infinity)" };
case "eternity":
return { symbol: `<b>Δ</b>${icon}`, color: "var(--color-eternity)" };
case "dilation":
return { symbol: `<b>Ψ</b>${icon}`, color: "var(--color-dilation)" };
case "reality":
return { symbol: `<b>Ϟ</b>${icon}`, color: "var(--color-reality)" };
case "imaginary":
return { symbol: `<i class="far fa-lightbulb" />${icon}`, color: "var(--color-ra--base)" };
default:
throw new Error("Unrecognized upgrade type in multiplier tab icons");
}
},
// Icons for base IP/EP
CONVERT_FROM(currency) {
if (currency === "AM") {
return {
symbol: `<i class='fas fa-atom' /><i class='fa-solid fa-arrow-right-arrow-left' />`,
color: "var(--color-antimatter)",
};
}
if (currency === "IP") {
return {
symbol: `<b>∞</b><i class='fa-solid fa-arrow-right-arrow-left' />`,
color: "var(--color-infinity)",
};
}
return {};
},
// IP and EP formula divisors
DIVISOR(currency) {
let color;
if (currency === "IP") color = "var(--color-infinity)";
if (currency === "EP") color = "var(--color-eternity)";
return {
symbol: `<i class='fas fa-calculator' />`,
color,
};
},
ANTIMATTER: {
symbol: `<i class='fas fa-atom' />`,
color: "var(--color-antimatter)",
},
DIMBOOST: {
symbol: `<i class="fas fa-angles-up" />`,
color: GameDatabase.reality.glyphTypes.power.color,
},
TICKSPEED: {
symbol: `<i class="fas fa-clock" />`,
color: "var(--color-eternity)",
},
GALAXY: {
symbol: `<i class="fas fa-bahai" />`,
color: "var(--color-eternity)",
},
ACHIEVEMENT: {
symbol: `<i class="fas fa-trophy" />`,
color: "var(--color-v--base)",
textColor: "black",
},
BREAK_INFINITY: {
symbol: `<i class="fab fa-skyatlas" />`,
color: "var(--color-infinity)",
textColor: "black",
},
INFINITY_POWER: {
symbol: `<b>∞</b><i class="fas fa-arrows-turn-right" />`,
color: "var(--color-infinity)",
textColor: "black",
},
IPOW_CONVERSION: {
symbol: `<i class="fas fa-arrow-down-up-across-line" />`,
color: "var(--color-infinity)",
textColor: "black",
},
TIME_STUDY: {
symbol: `<i class="fas fa-book" />`,
color: "var(--color-eternity)",
},
TACHYON_PARTICLES: {
symbol: `<i class="fas fa-meteor" />`,
color: "var(--color-dilation)",
},
GENERIC_GLYPH: {
symbol: `<i class="fas fa-clone" />`,
color: "var(--color-reality)",
},
SPECIFIC_GLYPH(type) {
return {
symbol: `<b>${GameDatabase.reality.glyphTypes[type].symbol}</b>`,
color: GameDatabase.reality.glyphTypes[type].color,
};
},
BLACK_HOLE: {
symbol: `<i class="fas fa-circle" />`,
color: "var(--color-reality)",
},
GAMESPEED: {
symbol: `<i class="fas fa-clock" />`,
color: "var(--color-reality)",
},
GENERIC_TERESA: {
symbol: "<b>Ϟ</b>",
color: "var(--color-teresa--base)",
},
GENERIC_ENSLAVED: {
symbol: `<div class="o-tab-btn--cel3">\uf0c1</div>`,
color: "var(--color-enslaved--base)",
},
GENERIC_V: {
symbol: "<b>⌬</b>",
color: "var(--color-v--base)",
textColor: "black",
},
GENERIC_RA: {
symbol: `<i class="fas fa-sun" />`,
color: "var(--color-ra--base)",
},
ALCHEMY: {
symbol: `<i class="fas fa-vial" />`,
color: "var(--color-ra-pet--effarig)",
},
BH_PULSE: {
symbol: `<i class="fas fa-expand-arrows-alt" />`,
color: "var(--color-reality)",
},
GENERIC_LAITELA: {
symbol: "<b>ᛝ</b>",
color: "var(--color-laitela--base)",
textColor: "var(--color-laitela--accent)",
},
SINGULARITY: {
symbol: `<i class="fas fa-arrows-up-down-left-right" />`,
color: "var(--color-laitela--base)",
textColor: "var(--color-laitela--accent)",
},
PELLE: {
symbol: "<b>♅</b>",
color: "var(--color-pelle--base)",
},
IAP: {
symbol: `<i class="fas fa-coins" />`,
color: "var(--color-accent)",
},
};

View File

@ -0,0 +1,48 @@
import "./helper-functions";
/**
* Most of the GameDB entries in this folder follow largely the same structure, but have been split into multiple
* for purposes of organization and ease-of-use. All fields may also be functions which may or may not accept an input
* argument, often having differing behavior based on its presence or absence - more often than not, the lack of an
* input argument will instead calculate the total contribution (eg. not providing a dimension tier will calculate
* the total across all dimensions).
* {
* @property {String} name Name to associate with this multiplier/effect
* @property {String} isBase Suppresses the leading × in multipliers if true. Primarily
* exists in order to avoid copy-pasting extensive entries in multValue
* @property {Decimal} displayOverride If present, displays this string instead of multipliers. This
* has higher priority than isBase
* @property {Decimal|Number} fakeValue Value to be used as a stand-in for a total when this entry
* is the parent resource of a list of other resources. Mostly used in entries that contribute to a whole differently
* than how they're further broken down (eg. IP/EP contibuting as multipliers but consisting of currencies)
* @property {Decimal|Number} multValue Value for multipliers given by this effect. Note that some
* entries may have a pow10 applied to them in order to "undo" logarithmic scaling in the UI
* @property {Number} powValue Numerical value for powers given by this effect
* @property {Number} dilationEffect Exponent to use for dilation effect
* @property {Boolean} isDilated Denotes if the multiplier is already dilated and needs an "anti-dilation"
* calculation to be applied to make the numbers in the UI correct. Defaults to false
* @property {Boolean} isActive Conditional determining if this component should be visible
* @property {Array String} overlay String array to be used as HTML for an overlay on the tab; all
* entries in the array are rendered on top of each other
* @property {Object} icon An object containing text and color for the bar that this
* entry has in the Vue component
* }
*/
import "./general";
import "./antimatter";
import "./antimatter-dimensions";
import "./infinity-dimensions";
import "./time-dimensions";
import "./infinity-points";
import "./eternity-points";
import "./tachyon-particles";
import "./dilated-time";
import "./tickspeed";
import "./galaxies";
import "./infinities";
import "./eternities";
import "./gamespeed";
// Some props in the tree are dynamically generated from value structure in the GameDB
import "./tree";

View File

@ -0,0 +1,54 @@
import { DC } from "../../constants";
import { GameDatabase } from "../game-database";
import { MultiplierTabIcons } from "./icons";
// See index.js for documentation
GameDatabase.multiplierTabValues.infinities = {
total: {
name: "Infinities gained per Crunch",
isBase: true,
multValue: () => gainedInfinities(),
isActive: () => Achievement(87).isUnlocked && !EternityChallenge(4).isRunning && !Pelle.isDoomed,
overlay: ["∞", "<i class='fa-solid fa-arrows-rotate' />"],
},
achievement: {
name: "Achievements",
multValue: () => DC.D1.timesEffectsOf(
Achievement(87),
Achievement(164)
),
isActive: () => Achievement(87).isUnlocked,
icon: MultiplierTabIcons.ACHIEVEMENT,
},
timeStudy: {
name: "Time Study 32",
multValue: () => TimeStudy(32).effectOrDefault(1),
isActive: () => TimeStudy(32).isBought,
icon: MultiplierTabIcons.TIME_STUDY,
},
realityUpgrades: {
name: "Reality Upgrades",
multValue: () => DC.D1.timesEffectsOf(RealityUpgrade(5), RealityUpgrade(7)),
isActive: () => PlayerProgress.realityUnlocked(),
icon: MultiplierTabIcons.UPGRADE("reality"),
},
glyph: {
name: "Equipped Glyphs",
multValue: () => getAdjustedGlyphEffect("infinityinfmult"),
isActive: () => PlayerProgress.realityUnlocked(),
icon: MultiplierTabIcons.GENERIC_GLYPH,
},
ra: {
name: "Ra Boost from Time Theorems",
multValue: () => Ra.unlocks.continuousTTBoost.effects.infinity.effectOrDefault(1),
isActive: () => Ra.unlocks.continuousTTBoost.isUnlocked,
icon: MultiplierTabIcons.GENERIC_RA,
},
singularity: {
name: "Singularity Milestones",
powValue: () => SingularityMilestone.infinitiedPow.effectOrDefault(1),
isActive: () => SingularityMilestone.infinitiedPow.canBeApplied,
icon: MultiplierTabIcons.SINGULARITY,
},
};

View File

@ -0,0 +1,295 @@
import { DC } from "../../constants";
import { GameDatabase } from "../game-database";
import { PlayerProgress } from "../../app/player-progress";
import { MultiplierTabHelper } from "./helper-functions";
import { MultiplierTabIcons } from "./icons";
// See index.js for documentation
GameDatabase.multiplierTabValues.ID = {
total: {
name: dim => {
if (dim) return `ID ${dim} Multiplier`;
if (EternityChallenge(7).isRunning) return "AD7 Production";
return "Infinity Power Production";
},
displayOverride: dim => (dim
? formatX(InfinityDimension(dim).multiplier, 2)
: `${format(InfinityDimension(1).productionPerSecond, 2)}/sec`
),
multValue: dim => (dim
? InfinityDimension(dim).multiplier
: InfinityDimensions.all
.filter(id => id.isProducing)
.map(id => id.multiplier)
.reduce((x, y) => x.times(y), DC.D1)),
isActive: dim => (InfinityDimension(dim ?? 1).isProducing || EternityMilestone.autoUnlockID.isReached) &&
!EternityChallenge(11).isRunning,
dilationEffect: () => {
const baseEff = player.dilation.active
? 0.75 * Effects.product(DilationUpgrade.dilationPenalty)
: 1;
return baseEff * (Effarig.isRunning ? Effarig.multDilation : 1);
},
isDilated: true,
overlay: ["∞", "<i class='fa-solid fa-cube' />"],
icon: dim => MultiplierTabIcons.DIMENSION("ID", dim),
},
purchase: {
name: dim => (dim ? `Purchased ID ${dim}` : "Purchases"),
multValue: dim => {
const getMult = id => Decimal.pow(InfinityDimension(id).powerMultiplier,
Math.floor(InfinityDimension(id).baseAmount / 10));
if (dim) return getMult(dim);
return InfinityDimensions.all
.filter(id => id.isProducing)
.map(id => getMult(id.tier))
.reduce((x, y) => x.times(y), DC.D1);
},
isActive: () => !EternityChallenge(2).isRunning && !EternityChallenge(10).isRunning,
icon: dim => MultiplierTabIcons.PURCHASE("ID", dim),
},
highestDim: {
name: () => `Amount of highest Dimension`,
displayOverride: () => {
const dim = MultiplierTabHelper.activeDimCount("ID");
return `ID ${dim}, ${format(InfinityDimension(dim).amount, 2)}`;
},
multValue: () => InfinityDimension(MultiplierTabHelper.activeDimCount("ID")).amount,
isActive: () => InfinityDimension(1).isProducing,
icon: MultiplierTabIcons.DIMENSION("ID"),
},
basePurchase: {
name: "Base purchases",
multValue: dim => {
const getMult = id => {
const purchases = id === 8
? Math.floor(InfinityDimension(id).baseAmount / 10)
: Math.min(InfinityDimensions.HARDCAP_PURCHASES, Math.floor(InfinityDimension(id).baseAmount / 10));
const baseMult = InfinityDimension(id)._powerMultiplier;
return Decimal.pow(baseMult, purchases);
};
if (dim) return getMult(dim);
return InfinityDimensions.all
.filter(id => id.isProducing)
.map(id => getMult(id.tier))
.reduce((x, y) => x.times(y), DC.D1);
},
isActive: dim => ImaginaryUpgrade(14).canBeApplied ||
(dim === 8 ? GlyphSacrifice.infinity.effectValue > 1 : Tesseracts.bought > 0),
icon: MultiplierTabIcons.PURCHASE("baseID"),
},
tesseractPurchase: {
name: "Tesseracts",
multValue: dim => {
const getMult = id => {
if (id === 8) return DC.D1;
const purchases = Math.floor(InfinityDimension(id).baseAmount / 10);
return Decimal.pow(InfinityDimension(id).powerMultiplier,
Math.clampMin(purchases - InfinityDimensions.HARDCAP_PURCHASES, 0));
};
if (dim) return getMult(dim);
return InfinityDimensions.all
.filter(id => id.isProducing)
.map(id => getMult(id.tier))
.reduce((x, y) => x.times(y), DC.D1);
},
isActive: () => Tesseracts.bought > 0,
icon: MultiplierTabIcons.PURCHASE("tesseractID"),
},
infinityGlyphSacrifice: {
name: "Infinity Glyph sacrifice",
multValue: () => (InfinityDimension(8).isProducing
? Decimal.pow(GlyphSacrifice.infinity.effectValue, Math.floor(InfinityDimension(8).baseAmount / 10))
: DC.D1),
isActive: () => GlyphSacrifice.infinity.effectValue > 1,
icon: MultiplierTabIcons.SACRIFICE("infinity"),
},
powPurchase: {
name: "Reflection of Intrusion",
powValue: () => ImaginaryUpgrade(14).effectOrDefault(1),
isActive: () => ImaginaryUpgrade(14).canBeApplied,
icon: MultiplierTabIcons.UPGRADE("imaginary"),
},
replicanti: {
name: "Replicanti Multiplier",
multValue: dim => Decimal.pow(replicantiMult(), dim ? 1 : MultiplierTabHelper.activeDimCount("ID")),
isActive: () => Replicanti.areUnlocked,
icon: MultiplierTabIcons.SPECIFIC_GLYPH("replication"),
},
achievementMult: {
name: "Achievement Multiplier",
multValue: dim => Decimal.pow(Achievements.power, dim ? 1 : MultiplierTabHelper.activeDimCount("ID")),
isActive: () => Achievement(75).canBeApplied && !Pelle.isDoomed,
icon: MultiplierTabIcons.ACHIEVEMENT,
},
achievement: {
// Note: This only applies to ID1
name: () => "Achievement 94",
multValue: dim => ((dim ?? 1) === 1 ? Achievement(94).effectOrDefault(1) : 1),
isActive: () => Achievement(94).canBeApplied,
icon: MultiplierTabIcons.ACHIEVEMENT,
},
timeStudy: {
name: dim => (dim ? `Time Studies (ID ${dim})` : "Time Studies"),
multValue: dim => {
const allMult = DC.D1.timesEffectsOf(
TimeStudy(82),
TimeStudy(92),
TimeStudy(162)
);
if (dim) return dim === 4 ? allMult.times(TimeStudy(72).effectOrDefault(1)) : allMult;
const maxActiveDim = MultiplierTabHelper.activeDimCount("ID");
return Decimal.pow(allMult, maxActiveDim).times(maxActiveDim >= 4 ? TimeStudy(72).effectOrDefault(1) : DC.D1);
},
isActive: () => PlayerProgress.eternityUnlocked(),
icon: MultiplierTabIcons.TIME_STUDY,
},
eternityUpgrade: {
name: "Eternity Upgrades",
multValue: dim => {
const allMult = DC.D1.timesEffectsOf(
EternityUpgrade.idMultEP,
EternityUpgrade.idMultEternities,
EternityUpgrade.idMultICRecords,
);
return Decimal.pow(allMult, dim ? 1 : MultiplierTabHelper.activeDimCount("ID"));
},
isActive: () => PlayerProgress.eternityUnlocked(),
icon: MultiplierTabIcons.UPGRADE("eternity"),
},
eu1: {
name: () => "Unspent Eternity Points",
multValue: dim => Decimal.pow(EternityUpgrade.idMultEP.effectOrDefault(1),
dim ? 1 : MultiplierTabHelper.activeDimCount("ID")),
isActive: () => EternityUpgrade.idMultEP.canBeApplied,
icon: MultiplierTabIcons.UPGRADE("eternity"),
},
eu2: {
name: () => "Eternity Count",
multValue: dim => Decimal.pow(EternityUpgrade.idMultEternities.effectOrDefault(1),
dim ? 1 : MultiplierTabHelper.activeDimCount("ID")),
isActive: () => EternityUpgrade.idMultEternities.canBeApplied,
icon: MultiplierTabIcons.UPGRADE("eternity"),
},
eu3: {
name: () => "Infinity Challenge Records",
multValue: dim => Decimal.pow(EternityUpgrade.idMultICRecords.effectOrDefault(1),
dim ? 1 : MultiplierTabHelper.activeDimCount("ID")),
isActive: () => EternityUpgrade.idMultICRecords.canBeApplied,
icon: MultiplierTabIcons.UPGRADE("eternity"),
},
infinityChallenge: {
name: "Infinity Challenges",
multValue: dim => {
const allMult = DC.D1.timesEffectsOf(
InfinityChallenge(1).reward,
InfinityChallenge(6).reward,
);
return Decimal.pow(allMult, dim ? 1 : MultiplierTabHelper.activeDimCount("ID"));
},
isActive: () => InfinityChallenge(1).isCompleted,
icon: MultiplierTabIcons.CHALLENGE("infinity"),
},
eternityChallenge: {
name: dim => (dim ? `Eternity Challenges (ID ${dim})` : " Eternity Challenges"),
multValue: dim => {
const allMult = DC.D1.timesEffectsOf(
EternityChallenge(4).reward,
EternityChallenge(9).reward,
).times(EternityChallenge(7).isRunning ? Tickspeed.perSecond : DC.D1);
if (dim) {
if (dim === 1) return allMult.times(EternityChallenge(2).reward.effectOrDefault(1));
return allMult;
}
const maxActiveDim = MultiplierTabHelper.activeDimCount("ID");
return Decimal.pow(allMult, maxActiveDim)
.times(maxActiveDim >= 1 ? EternityChallenge(2).reward.effectOrDefault(1) : DC.D1);
},
isActive: () => EternityChallenge(2).completions > 0,
icon: MultiplierTabIcons.CHALLENGE("eternity"),
},
tickspeed: {
name: () => "Tickspeed (EC7)",
displayOverride: () => {
const tickRate = Tickspeed.perSecond;
const activeDims = MultiplierTabHelper.activeDimCount("ID");
return `${format(tickRate, 2, 2)}/sec on ${formatInt(activeDims)} ${pluralize("Dimension", activeDims)}
${formatX(tickRate.pow(activeDims), 2, 2)}`;
},
multValue: () => Tickspeed.perSecond.pow(8),
isActive: () => EternityChallenge(7).isRunning,
icon: MultiplierTabIcons.TICKSPEED,
},
glyph: {
name: "Glyph Effects",
multValue: () => 1,
powValue: () => getAdjustedGlyphEffect("infinitypow") * getAdjustedGlyphEffect("effarigdimensions"),
isActive: () => PlayerProgress.realityUnlocked(),
icon: MultiplierTabIcons.GENERIC_GLYPH,
},
alchemy: {
name: "Glyph Alchemy",
multValue: dim => Decimal.pow(AlchemyResource.dimensionality.effectOrDefault(1),
dim ? 1 : MultiplierTabHelper.activeDimCount("ID")),
powValue: () => AlchemyResource.infinity.effectOrDefault(1) * Ra.momentumValue,
isActive: () => Ra.unlocks.unlockGlyphAlchemy.canBeApplied,
icon: MultiplierTabIcons.ALCHEMY,
},
imaginaryUpgrade: {
name: "Hyperbolic Apeirogon",
multValue: dim => Decimal.pow(ImaginaryUpgrade(8).effectOrDefault(1),
dim ? 1 : MultiplierTabHelper.activeDimCount("ID")),
isActive: () => ImaginaryUpgrade(8).canBeApplied,
icon: MultiplierTabIcons.UPGRADE("imaginary"),
},
pelle: {
name: "Pelle Rift Effects",
multValue: dim => {
const mult = DC.D1.timesEffectsOf(PelleRifts.recursion.milestones[1]);
const maxActiveDim = MultiplierTabHelper.activeDimCount("ID");
return Decimal.pow(mult, dim ? 1 : maxActiveDim)
.times(maxActiveDim >= 1 ? PelleRifts.decay.milestones[0].effectOrDefault(1) : DC.D1);
},
powValue: () => PelleRifts.paradox.effectOrDefault(DC.D1).toNumber(),
isActive: () => Pelle.isDoomed,
icon: MultiplierTabIcons.PELLE,
},
iap: {
name: "Shop Tab Purchases",
multValue: dim => Decimal.pow(ShopPurchase.allDimPurchases.currentMult,
dim ? 1 : MultiplierTabHelper.activeDimCount("ID")),
isActive: () => ShopPurchaseData.totalSTD > 0,
icon: MultiplierTabIcons.IAP,
},
powerConversion: {
name: "Infinity Power Conversion",
powValue: () => InfinityDimensions.powerConversionRate,
isActive: () => Currency.infinityPower.value.gt(1) && !EternityChallenge(9).isRunning,
icon: MultiplierTabIcons.IPOW_CONVERSION,
},
nerfV: {
name: "V's Reality",
powValue: () => 0.5,
isActive: () => V.isRunning,
icon: MultiplierTabIcons.GENERIC_V,
},
nerfCursed: {
name: "Cursed Glyphs",
powValue: () => getAdjustedGlyphEffect("curseddimensions"),
isActive: () => getAdjustedGlyphEffect("curseddimensions") !== 1,
icon: MultiplierTabIcons.SPECIFIC_GLYPH("cursed"),
},
nerfPelle: {
name: "Doomed Nerfs",
powValue: 0.5,
isActive: () => PelleStrikes.powerGalaxies.hasStrike,
icon: MultiplierTabIcons.PELLE,
}
};

View File

@ -0,0 +1,121 @@
import { DC } from "../../constants";
import { GameDatabase } from "../game-database";
import { PlayerProgress } from "../../app/player-progress";
import { MultiplierTabIcons } from "./icons";
// See index.js for documentation
GameDatabase.multiplierTabValues.IP = {
total: {
name: "Total IP Gained on Infinity",
isBase: true,
multValue: () => gainedInfinityPoints(),
isActive: () => new Decimal(Currency.infinities.value).gt(0) || gainedInfinityPoints().gt(0),
dilationEffect: () => (Laitela.isRunning ? 0.75 * Effects.product(DilationUpgrade.dilationPenalty) : 1),
isDilated: true,
overlay: ["∞", "<i class='fa-solid fa-layer-group' />"],
},
base: {
name: "Base Infinity Points",
isBase: true,
fakeValue: DC.D5,
multValue: () => {
const div = Effects.min(308, Achievement(103), TimeStudy(111));
return Decimal.pow10(player.records.thisInfinity.maxAM.log10() / div - 0.75);
},
isActive: () => player.break,
icon: MultiplierTabIcons.CONVERT_FROM("AM"),
},
antimatter: {
name: "Infinity Points from Antimatter",
displayOverride: () => `${format(player.records.thisInfinity.maxAM, 2, 2)} AM`,
// Just needs to match the value in base and be larger than 1
multValue: DC.D5,
isActive: () => player.break,
icon: MultiplierTabIcons.ANTIMATTER,
},
divisor: {
name: "Formula Improvement",
displayOverride: () => {
const div = Effects.min(308, Achievement(103), TimeStudy(111));
return `log(AM)/${formatInt(308)} ➜ log(AM)/${format(div, 2, 1)}`;
},
powValue: () => 308 / Effects.min(308, Achievement(103), TimeStudy(111)),
isActive: () => Achievement(103).canBeApplied || TimeStudy(111).isBought,
icon: MultiplierTabIcons.DIVISOR("IP"),
},
infinityUpgrade: {
name: () => `Repeatable ${formatX(2)} Infinity Upgrade`,
multValue: () => InfinityUpgrade.ipMult.effectOrDefault(1),
isActive: () => player.break && !Pelle.isDoomed,
icon: MultiplierTabIcons.UPGRADE("infinity"),
},
achievement: {
name: "Achievements",
multValue: () => DC.D1.timesEffectsOf(
Achievement(85),
Achievement(93),
Achievement(116),
Achievement(125),
Achievement(141).effects.ipGain,
),
isActive: () => player.break && !Pelle.isDoomed,
icon: MultiplierTabIcons.ACHIEVEMENT,
},
timeStudy: {
name: "Time Studies",
multValue: () => DC.D1.timesEffectsOf(
TimeStudy(41),
TimeStudy(51),
TimeStudy(141),
TimeStudy(142),
TimeStudy(143),
),
isActive: () => player.break && !Pelle.isDoomed,
icon: MultiplierTabIcons.TIME_STUDY,
},
dilationUpgrade: {
name: "Dilation Upgrade (Based on DT)",
multValue: () => DilationUpgrade.ipMultDT.effectOrDefault(1),
isActive: () => DilationUpgrade.ipMultDT.canBeApplied,
icon: MultiplierTabIcons.UPGRADE("dilation"),
},
glyph: {
name: "Equipped Glyphs",
multValue: () => Pelle.specialGlyphEffect.infinity.times(getAdjustedGlyphEffect("infinityIP")),
powValue: () => (GlyphAlteration.isAdded("infinity") ? getSecondaryGlyphEffect("infinityIP") : 1),
isActive: () => PlayerProgress.realityUnlocked() && !Pelle.isDoomed,
icon: MultiplierTabIcons.GENERIC_GLYPH,
},
alchemy: {
name: "Glyph Alchemy",
multValue: () => Replicanti.amount.powEffectOf(AlchemyResource.exponential),
isActive: () => Ra.unlocks.unlockGlyphAlchemy.canBeApplied,
icon: MultiplierTabIcons.ALCHEMY,
},
pelle: {
name: "Pelle Rift Effects",
multValue: () => DC.D1.timesEffectsOf(PelleRifts.vacuum).times(Pelle.specialGlyphEffect.infinity),
isActive: () => Pelle.isDoomed,
icon: MultiplierTabIcons.PELLE,
},
iap: {
name: "Shop Tab Purchases",
multValue: () => ShopPurchase.IPPurchases.currentMult,
isActive: () => ShopPurchaseData.totalSTD > 0,
icon: MultiplierTabIcons.IAP,
},
nerfTeresa: {
name: "Teresa's Reality",
powValue: () => 0.55,
isActive: () => Teresa.isRunning,
icon: MultiplierTabIcons.GENERIC_TERESA,
},
nerfV: {
name: "V's Reality",
powValue: () => 0.5,
isActive: () => V.isRunning,
icon: MultiplierTabIcons.GENERIC_V,
},
};

View File

@ -0,0 +1,66 @@
import { DC } from "../../constants";
import { GameDatabase } from "../game-database";
import { PlayerProgress } from "../../app/player-progress";
import { MultiplierTabIcons } from "./icons";
// See index.js for documentation
GameDatabase.multiplierTabValues.TP = {
total: {
name: "Total Tachyon Particles",
displayOverride: () => {
const baseTPStr = format(new Decimal(Currency.tachyonParticles.value), 2, 2);
return PelleRifts.paradox.milestones[1].canBeApplied
? `${baseTPStr}${formatPow(PelleRifts.paradox.milestones[1].effectValue, 1, 1)}`
: baseTPStr;
},
multValue: () => new Decimal(Currency.tachyonParticles.value)
.pow(PelleRifts.paradox.milestones[1].effectOrDefault(1)),
isActive: () => new Decimal(Currency.tachyonParticles.value).gt(0),
icon: MultiplierTabIcons.TACHYON_PARTICLES,
},
base: {
name: "Base Tachyon Particle Count",
isBase: true,
multValue: () => new Decimal(Currency.tachyonParticles.value).div(tachyonGainMultiplier()),
isActive: () => new Decimal(Currency.tachyonParticles.value).gt(0),
icon: MultiplierTabIcons.TACHYON_PARTICLES,
},
achievementMult: {
name: "Achievement Multiplier",
multValue: () => RealityUpgrade(8).effectOrDefault(1),
isActive: () => RealityUpgrade(8).canBeApplied && !Pelle.isDoomed,
icon: MultiplierTabIcons.ACHIEVEMENT,
},
achievement: {
name: "Achievement 132 Reward",
multValue: () => Achievement(132).effectOrDefault(1),
isActive: () => Achievement(132).canBeApplied,
icon: MultiplierTabIcons.ACHIEVEMENT,
},
dilation: {
name: `Dilation Upgrade (Repeatable TP multiplier)`,
multValue: () => DilationUpgrade.tachyonGain.effectOrDefault(1),
isActive: () => DilationUpgrade.tachyonGain.canBeApplied,
icon: MultiplierTabIcons.UPGRADE("dilation"),
},
realityUpgrade: {
name: "Reality Upgrades",
multValue: () => DC.D1.timesEffectsOf(RealityUpgrade(4), RealityUpgrade(15)),
isActive: () => PlayerProgress.realityUnlocked() && !Pelle.isDoomed,
icon: MultiplierTabIcons.UPGRADE("reality"),
},
dilationGlyphSacrifice: {
name: "Dilation Glyph Sacrifice",
multValue: () => GlyphSacrifice.dilation.effectValue,
isActive: () => GlyphSacrifice.dilation.effectValue > 1,
icon: MultiplierTabIcons.SACRIFICE("dilation"),
},
nerfEnslaved: {
name: "The Nameless Ones' Reality",
powValue: () => Enslaved.tachyonNerf,
isActive: () => Enslaved.isRunning,
icon: MultiplierTabIcons.GENERIC_ENSLAVED,
}
};

View File

@ -0,0 +1,81 @@
import { DC } from "../../constants";
import { GameDatabase } from "../game-database";
import { MultiplierTabHelper } from "./helper-functions";
import { MultiplierTabIcons } from "./icons";
// See index.js for documentation
GameDatabase.multiplierTabValues.tickspeed = {
total: {
name: "Total Tickspeed",
displayOverride: () => {
const tickRate = Tickspeed.perSecond;
const activeDims = MultiplierTabHelper.activeDimCount("AD");
return `${format(tickRate, 2, 2)}/sec on ${formatInt(activeDims)} ${pluralize("Dimension", activeDims)}
${formatX(tickRate.pow(activeDims), 2, 2)}`;
},
// This is necessary to make multValue entries from the other props scale properly, which are also all pow10
// due to the multiplier tab splitting up entries logarithmically
fakeValue: DC.E100,
multValue: () => Tickspeed.perSecond.pow(MultiplierTabHelper.activeDimCount("AD")),
// No point in showing this breakdown at all unless both components are nonzero; however they will always be nonzero
// due to the way the calculation works, so we have to manually hide it here
isActive: () => Tickspeed.perSecond.gt(1) && effectiveBaseGalaxies() > 0,
dilationEffect: () => (Effarig.isRunning ? Effarig.tickDilation : 1),
overlay: ["<i class='fa-solid fa-clock' />"],
icon: MultiplierTabIcons.TICKSPEED,
},
base: {
name: "Base Tickspeed from Achievements",
displayOverride: () => {
const val = DC.D1.dividedByEffectsOf(
Achievement(36),
Achievement(45),
Achievement(66),
Achievement(83)
);
return `${format(val, 2, 2)}/sec`;
},
multValue: () => new Decimal.pow10(100 * MultiplierTabHelper.decomposeTickspeed().base),
isActive: () => [36, 45, 66, 83].some(a => Achievement(a).canBeApplied),
icon: MultiplierTabIcons.ACHIEVEMENT,
},
upgrades: {
name: "Tickspeed Upgrades",
displayOverride: () => `${formatInt(Tickspeed.totalUpgrades)} Total`,
multValue: () => new Decimal.pow10(100 * MultiplierTabHelper.decomposeTickspeed().tickspeed),
isActive: true,
icon: MultiplierTabIcons.PURCHASE("AD"),
},
galaxies: {
name: "Galaxies",
displayOverride: () => {
const ag = player.galaxies + GalaxyGenerator.galaxies;
const rg = Replicanti.galaxies.total;
const tg = player.dilation.totalTachyonGalaxies;
return `${formatInt(ag + rg + tg)} Total`;
},
multValue: () => new Decimal.pow10(100 * MultiplierTabHelper.decomposeTickspeed().galaxies),
isActive: true,
icon: MultiplierTabIcons.GALAXY,
},
};
GameDatabase.multiplierTabValues.tickspeedUpgrades = {
purchased: {
name: "Purchased Tickspeed Upgrades",
displayOverride: () => (Laitela.continuumActive
? formatFloat(Tickspeed.continuumValue, 2, 2)
: formatInt(player.totalTickBought)),
multValue: () => Decimal.pow10(Laitela.continuumActive ? Tickspeed.continuumValue : player.totalTickBought),
isActive: () => true,
icon: MultiplierTabIcons.PURCHASE("AD"),
},
free: {
name: "Tickspeed Upgrades from TD",
displayOverride: () => formatInt(player.totalTickGained),
multValue: () => Decimal.pow10(player.totalTickGained),
isActive: () => Currency.timeShards.gt(0),
icon: MultiplierTabIcons.SPECIFIC_GLYPH("time"),
}
};

View File

@ -0,0 +1,265 @@
import { DC } from "../../constants";
import { GameDatabase } from "../game-database";
import { PlayerProgress } from "../../app/player-progress";
import { MultiplierTabHelper } from "./helper-functions";
import { MultiplierTabIcons } from "./icons";
// See index.js for documentation
GameDatabase.multiplierTabValues.TD = {
total: {
name: dim => {
if (dim) return `TD ${dim} Multiplier`;
if (EternityChallenge(7).isRunning) return "ID8 Production";
return "Time Shard Production";
},
displayOverride: dim => (dim
? formatX(TimeDimension(dim).multiplier, 2)
: `${format(TimeDimension(1).productionPerSecond, 2)}/sec`
),
multValue: dim => (dim
? TimeDimension(dim).multiplier
: TimeDimensions.all
.filter(td => td.isProducing)
.map(td => td.multiplier)
.reduce((x, y) => x.times(y), DC.D1)),
isActive: dim => TimeDimension(dim ?? 1).isProducing && !EternityChallenge(11).isRunning,
dilationEffect: () => {
const baseEff = player.dilation.active
? 0.75 * Effects.product(DilationUpgrade.dilationPenalty)
: 1;
return baseEff * (Effarig.isRunning ? Effarig.multDilation : 1);
},
isDilated: true,
overlay: ["Δ", "<i class='fa-solid fa-cube' />"],
icon: dim => MultiplierTabIcons.DIMENSION("TD", dim),
},
purchase: {
name: dim => (dim ? `Purchased TD ${dim}` : "Purchases"),
multValue: dim => {
const getMult = td => {
const d = TimeDimension(td);
const bought = td === 8 ? Math.clampMax(d.bought, 1e8) : d.bought;
return Decimal.pow(d.powerMultiplier, bought);
};
if (dim) return getMult(dim);
return TimeDimensions.all
.filter(td => td.isProducing)
.map(td => getMult(td.tier))
.reduce((x, y) => x.times(y), DC.D1);
},
isActive: () => !EternityChallenge(2).isRunning && !EternityChallenge(10).isRunning,
icon: dim => MultiplierTabIcons.PURCHASE("TD", dim),
},
highestDim: {
name: () => `Amount of highest Dimension`,
displayOverride: () => {
const dim = MultiplierTabHelper.activeDimCount("TD");
return `TD ${dim}, ${formatInt(TimeDimension(dim).amount)}`;
},
multValue: () => TimeDimension(MultiplierTabHelper.activeDimCount("TD")).amount,
isActive: () => TimeDimension(1).isProducing,
icon: MultiplierTabIcons.DIMENSION("TD"),
},
basePurchase: {
name: "Base purchases",
multValue: dim => {
const getMult = td => Decimal.pow(4,
td === 8 ? Math.clampMax(TimeDimension(td).bought, 1e8) : TimeDimension(td).bought);
if (dim) return getMult(dim);
return TimeDimensions.all
.filter(td => td.isProducing)
.map(td => getMult(td.tier))
.reduce((x, y) => x.times(y), DC.D1);
},
isActive: dim => (dim
? ImaginaryUpgrade(14).canBeApplied || (dim === 8 && GlyphSacrifice.time.effectValue > 1)
: TimeDimension(1).isProducing),
icon: dim => MultiplierTabIcons.PURCHASE("TD", dim),
},
timeGlyphSacrifice: {
name: "Time Glyph Sacrifice",
multValue: () => (TimeDimension(8).isProducing
? Decimal.pow(GlyphSacrifice.time.effectValue, Math.clampMax(TimeDimension(8).bought, 1e8))
: DC.D1),
isActive: () => GlyphSacrifice.time.effectValue > 1,
icon: MultiplierTabIcons.SACRIFICE("time"),
},
powPurchase: {
name: "Reflection of Intrusion",
powValue: () => ImaginaryUpgrade(14).effectOrDefault(1),
isActive: () => ImaginaryUpgrade(14).canBeApplied,
icon: MultiplierTabIcons.UPGRADE("imaginary"),
},
achievementMult: {
name: "Achievement Multiplier",
multValue: dim => Decimal.pow(EternityUpgrade.tdMultAchs.effectOrDefault(1),
dim ? 1 : MultiplierTabHelper.activeDimCount("TD")),
isActive: () => EternityUpgrade.tdMultAchs.canBeApplied && !Pelle.isDoomed,
icon: MultiplierTabIcons.ACHIEVEMENT,
},
achievement: {
name: "Achievement Rewards",
multValue: dim => {
const baseMult = DC.D1.timesEffectsOf(Achievement(105), Achievement(128));
return Decimal.pow(baseMult, dim ? 1 : MultiplierTabHelper.activeDimCount("TD"));
},
isActive: () => Achievement(105).canBeApplied || Achievement(128).canBeApplied,
icon: MultiplierTabIcons.ACHIEVEMENT,
},
timeStudy: {
name: dim => (dim ? `Time Studies (TD ${dim})` : "Time Studies"),
multValue: dim => {
const allMult = DC.D1.timesEffectsOf(
TimeStudy(93),
TimeStudy(103),
TimeStudy(151),
TimeStudy(221),
TimeStudy(301),
);
const dimMults = Array.repeat(DC.D1, 9);
for (let tier = 1; tier <= 8; tier++) {
dimMults[tier] = dimMults[tier].timesEffectsOf(
tier === 1 ? TimeStudy(11) : null,
tier === 3 ? TimeStudy(73) : null,
tier === 4 ? TimeStudy(227) : null
);
}
if (dim) return allMult.times(dimMults[dim]);
let totalMult = DC.D1;
for (let tier = 1; tier <= MultiplierTabHelper.activeDimCount("TD"); tier++) {
totalMult = totalMult.times(dimMults[tier]).times(allMult);
}
return totalMult;
},
isActive: () => TimeDimension(1).isProducing,
icon: MultiplierTabIcons.TIME_STUDY
},
eternityUpgrade: {
name: dim => (dim ? `Eternity Upgrades (TD ${dim})` : "Eternity Upgrades"),
multValue: dim => {
const allMult = DC.D1.timesEffectsOf(
EternityUpgrade.tdMultTheorems,
EternityUpgrade.tdMultRealTime,
);
return Decimal.pow(allMult, dim ? 1 : MultiplierTabHelper.activeDimCount("TD"));
},
isActive: () => TimeDimension(1).isProducing,
icon: MultiplierTabIcons.UPGRADE("eternity"),
},
eu1: {
name: () => "Unspent Time Theorems",
multValue: dim => Decimal.pow(EternityUpgrade.tdMultTheorems.effectOrDefault(1),
dim ? 1 : MultiplierTabHelper.activeDimCount("TD")),
isActive: () => EternityUpgrade.tdMultTheorems.canBeApplied,
icon: MultiplierTabIcons.UPGRADE("eternity"),
},
eu2: {
name: () => "Days played",
multValue: dim => Decimal.pow(EternityUpgrade.tdMultRealTime.effectOrDefault(1),
dim ? 1 : MultiplierTabHelper.activeDimCount("TD")),
isActive: () => EternityUpgrade.tdMultRealTime.canBeApplied,
icon: MultiplierTabIcons.UPGRADE("eternity"),
},
eternityChallenge: {
name: dim => (dim ? `Eternity Challenges (TD ${dim})` : "Eternity Challenges"),
multValue: dim => {
let allMult = DC.D1.timesEffectsOf(
EternityChallenge(1).reward,
EternityChallenge(10).reward,
).times(EternityChallenge(7).isRunning ? Tickspeed.perSecond : DC.D1);
if (EternityChallenge(9).isRunning) {
allMult = allMult.times(
Decimal.pow(Math.clampMin(Currency.infinityPower.value.pow(InfinityDimensions.powerConversionRate / 7)
.log2(), 1), 4).clampMin(1));
}
return Decimal.pow(allMult, dim ? 1 : MultiplierTabHelper.activeDimCount("TD"));
},
isActive: () => EternityChallenge(1).completions > 0,
icon: MultiplierTabIcons.CHALLENGE("eternity")
},
tickspeed: {
name: () => "Tickspeed (EC7)",
displayOverride: () => {
const tickRate = Tickspeed.perSecond;
const activeDims = MultiplierTabHelper.activeDimCount("TD");
return `${format(tickRate, 2, 2)}/sec on ${formatInt(activeDims)} ${pluralize("Dimension", activeDims)}
${formatX(tickRate.pow(activeDims), 2, 2)}`;
},
multValue: () => Tickspeed.perSecond.pow(MultiplierTabHelper.activeDimCount("TD")),
isActive: () => EternityChallenge(7).isRunning,
icon: MultiplierTabIcons.TICKSPEED,
},
dilationUpgrade: {
name: "Dilation Upgrade (Based on Replicanti)",
multValue: dim => {
const mult = Replicanti.areUnlocked && Replicanti.amount.gt(1)
? DilationUpgrade.tdMultReplicanti.effectValue
: DC.D1;
return Decimal.pow(mult, dim ? 1 : MultiplierTabHelper.activeDimCount("TD"));
},
isActive: () => DilationUpgrade.tdMultReplicanti.canBeApplied,
icon: MultiplierTabIcons.UPGRADE("dilation"),
},
realityUpgrade: {
name: "Temporal Transcendence",
multValue: dim => Decimal.pow(RealityUpgrade(22).effectOrDefault(1),
dim ? 1 : MultiplierTabHelper.activeDimCount("TD")),
isActive: () => RealityUpgrade(22).canBeApplied,
icon: MultiplierTabIcons.UPGRADE("reality"),
},
glyph: {
name: "Glyph Effects",
powValue: () => getAdjustedGlyphEffect("timepow") * getAdjustedGlyphEffect("effarigdimensions"),
isActive: () => PlayerProgress.realityUnlocked(),
icon: MultiplierTabIcons.GENERIC_GLYPH
},
alchemy: {
name: "Glyph Alchemy",
multValue: dim => Decimal.pow(AlchemyResource.dimensionality.effectOrDefault(1),
dim ? 1 : MultiplierTabHelper.activeDimCount("TD")),
powValue: () => AlchemyResource.time.effectOrDefault(1) * Ra.momentumValue,
isActive: () => Ra.unlocks.unlockGlyphAlchemy.canBeApplied,
icon: MultiplierTabIcons.ALCHEMY,
},
imaginaryUpgrade: {
name: "Suspicion of Interference",
powValue: () => ImaginaryUpgrade(11).effectOrDefault(1),
isActive: () => ImaginaryUpgrade(11).canBeApplied,
icon: MultiplierTabIcons.UPGRADE("imaginary"),
},
pelle: {
name: "Pelle Rift Effects",
multValue: dim => Decimal.pow(PelleRifts.chaos.effectOrDefault(1),
dim ? 1 : MultiplierTabHelper.activeDimCount("TD")),
powValue: () => PelleRifts.paradox.effectOrDefault(DC.D1).toNumber(),
isActive: () => Pelle.isDoomed,
icon: MultiplierTabIcons.PELLE,
},
iap: {
name: "Shop Tab Purchases",
multValue: dim => Decimal.pow(ShopPurchase.allDimPurchases.currentMult,
dim ? 1 : MultiplierTabHelper.activeDimCount("TD")),
isActive: () => ShopPurchaseData.totalSTD > 0,
icon: MultiplierTabIcons.IAP,
},
nerfV: {
name: "V's Reality",
powValue: () => 0.5,
isActive: () => V.isRunning,
icon: MultiplierTabIcons.GENERIC_V,
},
nerfCursed: {
name: "Cursed Glyphs",
powValue: () => getAdjustedGlyphEffect("curseddimensions"),
isActive: () => getAdjustedGlyphEffect("curseddimensions") !== 1,
icon: MultiplierTabIcons.SPECIFIC_GLYPH("cursed"),
},
};

View File

@ -0,0 +1,232 @@
/* eslint-disable max-depth */
/* eslint-disable camelcase */
import { GameDatabase } from "../game-database";
import { MultiplierTabHelper } from "./helper-functions";
const dynamicGenProps = ["TP", "DT", "infinities", "eternities", "gamespeed"];
const propList = {
AD: ["purchase", "dimboost", "sacrifice", "achievementMult", "achievement", "infinityUpgrade",
"breakInfinityUpgrade", "infinityPower", "infinityChallenge", "timeStudy", "eternityChallenge", "glyph", "v",
"alchemy", "pelle", "iap", "effectNC", "nerfIC", "nerfV", "nerfCursed", "nerfPelle"],
ID: ["purchase", "achievementMult", "achievement", "replicanti", "infinityChallenge", "timeStudy", "eternityUpgrade",
"eternityChallenge", "glyph", "alchemy", "imaginaryUpgrade", "pelle", "iap", "nerfV", "nerfCursed", "nerfPelle"],
TD: ["purchase", "achievementMult", "achievement", "timeStudy", "eternityUpgrade", "eternityChallenge",
"dilationUpgrade", "realityUpgrade", "glyph", "alchemy", "imaginaryUpgrade", "pelle", "iap", "nerfV", "nerfCursed"],
IP: ["base", "infinityUpgrade", "achievement", "timeStudy", "dilationUpgrade", "glyph", "alchemy", "pelle", "iap",
"nerfTeresa", "nerfV"],
EP: ["base", "eternityUpgrade", "timeStudy", "glyph", "realityUpgrade", "pelle", "iap", "nerfTeresa", "nerfV"],
};
// Some of the props above would contain every entry except "total" in their respective value GameDB entry, so we
// generate them dynamically instead
for (const prop of dynamicGenProps) {
propList[prop] = [];
for (const toCopy of Object.keys(GameDatabase.multiplierTabValues[prop])) {
if (toCopy !== "total") propList[prop].push(toCopy);
}
}
// Used for individual dimension breakdowns of effects (eg. full achievement mult into its values on individual ADs)
// Results in an array of ["key_1", "key_2", ... , "key_8"]
function append8(key) {
const props = [];
for (let dim = 1; dim <= 8; dim++) props.push(`${key}_${dim}`);
return props;
}
// Helper method to create very long lists of entries in the tree; format is "RESOURCE_SOURCE_DIMENSION"
function getProps(resource, tier) {
const props = propList[resource].map(s => `${resource}_${s}`);
if (!tier) return props;
const newProps = [];
for (const effect of props) newProps.push(`${effect}_${tier}`);
return newProps;
}
// Everything is multiplierTabTree is associated with values in GameDatabase.multiplierTabValues. The only explicitly
// initialized props here are the "root" props which are viewable on the tab with full breakdowns. After the initial
// specification, all children props are dynamically added based on the arrays in the helper functions above
GameDatabase.multiplierTabTree = {
AM_total: [
["AD_total", "tickspeed_total", "AM_effarigAM"]
],
AD_total: [
append8("AD_total"),
getProps("AD")
],
ID_total: [
append8("ID_total"),
getProps("ID")
],
TD_total: [
append8("TD_total"),
getProps("TD")
],
IP_total: [
getProps("IP")
],
IP_base: [
["IP_antimatter", "IP_divisor"]
],
EP_total: [
getProps("EP")
],
EP_base: [
["EP_IP", "EP_divisor"]
],
TP_total: [
getProps("TP")
],
DT_total: [
getProps("DT")
],
tickspeed_total: [
["tickspeed_base", "tickspeed_upgrades", "tickspeed_galaxies"]
],
tickspeed_upgrades: [
["tickspeedUpgrades_purchased", "tickspeedUpgrades_free"]
],
tickspeed_galaxies: [
["galaxies_antimatter", "galaxies_replicanti", "galaxies_tachyon"]
],
infinities_total: [
getProps("infinities")
],
eternities_total: [
getProps("eternities")
],
gamespeed_total: [
getProps("gamespeed")
],
};
// Gamespeed's two alternate displays are current and average gamespeed, distinguished by which of two
// mutually-exclusive entries appear in the list. We explicity modify props here as needed
const allGamespeed = GameDatabase.multiplierTabTree.gamespeed_total[0];
GameDatabase.multiplierTabTree.gamespeed_total[0] = [...allGamespeed].filter(key => key !== "gamespeed_blackHoleAvg");
GameDatabase.multiplierTabTree.gamespeed_total[1] = [...allGamespeed].filter(key => key !== "gamespeed_blackHoleCurr");
// DT doesn't explicitly have an entry to TP, due to it being its own total entry, so we link them together
GameDatabase.multiplierTabTree.DT_total[0].unshift("TP_total");
// Additional data specification for dynamically-generated props
const dimTypes = ["AD", "ID", "TD"];
const singleRes = ["IP", "EP", "DT"];
const targetedEffects = {
achievement: {
checkFn: MultiplierTabHelper.achievementDimCheck,
AD: [23, 28, 31, 34, 43, 48, 56, 64, 65, 68, 71, 72, 73, 74, 76, 84, 91, 92],
TD: [105, 128],
IP: [85, 93, 116, 125, 141],
DT: [132, 137]
},
timeStudy: {
checkFn: MultiplierTabHelper.timeStudyDimCheck,
AD: [71, 91, 101, 161, 193, 214, 234],
ID: [72, 82, 92, 102, 162],
TD: [11, 73, 93, 103, 151, 221, 227, 301],
IP: [41, 51, 141, 142, 143],
EP: [61, 121, 122, 123],
},
infinityChallenge: {
checkFn: MultiplierTabHelper.ICDimCheck,
AD: [3, 4, 8],
ID: [1, 6],
},
eternityChallenge: {
checkFn: MultiplierTabHelper.ECDimCheck,
ID: [2, 4, 7, 9],
TD: [1, 10],
},
};
// Highest actively-producing dimensions need a special case
for (const dim of dimTypes) {
GameDatabase.multiplierTabTree[`${dim}_total`][0].push(`${dim}_highestDim`);
GameDatabase.multiplierTabTree[`${dim}_total`][1].push(`${dim}_highestDim`);
}
// EC7 also needs a special case for tickspeed, since it doesn't appear on the multipliers themselves
for (const dim of ["ID", "TD"]) {
GameDatabase.multiplierTabTree[`${dim}_total`][0].push(`${dim}_tickspeed`);
GameDatabase.multiplierTabTree[`${dim}_total`][1].push(`${dim}_tickspeed`);
}
// Dynamically generate all values from existing values, but broken down by dimension
for (const res of dimTypes) {
for (const prop of getProps(res)) GameDatabase.multiplierTabTree[prop] = [append8(prop)];
for (let dim = 1; dim <= 8; dim++) GameDatabase.multiplierTabTree[`${res}_total_${dim}`] = [getProps(res, dim)];
}
// A few dynamically-generated props are largely useless in terms of what they connect to, in that they have very few
// entries or have 8 identical entries, so we explicitly remove those lists for a cleaner appearance on the UI
const removedRegexes = ["AD_sacrifice", "AD_breakInfinityUpgrade", "AD_nerfIC", "AD_infinityUpgrade", "AD_v",
"ID_replicanti", "ID_infinityChallenge", "ID_eternityUpgrades",
"TD_achievement", "TD_eternityUpgrade", "TD_dilationUpgrade", "TD_realityUpgrade",
".._achievementMult", ".._glyph", ".._alchemy", ".._imaginaryUpgrade", ".._iap",
".._nerfV", ".._nerfCursed", ".._nerfPelle", ".._pelle"
];
const removedProps = Object.keys(GameDatabase.multiplierTabTree)
.filter(key => removedRegexes.some(regex => key.match(regex)));
for (const prop of removedProps) {
GameDatabase.multiplierTabTree[prop] = undefined;
}
// We need to handle infinity power multiplier a bit differently; previous steps of dynamic generation fill it with
// 8 identical AD multipliers, but we want to replace it with ID mults and the conversion rate
GameDatabase.multiplierTabTree.AD_infinityPower = [["ID_total", "ID_powerConversion"]];
for (let dim = 1; dim <= 8; dim++) {
GameDatabase.multiplierTabTree[`AD_infinityPower_${dim}`] = [["ID_total", "ID_powerConversion"]];
}
// Tesseracts are added one layer deep, but we don't want to override the existing ID_purchase entry
GameDatabase.multiplierTabTree.ID_purchase.push(["ID_basePurchase", "ID_tesseractPurchase", "ID_infinityGlyphSacrifice",
"ID_powPurchase"]);
for (let dim = 1; dim <= 7; dim++) {
GameDatabase.multiplierTabTree[`ID_purchase_${dim}`] = [[`ID_basePurchase_${dim}`, `ID_tesseractPurchase_${dim}`,
"ID_powPurchase"]];
}
GameDatabase.multiplierTabTree.ID_purchase_8 = [[`ID_basePurchase_8`, `ID_infinityGlyphSacrifice`, "ID_powPurchase"]];
// These are also added one layer deep
GameDatabase.multiplierTabTree.TD_purchase.push(["TD_basePurchase", "TD_timeGlyphSacrifice", "TD_powPurchase"]);
GameDatabase.multiplierTabTree.TD_purchase_8 = [["TD_basePurchase_8", "TD_timeGlyphSacrifice", "TD_powPurchase"]];
// Dynamically fill effects which only affect certain dimensions, as noted in targetedEffects
for (const res of dimTypes) {
for (const eff of Object.keys(targetedEffects)) {
if (!targetedEffects[eff][res]) continue;
GameDatabase.multiplierTabTree[`${res}_${eff}`] = [[]];
for (const id of targetedEffects[eff][res]) {
for (let dim = 1; dim <= 8; dim++) {
const propStr = `${res}_${eff}_${dim}`;
const dimStr = `${res}${dim}`;
if (targetedEffects[eff].checkFn(id, dimStr)) {
if (!GameDatabase.multiplierTabTree[propStr]) GameDatabase.multiplierTabTree[propStr] = [[]];
GameDatabase.multiplierTabTree[propStr][0].push(`general_${eff}_${id}_${dimStr}`);
}
}
GameDatabase.multiplierTabTree[`${res}_${eff}`][0].push(`general_${eff}_${id}_${res}`);
}
}
}
// Dynamically fill effects which affect single resources as well
for (const res of singleRes) {
for (const eff of Object.keys(targetedEffects)) {
if (!targetedEffects[eff][res]) continue;
GameDatabase.multiplierTabTree[`${res}_${eff}`] = [[]];
for (const ach of targetedEffects[eff][res]) {
GameDatabase.multiplierTabTree[`${res}_${eff}`][0].push(`general_${eff}_${ach}`);
}
}
}
// Fill in eternity upgrade entries
GameDatabase.multiplierTabTree.ID_eternityUpgrade = [[`ID_eu1`, `ID_eu2`, `ID_eu3`]];
GameDatabase.multiplierTabTree.TD_eternityUpgrade = [[`TD_eu1`, `TD_eu2`]];
for (let dim = 1; dim <= 8; dim++) {
GameDatabase.multiplierTabTree[`ID_eternityUpgrade_${dim}`] = [[`ID_eu1_${dim}`, `ID_eu2_${dim}`, `ID_eu3_${dim}`]];
GameDatabase.multiplierTabTree[`TD_eternityUpgrade_${dim}`] = [[`TD_eu1_${dim}`, `TD_eu2_${dim}`]];
}

View File

@ -296,8 +296,8 @@ GameDatabase.reality.imaginaryUpgrades = [
at least ${formatInt(4)} empty Glyph slots`,
hasFailed: () => !Laitela.isRunning || Laitela.maxAllowedDimension !== 0 || Glyphs.activeList.length > 1,
checkRequirement: () => Laitela.isRunning && Laitela.maxAllowedDimension === 0 &&
Glyphs.activeList.length <= 1,
checkEvent: GAME_EVENT.REALITY_RESET_BEFORE,
Glyphs.activeList.length <= 1 && TimeStudy.reality.isBought,
checkEvent: GAME_EVENT.GAME_TICK_AFTER,
description: "Unlock Pelle, Celestial of Antimatter",
},
];

View File

@ -274,12 +274,12 @@ GameDatabase.reality.upgrades = [
name: "Parity of Singularity",
id: 20,
cost: 1500,
requirement: () => `${formatInt(1)} year total play time and the Black Hole unlocked
(Currently: ${Time.totalTimePlayed.toStringShort(false)})`,
requirement: () => `${formatInt(100)} days total play time after unlocking the Black Hole
(Currently: ${Time.timeSinceBlackHole.toStringShort(false)})`,
hasFailed: () => !BlackHole(1).isUnlocked && Currency.realityMachines.lt(100),
checkRequirement: () => Time.totalTimePlayed.totalYears >= 1 && BlackHole(1).isUnlocked,
checkRequirement: () => Time.timeSinceBlackHole.totalDays >= 100 && BlackHole(1).isUnlocked,
checkEvent: GAME_EVENT.GAME_TICK_AFTER,
description: "Unlock Black Hole 2",
description: "Unlock another Black Hole",
automatorPoints: 10,
shortDescription: () => `Second Black Hole`,
formatCost: value => format(value, 1, 0)

View File

@ -1,5 +1,6 @@
import { GameDatabase } from "./game-database";
// NOTE: IF ANY COSTS ARE CHANGED HERE, THEY ALSO NEED TO BE CHANGED ON THE BACKEND TOO
GameDatabase.shopPurchases = {
dimPurchases: {
key: "dimPurchases",
@ -52,7 +53,7 @@ GameDatabase.shopPurchases = {
description: "Get 6 hours worth of offline production. (Autobuyers don't work at full speed)",
singleUse: true,
onPurchase: () => {
kong.purchaseTimeSkip();
shop.purchaseTimeSkip();
}
},
bigTimeSkip: {
@ -61,7 +62,7 @@ GameDatabase.shopPurchases = {
description: "Get 24 hours worth of offline production. (Autobuyers don't work at full speed)",
singleUse: true,
onPurchase: () => {
kong.purchaseLongerTimeSkip();
shop.purchaseLongerTimeSkip();
}
},
};

View File

@ -171,6 +171,39 @@ GameDatabase.tabNotifications = {
tab: "autobuyers"
},
],
// Always externally triggered, but needs to be ignored in cel7 because they're unlocked differently
condition: () => !Pelle.isDoomed,
},
imaginaryMachineUnlock: {
id: 13,
tabsToHighLight: [
{
parent: "reality",
tab: "imag_upgrades"
}
],
condition: () => MachineHandler.isIMUnlocked,
events: [GAME_EVENT.GAME_TICK_AFTER]
},
laitelaUnlock: {
id: 14,
tabsToHighLight: [
{
parent: "celestials",
tab: "laitela"
},
],
// Always externally triggered
condition: () => true,
},
pelleUnlock: {
id: 15,
tabsToHighLight: [
{
parent: "celestials",
tab: "pelle"
},
],
// Always externally triggered
condition: () => true,
},

View File

@ -110,13 +110,22 @@ GameDatabase.tabs = [
id: 2,
hidable: true,
},
{
key: "multipliers",
name: "Multiplier Breakdown",
symbol: "<i class='fas fa-calculator'></i>",
component: "MultiplierBreakdownTab",
condition: () => PlayerProgress.infinityUnlocked(),
id: 3,
hidable: true,
},
{
key: "glyph sets",
name: "Glyph Set Records",
symbol: "<i class='fas fa-ellipsis-h'></i>",
component: "GlyphSetRecordsTab",
condition: () => PlayerProgress.realityUnlocked(),
id: 3,
id: 4,
hidable: true,
},
{
@ -125,7 +134,7 @@ GameDatabase.tabs = [
symbol: "<i class='fas fa-flag-checkered'></i>",
component: "SpeedrunMilestonesTab",
condition: () => player.speedrun.isActive,
id: 4,
id: 5,
hidable: true,
},
]
@ -206,7 +215,7 @@ GameDatabase.tabs = [
name: "Infinity Challenges",
symbol: "∞",
component: "infinity-challenges-tab",
condition: () => PlayerProgress.hasBroken() || Pelle.isDoomed,
condition: () => PlayerProgress.realityUnlocked() || PlayerProgress.hasBroken() || Pelle.isDoomed,
id: 1,
hidable: true
},
@ -467,7 +476,6 @@ GameDatabase.tabs = [
name: "Shop",
newUIClass: "shop",
hideAt: 1.5,
condition: () => 1===1 /*kong.enabled || player.IAP.totalSTD > 0, ||*/,
id: 10,
hidable: true,
subtabs: [

225
javascripts/core/shop.js Normal file
View File

@ -0,0 +1,225 @@
import { RebuyableMechanicState } from "./game-mechanics/index";
import Payments from "./payments";
export const shop = {};
shop.kongEnabled = false;
shop.init = function() {
if (document.referrer.indexOf("kongregate") === -1)
return;
shop.kongEnabled = true;
try {
kongregateAPI.loadAPI(() => {
window.kongregate = kongregateAPI.getAPI();
});
// eslint-disable-next-line no-console
} catch (err) { console.log("Couldn't load Kongregate API"); }
};
export const ShopPurchaseData = {
totalSTD: 0,
spentSTD: 0,
respecAvailable: false,
get availableSTD() {
return this.totalSTD - this.spentSTD;
},
get isIAPEnabled() {
return Cloud.loggedIn && this.availableSTD >= 0 && player.IAP.enabled;
},
updateLocalSTD(newData) {
this.totalSTD = newData.totalSTD;
this.spentSTD = newData.spentSTD;
this.respecAvailable = newData.respecAvailable;
for (const key of Object.keys(GameDatabase.shopPurchases)) this[key] = newData[key] ?? 0;
GameStorage.save();
},
// Reads STD props from the cloud and sets local cached values with the result
async syncSTD(showNotification = true, fetchedData = undefined) {
if (!Cloud.loggedIn) return;
let newSTDData;
if (fetchedData) {
newSTDData = fetchedData;
} else {
try {
const statusRes = await fetch(`${STD_BACKEND_URL}/STDData?user=${Cloud.user.id}`);
newSTDData = await statusRes.json();
} catch (e) {
GameUI.notify.error("Could not sync STD purchases!", 10000);
return;
}
}
if (showNotification) GameUI.notify.info("STD purchases successfully loaded!", 10000);
this.updateLocalSTD(newSTDData);
},
respecRequest() {
if (!Cloud.loggedIn) {
Modal.message.show(`You are not logged in to Google, anything on this tab cannot be used unless you log in.`);
} else if (player.options.confirmations.respecIAP) {
Modal.respecIAP.show();
} else {
this.respecAll();
}
},
async respecAll() {
if (!this.respecAvailable) {
Modal.message.show(`You do not have a respec available. Making an STD purchase allows you to respec your upgrades
once. You can only have at most one of these respecs, and they do not refund offline production purchases.`);
return;
}
let res;
try {
res = await fetch(`${STD_BACKEND_URL}/respec`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ user: Cloud.user.id })
});
} catch (e) {
GameUI.notify.error("Unable to respec STD purchases!", 10000);
return;
}
const stdData = await res.json();
if (stdData.success) GameUI.notify.info("STD respec successful!", 10000);
else GameUI.notify.error("No purchases to respec!", 10000);
this.updateLocalSTD(stdData.data);
},
};
// We track the local state of shop purchases here, so dynamically add all the keys which exist in the gameDB
for (const key of Object.keys(GameDatabase.shopPurchases)) ShopPurchaseData[key] = 0;
class ShopPurchaseState extends RebuyableMechanicState {
get currency() {
return ShopPurchaseData.availableSTD;
}
get isAffordable() {
return this.currency >= this.cost;
}
get description() {
return this.config.description;
}
get cost() {
return this.config.cost;
}
get purchases() {
return ShopPurchaseData[this.config.key];
}
set purchases(value) {
ShopPurchaseData[this.config.key] = value;
}
get shouldDisplayMult() {
return Boolean(this.config.multiplier);
}
get currentMult() {
if (!this.shouldDisplayMult) return "";
return this.config.multiplier(ShopPurchaseData.isIAPEnabled ? this.purchases : 0);
}
get nextMult() {
if (!this.shouldDisplayMult) return "";
return this.config.multiplier(ShopPurchaseData.isIAPEnabled ? this.purchases + 1 : 0);
}
// We want to still display the correct value in the button, so we need separate getters for it
get currentMultForDisplay() {
if (!this.shouldDisplayMult) return "";
return this.config.multiplier(this.purchases);
}
get nextMultForDisplay() {
if (!this.shouldDisplayMult) return "";
return this.config.multiplier(this.purchases + 1);
}
formatEffect(effect) {
return this.config.formatEffect?.(effect) || formatX(effect, 2, 0);
}
async purchase() {
if (!this.canBeBought) return false;
if (GameEnd.creditsEverClosed) return false;
if (this.config.singleUse && ui.$viewModel.modal.progressBar) return false;
// Contact the firebase server to verify the purchase
const success = await Payments.buyUpgrade(this.config.key);
if (!success) return false;
if (player.IAP.enabled) Speedrun.setSTDUse(true);
if (this.config.singleUse) this.config.onPurchase();
GameUI.update();
return true;
}
}
export const ShopPurchase = mapGameDataToObject(
GameDatabase.shopPurchases,
config => new ShopPurchaseState(config)
);
shop.purchaseTimeSkip = function() {
Speedrun.setSTDUse(true);
simulateTime(3600 * 6);
};
shop.purchaseLongerTimeSkip = function() {
Speedrun.setSTDUse(true);
simulateTime(3600 * 24);
};
shop.migratePurchases = function() {
if (!shop.kongEnabled) return;
try {
kongregate.mtx.requestUserItemList("", items);
// eslint-disable-next-line no-console
} catch (e) { console.log(e); }
function items(result) {
let ipPurchases = 0;
let dimPurchases = 0;
let epPurchases = 0;
let alldimPurchases = 0;
for (const item of result.data) {
if (item.identifier === "doublemult") {
player.IAP.totalSTD += 30;
player.IAP.spentSTD += 30;
dimPurchases++;
}
if (item.identifier === "doubleip") {
player.IAP.totalSTD += 40;
player.IAP.spentSTD += 40;
ipPurchases++;
}
if (item.identifier === "tripleep") {
player.IAP.totalSTD += 50;
player.IAP.spentSTD += 50;
epPurchases++;
}
if (item.identifier === "alldimboost") {
player.IAP.totalSTD += 60;
player.IAP.spentSTD += 60;
alldimPurchases++;
}
}
player.IAP.dimPurchases = dimPurchases;
player.IAP.allDimPurchases = alldimPurchases;
player.IAP.IPPurchases = ipPurchases;
player.IAP.EPPurchases = epPurchases;
}
};

View File

@ -49,6 +49,14 @@ export const Speedrun = {
player.speedrun.hasStarted = true;
player.speedrun.startDate = Date.now();
player.lastUpdate = Date.now();
// This needs to be calculated "live" because using spentSTD includes any offline progress purchases too
let currentSpent = 0;
for (const purchase of ShopPurchase.all) {
if (purchase.config.singleUse) continue;
currentSpent += purchase.purchases * purchase.cost;
}
this.setSTDUse(ShopPurchaseData.isIAPEnabled && currentSpent > 0);
},
isPausedAtStart() {
return player.speedrun.isActive && !player.speedrun.hasStarted;
@ -59,7 +67,11 @@ export const Speedrun = {
setSegmented(state) {
if (this.isPausedAtStart()) return;
player.speedrun.isSegmented = state;
}
},
setSTDUse(state) {
if (this.isPausedAtStart() || ShopPurchaseData.spentSTD === 0) return;
player.speedrun.usedSTD = state;
},
};
class SpeedrunMilestone extends GameMechanicState {

View File

@ -29,13 +29,9 @@ export const Cloud = {
auth: getAuth(),
db: getDatabase(),
user: null,
hasSeenSavingConflict: false,
shouldOverwriteCloudSave: true,
lastCloudHash: null,
resetTempState() {
this.hasSeenSavingConflict = false;
this.shouldOverwriteCloudSave = true;
this.lastCloudHash = null;
GameStorage.lastCloudSave = Date.now();
GameIntervals.checkCloudSave.restart();
@ -46,7 +42,13 @@ export const Cloud = {
},
async login() {
await signInWithPopup(this.auth, this.provider);
try {
await signInWithPopup(this.auth, this.provider);
ShopPurchaseData.syncSTD();
GameUI.notify.success(`Logged in as ${this.user.displayName}`);
} catch (e) {
GameUI.notify.error("Google Account login failed");
}
},
@ -84,13 +86,12 @@ export const Cloud = {
return {
farther: ProgressChecker.compareSaveProgress(cloud, local),
older: ProgressChecker.compareSaveTimes(cloud, local),
diffSTD: (cloud?.IAP?.totalSTD ?? 0) - (local?.IAP?.totalSTD ?? 0),
differentName: cloud?.options.saveFileName !== local?.options.saveFileName,
hashMismatch: this.lastCloudHash && this.lastCloudHash !== hash,
};
},
async saveCheck() {
async saveCheck(forceModal = false) {
const save = await this.load();
if (save === null) {
this.save();
@ -99,6 +100,7 @@ export const Cloud = {
const saveId = GameStorage.currentSlot;
const cloudSave = root.saves[saveId];
const thisCloudHash = sha512_256(GameSaveSerializer.serialize(cloudSave));
if (!this.lastCloudHash) this.lastCloudHash = thisCloudHash;
const localSave = GameStorage.saves[saveId];
const saveComparison = this.compareSaves(cloudSave, localSave, thisCloudHash);
@ -112,11 +114,11 @@ export const Cloud = {
const hasBoth = cloudSave && localSave;
// NOTE THIS CHECK IS INTENTIONALLY DIFFERENT FROM THE LOAD CHECK
const hasConflict = hasBoth && (saveComparison.older === -1 || saveComparison.farther !== 1 ||
saveComparison.diffSTD > 0 || saveComparison.differentName || saveComparison.hashMismatch);
if (hasConflict && !this.hasSeenSavingConflict) {
saveComparison.differentName || saveComparison.hashMismatch);
if (forceModal || (hasConflict && player.options.showCloudModal)) {
Modal.addCloudConflict(saveId, saveComparison, cloudSave, localSave, overwriteAndSendCloudSave);
Modal.cloudSaveConflict.show();
} else if (!hasConflict || (this.hasSeenSavingConflict && this.shouldOverwriteCloudSave)) {
} else if (!hasConflict || player.options.forceCloudOverwrite) {
overwriteAndSendCloudSave();
}
}
@ -158,7 +160,7 @@ export const Cloud = {
// Bring up the modal if cloud loading will overwrite a local save which is older or possibly farther
const hasBoth = cloudSave && localSave;
const hasConflict = hasBoth && (saveComparison.older === 1 || saveComparison.farther !== -1 ||
saveComparison.diffSTD < 0 || saveComparison.differentName);
saveComparison.differentName);
if (hasConflict) {
Modal.addCloudConflict(saveId, saveComparison, cloudSave, localSave, overwriteLocalSave);
Modal.cloudLoadConflict.show();
@ -187,6 +189,7 @@ export const Cloud = {
displayName: user.displayName,
email: user.email,
};
ShopPurchaseData.syncSTD();
} else {
this.user = null;
}

View File

@ -246,6 +246,19 @@ GameStorage.devMigrations = {
// These are unused now
delete player.celestials.effarig.typePriorityOrder;
delete player.celestials.teresa.typePriorityOrder;
// This property didn't even exist at the time of this change
movePropIfPossible("teresa", "effarig", "glyphScoreSettings", {
mode: AUTO_GLYPH_SCORE.LOWEST_SACRIFICE,
simpleEffectCount: 0,
types: GlyphTypes.list.mapToObject(t => t.id, t => ({
rarityThreshold: 0,
scoreThreshold: 0,
effectCount: 0,
effectChoices: t.effects.mapToObject(e => e.id, () => false),
effectScores: t.effects.mapToObject(e => e.id, () => 0),
})),
});
movePropIfPossible("effarig", "teresa", "bestAMSet", []);
},
player => {
player.blackHole = player.wormhole;
@ -453,15 +466,16 @@ GameStorage.devMigrations = {
GameStorage.migrations.removeDimensionCosts,
GameStorage.migrations.renameTickspeedPurchaseBumps,
player => {
player.celestials.teresa.unlockBits = arrayToBits(player.celestials.teresa.unlocks);
const safeArrayToBits = x => ((x === undefined) ? 0 : arrayToBits(x));
player.celestials.teresa.unlockBits = safeArrayToBits(player.celestials.teresa.unlocks);
delete player.celestials.teresa.unlocks;
player.celestials.effarig.unlockBits = arrayToBits(player.celestials.effarig.unlocks);
player.celestials.effarig.unlockBits = safeArrayToBits(player.celestials.effarig.unlocks);
delete player.celestials.effarig.unlocks;
player.celestials.v.unlockBits = arrayToBits(player.celestials.v.unlocks);
player.celestials.v.unlockBits = safeArrayToBits(player.celestials.v.unlocks);
delete player.celestials.v.unlocks;
player.celestials.ra.unlockBits = arrayToBits(player.celestials.ra.unlocks);
player.celestials.ra.unlockBits = safeArrayToBits(player.celestials.ra.unlocks);
delete player.celestials.ra.unlocks;
player.celestials.laitela.unlockBits = arrayToBits(player.celestials.laitela.unlocks);
player.celestials.laitela.unlockBits = safeArrayToBits(player.celestials.laitela.unlocks);
delete player.celestials.laitela.unlocks;
},
player => {
@ -713,21 +727,25 @@ GameStorage.devMigrations = {
}
},
player => {
for (let i = 0; i < player.dimensions.normal.length; i++) {
const dimension = player.dimensions.normal[i];
player.dimensions.antimatter[i].bought = dimension.bought;
player.dimensions.antimatter[i].costBumps = dimension.costBumps;
player.dimensions.antimatter[i].amount = new Decimal(dimension.amount);
if (player.dimensions.normal !== undefined) {
for (let i = 0; i < player.dimensions.normal.length; i++) {
const dimension = player.dimensions.normal[i];
player.dimensions.antimatter[i].bought = dimension.bought;
player.dimensions.antimatter[i].costBumps = dimension.costBumps;
player.dimensions.antimatter[i].amount = new Decimal(dimension.amount);
}
delete player.dimensions.normal;
}
delete player.dimensions.normal;
},
player => {
player.options.news = {
enabled: player.options.news,
repeatBuffer: 40,
AIChance: 0,
speed: 1
};
if (player.options.news.enabled === undefined) {
player.options.news = {
enabled: player.options.news,
repeatBuffer: 40,
AIChance: 0,
speed: 1
};
}
},
player => {
delete player.options.confirmations.glyphTrash;
@ -901,25 +919,42 @@ GameStorage.devMigrations = {
delete player.dilation.freeGalaxies;
},
player => {
player.auto.infinityDims = Array.range(0, 8).map(() => ({ lastTick: 0 }));
for (let i = 0; i < 8; i++) {
player.auto.infinityDims[i].isActive = player.infDimBuyers[i];
}
player.auto.timeDims = Array.range(0, 8).map(() => ({ lastTick: 0 }));
for (let i = 0; i < 8; i++) {
player.auto.timeDims[i].isActive = player.reality.tdbuyers[i];
}
player.auto.replicantiUpgrades = Array.range(0, 3).map(() => ({ lastTick: 0 }));
for (let i = 0; i < 3; i++) {
player.auto.replicantiUpgrades[i].isActive = player.replicanti.auto[i];
}
if (player.dilation.auto === undefined) {
// Not defined on old saves, we define it only to delete it later in this migration
player.dilation.auto = [true, true, true];
}
player.auto.dilationUpgrades = Array.range(0, 3).map(() => ({ lastTick: 0 }));
for (let i = 0; i < 3; i++) {
player.auto.dilationUpgrades[i].isActive = player.dilation.auto[i];
}
player.auto.blackHolePower = Array.range(0, 2).map(() => ({ lastTick: 0 }));
for (let i = 0; i < 2; i++) {
player.auto.blackHolePower[i].isActive = player.blackHole[i].autoPower;
}
if (player.reality.rebuyablesAuto === undefined) {
// Not defined on old saves, we define it only to delete it later in this migration
player.reality.rebuyablesAuto = [true, true, true, true, true];
}
player.auto.realityUpgrades = Array.range(0, 5).map(() => ({ lastTick: 0 }));
for (let i = 0; i < 5; i++) {
player.auto.realityUpgrades[i].isActive = player.reality.rebuyablesAuto[i];
}
player.auto.antimatterDims = player.auto.dimensions;
// Note: player.autobuyers, the old way of storing autobuyers, seems to have gotten lost in dev migrations
if (player.auto.antimatterDims === undefined) {
player.auto.antimatterDims = player.auto.dimensions;
}
player.auto.replicantiGalaxies.isActive = player.replicanti.galaxybuyer;
player.auto.ipMultBuyer.isActive = player.infMultBuyer;
player.auto.epMultBuyer.isActive = player.reality.epmultbuyer;
@ -1087,12 +1122,14 @@ GameStorage.devMigrations = {
delete player.celestials.v.maxGlyphsThisRun;
// Refactor news storage format to bitmask array
const oldNewsArray = player.news;
delete player.news;
player.news = {};
player.news.seen = {};
for (const id of oldNewsArray) NewsHandler.addSeenNews(id);
player.news.totalSeen = NewsHandler.uniqueTickersSeen;
if (Array.isArray(player.news)) {
const oldNewsArray = player.news;
delete player.news;
player.news = {};
player.news.seen = {};
for (const id of oldNewsArray) NewsHandler.addSeenNews(id);
player.news.totalSeen = NewsHandler.uniqueTickersSeen;
}
// Separate news-specific data
player.news.specialTickerData = {
@ -1171,8 +1208,11 @@ GameStorage.devMigrations = {
script.content = script.content.replaceAll(triadRegex, "30$1");
}
player.timestudy.studies = player.timestudy.studies.concat(player.celestials.v.triadStudies.map(id => id + 300));
delete player.celestials.v.triadStudies;
if (player.celestials.v.triadStudies !== undefined) {
player.timestudy.studies = player.timestudy.studies.concat(
player.celestials.v.triadStudies.map(id => id + 300));
delete player.celestials.v.triadStudies;
}
},
player => {
delete player.options.confirmations.harshAutoClean;
@ -1391,6 +1431,10 @@ GameStorage.devMigrations = {
const toMove = ["antimatterDims", "infinityDims", "timeDims", "replicantiUpgrades", "dilationUpgrades",
"blackHolePower", "realityUpgrades", "imaginaryUpgrades"];
for (const x of toMove) {
if (player.auto[x].all !== undefined) {
// Already up to date
continue;
}
const all = player.auto[x];
delete player.auto[x];
player.auto[x] = { all, isActive: true };
@ -1459,6 +1503,24 @@ GameStorage.devMigrations = {
player.achievementBits[10] &= ~4;
}
},
player => {
if (player.options.newUI) {
player.options.themeModern = player.options.theme ?? player.options.themeModern;
} else {
player.options.themeClassic = player.options.theme ?? player.options.themeClassic;
}
delete player.options.theme;
if (BlackHole(1).isUnlocked) player.records.timePlayedAtBHUnlock = player.records.totalTimePlayed;
},
player => {
player.IAP.enabled = !player.IAP.disabled;
const toDelete = ["totalSTD", "spentSTD", "exportSTD", "IPPurchases", "EPPurchases", "RMPurchases",
"dimPurchases", "allDimPurchases", "replicantiPurchases", "dilatedTimePurchases", "disabled"];
for (const key of toDelete) delete player.IAP[key];
// TODO Possibly update this with a dev STD migration?
}
],
patch(player) {

View File

@ -152,7 +152,7 @@ GameStorage.migrations = {
GameStorage.migrations.moveTS33(player);
GameStorage.migrations.addBestPrestigeCurrency(player);
kong.migratePurchases();
shop.migratePurchases();
}
},
@ -374,7 +374,7 @@ GameStorage.migrations = {
adjustThemes(player) {
delete player.options.themes;
if (player.options.theme === undefined) player.options.theme = "Normal";
if (player.options.theme === undefined) player.options.themeClassic = "Normal";
delete player.options.secretThemeKey;
},
@ -704,7 +704,7 @@ GameStorage.migrations = {
// (2) a two-way swap of costco sells dimboosts now and 8 nobody got time for that
// (3) a two-way swap of long lasting relationship and eternities are the new infinity
const swaps = { "4,3": "6,4", "6,4": "7,7", "7,7": "4,3",
"10,1": "11,7", "11,7": "10,1", "11,3": "12,4", "12,4": "11,3" };
"10,1": "11,7", "11,7": "10,1", "11,3": "12,4", "12,4": "11,3" };
const convertAchievementArray = (newAchievements, oldAchievements, isSecret) => {
for (const oldId of oldAchievements) {
let row = Math.floor(oldId / 10);
@ -883,8 +883,9 @@ GameStorage.migrations = {
},
removePriority(player) {
const dims = player.auto.antimatterDims.all ?? player.auto.antimatterDims;
for (let i = 0; i < 8; i++) {
delete player.auto.antimatterDims[i].priority;
delete dims[i].priority;
}
delete player.auto.tickspeed.priority;
},

View File

@ -169,11 +169,7 @@ export const GameStorage = {
},
export() {
const segmented = player.speedrun.isSegmented;
Speedrun.setSegmented(true);
const save = GameSaveSerializer.serialize(player);
Speedrun.setSegmented(segmented);
copyToClipboard(save);
copyToClipboard(this.exportModifiedSave());
GameUI.notify.info("Exported current savefile to your clipboard");
},
@ -185,19 +181,27 @@ export const GameStorage = {
const y = dateObj.getFullYear();
const m = dateObj.getMonth() + 1;
const d = dateObj.getDate();
const segmented = player.speedrun.isSegmented;
Speedrun.setSegmented(true);
const save = this.exportModifiedSave();
download(
`AD Save, Slot ${GameStorage.currentSlot + 1}${saveFileName} #${player.options.exportedFileCount} \
(${y}-${m}-${d}).txt`, GameSaveSerializer.serialize(player));
Speedrun.setSegmented(segmented);
(${y}-${m}-${d}).txt`, save);
GameUI.notify.info("Successfully downloaded current save file to your computer");
},
// There are a couple props which may need to export with different values, so we handle that here
exportModifiedSave() {
// Speedrun segmented is exported as true
const segmented = player.speedrun.isSegmented;
Speedrun.setSegmented(true);
// Serialize the altered data, then restore the old prop values afterwards and return
const save = GameSaveSerializer.serialize(player);
Speedrun.setSegmented(segmented);
return save;
},
hardReset() {
const IAP = JSON.parse(JSON.stringify(player.IAP));
this.loadPlayerObject(Player.defaultStart);
player.IAP = IAP;
this.save(true);
Tab.dimensions.antimatter.show();
Cloud.resetTempState();
@ -244,7 +248,7 @@ export const GameStorage = {
V.updateTotalRunUnlocks();
Enslaved.boostReality = false;
GameEnd.additionalEnd = 0;
Theme.set(player.options.theme);
Theme.set(Theme.currentName());
Notations.find(player.options.notation).setAsCurrent(true);
ADNotations.Settings.exponentCommas.show = player.options.commas;

View File

@ -1,8 +1,6 @@
import { DC } from "./constants";
export function getTickSpeedMultiplier() {
if (InfinityChallenge(3).isRunning) return DC.D1;
if (Ra.isRunning) return DC.C1D1_1245;
export function effectiveBaseGalaxies() {
// Note that this already includes the "50% more" active path effect
let replicantiGalaxies = Replicanti.galaxies.bought;
replicantiGalaxies *= (1 + Effects.sum(
@ -18,39 +16,14 @@ export function getTickSpeedMultiplier() {
replicantiGalaxies += nonActivePathReplicantiGalaxies * Effects.sum(EternityChallenge(8).reward);
let freeGalaxies = player.dilation.totalTachyonGalaxies;
freeGalaxies *= 1 + Math.max(0, Replicanti.amount.log10() / 1e6) * AlchemyResource.alternation.effectValue;
let galaxies = Math.max(player.galaxies + replicantiGalaxies + freeGalaxies + GalaxyGenerator.galaxies, 0);
if (galaxies < 3) {
// Magic numbers are to retain balancing from before while displaying
// them now as positive multipliers rather than negative percentages
let baseMultiplier = 1 / 1.1245;
if (player.galaxies === 1) baseMultiplier = 1 / 1.11888888;
if (player.galaxies === 2) baseMultiplier = 1 / 1.11267177;
if (NormalChallenge(5).isRunning) {
baseMultiplier = 1 / 1.08;
if (player.galaxies === 1) baseMultiplier = 1 / 1.07632;
if (player.galaxies === 2) baseMultiplier = 1 / 1.072;
}
const perGalaxy = 0.02 * Effects.product(
InfinityUpgrade.galaxyBoost,
InfinityUpgrade.galaxyBoost.chargedEffect,
BreakInfinityUpgrade.galaxyBoost,
TimeStudy(212),
TimeStudy(232),
Achievement(86),
Achievement(178),
InfinityChallenge(5).reward,
PelleUpgrade.galaxyPower,
PelleRifts.decay.milestones[1]
);
if (Pelle.isDoomed) galaxies *= 0.5;
return Math.max(player.galaxies + GalaxyGenerator.galaxies + replicantiGalaxies + freeGalaxies, 0);
}
galaxies *= Pelle.specialGlyphEffect.power;
return DC.D0_01.clampMin(baseMultiplier - (galaxies * perGalaxy));
}
let baseMultiplier = 0.8;
if (NormalChallenge(5).isRunning) baseMultiplier = 0.83;
galaxies -= 2;
galaxies *= Effects.product(
export function getTickSpeedMultiplier() {
if (InfinityChallenge(3).isRunning) return DC.D1;
if (Ra.isRunning) return DC.C1D1_1245;
let galaxies = effectiveBaseGalaxies();
const effects = Effects.product(
InfinityUpgrade.galaxyBoost,
InfinityUpgrade.galaxyBoost.chargedEffect,
BreakInfinityUpgrade.galaxyBoost,
@ -62,6 +35,27 @@ export function getTickSpeedMultiplier() {
PelleUpgrade.galaxyPower,
PelleRifts.decay.milestones[1]
);
if (galaxies < 3) {
// Magic numbers are to retain balancing from before while displaying
// them now as positive multipliers rather than negative percentages
let baseMultiplier = 1 / 1.1245;
if (player.galaxies === 1) baseMultiplier = 1 / 1.11888888;
if (player.galaxies === 2) baseMultiplier = 1 / 1.11267177;
if (NormalChallenge(5).isRunning) {
baseMultiplier = 1 / 1.08;
if (player.galaxies === 1) baseMultiplier = 1 / 1.07632;
if (player.galaxies === 2) baseMultiplier = 1 / 1.072;
}
const perGalaxy = 0.02 * effects;
if (Pelle.isDoomed) galaxies *= 0.5;
galaxies *= Pelle.specialGlyphEffect.power;
return DC.D0_01.clampMin(baseMultiplier - (galaxies * perGalaxy));
}
let baseMultiplier = 0.8;
if (NormalChallenge(5).isRunning) baseMultiplier = 0.83;
galaxies -= 2;
galaxies *= effects;
galaxies *= getAdjustedGlyphEffect("cursedgalaxies");
galaxies *= getAdjustedGlyphEffect("realitygalaxies");
galaxies *= 1 + ImaginaryUpgrade(9).effectOrDefault(0);
@ -78,6 +72,7 @@ export function buyTickSpeed() {
if (NormalChallenge(9).isRunning) {
Tickspeed.multiplySameCosts();
}
Tutorial.turnOffEffect(TUTORIAL_STATE.TICKSPEED);
Currency.antimatter.subtract(Tickspeed.cost);
player.totalTickBought++;
player.records.thisInfinity.lastBuyTime = player.records.thisInfinity.time;
@ -91,6 +86,7 @@ export function buyMaxTickSpeed() {
if (!Tickspeed.isAvailableForPurchase || !Tickspeed.isAffordable) return;
let boughtTickspeed = false;
Tutorial.turnOffEffect(TUTORIAL_STATE.TICKSPEED);
if (NormalChallenge(9).isRunning || InfinityChallenge(5).isRunning) {
const goal = Player.infinityGoal;
let cost = Tickspeed.cost;
@ -171,16 +167,20 @@ export const Tickspeed = {
},
get baseValue() {
let boughtTickspeed;
if (Laitela.continuumActive) boughtTickspeed = this.continuumValue;
else boughtTickspeed = player.totalTickBought;
return DC.E3.timesEffectsOf(
Achievement(36),
Achievement(45),
Achievement(66),
Achievement(83)
)
.times(getTickSpeedMultiplier().pow(boughtTickspeed + player.totalTickGained));
.times(getTickSpeedMultiplier().pow(this.totalUpgrades));
},
get totalUpgrades() {
let boughtTickspeed;
if (Laitela.continuumActive) boughtTickspeed = this.continuumValue;
else boughtTickspeed = player.totalTickBought;
return boughtTickspeed + player.totalTickGained;
},
get perSecond() {

View File

@ -75,6 +75,16 @@ export const Time = {
this.toMilliseconds(timespan, value => player.records.totalTimePlayed = value);
},
/**
* @returns {TimeSpan}
*/
get timeSinceBlackHole() {
return this.fromMilliseconds(() => {
const diff = player.records.totalTimePlayed - player.records.timePlayedAtBHUnlock;
return Math.max(0, diff);
});
},
/**
* @returns {TimeSpan}
*/

View File

@ -144,8 +144,6 @@ class TabState {
const tabNotificationKey = this.key + this._currentSubtab.key;
if (player.tabNotifications.has(tabNotificationKey)) player.tabNotifications.delete(tabNotificationKey);
// Makes it so that the glyph tooltip doesn't stay on tab change
ui.view.tabs.reality.currentGlyphTooltip = -1;
if (manual) Modal.hideAll();
EventHub.dispatch(GAME_EVENT.TAB_CHANGED, this, this._currentSubtab);
}

View File

@ -5,6 +5,7 @@ import { deepmergeAll } from "@/utility/deepmerge";
import { playFabLogin } from "./core/playfab";
import { SpeedrunMilestones } from "./core/speedrun";
import { supportedBrowsers } from "./supported-browsers";
import Payments from "./core/payments";
if (GlobalErrorHandler.handled) {
@ -321,9 +322,9 @@ export function getGameSpeedupFactor(effectsToConsider, blackHolesActiveOverride
let factor = 1;
if (effects.includes(GAME_SPEED_EFFECT.BLACK_HOLE)) {
if (BlackHoles.arePaused) {
if (BlackHoles.areNegative) {
factor *= player.blackHoleNegative;
} else {
} else if (!BlackHoles.arePaused) {
for (const blackHole of BlackHoles.list) {
if (!blackHole.isUnlocked) break;
const isActive = blackHolesActiveOverride === undefined
@ -634,7 +635,8 @@ export function gameLoop(passDiff, options = {}) {
function passivePrestigeGen() {
let eternitiedGain = 0;
if (RealityUpgrade(14).isBought) {
eternitiedGain = Effects.product(
eternitiedGain = DC.D1.timesEffectsOf(
Achievement(113),
RealityUpgrade(3),
RealityUpgrade(14)
);
@ -722,11 +724,31 @@ function laitelaRealityTick(realDiff) {
}
if (Laitela.realityReward > oldInfo.realityReward) {
completionText += `<br><br>Dark Matter Multiplier: ${formatX(oldInfo.realityReward, 2, 2)}
${formatX(Laitela.realityReward, 2, 2)}
<br>Best Completion Time: ${TimeSpan.fromSeconds(oldInfo.fastestCompletion).toStringShort()}
(${formatInt(8 - oldInfo.difficultyTier)})
${TimeSpan.fromSeconds(laitelaInfo.fastestCompletion).toStringShort()}
(${formatInt(8 - laitelaInfo.difficultyTier)})`;
${formatX(Laitela.realityReward, 2, 2)}`;
if (oldInfo.fastestCompletion === 3600 || oldInfo.fastestCompletion === 300 && oldInfo.difficultyTier > 0) {
if (Time.thisRealityRealTime.totalSeconds < 30) {
// First attempt - destabilising
completionText += `<br>Best Completion Time: None ➜ Destabilized
<br>Highest Active Dimension: ${formatInt(8 - oldInfo.difficultyTier)}
${formatInt(8 - laitelaInfo.difficultyTier)}`;
} else {
// First attempt - not destabilising
completionText += `<br>Best Completion Time: None ➜
${TimeSpan.fromSeconds(laitelaInfo.fastestCompletion).toStringShort()}
<br>Highest Active Dimension: ${formatInt(8 - laitelaInfo.difficultyTier)}`;
}
} else if (Time.thisRealityRealTime.totalSeconds < 30) {
// Second+ attempt - destabilising
completionText += `<br>Best Completion Time: ${TimeSpan.fromSeconds(oldInfo.fastestCompletion).toStringShort()}
Destablized
<br>Highest Active Dimension: ${formatInt(8 - oldInfo.difficultyTier)}
${formatInt(8 - laitelaInfo.difficultyTier)}`;
} else {
// Second+ attempt - not destabilising
completionText += `<br>Best Completion Time: ${TimeSpan.fromSeconds(oldInfo.fastestCompletion).toStringShort()}
${TimeSpan.fromSeconds(laitelaInfo.fastestCompletion).toStringShort()}
<br>Highest Active Dimension: ${formatInt(8 - oldInfo.difficultyTier)}`;
}
player.records.bestReality.laitelaSet = Glyphs.copyForRecords(Glyphs.active.filter(g => g !== null));
} else {
completionText += ` You need to destabilize in faster than
@ -1040,6 +1062,8 @@ export function init() {
Tabs.all.find(t => t.config.id === player.options.lastOpenTab).show(true);
kong.init();
if(steamOn){SteamFunctions.UIZoom()}
shop.init();
Payments.init();
}
window.tweenTime = 0;

View File

@ -1002,23 +1002,6 @@
padding: 1rem;
}
.c-glyph-choice-container {
display: flex;
flex-flow: row wrap;
width: 74rem;
}
.c-glyph-choice-single-glyph {
display: flex;
flex-direction: row;
width: 36rem;
height: 12rem;
justify-content: space-evenly;
align-items: center;
border-radius: var(--var-border-radius, 0.5rem);
margin: 0.5rem;
}
.c-glyph-choice-icon {
display: flex;
flex-direction: column;

View File

@ -277,6 +277,7 @@ body.t-s9 {
display: flex;
flex-direction: column;
justify-content: center;
color: var(--color-text);
}
.l-antimatter-dim-tab {
@ -635,7 +636,7 @@ body.t-s9 {
}
.t-inverted .c-autobuyer-box-row__modal,
.t-inverted-metro .c-autobuyer-box-row__modal {
.t-inverted-metro .c-autobuyer-box-row__modal {
background-color: var(--color-base);
border-color: var(--color-text);
}

View File

@ -61,6 +61,7 @@
flex-direction: column;
height: 100%;
align-items: center;
color: var(--color-text);
}
.s-base--metro .o-tab-btn {

View File

@ -61,7 +61,7 @@ html {
--color-bad: #b84b5f;
--color-gh-purple: #8957e5;
--color-notification: red;
--color-overlay: rgba(30, 30, 50, 90%);
--color-overlay: rgba(30, 30, 50, 70%);
--color-antimatter: #2196f3;
--color-infinity: #b67f33;
@ -330,7 +330,6 @@ button:focus {
}
.c-tooltip-content {
content: attr(ach-tooltip);
width: 16rem;
position: absolute;
z-index: 4;
@ -1464,6 +1463,7 @@ br {
border-radius: var(--var-border-radius, 0.5rem);
padding: 0.5rem;
transition: opacity 0.3s, visibility 0.3s;
pointer-events: none;
}
.s-base--dark .infotooltip .infotooltiptext {
@ -1601,16 +1601,16 @@ br {
}
.o-primary-btn--dimboost {
position: relative;
width: 21rem;
height: 4.5rem;
position: relative;
font-size: 0.9rem;
}
.o-primary-btn--galaxy {
position: relative;
width: 21rem;
height: 4.5rem;
position: relative;
font-size: 0.9rem;
}
@ -2304,6 +2304,9 @@ br {
justify-content: center;
align-items: center;
font-size: 1.2rem;
font-weight: bold;
color: var(--color-text);
margin: 0.5rem;
}
.l-game-header__buttons-line {
@ -2501,6 +2504,7 @@ br {
display: flex;
flex-direction: column;
align-items: center;
color: var(--color-text);
}
.l-infinity-dim-tab__ec8-purchases {
@ -2615,6 +2619,7 @@ br {
display: flex;
flex-direction: column;
align-items: center;
color: var(--color-text);
}
/* #endregion l-time-dim-tab */
@ -4187,11 +4192,6 @@ br {
border-color: var(--color-text);
}
.t-s6 .o-autobuyer-toggle-checkbox__label,
.t-s10 .o-autobuyer-toggle-checkbox__label {
border-color: #cccccc;
}
.t-s6 .o-autobuyer-toggle-checkbox__label,
.t-s10 .o-autobuyer-toggle-checkbox__label {
border-color: var(--color-text);
@ -4391,7 +4391,7 @@ br {
.c-autobuyer-box-row {
display: flex;
width: 90rem;
width: 96rem;
position: relative;
justify-content: center;
align-items: center;
@ -4467,7 +4467,7 @@ properly on certain themes. */
display: flex;
flex-grow: 1;
flex-wrap: wrap;
width: 91rem;
width: 97rem;
}
.c-small-autobuyer-input {
@ -4746,6 +4746,7 @@ properly on certain themes. */
.o-eternity-upgrade--useless {
color: black;
background-color: var(--color-pelle--base);
filter: grayscale(50%);
border-color: black;
}
@ -5208,6 +5209,7 @@ properly on certain themes. */
.o-dilation-upgrade--useless {
color: black;
background-color: var(--color-pelle--base);
filter: grayscale(50%);
}
.o-dilation-upgrade--unavailable {
@ -5245,10 +5247,6 @@ properly on certain themes. */
background-color: #d72621;
}
.t-dark .o-dilation-upgrade {
background-color: black;
}
.t-dark .o-dilation-upgrade--available:hover,
.t-s6 .o-dilation-upgrade--available:hover,
.t-s10 .o-dilation-upgrade--available:hover {
@ -5783,7 +5781,7 @@ properly on certain themes. */
.c-modal-IAP__warning {
font-size: 1.6rem;
font-weight: bold;
color: #ff6666;
color: var(--color-notification);
}
/* #endregion c-modal-import */
@ -7383,8 +7381,8 @@ kbd {
.c-ra-run-button {
display: flex;
flex-direction: column;
width: 31rem;
height: 33rem;
width: 33rem;
height: 36rem;
justify-content: center;
align-items: center;
font-family: Typewriter;
@ -7461,8 +7459,8 @@ kbd {
.c-ra-remembrance-unlock {
display: flex;
flex-direction: column;
width: 31rem;
height: 33rem;
width: 33rem;
height: 36rem;
justify-content: center;
align-items: center;
color: white;
@ -7636,8 +7634,8 @@ kbd {
left: 50%;
z-index: 0;
border-top: 0 solid black;
border-right: var(--var-border-width, 0.7rem) solid transparent;
border-left: var(--var-border-width, 0.7rem) solid transparent;
border-right: 0.7rem solid transparent;
border-left: 0.7rem solid transparent;
margin-bottom: 0;
margin-left: -0.7rem;
transition-duration: 0.3s;
@ -8087,7 +8085,7 @@ kbd {
}
.o-laitela-run-button--large {
width: 45rem;
width: 44rem;
}
.o-laitela-matter-amount {
@ -8098,7 +8096,7 @@ kbd {
.c-dark-matter-dimension-container {
display: flex;
flex-direction: column;
width: 50rem;
width: 51rem;
height: 15rem;
color: var(--color-laitela--accent);
background: var(--color-laitela--base);
@ -8124,6 +8122,7 @@ kbd {
width: 15rem;
height: 5rem;
font-family: Typewriter;
font-size: 1.2rem;
color: var(--color-laitela--accent);
background: var(--color-laitela--base);
border: var(--var-border-width, 0.2rem) solid var(--color-laitela--accent);
@ -8210,7 +8209,7 @@ kbd {
.c-laitela-milestone {
display: flex;
flex-direction: column;
width: 22rem;
width: 21rem;
height: 16%;
position: relative;
z-index: 0;
@ -8261,9 +8260,9 @@ kbd {
.c-laitela-milestone--completed {
width: 18.8rem;
height: 21.8rem;
height: 20.8rem;
top: -1.5rem;
left: 1.5rem;
left: 1rem;
background-color: transparent;
background-image: url("../images/laitela-icon.svg");
background-position: center;
@ -8281,8 +8280,6 @@ kbd {
background: var(--color-laitela--base);
border: var(--var-border-width, 0.2rem) solid var(--color-laitela--accent);
border-radius: var(--var-border-radius, 0.5rem);
margin: auto;
margin-top: 1rem;
margin-bottom: 1rem;
padding: 1rem;
}
@ -8362,7 +8359,7 @@ kbd {
.l-singularity-milestone-modal-container-inner {
display: flex;
flex-wrap: wrap;
width: 93rem;
width: 111rem;
height: 120rem;
justify-content: space-evenly;
padding-bottom: 2rem;
@ -8379,12 +8376,12 @@ kbd {
left: -0.5rem;
border: 0.1rem solid var(--color-laitela--accent);
border-radius: var(--var-border-radius, 0.6rem);
margin-top: 0.5rem;
margin: 1rem 0 0 1rem;
padding: 1rem 0.5rem;
}
.c-singularity-milestone-modal-sort-button {
width: 22rem;
width: 26.5rem;
height: 6rem;
font-family: Typewriter, serif;
font-size: 1.3rem;
@ -8404,7 +8401,7 @@ kbd {
}
.o-laitela-singularity-modal-button {
width: 22rem;
width: 21rem;
height: 4%;
font-weight: bold;
color: var(--color-laitela--accent);
@ -8480,7 +8477,7 @@ kbd {
}
.l-laitela-annihilation-container {
width: 50rem;
width: 51rem;
height: 20rem;
font-family: Typewriter, serif;
font-size: 1.2rem;
@ -8525,7 +8522,7 @@ kbd {
}
.c-laitela-automation-toggle {
width: 20rem;
width: 22rem;
height: 3rem;
font-family: Typewriter;
font-size: 1.1rem;
@ -9551,7 +9548,7 @@ input.o-automator-block-input {
.c-autobuyer-buy-box {
display: flex;
width: 90rem;
width: 96rem;
height: 6.4rem;
justify-content: center;
align-items: center;

View File

@ -47,6 +47,7 @@
border: 0.1rem solid;
border-radius: var(--var-border-radius, 0.4rem);
transition-duration: 0.2s;
text-decoration: line-through;
}
.c-pelle-useless-available {

View File

@ -16,7 +16,7 @@ export default {
},
methods: {
update() {
this.blob = player.options.theme === "S11";
this.blob = Theme.currentName() === "S11";
this.animateTachyons = player.options.animations.tachyonParticles &&
Tabs.current[this.$viewModel.subtab].name === "Time Dilation";
}

View File

@ -48,6 +48,8 @@ export default {
const challengeNotEnterable = !this.isUnlocked || this.isRunning || this.name === "C1";
return {
"o-challenge-btn": true,
"o-challenge-btn--broken": this.overrideLabel.length > 0 && this.name !== "C10",
"o-challenge-btn--broken-alt": this.overrideLabel.length > 0 && this.name === "C10",
"o-challenge-btn--running": this.isRunning || this.inC1,
"o-challenge-btn--completed": this.isCompleted && this.isUnlocked,
"o-challenge-btn--unlocked": !this.isCompleted && this.isUnlocked,
@ -96,5 +98,15 @@ export default {
</template>
<style scoped>
.o-challenge-btn--broken {
background: var(--color-enslaved--base);
clip-path: polygon(0% 0%, 25% 20%, 95% 0%, 100% 25%, 80% 70%, 95% 50%, 100% 100%, 45% 95%,
65% 70%, 15% 95%, 0% 45%, 10% 50%);
}
.o-challenge-btn--broken-alt {
background: var(--color-enslaved--base);
clip-path: polygon(0% 0%, 15% 0%, 25% 40%, 30% 0%, 55% 0%, 85% 30%, 75% 0%, 100% 0%,
90% 40%, 100% 65%, 90% 95%, 45% 45%, 70% 100%, 25% 100%, 5% 90%, 10% 60%);
}
</style>

View File

@ -1,4 +1,6 @@
<script>
import wordShift from "../../javascripts/core/wordShift";
import { isFunction, isString } from "@/utility";
/* eslint-disable no-empty-function */
@ -71,9 +73,19 @@ export default {
const value = description();
if (isString(value)) {
// This is a special case for scrambling EC6 description text
if (this.config.scrambleText) {
this.description = capitalize(value).replace("*", wordShift.wordCycle(this.config.scrambleText, true));
this.updateFunction = () =>
this.description = capitalize(description())
.replace("*", wordShift.wordCycle(this.config.scrambleText, true));
return;
}
this.description = capitalize(value);
this.updateFunction = () => this.description = capitalize(description());
return;
}
throw new Error(`DescriptionDisplay config.description is a function ` +

View File

@ -38,10 +38,9 @@ export default {
update() {
this.baseSpeed = getGameSpeedupFactor();
this.pulsedSpeed = getGameSpeedupForDisplay();
const ec12 = EternityChallenge(12);
this.hasSeenAlteredSpeed = PlayerProgress.realityUnlocked() || ec12.completions > 0 || ec12.isRunning;
this.hasSeenAlteredSpeed = PlayerProgress.seenAlteredSpeed();
this.isStopped = Enslaved.isStoringRealTime;
this.isEC12 = ec12.isRunning;
this.isEC12 = EternityChallenge(12).isRunning;
this.isPulsing = (this.baseSpeed !== this.pulsedSpeed) && Enslaved.canRelease(true);
},
formatNumber(num) {
@ -58,14 +57,17 @@ export default {
</script>
<template>
<b>
<span class="c-gamespeed">
<span>
{{ baseText }}
</span>
<span v-if="isPulsing">(<i class="fas fa-expand-arrows-alt u-fa-padding" /> {{ pulseSpeedText }})</span>
</b>
</span>
</template>
<style scoped>
.c-gamespeed {
font-weight: bold;
color: var(--color-text);
}
</style>

View File

@ -96,6 +96,7 @@ export default {
// This flag is used to prevent the tooltip from being shown in some touch event sequences
suppressTooltip: false,
isTouched: false,
tooltipEnabled: false,
sacrificeReward: 0,
uncappedRefineReward: 0,
refineReward: 0,
@ -307,6 +308,14 @@ export default {
this.$recompute("displayedInfo");
});
this.on$("tooltip-touched", () => this.hideTooltip());
this.on$(GAME_EVENT.TAB_CHANGED, () => this.hideTooltip());
// There are a few situations where a tooltip could attempt to render immediately upon component creation,
// which causes it to be placed in an odd "default" corner spot due to mouse position not being set properly.
// This is essentially a hack that force-suppresses tooltips from being shown in strange spots due to on-load
// events firing, but has the side effect that the mouse must leave and enter an element which was created
// underneath it in order to make the tooltip appear
setTimeout(() => this.tooltipEnabled = true, 10);
},
beforeDestroy() {
if (this.isCurrentTooltip) this.hideTooltip();
@ -361,6 +370,7 @@ export default {
this.$viewModel.tabs.reality.currentGlyphTooltip = -1;
},
showTooltip() {
if (!this.tooltipEnabled) return;
Glyphs.removeNewFlag(this.glyph);
this.tooltipLoaded = true;
this.$viewModel.tabs.reality.mouseoverGlyphInfo.inInventory = !this.circular;

View File

@ -98,7 +98,7 @@ export default {
Modal.glyphShowcasePanel.show({
name: this.text,
glyphSet: this.glyphs,
closeOn: GAME_EVENT.GLYPH_SET_SAVE_CHANGE,
closeEvent: GAME_EVENT.GLYPH_SET_SAVE_CHANGE,
displaySacrifice: this.showSacrifice,
});
}

View File

@ -55,6 +55,7 @@ export default {
"o-infinity-upgrade-btn--available": !this.isUseless && !this.isBought && this.canBeBought,
"o-infinity-upgrade-btn--unavailable": !this.isUseless && !this.isBought && !this.canBeBought,
"o-infinity-upgrade-btn--useless": this.isUseless,
"o-pelle-disabled": this.isUseless,
"o-infinity-upgrade-btn--chargeable": !this.isCharged && this.chargePossible &&
(this.showingCharged || this.shiftDown),
"o-infinity-upgrade-btn--charged": this.isCharged,

View File

@ -11,15 +11,12 @@ export default {
};
},
computed: {
localString() {
const desc = this.cloudSaveEnabled
? "Time since last local save:"
: "Time since last save:";
return `${desc} ${timeDisplayShort(this.currentTime - this.lastLocalSave)}`;
},
cloudString() {
if (!this.cloudSaveEnabled) return null;
return `Time since last cloud save: ${timeDisplayShort(this.currentTime - this.lastCloudSave)}`;
timeString() {
const localStr = timeDisplayShort(this.currentTime - this.lastLocalSave);
const cloudStr = timeDisplayShort(this.currentTime - this.lastCloudSave);
return this.cloudSaveEnabled
? `${localStr} (local) | ${cloudStr} (cloud)`
: localStr;
},
},
methods: {
@ -43,9 +40,7 @@ export default {
class="o-save-timer"
@click="save"
>
{{ localString }}
<br>
{{ cloudString }}
Time since last save: {{ timeString }}
</div>
</template>

View File

@ -5,6 +5,7 @@ export default {
return {
isActive: false,
isSegmented: false,
usedSTD: false,
hasStarted: false,
startDate: 0,
saveName: "",
@ -25,6 +26,9 @@ export default {
segmentText() {
return this.isSegmented ? "Segmented Speedrun (imported save)" : "Single-segment Speedrun (no save import)";
},
iapText() {
return this.usedSTD ? "IAPs have been used" : "No IAPs Used";
},
offlineText() {
const stateText = this.offlineProgress
? `<span style="color: var(--color-good)">Enabled</span>`
@ -48,6 +52,7 @@ export default {
// Short-circuit if speedrun isn't active; updating some later stuff can cause vue errors outside of speedruns
if (!this.isActive) return;
this.isSegmented = speedrun.isSegmented;
this.usedSTD = speedrun.usedSTD;
this.hasStarted = speedrun.hasStarted;
this.startDate = speedrun.startDate;
this.saveName = speedrun.name;
@ -98,6 +103,8 @@ export default {
<br>
<i>{{ segmentText }}</i>
<br>
<i>{{ iapText }}</i>
<br>
Total real playtime since start: {{ timePlayedStr }}
<br>
Offline Progress: <span v-html="offlineText" />

View File

@ -72,13 +72,10 @@ export default {
},
typeStyle() {
// Special case for cursed glyphs because its black default has poor contrast on some themes
// TODO Update this when we "fix" Modern UI Normal because #5151EC looks kinda weird
const color = this.glyph.type === "cursed"
? "#5151EC"
: GlyphTypes[this.type].color;
return {
color: `${color}`,
color: GlyphTypes[this.type].color,
"font-weight": "bold",
"text-shadow": this.type === "cursed" ? "0.05rem 0.05rem var(--color-text)" : undefined,
animation: this.type === "reality" ? "a-reality-glyph-description-cycle 10s infinite" : undefined,
};
},

View File

@ -19,10 +19,6 @@ export default {
type: Array,
required: true
},
closeOn: {
type: String,
required: true
},
isGlyphSelection: {
type: Boolean,
default: false
@ -52,9 +48,12 @@ export default {
}
return maxEffects;
},
},
created() {
this.on$(this.closeOn, this.emitClose);
containerClass() {
return {
"c-glyph-choice-container": true,
"c-glyph-choice-container-single": this.glyphs.length === 1,
};
}
},
methods: {
update() {
@ -85,7 +84,7 @@ export default {
:glyph-set="glyphs"
:force-color="true"
/>
<div class="c-glyph-choice-container">
<div :class="containerClass">
<GlyphShowcasePanelEntry
v-for="(glyph, idx) in glyphs"
:key="idx"
@ -100,3 +99,26 @@ export default {
</div>
</ModalWrapper>
</template>
<style scoped>
.c-glyph-choice-container {
display: flex;
flex-flow: row wrap;
width: 74rem;
}
.c-glyph-choice-container-single {
width: 37rem;
}
.c-glyph-choice-single-glyph {
display: flex;
flex-direction: row;
width: 36rem;
height: 12rem;
justify-content: space-evenly;
align-items: center;
border-radius: var(--var-border-radius, 0.5rem);
margin: 0.5rem;
}
</style>

View File

@ -1,87 +0,0 @@
<script>
import ModalWrapperChoice from "@/components/modals/ModalWrapperChoice";
import PrimaryButton from "@/components/PrimaryButton";
import SaveInfoEntry from "@/components/modals/cloud/SaveInfoEntry";
export default {
name: "ImportFileWarningModal",
components: {
ModalWrapperChoice,
PrimaryButton,
SaveInfoEntry
},
props: {
rawInput: {
type: String,
required: true
},
saveToImport: {
type: Object,
required: true
},
warningMessage: {
type: String,
required: true
}
},
data() {
return {
importCounter: 0,
};
},
computed: {
conflict() {
return this.$viewModel.modal.cloudConflict;
},
clicksLeft() {
return 5 - this.importCounter;
}
},
methods: {
handleClick() {
this.importCounter++;
if (this.clicksLeft > 0) return;
this.emitClose();
GameStorage.import(this.rawInput);
},
},
};
</script>
<template>
<ModalWrapperChoice :show-confirm="false">
<template #header>
Possible Import Conflict
</template>
<div class="c-modal-message__text">
You may not want to proceed with importing, as you may lose STD purchases.
</div>
<br>
<SaveInfoEntry
:save-data="conflict.importingSave"
:other-data="conflict.currentSave"
save-type="Save to Import"
/>
<br>
<SaveInfoEntry
:save-data="conflict.currentSave"
:other-data="conflict.importingSave"
save-type="Current Save"
/>
<span class="c-modal-IAP__warning">
{{ warningMessage }}
</span>
Your purchased STDs will not carry over to the imported save.
<br>
Click Import five times if you still wish to import.
<template #cancel-text>
Keep Current
</template>
<PrimaryButton
class="o-primary-btn--width-medium c-modal-message__okay-btn c-modal__confirm-btn"
@click="handleClick"
>
Import <span v-if="clicksLeft">({{ clicksLeft }})</span>
</PrimaryButton>
</ModalWrapperChoice>
</template>

View File

@ -17,7 +17,6 @@ export default {
data() {
return {
input: "",
importCounter: 0,
offlineImport: OFFLINE_PROGRESS_TYPE.IMPORTED,
};
},
@ -58,12 +57,6 @@ export default {
inputIsSecret() {
return isSecretImport(this.input) || Theme.isSecretTheme(this.input);
},
hasLessSTDs() {
return player.IAP.totalSTD > (this.player?.IAP?.totalSTD ?? 0) && !this.inputIsSecret;
},
clicksLeft() {
return 5 - this.importCounter;
},
timeSinceSave() {
return TimeSpan.fromMilliseconds(Date.now() - this.player.lastUpdate).toString();
},
@ -124,8 +117,6 @@ export default {
}
},
importSave() {
this.importCounter++;
if (this.hasLessSTDs && this.clicksLeft > 0) return;
if (!this.inputIsValid) return;
this.emitClose();
GameStorage.import(this.input);
@ -182,13 +173,6 @@ export default {
</div>
<span v-html="offlineDetails" />
</div>
<div
v-if="hasLessSTDs"
class="c-modal-IAP__warning"
>
IMPORTED SAVE HAS LESS STDs BOUGHT, YOU WILL LOSE THEM WITH YOUR SAVE.
<br>CLICK THE BUTTON 5 TIMES TO CONFIRM.
</div>
</template>
<div v-else-if="hasInput">
Not a valid save:
@ -202,7 +186,7 @@ export default {
class="o-primary-btn--width-medium c-modal-message__okay-btn c-modal__confirm-btn"
@click="importSave"
>
Import <span v-if="hasLessSTDs">({{ clicksLeft }})</span>
Import
</PrimaryButton>
</ModalWrapperChoice>
</template>

View File

@ -29,7 +29,7 @@ export default {
},
createRealityGlyph() {
if (GameCache.glyphInventorySpace.value === 0) {
Modal.message.show("Inventory cannot hold new Glyphs. Purge some Glyphs.",
Modal.message.show("No available inventory space; Sacrifice some Glyphs to free up space.",
{ closeEvent: GAME_EVENT.GLYPHS_CHANGED });
return;
}

View File

@ -20,13 +20,18 @@ export default {
return {
target: 0,
idx: 0,
isDoomed: false,
};
},
computed: {
resetTerm() { return this.isDoomed ? "Armageddon" : "Reality"; },
},
methods: {
update() {
this.target = this.targetSlot;
this.idx = this.inventoryIndex;
this.glyph = Glyphs.findByInventoryIndex(this.idx);
this.isDoomed = Pelle.isDoomed;
},
handleYesClick() {
Glyphs.swapIntoActive(this.glyph, this.targetSlot);
@ -43,6 +48,6 @@ export default {
<template #header>
You are about to replace a Glyph
</template>
Replacing a Glyph will restart this Reality.
Replacing a Glyph will restart this {{ resetTerm }}.
</ModalWrapperChoice>
</template>

View File

@ -7,8 +7,16 @@ export default {
ModalWrapperChoice
},
methods: {
returnedSTDCount() {
let std = 0;
for (const purchase of ShopPurchase.all) {
if (purchase.config.singleUse) continue;
std += purchase.purchases * purchase.cost;
}
return std;
},
handleYesClick() {
ShopPurchase.respecAll();
ShopPurchaseData.respecAll();
EventHub.ui.offAll(this);
}
},
@ -24,12 +32,15 @@ export default {
You are about to respec your Shop Purchases
</template>
<div class="c-modal-message__text">
Are you sure you want to respec your Shop Purchases? This will not cost any
Are you sure you want to respec your Shop Purchases? This will not cost anything and
return the {{ returnedSTDCount() }}
<img
src="images/std_coin.png"
class="o-shop-button-button__img"
>.
<!-- TODO: Should it cost any coins? -->
> you spent on all non-offline progress purchases.
<br>
<br>
<b class="o-warning">You will not be able to respec again unless you purchase more STD coins.</b>
</div>
</ModalWrapperChoice>
</template>
@ -43,4 +54,8 @@ export default {
height: 2.5rem;
vertical-align: middle;
}
.o-warning {
color: var(--color-infinity);
}
</style>

View File

@ -90,6 +90,7 @@ export default {
<br>
<br>
<div class="c-modal-hard-reset-danger">
<!-- TODO: Change the wording of this; originally this assumed it's only unlockable from a completed save -->
Starting a speedrun will overwrite your current save, replacing this save with the new speedrun save. Export
this save first if you want to keep a save with a beaten game. Type in "Gotta Go Fast!" below to confirm and
start the run.

Some files were not shown because too many files have changed in this diff Show More