Merge branch 'master' into Steam-Webpack

This commit is contained in:
ZackRhodes 2022-09-26 20:40:26 -04:00
commit 0dddaa6b90
560 changed files with 34020 additions and 21574 deletions

View File

@ -1,6 +1,7 @@
{
"extends": [
"eslint:recommended",
"plugin:import/recommended",
"plugin:vue/recommended"
],
"env": {
@ -14,7 +15,46 @@
"sourceType": "module",
"parser": "@babel/eslint-parser"
},
"settings": {
"import/resolver": {
"alias": {
"map": [
["@", "./src"]
],
"extensions": [".js", ".vue"]
}
}
},
"rules": {
"import/prefer-default-export": "off",
"import/no-unresolved": "error",
"import/named": "error",
"import/namespace": "error",
"import/default": "error",
"import/export": "error",
"import/no-named-as-default": "warn",
"import/no-named-as-default-member": "warn",
"import/no-duplicates": "warn",
"import/extensions": ["warn", "never"],
"import/first": "warn",
"import/newline-after-import": "warn",
"import/no-named-default": "warn",
"import/no-self-import": "warn",
"import/order": [
"warn",
{
"newlines-between": "always-and-inside-groups",
"pathGroups": [{ "pattern": "@/**", "group": "sibling" }]
}
],
"sort-imports": [
"warn",
{
"ignoreCase": true,
"allowSeparatedGroups": true
}
],
"no-console": "warn",
"no-template-curly-in-string": "warn",
"array-callback-return": "error",
@ -32,7 +72,6 @@
"allowElseIf": false
}
],
"vue/one-component-per-file": "error",
"vue/component-definition-name-casing": "warn",
"vue/order-in-components": "warn",
@ -161,7 +200,6 @@
],
"new-parens": "error",
"no-array-constructor": "warn",
"no-bitwise": "warn",
"no-inline-comments": "error",
"no-lonely-if": "error",
"no-mixed-spaces-and-tabs": "error",

3
.stylelintignore Normal file
View File

@ -0,0 +1,3 @@
public/stylesheets/fontawesome/**/*.css
public/stylesheets/codemirror/*.css
public/stylesheets/vis-network.css

425
.stylelintrc.json Normal file
View File

@ -0,0 +1,425 @@
{
"plugins": [
"stylelint-order"
],
"extends": [
"stylelint-config-standard"
],
"rules": {
"no-descending-specificity": null,
"font-family-no-missing-generic-family-keyword": null,
"no-empty-source": null,
"color-hex-length": "long",
"custom-property-empty-line-before": null,
"color-function-notation": null,
"declaration-empty-line-before": null,
"comment-empty-line-before": null,
"property-no-vendor-prefix": [
true,
{
"ignoreProperties": ["appearance", "background-clip", "backdrop-filter", "clip-path", "user-select"]
}
],
"unit-allowed-list": [
"rem",
"%",
"px",
"deg",
"s",
"ms",
"fr"
],
"custom-property-pattern": [
"^([_a-z][a-z0-9]*)([-_]{1,2}[a-z0-9]+)*$",
{
"message": "Expected custom property name to be kebab-case"
}
],
"selector-class-pattern": [
"^(CodeMirror.*|([_a-z][a-z0-9]*)([-_]{1,2}[a-z0-9]+)*)$",
{
"message": "Expected class selector name to be kebab-case"
}
],
"keyframes-name-pattern": [
"^a-([-_]{0,2}[a-z0-9]+)*$",
{
"message": "Keyframe name must begin with `a-` and be kebab-case"
}
],
"selector-id-pattern": [
"^([_a-z][a-z0-9]*)([-_]{1,2}[a-z0-9]+)*$",
{
"message": "Expected id selector name to be kebab-case"
}
],
"order/order": [
"custom-properties",
"at-rules",
"rules",
"declarations"
],
"order/properties-order": [
[
{
"groupName": "content",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"content"
]
},
{
"groupName": "display",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"display",
"visibility",
"float",
"clear",
"resize",
"overflow",
"overflow-x",
"overflow-y",
"white-space",
"word-break",
"overflow-wrap",
"tab-size",
"clip",
"zoom"
]
},
{
"groupName": "flex",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"flex",
"flex-grow",
"flex-shrink",
"flex-basis",
"flex-flow",
"flex-direction",
"flex-wrap"
]
},
{
"groupName": "grid",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"grid",
"grid-auto-columns",
"grid-auto-flow",
"grid-auto-rows",
"grid-template-areas",
"grid-template-columns",
"grid-template-rows",
"grid-row-gap",
"grid-column-gap",
"row-gap",
"column-gap",
"grid-row",
"grid-row-start",
"grid-row-end",
"grid-column",
"grid-column-start",
"grid-column-end"
]
},
{
"groupName": "table",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"table-layout",
"empty-cells",
"caption-side",
"border-spacing",
"border-collapse",
"list-style",
"list-style-position",
"list-style-type",
"list-style-image"
]
},
{
"groupName": "size",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"width",
"height",
"min-width",
"max-width",
"min-height",
"max-height"
]
},
{
"groupName": "position",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"position",
"will-change",
"inset",
"top",
"right",
"bottom",
"left",
"z-index"
]
},
{
"groupName": "alignment",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"place-content",
"justify-content",
"align-content",
"align-items",
"align-self",
"vertical-align",
"text-align",
"text-align-last"
]
},
{
"groupName": "scrollbar",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"scrollbar-color",
"scrollbar-width"
]
},
{
"groupName": "svg",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"stroke",
"stroke-width",
"stroke-linecap",
"stroke-dasharray",
"fill",
"text-anchor"
]
},
{
"groupName": "font",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"font",
"font-family",
"font-size",
"font-stretch",
"font-style",
"font-variant",
"font-weight",
"font-smoothing",
"font-smooth",
"line-height",
"src",
"unicode-range"
]
},
{
"groupName": "color",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"opacity",
"color"
]
},
{
"groupName": "text",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"text-shadow",
"text-decoration"
]
},
"appearance",
{
"groupName": "background",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"background",
"background-attachment",
"background-clip",
"background-color",
"background-image",
"background-origin",
"background-position",
"background-position-x",
"background-position-y",
"background-repeat",
"background-size"
]
},
{
"groupName": "border",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"border",
"border-color",
"border-style",
"border-width",
"border-top",
"border-top-color",
"border-top-style",
"border-top-width",
"border-right",
"border-right-color",
"border-right-style",
"border-right-width",
"border-bottom",
"border-bottom-color",
"border-bottom-style",
"border-bottom-width",
"border-left",
"border-left-color",
"border-left-style",
"border-left-width",
"border-radius",
"border-top-left-radius",
"border-top-right-radius",
"border-bottom-right-radius",
"border-bottom-left-radius",
"border-spacing"
]
},
{
"groupName": "box",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"box-shadow",
"box-sizing"
]
},
{
"groupName": "outline",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"outline",
"outline-width",
"outline-style",
"outline-color",
"outline-offset"
]
},
{
"groupName": "margin",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left"
]
},
{
"groupName": "padding",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left"
]
},
{
"groupName": "animation",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"transform",
"transform-origin",
"filter",
"mix-blend-mode",
"transition",
"transition-delay",
"transition-timing-function",
"transition-duration",
"transition-property",
"animation",
"animation-name",
"animation-duration",
"animation-play-state",
"animation-timing-function",
"animation-delay",
"animation-iteration-count",
"animation-direction",
"animation-fill-mode"
]
},
{
"groupName": "pointer",
"emptyLineBefore": "never",
"noEmptyLineBetween": true,
"properties": [
"pointer-events",
"user-select",
"cursor"
]
}
],
{
"unspecified": "bottomAlphabetical",
"emptyLineBeforeUnspecified": "always"
}
]
},
"overrides": [
{
"files": [
"*.vue",
"**/*.vue"
],
"extends": [
"stylelint-config-recommended",
"stylelint-config-html"
],
"rules": {
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": [
"deep",
"global"
]
}
],
"selector-pseudo-element-no-unknown": [
true,
{
"ignorePseudoElements": [
"v-deep",
"v-global",
"v-slotted"
]
}
]
}
}
]
}

View File

@ -1,8 +1,10 @@
/* eslint-disable no-bitwise */
/* eslint-disable no-console */
const fs = require("fs");
const path = require("path");
const proc = require("child_process");
const readline = require("readline");
function getHash(string) {
let hash = 0;
@ -31,9 +33,23 @@ if (newHash !== currentHash) {
fs.mkdirSync(tmpPath);
}
// eslint-disable-next-line no-console
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log("package-lock.json changes were detected");
const timeout = setTimeout(() => {
rl.close();
console.log("Running 'npm ci' (this might take a while)...");
proc.execSync("npm ci");
fs.writeFileSync(hashPath, newHash, {});
}, 5000);
// eslint-disable-next-line max-len
rl.question(`Press enter within the next five seconds to skip running 'npm ci' - this will leave your packages out of sync!`, () => {
console.log(`'npm ci' step skipped`);
rl.close();
clearTimeout(timeout);
});
}

View File

@ -1,13 +1,11 @@
import { GameMechanicState } from "../game-mechanics/index.js";
import { GameMechanicState } from "../game-mechanics/index";
class AchievementState extends GameMechanicState {
constructor(config) {
super(config);
this._row = Math.floor(this.id / 10);
this._column = this.id % 10;
// eslint-disable-next-line no-bitwise
this._bitmask = 1 << (this.column - 1);
// eslint-disable-next-line no-bitwise
this._inverseBitmask = ~this._bitmask;
this.registerEvents(config.checkEvent, args => this.tryUnlock(args));
}
@ -33,7 +31,6 @@ class AchievementState extends GameMechanicState {
}
get isUnlocked() {
// eslint-disable-next-line no-bitwise
return (player.achievementBits[this.row - 1] & this._bitmask) !== 0;
}
@ -52,13 +49,11 @@ class AchievementState extends GameMechanicState {
}
lock() {
// eslint-disable-next-line no-bitwise
player.achievementBits[this.row - 1] &= this._inverseBitmask;
}
unlock(auto) {
if (this.isUnlocked) return;
// eslint-disable-next-line no-bitwise
player.achievementBits[this.row - 1] |= this._bitmask;
if (this.id === 85 || this.id === 93) {
Autobuyer.bigCrunch.bumpAmount(4);
@ -66,7 +61,10 @@ class AchievementState extends GameMechanicState {
if (this.id === 55 && !PlayerProgress.realityUnlocked()) {
Modal.message.show(`Since you performed an Infinity in under a minute, the UI changed on the screen.
Instead of the Dimensions disappearing, they stay and the Big Crunch button appears on top of them.
This is purely visual, and is there to prevent flickering.`);
This is purely visual, and is there to prevent flickering.`, {}, 3);
}
if (this.id === 148 || this.id === 166) {
GameCache.staticGlyphWeights.invalidate();
}
if (auto) {
GameUI.notify.reality(`Automatically unlocked: ${this.name}`);
@ -169,8 +167,7 @@ export const Achievements = {
const unlockedRows = Achievements.allRows
.countWhere(row => row.every(ach => ach.isUnlocked));
const basePower = Math.pow(1.25, unlockedRows) * Math.pow(1.03, Achievements.effectiveCount);
let exponent = getAdjustedGlyphEffect("effarigachievement");
if (Ra.has(RA_UNLOCKS.ACHIEVEMENT_POW)) exponent *= 1.5;
const exponent = getAdjustedGlyphEffect("effarigachievement") * Ra.unlocks.achievementPower.effectOrDefault(1);
return Math.pow(basePower, exponent);
}),

View File

@ -1,13 +1,11 @@
import { GameMechanicState } from "../game-mechanics/index.js";
import { GameMechanicState } from "../game-mechanics/index";
class SecretAchievementState extends GameMechanicState {
constructor(config) {
super(config);
this._row = Math.floor(this.id / 10);
this._column = this.id % 10;
// eslint-disable-next-line no-bitwise
this._bitmask = 1 << (this.column - 1);
// eslint-disable-next-line no-bitwise
this._inverseBitmask = ~this._bitmask;
this.registerEvents(config.checkEvent, args => this.tryUnlock(args));
}
@ -25,7 +23,6 @@ class SecretAchievementState extends GameMechanicState {
}
get isUnlocked() {
// eslint-disable-next-line no-bitwise
return (player.secretAchievementBits[this.row - 1] & this._bitmask) !== 0;
}
@ -37,14 +34,12 @@ class SecretAchievementState extends GameMechanicState {
unlock() {
if (this.isUnlocked) return;
// eslint-disable-next-line no-bitwise
player.secretAchievementBits[this.row - 1] |= this._bitmask;
GameUI.notify.success(`Secret Achievement: ${this.name}`);
EventHub.dispatch(GAME_EVENT.ACHIEVEMENT_UNLOCKED);
}
lock() {
// eslint-disable-next-line no-bitwise
player.secretAchievementBits[this.row - 1] &= this._inverseBitmask;
}
}

View File

@ -1,80 +1,111 @@
import MessageModal from "@/components/modals/MessageModal";
import CelestialQuoteModal from "@/components/modals/CelestialQuoteModal";
import CloudSaveConflictModal from "@/components/modals/cloud/CloudSaveConflictModal";
import CloudLoadConflictModal from "@/components/modals/cloud/CloudLoadConflictModal";
import CloudManualLoginModal from "@/components/modals/cloud/CloudManualLoginModal";
import CloudSaveConflictModal from "@/components/modals/cloud/CloudSaveConflictModal";
import EternityChallengeStartModal from "@/components/modals/challenges/EternityChallengeStartModal";
import InfinityChallengeStartModal from "@/components/modals/challenges/InfinityChallengeStartModal";
import MessageModal from "@/components/modals/MessageModal";
import NormalChallengeStartModal from "@/components/modals/challenges/NormalChallengeStartModal";
import DimensionBoostModal from "@/components/modals/prestige/DimensionBoostModal";
import AntimatterGalaxyModal from "@/components/modals/prestige/AntimatterGalaxyModal";
import BigCrunchModal from "@/components/modals/prestige/BigCrunchModal";
import ReplicantiGalaxyModal from "@/components/modals/prestige/ReplicantiGalaxyModal";
import EternityModal from "@/components/modals/prestige/EternityModal";
import EnterDilationModal from "@/components/modals/prestige/EnterDilationModal";
import RealityModal from "@/components/modals/prestige/RealityModal";
import ResetRealityModal from "@/components/modals/prestige/ResetRealityModal";
import ExitCelestialModal from "@/components/modals/prestige/ExitCelestialModal";
import EnterCelestialsModal from "@/components/modals/prestige/EnterCelestialsModal";
import HardResetModal from "@/components/modals/prestige/HardResetModal";
import SpeedrunModeModal from "@/components/modals/SpeedrunModeModal";
import ChangeNameModal from "@/components/modals/ChangeNameModal";
import ArmageddonModal from "@/components/modals/prestige/ArmageddonModal";
import BigCrunchModal from "@/components/modals/prestige/BigCrunchModal";
import ChangeNameModal from "@/components/modals/ChangeNameModal";
import DimensionBoostModal from "@/components/modals/prestige/DimensionBoostModal";
import EnterCelestialsModal from "@/components/modals/prestige/EnterCelestialsModal";
import EnterDilationModal from "@/components/modals/prestige/EnterDilationModal";
import EternityModal from "@/components/modals/prestige/EternityModal";
import ExitCelestialModal from "@/components/modals/prestige/ExitCelestialModal";
import ExitDilationModal from "@/components/modals/prestige/ExitDilationModal";
import HardResetModal from "@/components/modals/prestige/HardResetModal";
import RealityModal from "@/components/modals/prestige/RealityModal";
import ReplicantiGalaxyModal from "@/components/modals/prestige/ReplicantiGalaxyModal";
import ResetRealityModal from "@/components/modals/prestige/ResetRealityModal";
import SpeedrunModeModal from "@/components/modals/SpeedrunModeModal";
import ConfirmationOptionsModal from "@/components/modals/options/ConfirmationOptionsModal";
import InfoDisplayOptionsModal from "@/components/modals/options/InfoDisplayOptionsModal";
import AwayProgressOptionsModal from "@/components/modals/options/AwayProgressOptionsModal";
import HotkeysModal from "@/components/modals/options/HotkeysModal";
import NewsOptionsModal from "@/components/modals/options/NewsOptionsModal";
import AnimationOptionsModal from "@/components/modals/options/AnimationOptionsModal";
import PreferredTreeModal from "@/components/modals/options/PreferredTreeModal";
import AwayProgressOptionsModal from "@/components/modals/options/AwayProgressOptionsModal";
import ConfirmationOptionsModal from "@/components/modals/options/ConfirmationOptionsModal";
import GlyphDisplayOptionsModal from "@/components/modals/options/GlyphDisplayOptionsModal";
import HiddenTabsModal from "@/components/modals/options/hidden-tabs/HiddenTabsModal";
import HotkeysModal from "@/components/modals/options/HotkeysModal";
import InfoDisplayOptionsModal from "@/components/modals/options/InfoDisplayOptionsModal";
import NewsOptionsModal from "@/components/modals/options/NewsOptionsModal";
import PreferredTreeModal from "@/components/modals/options/PreferredTreeModal";
import DeleteCompanionGlyphModal from "@/components/modals/glyph-management/DeleteCompanionGlyphModal";
import DeleteGlyphModal from "@/components/modals/glyph-management/DeleteGlyphModal";
import PurgeGlyphModal from "@/components/modals/glyph-management/PurgeGlyphModal";
import SacrificeGlyphModal from "@/components/modals/glyph-management/SacrificeGlyphModal";
import RefineGlyphModal from "@/components/modals/glyph-management/RefineGlyphModal";
import PurgeAllUnprotectedGlyphsModal from "@/components/modals/glyph-management/PurgeAllUnprotectedGlyphsModal";
import PurgeAllRejectedGlyphsModal from "@/components/modals/glyph-management/PurgeAllRejectedGlyphsModal";
import PurgeAllUnprotectedGlyphsModal from "@/components/modals/glyph-management/PurgeAllUnprotectedGlyphsModal";
import PurgeGlyphModal from "@/components/modals/glyph-management/PurgeGlyphModal";
import RefineGlyphModal from "@/components/modals/glyph-management/RefineGlyphModal";
import SacrificeGlyphModal from "@/components/modals/glyph-management/SacrificeGlyphModal";
import H2PModal from "@/components/modals/H2PModal";
import InformationModal from "@/components/modals/InformationModal";
import GlyphShowcasePanelModal from "@/components/modals/GlyphShowcasePanelModal";
import UndoGlyphModal from "@/components/modals/UndoGlyphModal";
import ReplaceGlyphModal from "@/components/modals/ReplaceGlyphModal";
import UiChoiceModal from "@/components/modals/UiChoiceModal";
import AwayProgressModal from "@/components/modals/AwayProgressModal";
import LoadGameModal from "@/components/modals/LoadGameModal";
import ImportSaveModal from "@/components/modals/ImportSaveModal";
import ImportAutomatorScriptModal from "@/components/modals/ImportAutomatorScriptModal";
import DeleteAutomatorScriptModal from "@/components/modals/DeleteAutomatorScriptModal";
import AutomatorScriptTemplate from "@/components/modals/AutomatorScriptTemplate";
import AwayProgressModal from "@/components/modals/AwayProgressModal";
import BreakInfinityModal from "@/components/modals/BreakInfinityModal";
import CatchupModal from "@/components/modals/catchup/CatchupModal";
import ChangelogModal from "@/components/modals/ChangelogModal";
import CreditsModal from "@/components/modals/CreditsModal";
import DeleteAutomatorScriptModal from "@/components/modals/DeleteAutomatorScriptModal";
import EnslavedHintsModal from "@/components/modals/EnslavedHintsModal";
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";
import PelleEffectsModal from "@/components/modals/PelleEffectsModal";
import RealityGlyphCreationModal from "@/components/modals/RealityGlyphCreationModal";
import ReplaceGlyphModal from "@/components/modals/ReplaceGlyphModal";
import RespecIAPModal from "@/components/modals/RespecIAPModal";
import SacrificeModal from "@/components/modals/SacrificeModal";
import SingularityMilestonesModal from "@/components/modals/SingularityMilestonesModal";
import StdStoreModal from "@/components/modals/StdStoreModal";
import StudyStringModal from "@/components/modals/StudyStringModal";
import SacrificeModal from "@/components/modals/SacrificeModal";
import BreakInfinityModal from "@/components/modals/BreakInfinityModal";
import GlyphSetSaveDeleteModal from "@/components/modals/GlyphSetSaveDeleteModal";
import RealityGlyphCreationModal from "@/components/modals/RealityGlyphCreationModal";
import EnslavedHintsModal from "@/components/modals/EnslavedHintsModal";
import SingularityMilestonesModal from "@/components/modals/SingularityMilestonesModal";
import PelleEffectsModal from "@/components/modals/PelleEffectsModal";
import SwitchAutomatorEditorModal from "@/components/modals/SwitchAutomatorEditorModal";
import UiChoiceModal from "@/components/modals/UiChoiceModal";
import UndoGlyphModal from "@/components/modals/UndoGlyphModal";
let nextModalID = 0;
export class Modal {
constructor(component, bare = false) {
constructor(component, priority = 0, closeEvent) {
this._component = component;
this._bare = bare;
this._modalConfig = {};
this._priority = priority;
this._closeEvent = closeEvent;
}
// We can't handle this in the Vue components because if the modal order changes, all the event listeners from the
// top modal end up getting removed from the EventHub due to the component being temporarily destroyed. This could
// result in the component sticking around because an event it was listening for happened while it wasn't on top.
applyCloseListeners(closeEvent) {
// Most of the time the close event will be a prestige event, in which case we want it to trigger on all higher
// prestiges as well
const prestigeOrder = [GAME_EVENT.DIMBOOST_AFTER, GAME_EVENT.GALAXY_RESET_AFTER, GAME_EVENT.BIG_CRUNCH_AFTER,
GAME_EVENT.ETERNITY_RESET_AFTER, GAME_EVENT.REALITY_RESET_AFTER];
let shouldClose = false;
for (const prestige of prestigeOrder) {
if (prestige === closeEvent) shouldClose = true;
if (shouldClose) EventHub.ui.on(prestige, () => this.removeFromQueue(), this._component);
}
// In a few cases we want to trigger a close based on a non-prestige event, so if the specified event wasn't in
// the prestige array above, we just add it on its own
if (!shouldClose) EventHub.ui.on(closeEvent, () => this.removeFromQueue(), this._component);
}
show(modalConfig) {
if (!GameUI.initialized) return;
this._uniqueID = nextModalID++;
this._props = Object.assign({}, modalConfig || {});
if (ui.view.modal.queue.length === 0) ui.view.modal.current = this;
// New modals go to the back of the queue (shown last).
if (!ui.view.modal.queue.includes(this)) ui.view.modal.queue.push(this);
if (this._closeEvent) this.applyCloseListeners(this._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.
modalQueue.unshift(this);
Modal.sortModalQueue();
}
get isOpen() {
@ -85,14 +116,30 @@ export class Modal {
return this._component;
}
get isBare() {
return this._bare;
}
get props() {
return this._props;
}
get priority() {
return this._priority;
}
removeFromQueue() {
EventHub.ui.offAll(this._component);
ui.view.modal.queue = ui.view.modal.queue.filter(m => m._uniqueID !== this._uniqueID);
if (ui.view.modal.queue.length === 0) ui.view.modal.current = undefined;
else ui.view.modal.current = ui.view.modal.queue[0];
}
static sortModalQueue() {
const modalQueue = ui.view.modal.queue;
modalQueue.sort((x, y) => y.priority - x.priority);
// Filter out multiple instances of the same modal.
const singleQueue = [...new Set(modalQueue)];
ui.view.modal.queue = singleQueue;
ui.view.modal.current = singleQueue[0];
}
static hide() {
if (!GameUI.initialized) return;
ui.view.modal.queue.shift();
@ -114,14 +161,19 @@ export class Modal {
}
static get isOpen() {
return ui.view.modal.current === this;
return ui.view.modal.current instanceof this;
}
}
class ChallengeConfirmationModal extends Modal {
show(id) {
this.id = id;
super.show();
super.show({ id });
}
}
class TimeModal extends Modal {
show(diff) {
super.show({ diff });
}
}
@ -132,122 +184,69 @@ Modal.startEternityChallenge = new ChallengeConfirmationModal(EternityChallengeS
Modal.startInfinityChallenge = new ChallengeConfirmationModal(InfinityChallengeStartModal);
Modal.startNormalChallenge = new ChallengeConfirmationModal(NormalChallengeStartModal);
Modal.dimensionBoost = new Modal(DimensionBoostModal);
Modal.antimatterGalaxy = new Modal(AntimatterGalaxyModal);
Modal.bigCrunch = new Modal(BigCrunchModal);
Modal.replicantiGalaxy = new Modal(ReplicantiGalaxyModal);
Modal.eternity = new Modal(EternityModal);
Modal.enterDilation = new Modal(EnterDilationModal);
Modal.reality = new Modal(RealityModal);
Modal.resetReality = new Modal(ResetRealityModal);
Modal.exitCelestialReality = new Modal(ExitCelestialModal);
Modal.celestials = new Modal(EnterCelestialsModal);
Modal.hardReset = new Modal(HardResetModal);
Modal.catchup = new TimeModal(CatchupModal, -1);
Modal.dimensionBoost = new Modal(DimensionBoostModal, 1, GAME_EVENT.DIMBOOST_AFTER);
Modal.antimatterGalaxy = new Modal(AntimatterGalaxyModal, 1, GAME_EVENT.GALAXY_RESET_AFTER);
Modal.bigCrunch = new Modal(BigCrunchModal, 1, GAME_EVENT.BIG_CRUNCH_AFTER);
Modal.replicantiGalaxy = new Modal(ReplicantiGalaxyModal, 1, GAME_EVENT.ETERNITY_RESET_AFTER);
Modal.eternity = new Modal(EternityModal, 1, GAME_EVENT.ETERNITY_RESET_AFTER);
Modal.enterDilation = new Modal(EnterDilationModal, 1, GAME_EVENT.REALITY_RESET_AFTER);
Modal.exitDilation = new Modal(ExitDilationModal, 1, GAME_EVENT.REALITY_RESET_AFTER);
Modal.reality = new Modal(RealityModal, 1, GAME_EVENT.REALITY_RESET_AFTER);
Modal.resetReality = new Modal(ResetRealityModal, 1, GAME_EVENT.REALITY_RESET_AFTER);
Modal.exitCelestialReality = new Modal(ExitCelestialModal, 1, GAME_EVENT.REALITY_RESET_AFTER);
Modal.celestials = new Modal(EnterCelestialsModal, 1);
Modal.hardReset = new Modal(HardResetModal, 1);
Modal.enterSpeedrun = new Modal(SpeedrunModeModal);
Modal.changeName = new Modal(ChangeNameModal);
Modal.armageddon = new Modal(ArmageddonModal);
Modal.armageddon = new Modal(ArmageddonModal, 1);
Modal.confirmationOptions = new Modal(ConfirmationOptionsModal);
Modal.infoDisplayOptions = new Modal(InfoDisplayOptionsModal);
Modal.awayProgressOptions = new Modal(AwayProgressOptionsModal);
Modal.glyphDisplayOptions = new Modal(GlyphDisplayOptionsModal);
Modal.hotkeys = new Modal(HotkeysModal);
Modal.newsOptions = new Modal(NewsOptionsModal);
Modal.animationOptions = new Modal(AnimationOptionsModal);
Modal.hiddenTabs = new Modal(HiddenTabsModal);
Modal.preferredTree = new Modal(PreferredTreeModal);
Modal.deleteCompanion = new Modal(DeleteCompanionGlyphModal);
Modal.glyphDelete = new Modal(DeleteGlyphModal);
Modal.glyphPurge = new Modal(PurgeGlyphModal);
Modal.glyphSacrifice = new Modal(SacrificeGlyphModal);
Modal.glyphRefine = new Modal(RefineGlyphModal);
Modal.deleteAllUnprotectedGlyphs = new Modal(PurgeAllUnprotectedGlyphsModal);
Modal.deleteAllRejectedGlyphs = new Modal(PurgeAllRejectedGlyphsModal);
Modal.deleteCompanion = new Modal(DeleteCompanionGlyphModal, 1);
Modal.glyphDelete = new Modal(DeleteGlyphModal, 1, GAME_EVENT.GLYPHS_CHANGED);
Modal.glyphPurge = new Modal(PurgeGlyphModal, 1, GAME_EVENT.GLYPHS_CHANGED);
Modal.glyphSacrifice = new Modal(SacrificeGlyphModal, 1, GAME_EVENT.GLYPHS_CHANGED);
Modal.glyphRefine = new Modal(RefineGlyphModal, 1, GAME_EVENT.GLYPHS_CHANGED);
Modal.deleteAllUnprotectedGlyphs = new Modal(PurgeAllUnprotectedGlyphsModal, 1, GAME_EVENT.GLYPHS_CHANGED);
Modal.deleteAllRejectedGlyphs = new Modal(PurgeAllRejectedGlyphsModal, 1, GAME_EVENT.GLYPHS_CHANGED);
Modal.glyphShowcasePanel = new Modal(GlyphShowcasePanelModal);
Modal.glyphUndo = new Modal(UndoGlyphModal);
Modal.glyphReplace = new Modal(ReplaceGlyphModal);
Modal.glyphUndo = new Modal(UndoGlyphModal, 1, GAME_EVENT.REALITY_RESET_AFTER);
Modal.glyphReplace = new Modal(ReplaceGlyphModal, 1, GAME_EVENT.REALITY_RESET_AFTER);
Modal.enslavedHints = new Modal(EnslavedHintsModal);
Modal.realityGlyph = new Modal(RealityGlyphCreationModal);
Modal.glyphSetSaveDelete = new Modal(GlyphSetSaveDeleteModal);
Modal.uiChoice = new Modal(UiChoiceModal);
Modal.h2p = new Modal(H2PModal);
Modal.information = new Modal(InformationModal);
Modal.credits = new Modal(CreditsModal, 1);
Modal.changelog = new Modal(ChangelogModal, 1);
Modal.awayProgress = new Modal(AwayProgressModal);
Modal.loadGame = new Modal(LoadGameModal);
Modal.import = new Modal(ImportSaveModal);
Modal.importScript = new Modal(ImportAutomatorScriptModal);
Modal.importWarning = new Modal(ImportFileWarningModal);
Modal.importScriptData = new Modal(ImportAutomatorDataModal);
Modal.automatorScriptDelete = new Modal(DeleteAutomatorScriptModal);
Modal.automatorScriptTemplate = new Modal(AutomatorScriptTemplate);
Modal.switchAutomatorEditorMode = new Modal(SwitchAutomatorEditorModal);
Modal.shop = new Modal(StdStoreModal);
Modal.studyString = new Modal(StudyStringModal);
Modal.singularityMilestones = new Modal(SingularityMilestonesModal);
Modal.pelleEffects = new Modal(PelleEffectsModal);
Modal.sacrifice = new Modal(SacrificeModal);
Modal.breakInfinity = new Modal(BreakInfinityModal);
Modal.celestialQuote = new class extends Modal {
show(celestial, lines) {
if (!GameUI.initialized) return;
const newLines = lines.map(l => Modal.celestialQuote.getLineMapping(celestial, l));
if (ui.view.modal.queue.includes(this)) {
// This shouldn't come up often, but in case we do have a pile of quotes
// being shown in a row:
this.lines[this.lines.length - 1].isEndQuote = true;
this.lines.push(...newLines);
return;
}
super.show();
this.lines = newLines;
}
getLineMapping(defaultCel, defaultLine) {
let overrideCelestial = "";
let l = defaultLine;
if (typeof l === "string") {
if (l.includes("<!")) {
overrideCelestial = this.getOverrideCel(l);
l = this.removeOverrideCel(l);
}
}
return {
celestial: defaultCel,
overrideCelestial,
line: l,
showName: l[0] !== "*",
isEndQuote: false
};
}
getOverrideCel(x) {
if (x.includes("<!")) {
const start = x.indexOf("<!"), end = x.indexOf("!>");
return x.substring(start + 2, end);
}
return "";
}
removeOverrideCel(x) {
if (x.includes("<!")) {
const start = x.indexOf("<!"), end = x.indexOf("!>");
return x.substring(0, start) + x.substring(end + 2);
}
return x;
}
}(CelestialQuoteModal, true);
Modal.cloudSaveConflict = new Modal(CloudSaveConflictModal);
Modal.cloudLoadConflict = new Modal(CloudLoadConflictModal);
Modal.manualCloud = new Modal(CloudManualLoginModal);
// eslint-disable-next-line max-params
Modal.addCloudConflict = function(saveId, saveComparison, cloudSave, localSave, onAccept) {
Modal.hide();
ui.view.modal.cloudConflict = {
saveId,
saveComparison,
cloud: getSaveInfo(cloudSave),
local: getSaveInfo(localSave),
onAccept
};
Modal.sacrifice = new Modal(SacrificeModal, 1, GAME_EVENT.DIMBOOST_AFTER);
Modal.breakInfinity = new Modal(BreakInfinityModal, 1, GAME_EVENT.ETERNITY_RESET_AFTER);
Modal.respecIAP = new Modal(RespecIAPModal);
function getSaveInfo(save) {
const resources = {
@ -262,50 +261,73 @@ Modal.addCloudConflict = function(saveId, saveComparison, cloudSave, localSave,
imaginaryMachines: 0,
dilatedTime: new Decimal(0),
bestLevel: 0,
totalSTD: 0,
saveName: "",
};
resources.realTimePlayed = save.records.realTimePlayed;
resources.totalAntimatter.copyFrom(new Decimal(save.records.totalAntimatter));
// This code ends up getting run on raw save data before any migrations are applied, so we need to default to props
// which only exist on the pre-reality version when applicable. Note that new Decimal(undefined) gives zero.
resources.realTimePlayed = save.records?.realTimePlayed ?? 100 * save.totalTimePlayed;
resources.totalAntimatter.copyFrom(new Decimal(save.records?.totalAntimatter));
resources.infinities.copyFrom(new Decimal(save.infinities));
resources.eternities.copyFrom(new Decimal(save.eternities));
resources.realities = save.realities;
resources.realities = save.realities ?? 0;
resources.infinityPoints.copyFrom(new Decimal(save.infinityPoints));
resources.eternityPoints.copyFrom(new Decimal(save.eternityPoints));
resources.realityMachines.copyFrom(new Decimal(save.reality.realityMachines));
resources.imaginaryMachines = save.reality.iMCap;
resources.realityMachines.copyFrom(new Decimal(save.reality?.realityMachines));
resources.imaginaryMachines = save.reality?.iMCap ?? 0;
resources.dilatedTime.copyFrom(new Decimal(save.dilation.dilatedTime));
resources.bestLevel = save.records.bestReality.glyphLevel;
resources.bestLevel = save.records?.bestReality.glyphLevel ?? 0;
resources.totalSTD = save?.IAP?.totalSTD ?? 0;
resources.saveName = save.options.saveFileName ?? "";
return resources;
}
Modal.cloudSaveConflict = new Modal(CloudSaveConflictModal);
Modal.cloudLoadConflict = new Modal(CloudLoadConflictModal);
Modal.manualCloud = new Modal(CloudManualLoginModal);
// eslint-disable-next-line max-params
Modal.addCloudConflict = function(saveId, saveComparison, cloudSave, localSave, onAccept) {
Modal.hide();
ui.view.modal.cloudConflict = {
saveId,
saveComparison,
cloud: getSaveInfo(cloudSave),
local: getSaveInfo(localSave),
onAccept
};
};
Modal.addImportConflict = function(importingSave, currentSave) {
Modal.hide();
ui.view.modal.cloudConflict = {
importingSave: getSaveInfo(importingSave),
currentSave: getSaveInfo(currentSave)
};
};
Modal.message = new class extends Modal {
show(text, callback, closeButton = false) {
show(text, props = {}, messagePriority = 0) {
if (!GameUI.initialized) return;
// It might be zero, so explicitly check for undefined
if (this.currPriority === undefined) this.currPriority = messagePriority;
else if (messagePriority < this.currPriority) return;
super.show();
if (this.message === undefined) {
this.message = text;
this.callback = callback;
this.closeButton = closeButton;
}
if (!this.queue) this.queue = [];
this.queue.push({ text, callback, closeButton });
// Sometimes we have stacked messages that get lost, since we don't have stacking modal system.
this.callback = props.callback;
this.closeButton = props.closeButton ?? false;
EventHub.ui.offAll(this._component);
if (props.closeEvent) this.applyCloseListeners(props.closeEvent);
// TODO: remove this console.log
// eslint-disable-next-line no-console
console.log(`Modal message: ${text}`);
}
hide() {
if (this.queue.length <= 1) {
EventHub.ui.offAll(this._component);
this.currPriority = undefined;
Modal.hide();
}
this.queue.shift();
if (this.queue && this.queue.length === 0) this.message = undefined;
else {
this.message = this.queue[0].text;
this.callback = this.queue[0].callback;
this.closeButton = this.queue[0].closeButton;
}
}
}(MessageModal);
}(MessageModal, 2);

View File

@ -35,6 +35,7 @@ export const notify = (function() {
success: (text, duration) => showNotification(text, "o-notification--success", duration),
error: (text, duration) => showNotification(text, "o-notification--error", duration),
info: (text, duration) => showNotification(text, "o-notification--info", duration),
infinity: (text, duration) => showNotification(text, "o-notification--infinity", duration),
eternity: (text, duration) => showNotification(text, "o-notification--eternity", duration),
reality: (text, duration) => showNotification(text, "o-notification--reality", duration),
blackHole: (text, duration) => showNotification(text, "o-notification--black-hole", duration),

View File

@ -1,5 +1,7 @@
import { sha512_256 } from "js-sha512";
import FullScreenAnimationHandler from "../full-screen-animation-handler";
export class GameOptions {
static toggleNews() {
@ -66,10 +68,9 @@ export function isSecretImport(data) {
export function tryImportSecret(data) {
const index = secretImportIndex(data);
if (index === 0 && document.body.style.animation === "") {
document.body.style.animation = "barrelRoll 5s 1";
if (index === 0) {
FullScreenAnimationHandler.display("a-barrel-roll", 5);
SecretAchievement(15).unlock();
setTimeout(() => document.body.style.animation = "", 5000);
return true;
}
if (index === 1) {

View File

@ -70,7 +70,7 @@ Theme.secretThemeIndex = function(name) {
"ef853879b60fa6755d9599fd756c94d112f987c0cd596abf48b08f33af5ff537",
"078570d37e6ffbf06e079e07c3c7987814e03436d00a17230ef5f24b1cb93290",
"a3d64c3d1e1749b60b2b3dba10ed5ae9425300e9600ca05bcbafe4df6c69941f",
"d910565e1664748188b313768c370649230ca348cb6330fe9df73bcfa68d974d",
"530fac71cc0b151b24d966493a6f4a0817921b37e4d3e593439e624c214ab2b2",
"cb72e4a679254df5f99110dc7a93924628b916d2e069e3ad206db92068cb0883",
"c8fac64da08d674123c32c936b14115ab384fe556fd24e431eb184a8dde21137",
"da3b3c152083f0c70245f104f06331497b97b52ac80edec05e26a33ee704cae7",
@ -103,7 +103,7 @@ Theme.tryUnlock = function(name) {
Theme.set(prefix);
SecretAchievement(25).unlock();
if (!isAlreadyUnlocked) {
GameUI.notify.success(`You have unlocked the ${name.capitalize()} theme!`);
GameUI.notify.success(`You have unlocked the ${name.capitalize()} theme!`, 5000);
}
return true;
};

View File

@ -1,4 +1,3 @@
// eslint-disable-next-line prefer-const
export const state = {
view: {
modal: {
@ -7,6 +6,11 @@ export const state = {
cloudConflict: [],
progressBar: undefined,
},
quotes: {
queue: [],
current: undefined,
history: undefined
},
tabs: {
reality: {
openGlyphWeights: false,

View File

@ -1,8 +1,10 @@
import { notify } from "./notify.js";
import { state } from "./ui.init.js";
import VTooltip from "v-tooltip";
import { useLongPress, useRepeatingClick } from "./longpress";
import VueGtag from "vue-gtag";
import { useLongPress, useRepeatingClick } from "./longpress";
import { notify } from "./notify";
import { state } from "./ui.init";
import GameUIComponent from "@/components/GameUIComponent";
Vue.mixin({
@ -95,7 +97,7 @@ const ReactivityComplainer = {
throw new Error(`Boi you fukked up - ${path} became REACTIVE (oh shite)`);
}
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
const prop = obj[key];
if (typeof prop === "object") {
this.checkReactivity(prop, `${path}.${key}`);
@ -113,13 +115,13 @@ export const GameUI = {
touchDevice: ("ontouchstart" in window ||
window.navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0 ||
(window.DocumentTouch && document instanceof DocumentTouch)),
dispatch(event) {
dispatch(event, args) {
const index = this.events.indexOf(event);
if (index !== -1) {
this.events.splice(index, 1);
}
if (event !== GAME_EVENT.UPDATE) {
this.events.push(event);
this.events.push([event, args]);
}
if (this.flushPromise) return;
this.flushPromise = Promise.resolve()
@ -132,7 +134,7 @@ export const GameUI = {
PerformanceStats.start("Vue Update");
}
for (const event of this.events) {
EventHub.ui.dispatch(event);
EventHub.ui.dispatch(event[0], event[1]);
}
EventHub.ui.dispatch(GAME_EVENT.UPDATE);
ReactivityComplainer.complain();

View File

@ -1,4 +1,4 @@
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
import { Autobuyer, AutobuyerState } from "./autobuyer";
Autobuyer.annihilation = new class AnnihilationAutobuyerState extends AutobuyerState {
get data() {
@ -10,7 +10,7 @@ Autobuyer.annihilation = new class AnnihilationAutobuyerState extends AutobuyerS
}
get isUnlocked() {
return Laitela.darkMatterMult > 1;
return Laitela.darkMatterMult > 1 && !Pelle.isDoomed;
}
get multiplier() {

View File

@ -1,5 +1,6 @@
import { Autobuyer, UpgradeableAutobuyerState } from "./autobuyer.js";
import { DC } from "../constants.js";
import { DC } from "../constants";
import { Autobuyer, UpgradeableAutobuyerState } from "./autobuyer";
class AntimatterDimensionAutobuyerState extends UpgradeableAutobuyerState {
get tier() {
@ -7,7 +8,7 @@ class AntimatterDimensionAutobuyerState extends UpgradeableAutobuyerState {
}
get name() {
return AntimatterDimension(this.tier).displayName;
return AntimatterDimension(this.tier).shortDisplayName;
}
get fullName() {
@ -15,16 +16,16 @@ class AntimatterDimensionAutobuyerState extends UpgradeableAutobuyerState {
}
get data() {
return player.auto.antimatterDims[this.tier - 1];
return player.auto.antimatterDims.all[this.tier - 1];
}
get baseInterval() {
return Player.defaultStart.auto.antimatterDims[this.tier - 1].interval;
return Player.defaultStart.auto.antimatterDims.all[this.tier - 1].interval;
}
get isUnlocked() {
if (Pelle.isDisabled(`antimatterDimAutobuyer${this.tier}`)) return false;
return NormalChallenge(this.tier).isCompleted;
return this.data.isBought || this.canBeUpgraded;
}
get isBought() {
@ -39,6 +40,10 @@ class AntimatterDimensionAutobuyerState extends UpgradeableAutobuyerState {
return !Pelle.isDisabled(`antimatterDimAutobuyer${this.tier}`);
}
get canBeUpgraded() {
return NormalChallenge(this.tier).isCompleted;
}
get disabledByContinuum() {
return Laitela.continuumActive;
}
@ -113,7 +118,7 @@ class AntimatterDimensionAutobuyerState extends UpgradeableAutobuyerState {
}
get resetTickOn() {
return Perk.antimatterNoReset.isBought ? PRESTIGE_EVENT.ANTIMATTER_GALAXY : PRESTIGE_EVENT.DIMENSION_BOOST;
return Perk.antimatterNoReset.canBeApplied ? PRESTIGE_EVENT.ANTIMATTER_GALAXY : PRESTIGE_EVENT.DIMENSION_BOOST;
}
reset() {
@ -126,14 +131,17 @@ class AntimatterDimensionAutobuyerState extends UpgradeableAutobuyerState {
static get entryCount() { return 8; }
static get autobuyerGroupName() { return "Antimatter Dimension"; }
static get isActive() { return player.auto.antimatterDims.isActive; }
static set isActive(value) { player.auto.antimatterDims.isActive = value; }
static createAccessor() {
const accessor = super.createAccessor();
/** @returns {boolean} */
accessor.allBought = () => accessor.zeroIndexed.every(x => x.isBought);
/** @returns {boolean} */
Object.defineProperties(accessor, {
allBought: { get: () => accessor.zeroIndexed.every(x => x.isBought) },
// We can get away with this since allUnlimitedBulk is the same for all AD autos
accessor.allUnlimitedBulk = () => accessor.zeroIndexed[0].hasUnlimitedBulk;
accessor.bulkCap = accessor.zeroIndexed[0].bulkCap;
allUnlimitedBulk: { get: () => accessor.zeroIndexed[0].hasUnlimitedBulk },
bulkCap: { get: () => accessor.zeroIndexed[0].bulkCap }
});
return accessor;
}
}

View File

@ -19,7 +19,8 @@ export class AutobuyerState {
get id() { return this._id; }
get canTick() {
return this.isActive && player.auto.autobuyersOn && (this.isUnlocked || this.isBought);
const isDisabled = !player.auto.autobuyersOn || !this.constructor.isActive;
return this.isActive && !isDisabled && (this.isUnlocked || this.isBought);
}
get isActive() {
@ -46,7 +47,6 @@ export class AutobuyerState {
// eslint-disable-next-line no-empty-function
reset() { }
/** @returns {number} */
static get entryCount() { return 1; }
/**
@ -54,6 +54,9 @@ export class AutobuyerState {
* @returns {string}
*/
static get autobuyerGroupName() { throw new NotImplementedError(); }
static get isActive() { return true; }
/** @abstract */
static set isActive(value) { throw new NotImplementedError(); }
static createAccessor() {
const entryCount = this.entryCount;
@ -62,16 +65,20 @@ export class AutobuyerState {
const oneIndexed = [null, ...zeroIndexed];
/** @param {number} id */
const accessor = id => oneIndexed[id];
accessor.oneIndexed = oneIndexed;
accessor.zeroIndexed = zeroIndexed;
accessor.entryCount = entryCount;
accessor.groupName = this.autobuyerGroupName;
/** @returns {boolean} */
accessor.anyUnlocked = () => zeroIndexed.some(x => x.isUnlocked);
/** @returns {boolean} */
accessor.allUnlocked = () => zeroIndexed.every(x => x.isUnlocked);
/** @returns {boolean} */
accessor.allActive = () => zeroIndexed.every(x => x.isActive);
Object.defineProperties(accessor, {
oneIndexed: { get: () => oneIndexed },
zeroIndexed: { get: () => zeroIndexed },
entryCount: { get: () => entryCount },
anyUnlocked: { get: () => zeroIndexed.some(x => x.isUnlocked) },
allUnlocked: { get: () => zeroIndexed.every(x => x.isUnlocked) },
allActive: { get: () => zeroIndexed.every(x => x.isActive) },
groupName: { get: () => this.autobuyerGroupName },
isActive: {
get: () => this.isActive,
set: value => { this.isActive = value; },
},
});
accessor.toggle = () => this.isActive = !this.isActive;
return accessor;
}
}
@ -157,8 +164,12 @@ export class UpgradeableAutobuyerState extends IntervaledAutobuyerState {
static createAccessor() {
const accessor = super.createAccessor();
/** @returns {boolean} */
accessor.allMaxedInterval = () => accessor.zeroIndexed.every(x => x.hasMaxedInterval);
Object.defineProperty(accessor, "allMaxedInterval", {
get: () => accessor.zeroIndexed.every(x => x.hasMaxedInterval)
});
Object.defineProperty(accessor, "hasInstant", {
get: () => accessor.zeroIndexed.some(x => x.interval < player.options.updateRate)
});
return accessor;
}
}

View File

@ -1,4 +1,4 @@
import { Autobuyer } from "./autobuyer.js";
import { Autobuyer } from "./autobuyer";
export const Autobuyers = (function() {
const antimatterDimensions = Autobuyer.antimatterDimension.zeroIndexed;
@ -101,6 +101,6 @@ EventHub.logic.on(GAME_EVENT.REALITY_RESET_AFTER, () => Autobuyers.reset());
EventHub.logic.on(GAME_EVENT.DIMBOOST_AFTER, () => Autobuyers.resetTick(PRESTIGE_EVENT.DIMENSION_BOOST));
EventHub.logic.on(GAME_EVENT.GALAXY_RESET_AFTER, () => Autobuyers.resetTick(PRESTIGE_EVENT.ANTIMATTER_GALAXY));
EventHub.logic.on(GAME_EVENT.INFINITY_RESET_AFTER, () => Autobuyers.resetTick(PRESTIGE_EVENT.INFINITY));
EventHub.logic.on(GAME_EVENT.BIG_CRUNCH_AFTER, () => Autobuyers.resetTick(PRESTIGE_EVENT.INFINITY));
EventHub.logic.on(GAME_EVENT.ETERNITY_RESET_AFTER, () => Autobuyers.resetTick(PRESTIGE_EVENT.ETERNITY));
EventHub.logic.on(GAME_EVENT.REALITY_RESET_AFTER, () => Autobuyers.resetTick(PRESTIGE_EVENT.REALITY));

View File

@ -1,4 +1,4 @@
import { Autobuyer, UpgradeableAutobuyerState } from "./autobuyer.js";
import { Autobuyer, UpgradeableAutobuyerState } from "./autobuyer";
Autobuyer.bigCrunch = new class BigCrunchAutobuyerState extends UpgradeableAutobuyerState {
get data() {
@ -10,6 +10,12 @@ Autobuyer.bigCrunch = new class BigCrunchAutobuyerState extends UpgradeableAutob
}
get isUnlocked() {
return Pelle.isDoomed
? PelleStrikes.infinity.hasStrike
: this.canBeUpgraded;
}
get canBeUpgraded() {
return NormalChallenge(12).isCompleted;
}

View File

@ -1,8 +1,8 @@
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
import { Autobuyer, AutobuyerState } from "./autobuyer";
class BlackHolePowerAutobuyerState extends AutobuyerState {
get data() {
return player.auto.blackHolePower[this.id - 1];
return player.auto.blackHolePower.all[this.id - 1];
}
get name() {
@ -10,15 +10,22 @@ class BlackHolePowerAutobuyerState extends AutobuyerState {
}
get isUnlocked() {
return Ra.has(RA_UNLOCKS.AUTO_BLACK_HOLE_POWER);
return Ra.unlocks.blackHolePowerAutobuyers.canBeApplied;
}
get hasUnlimitedBulk() {
return true;
}
tick() {
BlackHole(this.id).powerUpgrade.purchase();
const bh = BlackHole(this.id);
while (Currency.realityMachines.gte(bh.powerUpgrade.cost)) bh.powerUpgrade.purchase();
}
static get entryCount() { return 2; }
static get autobuyerGroupName() { return "Black Hole Power"; }
static get isActive() { return player.auto.blackHolePower.isActive; }
static set isActive(value) { player.auto.blackHolePower.isActive = value; }
}
Autobuyer.blackHolePower = BlackHolePowerAutobuyerState.createAccessor();

View File

@ -1,4 +1,4 @@
import { Autobuyer, IntervaledAutobuyerState } from "./autobuyer.js";
import { Autobuyer, IntervaledAutobuyerState } from "./autobuyer";
Autobuyer.darkMatterDimsAscension =
new class DarkMatterDimensionAscensionAutobuyerState extends IntervaledAutobuyerState {
@ -11,7 +11,7 @@ new class DarkMatterDimensionAscensionAutobuyerState extends IntervaledAutobuyer
}
get isUnlocked() {
return SingularityMilestone.darkDimensionAutobuyers.isUnlocked;
return SingularityMilestone.darkDimensionAutobuyers.canBeApplied;
}
get interval() {

View File

@ -1,4 +1,4 @@
import { Autobuyer, IntervaledAutobuyerState } from "./autobuyer.js";
import { Autobuyer, IntervaledAutobuyerState } from "./autobuyer";
Autobuyer.darkMatterDims = new class DarkMatterDimensionAutobuyerState extends IntervaledAutobuyerState {
get data() {
@ -10,7 +10,7 @@ Autobuyer.darkMatterDims = new class DarkMatterDimensionAutobuyerState extends I
}
get isUnlocked() {
return SingularityMilestone.darkDimensionAutobuyers.isUnlocked;
return SingularityMilestone.darkDimensionAutobuyers.canBeApplied;
}
get interval() {

View File

@ -1,10 +1,10 @@
import { Autobuyer, IntervaledAutobuyerState } from "./autobuyer.js";
import { Autobuyer, IntervaledAutobuyerState } from "./autobuyer";
class DilationUpgradeAutobuyerState extends IntervaledAutobuyerState {
get _upgradeName() { return ["dtGain", "galaxyThreshold", "tachyonGain"][this.id - 1]; }
get data() {
return player.auto.dilationUpgrades[this.id - 1];
return player.auto.dilationUpgrades.all[this.id - 1];
}
get name() {
@ -35,6 +35,8 @@ class DilationUpgradeAutobuyerState extends IntervaledAutobuyerState {
static get entryCount() { return 3; }
static get autobuyerGroupName() { return "Dilation Upgrade"; }
static get isActive() { return player.auto.dilationUpgrades.isActive; }
static set isActive(value) { player.auto.dilationUpgrades.isActive = value; }
}
Autobuyer.dilationUpgrade = DilationUpgradeAutobuyerState.createAccessor();

View File

@ -1,4 +1,4 @@
import { Autobuyer, UpgradeableAutobuyerState } from "./autobuyer.js";
import { Autobuyer, UpgradeableAutobuyerState } from "./autobuyer";
Autobuyer.dimboost = new class DimBoostAutobuyerState extends UpgradeableAutobuyerState {
get data() {
@ -11,6 +11,10 @@ Autobuyer.dimboost = new class DimBoostAutobuyerState extends UpgradeableAutobuy
get isUnlocked() {
if (Pelle.isDisabled("dimBoostAutobuyer")) return false;
return this.canBeUpgraded;
}
get canBeUpgraded() {
return NormalChallenge(10).isCompleted;
}

View File

@ -1,4 +1,4 @@
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
import { Autobuyer, AutobuyerState } from "./autobuyer";
Autobuyer.eternity = new class EternityAutobuyerState extends AutobuyerState {
get data() {

View File

@ -1,4 +1,4 @@
import { Autobuyer, UpgradeableAutobuyerState } from "./autobuyer.js";
import { Autobuyer, UpgradeableAutobuyerState } from "./autobuyer";
Autobuyer.galaxy = new class GalaxyAutobuyerState extends UpgradeableAutobuyerState {
get data() {
@ -11,6 +11,10 @@ Autobuyer.galaxy = new class GalaxyAutobuyerState extends UpgradeableAutobuyerSt
get isUnlocked() {
if (Pelle.isDisabled("galaxyAutobuyer")) return false;
return this.canBeUpgraded;
}
get canBeUpgraded() {
return NormalChallenge(11).isCompleted;
}

View File

@ -1,4 +1,4 @@
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
import { Autobuyer, AutobuyerState } from "./autobuyer";
class ImaginaryUpgradeAutobuyerState extends AutobuyerState {
get name() {
@ -6,19 +6,26 @@ class ImaginaryUpgradeAutobuyerState extends AutobuyerState {
}
get data() {
return player.auto.imaginaryUpgrades[this.id - 1];
return player.auto.imaginaryUpgrades.all[this.id - 1];
}
get isUnlocked() {
return ImaginaryUpgrade(20).isBought;
return ImaginaryUpgrade(20).canBeApplied;
}
get hasUnlimitedBulk() {
return true;
}
tick() {
ImaginaryUpgrade(this.id).purchase();
const upg = ImaginaryUpgrade(this.id);
while (Currency.imaginaryMachines.gte(upg.cost)) upg.purchase();
}
static get entryCount() { return 10; }
static get autobuyerGroupName() { return "Imaginary Upgrade"; }
static get isActive() { return player.auto.imaginaryUpgrades.isActive; }
static set isActive(value) { player.auto.imaginaryUpgrades.isActive = value; }
}
Autobuyer.imaginaryUpgrade = ImaginaryUpgradeAutobuyerState.createAccessor();

View File

@ -1,27 +1,27 @@
import "./autobuyer.js";
import "./autobuyer";
import "./antimatter-dimension-autobuyer.js";
import "./tickspeed-autobuyer.js";
import "./dimboost-autobuyer.js";
import "./galaxy-autobuyer.js";
import "./big-crunch-autobuyer.js";
import "./sacrifice-autobuyer.js";
import "./eternity-autobuyer.js";
import "./reality-autobuyer.js";
import "./antimatter-dimension-autobuyer";
import "./tickspeed-autobuyer";
import "./dimboost-autobuyer";
import "./galaxy-autobuyer";
import "./big-crunch-autobuyer";
import "./sacrifice-autobuyer";
import "./eternity-autobuyer";
import "./reality-autobuyer";
import "./infinity-dimension-autobuyer.js";
import "./time-dimension-autobuyer.js";
import "./time-theorem-autobuyer.js";
import "./black-hole-power-autobuyer.js";
import "./reality-upgrade-autobuyer.js";
import "./imaginary-upgrade-autobuyer.js";
import "./replicanti-upgrade-autobuyer.js";
import "./dilation-upgrade-autobuyer.js";
import "./prestige-currency-multiplier-autobuyer.js";
import "./replicanti-galaxy-autobuyer.js";
import "./dark-matter-dimension-autobuyer.js";
import "./dark-matter-dimension-ascension-autobuyer.js";
import "./singularity-autobuyer.js";
import "./annihilation-autobuyer.js";
import "./infinity-dimension-autobuyer";
import "./time-dimension-autobuyer";
import "./time-theorem-autobuyer";
import "./black-hole-power-autobuyer";
import "./reality-upgrade-autobuyer";
import "./imaginary-upgrade-autobuyer";
import "./replicanti-upgrade-autobuyer";
import "./dilation-upgrade-autobuyer";
import "./prestige-currency-multiplier-autobuyer";
import "./replicanti-galaxy-autobuyer";
import "./dark-matter-dimension-autobuyer";
import "./dark-matter-dimension-ascension-autobuyer";
import "./singularity-autobuyer";
import "./annihilation-autobuyer";
export * from "./autobuyers.js";
export * from "./autobuyers";

View File

@ -1,5 +1,6 @@
import { InfinityDimensions } from "../globals.js";
import { Autobuyer, IntervaledAutobuyerState } from "./autobuyer.js";
import { InfinityDimensions } from "../globals";
import { Autobuyer, IntervaledAutobuyerState } from "./autobuyer";
class InfinityDimensionAutobuyerState extends IntervaledAutobuyerState {
get tier() {
@ -11,7 +12,7 @@ class InfinityDimensionAutobuyerState extends IntervaledAutobuyerState {
}
get name() {
return this.dimension.displayName;
return this.dimension.shortDisplayName;
}
get fullName() {
@ -19,7 +20,7 @@ class InfinityDimensionAutobuyerState extends IntervaledAutobuyerState {
}
get data() {
return player.auto.infinityDims[this.tier - 1];
return player.auto.infinityDims.all[this.tier - 1];
}
get interval() {
@ -27,7 +28,7 @@ class InfinityDimensionAutobuyerState extends IntervaledAutobuyerState {
}
get isUnlocked() {
return EternityMilestone.autobuyerID(this.tier).isReached || PelleUpgrade.IDAutobuyers.canBeApplied;
return EternityMilestone[`autobuyerID${this.tier}`].isReached || PelleUpgrade.IDAutobuyers.canBeApplied;
}
get resetTickOn() {
@ -49,6 +50,8 @@ class InfinityDimensionAutobuyerState extends IntervaledAutobuyerState {
static get entryCount() { return 8; }
static get autobuyerGroupName() { return "Infinity Dimension"; }
static get isActive() { return player.auto.infinityDims.isActive; }
static set isActive(value) { player.auto.infinityDims.isActive = value; }
}
Autobuyer.infinityDimension = InfinityDimensionAutobuyerState.createAccessor();

View File

@ -1,4 +1,4 @@
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
import { Autobuyer, AutobuyerState } from "./autobuyer";
Autobuyer.ipMult = new class IPMultAutobuyerState extends AutobuyerState {
get data() {

View File

@ -1,4 +1,4 @@
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
import { Autobuyer, AutobuyerState } from "./autobuyer";
Autobuyer.reality = new class RealityAutobuyerState extends AutobuyerState {
get data() {

View File

@ -1,4 +1,4 @@
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
import { Autobuyer, AutobuyerState } from "./autobuyer";
class RealityUpgradeAutobuyerState extends AutobuyerState {
get name() {
@ -6,19 +6,26 @@ class RealityUpgradeAutobuyerState extends AutobuyerState {
}
get data() {
return player.auto.realityUpgrades[this.id - 1];
return player.auto.realityUpgrades.all[this.id - 1];
}
get isUnlocked() {
return Ra.has(RA_UNLOCKS.AUTO_RU_AND_INSTANT_EC);
return Ra.unlocks.instantECAndRealityUpgradeAutobuyers.canBeApplied;
}
get hasUnlimitedBulk() {
return true;
}
tick() {
RealityUpgrade(this.id).purchase();
const upg = RealityUpgrade(this.id);
while (Currency.realityMachines.gte(upg.cost)) upg.purchase();
}
static get entryCount() { return 5; }
static get autobuyerGroupName() { return "Reality Upgrade"; }
static get isActive() { return player.auto.realityUpgrades.isActive; }
static set isActive(value) { player.auto.realityUpgrades.isActive = value; }
}
Autobuyer.realityUpgrade = RealityUpgradeAutobuyerState.createAccessor();

View File

@ -1,4 +1,4 @@
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
import { Autobuyer, AutobuyerState } from "./autobuyer";
Autobuyer.replicantiGalaxy = new class ReplicantiGalaxyAutobuyerState extends AutobuyerState {
get data() {

View File

@ -1,4 +1,4 @@
import { Autobuyer, IntervaledAutobuyerState } from "./autobuyer.js";
import { Autobuyer, IntervaledAutobuyerState } from "./autobuyer";
class ReplicantiUpgradeAutobuyerState extends IntervaledAutobuyerState {
get _upgradeName() { return ["chance", "interval", "galaxies"][this.id - 1]; }
@ -8,7 +8,7 @@ class ReplicantiUpgradeAutobuyerState extends IntervaledAutobuyerState {
}
get data() {
return player.auto.replicantiUpgrades[this.id - 1];
return player.auto.replicantiUpgrades.all[this.id - 1];
}
get interval() {
@ -36,6 +36,8 @@ class ReplicantiUpgradeAutobuyerState extends IntervaledAutobuyerState {
static get entryCount() { return 3; }
static get autobuyerGroupName() { return "Replicanti Upgrade"; }
static get isActive() { return player.auto.replicantiUpgrades.isActive; }
static set isActive(value) { player.auto.replicantiUpgrades.isActive = value; }
}
Autobuyer.replicantiUpgrade = ReplicantiUpgradeAutobuyerState.createAccessor();

View File

@ -1,4 +1,4 @@
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
import { Autobuyer, AutobuyerState } from "./autobuyer";
Autobuyer.sacrifice = new class SacrificeAutobuyerState extends AutobuyerState {
get data() {

View File

@ -1,4 +1,4 @@
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
import { Autobuyer, AutobuyerState } from "./autobuyer";
Autobuyer.singularity = new class SingularityAutobuyerState extends AutobuyerState {
get data() {
@ -10,7 +10,7 @@ Autobuyer.singularity = new class SingularityAutobuyerState extends AutobuyerSta
}
get isUnlocked() {
return SingularityMilestone.autoCondense.isUnlocked;
return SingularityMilestone.autoCondense.canBeApplied;
}
get bulk() {

View File

@ -1,5 +1,6 @@
import { Autobuyer, UpgradeableAutobuyerState } from "./autobuyer.js";
import { DC } from "../constants.js";
import { DC } from "../constants";
import { Autobuyer, UpgradeableAutobuyerState } from "./autobuyer";
Autobuyer.tickspeed = new class TickspeedAutobuyerState extends UpgradeableAutobuyerState {
get data() {
@ -12,6 +13,10 @@ Autobuyer.tickspeed = new class TickspeedAutobuyerState extends UpgradeableAutob
get isUnlocked() {
if (Pelle.isDisabled("tickspeedAutobuyer")) return false;
return this.canBeUpgraded;
}
get canBeUpgraded() {
return NormalChallenge(9).isCompleted;
}
@ -81,7 +86,7 @@ Autobuyer.tickspeed = new class TickspeedAutobuyerState extends UpgradeableAutob
}
get resetTickOn() {
return Perk.antimatterNoReset.isBought ? PRESTIGE_EVENT.ANTIMATTER_GALAXY : PRESTIGE_EVENT.DIMENSION_BOOST;
return Perk.antimatterNoReset.canBeApplied ? PRESTIGE_EVENT.ANTIMATTER_GALAXY : PRESTIGE_EVENT.DIMENSION_BOOST;
}
reset() {

View File

@ -1,4 +1,4 @@
import { Autobuyer, IntervaledAutobuyerState } from "./autobuyer.js";
import { Autobuyer, IntervaledAutobuyerState } from "./autobuyer";
class TimeDimensionAutobuyerState extends IntervaledAutobuyerState {
get tier() {
@ -6,7 +6,7 @@ class TimeDimensionAutobuyerState extends IntervaledAutobuyerState {
}
get name() {
return TimeDimension(this.tier).displayName;
return TimeDimension(this.tier).shortDisplayName;
}
get fullName() {
@ -14,7 +14,7 @@ class TimeDimensionAutobuyerState extends IntervaledAutobuyerState {
}
get data() {
return player.auto.timeDims[this.tier - 1];
return player.auto.timeDims.all[this.tier - 1];
}
get interval() {
@ -46,6 +46,8 @@ class TimeDimensionAutobuyerState extends IntervaledAutobuyerState {
static get entryCount() { return 8; }
static get autobuyerGroupName() { return "Time Dimension"; }
static get isActive() { return player.auto.timeDims.isActive; }
static set isActive(value) { player.auto.timeDims.isActive = value; }
}
Autobuyer.timeDimension = TimeDimensionAutobuyerState.createAccessor();

View File

@ -1,4 +1,4 @@
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
import { Autobuyer, AutobuyerState } from "./autobuyer";
Autobuyer.timeTheorem = new class TimeTheoremAutobuyerState extends AutobuyerState {
get data() {
@ -14,7 +14,7 @@ Autobuyer.timeTheorem = new class TimeTheoremAutobuyerState extends AutobuyerSta
}
get hasUnlimitedBulk() {
return Perk.ttBuyMax.isBought;
return Perk.ttBuyMax.canBeApplied;
}
tick() {

View File

@ -1,3 +1,5 @@
import { AutomatorPanels } from "../../../src/components/tabs/automator/AutomatorDocs";
/** @abstract */
class AutomatorCommandInterface {
constructor(id) {
@ -21,6 +23,7 @@ export const AUTOMATOR_COMMAND_STATUS = Object.freeze({
NEXT_TICK_NEXT_INSTRUCTION: 2,
// This is used to handle some special cases, like branches/loops:
SAME_INSTRUCTION: 3,
SKIP_INSTRUCTION: 4,
});
export const AUTOMATOR_MODE = Object.freeze({
@ -111,7 +114,6 @@ class AutomatorStackEntry {
export class AutomatorScript {
constructor(id) {
if (!id) throw new Error("Invalid Automator script ID");
this._id = id;
this.compile();
}
@ -141,7 +143,7 @@ export class AutomatorScript {
}
save(content) {
this.persistent.content = content;
if (AutomatorData.isWithinLimit()) this.persistent.content = content;
this.compile();
}
@ -149,14 +151,16 @@ export class AutomatorScript {
this._compiled = AutomatorGrammar.compile(this.text).compiled;
}
static create(name) {
let id = Object.keys(player.reality.automator.scripts).length + 1;
static create(name, content = "") {
const scripts = Object.keys(player.reality.automator.scripts);
const missingIndex = scripts.findIndex((x, y) => y + 1 !== Number(x));
let id = 1 + (missingIndex === -1 ? scripts.length : missingIndex);
// On a fresh save, this executes before player is properly initialized
if (!player.reality.automator.scripts || id === 0) id = 1;
player.reality.automator.scripts[id] = {
id,
name,
content: "",
content,
};
return new AutomatorScript(id);
}
@ -171,36 +175,54 @@ export const AutomatorData = {
lastEvent: 0,
eventLog: [],
isEditorFullscreen: false,
needsRecompile: true,
cachedErrors: 0,
// This is to hold finished script templates as text in order to make the custom blocks for blockmato
blockTemplates: [],
MAX_ALLOWED_SCRIPT_CHARACTERS: 10000,
MAX_ALLOWED_TOTAL_CHARACTERS: 60000,
MAX_ALLOWED_SCRIPT_NAME_LENGTH: 15,
MAX_ALLOWED_SCRIPT_COUNT: 20,
MAX_ALLOWED_CONSTANT_NAME_LENGTH: 20,
// Note that a study string with ALL studies in unshortened form without duplicated studies is ~230 characters
MAX_ALLOWED_CONSTANT_VALUE_LENGTH: 250,
MAX_ALLOWED_CONSTANT_COUNT: 30,
scriptIndex() {
return player.reality.automator.state.editorScript;
},
currentScriptName() {
return player.reality.automator.scripts[this.scriptIndex()].name;
},
currentScriptText() {
return player.reality.automator.scripts[this.scriptIndex()].content;
currentScriptText(index) {
const toCheck = index || this.scriptIndex();
return player.reality.automator.scripts[toCheck]?.content;
},
createNewScript(newScript, name) {
const newScriptID = Object.values(player.reality.automator.scripts).length + 1;
player.reality.automator.scripts[newScriptID] = {
id: `${newScriptID}`,
name,
content: newScript
};
createNewScript(content, name) {
const newScript = AutomatorScript.create(name, content);
GameUI.notify.info(`Imported Script "${name}"`);
player.reality.automator.state.editorScript = newScriptID;
player.reality.automator.state.editorScript = newScript.id;
EventHub.dispatch(GAME_EVENT.AUTOMATOR_SAVE_CHANGED);
},
currentErrors(script) {
const toCheck = script || this.currentScriptText();
return AutomatorGrammar.compile(toCheck).errors;
recalculateErrors() {
const toCheck = this.currentScriptText();
this.cachedErrors = AutomatorGrammar.compile(toCheck).errors;
this.cachedErrors.sort((a, b) => a.startLine - b.startLine);
},
currentErrors() {
if (this.needsRecompile) {
this.recalculateErrors();
this.needsRecompile = false;
}
return this.cachedErrors;
},
logCommandEvent(message, line) {
const currTime = Date.now();
this.eventLog.push({
// Messages often overflow the 120 col limit and extra spacing gets included in the message - remove it
message: message.replaceAll(/\s?\n\s+/gu, " "),
line,
line: AutomatorBackend.translateLineNumber(line),
thisReality: Time.thisRealityRealTime.totalSeconds,
timestamp: currTime,
timegap: currTime - this.lastEvent
@ -212,11 +234,118 @@ export const AutomatorData = {
clearEventLog() {
this.eventLog = [];
this.lastEvent = 0;
},
// We need to get the current character count from the editor itself instead of the player object, because otherwise
// any changes made after getting above either limit will never be saved. Note that if the player is on the automator
// subtab before the automator is unlocked, editor is undefined
singleScriptCharacters() {
return player.reality.automator.type === AUTOMATOR_TYPE.TEXT
? AutomatorTextUI.editor?.getDoc().getValue().length ?? 0
: BlockAutomator.parseLines(BlockAutomator.lines).join("\n").length;
},
totalScriptCharacters() {
return Object.values(player.reality.automator.scripts)
.filter(s => s.id !== this.scriptIndex())
.map(s => s.content.length)
.reduce((sum, len) => sum + len, 0) +
this.singleScriptCharacters();
},
isWithinLimit() {
return this.singleScriptCharacters() <= this.MAX_ALLOWED_SCRIPT_CHARACTERS &&
this.totalScriptCharacters() <= this.MAX_ALLOWED_TOTAL_CHARACTERS;
},
};
export const LineEnum = { Active: "active", Event: "event", Error: "error" };
// Manages line highlighting in a way which is agnostic to the current editor mode (line or block). Ironically this is
// actually easier to manage in block mode as the Vue components render each line individually and we can just
// conditionally add classes in the template. The highlighting in text mode needs to be spliced and removed inline
// within the CodeMirror editor
export const AutomatorHighlighter = {
lines: {
active: -1,
event: -1,
error: -1,
},
updateHighlightedLine(line, key) {
if (player.reality.automator.type === AUTOMATOR_TYPE.TEXT && line !== -1) {
if (!AutomatorTextUI.editor) return;
this.removeHighlightedTextLine(key);
this.addHighlightedTextLine(line, key);
} else {
this.lines[key] = line;
}
},
// We need to specifically remove the highlighting class from the old line before splicing it in for the new line
removeHighlightedTextLine(key) {
const removedLine = this.lines[key] - 1;
AutomatorTextUI.editor.removeLineClass(removedLine, "background", `c-automator-editor__${key}-line`);
AutomatorTextUI.editor.removeLineClass(removedLine, "gutter", `c-automator-editor__${key}-line-gutter`);
this.lines[key] = -1;
},
addHighlightedTextLine(line, key) {
AutomatorTextUI.editor.addLineClass(line - 1, "background", `c-automator-editor__${key}-line`);
AutomatorTextUI.editor.addLineClass(line - 1, "gutter", `c-automator-editor__${key}-line-gutter`);
this.lines[key] = line;
},
clearAllHighlightedLines() {
for (const lineType of Object.values(LineEnum)) {
if (player.reality.automator.type === AUTOMATOR_TYPE.TEXT && AutomatorTextUI.editor) {
for (let line = 0; line < AutomatorTextUI.editor.doc.size; line++) {
AutomatorTextUI.editor.removeLineClass(line, "background", `c-automator-editor__${lineType}-line`);
AutomatorTextUI.editor.removeLineClass(line, "gutter", `c-automator-editor__${lineType}-line-gutter`);
}
}
this.lines[lineType] = -1;
}
}
};
// Manages line highlighting in a way which is agnostic to the current editor mode (line or block)
export const AutomatorScroller = {
// Block editor counts lines differently due to modified loop structure; this method handles that internally
scrollToRawLine(line) {
const targetLine = player.reality.automator.type === AUTOMATOR_TYPE.TEXT
? line
: AutomatorBackend.translateLineNumber(line);
this.scrollToLine(targetLine);
},
scrollToLine(line) {
let editor, textHeight, lineToScroll;
if (player.reality.automator.type === AUTOMATOR_TYPE.TEXT) {
// We can't use CodeMirror's scrollIntoView() method as that forces the entire viewport to keep the line in view.
// This can potentially cause a softlock with "follow execution" enabled on sufficiently short screens.
editor = document.querySelector(".CodeMirror-scroll");
textHeight = AutomatorTextUI.editor.defaultTextHeight();
lineToScroll = line + 1;
} else {
editor = BlockAutomator.editor;
textHeight = 34.5;
lineToScroll = line;
}
// In both cases we might potentially try to scroll before the editor has properly initialized (ie. the automator
// itself ends up loading up faster than the editor UI element)
if (!editor) return;
const paddedHeight = editor.clientHeight - 40;
const newScrollPos = textHeight * (lineToScroll - 1);
if (newScrollPos > editor.scrollTop + paddedHeight) editor.scrollTo(0, newScrollPos - paddedHeight);
if (newScrollPos < editor.scrollTop) editor.scrollTo(0, newScrollPos);
if (player.reality.automator.type === AUTOMATOR_TYPE.BLOCK) {
BlockAutomator.gutter.style.bottom = `${editor.scrollTop}px`;
}
}
};
export const AutomatorBackend = {
MAX_COMMANDS_PER_UPDATE: 100,
hasJustCompleted: false,
_scripts: [],
get state() {
@ -243,20 +372,292 @@ export const AutomatorBackend = {
return this.isOn && this.mode === AUTOMATOR_MODE.RUN;
},
findRawScriptObject(id) {
const scripts = player.reality.automator.scripts;
const index = Object.values(scripts).findIndex(s => s.id === id);
return scripts[parseInt(Object.keys(scripts)[index], 10)];
},
get currentRunningScript() {
return this.findRawScriptObject(this.state.topLevelScript);
},
get currentEditingScript() {
return this.findRawScriptObject(player.reality.automator.state.editorScript);
},
get scriptName() {
return this.findScript(this.state.topLevelScript).name;
return this.currentRunningScript?.name ?? "";
},
hasDuplicateName(name) {
const nameArray = Object.values(player.reality.automator.scripts).map(s => s.name);
return nameArray.filter(n => n === name).length > 1;
},
// Scripts are internally stored and run as text, but block mode has a different layout for loops that
// shifts a lot of commands around. Therefore we need to conditionally change it based on mode in order
// to make sure the player is presented with the correct line number
translateLineNumber(num) {
if (player.reality.automator.type === AUTOMATOR_TYPE.TEXT) return num;
return BlockAutomator.lineNumber(num);
},
get currentLineNumber() {
if (this.stack.top === null)
return -1;
return this.stack.top.lineNumber;
if (!this.stack.top) return -1;
return this.translateLineNumber(this.stack.top.lineNumber);
},
get currentInterval() {
return Math.clampMin(Math.pow(0.994, Currency.realities.value) * 500, 1);
},
get currentRawText() {
return this.currentRunningScript?.content ?? "";
},
get currentScriptLength() {
return this.currentRawText.split("\n").length;
},
// Finds which study presets are referenced within the specified script
getUsedPresets(scriptID) {
const script = this.findRawScriptObject(scriptID);
if (!script) return null;
const foundPresets = new Set();
const lines = script.content.split("\n");
for (const rawLine of lines) {
const matchPresetID = rawLine.match(/studies( nowait)? load id ([1-6])/ui);
if (matchPresetID) foundPresets.add(Number(matchPresetID[2]) - 1);
const matchPresetName = rawLine.match(/studies( nowait)? load name (\S+)/ui);
if (matchPresetName) {
// A script might pass the regex match, but actually be referencing a preset which doesn't exist by name
const presetID = player.timestudy.presets.findIndex(p => p.name === matchPresetName[2]);
if (presetID !== -1) foundPresets.add(presetID);
}
}
const presets = Array.from(foundPresets);
presets.sort();
return presets;
},
// Finds which constants are referenced within the specified script
getUsedConstants(scriptID) {
const script = this.findRawScriptObject(scriptID);
if (!script) return null;
const foundConstants = new Set();
const lines = script.content.split("\n");
for (const rawLine of lines) {
const availableConstants = Object.keys(player.reality.automator.constants);
// Needs a space-padded regex match so that (for example) a constant "unl" doesn't match to an unlock command
// Additionally we need a negative lookbehind in order to ignore matches with presets which have the same name
for (const key of availableConstants) {
if (rawLine.match(`(?<![Nn][Aa][Mm][Ee])\\s${key}(\\s|$)`)) foundConstants.add(key);
}
}
const constants = Array.from(foundConstants);
constants.sort();
return constants;
},
// We can't just concatenate different parts of script data together or use some kind of delimiting character string
// due to the fact that comments can essentially contain character sequences with nearly arbitrary content and
// length. Instead, we take the approach of concatenating all data together with their lengths prepended at the start
// of each respective data string. For example:
// ["blob", "11,21,31"] => "00004blob0000811,21,31"
// Note that the whole string can be unambiguously parsed from left-to-right regardless of the actual data contents.
// All numerical values are assumed to be exactly 5 characters long for consistency and since the script length limit
// is 5 digits long.
serializeAutomatorData(dataArray) {
const paddedNumber = num => `0000${num}`.slice(-5);
const segments = [];
for (const data of dataArray) {
segments.push(`${paddedNumber(data.length)}${data}`);
}
return segments.join("");
},
// Inverse of the operation performed by serializeAutomatorData(). Can throw an error for malformed inputs, but this
// will always be caught farther up the call chain and interpreted properly as an invalid dataString.
deserializeAutomatorData(dataString) {
if (dataString === "") throw new Error("Attempted deserialization of empty string");
const dataArray = [];
let remainingData = dataString;
while (remainingData.length > 0) {
const segmentLength = Number(remainingData.slice(0, 5));
remainingData = remainingData.substr(5);
if (Number.isNaN(segmentLength) || remainingData.length < segmentLength) {
throw new Error("Inconsistent or malformed serialized automator data");
} else {
const segmentData = remainingData.slice(0, segmentLength);
remainingData = remainingData.substr(segmentLength);
dataArray.push(segmentData);
}
}
return dataArray;
},
// This exports only the text contents of the currently-visible script
exportCurrentScriptContents() {
// Cut off leading and trailing whitespace
const trimmed = AutomatorData.currentScriptText().replace(/^\s*(.*?)\s*$/u, "$1");
if (trimmed.length === 0) return null;
// Serialize the script name and content
const name = AutomatorData.currentScriptName();
return GameSaveSerializer.encodeText(this.serializeAutomatorData([name, trimmed]), "automator script");
},
// This parses script content from an encoded export string; does not actually import anything
parseScriptContents(rawInput) {
let decoded, parts;
try {
decoded = GameSaveSerializer.decodeText(rawInput, "automator script");
parts = this.deserializeAutomatorData(decoded);
} catch (e) {
// TODO Remove everything but "return null" in this catch block before release; this is only here to maintain
// compatability with scripts from older test versions and will never be called on scripts exported post-release
if (decoded) {
parts = decoded.split("||");
if (parts.length === 3 && parts[1].length === parseInt(parts[0], 10)) {
return {
name: parts[1],
content: parts[2],
};
}
}
return null;
}
return {
name: parts[0],
content: parts[1],
};
},
// Creates a new script from the supplied import string
importScriptContents(rawInput) {
const parsed = this.parseScriptContents(rawInput);
AutomatorData.createNewScript(parsed.content, parsed.name);
this.initializeFromSave();
},
// This exports the selected script along with any constants and study presets it uses or references
exportFullScriptData(scriptID) {
const script = this.findRawScriptObject(scriptID);
const trimmed = script.content.replace(/^\s*(.*?)\s*$/u, "$1");
if (trimmed.length === 0) return null;
const foundPresets = new Set();
const foundConstants = new Set();
const lines = trimmed.split("\n");
// We find just the keys first, the rest of the associated data is serialized later
for (const rawLine of lines) {
const matchPresetID = rawLine.match(/studies( nowait)? load id ([1-6])/ui);
if (matchPresetID) foundPresets.add(Number(matchPresetID[2]) - 1);
const matchPresetName = rawLine.match(/studies( nowait)? load name (\S+)/ui);
if (matchPresetName) {
// A script might pass the regex match, but actually be referencing a preset which doesn't exist by name
const presetID = player.timestudy.presets.findIndex(p => p.name === matchPresetName[2]);
if (presetID !== -1) foundPresets.add(presetID);
}
const availableConstants = Object.keys(player.reality.automator.constants);
for (const key of availableConstants) if (rawLine.match(`\\s${key}(\\s|$)`)) foundConstants.add(key);
}
// Serialize presets
const presets = [];
for (const id of Array.from(foundPresets)) {
const preset = player.timestudy.presets[id];
presets.push(`${id}:${preset?.name ?? ""}:${preset?.studies ?? ""}`);
}
// Serialize constants
const constants = [];
for (const name of Array.from(foundConstants)) {
constants.push(`${name}:${player.reality.automator.constants[name]}`);
}
// Serialize all the variables for the full data export
const serialized = this.serializeAutomatorData([script.name, presets.join("*"), constants.join("*"), trimmed]);
return GameSaveSerializer.encodeText(serialized, "automator data");
},
// This parses scripts which also have attached information in the form of associated constants and study presets.
// Note that it doesn't actually import or assign the data to the save file at this point.
parseFullScriptData(rawInput) {
let decoded, parts;
try {
decoded = GameSaveSerializer.decodeText(rawInput, "automator data");
parts = this.deserializeAutomatorData(decoded);
} catch (e) {
return null;
}
if (parts.length !== 4) return null;
// Parse preset data (needs the conditional because otherwise it'll use the empty string to assign 0/undef/undef)
const presetData = parts[1];
const presets = [];
if (presetData) {
for (const preset of presetData.split("*")) {
const props = preset.split(":");
presets.push({
id: Number(props[0]),
name: props[1],
studies: props[2],
});
}
}
presets.sort((a, b) => a.id - b.id);
// Parse constant data
const constantData = parts[2];
const constants = [];
for (const constant of constantData.split("*")) {
if (constant === "") continue;
const props = constant.split(":");
constants.push({
key: props[0],
value: props[1],
});
}
return {
name: parts[0],
presets,
constants,
content: parts[3],
};
},
// This imports a given script, with options supplied for ignoring included presets and constants
// within the import data.
importFullScriptData(rawInput, ignore) {
const parsed = this.parseFullScriptData(rawInput);
AutomatorData.createNewScript(parsed.content, parsed.name);
if (!ignore.presets) {
for (const preset of parsed.presets) {
player.timestudy.presets[preset.id] = { name: preset.name, studies: preset.studies };
}
}
if (!ignore.constants) {
for (const constant of parsed.constants) {
const alreadyExists = player.reality.automator.constants[constant.key];
const canMakeNew = Object.keys(player.reality.automator.constants).length <
AutomatorData.MAX_ALLOWED_CONSTANT_COUNT;
if (alreadyExists || canMakeNew) {
player.reality.automator.constants[constant.key] = constant.value;
}
}
}
this.initializeFromSave();
},
update(diff) {
if (!this.isOn) return;
let stack;
@ -268,7 +669,7 @@ export const AutomatorBackend = {
stack = AutomatorBackend.stack.top;
// If single step completes the last line and repeat is off, the command stack will be empty and
// scrolling will cause an error
if (stack) AutomatorTextUI.scrollToLine(stack.lineNumber - 1);
if (stack && this.state.followExecution) AutomatorScroller.scrollToRawLine(stack.lineNumber);
this.state.mode = AUTOMATOR_MODE.PAUSE;
return;
case AUTOMATOR_MODE.RUN:
@ -291,6 +692,7 @@ export const AutomatorBackend = {
step() {
if (this.stack.isEmpty) return false;
for (let steps = 0; steps < 100 && !this.hasJustCompleted; steps++) {
switch (this.runCurrentCommand()) {
case AUTOMATOR_COMMAND_STATUS.SAME_INSTRUCTION:
return true;
@ -301,8 +703,27 @@ export const AutomatorBackend = {
case AUTOMATOR_COMMAND_STATUS.NEXT_TICK_NEXT_INSTRUCTION:
this.nextCommand();
return false;
case AUTOMATOR_COMMAND_STATUS.SKIP_INSTRUCTION:
this.nextCommand();
}
throw new Error("Unrecognized return code from command");
// We need to break out of the loop if the last commands are all SKIP_INSTRUCTION, or else it'll start
// trying to execute from an undefined stack if it isn't set to automatically repeat
if (!this.stack.top) this.hasJustCompleted = true;
}
// This should in practice never happen by accident due to it requiring 100 consecutive commands that don't do
// anything (looping a smaller group of no-ops will instead trigger the loop check every tick). Nevertheless,
// better to not have an explicit infinite loop so that the game doesn't hang if the player decides to be funny
// and input 3000 comments in a row. If hasJustCompleted is true, then we actually broke out because the end of
// the script has no-ops and we just looped through them, and therefore shouldn't show these messages
if (!this.hasJustCompleted) {
GameUI.notify.error("Automator halted - too many consecutive no-ops detected");
AutomatorData.logCommandEvent("Automator halted due to excessive no-op commands", this.currentLineNumber);
}
this.stop();
return false;
},
singleStep() {
@ -351,27 +772,24 @@ export const AutomatorBackend = {
},
findScript(id) {
// I tried really hard to convert IDs from strings into numbers for some cleanup but I just kept getting constant
// errors everywhere. It needs to be a number so that importing works properly without ID assignment being a mess,
// but apparently some deeper things seem to break in a way I can't easily fix.
return this._scripts.find(e => `${e.id}` === `${id}`);
return this._scripts.find(e => e.id === id);
},
_createDefaultScript() {
const defaultScript = AutomatorScript.create("Untitled");
const defaultScript = AutomatorScript.create("New Script");
this._scripts = [defaultScript];
this.state.topLevelScript = defaultScript.id;
return defaultScript.id;
},
initializeFromSave() {
const scriptIds = Object.keys(player.reality.automator.scripts);
const scriptIds = Object.keys(player.reality.automator.scripts).map(id => parseInt(id, 10));
if (scriptIds.length === 0) {
scriptIds.push(this._createDefaultScript());
} else {
this._scripts = scriptIds.map(s => new AutomatorScript(s));
}
if (!scriptIds.includes(`${this.state.topLevelScript}`)) this.state.topLevelScript = scriptIds[0];
if (!scriptIds.includes(this.state.topLevelScript)) this.state.topLevelScript = scriptIds[0];
const currentScript = this.findScript(this.state.topLevelScript);
if (currentScript.commands) {
const commands = currentScript.commands;
@ -382,27 +800,34 @@ export const AutomatorBackend = {
},
saveScript(id, data) {
if (!this.findScript(id)) return;
this.findScript(id).save(data);
if (id === this.state.topLevelScript) this.stop();
},
newScript() {
const newScript = AutomatorScript.create("Untitled");
const newScript = AutomatorScript.create("New Script");
this._scripts.push(newScript);
return newScript;
},
// Note that deleting scripts leaves gaps in the automator script indexing since automator scripts can't be
// dynamically re-indexed while the automator is running without causing a stutter from recompiling scripts.
deleteScript(id) {
// We need to delete scripts from two places - in the savefile and compiled AutomatorScript Objects
const saveId = Object.values(player.reality.automator.scripts).findIndex(s => s.id === id);
delete player.reality.automator.scripts[parseInt(Object.keys(player.reality.automator.scripts)[saveId], 10)];
const idx = this._scripts.findIndex(e => e.id === id);
this._scripts.splice(idx, 1);
delete player.reality.automator.scripts[id];
if (this._scripts.length === 0) {
this._createDefaultScript();
this.clearEditor();
}
if (id === this.state.topLevelScript) {
this.stop();
this.state.topLevelScript = this._scripts[0].id;
}
EventHub.dispatch(GAME_EVENT.AUTOMATOR_SAVE_CHANGED);
},
toggleRepeat() {
@ -422,7 +847,7 @@ export const AutomatorBackend = {
const state = this.state;
const focusedScript = state.topLevelScript === state.editorScript;
if (focusedScript && this.isRunning && state.followExecution) {
AutomatorTextUI.scrollToLine(AutomatorBackend.stack.top.lineNumber - 1);
AutomatorScroller.scrollToRawLine(AutomatorBackend.stack.top.lineNumber);
}
},
@ -434,6 +859,8 @@ export const AutomatorBackend = {
stop() {
this.stack.clear();
this.state.mode = AUTOMATOR_MODE.PAUSE;
this.hasJustCompleted = true;
AutomatorHighlighter.clearAllHighlightedLines();
},
pause() {
@ -441,6 +868,7 @@ export const AutomatorBackend = {
},
start(scriptID = this.state.topLevelScript, initialMode = AUTOMATOR_MODE.RUN, compile = true) {
this.hasJustCompleted = false;
this.state.topLevelScript = scriptID;
const scriptObject = this.findScript(scriptID);
if (compile) scriptObject.compile();
@ -460,6 +888,34 @@ export const AutomatorBackend = {
this.reset(this.stack._data[0].commands);
},
changeModes(scriptID) {
Tutorial.moveOn(TUTORIAL_STATE.AUTOMATOR);
if (player.reality.automator.type === AUTOMATOR_TYPE.BLOCK) {
// This saves the script after converting it.
BlockAutomator.parseTextFromBlocks();
player.reality.automator.type = AUTOMATOR_TYPE.TEXT;
if (player.reality.automator.currentInfoPane === AutomatorPanels.BLOCKS) {
player.reality.automator.currentInfoPane = AutomatorPanels.COMMANDS;
}
} else {
const toConvert = AutomatorTextUI.editor.getDoc().getValue();
// Needs to be called to update the lines prop in the BlockAutomator object
BlockAutomator.fromText(toConvert);
AutomatorBackend.saveScript(scriptID, toConvert);
player.reality.automator.type = AUTOMATOR_TYPE.BLOCK;
player.reality.automator.currentInfoPane = AutomatorPanels.BLOCKS;
}
AutomatorHighlighter.clearAllHighlightedLines();
},
clearEditor() {
if (player.reality.automator.type === AUTOMATOR_TYPE.BLOCK) {
BlockAutomator.clearEditor();
} else {
AutomatorTextUI.clearEditor();
}
},
stack: {
_data: [],
push(commands) {

View File

@ -1,12 +1,12 @@
import { AutomatorGrammar } from "./parser.js";
import { AutomatorLexer } from "./lexer.js";
import { AutomatorGrammar } from "./parser";
import { AutomatorLexer } from "./lexer";
(function() {
function walkSuggestion(suggestion, prefix, output) {
if (suggestion.$autocomplete &&
suggestion.$autocomplete.startsWith(prefix) && suggestion.$autocomplete !== prefix) {
output.add(suggestion.$autocomplete);
}
const hasAutocomplete = suggestion.$autocomplete &&
suggestion.$autocomplete.startsWith(prefix) && suggestion.$autocomplete !== prefix;
const isUnlocked = suggestion.$unlocked ? suggestion.$unlocked() : true;
if (hasAutocomplete && isUnlocked) output.add(suggestion.$autocomplete);
for (const s of suggestion.categoryMatches) {
walkSuggestion(AutomatorLexer.tokenIds[s], prefix, output);
}
@ -60,15 +60,10 @@ import { AutomatorLexer } from "./lexer.js";
{ regex: /blob\s\s/ui, token: "blob" },
{
// eslint-disable-next-line max-len
regex: /auto\s|if\s|pause\s|studies\s|tt\s|time theorems\s|until\s|wait\s|while\s|black[ \t]+hole\s|stored?[ \t]time\s|notify/ui,
regex: /(auto|if|pause|studies|time[ \t]+theorems?|until|wait|while|black[ \t]+hole|stored?[ \t]+game[ \t]+time|notify)\s/ui,
token: "keyword",
next: "commandArgs"
},
{
regex: /define\s/ui,
token: "keyword",
next: "defineIdentifier"
},
{
regex: /start\s|unlock\s/ui,
token: "keyword",
@ -85,8 +80,8 @@ import { AutomatorLexer } from "./lexer.js";
{ sol: true, next: "start" },
{ regex: /load(\s+|$)/ui, token: "variable-2", next: "studiesLoad" },
{ regex: /respec/ui, token: "variable-2", next: "commandDone" },
{ regex: /purchase/ui, token: "variable-2", next: "studiesList" },
{ regex: /nowait(\s+|$)/ui, token: "property" },
{ regex: /(?=\S)/ui, next: "studiesList" },
],
studiesList: [
commentRule,
@ -101,9 +96,15 @@ import { AutomatorLexer } from "./lexer.js";
studiesLoad: [
commentRule,
{ sol: true, next: "start" },
{ regex: /preset(\s+|$)/ui, token: "variable-2", next: "studiesLoadPreset" },
{ regex: /id(\s+|$)/ui, token: "variable-2", next: "studiesLoadId" },
{ regex: /name(\s+|$)/ui, token: "variable-2", next: "studiesLoadPreset" },
{ regex: /\S+/ui, token: "error" },
],
studiesLoadId: [
commentRule,
{ sol: true, next: "start" },
{ regex: /\d/ui, token: "qualifier", next: "commandDone" },
],
studiesLoadPreset: [
commentRule,
{ sol: true, next: "start" },
@ -122,11 +123,6 @@ import { AutomatorLexer } from "./lexer.js";
{ regex: /\}/ui, dedent: true },
{ regex: /\S+/ui, token: "error" },
],
defineIdentifier: [
commentRule,
{ sol: true, next: "start" },
{ regex: /[a-zA-Z_][a-zA-Z_0-9]*/u, token: "variable", next: "commandArgs" },
],
startUnlock: [
commentRule,
{ sol: true, next: "start" },
@ -144,7 +140,7 @@ import { AutomatorLexer } from "./lexer.js";
{ regex: /nowait(\s|$)/ui, token: "property" },
{ regex: /".*"/ui, token: "string", next: "commandDone" },
{ regex: /(on|off|dilation|load|respec)(\s|$)/ui, token: "variable-2" },
{ regex: /(preset|eternity|reality|use)(\s|$)/ui, token: "variable-2" },
{ regex: /(eternity|reality|use)(\s|$)/ui, token: "variable-2" },
{ regex: /(antimatter|infinity|time)(\s|$|(?=,))/ui, token: "variable-2" },
{ regex: /(active|passive|idle)(\s|$|(?=,))/ui, token: "variable-2" },
{ regex: /(light|dark)(\s|$|(?=,))/ui, token: "variable-2" },
@ -160,6 +156,7 @@ import { AutomatorLexer } from "./lexer.js";
{ regex: / sec(onds ?) ?| min(utes ?) ?| hours ?/ui, token: "variable-2" },
{ regex: /([0-9]+:[0-5][0-9]:[0-5][0-9]|[0-5]?[0-9]:[0-5][0-9]|t[1-4])/ui, token: "number" },
{ regex: /-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/ui, token: "number" },
{ regex: /[a-zA-Z_][a-zA-Z_0-9]*/u, token: "variable" },
{ regex: /\{/ui, indent: true, next: "commandDone" },
// This seems necessary to have a closing curly brace de-indent automatically in some cases
{ regex: /\}/ui, dedent: true },

View File

@ -1,4 +1,4 @@
import { AutomatorLexer } from "./lexer.js";
import { AutomatorLexer } from "./lexer";
/**
* Note: the $ shorthand for the parser object is required by Chevrotain. Don't mess with it.
@ -6,10 +6,8 @@ import { AutomatorLexer } from "./lexer.js";
export const AutomatorCommands = ((() => {
const T = AutomatorLexer.tokenMap;
// The splitter tries to get a number 1 through 6, or anything else. Note: eslint complains
// about lack of u flag here for some reason.
// eslint-disable-next-line require-unicode-regexp
const presetSplitter = new RegExp(/preset[ \t]+(?:([1-6]$)|(.+$))/ui);
const presetSplitter = /name[ \t]+(.+$)/ui;
const idSplitter = /id[ \t]+(\d)/ui;
function prestigeNotify(flag) {
if (!AutomatorBackend.isOn) return;
@ -28,13 +26,13 @@ export const AutomatorCommands = ((() => {
return {
run: () => {
if (!evalComparison()) {
AutomatorData.logCommandEvent(`Checked ${parseConditionalIntoText(ctx)} (false),
exiting loop at line ${ctx.RCurly[0].startLine + 1} (end of loop)`, ctx.startLine);
AutomatorData.logCommandEvent(`Checked ${parseConditionalIntoText(ctx)} (false), exiting loop at
line ${AutomatorBackend.translateLineNumber(ctx.RCurly[0].startLine + 1)} (end of loop)`, ctx.startLine);
return AUTOMATOR_COMMAND_STATUS.NEXT_TICK_NEXT_INSTRUCTION;
}
AutomatorBackend.push(commands);
AutomatorData.logCommandEvent(`Checked ${parseConditionalIntoText(ctx)} (true),
moving to line ${ctx.LCurly[0].startLine + 1} (start of loop)`, ctx.startLine);
AutomatorData.logCommandEvent(`Checked ${parseConditionalIntoText(ctx)} (true), moving to
line ${AutomatorBackend.translateLineNumber(ctx.LCurly[0].startLine + 1)} (start of loop)`, ctx.startLine);
return AUTOMATOR_COMMAND_STATUS.SAME_INSTRUCTION;
},
blockCommands: commands,
@ -44,11 +42,12 @@ export const AutomatorCommands = ((() => {
// Extracts the conditional out of a command and returns it as text
function parseConditionalIntoText(ctx) {
const comp = ctx.comparison[0].children;
const getters = comp.compareValue.map(cv => (
cv.children.AutomatorCurrency
? () => cv.children.AutomatorCurrency[0].image
: () => format(cv.children.$value, 2, 2)
));
const getters = comp.compareValue.map(cv => {
if (cv.children.AutomatorCurrency) return () => cv.children.AutomatorCurrency[0].image;
const val = cv.children.$value;
if (typeof val === "string") return () => val;
return () => format(val, 2, 2);
});
const compareFn = comp.ComparisonOperator[0].image;
return `${getters[0]()} ${compareFn} ${getters[1]()}`;
}
@ -91,12 +90,6 @@ export const AutomatorCommands = ((() => {
// eslint-disable-next-line complexity
validate: (ctx, V) => {
ctx.startLine = ctx.Auto[0].startLine;
if (ctx.PrestigeEvent && ctx.PrestigeEvent[0].tokenType === T.Reality && (ctx.duration || ctx.xHighest)) {
V.addError((ctx.duration || ctx.xHighest)[0],
"Auto Reality cannot be set to a duration or x highest",
"Use RM for Auto Reality");
return false;
}
if (ctx.PrestigeEvent && ctx.currencyAmount) {
const desired$ = ctx.PrestigeEvent[0].tokenType.$prestigeCurrency;
const specified$ = ctx.currencyAmount[0].children.AutomatorCurrency[0].tokenType.name;
@ -107,41 +100,51 @@ export const AutomatorCommands = ((() => {
}
}
if (ctx.PrestigeEvent && ctx.PrestigeEvent[0].tokenType === T.Infinity &&
(ctx.duration || ctx.xHighest) && !EternityMilestone.bigCrunchModes.isReached) {
if (!ctx.PrestigeEvent) return true;
const advSetting = ctx.duration || ctx.xHighest;
// Do not change to switch statement; T.XXX are Objects, not primitive values
if (ctx.PrestigeEvent[0].tokenType === T.Infinity) {
if (!Autobuyer.bigCrunch.isUnlocked) {
V.addError(ctx.PrestigeEvent, "Infinity autobuyer is not unlocked",
"Complete the Big Crunch Autobuyer challenge to use this command");
return false;
}
if (advSetting && !EternityMilestone.bigCrunchModes.isReached) {
V.addError((ctx.duration || ctx.xHighest)[0],
"Advanced Infinity autobuyer settings are not unlocked",
`Reach ${quantifyInt("Eternity", EternityMilestone.bigCrunchModes.config.eternities)} to use this command`);
`Reach ${quantifyInt("Eternity", EternityMilestone.bigCrunchModes.config.eternities)}
to use this command`);
return false;
}
if (ctx.PrestigeEvent && ctx.PrestigeEvent[0].tokenType === T.Eternity &&
(ctx.duration || ctx.xHighest) && !RealityUpgrade(13).isBought) {
V.addError((ctx.duration || ctx.xHighest)[0],
"Advanced Eternity autobuyer settings are not unlocked",
"Purchase the Reality Upgrade which unlocks advanced Eternity autobuyer settings");
return false;
}
if (ctx.PrestigeEvent && ctx.PrestigeEvent[0].tokenType === T.Eternity &&
!EternityMilestone.autobuyerEternity.isReached) {
if (ctx.PrestigeEvent[0].tokenType === T.Eternity) {
if (!EternityMilestone.autobuyerEternity.isReached) {
V.addError(ctx.PrestigeEvent, "Eternity autobuyer is not unlocked",
`Reach ${quantifyInt("Eternity", EternityMilestone.autobuyerEternity.config.eternities)}
to use this command`);
return false;
}
if (ctx.PrestigeEvent && ctx.PrestigeEvent[0].tokenType === T.Infinity && !NormalChallenge(12).isCompleted) {
V.addError(ctx.PrestigeEvent, "Infinity autobuyer is not unlocked",
"Complete the Big Crunch Autobuyer challenge to use this command");
if (advSetting && !RealityUpgrade(13).isBought) {
V.addError((ctx.duration || ctx.xHighest)[0],
"Advanced Eternity autobuyer settings are not unlocked",
"Purchase the Reality Upgrade which unlocks advanced Eternity autobuyer settings");
return false;
}
if (ctx.PrestigeEvent && ctx.PrestigeEvent[0].tokenType === T.Reality && !RealityUpgrade(25).isBought) {
}
if (ctx.PrestigeEvent[0].tokenType === T.Reality) {
if (!RealityUpgrade(25).isBought) {
V.addError(ctx.PrestigeEvent, "Reality autobuyer is not unlocked",
"Purchase the Reality Upgrade which unlocks the Reality autobuyer");
return false;
}
if (advSetting) {
V.addError((ctx.duration || ctx.xHighest)[0],
"Auto Reality cannot be set to a duration or x highest",
"Use RM for Auto Reality");
return false;
}
}
return true;
},
compile: ctx => {
@ -202,8 +205,8 @@ export const AutomatorCommands = ((() => {
else input = (on ? "ON" : "OFF");
return {
target: ctx.PrestigeEvent[0].tokenType.name.toUpperCase(),
inputValue: input,
singleSelectionInput: ctx.PrestigeEvent[0].tokenType.name.toUpperCase(),
singleTextInput: input,
...automatorBlocksMap.AUTO
};
}
@ -220,8 +223,13 @@ export const AutomatorCommands = ((() => {
validate: (ctx, V) => {
ctx.startLine = ctx.BlackHole[0].startLine;
if (!BlackHole(1).isUnlocked) {
if (Enslaved.isRunning || Pelle.isDisabled("blackhole")) {
V.addError(ctx.BlackHole[0], "Black Hole is disabled in your current Reality",
"Return to normal Reality conditions to use this command again");
} else {
V.addError(ctx.BlackHole[0], "Black Hole is not unlocked",
"Unlock the Black Hole in order to pause or unpause it");
}
return false;
}
return true;
@ -235,7 +243,7 @@ export const AutomatorCommands = ((() => {
};
},
blockify: ctx => ({
target: ctx.On ? "ON" : "OFF",
singleSelectionInput: ctx.On ? "ON" : "OFF",
...automatorBlocksMap["BLACK HOLE"]
})
},
@ -249,7 +257,7 @@ export const AutomatorCommands = ((() => {
return true;
},
// This is an easter egg, it shouldn't do anything
compile: () => () => AUTOMATOR_COMMAND_STATUS.NEXT_INSTRUCTION,
compile: () => () => AUTOMATOR_COMMAND_STATUS.SKIP_INSTRUCTION,
blockify: () => ({
...automatorBlocksMap.BLOB,
})
@ -264,58 +272,12 @@ export const AutomatorCommands = ((() => {
return true;
},
// Comments should be no-ops
compile: () => () => AUTOMATOR_COMMAND_STATUS.NEXT_INSTRUCTION,
compile: () => () => AUTOMATOR_COMMAND_STATUS.SKIP_INSTRUCTION,
blockify: ctx => ({
...automatorBlocksMap.COMMENT,
inputValue: ctx.Comment[0].image.replace(/(#|\/\/)\s?/u, ""),
singleTextInput: ctx.Comment[0].image.replace(/(#|\/\/)\s?/u, ""),
})
},
{
id: "define",
block: null,
rule: $ => () => {
$.CONSUME(T.Define);
$.CONSUME(T.Identifier);
$.CONSUME(T.EqualSign);
$.OR([
{ ALT: () => $.SUBRULE($.duration) },
{ ALT: () => $.SUBRULE($.studyList) },
]);
},
validate: (ctx, V) => {
ctx.startLine = ctx.Define[0].startLine;
if (!ctx.Identifier || ctx.Identifier[0].isInsertedInRecovery || ctx.Identifier[0].image === "") {
V.addError(ctx.Define, "Missing variable name",
"Provide a variable name that isn't a command name between DEFINE and =");
return false;
}
return true;
},
// Since define creates constants, they are all resolved at compile. The actual define instruction
// doesn't have to do anything.
compile: () => () => AUTOMATOR_COMMAND_STATUS.NEXT_INSTRUCTION,
blockify: ctx => {
const studyListData = ctx.studyList[0].children.studyListEntry;
const studyList = [];
for (const entry of studyListData) {
if (entry.children.NumberLiteral) {
// Single study ID or numerical value
studyList.push(entry.children.NumberLiteral[0].image);
} else if (entry.children.StudyPath) {
// Study path (eg. "time")
studyList.push(entry.children.StudyPath[0].image);
} else {
// Study range (eg. "41-71")
const range = entry.children.studyRange[0].children;
studyList.push(`${range.firstStudy[0].image}-${range.lastStudy[0].image}`);
}
}
return {
...automatorBlocksMap.DEFINE,
inputValue: `${ctx.Identifier[0].image} = ${studyList.join(",")}`,
};
}
},
{
id: "ifBlock",
rule: $ => () => {
@ -344,7 +306,7 @@ export const AutomatorCommands = ((() => {
};
if (!evalComparison()) {
AutomatorData.logCommandEvent(`Checked ${parseConditionalIntoText(ctx)} (false),
skipping to line ${ctx.RCurly[0].startLine + 1}`, ctx.startLine);
skipping to line ${AutomatorBackend.translateLineNumber(ctx.RCurly[0].startLine + 1)}`, ctx.startLine);
return AUTOMATOR_COMMAND_STATUS.NEXT_INSTRUCTION;
}
AutomatorBackend.push(commands);
@ -363,7 +325,8 @@ export const AutomatorCommands = ((() => {
nest: commands,
...automatorBlocksMap.IF,
...comparison,
target: standardizeAutomatorCurrencyName(comparison.target)
genericInput1: standardizeAutomatorValues(comparison.genericInput1),
genericInput2: standardizeAutomatorValues(comparison.genericInput2)
};
}
},
@ -387,7 +350,7 @@ export const AutomatorCommands = ((() => {
},
blockify: ctx => ({
...automatorBlocksMap.NOTIFY,
inputValue: ctx.StringLiteral[0].image,
singleTextInput: ctx.StringLiteral[0].image,
})
},
{
@ -404,6 +367,12 @@ export const AutomatorCommands = ((() => {
ctx.startLine = ctx.Pause[0].startLine;
let duration;
if (ctx.Identifier) {
if (!V.isValidVarFormat(ctx.Identifier[0], AUTOMATOR_VAR_TYPES.DURATION)) {
V.addError(ctx, `Constant ${ctx.Identifier[0].image} is not a valid time duration constant`,
`Ensure that ${ctx.Identifier[0].image} is a number of seconds less than
${format(Number.MAX_VALUE / 1000)}`);
return false;
}
const lookup = V.lookupVar(ctx.Identifier[0], AUTOMATOR_VAR_TYPES.DURATION);
duration = lookup ? lookup.value : lookup;
} else {
@ -415,8 +384,14 @@ export const AutomatorCommands = ((() => {
compile: ctx => {
const duration = ctx.$duration;
return S => {
const dur = ctx.duration[0].children;
const timeString = `${dur.NumberLiteral[0].image} ${dur.TimeUnit[0].image.replace("\\s", "")}`;
let timeString;
if (ctx.duration) {
const c = ctx.duration[0].children;
timeString = `${c.NumberLiteral[0].image} ${c.TimeUnit[0].image}`;
} else {
// This is the case for a defined constant; its value was parsed out during validation
timeString = TimeSpan.fromMilliseconds(duration);
}
if (S.commandState === null) {
S.commandState = { timeMs: 0 };
AutomatorData.logCommandEvent(`Pause started (waiting ${timeString})`, ctx.startLine);
@ -432,10 +407,16 @@ export const AutomatorCommands = ((() => {
};
},
blockify: ctx => {
let blockArg;
if (ctx.duration) {
const c = ctx.duration[0].children;
blockArg = `${c.NumberLiteral[0].image} ${c.TimeUnit[0].image}`;
} else {
blockArg = `${ctx.Identifier[0].image}`;
}
return {
...automatorBlocksMap.PAUSE,
inputValue: `${c.NumberLiteral[0].image} ${c.TimeUnit[0].image}`
singleTextInput: blockArg
};
}
},
@ -477,15 +458,15 @@ export const AutomatorCommands = ((() => {
const available = prestigeToken.$prestigeAvailable();
if (!available) {
if (!nowait) return AUTOMATOR_COMMAND_STATUS.NEXT_TICK_SAME_INSTRUCTION;
AutomatorData.logCommandEvent(`Auto-${ctx.PrestigeEvent.image} attempted, but skipped due to NOWAIT`,
AutomatorData.logCommandEvent(`${ctx.PrestigeEvent.image} attempted, but skipped due to NOWAIT`,
ctx.startLine);
return AUTOMATOR_COMMAND_STATUS.NEXT_INSTRUCTION;
}
if (respec) prestigeToken.$respec();
prestigeToken.$prestige();
const prestigeName = ctx.PrestigeEvent[0].image.toUpperCase();
AutomatorData.logCommandEvent(`Auto-${prestigeName} triggered
(${findLastPrestigeRecord(prestigeName)})`, ctx.startLine);
AutomatorData.logCommandEvent(`${prestigeName} triggered (${findLastPrestigeRecord(prestigeName)})`,
ctx.startLine);
return AUTOMATOR_COMMAND_STATUS.NEXT_TICK_NEXT_INSTRUCTION;
};
},
@ -493,7 +474,7 @@ export const AutomatorCommands = ((() => {
...automatorBlocksMap[
ctx.PrestigeEvent[0].tokenType.name.toUpperCase()
],
wait: ctx.Nowait === undefined,
nowait: ctx.Nowait !== undefined,
respec: ctx.Respec !== undefined
})
},
@ -519,7 +500,7 @@ export const AutomatorCommands = ((() => {
}
return AUTOMATOR_COMMAND_STATUS.NEXT_TICK_SAME_INSTRUCTION;
},
blockify: () => ({ target: "DILATION", ...automatorBlocksMap.START })
blockify: () => ({ singleSelectionInput: "DILATION", ...automatorBlocksMap.START })
},
{
id: "startEC",
@ -553,15 +534,15 @@ export const AutomatorCommands = ((() => {
};
},
blockify: ctx => ({
target: "EC",
inputValue: ctx.eternityChallenge[0].children.$ecNumber,
singleSelectionInput: "EC",
singleTextInput: ctx.eternityChallenge[0].children.$ecNumber,
...automatorBlocksMap.START
})
},
{
id: "storeTime",
id: "storeGameTime",
rule: $ => () => {
$.CONSUME(T.StoreTime);
$.CONSUME(T.StoreGameTime);
$.OR([
{ ALT: () => $.CONSUME(T.On) },
{ ALT: () => $.CONSUME(T.Off) },
@ -569,10 +550,10 @@ export const AutomatorCommands = ((() => {
]);
},
validate: (ctx, V) => {
ctx.startLine = ctx.StoreTime[0].startLine;
ctx.startLine = ctx.StoreGameTime[0].startLine;
if (!Enslaved.isUnlocked) {
V.addError(ctx.StoreTime[0], "You do not yet know how to store time",
"Unlock the ability to store time");
V.addError(ctx.StoreGameTime[0], "You do not yet know how to store game time",
"Unlock the ability to store game time");
return false;
}
return true;
@ -581,9 +562,9 @@ export const AutomatorCommands = ((() => {
if (ctx.Use) return () => {
if (Enslaved.isUnlocked) {
Enslaved.useStoredTime(false);
AutomatorData.logCommandEvent(`Stored time used`, ctx.startLine);
AutomatorData.logCommandEvent(`Stored game time used`, ctx.startLine);
} else {
AutomatorData.logCommandEvent(`Attempted to use stored time, but failed (not unlocked yet)`,
AutomatorData.logCommandEvent(`Attempted to use stored game time, but failed (not unlocked yet)`,
ctx.startLine);
}
return AUTOMATOR_COMMAND_STATUS.NEXT_INSTRUCTION;
@ -591,14 +572,14 @@ export const AutomatorCommands = ((() => {
const on = Boolean(ctx.On);
return () => {
if (on !== player.celestials.enslaved.isStoring) Enslaved.toggleStoreBlackHole();
AutomatorData.logCommandEvent(`Storing time toggled ${ctx.On ? "ON" : "OFF"}`, ctx.startLine);
AutomatorData.logCommandEvent(`Storing game time toggled ${ctx.On ? "ON" : "OFF"}`, ctx.startLine);
return AUTOMATOR_COMMAND_STATUS.NEXT_INSTRUCTION;
};
},
blockify: ctx => ({
// eslint-disable-next-line no-nested-ternary
target: ctx.Use ? "USE" : (ctx.On ? "ON" : "OFF"),
...automatorBlocksMap["STORE TIME"]
singleSelectionInput: ctx.Use ? "USE" : (ctx.On ? "ON" : "OFF"),
...automatorBlocksMap["STORE GAME TIME"]
})
},
{
@ -606,6 +587,7 @@ export const AutomatorCommands = ((() => {
rule: $ => () => {
$.CONSUME(T.Studies);
$.OPTION(() => $.CONSUME(T.Nowait));
$.CONSUME(T.Purchase);
$.OR([
{ ALT: () => $.SUBRULE($.studyList) },
{ ALT: () => $.CONSUME1(T.Identifier) },
@ -614,13 +596,18 @@ export const AutomatorCommands = ((() => {
validate: (ctx, V) => {
ctx.startLine = ctx.Studies[0].startLine;
if (ctx.Identifier) {
if (!V.isValidVarFormat(ctx.Identifier[0], AUTOMATOR_VAR_TYPES.STUDIES)) {
V.addError(ctx, `Constant ${ctx.Identifier[0].image} is not a valid Time Study constant`,
`Ensure that ${ctx.Identifier[0].image} is a properly-formatted Time Study string`);
return false;
}
const varInfo = V.lookupVar(ctx.Identifier[0], AUTOMATOR_VAR_TYPES.STUDIES);
if (!varInfo) return;
ctx.$studies = varInfo.value;
ctx.$studies.image = ctx.Identifier[0].image;
} else if (ctx.studyList) {
ctx.$studies = V.visit(ctx.studyList);
}
return true;
},
compile: ctx => {
const studies = ctx.$studies;
@ -665,9 +652,9 @@ export const AutomatorCommands = ((() => {
};
},
blockify: ctx => ({
inputValue: ctx.$studies.image,
wait: ctx.Nowait === undefined,
...automatorBlocksMap.STUDIES
singleTextInput: ctx.$studies.image,
nowait: ctx.Nowait !== undefined,
...automatorBlocksMap["STUDIES PURCHASE"]
})
},
{
@ -676,38 +663,53 @@ export const AutomatorCommands = ((() => {
$.CONSUME(T.Studies);
$.OPTION(() => $.CONSUME(T.Nowait));
$.CONSUME(T.Load);
$.CONSUME(T.Preset);
$.OR([
{ ALT: () => $.CONSUME1(T.Id) },
{ ALT: () => $.CONSUME1(T.Name) },
]);
},
validate: (ctx, V) => {
ctx.startLine = ctx.Studies[0].startLine;
if (!ctx.Preset || ctx.Preset[0].isInsertedInRecovery || ctx.Preset[0].image === "") {
V.addError(ctx, "Missing preset and preset name",
`Provide the name of a saved study preset from the Time Studies page. Note this command will not work
with presets with purely numerical names.`);
if (ctx.Id) {
const split = idSplitter.exec(ctx.Id[0].image);
if (!split || ctx.Id[0].isInsertedInRecovery) {
V.addError(ctx, "Missing preset id",
"Provide the id of a saved study preset slot from the Time Studies page");
return false;
}
const split = presetSplitter.exec(ctx.Preset[0].image);
if (!split) {
V.addError(ctx.Preset[0], "Missing preset name or number",
"Provide the name or index (1-6) of a saved study preset from the Time Studies page");
const id = parseInt(split[1], 10);
if (id < 1 || id > 6) {
V.addError(ctx.Id[0], `Could not find a preset with an id of ${id}`,
"Type in a valid id (1 - 6) for your study preset");
return false;
}
ctx.Preset[0].splitPresetResult = split;
let presetIndex;
if (split[2]) {
// We don't need to do any verification if it's a number; if it's a name, we
// check to make sure it exists:
presetIndex = player.timestudy.presets.findIndex(e => e.name === split[2]) + 1;
ctx.$presetIndex = id;
return true;
}
if (ctx.Name) {
const split = presetSplitter.exec(ctx.Name[0].image);
if (!split || ctx.Name[0].isInsertedInRecovery) {
V.addError(ctx, "Missing preset name",
"Provide the name of a saved study preset from the Time Studies page");
return false;
}
// If it's a name, we check to make sure it exists:
const presetIndex = player.timestudy.presets.findIndex(e => e.name === split[1]) + 1;
if (presetIndex === 0) {
V.addError(ctx.Preset[0], `Could not find preset named ${split[2]} (Note: Names are case-sensitive)`,
V.addError(ctx.Name[0], `Could not find preset named ${split[1]} (Note: Names are case-sensitive)`,
"Check to make sure you typed in the correct name for your study preset");
return false;
}
} else {
presetIndex = parseInt(split[1], 10);
}
ctx.$presetIndex = presetIndex;
return true;
}
return false;
},
compile: ctx => {
const presetIndex = ctx.$presetIndex;
@ -720,10 +722,13 @@ export const AutomatorCommands = ((() => {
// if there are then we keep trying on this line until there aren't, unless we are given nowait
const missingStudyCount = imported.purchasedStudies
.filter(s => !GameCache.currentStudyTree.value.purchasedStudies.includes(s)).length;
const presetRepresentation = ctx.Name ? ctx.Name[0].image : ctx.Id[0].image;
if (missingStudyCount === 0) {
AutomatorData.logCommandEvent(`Fully loaded study preset ${ctx.Preset[0].image}`, ctx.startLine);
AutomatorData.logCommandEvent(`Fully loaded study preset ${presetRepresentation}`, ctx.startLine);
} else if (afterCount > beforeCount) {
AutomatorData.logCommandEvent(`Partially loaded study preset ${ctx.Preset[0].image}
AutomatorData.logCommandEvent(`Partially loaded study preset ${presetRepresentation}
(missing ${quantifyInt("study", missingStudyCount)})`, ctx.startLine);
}
return ctx.Nowait !== undefined || missingStudyCount === 0
@ -732,9 +737,10 @@ export const AutomatorCommands = ((() => {
};
},
blockify: ctx => ({
inputValue: ctx.$presetIndex,
wait: ctx.Nowait === undefined,
...automatorBlocksMap.LOAD
singleSelectionInput: ctx.Name ? "NAME" : "ID",
singleTextInput: ctx.Name ? player.timestudy.presets[ctx.$presetIndex - 1].name : ctx.$presetIndex,
nowait: ctx.Nowait !== undefined,
...automatorBlocksMap["STUDIES LOAD"]
})
},
{
@ -752,37 +758,7 @@ export const AutomatorCommands = ((() => {
AutomatorData.logCommandEvent(`Turned study respec ON`, ctx.startLine);
return AUTOMATOR_COMMAND_STATUS.NEXT_INSTRUCTION;
},
blockify: () => automatorBlocksMap.RESPEC
},
{
id: "tt",
rule: $ => () => {
$.OPTION(() => $.CONSUME(T.Buy));
$.CONSUME(T.TT);
$.CONSUME(T.TTCurrency);
},
validate: ctx => {
ctx.startLine = (ctx.Buy || ctx.TT)[0].startLine;
return true;
},
compile: ctx => {
const buyFunction = ctx.TTCurrency[0].tokenType.$buyTT;
return () => {
const boughtTT = buyFunction();
if (boughtTT) {
AutomatorData.logCommandEvent(`${formatInt(boughtTT)} TT purchased with ${ctx.TTCurrency[0].image}`,
ctx.startLine);
return AUTOMATOR_COMMAND_STATUS.NEXT_INSTRUCTION;
}
AutomatorData.logCommandEvent(`Attempted to purchase TT with ${ctx.TTCurrency[0].image}
but could not afford any`, ctx.startLine);
return AUTOMATOR_COMMAND_STATUS.NEXT_TICK_NEXT_INSTRUCTION;
};
},
blockify: ctx => ({
target: ctx.TTCurrency[0].tokenType.name.toUpperCase(),
...automatorBlocksMap.TT
})
blockify: () => automatorBlocksMap["STUDIES RESPEC"]
},
{
id: "unlockDilation",
@ -816,8 +792,8 @@ export const AutomatorCommands = ((() => {
};
},
blockify: ctx => ({
target: "DILATION",
wait: ctx.Nowait === undefined,
singleSelectionInput: "DILATION",
nowait: ctx.Nowait !== undefined,
...automatorBlocksMap.UNLOCK
})
},
@ -853,9 +829,9 @@ export const AutomatorCommands = ((() => {
};
},
blockify: ctx => ({
target: "EC",
inputValue: ctx.eternityChallenge[0].children.$ecNumber,
wait: ctx.Nowait === undefined,
singleSelectionInput: "EC",
singleTextInput: ctx.eternityChallenge[0].children.$ecNumber,
nowait: ctx.Nowait !== undefined,
...automatorBlocksMap.UNLOCK
})
},
@ -908,8 +884,9 @@ export const AutomatorCommands = ((() => {
return AUTOMATOR_COMMAND_STATUS.NEXT_INSTRUCTION;
}
AutomatorBackend.push(commands);
AutomatorData.logCommandEvent(`${prestigeName} prestige has not occurred yet,
moving to line ${ctx.LCurly[0].startLine + 1} (start of until loop)`, ctx.startLine);
AutomatorData.logCommandEvent(`${prestigeName} prestige has not occurred yet, moving to line
${AutomatorBackend.translateLineNumber(ctx.LCurly[0].startLine + 1)} (start of until loop)`,
ctx.startLine);
return AUTOMATOR_COMMAND_STATUS.SAME_INSTRUCTION;
},
blockCommands: commands
@ -924,11 +901,12 @@ export const AutomatorCommands = ((() => {
nest: commands,
...automatorBlocksMap.UNTIL,
...comparison,
target: standardizeAutomatorCurrencyName(comparison.target)
genericInput1: standardizeAutomatorValues(comparison.genericInput1),
genericInput2: standardizeAutomatorValues(comparison.genericInput2)
};
}
return {
target: ctx.PrestigeEvent[0].tokenType.name.toUpperCase(),
genericInput1: ctx.PrestigeEvent[0].tokenType.name.toUpperCase(),
nest: commands,
...automatorBlocksMap.UNTIL
};
@ -974,7 +952,8 @@ export const AutomatorCommands = ((() => {
nest: commands,
...automatorBlocksMap.WAIT,
...comparison,
target: standardizeAutomatorCurrencyName(comparison.target)
genericInput1: standardizeAutomatorValues(comparison.genericInput1),
genericInput2: standardizeAutomatorValues(comparison.genericInput2)
};
}
},
@ -1012,7 +991,7 @@ export const AutomatorCommands = ((() => {
};
},
blockify: ctx => ({
target: ctx.PrestigeEvent[0].tokenType.name.toUpperCase(),
genericInput1: ctx.PrestigeEvent[0].tokenType.name.toUpperCase(),
...automatorBlocksMap.WAIT
})
},
@ -1039,7 +1018,8 @@ export const AutomatorCommands = ((() => {
nest: commands,
...automatorBlocksMap.WHILE,
...comparison,
target: standardizeAutomatorCurrencyName(comparison.target)
genericInput1: standardizeAutomatorValues(comparison.genericInput1),
genericInput2: standardizeAutomatorValues(comparison.genericInput2)
};
}
}

View File

@ -35,12 +35,11 @@ export const AutomatorPoints = {
}
};
GameDatabase.reality.otherAutomatorPoints = (function() {
return [
GameDatabase.reality.otherAutomatorPoints = [
{
name: "Reality Count",
automatorPoints: () => 2 * Math.clampMax(Currency.realities.value, 100),
shortDescription: () => `+${formatInt(2)} per Reality, up to ${formatInt(100)} Realities`,
automatorPoints: () => 2 * Math.clampMax(Currency.realities.value, 50),
shortDescription: () => `+${formatInt(2)} per Reality, up to ${formatInt(50)} Realities`,
symbol: "Ϟ",
},
{
@ -50,4 +49,3 @@ GameDatabase.reality.otherAutomatorPoints = (function() {
symbol: "<i class='fas fa-circle'></i>",
},
];
}());

View File

@ -1,6 +1,6 @@
import { AutomatorCommands } from "./automator-commands.js";
import { AutomatorGrammar } from "./parser.js";
import { AutomatorLexer } from "./lexer.js";
import { AutomatorCommands } from "./automator-commands";
import { AutomatorGrammar } from "./parser";
import { AutomatorLexer } from "./lexer";
(function() {
if (AutomatorGrammar === undefined) {
@ -24,6 +24,16 @@ import { AutomatorLexer } from "./lexer.js";
if (ownMethod) ownMethod.call(this, ctx);
};
}
const lexResult = AutomatorLexer.lexer.tokenize(rawText);
const tokens = lexResult.tokens;
parser.input = tokens;
this.parseResult = parser.script();
this.visit(this.parseResult);
this.addLexerErrors(lexResult.errors);
this.addParserErrors(parser.errors, tokens);
this.modifyErrorMessages();
this.errorCount = lexResult.errors.length + this.errors.length + parser.errors.length;
}
addLexerErrors(errors) {
@ -154,6 +164,11 @@ import { AutomatorLexer } from "./lexer.js";
modifiedErrors.push(err);
lastLine = err.startLine;
}
for (const err of modifiedErrors) {
err.startLine = AutomatorBackend.translateLineNumber(err.startLine);
}
this.errors = modifiedErrors;
}
@ -175,34 +190,58 @@ import { AutomatorLexer } from "./lexer.js";
lookupVar(identifier, type) {
const varName = identifier.image;
const varInfo = this.variables[varName];
if (varInfo === undefined) {
const varInfo = {};
const constants = player.reality.automator.constants;
if (!Object.keys(constants).includes(varName)) {
this.addError(identifier, `Variable ${varName} has not been defined`,
`Use DEFINE to define ${varName} in order to reference it, or check for typos`);
`Use the definition panel to define ${varName} in order to reference it, or check for typos`);
return undefined;
}
if (varInfo.type === AUTOMATOR_VAR_TYPES.UNKNOWN) {
varInfo.firstUseLineNumber = identifier.image.startLine;
varInfo.type = type;
if (type === AUTOMATOR_VAR_TYPES.STUDIES) {
// The only time we have an unknown studies is if there was only one listed
const value = constants[varName];
let tree;
switch (type) {
case AUTOMATOR_VAR_TYPES.NUMBER:
varInfo.value = new Decimal(value);
break;
case AUTOMATOR_VAR_TYPES.STUDIES:
tree = new TimeStudyTree(value);
varInfo.value = {
normal: [varInfo.value.toNumber()],
ec: 0
normal: tree.selectedStudies.map(ts => ts.id),
ec: tree.ec
};
break;
case AUTOMATOR_VAR_TYPES.DURATION:
varInfo.value = parseInt(1000 * value, 10);
break;
default:
throw new Error("Unrecognized variable format in automator constant lookup");
}
} else if (varInfo.type !== type) {
const inferenceMessage = varInfo.firstUseLineNumber
? `\nIts use on line ${varInfo.firstUseLineNumber} identified it as a ${varInfo.type.name}`
: "";
this.addError(identifier, `Variable ${varName} is not a ${type.name}${inferenceMessage}`,
"Defined variables cannot be used as both studies and numbers - define a second variable instead");
return undefined;
}
if (varInfo.value === undefined) throw new Error("Unexpected undefined Automator variable value");
return varInfo;
}
isValidVarFormat(identifier, type) {
const varName = identifier.image;
const constants = player.reality.automator.constants;
if (!Object.keys(constants).includes(varName)) return false;
const value = constants[varName];
switch (type) {
case AUTOMATOR_VAR_TYPES.NUMBER:
// We can't rely on native Decimal parsing here because it largely just discards input past invalid
// characters and constructs something based on the start of the input string. Notably, this makes
// 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;
case AUTOMATOR_VAR_TYPES.DURATION:
return !Number.isNaN(parseInt(1000 * value, 10));
default:
throw new Error("Unrecognized variable format in automator constant lookup");
}
}
duration(ctx) {
if (ctx.$value) return ctx.$value;
if (!ctx.TimeUnit || ctx.TimeUnit[0].isInsertedInRecovery) {
@ -238,50 +277,6 @@ import { AutomatorLexer } from "./lexer.js";
return ctx.$value;
}
define(ctx) {
const varName = ctx.Identifier[0].image;
if (this.variables[varName] !== undefined) {
this.addError(ctx.Identifier[0],
`Variable ${varName} already defined on line ${this.variables[varName].definitionLineNumber}`,
"Variables cannot be defined twice; remove or rename the second DEFINE");
return;
}
if (!ctx.duration && !ctx.studyList) return;
const def = {
name: varName,
definitionLineNumber: ctx.Identifier[0].startLine,
firstUseLineNumber: 0,
type: AUTOMATOR_VAR_TYPES.UNKNOWN,
value: undefined,
};
this.variables[varName] = def;
if (ctx.duration) {
def.type = AUTOMATOR_VAR_TYPES.DURATION;
def.value = this.visit(ctx.duration);
return;
}
// We don't visit studyList because it might actually be just a number in this case
const studies = ctx.studyList[0].children.studyListEntry;
if (
studies.length > 1 ||
studies[0].children.studyRange ||
studies[0].children.StudyPath ||
studies[0].children.Comma
) {
def.type = AUTOMATOR_VAR_TYPES.STUDIES;
def.value = this.visit(ctx.studyList);
return;
}
// We assume the value is a number; in some cases, we might overwrite it if we see
// this variable used in studies
def.value = new Decimal(studies[0].children.NumberLiteral[0].image);
if (!/^[1-9][0-9]*[1-9]$/u.test(studies[0].children.NumberLiteral[0].image)) {
// Study numbers are pretty specific number patterns
def.type = AUTOMATOR_VAR_TYPES.NUMBER;
}
}
studyRange(ctx, studiesOut) {
if (!ctx.firstStudy || ctx.firstStudy[0].isInsertedInRecovery ||
!ctx.lastStudy || ctx.lastStudy[0].isInsertedInRecovery) {
@ -347,8 +342,12 @@ import { AutomatorLexer } from "./lexer.js";
if (ctx.NumberLiteral) {
ctx.$value = new Decimal(ctx.NumberLiteral[0].image);
} else if (ctx.Identifier) {
if (!this.isValidVarFormat(ctx.Identifier[0], AUTOMATOR_VAR_TYPES.NUMBER)) {
this.addError(ctx, `Constant ${ctx.Identifier[0].image} cannot be used for comparison`,
`Ensure that ${ctx.Identifier[0].image} contains a properly-formatted number and not a Time Study string`);
}
const varLookup = this.lookupVar(ctx.Identifier[0], AUTOMATOR_VAR_TYPES.NUMBER);
if (varLookup) ctx.$value = varLookup.value;
if (varLookup) ctx.$value = ctx.Identifier[0].image;
}
}
@ -438,9 +437,12 @@ import { AutomatorLexer } from "./lexer.js";
}
comparison(ctx) {
const getters = ctx.compareValue.map(cv => (
cv.children.AutomatorCurrency ? cv.children.AutomatorCurrency[0].tokenType.$getter : () => cv.children.$value
));
const getters = ctx.compareValue.map(cv => {
if (cv.children.AutomatorCurrency) return cv.children.AutomatorCurrency[0].tokenType.$getter;
const val = cv.children.$value;
if (typeof val === "string") return () => player.reality.automator.constants[val];
return () => val;
});
const compareFun = ctx.ComparisonOperator[0].tokenType.$compare;
return () => compareFun(getters[0](), getters[1]());
}
@ -469,48 +471,34 @@ import { AutomatorLexer } from "./lexer.js";
// eslint-disable-next-line no-loop-func
this[cmd.id] = (ctx, output) => {
if (ownMethod && ownMethod !== super[cmd.id]) ownMethod.call(this, ctx, output);
try {
const block = blockify(ctx, this);
output.push({
...block,
id: UIID.next()
});
} catch {
// If a command is invalid, it will throw an exception in blockify and fail to assign a value to block
// We can't, generally, make good guesses to fill in any missing values in order to avoid the exception,
// so we instead just ignore that block
}
};
}
this.validateVisitor();
}
comparison(ctx) {
const isCurrency = ctx.compareValue.map(cv => Boolean(cv.children.AutomatorCurrency));
// eslint-disable-next-line no-bitwise
if (!(isCurrency[0] ^ isCurrency[1])) {
throw new Error("arbitrary comparisons are not supported in block mode yet");
}
const currencyIndex = isCurrency[0] ? 0 : 1;
const flipped = currencyIndex === 1;
const valueChildren = ctx.compareValue[1 - currencyIndex].children;
const isDecimalValue = Boolean(valueChildren.$value);
const value = isDecimalValue ? valueChildren.$value.toString() : valueChildren.NumberLiteral[0].image;
let operator = ctx.ComparisonOperator[0].image;
if (flipped) {
switch (operator) {
case ">":
operator = "<";
break;
case "<":
operator = ">";
break;
case ">=":
operator = "<=";
break;
case "<=":
operator = ">=";
break;
}
}
const parseInput = index => {
const comp = ctx.compareValue[index];
const isCurrency = Boolean(comp.children.AutomatorCurrency);
if (isCurrency) return comp.children.AutomatorCurrency[0].image;
return comp.children.$value;
};
return {
target: ctx.compareValue[currencyIndex].children.AutomatorCurrency[0].image,
secondaryTarget: operator,
inputValue: value,
compOperator: ctx.ComparisonOperator[0].image,
genericInput1: parseInput(0),
genericInput2: parseInput(1),
};
}
@ -532,18 +520,10 @@ import { AutomatorLexer } from "./lexer.js";
function compile(input, validateOnly = false) {
// The lexer and codemirror choke on the last line of the script, so we pad it with an invisible newline
const script = `${input}\n `;
const lexResult = AutomatorLexer.lexer.tokenize(script);
const tokens = lexResult.tokens;
parser.input = tokens;
const parseResult = parser.script();
const validator = new Validator(script);
validator.visit(parseResult);
validator.addLexerErrors(lexResult.errors);
validator.addParserErrors(parser.errors, tokens);
validator.modifyErrorMessages();
let compiled;
if (validator.errors.length === 0 && !validateOnly) {
compiled = new Compiler().visit(parseResult);
if (validator.errorCount === 0 && !validateOnly) {
compiled = new Compiler().visit(validator.parseResult);
}
return {
errors: validator.errors,
@ -553,33 +533,49 @@ import { AutomatorLexer } from "./lexer.js";
AutomatorGrammar.compile = compile;
function blockifyTextAutomator(input) {
const lexResult = AutomatorLexer.lexer.tokenize(input);
const tokens = lexResult.tokens;
AutomatorGrammar.parser.input = tokens;
const parseResult = AutomatorGrammar.parser.script();
const validator = new Validator(input);
validator.visit(parseResult);
if (lexResult.errors.length === 0 && AutomatorGrammar.parser.errors.length === 0 && validator.errors.length === 0) {
const b = new Blockifier();
const blocks = b.visit(parseResult);
return blocks;
}
const blockifier = new Blockifier();
const blocks = blockifier.visit(validator.parseResult);
return null;
// The Validator grabs all the lines from the visible script, but the Blockifier will fail to visit any lines
// associated with unparsable commands. This results in a discrepancy in line count whenever a line can't be
// parsed as a specific command, and in general this is a problem we can't try to guess a fix for, so we just
// don't convert it at all. In both cases nested commands are stored recursively, but with different structure.
const validatedCount = entry => {
if (!entry) return 0;
const commandDepth = entry.children;
let foundChildren = 0;
// Inner nested commands are found within a prop given the same name as the command itself - this should only
// actually evaluate to nonzero for at most one key, and will be undefined for all others
for (const key of Object.keys(commandDepth)) {
const nestedBlock = commandDepth[key][0]?.children?.block;
const nestedCommands = nestedBlock ? nestedBlock[0].children.command : [];
foundChildren += nestedCommands
? nestedCommands.map(c => validatedCount(c) + 1).reduce((sum, val) => sum + val, 0)
: 0;
// Trailing newlines get turned into a command with a single EOF argument; we return -1 because one level up
// on the recursion this looks like an otherwise valid command and would be counted as such
if (key === "EOF") return -1;
}
return foundChildren;
};
const visitedCount = block => {
if (!block.nest) return 1;
return 1 + block.nest.map(b => visitedCount(b)).reduce((sum, val) => sum + val, 0);
};
// Note: top-level structure is slightly different than the nesting structure
const validatedBlocks = validator.parseResult.children.block[0].children.command
.map(c => validatedCount(c) + 1)
.reduce((sum, val) => sum + val, 0);
const visitedBlocks = blocks.map(b => visitedCount(b)).reduce((sum, val) => sum + val, 0);
return { blocks, validatedBlocks, visitedBlocks };
}
AutomatorGrammar.blockifyTextAutomator = blockifyTextAutomator;
function validateLine(input) {
const lexResult = AutomatorLexer.lexer.tokenize(input);
const tokens = lexResult.tokens;
AutomatorGrammar.parser.input = tokens;
const parseResult = AutomatorGrammar.parser.script();
const validator = new Validator(input);
validator.visit(parseResult);
validator.addLexerErrors(lexResult.errors);
validator.addParserErrors(parser.errors, tokens);
validator.modifyErrorMessages();
return validator;
}

View File

@ -1,5 +1,5 @@
import "./compiler.js";
import "./automator-codemirror.js";
import "./compiler";
import "./automator-codemirror";
export { AutomatorGrammar } from "./parser.js";
export { standardizeAutomatorCurrencyName } from "./lexer.js";
export { AutomatorGrammar } from "./parser";
export { forbiddenConstantPatterns, standardizeAutomatorValues } from "./lexer";

View File

@ -2,7 +2,8 @@
/* eslint-disable require-unicode-regexp */
/* eslint-disable camelcase */
import { createToken, Lexer } from "chevrotain";
import { DC } from "../constants.js";
import { DC } from "../constants";
export const AutomatorLexer = (() => {
const createCategory = name => createToken({ name, pattern: Lexer.NA, longer_alt: Identifier });
@ -70,7 +71,6 @@ export const AutomatorLexer = (() => {
const PrestigeEvent = createCategory("PrestigeEvent");
const StudyPath = createCategory("StudyPath");
const TimeUnit = createCategory("TimeUnit");
const TTCurrency = createCategory("TTCurrency");
createInCategory(ComparisonOperator, "OpGTE", />=/, {
$autocomplete: ">=",
@ -102,17 +102,14 @@ export const AutomatorLexer = (() => {
EqualSign.$compare = (a, b) => Decimal.eq(a, b);
createInCategory(AutomatorCurrency, "EP", /ep/i, {
extraCategories: [TTCurrency],
$buyTT: () => TimeTheorems.buyOne(true, "ep"),
$getter: () => Currency.eternityPoints.value
});
createInCategory(AutomatorCurrency, "IP", /ip/i, {
extraCategories: [TTCurrency],
$buyTT: () => TimeTheorems.buyOne(true, "ip"),
$getter: () => Currency.infinityPoints.value
});
createInCategory(AutomatorCurrency, "AM", /am/i, {
extraCategories: [TTCurrency],
$buyTT: () => TimeTheorems.buyOne(true, "am"),
$getter: () => Currency.antimatter.value
});
@ -146,7 +143,7 @@ export const AutomatorLexer = (() => {
$getter: () => (isRealityAvailable() ? MachineHandler.gainedRealityMachines : DC.D0)
});
createInCategory(AutomatorCurrency, "PendingGlyphLevel", /pending[ \t]+glyph[ \t]+level/i, {
$autocomplete: "pending glyph level",
$autocomplete: "pending Glyph level",
$getter: () => new Decimal(isRealityAvailable() ? gainedGlyphLevel().actualLevel : 0),
});
@ -274,24 +271,23 @@ export const AutomatorLexer = (() => {
createKeyword("Auto", /auto/i);
createKeyword("Buy", /buy/i);
createKeyword("Blob", /blob\s\s/i);
createKeyword("Define", /define/i);
// Necessary to hide it from Codemirror's tab auto-completion
createKeyword("Blob", /blob\s\s/i, {
$unlocked: () => false,
});
createKeyword("If", /if/i);
createKeyword("Load", /load/i);
createKeyword("Max", /max/i);
createKeyword("All", /all/i, {
extraCategories: [TTCurrency],
$buyTT: () => TimeTheorems.buyOneOfEach(),
});
createKeyword("Notify", /notify/i);
createKeyword("Nowait", /nowait/i);
createKeyword("Off", /off/i);
createKeyword("On", /on/i);
createKeyword("Pause", /pause/i);
// Presets are a little special, because they can be named anything (like ec12 or wait)
// Names are a little special, because they can be named anything (like ec12 or wait)
// So, we consume the label at the same time as we consume the preset. In order to report
// errors, we also match just the word preset. And, we have to not match comments.
createKeyword("Preset", /preset([ \t]+(\/(?!\/)|[^\n#/])*)?/i);
// errors, we also match just the word name. And, we have to not match comments.
createKeyword("Name", /name([ \t]+(\/(?!\/)|[^\n#/])*)?/i);
createKeyword("Id", /id\b([ \t]+\d)?/i);
createKeyword("Purchase", /purchase/i);
createKeyword("Respec", /respec/i);
createKeyword("Restart", /restart/i);
createKeyword("Start", /start/i);
@ -303,9 +299,11 @@ export const AutomatorLexer = (() => {
createKeyword("While", /while/i);
createKeyword("BlackHole", /black[ \t]+hole/i, {
$autocomplete: "black hole",
$unlocked: () => BlackHole(1).isUnlocked,
});
createKeyword("StoreTime", /stored?[ \t]+time/i, {
$autocomplete: "store time",
createKeyword("StoreGameTime", /stored?[ \t]+game[ \t]+time/i, {
$autocomplete: "store game time",
$unlocked: () => Enslaved.isUnlocked,
});
createKeyword("Dilation", /dilation/i);
@ -339,7 +337,6 @@ export const AutomatorLexer = (() => {
Keyword, ...keywordTokens,
PrestigeEvent, ...tokenLists.PrestigeEvent,
StudyPath, ...tokenLists.StudyPath,
TTCurrency,
TimeUnit, ...tokenLists.TimeUnit,
Identifier,
];
@ -366,27 +363,44 @@ export const AutomatorLexer = (() => {
const automatorCurrencyNames = tokenLists.AutomatorCurrency.map(i => i.$autocomplete.toUpperCase());
const standardizeAutomatorCurrencyName = function(x) {
// This first line exists for this function to usually return quickly;
// otherwise it's called enough to cause lag.
const standardizeAutomatorValues = function(x) {
try {
if (automatorCurrencyNames.includes(x.toUpperCase())) return x.toUpperCase();
} catch {
// This only happens if the input is a number or Decimal, in which case we don't attempt to change any formatting
// and simply return
return x;
}
for (const i of tokenLists.AutomatorCurrency) {
// Check for a match of the full string.
if (x.match(i.PATTERN) && x.match(i.PATTERN)[0].length === x.length) {
return i.$autocomplete.toUpperCase();
}
}
// If we get to this point something has gone wrong, a currency name didn't match any of the currency regexps.
throw new Error(`${x} does not seem to be an automator currency`);
// If we get to this point, we haven't matched a currency name and instead assume it's a defined constant and
// return it without any format changes since these are case-sensitive
return x;
};
// In order to disallow individual words within command key words/phrases, we need to ignore certain patterns (mostly
// ones with special regex characters), split the rest of them up across all spaces and tabs, and then flatten the
// final resulting array. Note that this technically duplicates words present in multiple phrases (eg. "pending")
const ignoredPatterns = ["Identifier", "LCurly", "RCurly"];
const forbiddenConstantPatterns = lexer.lexerDefinition
.filter(p => !ignoredPatterns.includes(p.name))
.map(p => p.PATTERN.source)
.flatMap(p => ((p.includes("(") || p.includes(")")) ? p : p.split("[ \\t]+")));
return {
lexer,
tokens: automatorTokens,
tokenIds,
tokenMap,
standardizeAutomatorCurrencyName,
standardizeAutomatorValues,
forbiddenConstantPatterns,
};
})();
export const standardizeAutomatorCurrencyName = AutomatorLexer.standardizeAutomatorCurrencyName;
export const standardizeAutomatorValues = AutomatorLexer.standardizeAutomatorValues;
export const forbiddenConstantPatterns = AutomatorLexer.forbiddenConstantPatterns;

View File

@ -1,6 +1,7 @@
import { Parser, EOF } from "chevrotain";
import { AutomatorCommands } from "./automator-commands.js";
import { AutomatorLexer } from "./lexer.js";
import { EOF, Parser } from "chevrotain";
import { AutomatorCommands } from "./automator-commands";
import { AutomatorLexer } from "./lexer";
export const AutomatorGrammar = (function() {
const T = AutomatorLexer.tokenMap;

View File

@ -63,11 +63,13 @@ export class ScriptTemplate {
storeTreeData(params) {
const nowaitStr = params.treeNowait ? " nowait" : "";
if (params.treePreset) {
const presetObj = player.timestudy.presets.find(p => p.name === params.treePreset);
this.storedTreeStr = `studies${nowaitStr} load preset ${presetObj.name}`;
const presetObj = player.timestudy.presets.map((p, i) => ({ ...p, id: i + 1 }))
.find(p => (p.name === params.treePreset || p.id === Number(params.treePreset)));
const preset = presetObj.name ? `name ${presetObj.name}` : `id ${presetObj.id}`;
this.storedTreeStr = `studies${nowaitStr} load ${preset}`;
this.storedTreeObj = new TimeStudyTree(presetObj.studies);
} else {
this.storedTreeStr = `studies${nowaitStr} ${params.treeStudies}`;
this.storedTreeStr = `studies${nowaitStr} purchase ${params.treeStudies}`;
this.storedTreeObj = new TimeStudyTree(params.treeStudies);
}
if (this.storedTreeObj.invalidStudies.length > 0) this.warnings.push("Tree contains invalid Study IDs");
@ -233,7 +235,7 @@ export class ScriptTemplate {
}
this.lines.push(`auto infinity off`);
this.lines.push(`auto eternity ${this.parseAutobuyerProp(params.autoEterMode, params.autoEterValue)}`);
this.lines.push(`while tt < ${this.format(TimeStudy.dilation.totalTimeTheoremRequirement)} {`);
this.lines.push(`while total tt < ${this.format(TimeStudy.dilation.totalTimeTheoremRequirement)} {`);
this.lines.push(` ${this.storedTreeStr}`);
this.lines.push(" studies respec");
this.lines.push(" wait eternity");

View File

@ -1,12 +1,8 @@
import { GameMechanicState, SetPurchasableMechanicState, RebuyableMechanicState } from "./game-mechanics/index.js";
import { DC } from "./constants.js";
import { SpeedrunMilestones } from "./speedrun.js";
import { DC } from "./constants";
import FullScreenAnimationHandler from "./full-screen-animation-handler";
export function bigCrunchAnimation() {
document.body.style.animation = "implode 2s 1";
setTimeout(() => {
document.body.style.animation = "";
}, 2000);
FullScreenAnimationHandler.display("a-implode", 2);
}
function handleChallengeCompletion() {
@ -23,9 +19,21 @@ function handleChallengeCompletion() {
}
}
export function manualBigCrunchResetRequest() {
if (!Player.canCrunch) return;
if (GameEnd.creditsEverClosed) return;
// Before the player has broken infinity, the confirmation modal should never be shown
if ((player.break || PlayerProgress.eternityUnlocked()) &&
player.options.confirmations.bigCrunch) {
Modal.bigCrunch.show();
} else {
bigCrunchResetRequest();
}
}
export function bigCrunchResetRequest(disableAnimation = false) {
if (!Player.canCrunch) return;
if (!disableAnimation && player.options.animations.bigCrunch && document.body.style.animation === "") {
if (!disableAnimation && player.options.animations.bigCrunch && !FullScreenAnimationHandler.isDisplaying) {
bigCrunchAnimation();
setTimeout(bigCrunchReset, 1000);
} else {
@ -36,7 +44,6 @@ export function bigCrunchResetRequest(disableAnimation = false) {
export function bigCrunchReset() {
if (!Player.canCrunch) return;
const firstInfinity = !PlayerProgress.infinityUnlocked();
EventHub.dispatch(GAME_EVENT.BIG_CRUNCH_BEFORE);
bigCrunchUpdateStatistics();
@ -45,17 +52,13 @@ export function bigCrunchReset() {
Currency.infinityPoints.add(infinityPoints);
Currency.infinities.add(gainedInfinities().round());
bigCrunchTabChange(firstInfinity);
bigCrunchTabChange(!PlayerProgress.infinityUnlocked());
bigCrunchResetValues();
bigCrunchCheckUnlocks();
if (Pelle.isDoomed) PelleStrikes.infinity.trigger();
EventHub.dispatch(GAME_EVENT.BIG_CRUNCH_AFTER);
if (firstInfinity && !Pelle.isDoomed) Modal.message.show(`Upon Infinity, all Dimensions, Dimension Boosts, and Antimatter
Galaxies are reset, but in return, you gain an Infinity Point (IP). This allows you to buy multiple upgrades that
you can find in the Infinity tab. You will also gain one Infinity, which is the stat shown in the Statistics
tab.`);
}
function bigCrunchUpdateStatistics() {
@ -152,311 +155,6 @@ export function secondSoftReset(forcedNDReset = false) {
AchievementTimers.marathon2.reset();
}
class ChargedInfinityUpgradeState extends GameMechanicState {
constructor(config, upgrade) {
super(config);
this._upgrade = upgrade;
}
get isEffectActive() {
return this._upgrade.isBought && this._upgrade.isCharged;
}
}
export class InfinityUpgrade extends SetPurchasableMechanicState {
constructor(config, requirement) {
super(config);
if (Array.isArray(requirement) || typeof requirement === "function") {
this._requirements = requirement;
} else if (requirement === undefined) {
this._requirements = [];
} else {
this._requirements = [requirement];
}
if (config.charged) {
this._chargedEffect = new ChargedInfinityUpgradeState(config.charged, this);
}
}
get currency() {
return Currency.infinityPoints;
}
get set() {
return player.infinityUpgrades;
}
get isAvailableForPurchase() {
return typeof this._requirements === "function" ? this._requirements()
: this._requirements.every(x => x.isBought);
}
get isEffectActive() {
return this.isBought && !this.isCharged;
}
get chargedEffect() {
return this._chargedEffect;
}
purchase() {
if (super.purchase()) {
// This applies the 4th column of infinity upgrades retroactively
if (this.config.id.includes("skip")) skipResetsIfPossible();
EventHub.dispatch(GAME_EVENT.INFINITY_UPGRADE_BOUGHT);
return true;
}
if (this.canCharge) {
this.charge();
return true;
}
return false;
}
get hasChargeEffect() {
return this.config.charged !== undefined;
}
get isCharged() {
return player.celestials.ra.charged.has(this.id);
}
get canCharge() {
return this.isBought &&
this.hasChargeEffect &&
!this.isCharged &&
Ra.chargesLeft !== 0 &&
!Pelle.isDisabled("chargedInfinityUpgrades");
}
charge() {
player.celestials.ra.charged.add(this.id);
}
disCharge() {
player.celestials.ra.charged.delete(this.id);
}
}
export function totalIPMult() {
if (Effarig.isRunning && Effarig.currentStage === EFFARIG_STAGES.INFINITY) {
return DC.D1;
}
let ipMult = DC.D1
.times(ShopPurchase.IPPurchases.currentMult)
.timesEffectsOf(
TimeStudy(41),
TimeStudy(51),
TimeStudy(141),
TimeStudy(142),
TimeStudy(143),
Achievement(85),
Achievement(93),
Achievement(116),
Achievement(125),
Achievement(141).effects.ipGain,
InfinityUpgrade.ipMult,
DilationUpgrade.ipMultDT,
GlyphEffect.ipMult
);
ipMult = ipMult.times(Replicanti.amount.powEffectOf(AlchemyResource.exponential));
return ipMult;
}
export function disChargeAll() {
const upgrades = [
InfinityUpgrade.totalTimeMult,
InfinityUpgrade.dim18mult,
InfinityUpgrade.dim36mult,
InfinityUpgrade.resetBoost,
InfinityUpgrade.buy10Mult,
InfinityUpgrade.dim27mult,
InfinityUpgrade.dim45mult,
InfinityUpgrade.galaxyBoost,
InfinityUpgrade.thisInfinityTimeMult,
InfinityUpgrade.unspentIPMult,
InfinityUpgrade.dimboostMult,
InfinityUpgrade.ipGen
];
for (const upgrade of upgrades) {
if (upgrade.isCharged) {
upgrade.disCharge();
}
}
player.celestials.ra.disCharge = false;
}
(function() {
const db = GameDatabase.infinity.upgrades;
const upgrade = (config, requirement) => new InfinityUpgrade(config, requirement);
InfinityUpgrade.totalTimeMult = upgrade(db.totalTimeMult);
InfinityUpgrade.dim18mult = upgrade(db.dim18mult, InfinityUpgrade.totalTimeMult);
InfinityUpgrade.dim36mult = upgrade(db.dim36mult, InfinityUpgrade.dim18mult);
InfinityUpgrade.resetBoost = upgrade(db.resetBoost, InfinityUpgrade.dim36mult);
InfinityUpgrade.buy10Mult = upgrade(db.buy10Mult);
InfinityUpgrade.dim27mult = upgrade(db.dim27mult, InfinityUpgrade.buy10Mult);
InfinityUpgrade.dim45mult = upgrade(db.dim45mult, InfinityUpgrade.dim27mult);
InfinityUpgrade.galaxyBoost = upgrade(db.galaxyBoost, InfinityUpgrade.dim45mult);
InfinityUpgrade.thisInfinityTimeMult = upgrade(db.thisInfinityTimeMult);
InfinityUpgrade.unspentIPMult = upgrade(db.unspentIPMult, InfinityUpgrade.thisInfinityTimeMult);
InfinityUpgrade.dimboostMult = upgrade(db.dimboostMult, InfinityUpgrade.unspentIPMult);
InfinityUpgrade.ipGen = upgrade(db.ipGen, InfinityUpgrade.dimboostMult);
InfinityUpgrade.skipReset1 = upgrade(db.skipReset1);
InfinityUpgrade.skipReset2 = upgrade(db.skipReset2, InfinityUpgrade.skipReset1);
InfinityUpgrade.skipReset3 = upgrade(db.skipReset3, InfinityUpgrade.skipReset2);
InfinityUpgrade.skipResetGalaxy = upgrade(db.skipResetGalaxy, InfinityUpgrade.skipReset3);
InfinityUpgrade.ipOffline = upgrade(db.ipOffline, () => Achievement(41).isUnlocked);
}());
// The repeatable 2xIP upgrade has an odd cost structure - it follows a shallow exponential (step *10) up to e3M, at
// which point it follows a steeper one (step *1e10) up to e6M before finally hardcapping. At the hardcap, there's
// an extra bump that increases the multipler itself from e993k to e1M. All these numbers are specified in
// GameDatabase.infinity.upgrades.ipMult
class InfinityIPMultUpgrade extends GameMechanicState {
get cost() {
if (this.purchaseCount >= this.purchasesAtIncrease) {
return this.config.costIncreaseThreshold
.times(Decimal.pow(this.costIncrease, this.purchaseCount - this.purchasesAtIncrease));
}
return Decimal.pow(this.costIncrease, this.purchaseCount + 1);
}
get purchaseCount() {
return player.IPMultPurchases;
}
get purchasesAtIncrease() {
return this.config.costIncreaseThreshold.log10() - 1;
}
get hasIncreasedCost() {
return this.purchaseCount >= this.purchasesAtIncrease;
}
get costIncrease() {
return this.hasIncreasedCost ? 1e10 : 10;
}
get isCapped() {
return this.cost.gte(this.config.costCap);
}
get isBought() {
return this.isCapped;
}
get isRequirementSatisfied() {
return Achievement(41).isUnlocked;
}
get canBeBought() {
return !Pelle.isDoomed && !this.isCapped && Currency.infinityPoints.gte(this.cost) && this.isRequirementSatisfied;
}
// This is only ever called with amount = 1 or within buyMax under conditions that ensure the scaling doesn't
// change mid-purchase
purchase(amount = 1) {
if (!this.canBeBought) return;
if (!TimeStudy(181).isBought) {
Autobuyer.bigCrunch.bumpAmount(DC.D2.pow(amount));
}
Currency.infinityPoints.subtract(Decimal.sumGeometricSeries(amount, this.cost, this.costIncrease, 0));
player.IPMultPurchases += amount;
GameUI.update();
}
buyMax() {
if (!this.canBeBought) return;
if (!this.hasIncreasedCost) {
// Only allow IP below the softcap to be used
const availableIP = Currency.infinityPoints.value.clampMax(this.config.costIncreaseThreshold);
const purchases = Decimal.affordGeometricSeries(availableIP, this.cost, this.costIncrease, 0).toNumber();
if (purchases <= 0) return;
this.purchase(purchases);
}
// Do not replace it with `if else` - it's specifically designed to process two sides of threshold separately
// (for example, we have 1e4000000 IP and no mult - first it will go to (but not including) 1e3000000 and then
// it will go in this part)
if (this.hasIncreasedCost) {
const availableIP = Currency.infinityPoints.value.clampMax(this.config.costCap);
const purchases = Decimal.affordGeometricSeries(availableIP, this.cost, this.costIncrease, 0).toNumber();
if (purchases <= 0) return;
this.purchase(purchases);
}
}
}
InfinityUpgrade.ipMult = new InfinityIPMultUpgrade(GameDatabase.infinity.upgrades.ipMult);
export class BreakInfinityUpgrade extends SetPurchasableMechanicState {
get currency() {
return Currency.infinityPoints;
}
get set() {
return player.infinityUpgrades;
}
onPurchased() {
if (this.id === "postGalaxy") {
SpeedrunMilestones(7).tryComplete();
PelleStrikes.powerGalaxies.trigger();
}
}
}
(function() {
const db = GameDatabase.infinity.breakUpgrades;
const upgrade = props => new BreakInfinityUpgrade(props);
BreakInfinityUpgrade.totalAMMult = upgrade(db.totalAMMult);
BreakInfinityUpgrade.currentAMMult = upgrade(db.currentAMMult);
BreakInfinityUpgrade.galaxyBoost = upgrade(db.galaxyBoost);
BreakInfinityUpgrade.infinitiedMult = upgrade(db.infinitiedMult);
BreakInfinityUpgrade.achievementMult = upgrade(db.achievementMult);
BreakInfinityUpgrade.slowestChallengeMult = upgrade(db.slowestChallengeMult);
BreakInfinityUpgrade.infinitiedGen = upgrade(db.infinitiedGen);
BreakInfinityUpgrade.autobuyMaxDimboosts = upgrade(db.autobuyMaxDimboosts);
BreakInfinityUpgrade.autobuyerSpeed = upgrade(db.autobuyerSpeed);
}());
class RebuyableBreakInfinityUpgradeState extends RebuyableMechanicState {
get currency() {
return Currency.infinityPoints;
}
get boughtAmount() {
return player.infinityRebuyables[this.id];
}
set boughtAmount(value) {
player.infinityRebuyables[this.id] = value;
}
get isCapped() {
return this.boughtAmount === this.config.maxUpgrades;
}
}
BreakInfinityUpgrade.tickspeedCostMult = new class extends RebuyableBreakInfinityUpgradeState {
onPurchased() {
GameCache.tickSpeedMultDecrease.invalidate();
}
}(GameDatabase.infinity.breakUpgrades.tickspeedCostMult);
BreakInfinityUpgrade.dimCostMult = new class extends RebuyableBreakInfinityUpgradeState {
onPurchased() {
GameCache.dimensionMultDecrease.invalidate();
}
}(GameDatabase.infinity.breakUpgrades.dimCostMult);
BreakInfinityUpgrade.ipGen = new RebuyableBreakInfinityUpgradeState(GameDatabase.infinity.breakUpgrades.ipGen);
export function preProductionGenerateIP(diff) {
if (InfinityUpgrade.ipGen.isBought) {
const genPeriod = Time.bestInfinity.totalMilliseconds * 10;

View File

@ -1,5 +1,5 @@
import { DC } from "./constants.js";
import { SpeedrunMilestones } from "./speedrun.js";
import { DC } from "./constants";
import { SpeedrunMilestones } from "./speedrun";
class BlackHoleUpgradeState {
constructor(config) {
@ -33,6 +33,12 @@ class BlackHoleUpgradeState {
purchase() {
if (!this.isAffordable || this.value === 0) return;
// Keep the cycle phase consistent before and after purchase so that upgrading doesn't cause weird behavior
// such as immediately activating it when inactive (or worse, skipping past the active segment entirely).
const bh = BlackHole(this.id);
const beforeProg = bh.isCharged ? 1 - bh.stateProgress : bh.stateProgress;
Currency.realityMachines.purchase(this.cost);
this.incrementAmount();
this._lazyValue.invalidate();
@ -40,6 +46,13 @@ class BlackHoleUpgradeState {
if (this.onPurchase) {
this.onPurchase();
}
// Adjust the phase to what it was before purchase by changing it directly. This will often result in passing
// in a negative argument to updatePhase(), but this shouldn't cause any problems because it'll never make
// the phase itself negative. In very rare cases this may result in a single auto-pause getting skipped
const stateTime = bh.isCharged ? bh.duration : bh.interval;
bh.updatePhase(stateTime * beforeProg - bh.phase);
EventHub.dispatch(GAME_EVENT.BLACK_HOLE_UPGRADE_BOUGHT);
}
}
@ -134,7 +147,7 @@ class BlackHoleState {
// When inactive, returns time until active; when active, returns time until inactive (or paused for hole 2)
get timeToNextStateChange() {
let remainingTime = this.timeWithPreviousActiveToNextStateChange;
const remainingTime = this.timeWithPreviousActiveToNextStateChange;
if (this.id === 1) return remainingTime;
@ -143,15 +156,29 @@ class BlackHoleState {
if (BlackHole(1).isCharged) return Math.min(remainingTime, BlackHole(1).timeToNextStateChange);
return BlackHole(1).timeToNextStateChange;
}
if (BlackHole(1).isCharged) {
if (remainingTime < BlackHole(1).timeToNextStateChange) return remainingTime;
remainingTime -= BlackHole(1).timeToNextStateChange;
return BlackHole(1).timeUntilTimeActive(remainingTime);
}
let totalTime = BlackHole(1).isCharged
? BlackHole(1).timeToNextStateChange + BlackHole(1).interval
: BlackHole(1).timeToNextStateChange;
totalTime += Math.floor(remainingTime / BlackHole(1).duration) * BlackHole(1).cycleLength;
totalTime += remainingTime % BlackHole(1).duration;
// Given x, return time it takes for this black hole to get x time active
timeUntilTimeActive(inputTimeActive) {
// Avoid error about reassigning parameter.
let timeActive = inputTimeActive;
if (this.isCharged) {
// We start at the next full activation, so if we have a partial activation
// then that reduces the time required.
// Make sure to handle the case when the current partial activation is enough.
if (timeActive < this.timeToNextStateChange) return timeActive;
// If it's not enough, we can subtract it from our time.
timeActive -= this.timeToNextStateChange;
}
// Determine the time until the next full activation.
let totalTime = this.isCharged
? this.timeToNextStateChange + this.interval
: this.timeToNextStateChange;
// This is the number of full cycles needed...
totalTime += Math.floor(timeActive / this.duration) * this.cycleLength;
// And the time from a partial cycle.
totalTime += timeActive % this.duration;
return totalTime;
}
@ -171,7 +198,7 @@ class BlackHoleState {
return `<i class="fas fa-expand-arrows-alt u-fa-padding"></i> Pulsing`;
}
if (Enslaved.isStoringGameTime) {
if (Ra.has(RA_UNLOCKS.ADJUSTABLE_STORED_TIME)) {
if (Ra.unlocks.adjustableStoredTime.canBeApplied) {
const storedTimeWeight = player.celestials.enslaved.storedFraction;
if (storedTimeWeight !== 0) {
return `<i class="fas fa-compress-arrows-alt"></i> Charging (${formatPercents(storedTimeWeight, 1)})`;
@ -221,37 +248,6 @@ class BlackHoleState {
// will this cause other bugs?
this._data.phase += activePeriod;
// This conditional is a bit convoluted because the more straightforward check of just pausing if it activates
// soon will result in it pausing every tick, including the tick it gets manually unpaused. This is unintuitive
// because it forces the player to change auto-pause modes every time it reaches activation again. Instead, we
// check if before the conditional is false before this tick and true afterwards; this ensures it only ever pauses
// once per cycle, right at the activation threshold. We give it a buffer equal to the acceleration time so that
// it's at full speed once by the time it actually activates.
const beforeTick = this.phase - activePeriod, afterTick = this.phase;
const threhold = this.interval - BlackHoles.ACCELERATION_TIME;
const willActivateOnUnpause = !this.isActive && beforeTick < threhold && afterTick >= threhold;
switch (player.blackHoleAutoPauseMode) {
case BLACK_HOLE_PAUSE_MODE.NO_PAUSE:
break;
case BLACK_HOLE_PAUSE_MODE.PAUSE_BEFORE_BH1:
if (this.id === 1 && willActivateOnUnpause) {
BlackHoles.togglePause();
GameUI.notify.blackHole(`${RealityUpgrade(20).isBought ? "Black Holes" : "Black Hole"}
automatically paused.`);
return;
}
break;
case BLACK_HOLE_PAUSE_MODE.PAUSE_BEFORE_BH2:
if (willActivateOnUnpause && (this.id === 2 || (this.id === 1 && BlackHole(2).isCharged))) {
BlackHoles.togglePause();
GameUI.notify.blackHole(`Black Holes automatically paused.`);
return;
}
break;
default:
throw new Error("Unrecognized BH offline pausing mode");
}
if (this.phase >= this.cycleLength) {
// One activation for each full cycle.
this._data.activations += Math.floor(this.phase / this.cycleLength);
@ -355,15 +351,22 @@ export const BlackHoles = {
Currency.realityMachines.purchase(100);
SpeedrunMilestones(17).tryComplete();
Achievement(144).unlock();
EventHub.dispatch(GAME_EVENT.BLACK_HOLE_UNLOCKED);
},
togglePause: () => {
togglePause: (automatic = false) => {
if (!BlackHoles.areUnlocked) return;
if (player.blackHolePause) player.requirementChecks.reality.slowestBH = 1;
player.blackHolePause = !player.blackHolePause;
player.blackHolePauseTime = player.records.realTimePlayed;
const pauseType = BlackHoles.areNegative ? "inverted" : "paused";
GameUI.notify.blackHole(player.blackHolePause ? `Black Hole ${pauseType}` : "Black Hole unpaused");
const blackHoleString = RealityUpgrade(20).isBought ? "Black Holes" : "Black Hole";
// If black holes are going unpaused -> paused, use "inverted" or "paused" depending o
// 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 automaticString = automatic ? "automatically " : "";
GameUI.notify.blackHole(`${blackHoleString} ${automaticString}${pauseType}`);
},
get unpauseAccelerationFactor() {
@ -388,12 +391,17 @@ export const BlackHoles = {
if (!this.areUnlocked || this.arePaused) return;
// This code is intended to successfully update the black hole phases
// even for very large values of blackHoleDiff.
const seconds = blackHoleDiff / 1000;
const activePeriods = this.realTimePeriodsWithBlackHoleActive(seconds);
// With auto-pause settings, this code also has to take account of that.
const rawSeconds = blackHoleDiff / 1000;
const [autoPause, seconds] = this.autoPauseData(rawSeconds);
const activePeriods = this.realTimePeriodsWithBlackHoleActive(seconds, true);
for (const blackHole of this.list) {
if (!blackHole.isUnlocked) break;
blackHole.updatePhase(activePeriods[blackHole.id - 1]);
}
if (autoPause) {
BlackHoles.togglePause(true);
}
},
/**
@ -483,6 +491,7 @@ export const BlackHoles = {
const speedupWithoutBlackHole = getGameSpeedupFactor(effectsToConsider);
const speedups = [1];
effectsToConsider.push(GAME_SPEED_EFFECT.BLACK_HOLE);
// Crucial thing: this works even if the black holes are paused, it's just that the speedups will be 1.
for (const blackHole of this.list) {
if (!blackHole.isUnlocked) break;
speedups.push(getGameSpeedupFactor(effectsToConsider, blackHole.id) / speedupWithoutBlackHole);
@ -491,7 +500,13 @@ export const BlackHoles = {
},
calculateGameTimeFromRealTime(realTime, speedups) {
const effectivePeriods = this.realTimePeriodsWithBlackHoleEffective(realTime, speedups);
// We could do this.autoPauseData(realTime)[1] here but that seems less clear.
// Using _ as an unused variable should be reasonable.
// eslint-disable-next-line no-unused-vars
const [_, realerTime] = this.autoPauseData(realTime);
const effectivePeriods = this.realTimePeriodsWithBlackHoleEffective(realerTime, speedups);
// This adds in time with black holes paused at the end of the list.
effectivePeriods[0] += realTime - realerTime;
return effectivePeriods
.map((period, i) => period * speedups[i])
.sum();
@ -535,5 +550,119 @@ export const BlackHoles = {
activePeriods.push(activeTime);
}
return activePeriods;
},
/**
* Takes BH number (1 or 2) and number of steps to do in an internal BH simulation.
* Returns real time until we can pause before given BH (i.e., we have a gap of at least 5 seconds before it),
* or null if we can't pause before it.
*/
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;
}
// 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.
// I think the bound of 100 means it can fail only in the case one black hole interval is under 5s
// and the other isn't. In practice, by this point the other interval is usually about 15 seconds
// and both durations are fairly long (a few minutes), making the longest that a gap between activations
// can be 20 seconds (so it's fairly OK not to pause).
// Precalculate some stuff that won't change (or in the case of charged and phases, stuff we'll change ourself
// but just in this simulation) while we call this function.
const charged = [BlackHole(1).isCharged, BlackHole(2).isCharged];
const phases = [BlackHole(1).phase, BlackHole(2).phase];
const durations = [BlackHole(1).duration, BlackHole(2).duration];
const intervals = [BlackHole(1).interval, BlackHole(2).interval];
// This is technically somewhat incorrect, because assuming durations aren't tiny, the maximum
// possible gap between BH2 activations is the *sum* of the intervals. However, that's still 10 seconds
// if this conditional is true, and pausing the BH because of a 10-second activation gap
// doesn't seem to make much sense. If this is an issue, we could use the sum of the intervals.
// This should also stop this function from being relatively computationally expensive
// if both intervals are 3 seconds (so the next pause would be when they happen to align,
// which is rare and will probably lead to a full 100 steps).
if (intervals[0] <= BlackHoles.ACCELERATION_TIME && intervals[1] <= BlackHoles.ACCELERATION_TIME) {
return null;
}
// Make a list of things to bound phase by.
const phaseBoundList = [[intervals[0]], [durations[0], intervals[1]], [durations[0], durations[1]]];
// Time tracking.
let inactiveTime = 0;
let totalTime = 0;
for (let i = 0; i < steps; i++) {
// Currently active BH (if BH1 and BH2 are both charged, 2,
// if only BH1 is, 1, if BH1 isn't, 0 regardless of BH2).
// eslint-disable-next-line no-nested-ternary
const current = charged[0] ? (charged[1] ? 2 : 1) : 0;
// Get the list of phase bounds.
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];
if (current === 2) {
// Check if there was enough time before this activation to pause.
if (inactiveTime >= BlackHoles.ACCELERATION_TIME) {
return totalTime - BlackHoles.ACCELERATION_TIME;
}
// Not enough time, reset inactive time to 0.
inactiveTime = 0;
} else {
// BH2 is inactive, add to inactive time.
inactiveTime += minTime;
}
// Add to total time in any case.
totalTime += minTime;
// If BH1 is active we should update BH2.
if (current > 0) {
phases[1] += minTime;
if (phases[1] >= phaseBounds[1]) {
charged[1] = !charged[1];
phases[1] -= phaseBounds[1];
}
}
// Update BH1 no matter what.
phases[0] += minTime;
if (phases[0] >= phaseBounds[0]) {
charged[0] = !charged[0];
phases[0] -= phaseBounds[0];
}
}
// We didn't activate so we return null.
return null;
},
/**
* Takes amount of real time.
* Returns 2-item array:
* [will BH be paused in the given amount of real time, real time until pause if so].
*/
autoPauseData(realTime) {
// This can be called when determining offline time if the black holes are already paused.
// In that case we don't need to pause them (need to pause = false), but they're already paused (0 time).
// This saves us some computation.
if (this.arePaused) return [false, 0];
if (player.blackHoleAutoPauseMode === BLACK_HOLE_PAUSE_MODE.NO_PAUSE) {
return [false, realTime];
}
const timeLeft = this.timeToNextPause(player.blackHoleAutoPauseMode);
// Cases in which we don't pause in the given amount of real time:
// null = no pause, (timeLeft < 1e-9) = we auto-paused and there was maybe rounding error,
// now the player's unpaused at this exact point (so we shouldn't pause again),
// (timeLeft > realTime) = we will pause but it'll take longer than the given time.
if (timeLeft === null || timeLeft < 1e-9 || timeLeft > realTime) {
return [false, realTime];
}
return [true, timeLeft];
}
};

View File

@ -0,0 +1,48 @@
import { RebuyableMechanicState, SetPurchasableMechanicState } from "./game-mechanics/index";
import { SpeedrunMilestones } from "./speedrun";
export class BreakInfinityUpgradeState extends SetPurchasableMechanicState {
get currency() {
return Currency.infinityPoints;
}
get set() {
return player.infinityUpgrades;
}
onPurchased() {
if (this.id === "postGalaxy") {
SpeedrunMilestones(7).tryComplete();
PelleStrikes.powerGalaxies.trigger();
}
}
}
class RebuyableBreakInfinityUpgradeState extends RebuyableMechanicState {
get currency() {
return Currency.infinityPoints;
}
get boughtAmount() {
return player.infinityRebuyables[this.id];
}
set boughtAmount(value) {
player.infinityRebuyables[this.id] = value;
}
get isCapped() {
return this.boughtAmount === this.config.maxUpgrades;
}
onPurchased() {
this.config.onPurchased?.();
}
}
export const BreakInfinityUpgrade = mapGameDataToObject(
GameDatabase.infinity.breakUpgrades,
config => (config.rebuyable
? new RebuyableBreakInfinityUpgradeState(config)
: new BreakInfinityUpgradeState(config))
);

View File

@ -82,6 +82,14 @@ export const GameCache = {
buyablePerks: new Lazy(() => Perks.all.filter(p => p.canBeBought)),
// Cached because it needs to be checked upon any change to antimatter, but that's a hot path and we want to keep
// unnecessary repetitive calculations and accessing to a minimum
cheapestAntimatterAutobuyer: new Lazy(() => Autobuyer.antimatterDimension.zeroIndexed.concat(Autobuyer.tickspeed)
.filter(ab => !ab.isBought)
.map(ab => ab.antimatterCost.toNumber())
.min()
),
// The effect is defined in antimatter_dimensions.js because that's where the non-cached
// code originally lived.
antimatterDimensionCommonMultiplier: new Lazy(() => antimatterDimensionCommonMultiplier()),
@ -94,8 +102,14 @@ export const GameCache = {
timeDimensionCommonMultiplier: new Lazy(() => timeDimensionCommonMultiplier()),
glyphInventorySpace: new Lazy(() => Glyphs.freeInventorySpace),
glyphEffects: new Lazy(() => orderedEffectList.mapToObject(k => k, k => getAdjustedGlyphEffectUncached(k))),
staticGlyphWeights: new Lazy(() => staticGlyphWeights()),
logTotalGlyphSacrifice: new Lazy(() => GlyphSacrificeHandler.logTotalSacrifice),
totalIPMult: new Lazy(() => totalIPMult()),
challengeTimeSum: new Lazy(() => player.challenge.normal.bestTimes.sum()),
@ -104,7 +118,9 @@ export const GameCache = {
};
EventHub.logic.on(GAME_EVENT.GLYPHS_CHANGED, () => {
GameCache.glyphInventorySpace.invalidate();
GameCache.glyphEffects.invalidate();
GameCache.staticGlyphWeights.invalidate();
}, GameCache.glyphEffects);
GameCache.antimatterDimensionFinalMultipliers.invalidate = function() {

View File

@ -1,7 +1,9 @@
import { GameDatabase } from "../secret-formula/game-database.js";
import { GameMechanicState } from "../game-mechanics/index.js";
import { CelestialQuotes } from "./quotes.js";
import { SpeedrunMilestones } from "../speedrun.js";
import { BitUpgradeState, GameMechanicState } from "../game-mechanics/index";
import { GameDatabase } from "../secret-formula/game-database";
import { SpeedrunMilestones } from "../speedrun";
import { Quotes } from "./quotes";
/**
* Information about how to format runUnlocks:
@ -25,13 +27,13 @@ class VRunUnlockState extends GameMechanicState {
}
get canBeReduced() {
return this.completions < this.config.values.length &&
return this.completions < this.config.values.length && this.completions !== 0 &&
new Decimal(this.reduction).neq(this.config.maxShardReduction(this.conditionBaseValue));
}
get isReduced() {
if (player.celestials.v.goalReductionSteps[this.id] === 0) return false;
return (V.has(V_UNLOCKS.SHARD_REDUCTION) && this.reduction > 0);
return (VUnlocks.shardReduction.canBeApplied && this.reduction > 0);
}
get reductionCost() {
@ -82,17 +84,52 @@ class VRunUnlockState extends GameMechanicState {
this.completions++;
GameUI.notify.success(`You have unlocked V-Achievement '${this.config.name}' tier ${this.completions}`);
for (const quote of Object.values(V.quotes)) {
// Quotes without requirements will be shown in other ways - need to check if it exists before calling though
if (quote.requirement && quote.requirement()) {
// TODO If multiple quotes show up simultaneously, this only seems to actually show one of them and skips the
// rest. This might be related to the modal stacking issue
V.quotes.show(quote);
V.updateTotalRunUnlocks();
for (const quote of V.quotes.all) {
// Quotes without requirements will be shown in other ways
if (quote.requirement) {
quote.show();
}
}
}
}
}
V.updateTotalRunUnlocks();
class VUnlockState extends BitUpgradeState {
get bits() { return player.celestials.v.unlockBits; }
set bits(value) { player.celestials.v.unlockBits = value; }
get pelleDisabled() {
return Pelle.isDoomed && this !== VUnlocks.vAchievementUnlock;
}
get isEffectActive() {
return this.isUnlocked && !this.pelleDisabled;
}
get description() {
return typeof this.config.description === "function" ? this.config.description()
: this.config.description;
}
get rewardText() {
return typeof this.config.reward === "function" ? this.config.reward()
: this.config.reward;
}
get canBeUnlocked() {
return this.config.requirement() && !this.isUnlocked;
}
get formattedEffect() {
if (!this.config.effect || !this.config.format) return "";
return this.config.format(this.effectValue);
}
onUnlock() {
GameUI.notify.success(this.description);
}
}
@ -109,75 +146,19 @@ export const VRunUnlocks = {
all: VRunUnlock.index.compact(),
};
export const V_UNLOCKS = {
V_ACHIEVEMENT_UNLOCK: {
id: 0,
reward: "Unlock V, The Celestial Of Achievements",
description: "Meet all the above requirements simultaneously",
requirement: () => Object.values(GameDatabase.celestials.v.mainUnlock).every(e => e.progress() >= 1)
},
SHARD_REDUCTION: {
id: 1,
reward: () => `You can spend Perk Points to reduce the goal requirement of all tiers of each V-Achievement.`,
get description() { return `Have ${formatInt(2)} V-Achievements`; },
requirement: () => V.spaceTheorems >= 2
},
ND_POW: {
id: 2,
reward: "Antimatter Dimension power based on total Space Theorems.",
get description() { return `Have ${formatInt(5)} V-Achievements`; },
effect: () => 1 + Math.sqrt(V.spaceTheorems) / 100,
format: x => formatPow(x, 3, 3),
requirement: () => V.spaceTheorems >= 5
},
FAST_AUTO_EC: {
id: 3,
reward: "Achievement multiplier reduces Auto-EC completion time.",
get description() { return `Have ${formatInt(10)} V-Achievements`; },
effect: () => Achievements.power,
// Base rate is 60 ECs at 20 minutes each
format: x => (Ra.has(RA_UNLOCKS.AUTO_RU_AND_INSTANT_EC)
? "Instant (Ra upgrade)"
: `${TimeSpan.fromMinutes(60 * 20 / x).toStringShort()} for full completion`),
requirement: () => V.spaceTheorems >= 10
},
AUTO_AUTOCLEAN: {
id: 4,
reward: "Unlock the ability to Auto Purge on Reality.",
get description() { return `Have ${formatInt(16)} V-Achievements`; },
requirement: () => V.spaceTheorems >= 16
},
ACHIEVEMENT_BH: {
id: 5,
reward: "Achievement multiplier affects Black Hole power.",
get description() { return `Have ${formatInt(30)} V-Achievements`; },
effect: () => Achievements.power,
format: x => formatX(x, 2, 0),
requirement: () => V.spaceTheorems >= 30
},
RA_UNLOCK: {
id: 6,
get reward() {
return `Reduce the Space Theorem cost of Time Studies by ${formatInt(2)}.
Unlock Ra, Celestial of the Forgotten.`;
},
get description() { return `Have ${formatInt(36)} V-Achievements`; },
requirement: () => V.spaceTheorems >= 36
}
};
export const VUnlocks = mapGameDataToObject(
GameDatabase.celestials.v.unlocks,
config => new VUnlockState(config)
);
export const V = {
displayName: "V",
possessiveName: "V's",
spaceTheorems: 0,
checkForUnlocks() {
for (const key of Object.keys(V_UNLOCKS)) {
const unl = V_UNLOCKS[key];
if (unl.id === V_UNLOCKS.V_ACHIEVEMENT_UNLOCK.id) continue;
if (unl.requirement() && !this.has(unl)) {
// eslint-disable-next-line no-bitwise
player.celestials.v.unlockBits |= (1 << unl.id);
GameUI.notify.success(unl.description);
}
for (const unl of VUnlocks.all) {
if (unl === VUnlocks.vAchievementUnlock) continue;
unl.unlock();
}
if (this.isRunning) {
@ -187,27 +168,22 @@ export const V = {
if (this.spaceTheorems >= 36) SpeedrunMilestones(22).tryComplete();
}
if (V.has(V_UNLOCKS.RA_UNLOCK) && !Ra.has(RA_UNLOCKS.AUTO_TP)) {
if (VUnlocks.raUnlock.canBeApplied && !Ra.unlocks.autoTP.canBeApplied) {
Ra.checkForUnlocks();
}
},
get canUnlockCelestial() {
return V_UNLOCKS.V_ACHIEVEMENT_UNLOCK.requirement();
return VUnlocks.vAchievementUnlock.canBeUnlocked;
},
unlockCelestial() {
// eslint-disable-next-line no-bitwise
player.celestials.v.unlockBits |= (1 << V_UNLOCKS.V_ACHIEVEMENT_UNLOCK.id);
GameUI.notify.success("You have unlocked V, The Celestial Of Achievements!");
V.quotes.show(V.quotes.UNLOCK);
},
has(info) {
// eslint-disable-next-line no-bitwise
return Boolean(player.celestials.v.unlockBits & (1 << info.id));
player.celestials.v.unlockBits |= (1 << VUnlocks.vAchievementUnlock.id);
GameUI.notify.success("You have unlocked V, The Celestial Of Achievements!", 10000);
V.quotes.unlock.show();
},
initializeRun() {
clearCelestialRuns();
player.celestials.v.run = true;
this.quotes.show(this.quotes.REALITY_ENTER);
this.quotes.realityEnter.show();
},
updateTotalRunUnlocks() {
let sum = 0;
@ -237,7 +213,7 @@ export const V = {
return player.celestials.v.run;
},
get isFlipped() {
return Ra.has(RA_UNLOCKS.HARD_V);
return Ra.unlocks.unlockHardV.isUnlocked;
},
get isFullyCompleted() {
return this.spaceTheorems >= 66;
@ -248,100 +224,10 @@ export const V = {
nextHardReductionCost(currReductionSteps) {
return 1000 * Math.pow(1.15, currReductionSteps);
},
quotes: new CelestialQuotes("v", {
INITIAL: CelestialQuotes.singleLine(
1, "How pathetic..."
),
UNLOCK: {
id: 2,
lines: [
"Welcome to my Reality.",
"I am surprised you could reach it.",
"This is my realm after all...",
"Not everyone is as great as me.",
]
},
REALITY_ENTER: {
id: 3,
lines: [
"Good luck with that!",
"You will need it.",
"My reality is flawless. You will fail.",
]
},
REALITY_COMPLETE: {
id: 4,
lines: [
"So fast...",
"Do not think so much of yourself.",
"This is just the beginning.",
"You will never be better than me.",
]
},
ACHIEVEMENT_1: {
id: 5,
requirement: () => V.spaceTheorems >= 1,
lines: [
"Only one? Pathetic.",
"Your accomplishments pale in comparison to mine.",
]
},
ACHIEVEMENT_6: {
id: 6,
requirement: () => V.spaceTheorems >= 6,
lines: [
"This is nothing.",
"Do not be so full of yourself.",
]
},
HEX_1: {
id: 7,
requirement: () => player.celestials.v.runUnlocks.filter(a => a === 6).length >= 1,
lines: [
"Do not think it will get any easier from now on.",
"You are awfully proud for such a little achievement.",
]
},
ACHIEVEMENT_12: {
id: 8,
requirement: () => V.spaceTheorems >= 12,
lines: [
"How did you...",
"This barely amounts to anything!",
"You will never complete them all.",
]
},
ACHIEVEMENT_24: {
id: 9,
requirement: () => V.spaceTheorems >= 24,
lines: [
"Impossible...",
"After how difficult it was for me...",
]
},
HEX_3: {
id: 10,
requirement: () => player.celestials.v.runUnlocks.filter(a => a === 6).length >= 3,
lines: [
"No... No... No...",
"This cannot be...",
]
},
ALL_ACHIEVEMENTS: {
id: 11,
requirement: () => V.spaceTheorems >= 36,
lines: [
"I... how did you do it...",
"I worked so hard to get them...",
"I am the greatest...",
"No one is better than me...",
"No one... no one... no on-",
]
}
}),
quotes: Quotes.v,
symbol: "⌬"
};
EventHub.logic.on(GAME_EVENT.TAB_CHANGED, () => {
if (Tab.celestials.v.isOpen) V.quotes.show(V.quotes.INITIAL);
if (Tab.celestials.v.isOpen) V.quotes.initial.show();
});

View File

@ -1,10 +1,10 @@
import { Teresa } from "./teresa.js";
import { Effarig } from "./effarig.js";
import { Enslaved } from "./enslaved.js";
import { V } from "./V.js";
import { Ra } from "./ra/ra.js";
import { Laitela } from "./laitela/laitela.js";
import { Effarig } from "./effarig";
import { Enslaved } from "./enslaved";
import { Laitela } from "./laitela/laitela";
import { Pelle } from "./pelle/pelle";
import { Ra } from "./ra/ra";
import { Teresa } from "./teresa";
import { V } from "./V";
export const Celestials = {
teresa: Teresa,
@ -19,59 +19,95 @@ export const Celestials = {
GameDatabase.celestials.descriptions = [
{
name: "Teresa",
description() {
return `Glyph Time Theorem generation is disabled and\
you gain less Infinity Points and Eternity Points (x^${format(0.55, 2, 2)}).`;
effects() {
return `Glyph Time Theorem generation is disabled.
You gain less Infinity Points and Eternity Points (x^${format(0.55, 2, 2)}).`;
},
},
{
name: "Effarig",
description() {
return `all Dimension multipliers, gamespeed, and tickspeed are severely lowered, like Dilation.
effects() {
return `All Dimension multipliers, game speed, and tickspeed are severely lowered, like Dilation.
Infinity Power reduces the production and game speed penalties and Time Shards reduce the tickspeed penalty.
Glyph levels are temporarily capped${Effarig.isRunning ? ` to ${Effarig.glyphLevelCap}` : ``},
rarity is unaffected. You will exit Effarig's Reality when you complete a Layer of it for the first time.`;
Glyph levels are temporarily capped to ${formatInt(Effarig.glyphLevelCap)}, rarity is unaffected.`;
},
description() {
return `You will exit Effarig's Reality when you complete a Layer of it for the first time.`;
}
},
{
name: "The Enslaved Ones",
description() {
return `\nGlyph levels will be boosted to a minimum of ${formatInt(5000)}
Infinity, Time, and 8th Antimatter Dimension purchases are limited to ${formatInt(1)} each
Antimatter Dimension multipliers are always Dilated (the Glyph effect still only applies in actual Dilation)
Time Study 192 (uncapped Replicanti) is locked
The Black Hole is disabled
Tachyon Particle production and Dilated Time production are severely reduced
Time Theorem generation from Dilation Glyphs is disabled
Certain challenge goals have been increased
Stored Time is discharged at a reduced effectiveness (exponent^${format(0.55, 2, 2)}) `;
name: "The Nameless Ones",
effects() {
return `Glyph levels are boosted to a minimum of ${formatInt(5000)}.
Infinity, Time, and 8th Antimatter Dimension purchases are limited to ${formatInt(1)} each.
Antimatter Dimension multipliers are always Dilated (the Glyph effect still only applies in actual Dilation).
Time Study 192 (uncapped Replicanti) is locked.
The Black Hole is disabled.
Tachyon Particle production and Dilated Time production are severely reduced.
Time Theorem generation from Dilation Glyphs is disabled.
Certain challenge goals are increased.
Stored game time is discharged at a reduced effectiveness (exponent^${format(0.55, 2, 2)}).`;
}
},
{
name: "V",
description() {
return `all Dimension multipliers, Eternity Point gain, Infinity Point gain, and Dilated Time gain per second\
are square-rooted, and Replicanti interval is squared.`;
effects() {
const vEffect = `All Dimension multipliers, Eternity Point gain, Infinity Point gain, and Dilated Time gain\
per second are square-rooted.
The Replicanti interval is squared.`;
const vEffectAdditional = `
The Exponential Glyph Alchemy effect is disabled.`;
return Ra.unlocks.unlockGlyphAlchemy.canBeApplied
? vEffect + vEffectAdditional
: vEffect;
}
},
{
name: "Ra",
description() {
return `you only have ${formatInt(4)} Dimension Boosts and can't gain any more, and the Tickspeed purchase
multiplier is fixed at ${formatX(1.1245, 0, 3)}.\n`;
effects() {
return `You only have ${formatInt(4)} Dimension Boosts and can't gain any more.
The Tickspeed purchase multiplier is fixed at ${formatX(1.1245, 0, 3)}.`;
},
},
{
name: "Lai'tela",
effects() {
let disabledDims;
const highestActive = 8 - Laitela.difficultyTier;
switch (highestActive) {
case 0:
disabledDims = "all Dimensions";
break;
case 1:
disabledDims = "2nd and higher Dimensions";
break;
case 2:
disabledDims = "3rd and higher Dimensions";
break;
case 7:
disabledDims = "8th Dimensions";
break;
default:
disabledDims = `${highestActive + 1}th and higher Dimensions`;
break;
}
const disabledText = highestActive === 8
? ""
: `Production from ${disabledDims} is disabled.`;
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.
${disabledText}`;
},
description() {
return `Infinity Point and Eternity Point gain are Dilated.\
Game speed is reduced to ${formatInt(1)} and gradually comes back over ${formatInt(10)} minutes,\
and Black Hole storing/discharging/pulsing are disabled.\n
Antimatter generates entropy inside of this Reality.\
return `Antimatter generates entropy inside of this Reality.\
At ${formatPercents(1)} entropy, the Reality becomes destabilized\
and you gain a reward based on how quickly you reached ${formatPercents(1)}.\
If you can destabilize in less than ${formatInt(30)} seconds, the Reality gives a stronger reward,\
but becomes significantly more difficult.`;
and you gain a reward based on how quickly you reached ${formatPercents(1)}.
Destabilizing the Reality in less than ${formatInt(30)} seconds makes it become significantly more difficult,\
in exchange for giving a much stronger reward.\
Doing this ${formatInt(8)} times will also give a ${formatX(8)} to Dark Energy gain.`;
}
},

View File

@ -1,7 +1,9 @@
import { GameDatabase } from "../secret-formula/game-database.js";
import { GameMechanicState } from "../game-mechanics/index.js";
import { CelestialQuotes } from "./quotes.js";
import { DC } from "../constants.js";
import { BitUpgradeState } from "../game-mechanics/index";
import { GameDatabase } from "../secret-formula/game-database";
import { DC } from "../constants";
import { Quotes } from "./quotes";
export const EFFARIG_STAGES = {
INFINITY: 1,
@ -12,16 +14,12 @@ export const EFFARIG_STAGES = {
export const Effarig = {
displayName: "Effarig",
possessiveName: "Effarig's",
initializeRun() {
const isRestarting = player.celestials.effarig.run;
clearCelestialRuns();
player.celestials.effarig.run = true;
recalculateAllGlyphs();
Tab.reality.glyphs.show(false);
if (!isRestarting) {
Modal.message.show(`Your Glyph levels have been limited to ${Effarig.glyphLevelCap}. Infinity Power
reduces the nerf to multipliers and game speed, and Time Shards reduce the nerf to tickspeed.`);
}
},
get isRunning() {
return player.celestials.effarig.run;
@ -55,16 +53,14 @@ export const Effarig = {
get glyphEffectAmount() {
const genEffectBitmask = Glyphs.activeList
.filter(g => generatedTypes.includes(g.type))
// eslint-disable-next-line no-bitwise
.reduce((prev, curr) => prev | curr.effects, 0);
const nongenEffectBitmask = Glyphs.activeList
.filter(g => !generatedTypes.includes(g.type))
// eslint-disable-next-line no-bitwise
.reduce((prev, curr) => prev | curr.effects, 0);
return countValuesFromBitmask(genEffectBitmask) + countValuesFromBitmask(nongenEffectBitmask);
},
get shardsGained() {
if (!Teresa.has(TERESA_UNLOCKS.EFFARIG)) return 0;
if (!TeresaUnlocks.effarig.canBeApplied) return 0;
return Math.floor(Math.pow(Currency.eternityPoints.exponent / 7500, this.glyphEffectAmount)) *
AlchemyResource.effarig.effectValue;
},
@ -101,139 +97,44 @@ export const Effarig = {
// Will return 0 if Effarig Infinity is uncompleted
return Math.floor(replicantiCap().pLog10() / LOG10_MAX_VALUE - 1);
},
quotes: new CelestialQuotes("effarig", {
INITIAL: {
id: 1,
lines: [
"Welcome to my humble abode.",
"I am Effarig, and I govern Glyphs.",
"I am different from Teresa; not as simplistic as you think.",
"I use the shards of Glyphs to enforce my will.",
"I collect them for the bounty of this realm.",
"What are you waiting for? Get started.",
]
},
UNLOCK_WEIGHTS: CelestialQuotes.singleLine(
2, "Do you like my little shop? It is not much, but it is mine."
),
UNLOCK_GLYPH_FILTER: CelestialQuotes.singleLine(
3, "This purchase will help you out."
),
UNLOCK_SET_SAVES: CelestialQuotes.singleLine(
4, "Is that too much? I think it is too much."
),
UNLOCK_RUN: {
id: 5,
lines: [
"You bought out my entire stock... well, at least I am rich now.",
"The heart of my Reality is suffering. Each Layer is harder than the last.",
"I hope you never complete it.",
]
},
COMPLETE_INFINITY: {
id: 6,
lines: [
"* You have completed Effarig's Infinity.",
"This is the first threshold. It only gets worse from here.",
"None but me know enough about my domain to get further.",
]
},
COMPLETE_ETERNITY: {
id: 7,
lines: [
"* You have completed Effarig's Eternity.",
"This is the limit. I do not want you to proceed past this point.",
"You will not finish this in your lifetime.",
"I will just wait here until you give up.",
]
},
COMPLETE_REALITY: {
id: 8,
lines: [
"* You have completed Effarig's Reality.",
"So this is the diabolical power... what frightened the others...",
"Do you think this was worth it? Trampling on what I have done?",
"And for what purpose? You could have joined, we could have cooperated.",
"But no. It is over. Leave while I cling onto what is left.",
]
}
}),
quotes: Quotes.effarig,
symbol: "Ϙ"
};
class EffarigUnlockState extends GameMechanicState {
constructor(config) {
super(config);
if (this.id < 0 || this.id > 31) throw new Error(`Id ${this.id} out of bit range`);
}
class EffarigUnlockState extends BitUpgradeState {
get bits() { return player.celestials.effarig.unlockBits; }
set bits(value) { player.celestials.effarig.unlockBits = value; }
get cost() {
return this.config.cost;
}
get isUnlocked() {
// eslint-disable-next-line no-bitwise
return Boolean(player.celestials.effarig.unlockBits & (1 << this.id));
}
get canBeApplied() {
return this.isUnlocked && !Pelle.isDisabled("effarig");
}
unlock() {
// eslint-disable-next-line no-bitwise
player.celestials.effarig.unlockBits |= (1 << this.id);
get isEffectActive() {
return !Pelle.isDisabled("effarig");
}
purchase() {
if (this.isUnlocked || !Currency.relicShards.purchase(this.cost)) return;
this.unlock();
switch (this) {
case EffarigUnlock.adjuster:
Effarig.quotes.show(Effarig.quotes.UNLOCK_WEIGHTS);
ui.view.tabs.reality.openGlyphWeights = true;
Tab.reality.glyphs.show();
break;
case EffarigUnlock.glyphFilter:
Effarig.quotes.show(Effarig.quotes.UNLOCK_GLYPH_FILTER);
player.reality.showSidebarPanel = GLYPH_SIDEBAR_MODE.FILTER_SETTINGS;
break;
case EffarigUnlock.setSaves:
Effarig.quotes.show(Effarig.quotes.UNLOCK_SET_SAVES);
player.reality.showSidebarPanel = GLYPH_SIDEBAR_MODE.SAVED_SETS;
break;
case EffarigUnlock.run:
Effarig.quotes.show(Effarig.quotes.UNLOCK_RUN);
break;
default:
throw new Error("Unknown Effarig upgrade");
}
this.config.onPurchased?.();
}
}
export const EffarigUnlock = (function() {
const db = GameDatabase.celestials.effarig.unlocks;
return {
adjuster: new EffarigUnlockState(db.adjuster),
glyphFilter: new EffarigUnlockState(db.glyphFilter),
setSaves: new EffarigUnlockState(db.setSaves),
run: new EffarigUnlockState(db.run),
infinity: new EffarigUnlockState(db.infinity),
eternity: new EffarigUnlockState(db.eternity),
reality: new EffarigUnlockState(db.reality),
};
}());
export const EffarigUnlock = mapGameDataToObject(
GameDatabase.celestials.effarig.unlocks,
config => new EffarigUnlockState(config)
);
EventHub.logic.on(GAME_EVENT.TAB_CHANGED, () => {
if (Tab.celestials.effarig.isOpen) Effarig.quotes.show(Effarig.quotes.INITIAL);
if (Tab.celestials.effarig.isOpen) Effarig.quotes.initial.show();
});
EventHub.logic.on(GAME_EVENT.BIG_CRUNCH_BEFORE, () => {
if (!Effarig.isRunning) return;
Effarig.quotes.show(Effarig.quotes.COMPLETE_INFINITY);
Effarig.quotes.completeInfinity.show();
});
EventHub.logic.on(GAME_EVENT.ETERNITY_RESET_BEFORE, () => {
if (!Effarig.isRunning) return;
Effarig.quotes.show(Effarig.quotes.COMPLETE_ETERNITY);
Effarig.quotes.completeEternity.show();
});

View File

@ -1,6 +1,7 @@
import { GameDatabase } from "../secret-formula/game-database.js";
import { GameMechanicState } from "../game-mechanics/index.js";
import { CelestialQuotes } from "./quotes.js";
import { BitUpgradeState } from "../game-mechanics/index";
import { GameDatabase } from "../secret-formula/game-database";
import { Quotes } from "./quotes";
export const ENSLAVED_UNLOCKS = {
FREE_TICKSPEED_SOFTCAP: {
@ -17,13 +18,14 @@ export const ENSLAVED_UNLOCKS = {
const hasRarityRequirement = strengthToRarity(player.records.bestReality.glyphStrength) >= 100;
return hasLevelRequirement && hasRarityRequirement;
},
description: () => `Unlock The Enslaved Ones' Reality (requires
description: () => `Unlock The Nameless Ones' Reality (requires
a level ${formatInt(5000)} Glyph and a ${formatRarity(100)} rarity Glyph)`,
}
};
export const Enslaved = {
displayName: "Enslaved",
displayName: "The Nameless Ones",
possessiveName: "The Nameless Ones'",
boostReality: false,
BROKEN_CHALLENGES: [2, 3, 4, 5, 7, 8, 10, 11, 12],
nextTickDiff: 50,
@ -35,36 +37,45 @@ export const Enslaved = {
currentBlackHoleStoreAmountPerMs: 0,
tachyonNerf: 0.3,
toggleStoreBlackHole() {
if (Pelle.isDoomed) return;
if (!this.canModifyGameTimeStorage) return;
player.celestials.enslaved.isStoring = !player.celestials.enslaved.isStoring;
player.celestials.enslaved.isStoringReal = false;
if (!Ra.has(RA_UNLOCKS.ADJUSTABLE_STORED_TIME)) {
if (!Ra.unlocks.adjustableStoredTime.canBeApplied) {
player.celestials.enslaved.storedFraction = 1;
}
},
toggleStoreReal() {
if (Pelle.isDoomed) return;
if (!this.canModifyRealTimeStorage && !this.isStoredRealTimeCapped) return;
player.celestials.enslaved.isStoringReal = !player.celestials.enslaved.isStoringReal;
player.celestials.enslaved.isStoring = false;
},
toggleAutoStoreReal() {
if (Pelle.isDoomed) return;
if (!this.canModifyRealTimeStorage) return;
player.celestials.enslaved.autoStoreReal = !player.celestials.enslaved.autoStoreReal;
},
get canModifyGameTimeStorage() {
return Enslaved.isUnlocked && !Pelle.isDoomed && !BlackHoles.arePaused && !EternityChallenge(12).isRunning &&
!Enslaved.isRunning && !Laitela.isRunning;
},
get canModifyRealTimeStorage() {
return Enslaved.isUnlocked && !Pelle.isDoomed;
},
get isStoredRealTimeCapped() {
return player.celestials.enslaved.storedReal < this.storedRealTimeCap;
},
// We assume that the situations where you can't modify time storage settings (of either type) are exactly the cases
// where they have also been explicitly disabled via other game mechanics. This also reduces UI boilerplate code.
get isStoringGameTime() {
return Enslaved.isUnlocked && player.celestials.enslaved.isStoring && !BlackHoles.arePaused &&
!EternityChallenge(12).isRunning && !Laitela.isRunning;
return this.canModifyGameTimeStorage && player.celestials.enslaved.isStoring;
},
get isStoringRealTime() {
return Enslaved.isUnlocked && player.celestials.enslaved.isStoringReal;
return this.canModifyRealTimeStorage && player.celestials.enslaved.isStoringReal;
},
get storedRealTimeEfficiency() {
return 0.7;
},
get storedRealTimeCap() {
const addedCap = Ra.has(RA_UNLOCKS.IMPROVED_STORED_TIME)
? RA_UNLOCKS.IMPROVED_STORED_TIME.effect.realTimeCap()
: 0;
const addedCap = Ra.unlocks.improvedStoredTime.effects.realTimeCap.effectOrDefault(0);
return 1000 * 3600 * 8 + addedCap;
},
get isAutoReleasing() {
@ -81,6 +92,8 @@ export const Enslaved = {
player.celestials.enslaved.isStoringReal = false;
player.celestials.enslaved.storedReal = maxTime;
}
// More than 24 hours in milliseconds
if (player.celestials.enslaved.storedReal > (24 * 60 * 60 * 1000)) SecretAchievement(46).unlock();
player.lastUpdate = thisUpdate;
},
autoStoreRealTime(diffMs) {
@ -96,9 +109,7 @@ export const Enslaved = {
},
// "autoRelease" should only be true when called with the Ra upgrade
useStoredTime(autoRelease) {
if (Pelle.isDoomed) return;
if (!this.canRelease(autoRelease)) return;
if (EternityChallenge(12).isRunning) return;
player.requirementChecks.reality.slowestBH = 1;
let release = player.celestials.enslaved.stored;
if (Enslaved.isRunning) {
@ -124,7 +135,7 @@ export const Enslaved = {
},
buyUnlock(info) {
if (!this.canBuy(info)) return false;
if (info.id === ENSLAVED_UNLOCKS.RUN.id) this.quotes.show(this.quotes.UNLOCK_RUN);
if (info.id === ENSLAVED_UNLOCKS.RUN.id) this.quotes.unlockRun.show();
player.celestials.enslaved.stored -= info.price;
player.celestials.enslaved.unlocks.push(info.id);
return true;
@ -132,16 +143,26 @@ export const Enslaved = {
initializeRun() {
clearCelestialRuns();
player.celestials.enslaved.run = true;
player.secretUnlocks.viewSecretTS = false;
player.celestials.enslaved.hasSecretStudy = false;
this.feltEternity = false;
this.quotes.show(this.quotes.START_RUN);
// Re-validation needs to be done here because this code gets called after the automator attempts to start.
// This is a special case for Nameless because it's one of the only two cases where a command becomes locked
// again (the other being Pelle entry, which just force-stops the automator entirely).
AutomatorData.recalculateErrors();
if (AutomatorBackend.state.mode === AUTOMATOR_MODE.RUN && AutomatorData.currentErrors().length) {
AutomatorBackend.stop();
GameUI.notify.error("This Reality forbids Black Holes! (Automator stopped)");
}
this.quotes.startRun.show();
},
get isRunning() {
return player.celestials.enslaved.run;
},
completeRun() {
player.celestials.enslaved.completed = true;
this.quotes.show(this.quotes.COMPLETE_REALITY);
this.quotes.completeReality.show();
},
get isCompleted() {
return player.celestials.enslaved.completed;
@ -154,6 +175,9 @@ export const Enslaved = {
return Math.max(baseRealityBoostRatio, Math.floor(player.celestials.enslaved.storedReal /
Math.max(1000, Time.thisRealityRealTime.totalMilliseconds)));
},
get canAmplify() {
return this.realityBoostRatio > 1 && !Pelle.isDoomed && !isInCelestialReality();
},
storedTimeInsideEnslaved(stored) {
if (stored <= 1e3) return stored;
return Math.pow(10, Math.pow(Math.log10(stored / 1e3), 0.55)) * 1e3;
@ -162,7 +186,8 @@ export const Enslaved = {
if (!this.feltEternity) {
EnslavedProgress.feelEternity.giveProgress();
this.feltEternity = true;
Modal.message.show("Time in Eternity will be scaled by number of Eternities");
Modal.message.show(`Time in this Eternity will be multiplied by number of Eternities,
up to a maximum of ${formatX(1e66)}.`, { closeEvent: GAME_EVENT.REALITY_RESET_AFTER }, 1);
}
},
get feltEternity() {
@ -188,101 +213,45 @@ export const Enslaved = {
}
return true;
},
quotes: new CelestialQuotes("enslaved", {
INITIAL: {
id: 1,
lines: [
"A visitor? I have not had one... eons.",
"I... had a name. It has been lost... to this place.",
"The others... will not let me rest. I do their work with time...",
"Place time... into places... that need it...",
"Watch myself grow... pass and die.",
"Perhaps you... will break these chains... I will wait.",
]
},
UNLOCK_RUN: {
id: 2,
lines: [
"The others... used me. They will use... or destroy you.",
"End my suffering... power will be yours...",
]
},
START_RUN: {
id: 3,
lines: [
"So little space... but no... prison... is perfect.",
"They squeezed... this Reality... too tightly. Cracks appeared.",
"Search... everywhere. I will help... where I can.",
]
},
COMPLETE_REALITY: {
id: 4,
lines: [
"All... fragments... clones... freed.",
"I have given... tools... of my imprisoning. Use them...",
"Freedom from torture... is torture itself.",
]
},
EC6C10: CelestialQuotes.singleLine(
5, "... did not... underestimate you..."
),
HINT_UNLOCK: {
id: 6,
lines: [
"... you need... to look harder...",
"I think... I can help...",
"* You have unlocked help from The Enslaved Ones."
]
},
}),
symbol: "<i class='fas fa-link'></i>"
quotes: Quotes.enslaved,
// Unicode f0c1.
symbol: "\uf0c1"
};
class EnslavedProgressState extends GameMechanicState {
constructor(config) {
super(config);
if (this.id < 0 || this.id > 31) throw new Error(`Id ${this.id} out of bit range`);
}
class EnslavedProgressState extends BitUpgradeState {
get bits() { return player.celestials.enslaved.hintBits; }
set bits(value) { player.celestials.enslaved.hintBits = value; }
get hasProgress() {
// eslint-disable-next-line no-bitwise
return Boolean(player.celestials.enslaved.progressBits & (1 << this.id));
}
get hasHint() {
// eslint-disable-next-line no-bitwise
return this.hasProgress || Boolean(player.celestials.enslaved.hintBits & (1 << this.id));
return this.hasProgress || this.isUnlocked;
}
get hintInfo() {
return this.config.hint;
}
get completedInfo() {
return typeof this.config.condition === "function" ? this.config.condition() : this.config.condition;
}
giveProgress() {
// Bump the last hint time appropriately if the player found the hint
if (this.hasHint && !this.hasProgress) {
player.celestials.enslaved.zeroHintTime -= Math.log(2) / Math.log(3) * TimeSpan.fromDays(1).totalMilliseconds;
GameUI.notify.success("You found a crack in The Enslaved Ones' Reality!");
GameUI.notify.success("You found a crack in The Nameless Ones' Reality!", 10000);
}
// eslint-disable-next-line no-bitwise
player.celestials.enslaved.progressBits |= (1 << this.id);
}
giveHint() {
// eslint-disable-next-line no-bitwise
player.celestials.enslaved.hintBits |= (1 << this.id);
}
}
export const EnslavedProgress = (function() {
const db = GameDatabase.celestials.enslaved.progress;
return {
hintsUnlocked: new EnslavedProgressState(db.hintsUnlocked),
ec1: new EnslavedProgressState(db.ec1),
feelEternity: new EnslavedProgressState(db.feelEternity),
ec6: new EnslavedProgressState(db.ec6),
c10: new EnslavedProgressState(db.c10),
secretStudy: new EnslavedProgressState(db.secretStudy),
storedTime: new EnslavedProgressState(db.storedTime),
challengeCombo: new EnslavedProgressState(db.challengeCombo),
};
}());
export const EnslavedProgress = mapGameDataToObject(
GameDatabase.celestials.enslaved.progress,
config => new EnslavedProgressState(config)
);
export const Tesseracts = {
get bought() {
@ -299,6 +268,7 @@ export const Tesseracts = {
buyTesseract() {
if (!this.canBuyTesseract) return;
if (GameEnd.creditsEverClosed) return;
player.celestials.enslaved.tesseracts++;
},
@ -335,5 +305,5 @@ export const Tesseracts = {
};
EventHub.logic.on(GAME_EVENT.TAB_CHANGED, () => {
if (Tab.celestials.enslaved.isOpen) Enslaved.quotes.show(Enslaved.quotes.INITIAL);
if (Tab.celestials.enslaved.isOpen) Enslaved.quotes.initial.show();
});

View File

@ -1,5 +1,5 @@
import { DimensionState } from "../../dimensions/dimension.js";
import { DC } from "../../constants.js";
import { DC } from "../../constants";
import { DimensionState } from "../../dimensions/dimension";
/**
* Constants for easily adjusting values
@ -50,8 +50,8 @@ export class DarkMatterDimensionState extends DimensionState {
const perUpgrade = INTERVAL_PER_UPGRADE;
const tierFactor = Math.pow(4, this.tier - 1);
return 1000 * tierFactor * Math.pow(perUpgrade, this.data.intervalUpgrades) *
Math.pow(SingularityMilestone.ascensionIntervalScaling.effectValue, this.ascensions) *
SingularityMilestone.darkDimensionIntervalReduction.effectValue;
Math.pow(SingularityMilestone.ascensionIntervalScaling.effectOrDefault(1200), this.ascensions) *
SingularityMilestone.darkDimensionIntervalReduction.effectOrDefault(1);
}
get interval() {
@ -70,7 +70,7 @@ export class DarkMatterDimensionState extends DimensionState {
}
get powerDMPerAscension() {
return POWER_DM_PER_ASCENSION + SingularityMilestone.improvedAscensionDM.effectValue;
return POWER_DM_PER_ASCENSION + SingularityMilestone.improvedAscensionDM.effectOrDefault(0);
}
get powerDM() {
@ -85,7 +85,7 @@ export class DarkMatterDimensionState extends DimensionState {
}
get powerDE() {
if (!this.isUnlocked) return 0;
if (!this.isUnlocked || Pelle.isDoomed) return 0;
const tierFactor = Math.pow(15, this.tier - 1);
const destabilizeBoost = Laitela.isFullyDestabilized ? 8 : 1;
return new Decimal(((1 + this.data.powerDEUpgrades * 0.1) *
@ -102,14 +102,14 @@ export class DarkMatterDimensionState extends DimensionState {
get intervalAfterAscension() {
const purchases = Decimal.affordGeometricSeries(Currency.darkMatter.value, this.rawIntervalCost,
this.intervalCostIncrease, 0).toNumber();
return Math.clampMin(this.intervalPurchaseCap, SingularityMilestone.ascensionIntervalScaling.effectValue *
return Math.clampMin(this.intervalPurchaseCap, SingularityMilestone.ascensionIntervalScaling.effectOrDefault(1200) *
this.rawInterval * Math.pow(INTERVAL_PER_UPGRADE, purchases));
}
get adjustedStartingCost() {
const tiers = [null, 0, 2, 5, 13];
return 10 * Math.pow(COST_MULT_PER_TIER, tiers[this.tier]) *
SingularityMilestone.darkDimensionCostReduction.effectValue;
SingularityMilestone.darkDimensionCostReduction.effectOrDefault(1);
}
get rawIntervalCost() {
@ -122,7 +122,7 @@ export class DarkMatterDimensionState extends DimensionState {
}
get intervalCostIncrease() {
return Math.pow(INTERVAL_COST_MULT, SingularityMilestone.intervalCostScalingReduction.effectValue);
return Math.pow(INTERVAL_COST_MULT, SingularityMilestone.intervalCostScalingReduction.effectOrDefault(1));
}
get rawPowerDMCost() {
@ -263,7 +263,7 @@ export const DarkMatterDimensions = {
dim.timeSinceLastUpdate -= dim.interval * ticks;
}
}
if (SingularityMilestone.dim4Generation.isUnlocked && Laitela.annihilationUnlocked) {
if (SingularityMilestone.dim4Generation.canBeApplied && Laitela.annihilationUnlocked) {
DarkMatterDimension(4).amount = DarkMatterDimension(4).amount
.plus(SingularityMilestone.dim4Generation.effectValue * realDiff / 1000);
}

View File

@ -1,9 +1,11 @@
import { CelestialQuotes } from "../quotes.js";
import { DC } from "../../constants.js";
import { DarkMatterDimensions } from "./dark-matter-dimension.js";
import { DC } from "../../constants";
import { Quotes } from "../quotes";
import { DarkMatterDimensions } from "./dark-matter-dimension";
export const Laitela = {
displayName: "Lai'tela",
possessiveName: "Lai'tela's",
get celestial() {
return player.celestials.laitela;
},
@ -44,7 +46,7 @@ export const Laitela = {
},
get matterExtraPurchaseFactor() {
return (1 + 0.5 * Math.pow(Decimal.pLog10(Currency.darkMatter.max) / 50, 0.4) *
(1 + SingularityMilestone.continuumMult.effectValue));
(1 + SingularityMilestone.continuumMult.effectOrDefault(0)));
},
get realityReward() {
return Math.clampMin(Math.pow(100, this.difficultyTier) *
@ -52,7 +54,7 @@ export const Laitela = {
},
// Note that entropy goes from 0 to 1, with 1 being completion
get entropyGainPerSecond() {
return Math.clamp(Math.pow(Currency.antimatter.value.log10() / 1e11, 2), 0, 100) / 200;
return Math.clamp(Math.pow(Currency.antimatter.value.add(1).log10() / 1e11, 2), 0, 100) / 200;
},
get darkMatterMultGain() {
return Decimal.pow(Currency.darkMatter.value.dividedBy(this.annihilationDMRequirement)
@ -78,7 +80,7 @@ export const Laitela = {
this.celestial.darkMatterMult += this.darkMatterMultGain;
DarkMatterDimensions.reset();
Currency.darkEnergy.reset();
Laitela.quotes.show(Laitela.quotes.ANNIHILATION);
Laitela.quotes.annihilation.show();
Achievement(176).unlock();
return true;
},
@ -119,135 +121,10 @@ export const Laitela = {
this.celestial.difficultyTier = 0;
this.celestial.singularityCapIncreases = 0;
},
quotes: new CelestialQuotes("laitela", {
UNLOCK: {
id: 1,
lines: [
"You finally reached me.",
"I guess it is time to reveal to you,",
"The secrets hidden beneath existence.",
"The omnipresent ruling perfection. Continuum.",
"And the binding keys to the multiverse,",
"Dark Matter and Dark Energy.",
"My knowledge is endless and my wisdom divine.",
"So you can play around all you want.",
"I am Lai'tela, the Celestial of Dimensions,",
"And I will be watching you forever.",
]
},
FIRST_DESTABILIZE: {
id: 2,
destabilize: 1,
lines: [
"It is fine. Unlike the others, I never had a Reality.",
"I built this one just now, precisely so it would collapse.",
"I can rebuild this Reality over and over, unlike them.",
"I could trap all of them if I wanted.",
"You will never find a way to overpower me.",
]
},
FIRST_SINGULARITY: {
id: 3,
singularities: 1,
lines: [
"It is weird, how all beings question things.",
"You are different. You can build and manipulate Dimensions.",
"Were you truly once one of them?",
"You have taken control of the darkness so quickly.",
"Molded them into Dimensions and Points just like one of us.",
"What... ARE you?",
]
},
// Note: This happens around e10-e11 singularities
ANNIHILATION: {
id: 4,
lines: [
"Back to square one.",
"We, the Celestials transcend time and existence.",
"We always know that whatever is lost always comes back eventually.",
"Even if we were to cease, we would just come back stronger.",
"The cycle... repeats forever.",
"Do they also understand? Or was it only you as well?",
"I feel like I should know the answer...",
]
},
HALF_DIMENSIONS: {
id: 5,
destabilize: 4,
lines: [
"You seem to be having too much fun.",
"Just like they did before meeting their... fate.",
"You freed them of their eternal imprisonment, yes?",
"I always regret how harsh I was that day.",
"Maybe it doesn't matter.",
"But I digress. Let's keep constricting this Reality.",
]
},
SINGULARITY_1: {
id: 6,
singularities: 1e8,
lines: [
"What was it again...? Antimatter?",
"That was the first thing you turned into Dimensions?",
"It could not have been an accident.",
"How did you... attain the power to control it?",
"This never happened in all of existence... or did it?",
"My endless knowledge... is it waning?",
]
},
SINGULARITY_2: {
id: 7,
singularities: 1e16,
lines: [
"Of those who tried to control dimensions...",
"Who were they? I cannot seem to remember...",
"And how... did they vanish?",
"Are they... us? Simply transcending existence?",
"Did they surpass us and become something we can't comprehend?",
"Are we all imprisoned in this falsity...",
]
},
SINGULARITY_3: {
id: 8,
singularities: 1e24,
lines: [
"Is this a cycle?",
"Will our existence just end and start anew...",
"Just like... the Dimensions I rule?",
"And if such... what will bring our end?",
"I knew the answer to all these questions...",
"But I forgot all of them...",
"Your power... is it... erasing mine...?",
]
},
SINGULARITY_4: {
id: 9,
singularities: 1e32,
lines: [
"I don't know for how much... longer I can hold.",
"There is... next to nothing left...",
"You have attained... complete and total mastery... over the dark...",
"While I can barely... hold onto my name anymore...",
"What am I meant to be doing anyways?",
"Did... my mistakes cause all of this?",
]
},
FULL_DESTABILIZE: {
id: 10,
destabilize: 8,
lines: [
"I feel... like I had something to say...",
"Who am I? I am not sure...",
"I cannot... hold onto the darkness any longer...",
"I... have nothing left...",
"Something about... destabilizing... collapsing...",
"The end...",
]
},
}),
quotes: Quotes.laitela,
symbol: "ᛝ"
};
EventHub.logic.on(GAME_EVENT.TAB_CHANGED, () => {
if (Tab.celestials.laitela.isOpen) Laitela.quotes.show(Laitela.quotes.UNLOCK);
if (Tab.celestials.laitela.isOpen) Laitela.quotes.unlock.show();
});

View File

@ -1,4 +1,5 @@
import { GameMechanicState } from "../../game-mechanics/index.js";
import { GameMechanicState } from "../../game-mechanics/index";
import { deepmergeAll } from "@/utility/deepmerge";
class SingularityMilestoneState extends GameMechanicState {
@ -30,14 +31,18 @@ class SingularityMilestoneState extends GameMechanicState {
return Currency.singularities.gte(this.start);
}
get increaseThreshold() {
return this.config.increaseThreshold;
}
nerfCompletions(completions) {
const softcap = this.config.increaseThreshold;
const softcap = this.increaseThreshold;
if (!softcap || (completions < softcap)) return completions;
return softcap + (completions - softcap) / 3;
}
unnerfCompletions(completions) {
const softcap = this.config.increaseThreshold;
const softcap = this.increaseThreshold;
if (!softcap || (completions < softcap)) return completions;
return softcap + (completions - softcap) * 3;
}
@ -94,42 +99,13 @@ class SingularityMilestoneState extends GameMechanicState {
}
}
export const SingularityMilestone = (function() {
const db = GameDatabase.celestials.singularityMilestones;
return {
continuumMult: new SingularityMilestoneState(db.continuumMult),
darkMatterMult: new SingularityMilestoneState(db.darkMatterMult),
darkEnergyMult: new SingularityMilestoneState(db.darkEnergyMult),
darkDimensionCostReduction: new SingularityMilestoneState(db.darkDimensionCostReduction),
singularityMult: new SingularityMilestoneState(db.singularityMult),
darkDimensionIntervalReduction: new SingularityMilestoneState(db.darkDimensionIntervalReduction),
ascensionIntervalScaling: new SingularityMilestoneState(db.ascensionIntervalScaling),
autoCondense: new SingularityMilestoneState(db.autoCondense),
darkDimensionAutobuyers: new SingularityMilestoneState(db.darkDimensionAutobuyers),
darkAutobuyerSpeed: new SingularityMilestoneState(db.darkAutobuyerSpeed),
improvedSingularityCap: new SingularityMilestoneState(db.improvedSingularityCap),
darkFromTesseracts: new SingularityMilestoneState(db.darkFromTesseracts),
dilatedTimeFromSingularities: new SingularityMilestoneState(db.dilatedTimeFromSingularities),
darkFromGlyphLevel: new SingularityMilestoneState(db.darkFromGlyphLevel),
gamespeedFromSingularities: new SingularityMilestoneState(db.gamespeedFromSingularities),
darkFromTheorems: new SingularityMilestoneState(db.darkFromTheorems),
dim4Generation: new SingularityMilestoneState(db.dim4Generation),
darkFromDM4: new SingularityMilestoneState(db.darkFromDM4),
theoremPowerFromSingularities: new SingularityMilestoneState(db.theoremPowerFromSingularities),
darkFromGamespeed: new SingularityMilestoneState(db.darkFromGamespeed),
glyphLevelFromSingularities: new SingularityMilestoneState(db.glyphLevelFromSingularities),
darkFromDilatedTime: new SingularityMilestoneState(db.darkFromDilatedTime),
tesseractMultFromSingularities: new SingularityMilestoneState(db.tesseractMultFromSingularities),
improvedAscensionDM: new SingularityMilestoneState(db.improvedAscensionDM),
realityDEMultiplier: new SingularityMilestoneState(db.realityDEMultiplier),
intervalCostScalingReduction: new SingularityMilestoneState(db.intervalCostScalingReduction),
multFromInfinitied: new SingularityMilestoneState(db.multFromInfinitied),
infinitiedPow: new SingularityMilestoneState(db.infinitiedPow),
};
}());
export const SingularityMilestone = mapGameDataToObject(
GameDatabase.celestials.singularityMilestones,
config => new SingularityMilestoneState(config)
);
export const SingularityMilestones = {
all: Object.values(SingularityMilestone),
all: SingularityMilestone.all,
lastNotified: player.celestials.laitela.lastCheckedMilestones,
get sorted() {
@ -227,7 +203,7 @@ export const SingularityMilestones = {
// Sorted list of all the values where a singularity milestone exists, used for "new milestone" styling
const SingularityMilestoneThresholds = (function() {
return Object.values(GameDatabase.celestials.singularityMilestones)
return SingularityMilestones.all
.map(m => Array.range(0, Math.min(50, m.limit))
.filter(r => !m.increaseThreshold || r <= m.increaseThreshold ||
(r > m.increaseThreshold && ((r - m.increaseThreshold) % 3) === 2))
@ -243,7 +219,7 @@ export const Singularity = {
},
get gainPerCapIncrease() {
return SingularityMilestone.improvedSingularityCap.effectValue;
return SingularityMilestone.improvedSingularityCap.effectOrDefault(11);
},
get singularitiesGained() {
@ -264,7 +240,7 @@ export const Singularity = {
// Total additional time auto-condense will wait after reaching the condensing requirement
get timeDelayFromAuto() {
return this.timePerCondense * (SingularityMilestone.autoCondense.effectValue - 1);
return this.timePerCondense * (SingularityMilestone.autoCondense.effectOrDefault(Infinity) - 1);
},
get capIsReached() {
@ -282,16 +258,16 @@ export const Singularity = {
},
perform() {
if (!this.capIsReached) return;
if (!this.capIsReached || Pelle.isDoomed) return;
EventHub.dispatch(GAME_EVENT.SINGULARITY_RESET_BEFORE);
Currency.darkEnergy.reset();
Currency.singularities.add(this.singularitiesGained);
for (const quote of Object.values(Laitela.quotes)) {
if (Currency.singularities.value >= quote.singularities) {
Laitela.quotes.show(quote);
for (const quote of Laitela.quotes.all) {
if (quote.requirement) {
quote.show();
}
}
@ -307,5 +283,4 @@ EventHub.logic.on(GAME_EVENT.SINGULARITY_RESET_AFTER, () => {
if (newMilestones === 1) GameUI.notify.blackHole(`You reached a Singularity milestone!`);
else GameUI.notify.blackHole(`You reached ${formatInt(newMilestones)} Singularity milestones!`);
SingularityMilestones.lastNotified = Currency.singularities.value;
if (SingularityMilestones.all.every(completions => completions > 0)) Achievement(177).unlock();
});

View File

@ -1,5 +1,5 @@
import { DC } from "../../constants";
import { RebuyableMechanicState } from "../../game-mechanics/rebuyable";
import { PelleRifts } from "./rifts";
export const GalaxyGenerator = {
@ -23,10 +23,7 @@ export const GalaxyGenerator = {
get gainPerSecond() {
if (!Pelle.hasGalaxyGenerator) return 0;
// Pretend it's here to avoid softlocks and not because the bottom code returns 1 when you don't have this upg
if (!GalaxyGeneratorUpgrades.additive.canBeApplied) return 0.1;
return DC.D1.timesEffectsOf(
GalaxyGeneratorUpgrades.additive,
return new Decimal(GalaxyGeneratorUpgrades.additive.effectValue).timesEffectsOf(
GalaxyGeneratorUpgrades.multiplicative,
GalaxyGeneratorUpgrades.antimatterMult,
GalaxyGeneratorUpgrades.IPMult,
@ -61,18 +58,35 @@ export const GalaxyGenerator = {
loop(diff) {
if (this.isCapped) {
Pelle.quotes.show(Pelle.quotes.GALAXY_GENERATOR_RIFTS);
Pelle.quotes.galaxyGeneratorRifts.show();
}
if (this.sacrificeActive) {
this.capRift.reducedTo = Math.max(this.capRift.reducedTo - 0.03 * diff / 1000, 0);
if (this.capRift.reducedTo === 0) {
player.celestials.pelle.galaxyGenerator.sacrificeActive = false;
player.celestials.pelle.galaxyGenerator.phase++;
const phase = player.celestials.pelle.galaxyGenerator.phase;
if (phase === 1) {
Pelle.quotes.galaxyGeneratorPhase1.show();
} else if (phase === 4) {
Pelle.quotes.galaxyGeneratorPhase4.show();
}
if (!this.capObj) {
Pelle.quotes.show(Pelle.quotes.END);
Pelle.quotes.end.show();
}
}
PelleRifts.all.forEach(x => x.checkMilestoneStates());
// Force-unequip glyphs when the player loses the respective milestone. We call the respec option as normally
// except for one particular case - when we want to respec into protected slots but have no room to do so. In
// that case, we force-respec into the inventory instead
if (!PelleRifts.vacuum.milestones[0].canBeApplied && Glyphs.active.filter(g => g).length > 0) {
Glyphs.unequipAll(player.options.respecIntoProtected && Glyphs.findFreeIndex(true) === -1);
Glyphs.refreshActive();
}
}
player.celestials.pelle.galaxyGenerator.generatedGalaxies += this.gainPerSecond * diff / 1000;
player.celestials.pelle.galaxyGenerator.generatedGalaxies = Math.min(
@ -106,9 +120,7 @@ export class GalaxyGeneratorUpgrade extends RebuyableMechanicState {
}
}
export const GalaxyGeneratorUpgrades = (function() {
return mapGameDataToObject(
export const GalaxyGeneratorUpgrades = mapGameDataToObject(
GameDatabase.celestials.pelle.galaxyGeneratorUpgrades,
config => new GalaxyGeneratorUpgrade(config)
);
}());

View File

@ -0,0 +1,48 @@
export const END_STATE_MARKERS = {
// Tab zalgoification starts as soon as endState > 0
GAME_END: 1,
TAB_START_HIDE: 1.5,
INTERACTIVITY_DISABLED: 2.5,
FADE_AWAY: 2.5,
SAVE_DISABLED: 4,
END_NUMBERS: 4.2,
CREDITS_START: 4.5,
SHOW_NEW_GAME: 13,
SPECTATE_GAME: 13.5,
CREDITS_END: 14.5,
};
export const GameEnd = {
get endState() {
if (this.removeAdditionalEnd) return this.additionalEnd;
return Math.max((Math.log10(player.celestials.pelle.records.totalAntimatter.plus(1).log10() + 1) - 8.7) /
(Math.log10(9e15) - 8.7) + this.additionalEnd, 0);
},
_additionalEnd: 0,
get additionalEnd() {
return (player.isGameEnd || this.removeAdditionalEnd) ? this._additionalEnd : 0;
},
set additionalEnd(x) {
this._additionalEnd = (player.isGameEnd || this.removeAdditionalEnd) ? x : 0;
},
removeAdditionalEnd: false,
creditsClosed: false,
creditsEverClosed: false,
gameLoop(diff) {
if (this.removeAdditionalEnd) {
this.additionalEnd -= Math.min(diff / 200, 0.5);
if (this.additionalEnd < 4) {
this.additionalEnd = 0;
this.removeAdditionalEnd = false;
}
}
if (this.endState >= END_STATE_MARKERS.GAME_END && ui.$viewModel.modal.progressBar === undefined) {
player.isGameEnd = true;
this.additionalEnd += Math.min(diff / 1000 / 20, 0.1);
}
}
};

View File

@ -1,9 +1,14 @@
import { DC } from "../../constants";
import { Currency } from "../../currency";
import { DC } from "../../constants";
import { RebuyableMechanicState } from "../../game-mechanics/rebuyable";
import { SetPurchasableMechanicState } from "../../utils";
import { Quotes } from "../quotes";
import wordShift from "../../wordShift";
import zalgo from "./zalgo";
import { CelestialQuotes } from "../quotes.js";
const disabledMechanicUnlocks = {
achievements: () => ({}),
@ -19,7 +24,7 @@ const disabledMechanicUnlocks = {
autoec: () => ({}),
replicantiIntervalMult: () => ({}),
tpMults: () => ({}),
glyphs: () => !PelleRifts.famine.milestones[0].canBeApplied,
glyphs: () => !PelleRifts.vacuum.milestones[0].canBeApplied,
V: () => ({}),
singularity: () => ({}),
continuum: () => ({}),
@ -50,17 +55,11 @@ const disabledMechanicUnlocks = {
export const Pelle = {
symbol: "♅",
// Suppress the randomness for this form
possessiveName: "Pelle's",
get displayName() {
return Date.now() % 4000 > 500 ? "Pelle" : Pelle.modalTools.randomCrossWords("Pelle");
},
additionalEnd: 0,
addAdditionalEnd: true,
get endState() {
return Math.max((Math.log10(player.celestials.pelle.records.totalAntimatter.plus(1).log10() + 1) - 8.7) /
(Math.log10(9e15) - 8.7) + this.additionalEnd, 0);
return Date.now() % 4000 > 500 ? "Pelle" : wordShift.randomCrossWords("Pelle");
},
get isUnlocked() {
@ -99,16 +98,16 @@ export const Pelle = {
}
finishProcessReality({ reset: true, armageddon: true });
disChargeAll();
this.cel.armageddonDuration = 0;
player.celestials.enslaved.isStoringReal = false;
player.celestials.enslaved.autoStoreReal = false;
if (PelleStrikes.dilation.hasStrike) player.dilation.active = true;
EventHub.dispatch(GAME_EVENT.ARMAGEDDON_AFTER, gainStuff);
},
gameLoop(diff) {
if (this.isDoomed) {
this.cel.armageddonDuration += diff;
Currency.realityShards.add(this.realityShardGainPerSecond.times(diff).div(1000));
PelleRifts.all.forEach(r => r.fill(diff));
if (this.endState >= 1 && Pelle.addAdditionalEnd) this.additionalEnd += Math.min(diff / 1000 / 20, 0.1);
}
},
@ -120,12 +119,9 @@ export const Pelle = {
return this.cel.doomed;
},
get currentArmageddonDuration() {
return this.cel.armageddonDuration;
},
get disabledAchievements() {
return [143, 142, 141, 125, 118, 117, 111, 104, 103, 92, 91, 78, 76, 74, 65, 55, 54, 37];
return [164, 143, 142, 141, 137, 134, 133, 132, 125, 118, 117, 111, 104, 103, 93, 92, 91, 87, 85, 78, 76,
74, 65, 55, 54, 37];
},
get uselessInfinityUpgrades() {
@ -133,9 +129,7 @@ export const Pelle = {
},
get uselessTimeStudies() {
const uselessTimeStudies = [32, 41, 51, 61, 62, 121, 122, 123, 141, 142, 143, 192, 213];
if (PelleUpgrade.replicantiGalaxyNoReset.canBeApplied) uselessTimeStudies.push(33);
return uselessTimeStudies;
return [32, 41, 51, 61, 62, 121, 122, 123, 141, 142, 143, 192, 213];
},
get disabledRUPGs() {
@ -144,59 +138,24 @@ 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];
},
// Glyph effects are controlled through other means, but are also enumerated here for accessing to improve UX. Note
// that this field is NEGATED, describing an effect allowlist instead of a blocklist, as most of the effects are
// already disabled by virtue of the glyph type being unequippable and many of the remaining ones are also disabled.
get enabledGlyphEffects() {
return ["timepow", "timespeed", "timeshardpow",
"dilationpow", "dilationgalaxyThreshold",
"replicationpow",
"powerpow", "powermult", "powerdimboost", "powerbuy10",
"infinitypow", "infinityrate",
"companiondescription", "companionEP"];
60, 61, 62, 80, 81, 82, 83, 100, 105, 106, 201, 202, 203, 204];
},
get specialGlyphEffect() {
const isUnlocked = this.isDoomed && PelleRifts.chaos.milestones[1].canBeApplied;
let description;
switch (Pelle.activeGlyphType) {
case "infinity":
description = "Infinity Point gain {value} (based on current IP)";
break;
case "time":
description = "Eternity Point gain {value} (based on current EP)";
break;
case "replication":
description = "Replication speed {value} (based on Famine)";
break;
case "dilation":
description = "Dilated Time gain {value} (based on Tachyon Galaxies)";
break;
case "power":
description = `Galaxies are ${formatPercents(0.02)} stronger`;
break;
case "companion":
description = `You feel ${formatPercents(0.34)} better`;
break;
default:
description = "No glyph equipped!";
break;
}
const description = this.getSpecialGlyphEffectDescription(this.activeGlyphType);
const isActive = type => isUnlocked && this.activeGlyphType === type;
return {
isUnlocked,
description,
infinity: (isActive("infinity") && player.challenge.eternity.current <= 8)
? Currency.infinityPoints.value.pow(0.2)
? Currency.infinityPoints.value.plus(1).pow(0.2)
: DC.D1,
time: isActive("time")
? Currency.eternityPoints.value.plus(1).pow(0.3)
: DC.D1,
replication: isActive("replication")
? 10 ** 53 ** (PelleRifts.famine.percentage)
? 10 ** 53 ** (PelleRifts.vacuum.percentage)
: 1,
dilation: isActive("dilation")
? Decimal.pow(player.dilation.totalTachyonGalaxies, 1.5).max(1)
@ -210,9 +169,32 @@ export const Pelle = {
isScaling: () => ["infinity", "time", "replication", "dilation"].includes(this.activeGlyphType),
};
},
get uselessRaMilestones() {
return [0, 1, 15, 18, 19, 21];
getSpecialGlyphEffectDescription(type) {
switch (type) {
case "infinity":
return `Infinity Point gain ${player.challenge.eternity.current <= 8
? formatX(Currency.infinityPoints.value.plus(1).pow(0.2), 2)
: formatX(DC.D1, 2)} (based on current IP)`;
case "time":
return `Eternity Point gain ${formatX(Currency.eternityPoints.value.plus(1).pow(0.3), 2)}
(based on current EP)`;
case "replication":
return `Replication speed ${formatX(10 ** 53 ** (PelleRifts.vacuum.percentage), 2)} \
(based on ${wordShift.wordCycle(PelleRifts.vacuum.name)})`;
case "dilation":
return `Dilated Time gain ${formatX(Decimal.pow(player.dilation.totalTachyonGalaxies, 1.5).max(1), 2)}
(based on Tachyon Galaxies)`;
case "power":
return `Galaxies are ${formatPercents(0.02)} stronger`;
case "companion":
return `You feel ${formatPercents(0.34)} better`;
// Undefined means that there is no glyph equipped, needs to be here since this function is used in
// both Current Glyph Effects and Glyph Tooltip
case undefined:
return "No Glyph equipped!";
default:
return "";
}
},
get remnantRequirementForDilation() {
@ -281,7 +263,7 @@ export const Pelle = {
// Transition text from "from" to "to", stage is 0-1, 0 is fully "from" and 1 is fully "to"
// Also adds more zalgo the bigger the stage
transitionText(from, to, stage = 0) {
const len = (from.length * (1 - stage) + to.length * stage);
const len = Math.round((from.length * (1 - stage) + to.length * stage) * 1e8) / 1e8;
const toInterval = len * (1 - stage);
let req = toInterval;
let str = "";
@ -298,117 +280,31 @@ export const Pelle = {
return zalgo(str, Math.floor(stage ** 2 * 7));
},
endTabNames: "End Is Nigh Destruction Is Imminent Help Us Good Bye".split(" "),
endTabNames: "End Is Nigh Destruction Is Imminent Help Us Good Bye Forever".split(" "),
modalTools: {
bracketOrder: ["()", "[]", "{}", "<>", "||"],
wordCycle(x) {
const list = x.split("-");
const len = list.length;
const maxWordLen = list.reduce((acc, str) => Math.max(acc, str.length), 0);
const tick = Math.floor(Date.now() / 200) % (len * 5);
const largeTick = Math.floor(tick / 5);
const bP = this.bracketOrder[largeTick];
let v = list[largeTick];
if (tick % 5 < 1 || tick % 5 > 3) {
v = this.randomCrossWords(v);
}
// Stands for Bracket Pair.
const space = (maxWordLen - v.length) / 2;
return bP[0] + ".".repeat(Math.floor(space)) + v + ".".repeat(Math.ceil(space)) + bP[1];
},
randomCrossWords(str) {
const x = str.split("");
for (let i = 0; i < x.length / 1.7; i++) {
const randomIndex = Math.floor(this.predictableRandom(Math.floor(Date.now() / 500) % 964372 + i) * x.length);
// .splice should return the deleted index.
x[randomIndex] = this.randomSymbol;
}
return x.join("");
},
predictableRandom(x) {
let start = Math.pow(x % 97, 4.3) * 232344573;
const a = 15485863;
const b = 521791;
start = (start * a) % b;
for (let i = 0; i < (x * x) % 90 + 90; i++) {
start = (start * a) % b;
}
return start / b;
},
celCycle(x) {
// Gets trailing number and removes it
const cels = x.split("-").map(cel => [parseInt(cel, 10), cel.replace(/\d+/u, "")]);
const totalTime = cels.reduce((acc, cel) => acc + cel[0], 0);
let tick = (Date.now() / 100) % totalTime;
let index = -1;
while (tick >= 0 && index < cels.length - 1) {
index++;
tick -= cels[index][0];
}
return `<!${cels[index][1]}!>`;
},
get randomSymbol() {
return String.fromCharCode(Math.floor(Math.random() * 50) + 192);
}
},
quotes: new CelestialQuotes("pelle", (function() {
const wc = function(x) {
return Pelle.modalTools.wordCycle.bind(Pelle.modalTools)(x);
};
const cc = function(x) {
return Pelle.modalTools.celCycle.bind(Pelle.modalTools)(x);
};
const p = function(line) {
if (!line.includes("[") && !line.includes("<")) return line;
const sep = " ---TEMPSEPERATOR--- ";
const ops = [];
for (let i = 0; i < line.length; i++) {
if (line[i] === "[") ops.push(wc);
else if (line[i] === "<") ops.push(cc);
}
let l = line.replace("[", sep).replace("]", sep);
l = l.replace("<", sep).replace(">", sep).split(sep);
return () => l.map((v, x) => ((x % 2) ? ops[x / 2 - 0.5](v) : v)).join("");
};
const quotesObject = {};
let iterator = 0;
for (const i in GameDatabase.celestials.pelle.quotes) {
iterator++;
quotesObject[i] = {
id: iterator,
lines: GameDatabase.celestials.pelle.quotes[i].map(x => p(x))
};
}
return quotesObject;
}())),
hasQuote(x) {
return player.celestials.pelle.quotes.includes(x);
},
quotes: Quotes.pelle,
};
EventHub.logic.on(GAME_EVENT.ARMAGEDDON_AFTER, () => {
if (Currency.remnants.gte(1)) {
Pelle.quotes.show(Pelle.quotes.ARM);
Pelle.quotes.arm.show();
}
});
EventHub.logic.on(GAME_EVENT.PELLE_STRIKE_UNLOCKED, () => {
if (PelleStrikes.infinity.hasStrike) {
Pelle.quotes.show(Pelle.quotes.STRIKE_1);
Pelle.quotes.strike1.show();
}
if (PelleStrikes.powerGalaxies.hasStrike) {
Pelle.quotes.show(Pelle.quotes.STRIKE_2);
Pelle.quotes.strike2.show();
}
if (PelleStrikes.eternity.hasStrike) {
Pelle.quotes.show(Pelle.quotes.STRIKE_3);
Pelle.quotes.strike3.show();
}
if (PelleStrikes.ECs.hasStrike) {
Pelle.quotes.show(Pelle.quotes.STRIKE_4);
Pelle.quotes.strike4.show();
}
if (PelleStrikes.dilation.hasStrike) {
Pelle.quotes.show(Pelle.quotes.STRIKE_5);
Pelle.quotes.strike5.show();
}
});
@ -464,15 +360,13 @@ export class PelleUpgradeState extends SetPurchasableMechanicState {
}
export const PelleUpgrade = (function() {
return mapGameDataToObject(
export const PelleUpgrade = mapGameDataToObject(
GameDatabase.celestials.pelle.upgrades,
config => (config.rebuyable
? new RebuyablePelleUpgradeState(config)
: new PelleUpgradeState(config)
)
);
}());
PelleUpgrade.rebuyables = PelleUpgrade.all.filter(u => u.isRebuyable);
PelleUpgrade.singles = PelleUpgrade.all.filter(u => !u.isRebuyable);

View File

@ -20,7 +20,7 @@ class RiftMilestoneState extends GameMechanicState {
}
get isUnlocked() {
if (this.resource === "pestilence" && PelleRifts.chaos.milestones[0].isEffectActive) return true;
if (this.resource === "decay" && PelleRifts.chaos.milestones[0].isEffectActive) return true;
return this.requirement <= PelleRifts[this.resource].percentage;
}
@ -131,6 +131,10 @@ class RiftState extends GameMechanicState {
return this.percentage >= 1;
}
get galaxyGeneratorText() {
return this.config.galaxyGeneratorText;
}
toggle() {
const active = PelleRifts.all.filter(r => r.isActive).length;
if (!this.isActive && active === 2) GameUI.notify.error(`You can only have 2 rifts active at the same time!`);
@ -165,16 +169,14 @@ class RiftState extends GameMechanicState {
this.fillCurrency.value = Math.max(this.fillCurrency.value - spent, 0);
this.totalFill = Math.clampMax(this.totalFill + spent, this.maxValue);
}
if (PelleRifts.famine.milestones[0].canBeApplied) Glyphs.refreshActive();
if (PelleRifts.vacuum.milestones[0].canBeApplied) Glyphs.refreshActive();
this.checkMilestoneStates();
}
}
export const PelleRifts = (function() {
return mapGameDataToObject(
export const PelleRifts = mapGameDataToObject(
GameDatabase.celestials.pelle.rifts,
config => new RiftState(config)
);
}());
PelleRifts.totalMilestones = () => PelleRifts.all.flatMap(x => x.milestones).countWhere(x => x.canBeApplied);

View File

@ -1,15 +1,15 @@
import { GameMechanicState } from "../../utils";
import { BitUpgradeState } from "../../utils";
// TODO: BitUpgradeState? wrapper for this + effarig + enslaved
class PelleStrikeState extends GameMechanicState {
constructor(config) {
super(config);
if (this.id < 0 || this.id > 31) throw new Error(`Id ${this.id} out of bit range`);
}
class PelleStrikeState extends BitUpgradeState {
get bits() { return player.celestials.pelle.progressBits; }
set bits(value) { player.celestials.pelle.progressBits = value; }
get hasStrike() {
// eslint-disable-next-line no-bitwise
return Boolean(player.celestials.pelle.progressBits & (1 << this.id));
return this.isUnlocked;
}
get canBeUnlocked() {
return Pelle.isDoomed && !this.hasStrike;
}
get requirement() {
@ -22,8 +22,8 @@ class PelleStrikeState extends GameMechanicState {
return typeof x === "function" ? x() : x;
}
get reward() {
return this.config.rewardDescription;
reward() {
return this.config.rewardDescription();
}
get rift() {
@ -31,10 +31,14 @@ class PelleStrikeState extends GameMechanicState {
}
trigger() {
if (!Pelle.isDoomed || this.hasStrike) return;
this.unlockStrike();
this.unlock();
}
// If it's death, reset the records
onUnlock() {
GameUI.notify.strike(`You encountered a Pelle Strike: ${this.requirement}`);
player.celestials.pelle.collapsed.rifts = false;
// If it's paradox, reset the records
if (this.id === 5) {
Pelle.cel.records.totalAntimatter = new Decimal("1e180000");
Pelle.cel.records.totalInfinityPoints = new Decimal("1e60000");
@ -46,21 +50,12 @@ class PelleStrikeState extends GameMechanicState {
// softlocked, or starting it too late and getting not-softlocked.
Pelle.cel.records.totalEternityPoints = new Decimal("1e1050");
}
}
unlockStrike() {
GameUI.notify.strike(`You encountered a Pelle Strike: ${this.requirement}`);
player.celestials.pelle.collapsed.rifts = false;
Tab.celestials.pelle.show();
// eslint-disable-next-line no-bitwise
player.celestials.pelle.progressBits |= (1 << this.id);
EventHub.dispatch(GAME_EVENT.PELLE_STRIKE_UNLOCKED);
}
}
export const PelleStrikes = (function() {
return mapGameDataToObject(
export const PelleStrikes = mapGameDataToObject(
GameDatabase.celestials.pelle.strikes,
config => new PelleStrikeState(config)
);
}());

View File

@ -1,44 +1,178 @@
export class CelestialQuotes {
constructor(celestialName, quoteData) {
this.quotesById = [];
for (const quoteKey of Object.keys(quoteData)) {
if (this[quoteKey] !== undefined) {
throw new Error(`Celestial quote keys should not replace existing properties (${quoteKey})`);
}
const quote = quoteData[quoteKey];
this[quoteKey] = quote;
this.quotesById[quote.id] = quote;
}
this._celestial = celestialName;
}
import { BitUpgradeState } from "../game-mechanics/index";
import wordShift from "../wordShift";
static singleLine(id, line) {
return {
id,
lines: [line]
export const Quote = {
addToQueue(quote) {
ui.view.quotes.queue.push(quote);
if (!ui.view.quotes.current) this.advanceQueue();
},
advanceQueue() {
ui.view.quotes.current = ui.view.quotes.queue.shift();
},
showHistory(history) {
ui.view.quotes.history = history;
},
clearQueue() {
ui.view.quotes.queue = [];
ui.view.quotes.current = undefined;
},
clearHistory() {
ui.view.quotes.history = undefined;
},
clearAll() {
this.clearQueue();
this.clearHistory();
},
get isOpen() {
return ui.view.quotes.current !== undefined;
},
get isHistoryOpen() {
return ui.view.quotes.history !== undefined;
}
};
// Gives an array specifying proportions of celestials to blend together on the modal, as a function of time, to
// provide a smoother transition between different celestials to reduce potential photosensitivity issues
function blendCel(cels) {
const totalTime = cels.map(cel => cel[1]).sum();
const tick = (Date.now() / 1000) % totalTime;
// Blend the first blendTime seconds with the previous celestial and the last blendTime seconds with the next;
// note that this results in a total transition time of 2*blendTime. We specifically set this to be half the duration
// of the first entry - this is because in the case of all intervals having the same duration, this guarantees two
// blended entries at all points in time.
const blendTime = cels[0][1] / 2;
let start = 0;
for (let index = 0; index < cels.length; index++) {
const prevCel = cels[(index + cels.length - 1) % cels.length], currCel = cels[index],
nextCel = cels[(index + 1) % cels.length];
// Durations of time from after last transition and after next transition. May be negative, which is how we
// check to see if we're in the correct time interval (last should be positive, next should be negative)
const lastTime = tick - start, nextTime = lastTime - currCel[1];
if (nextTime > 0) {
start += currCel[1];
continue;
}
fromID(id) {
return this.quotesById[id];
if (lastTime <= blendTime) {
const t = 0.5 * lastTime / blendTime;
return [[prevCel[0], 0.5 - t], [currCel[0], 0.5 + t]];
}
if (-nextTime <= blendTime) {
const t = 0.5 * nextTime / blendTime;
return [[currCel[0], 0.5 - t], [nextCel[0], 0.5 + t]];
}
get seenArray() {
return player.celestials[this._celestial].quotes;
// In principle the animation properties should never get to this return case, but we leave it here just in case -
// the worst side-effect of reaching here is that some UI elements may appear to lose click detection for a
// fraction of a second when transitioning from two blended entries to one
return [[currCel[0], 1]];
}
throw new Error("Could not blend celestial fractions in Quote modal");
}
seen(data) {
return this.seenArray.includes(data.id);
class QuoteLine {
constructor(line, parent) {
this._parent = parent;
this._showCelestialName = line.showCelestialName ?? true;
this._celestialArray = line.background
? () => blendCel(line.background)
: [[parent.celestial, 1]];
const replacementMatch = /\$(\d+)/gu;
this._line = typeof line === "string"
? line
// This matches each digit after a $ and replaces it with the wordCycle of an array with the digit it matched.
: () => line.text.replaceAll(replacementMatch, (_, i) => wordShift.wordCycle(line[i]));
}
show(data) {
if (this.seen(data)) return;
this.seenArray.push(data.id);
Modal.celestialQuote.show(this._celestial, data.lines);
get line() {
return typeof this._line === "function" ? this._line() : this._line;
}
forget(data) {
const index = this.seenArray.indexOf(data.id);
if (index >= 0) this.seenArray.splice(index, 1);
get celestials() {
return typeof this._celestialArray === "function" ? this._celestialArray() : this._celestialArray;
}
get celestialSymbols() {
return this.celestials.map(c => Celestials[c[0]].symbol);
}
get showCelestialName() {
return this._showCelestialName;
}
get celestialName() {
return Celestials[this._parent.celestial].displayName;
}
}
class CelQuotes extends BitUpgradeState {
constructor(config, celestial) {
super(config);
this._celestial = celestial;
this._lines = config.lines.map(line => new QuoteLine(line, this));
}
get bits() { return player.celestials[this._celestial].quoteBits; }
set bits(value) { player.celestials[this._celestial].quoteBits = value; }
get requirement() {
// If requirement is defined, it is always a function returning a boolean.
return this.config.requirement?.();
}
get celestial() {
return this._celestial;
}
line(id) {
return this._lines[id];
}
get totalLines() {
return this._lines.length;
}
show() { this.unlock(); }
onUnlock() { this.present(); }
present() {
Quote.addToQueue(this);
}
}
export const Quotes = {
teresa: mapGameDataToObject(
GameDatabase.celestials.quotes.teresa,
config => new CelQuotes(config, "teresa")
),
effarig: mapGameDataToObject(
GameDatabase.celestials.quotes.effarig,
config => new CelQuotes(config, "effarig")
),
enslaved: mapGameDataToObject(
GameDatabase.celestials.quotes.enslaved,
config => new CelQuotes(config, "enslaved")
),
v: mapGameDataToObject(
GameDatabase.celestials.quotes.v,
config => new CelQuotes(config, "v")
),
ra: mapGameDataToObject(
GameDatabase.celestials.quotes.ra,
config => new CelQuotes(config, "ra")
),
laitela: mapGameDataToObject(
GameDatabase.celestials.quotes.laitela,
config => new CelQuotes(config, "laitela")
),
pelle: mapGameDataToObject(
GameDatabase.celestials.quotes.pelle,
config => new CelQuotes(config, "pelle")
),
};

View File

@ -1,4 +1,4 @@
import { GameMechanicState } from "../../game-mechanics/index.js";
import { GameMechanicState } from "../../game-mechanics/index";
/**
* @abstract
@ -194,7 +194,7 @@ class AlchemyReaction {
// Reactions are per-10 products because that avoids decimals in the UI for reagents, but efficiency losses can make
// products have decimal coefficients.
get baseProduction() {
return this.isReality ? 1 : 5 * Effects.sum(GlyphSacrifice.reality);
return this.isReality ? 1 : 5;
}
get reactionEfficiency() {
@ -224,44 +224,16 @@ class AlchemyReaction {
}
}
export const AlchemyResource = (function() {
function createResource(resource) {
const config = GameDatabase.celestials.alchemy.resources[resource];
config.id = resource;
if (config.isBaseResource) {
return new BasicAlchemyResourceState(config);
}
return new AdvancedAlchemyResourceState(config);
}
return {
power: createResource(ALCHEMY_RESOURCE.POWER),
infinity: createResource(ALCHEMY_RESOURCE.INFINITY),
time: createResource(ALCHEMY_RESOURCE.TIME),
replication: createResource(ALCHEMY_RESOURCE.REPLICATION),
dilation: createResource(ALCHEMY_RESOURCE.DILATION),
cardinality: createResource(ALCHEMY_RESOURCE.CARDINALITY),
eternity: createResource(ALCHEMY_RESOURCE.ETERNITY),
dimensionality: createResource(ALCHEMY_RESOURCE.DIMENSIONALITY),
inflation: createResource(ALCHEMY_RESOURCE.INFLATION),
alternation: createResource(ALCHEMY_RESOURCE.ALTERNATION),
effarig: createResource(ALCHEMY_RESOURCE.EFFARIG),
synergism: createResource(ALCHEMY_RESOURCE.SYNERGISM),
momentum: createResource(ALCHEMY_RESOURCE.MOMENTUM),
decoherence: createResource(ALCHEMY_RESOURCE.DECOHERENCE),
exponential: createResource(ALCHEMY_RESOURCE.EXPONENTIAL),
force: createResource(ALCHEMY_RESOURCE.FORCE),
uncountability: createResource(ALCHEMY_RESOURCE.UNCOUNTABILITY),
boundless: createResource(ALCHEMY_RESOURCE.BOUNDLESS),
multiversal: createResource(ALCHEMY_RESOURCE.MULTIVERSAL),
unpredictability: createResource(ALCHEMY_RESOURCE.UNPREDICTABILITY),
reality: createResource(ALCHEMY_RESOURCE.REALITY)
};
}());
export const AlchemyResource = mapGameDataToObject(
GameDatabase.celestials.alchemy.resources,
config => (config.isBaseResource
? new BasicAlchemyResourceState(config)
: new AdvancedAlchemyResourceState(config))
);
export const AlchemyResources = {
all: Object.values(AlchemyResource),
base: Object.values(AlchemyResource).filter(r => r.isBaseResource)
all: AlchemyResource.all,
base: AlchemyResource.all.filter(r => r.isBaseResource)
};
export const AlchemyReactions = (function() {
@ -269,7 +241,7 @@ export const AlchemyReactions = (function() {
function mapReagents(resource) {
return resource.config.reagents
.map(r => ({
resource: AlchemyResources.all[r.resource],
resource: AlchemyResources.all.find(x => x.id === r.resource),
cost: r.amount
}));
}

View File

@ -1,5 +1,56 @@
import { GameMechanicState } from "../../utils.js";
import { CelestialQuotes } from "../quotes.js";
import { BitUpgradeState, GameMechanicState } from "../../game-mechanics/index";
import { Quotes } from "../quotes";
class RaUnlockState extends BitUpgradeState {
get bits() { return player.celestials.ra.unlockBits; }
set bits(value) { player.celestials.ra.unlockBits = value; }
get disabledByPelle() {
return Pelle.isDoomed && this.config.disabledByPelle;
}
get isEffectActive() {
return this.isUnlocked && !this.disabledByPelle;
}
get requirementText() {
const pet = this.pet.name;
return this.level === 1
? `Unlock ${pet}`
: `Get ${pet} to level ${this.level}`;
}
get reward() {
return typeof this.config.reward === "function"
? this.config.reward()
: this.config.reward;
}
get displayIcon() {
return this.disabledByPelle ? `<span class="fas fa-ban"></span>` : this.config.displayIcon;
}
get pet() {
return Ra.pets[this.config.pet];
}
get level() {
return this.config.level;
}
get canBeUnlocked() {
return this.pet.level >= this.level && !this.isUnlocked;
}
onUnlock() {
this.config.onUnlock?.();
}
}
const unlocks = mapGameDataToObject(
GameDatabase.celestials.ra.unlocks,
config => new RaUnlockState(config)
);
class RaPetState extends GameMechanicState {
get data() {
@ -35,7 +86,7 @@ class RaPetState extends GameMechanicState {
}
get isUnlocked() {
return this.requiredUnlock === undefined || Ra.has(this.requiredUnlock);
return this.requiredUnlock === undefined || this.requiredUnlock.isUnlocked;
}
get isCapped() {
@ -43,7 +94,7 @@ class RaPetState extends GameMechanicState {
}
get level() {
return this.data.level;
return this.isUnlocked ? this.data.level : 0;
}
set level(value) {
@ -71,10 +122,11 @@ class RaPetState extends GameMechanicState {
}
get memoryChunksPerSecond() {
let res = this.canGetMemoryChunks ? this.rawMemoryChunksPerSecond : 0;
res *= RA_UNLOCKS.TT_BOOST.effect.memoryChunks();
res *= this.chunkUpgradeCurrentMult;
if (this.hasRecollection) res *= RA_UNLOCKS.RA_RECOLLECTION_UNLOCK.effect;
if (!this.canGetMemoryChunks) return 0;
let res = this.rawMemoryChunksPerSecond * this.chunkUpgradeCurrentMult *
Effects.product(Ra.unlocks.continuousTTBoost.effects.memoryChunks, GlyphSacrifice.reality);
if (this.hasRemembrance) res *= Ra.remembrance.multiplier;
else if (Ra.petWithRemembrance) res *= Ra.remembrance.nerf;
return res;
}
@ -82,8 +134,8 @@ class RaPetState extends GameMechanicState {
return this.isUnlocked && Ra.isRunning;
}
get hasRecollection() {
return Ra.petWithRecollection === this.name;
get hasRemembrance() {
return Ra.petWithRemembrance === this.name;
}
get memoryUpgradeCurrentMult() {
@ -140,6 +192,12 @@ class RaPetState extends GameMechanicState {
Ra.checkForUnlocks();
}
get unlocks() {
return Ra.unlocks.all
.filter(x => x.pet === this)
.sort((a, b) => a.level - b.level);
}
tick(realDiff, generateChunks) {
const seconds = realDiff / 1000;
const newMemoryChunks = generateChunks
@ -162,12 +220,24 @@ class RaPetState extends GameMechanicState {
}
}
const pets = mapGameDataToObject(
GameDatabase.celestials.ra.pets,
config => new RaPetState(config)
);
export const Ra = {
displayName: "Ra",
pets: mapGameDataToObject(
GameDatabase.celestials.ra,
config => new RaPetState(config)
),
possessiveName: "Ra's",
unlocks,
pets,
remembrance: {
multiplier: 5,
nerf: 0.5,
requiredLevels: 20,
get isUnlocked() {
return Ra.totalPetLevel >= this.requiredLevels;
}
},
// Dev/debug function for easier testing
reset() {
const data = player.celestials.ra;
@ -190,7 +260,7 @@ export const Ra = {
for (const pet of Ra.pets.all) pet.tick(realDiff, generateChunks);
},
get productionPerMemoryChunk() {
let res = RA_UNLOCKS.TT_BOOST.effect.memories() * Achievement(168).effectOrDefault(1);
let res = Effects.product(Ra.unlocks.continuousTTBoost.effects.memories, Achievement(168));
for (const pet of Ra.pets.all) {
if (pet.isUnlocked) res *= pet.memoryProductionMultiplier;
}
@ -201,7 +271,7 @@ export const Ra = {
for (const pet of Ra.pets.all) {
if (pet.memoryProductionMultiplier !== 1) boostList.push(pet.memoryGain);
}
if (Ra.has(RA_UNLOCKS.TT_BOOST)) boostList.push("current Time Theorems");
if (Ra.unlocks.continuousTTBoost.canBeApplied) boostList.push("current Time Theorems");
if (boostList.length === 1) return `${boostList[0]}`;
if (boostList.length === 2) return `${boostList[0]} and ${boostList[1]}`;
@ -231,55 +301,34 @@ export const Ra = {
return this.levelCap * this.pets.all.length;
},
checkForUnlocks() {
if (!V.has(V_UNLOCKS.RA_UNLOCK)) return;
for (const unl of Object.values(RA_UNLOCKS)) {
const isUnlockable = unl.totalLevels === undefined
? unl.pet.isUnlocked && unl.pet.level >= unl.level
: this.totalPetLevel >= unl.totalLevels;
if (isUnlockable && !this.has(unl)) {
// eslint-disable-next-line no-bitwise
player.celestials.ra.unlockBits |= (1 << unl.id);
if (unl.id === RA_UNLOCKS.ALWAYS_GAMESPEED.id) {
const allGlyphs = player.reality.glyphs.active
.concat(player.reality.glyphs.inventory);
for (const glyph of allGlyphs) {
Glyphs.applyGamespeed(glyph);
}
}
}
if (!VUnlocks.raUnlock.canBeApplied) return;
for (const unl of Ra.unlocks.all) {
unl.unlock();
}
for (const quote of Object.values(Ra.quotes)) {
// Quotes without requirements will be shown in other ways - need to check if it exists before calling though
if (quote.requirement && quote.requirement()) {
// TODO If multiple quotes show up simultaneously, this only seems to actually show one of them and skips the
// rest. This might be related to the modal stacking issue
Ra.quotes.show(quote);
}
}
Ra.checkForQuotes();
},
has(info) {
// eslint-disable-next-line no-bitwise
return Boolean(player.celestials.ra.unlockBits & (1 << info.id));
checkForQuotes() {
for (const quote of Ra.quotes.all) {
// Quotes without requirements will be shown in other ways
if (quote.requirement) {
quote.show();
}
}
},
initializeRun() {
clearCelestialRuns();
player.celestials.ra.run = true;
this.quotes.show(this.quotes.REALITY_ENTER);
this.quotes.realityEnter.show();
},
toggleMode() {
player.celestials.ra.activeMode = !player.celestials.ra.activeMode;
},
gamespeedDTMult() {
if (!Ra.has(RA_UNLOCKS.PEAK_GAMESPEED)) return 1;
return Math.max(Math.pow(Math.log10(player.celestials.ra.peakGamespeed) - 90, 3), 1);
},
// This gets widely used in lots of places since the relevant upgrade is "all forms of continuous non-dimension
// production", which in this case is infinities, eternities, replicanti, dilated time, and time theorem generation.
// It also includes the 1% IP time study, Teresa's 1% EP upgrade, and the charged RM generation upgrade. Note that
// removing the hardcap of 10 may cause runaways.
theoremBoostFactor() {
if (!Ra.has(RA_UNLOCKS.TT_BOOST)) return 0;
return Math.min(10, Math.max(0, Currency.timeTheorems.value.pLog10() - 350) / 50);
},
get isUnlocked() {
@ -289,22 +338,19 @@ export const Ra = {
return player.celestials.ra.run;
},
get totalCharges() {
return Math.min(12, Math.floor(Ra.pets.teresa.level / 2));
return Ra.unlocks.chargedInfinityUpgrades.effectOrDefault(0);
},
get chargesLeft() {
return this.totalCharges - player.celestials.ra.charged.size;
},
get chargeUnlocked() {
return V.has(V_UNLOCKS.RA_UNLOCK) && Ra.pets.teresa.level > 1;
},
get canBuyTriad() {
return this.pets.v.level >= 5;
return Ra.unlocks.unlockHardV.canBeApplied;
},
get petWithRecollection() {
return player.celestials.ra.petWithRecollection;
get petWithRemembrance() {
return player.celestials.ra.petWithRemembrance;
},
set petWithRecollection(name) {
player.celestials.ra.petWithRecollection = name;
set petWithRemembrance(name) {
player.celestials.ra.petWithRemembrance = name;
},
updateAlchemyFlow(realityRealTime) {
const perSecond = 1000 / realityRealTime;
@ -314,7 +360,7 @@ export const Ra = {
}
},
applyAlchemyReactions(realityRealTime) {
if (!Ra.has(RA_UNLOCKS.EFFARIG_UNLOCK)) return;
if (!Ra.unlocks.effarigUnlock.canBeApplied) return;
const sortedReactions = AlchemyReactions.all
.compact()
.sort((r1, r2) => r2.priority - r1.priority);
@ -330,136 +376,7 @@ export const Ra = {
const hoursFromUnlock = TimeSpan.fromMilliseconds(player.celestials.ra.momentumTime).totalHours;
return Math.clampMax(1 + 0.002 * hoursFromUnlock, AlchemyResource.momentum.effectValue);
},
quotes: new CelestialQuotes("ra", {
UNLOCK: {
id: 1,
lines: [
"A... visitor?",
"I am here! I am the one you are looking for... I think...",
"What even was I again?",
"Oh right, the Celestial of Memories.",
]
},
REALITY_ENTER: {
id: 2,
lines: [
"I have not seen the others in so long...",
"Can you help me remember them?",
"I could give you powers in exchange.",
]
},
TERESA_START: {
id: 3,
requirement: () => Ra.pets.teresa.level >= 2,
lines: [
"Te... re... sa...",
"I think I remember.",
]
},
TERESA_LATE: {
id: 4,
requirement: () => Ra.pets.teresa.level >= 15,
lines: [
"Teresa dealt with machines, I believe.",
"I remember visiting Teresas shop a few times.",
"Wait, someone else had a shop too, right?",
]
},
EFFARIG_START: {
id: 5,
requirement: () => Ra.pets.effarig.level >= 2,
lines: [
"Eff... a... rig",
"I remember Effarig being friendly.",
]
},
EFFARIG_LATE: {
id: 6,
requirement: () => Ra.pets.effarig.level >= 15,
lines: [
"Effarig was very particular?",
"And I also remember a frightening Reality...",
"It was about... suffering?",
]
},
ENSLAVED_START: {
id: 7,
requirement: () => Ra.pets.enslaved.level >= 2,
lines: [
"I cannot remember this one completely...",
]
},
ENSLAVED_LATE: {
id: 8,
requirement: () => Ra.pets.enslaved.level >= 15,
lines: [
"I am starting to remember...",
"Why I am here...",
"Why I am alone...",
"Help me.",
]
},
V_START: {
id: 9,
requirement: () => Ra.pets.v.level >= 2,
lines: [
"Had I met this one?",
"So lonely, yet willingly so...",
]
},
V_LATE: {
id: 10,
requirement: () => Ra.pets.v.level >= 15,
lines: [
"I think I met V once...",
"I can remember the achievements.",
]
},
RECOLLECTION: {
id: 11,
requirement: () => Ra.has(RA_UNLOCKS.RA_RECOLLECTION_UNLOCK),
lines: [
"I remembered something!",
"Watch this!",
"Recollection!",
"I can focus even harder on remembering them now!",
]
},
MID_MEMORIES: {
id: 12,
requirement: () => Ra.totalPetLevel >= 50,
lines: [
"Realities are my homes, yet I cannot make my own Reality.",
"I can only copy the ones of my friends.",
"But... why am I hearing voices?",
"Are they asking for help?",
]
},
LATE_MEMORIES: {
id: 13,
requirement: () => Ra.totalPetLevel >= 80,
lines: [
"I think they are telling me to stop.",
"You... whatever you are?",
"What is happening?",
"Am I doing something wrong?",
]
},
MAX_LEVELS: {
id: 14,
requirement: () => Ra.totalPetLevel === Ra.maxTotalPetLevel,
lines: [
"Finally, I remember everything.",
"This darkness that banished me.",
"Lai'tela...",
"They were right to banish me.",
"My powers...",
"They steal, they corrupt.",
"Please leave.",
"I do not want to hurt you too.",
]
},
}),
quotes: Quotes.ra,
symbol: "<i class='fas fa-sun'></i>"
};
@ -486,7 +403,7 @@ export const GlyphAlteration = {
},
get isUnlocked() {
if (Pelle.isDisabled("alteration")) return false;
return Ra.has(RA_UNLOCKS.ALTERED_GLYPHS);
return Ra.unlocks.alteredGlyphs.canBeApplied;
},
isAdded(type) {
return this.isUnlocked && this.getSacrificePower(type) >= this.additionThreshold;
@ -502,285 +419,19 @@ export const GlyphAlteration = {
return Math.log10(Math.clampMin(capped / this.boostingThreshold, 1)) / 2;
},
getAdditionColor(type) {
return this.isAdded(type)
? "#CCCCCC"
: undefined;
const color = Theme.current().isDark() ? "#CCCCCC" : "black";
return this.isAdded(type) ? color : undefined;
},
getEmpowermentColor(type) {
return this.isEmpowered(type)
? "#EEEE30"
: undefined;
const color = Theme.current().isDark() ? "#EEEE30" : "#C6C610";
return this.isEmpowered(type) ? color : undefined;
},
getBoostColor(type) {
return this.isBoosted(type)
? "#60DDDD"
: undefined;
const color = Theme.current().isDark() ? "#60DDDD" : "#28BDBD";
return this.isBoosted(type) ? color : undefined;
},
};
export const RA_UNLOCKS = {
AUTO_TP: {
id: 0,
description: "Unlock Teresa",
reward: "Tachyon Particles are given immediately when Time Dilation is active",
pet: Ra.pets.teresa,
level: 1,
displayIcon: `<span class="fas fa-atom"></span>`
},
CHARGE: {
id: 1,
description: "Get Teresa to level 2",
reward: () => `Unlock Charged Infinity Upgrades. You get one more maximum
Charged Infinity Upgrade every ${formatInt(2)} levels`,
pet: Ra.pets.teresa,
level: 2,
displayIcon: `<span class="fas fa-infinity"></span>`
},
TERESA_XP: {
id: 2,
description: "Get Teresa to level 5",
reward: "All Memory Chunks produce more Memories based on Reality Machines",
pet: Ra.pets.teresa,
level: 5,
displayIcon: `Δ`
},
ALTERED_GLYPHS: {
id: 3,
description: "Get Teresa to level 8",
reward: "Unlock Altered Glyphs, which grant new effects to Glyphs based on Glyph Sacrifice",
pet: Ra.pets.teresa,
level: 10,
displayIcon: `<span class="fas fa-bolt"></span>`
},
EFFARIG_UNLOCK: {
id: 4,
description: "Get Teresa to level 10",
reward: "Unlock Effarig's Memories",
pet: Ra.pets.teresa,
level: 8,
displayIcon: `Ϙ`
},
PERK_SHOP_INCREASE: {
id: 5,
description: "Get Teresa to level 15",
reward: "Perk shop caps are raised",
pet: Ra.pets.teresa,
level: 15,
displayIcon: `<span class="fas fa-project-diagram"></span>`
},
START_TP: {
id: 6,
description: "Get Teresa to level 25",
reward: `When unlocking Time Dilation in non-celestial Realities, gain Tachyon Particles as if you reached
the square root of your total antimatter in Dilation`,
effect: () => player.records.totalAntimatter.pow(0.5),
pet: Ra.pets.teresa,
level: 25,
displayIcon: `<i class="far fa-dot-circle"></i>`
},
EXTRA_CHOICES_AND_RELIC_SHARD_RARITY_ALWAYS_MAX: {
id: 7,
description: "Unlock Effarig",
reward: () => `Get ${formatX(2)} Glyph choices and the bonus to Glyph rarity from Relic Shards
is always its maximum value`,
pet: Ra.pets.effarig,
level: 1,
displayIcon: `<i class="fas fa-grip-horizontal"></i>`
},
GLYPH_ALCHEMY: {
id: 8,
description: "Get Effarig to level 2",
reward: `Unlock Glyph Alchemy, which adds alchemical resources you can increase by Refining Glyphs. You unlock
more resources through Effarig levels. Access through a new Reality tab.`,
pet: Ra.pets.effarig,
level: 2,
displayIcon: `<span class="fas fa-vial"></span>`
},
EFFARIG_XP: {
id: 9,
description: "Get Effarig to level 5",
reward: "All Memory Chunks produce more Memories based on highest Glyph level",
pet: Ra.pets.effarig,
level: 5,
displayIcon: `<span class="fas fa-clone"></span>`
},
GLYPH_EFFECT_COUNT: {
id: 10,
description: "Get Effarig to level 8",
reward: () => `Glyphs always have ${formatInt(4)} effects, and Effarig Glyphs can now have up to ${formatInt(7)}`,
pet: Ra.pets.effarig,
level: 10,
displayIcon: `<span class="fas fa-braille"></span>`
},
ENSLAVED_UNLOCK: {
id: 11,
description: "Get Effarig to level 10",
reward: "Unlock Enslaved's Memories",
pet: Ra.pets.effarig,
level: 8,
displayIcon: `<span class="fas fa-link"></span>`
},
SHARD_LEVEL_BOOST: {
id: 12,
description: "Get Effarig to level 15",
reward: "Glyph level is increased based on Relic Shards gained",
effect: () => 100 * Math.pow(Math.log10(Math.max(Effarig.shardsGained, 1)), 2),
pet: Ra.pets.effarig,
level: 15,
displayIcon: `<span class="fas fa-fire"></span>`
},
MAX_RARITY_AND_SHARD_SACRIFICE_BOOST: {
id: 13,
description: "Get Effarig to level 25",
reward: () => `Glyphs are always generated with ${formatPercents(1)} rarity and ` +
`Glyph Sacrifice gain is raised to a power based on Relic Shards`,
pet: Ra.pets.effarig,
level: 25,
displayIcon: `<i class="fas fa-ankh"></i>`
},
AUTO_BLACK_HOLE_POWER: {
id: 14,
description: "Unlock Enslaved",
reward: "Unlock Black Hole power upgrade autobuyers",
pet: Ra.pets.enslaved,
level: 1,
displayIcon: `<span class="fas fa-circle"></span>`
},
IMPROVED_STORED_TIME: {
id: 15,
description: "Get Enslaved to level 2",
reward: "Stored game time is amplified and you can store more real time, increasing with Enslaved levels",
effect: {
gameTimeAmplification: () => Math.pow(20, Math.clampMax(Ra.pets.enslaved.level, Ra.levelCap)),
realTimeCap: () => 1000 * 3600 * Ra.pets.enslaved.level,
},
pet: Ra.pets.enslaved,
level: 2,
displayIcon: `<span class="fas fa-history"></span>`
},
ENSLAVED_XP: {
id: 16,
description: "Get Enslaved to level 5",
reward: "All Memory Chunks produce more Memories based on total time played",
pet: Ra.pets.enslaved,
level: 5,
displayIcon: `<span class="fas fa-stopwatch"></span>`
},
ADJUSTABLE_STORED_TIME: {
id: 17,
description: "Get Enslaved to level 8",
reward: () => `Black Hole charging can be done at an adjustable rate and automatically
pulsed every ${formatInt(5)} ticks. You can change these in the Black Hole and The Enslaved Ones' tabs`,
pet: Ra.pets.enslaved,
level: 10,
displayIcon: `<span class="fas fa-expand-arrows-alt"></span>`
},
V_UNLOCK: {
id: 18,
description: "Get Enslaved to level 10",
reward: "Unlock V's Memories",
pet: Ra.pets.enslaved,
level: 8,
displayIcon: ``
},
PEAK_GAMESPEED: {
id: 19,
description: "Get Enslaved to level 15",
reward: "Gain more Dilated Time based on peak game speed in each Reality",
pet: Ra.pets.enslaved,
level: 15,
displayIcon: `<span class="fas fa-tachometer-alt"></span>`
},
ALWAYS_GAMESPEED: {
id: 20,
description: "Get Enslaved to level 25",
reward: `All basic Glyphs gain the increased game speed effect from Time Glyphs,
and Time Glyphs gain an additional effect`,
pet: Ra.pets.enslaved,
level: 25,
displayIcon: `<span class="fas fa-clock"></span>`
},
AUTO_RU_AND_INSTANT_EC: {
id: 21,
description: "Unlock V",
reward: "The rebuyable Reality upgrades are bought automatically and Auto-Eternity Challenges happen instantly",
pet: Ra.pets.v,
level: 1,
displayIcon: `<span class="fas fa-sync-alt"></span>`
},
AUTO_DILATION_UNLOCK: {
id: 22,
description: "Get V to level 2",
reward: () => `Time Dilation is unlocked automatically for free at
${formatInt(TimeStudy.dilation.totalTimeTheoremRequirement)} Time Theorems outside of Celestial Realities`,
pet: Ra.pets.v,
level: 2,
displayIcon: `<span class="fas fa-fast-forward"></span>`
},
V_XP: {
id: 23,
description: "Get V to level 5",
reward: () => `All Memory Chunks produce more Memories based on total Celestial levels,
and unlock a Triad Study every ${formatInt(5)} levels (to a maximum of ${formatInt(4)} Triad Studies).
Triad Studies are located at the bottom of the Time Studies page`,
pet: Ra.pets.v,
level: 5,
displayIcon: `<span class="fas fa-book"></span>`
},
HARD_V: {
id: 24,
description: "Get V to level 8",
reward: "Unlock Hard V-Achievements",
pet: Ra.pets.v,
level: 8,
displayIcon: `<span class="fas fa-trophy"></span>`
},
TT_BOOST: {
id: 25,
description: "Get V to level 10",
reward: "Time Theorems boost all forms of continuous non-dimension production",
effect: {
// All of these are accessed directly from RA_UNLOCKS across much of the game, but are effectively dummied out
// before the upgrade itself is unlocked due to theoremBoostFactor evaluating to zero if the upgrade is missing.
ttGen: () => Math.pow(10, 5 * Ra.theoremBoostFactor()),
eternity: () => Math.pow(10, 2 * Ra.theoremBoostFactor()),
infinity: () => Math.pow(10, 15 * Ra.theoremBoostFactor()),
replicanti: () => Math.pow(10, 20 * Ra.theoremBoostFactor()),
dilatedTime: () => Math.pow(10, 3 * Ra.theoremBoostFactor()),
memories: () => 1 + Ra.theoremBoostFactor() / 50,
memoryChunks: () => 1 + Ra.theoremBoostFactor() / 50,
autoPrestige: () => 1 + 2.4 * Ra.theoremBoostFactor()
},
pet: Ra.pets.v,
level: 10,
displayIcon: `<span class="fas fa-university"></span>`
},
TT_ACHIEVEMENT: {
id: 26,
description: "Get V to level 15",
reward: "Achievement multiplier applies to Time Theorem generation",
effect: () => Achievements.power,
pet: Ra.pets.v,
level: 15,
displayIcon: `<span class="fas fa-graduation-cap"></span>`
},
ACHIEVEMENT_POW: {
id: 27,
description: "Get V to level 25",
reward: () => `Achievement multiplier is raised ${formatPow(1.5, 1, 1)}`,
pet: Ra.pets.v,
level: 25,
displayIcon: `<i class="fab fa-buffer"></i>`
},
RA_RECOLLECTION_UNLOCK: {
id: 28,
description: "Get 20 total Celestial Memory levels",
reward: "Unlock Recollection",
effect: 3,
totalLevels: 20,
}
};
EventHub.logic.on(GAME_EVENT.TAB_CHANGED, () => {
if (Tab.celestials.ra.isOpen) Ra.quotes.show(Ra.quotes.UNLOCK);
if (Tab.celestials.ra.isOpen) Ra.quotes.unlock.show();
});

View File

@ -1,52 +1,14 @@
import { GameDatabase } from "../secret-formula/game-database.js";
import { RebuyableMechanicState } from "../game-mechanics/index.js";
import { CelestialQuotes } from "./quotes.js";
import { BitUpgradeState, RebuyableMechanicState } from "../game-mechanics/index";
import { GameDatabase } from "../secret-formula/game-database";
export const TERESA_UNLOCKS = {
RUN: {
id: 0,
price: 1e14,
description: "Unlock Teresa's Reality.",
},
EPGEN: {
id: 1,
price: 1e18,
get description() {
if (Pelle.isDoomed) return "This has no effect while in Doomed.";
return "Unlock passive Eternity Point generation.";
},
},
EFFARIG: {
id: 2,
price: 1e21,
description: "Unlock Effarig, Celestial of Ancient Relics.",
},
SHOP: {
id: 3,
price: 1e24,
description: "Unlock the Perk Point Shop.",
},
UNDO: {
id: 4,
price: 1e10,
description: "Unlock \"Undo\" of equipping a Glyph.",
},
START_EU: {
id: 5,
price: 1e6,
get description() {
if (Pelle.isDoomed) return "This has no effect while in Doomed.";
return "You start Reality with all Eternity Upgrades unlocked.";
},
}
};
import { Quotes } from "./quotes";
export const Teresa = {
timePoured: 0,
unlockInfo: TERESA_UNLOCKS,
lastUnlock: "SHOP",
lastUnlock: "shop",
pouredAmountCap: 1e24,
displayName: "Teresa",
possessiveName: "Teresa's",
get isUnlocked() {
return Achievement(147).isUnlocked;
},
@ -60,18 +22,9 @@ export const Teresa = {
this.checkForUnlocks();
},
checkForUnlocks() {
for (const info of Object.values(Teresa.unlockInfo)) {
if (!this.has(info) && this.pouredAmount >= info.price) {
// eslint-disable-next-line no-bitwise
player.celestials.teresa.unlockBits |= (1 << info.id);
EventHub.dispatch(GAME_EVENT.CELESTIAL_UPGRADE_UNLOCKED, this, info);
for (const info of TeresaUnlocks.all) {
info.unlock();
}
}
},
has(info) {
if (!info.hasOwnProperty("id")) throw "Pass in the whole TERESA UNLOCK object";
// eslint-disable-next-line no-bitwise
return Boolean(player.celestials.teresa.unlockBits & (1 << info.id));
},
initializeRun() {
clearCelestialRuns();
@ -104,31 +57,7 @@ export const Teresa = {
get runCompleted() {
return player.celestials.teresa.bestRunAM.gt(0);
},
quotes: new CelestialQuotes("teresa", {
INITIAL: {
id: 1,
lines: [
"We have been observing you.",
"You have shown promise with your bending of Reality.",
"We are the Celestials, and we want you to join us.",
"My name is Teresa, the Celestial Of Reality.",
"Prove your worth.",
]
},
UNLOCK_REALITY: CelestialQuotes.singleLine(
2, "I will let you inside my Reality, mortal. Do not get crushed by it."
),
COMPLETE_REALITY: CelestialQuotes.singleLine(
3, "Why are you still here... you were supposed to fail."
),
EFFARIG: {
id: 4,
lines: [
"You are still no match for us.",
"I hope the others succeed where I have failed."
]
}
}),
quotes: Quotes.teresa,
symbol: "Ϟ"
};
@ -160,12 +89,15 @@ class PerkShopUpgradeState extends RebuyableMechanicState {
}
onPurchased() {
if (this.id === 0) {
GameCache.staticGlyphWeights.invalidate();
}
if (this.id === 1) {
Autobuyer.reality.bumpAmount(2);
}
// Give a single music glyph
if (this.id === 4) {
if (Glyphs.freeInventorySpace === 0) {
if (this.id === 4 && !Pelle.isDoomed) {
if (GameCache.glyphInventorySpace.value === 0) {
// Refund the perk point if they didn't actually get a glyph
Currency.perkPoints.add(1);
GameUI.notify.error("You have no empty inventory space!");
@ -175,35 +107,55 @@ class PerkShopUpgradeState extends RebuyableMechanicState {
}
}
// Fill the inventory with music glyphs
if (this.id === 5) {
const toCreate = Glyphs.freeInventorySpace;
if (this.id === 5 && !Pelle.isDoomed) {
const toCreate = GameCache.glyphInventorySpace.value;
for (let count = 0; count < toCreate; count++) Glyphs.addToInventory(GlyphGenerator.musicGlyph());
GameUI.notify.success(`Created ${quantifyInt("Music Glyph", toCreate)}`);
}
}
}
export const PerkShopUpgrade = (function() {
const db = GameDatabase.celestials.perkShop;
return {
glyphLevel: new PerkShopUpgradeState(db.glyphLevel),
rmMult: new PerkShopUpgradeState(db.rmMult),
bulkDilation: new PerkShopUpgradeState(db.bulkDilation),
autoSpeed: new PerkShopUpgradeState(db.autoSpeed),
musicGlyph: new PerkShopUpgradeState(db.musicGlyph),
fillMusicGlyph: new PerkShopUpgradeState(db.fillMusicGlyph),
};
}());
class TeresaUnlockState extends BitUpgradeState {
get bits() { return player.celestials.teresa.unlockBits; }
set bits(value) { player.celestials.teresa.unlockBits = value; }
get price() {
return this.config.price;
}
get pelleDisabled() {
return Pelle.isDoomed && this.config.isDisabledInDoomed;
}
get isEffectActive() {
return !this.pelleDisabled;
}
get canBeUnlocked() {
return !this.isUnlocked && Teresa.pouredAmount >= this.price;
}
get description() {
return typeof this.config.description === "function" ? this.config.description() : this.config.description;
}
onUnlock() {
this.config.onUnlock?.();
}
}
export const TeresaUnlocks = mapGameDataToObject(
GameDatabase.celestials.teresa.unlocks,
config => new TeresaUnlockState(config)
);
export const PerkShopUpgrade = mapGameDataToObject(
GameDatabase.celestials.perkShop,
config => new PerkShopUpgradeState(config)
);
EventHub.logic.on(GAME_EVENT.TAB_CHANGED, () => {
if (Tab.celestials.teresa.isOpen) Teresa.quotes.show(Teresa.quotes.INITIAL);
});
EventHub.logic.on(GAME_EVENT.CELESTIAL_UPGRADE_UNLOCKED, ([celestial, upgradeInfo]) => {
if (celestial === Teresa) {
if (upgradeInfo === TERESA_UNLOCKS.RUN) Teresa.quotes.show(Teresa.quotes.UNLOCK_REALITY);
if (upgradeInfo === TERESA_UNLOCKS.EFFARIG) Teresa.quotes.show(Teresa.quotes.EFFARIG);
}
if (Tab.celestials.teresa.isOpen) Teresa.quotes.initial.show();
});
EventHub.logic.on(GAME_EVENT.GAME_LOAD, () => Teresa.checkForUnlocks());

View File

@ -229,38 +229,105 @@ window.GlyphRarities = [
{
minStrength: 3.5,
name: "Celestial",
color: "#5151ec"
darkColor: "#5151ec",
lightColor: "#6666e9"
}, {
minStrength: 3.25,
name: "Transcendent",
color: "#03ffec"
darkColor: "#03ffec",
lightColor: "#00bdad"
}, {
minStrength: 3,
name: "Mythical",
color: "#d50000"
darkColor: "#d50000",
lightColor: "#d50000"
}, {
minStrength: 2.75,
name: "Legendary",
color: "#ff9800"
darkColor: "#ff9800",
lightColor: "#d68100"
}, {
minStrength: 2.5,
name: "Epic",
color: "#9c27b0"
darkColor: "#9c27b0",
lightColor: "#9c27b0"
}, {
minStrength: 2,
name: "Rare",
color: "#2196f3"
darkColor: "#2196f3",
lightColor: "#1187ee"
}, {
minStrength: 1.5,
name: "Uncommon",
color: "#43a047"
darkColor: "#43a047",
lightColor: "#3c9040"
}, {
minStrength: 1,
name: "Common",
color: "white"
darkColor: "white",
lightColor: "black"
},
];
window.GLYPH_TYPES = [
"power",
"infinity",
"replication",
"time",
"dilation",
"effarig",
"reality",
"cursed",
"companion"
];
window.BASIC_GLYPH_TYPES = [
"power",
"infinity",
"replication",
"time",
"dilation"
];
window.ALCHEMY_BASIC_GLYPH_TYPES = [
"power",
"infinity",
"replication",
"time",
"dilation",
"effarig"
];
window.GLYPH_SYMBOLS = {
power: "Ω",
infinity: "∞",
replication: "Ξ",
time: "Δ",
dilation: "Ψ",
effarig: "Ϙ",
reality: "Ϟ",
cursed: "⸸",
companion: "♥"
};
window.CANCER_GLYPH_SYMBOLS = {
power: "⚡",
infinity: "8",
replication: "⚤",
time: "🕟",
dilation: "☎",
effarig: "🦒",
reality: "⛧",
cursed: "☠",
companion: "³"
};
window.ALTERATION_TYPE = {
ADDITION: 1,
EMPOWER: 2,
BOOST: 3
};
window.BLACK_HOLE_PAUSE_MODE = {
NO_PAUSE: 0,
PAUSE_BEFORE_BH1: 1,
@ -371,3 +438,28 @@ window.SORT_ORDER = {
ASCENDING: 0,
DESCENDING: 1,
};
// One-indexed and ordered to simplify code elsewhere, do not change to be zero-indexed or reorder
window.PROGRESS_STAGE = {
PRE_INFINITY: 1,
EARLY_INFINITY: 2,
BREAK_INFINITY: 3,
REPLICANTI: 4,
EARLY_ETERNITY: 5,
ETERNITY_CHALLENGES: 6,
EARLY_DILATION: 7,
LATE_ETERNITY: 8,
EARLY_REALITY: 9,
TERESA: 10,
EFFARIG: 11,
ENSLAVED: 12,
V: 13,
RA: 14,
IMAGINARY_MACHINES: 15,
LAITELA: 16,
PELLE: 17,
};

View File

@ -35,11 +35,14 @@ window.GlobalErrorHandler = {
},
crash(message) {
if (window.GameUI !== undefined && GameUI.initialized) {
Modal.message.show(`${message}<br>Check the console for more details`);
Modal.message.show(`${message}<br>Check the console for more details`, {}, 3);
}
// eslint-disable-next-line no-debugger
debugger;
}
};
window.onerror = event => GlobalErrorHandler.onerror(event);
window.onerror = (event, source) => {
if (!source.endsWith(".js")) return;
GlobalErrorHandler.onerror(event);
};

View File

@ -1,4 +1,4 @@
import { DC } from "./constants.js";
import { DC } from "./constants";
/**
@ -200,6 +200,10 @@ Currency.antimatter = new class extends DecimalCurrency {
get value() { return player.antimatter; }
set value(value) {
if (InfinityChallenges.nextIC) InfinityChallenges.notifyICUnlock(value);
if (GameCache.cheapestAntimatterAutobuyer.value && value.gte(GameCache.cheapestAntimatterAutobuyer.value)) {
TabNotification.newAutobuyer.tryTrigger();
}
player.antimatter = value;
player.records.thisInfinity.maxAM = player.records.thisInfinity.maxAM.max(value);
player.records.thisEternity.maxAM = player.records.thisEternity.maxAM.max(value);

View File

@ -1,6 +1,8 @@
import { DC } from "./constants.js";
import { sha512_256 } from "js-sha512";
import { DC } from "./constants";
import FullScreenAnimationHandler from "./full-screen-animation-handler";
/* eslint-disable no-console */
// Disabling no-console here seems
// reasonable, since these are the devtools after all
@ -92,22 +94,21 @@ dev.tripleEverything = function() {
};
dev.barrelRoll = function() {
document.body.style.animation = "barrelRoll 5s 1";
setTimeout(() => document.body.style.animation = "", 5000);
FullScreenAnimationHandler.display("a-barrel-roll", 5);
};
dev.spin3d = function() {
if (document.body.style.animation === "") document.body.style.animation = "spin3d 3s infinite";
if (document.body.style.animation === "") document.body.style.animation = "a-spin3d 3s infinite";
else document.body.style.animation = "";
};
dev.spin4d = function() {
if (document.body.style.animation === "") document.body.style.animation = "spin4d 3s infinite";
if (document.body.style.animation === "") document.body.style.animation = "a-spin4d 3s infinite";
else document.body.style.animation = "";
};
dev.cancerize = function() {
Theme.tryUnlock("Cancer");
Theme.tryUnlock("Design");
Notation.emoji.setAsCurrent();
};
@ -165,8 +166,8 @@ dev.resetDilation = function() {
// when making a special glyph, so no max-params
// eslint-disable-next-line max-params
dev.giveSpecialGlyph = function(color, symbol, level, rawLevel = level) {
if (!specialGlyphSymbols.hasOwnProperty(symbol)) return;
if (Glyphs.freeInventorySpace === 0) return;
if (!Object.prototype.hasOwnProperty.call(specialGlyphSymbols, symbol)) return;
if (GameCache.glyphInventorySpace.value === 0) return;
const glyph = GlyphGenerator.randomGlyph({ actualLevel: level, rawLevel });
glyph.symbol = symbol;
glyph.color = color;
@ -174,12 +175,12 @@ dev.giveSpecialGlyph = function(color, symbol, level, rawLevel = level) {
};
dev.giveGlyph = function(level, rawLevel = level) {
if (Glyphs.freeInventorySpace === 0) return;
if (GameCache.glyphInventorySpace.value === 0) return;
Glyphs.addToInventory(GlyphGenerator.randomGlyph({ actualLevel: level, rawLevel }));
};
dev.giveRealityGlyph = function(level) {
if (Glyphs.freeInventorySpace === 0) return;
if (GameCache.glyphInventorySpace.value === 0) return;
Glyphs.addToInventory(GlyphGenerator.realityGlyph(level));
};
@ -349,11 +350,11 @@ dev.printResourceTotals = function() {
};
dev.unlockCelestialQuotes = function(celestial) {
const quotes = Celestials[celestial].quotes;
for (const q of quotes.quotesById) {
if (q === undefined) continue;
quotes.show(q);
}
Quotes[celestial].all.forEach(x => x.show());
};
dev.presentCelestialQuotes = function(celestial) {
Quotes[celestial].all.forEach(x => x.present());
};
// This doesn't check everything but hopefully it gets some of the more obvious ones.
@ -416,19 +417,16 @@ dev.testReplicantiCode = function(singleId, useDebugger = false) {
],
[
function() {
// eslint-disable-next-line no-bitwise
player.achievementBits[8] |= 16;
}
],
[
function() {
// eslint-disable-next-line no-bitwise
player.achievementBits[12] |= 8;
}
],
[
function() {
// eslint-disable-next-line no-bitwise
player.achievementBits[12] |= 128;
}
],
@ -458,7 +456,6 @@ dev.testReplicantiCode = function(singleId, useDebugger = false) {
],
[
function() {
// eslint-disable-next-line no-bitwise
player.reality.upgReqs = (1 << 6);
player.reality.upgradeBits = 64;
}
@ -594,11 +591,20 @@ dev.testGlyphs = function(config) {
runTrial(0);
};
dev.devMode = function() {
player.devMode = !player.devMode;
};
// May want to make this command in particular publicly known if automator gating is a common complaint post-release
dev.unlockAutomator = function() {
player.reality.automator.forceUnlock = true;
};
// This bypasses any conflict checking and forces the current save to overwrite the cloud save. This largely exists
// because normal cloud saving checks for a conflict and then always shows a modal if a conflict is found, only actually
// saving if the player says to in the modal. The check can fail if the cloud save is somehow malformed and missing
// props. This can lead to the check always failing, the modal never showing up, and cloud saving never occurring. That
// should in principle only show up in dev, as migrations aren't run on cloud saves, but this allows fixing in case.
dev.forceCloudSave = async function() {
const save = await Cloud.load();
const root = GameSaveSerializer.deserialize(save);
const saveId = GameStorage.currentSlot;
root.saves[saveId] = GameStorage.saves[saveId];
Cloud.save(saveId);
};

View File

@ -1,20 +1,15 @@
import { SetPurchasableMechanicState, RebuyableMechanicState } from "./game-mechanics/index.js";
import { DC } from "./constants.js";
import { SpeedrunMilestones } from "./speedrun.js";
import { RebuyableMechanicState, SetPurchasableMechanicState } from "./game-mechanics/index";
import { DC } from "./constants";
import FullScreenAnimationHandler from "./full-screen-animation-handler";
import { SpeedrunMilestones } from "./speedrun";
export function animateAndDilate() {
document.body.style.animation = "dilate 2s 1 linear";
setTimeout(() => {
document.body.style.animation = "";
}, 2000);
FullScreenAnimationHandler.display("a-dilate", 2);
setTimeout(startDilatedEternity, 1000);
}
export function animateAndUndilate() {
document.body.style.animation = "undilate 2s 1 linear";
setTimeout(() => {
document.body.style.animation = "";
}, 2000);
FullScreenAnimationHandler.display("a-undilate", 2);
setTimeout(() => {
eternity(false, false, { switchingDilation: true });
}, 1000);
@ -22,10 +17,11 @@ export function animateAndUndilate() {
export function startDilatedEternityRequest() {
if (!PlayerProgress.dilationUnlocked() || (Pelle.isDoomed && !Pelle.canDilateInPelle)) return;
const playAnimation = player.options.animations.dilation && document.body.style.animation === "";
const playAnimation = player.options.animations.dilation && !FullScreenAnimationHandler.isDisplaying;
if (player.dilation.active) {
// TODO Dilation modal
if (playAnimation) {
if (player.options.confirmations.dilation) {
Modal.exitDilation.show();
} else if (playAnimation) {
animateAndUndilate();
} else {
eternity(false, false, { switchingDilation: true });
@ -37,20 +33,20 @@ export function startDilatedEternityRequest() {
} else {
startDilatedEternity();
}
if (Pelle.isDoomed && !player.options.confirmations.dilation) {
PelleStrikes.dilation.trigger();
}
}
export function startDilatedEternity(auto) {
if (!PlayerProgress.dilationUnlocked()) return;
if (!PlayerProgress.dilationUnlocked()) return false;
if (GameEnd.creditsEverClosed) return false;
if (player.dilation.active) {
eternity(false, auto, { switchingDilation: true });
return;
return false;
}
Achievement(136).unlock();
eternity(false, auto, { switchingDilation: true });
player.dilation.active = true;
if (Pelle.isDoomed) PelleStrikes.dilation.trigger();
return true;
}
const DIL_UPG_NAMES = [
@ -60,6 +56,7 @@ const DIL_UPG_NAMES = [
];
export function buyDilationUpgrade(id, bulk = 1) {
if (GameEnd.creditsEverClosed) return false;
// Upgrades 1-3 are rebuyable, and can be automatically bought in bulk with a perk shop upgrade
const upgrade = DilationUpgrade[DIL_UPG_NAMES[id]];
if (id > 3 && id < 11) {
@ -114,12 +111,12 @@ export function getTachyonGalaxyMult(thresholdUpgrade) {
}
export function getDilationGainPerSecond() {
const mult = NG.multiplier;
if (Pelle.isDoomed) {
const tachyonEffect = Currency.tachyonParticles.value.pow(PelleRifts.death.milestones[1].effectOrDefault(1));
const tachyonEffect = Currency.tachyonParticles.value.pow(PelleRifts.paradox.milestones[1].effectOrDefault(1));
return new Decimal(tachyonEffect)
.timesEffectsOf(DilationUpgrade.dtGain, DilationUpgrade.dtGainPelle, DilationUpgrade.flatDilationMult)
.times(Pelle.specialGlyphEffect.dilation).div(3e4).times(mult);
.times(ShopPurchase.dilatedTimePurchases.currentMult ** 0.5)
.times(Pelle.specialGlyphEffect.dilation).div(3e4);
}
let dtRate = new Decimal(Currency.tachyonParticles.value)
.timesEffectsOf(
@ -127,21 +124,22 @@ export function getDilationGainPerSecond() {
Achievement(132),
Achievement(137),
RealityUpgrade(1),
AlchemyResource.dilation
AlchemyResource.dilation,
Ra.unlocks.continuousTTBoost.effects.dilatedTime,
Ra.unlocks.peakGamespeedDT
);
dtRate = dtRate.times(getAdjustedGlyphEffect("dilationDT"));
dtRate = dtRate.times(ShopPurchase.dilatedTimePurchases.currentMult);
dtRate = dtRate.times(
Math.clampMin(Decimal.log10(Replicanti.amount) * getAdjustedGlyphEffect("replicationdtgain"), 1));
dtRate = dtRate.times(Ra.gamespeedDTMult());
if (Enslaved.isRunning && !dtRate.eq(0)) dtRate = Decimal.pow10(Math.pow(dtRate.plus(1).log10(), 0.85) - 1);
dtRate = dtRate.times(RA_UNLOCKS.TT_BOOST.effect.dilatedTime());
dtRate = dtRate.times(mult);
if (V.isRunning) dtRate = dtRate.pow(0.5);
return dtRate;
}
function tachyonGainMultiplier() {
if (Pelle.isDisabled("tpMults")) return new Decimal(1);
const pow = Enslaved.isRunning ? Enslaved.tachyonNerf : 1;
return DC.D1.timesEffectsOf(
DilationUpgrade.tachyonGain,
GlyphSacrifice.dilation,
@ -149,7 +147,7 @@ function tachyonGainMultiplier() {
RealityUpgrade(4),
RealityUpgrade(8),
RealityUpgrade(15)
);
).pow(pow);
}
export function rewardTP() {
@ -157,13 +155,22 @@ export function rewardTP() {
player.dilation.lastEP = Currency.eternityPoints.value;
}
// This function exists to apply Teresa-25 in a consistent way; TP multipliers can be very volatile and
// applying the reward only once upon unlock promotes min-maxing the upgrade by unlocking dilation with
// TP multipliers as large as possible. Applying the reward to a base TP value and letting the multipliers
// act dynamically on this fixed base value elsewhere solves that issue
export function getBaseTP(antimatter) {
const am = (isInCelestialReality() || Pelle.isDoomed)
? antimatter
: Ra.unlocks.unlockDilationStartingTP.effectOrDefault(antimatter);
let baseTP = Decimal.pow(Decimal.log10(am) / 400, 1.5);
if (Enslaved.isRunning) baseTP = baseTP.pow(Enslaved.tachyonNerf);
return baseTP;
}
// Returns the TP that would be gained this run
export function getTP(antimatter) {
let tachyon = Decimal
.pow(Decimal.log10(antimatter) / 400, 1.5)
.times(tachyonGainMultiplier());
if (Enslaved.isRunning) tachyon = tachyon.pow(Enslaved.tachyonNerf);
return tachyon;
return getBaseTP(antimatter).times(tachyonGainMultiplier());
}
// Returns the amount of TP gained, subtracting out current TP; used only for displaying gained TP
@ -227,26 +234,12 @@ class RebuyableDilationUpgradeState extends RebuyableMechanicState {
}
}
export const DilationUpgrade = (function() {
const db = GameDatabase.eternity.dilation;
return {
dtGain: new RebuyableDilationUpgradeState(db.dtGain),
galaxyThreshold: new RebuyableDilationUpgradeState(db.galaxyThreshold),
tachyonGain: new RebuyableDilationUpgradeState(db.tachyonGain),
doubleGalaxies: new DilationUpgradeState(db.doubleGalaxies),
tdMultReplicanti: new DilationUpgradeState(db.tdMultReplicanti),
ndMultDT: new DilationUpgradeState(db.ndMultDT),
ipMultDT: new DilationUpgradeState(db.ipMultDT),
timeStudySplit: new DilationUpgradeState(db.timeStudySplit),
dilationPenalty: new DilationUpgradeState(db.dilationPenalty),
ttGenerator: new DilationUpgradeState(db.ttGenerator),
dtGainPelle: new RebuyableDilationUpgradeState(db.dtGainPelle),
galaxyMultiplier: new RebuyableDilationUpgradeState(db.galaxyMultiplier),
tickspeedPower: new RebuyableDilationUpgradeState(db.tickspeedPower),
galaxyThresholdPelle: new DilationUpgradeState(db.galaxyThresholdPelle),
flatDilationMult: new DilationUpgradeState(db.flatDilationMult),
};
}());
export const DilationUpgrade = mapGameDataToObject(
GameDatabase.eternity.dilation,
config => (config.rebuyable
? new RebuyableDilationUpgradeState(config)
: new DilationUpgradeState(config))
);
export const DilationUpgrades = {
rebuyable: [
@ -254,11 +247,5 @@ export const DilationUpgrades = {
DilationUpgrade.galaxyThreshold,
DilationUpgrade.tachyonGain,
],
fromId: (function() {
const upgradesById = [];
for (const upgrade of Object.values(DilationUpgrade)) {
upgradesById[upgrade.id] = upgrade;
}
return id => upgradesById[id];
}()),
fromId: id => DilationUpgrade.all.find(x => x.id === Number(id))
};

View File

@ -1,4 +1,4 @@
import { DC } from "./constants.js";
import { DC } from "./constants";
class DimBoostRequirement {
constructor(tier, amount) {
@ -32,7 +32,7 @@ export class DimBoost {
Achievement(117),
Achievement(142),
GlyphEffect.dimBoostPower,
PelleRifts.war.milestones[0]
PelleRifts.recursion.milestones[0]
).powEffectsOf(InfinityUpgrade.dimboostMult.chargedEffect);
if (GlyphAlteration.isAdded("effarig")) boost = boost.pow(getSecondaryGlyphEffect("effarigforgotten"));
return boost;
@ -141,15 +141,15 @@ export class DimBoost {
if (boosts >= DimBoost.maxDimensionsUnlockable - 1) dimensionRange = `to all Dimensions`;
let boostEffects;
if (NormalChallenge(8).isRunning) boostEffects = newUnlock === "" ? "" : ` to ${newUnlock}`;
else if (newUnlock === "") boostEffects = ` to ${formattedMultText} ${dimensionRange}`;
else boostEffects = ` to ${newUnlock} and ${formattedMultText} ${dimensionRange}`;
if (NormalChallenge(8).isRunning) boostEffects = newUnlock;
else if (newUnlock === "") boostEffects = `${formattedMultText} ${dimensionRange}`;
else boostEffects = `${newUnlock} and ${formattedMultText} ${dimensionRange}`;
const areDimensionsReset = `Reset
${((Perk.antimatterNoReset.isBought || Achievement(111).isUnlocked) &&
(!Pelle.isDoomed || PelleUpgrade.dimBoostResetsNothing.isBought)) ? "nothing" : "your Dimensions"}`;
return `${areDimensionsReset}${boostEffects}`;
if (boostEffects === "") return "Dimension Boosts are currently useless";
const areDimensionsKept = (Perk.antimatterNoReset.isBought || Achievement(111).canBeApplied) &&
(!Pelle.isDoomed || PelleUpgrade.dimBoostResetsNothing.isBought);
if (areDimensionsKept) return boostEffects[0].toUpperCase() + boostEffects.substring(1);
return `Reset your Dimensions to ${boostEffects}`;
}
static get purchasedBoosts() {
@ -165,22 +165,24 @@ export class DimBoost {
}
}
export function softReset(bulk, forcedNDReset = false, forcedAMReset = false) {
export function softReset(tempBulk, forcedNDReset = false, forcedAMReset = false) {
if (Currency.antimatter.gt(Player.infinityLimit)) return;
const bulk = Math.min(tempBulk, DimBoost.maxBoosts - player.dimensionBoosts);
EventHub.dispatch(GAME_EVENT.DIMBOOST_BEFORE, bulk);
player.dimensionBoosts = Math.max(0, player.dimensionBoosts + bulk);
resetChallengeStuff();
if (
forcedNDReset ||
!Perk.antimatterNoReset.isBought ||
(Pelle.isDoomed && !PelleUpgrade.dimBoostResetsNothing.canBeApplied)
) {
const canKeepDimensions = Pelle.isDoomed
? PelleUpgrade.dimBoostResetsNothing.canBeApplied
: Perk.antimatterNoReset.canBeApplied;
if (forcedNDReset || !canKeepDimensions) {
AntimatterDimensions.reset();
player.sacrificed = DC.D0;
resetTickspeed();
}
skipResetsIfPossible();
const canKeepAntimatter = (Achievement(111).isUnlocked || Perk.antimatterNoReset.isBought) && !Pelle.isDoomed;
const canKeepAntimatter = Pelle.isDoomed
? PelleUpgrade.dimBoostResetsNothing.canBeApplied
: (Achievement(111).isUnlocked || Perk.antimatterNoReset.canBeApplied);
if (!forcedAMReset && canKeepAntimatter) {
Currency.antimatter.bumpTo(Currency.antimatter.startingValue);
} else {
@ -199,10 +201,21 @@ export function skipResetsIfPossible() {
else if (InfinityUpgrade.skipReset1.isBought && player.dimensionBoosts < 1) player.dimensionBoosts = 1;
}
export function manualRequestDimensionBoost(bulk) {
if (Currency.antimatter.gt(Player.infinityLimit) || !DimBoost.requirement.isSatisfied) return;
if (!DimBoost.canBeBought) return;
if (GameEnd.creditsEverClosed) return;
if (player.options.confirmations.dimensionBoost) {
Modal.dimensionBoost.show({ bulk });
return;
}
requestDimensionBoost(bulk);
}
export function requestDimensionBoost(bulk) {
if (Currency.antimatter.gt(Player.infinityLimit) || !DimBoost.requirement.isSatisfied) return;
if (!DimBoost.canBeBought) return;
if (BreakInfinityUpgrade.autobuyMaxDimboosts.isBought && bulk) maxBuyDimBoosts(true);
if (BreakInfinityUpgrade.autobuyMaxDimboosts.isBought && bulk) maxBuyDimBoosts();
else softReset(1);
}

View File

@ -1,5 +1,6 @@
import { DimensionState } from "./dimension.js";
import { DC } from "../constants.js";
import { DC } from "../constants";
import { DimensionState } from "./dimension";
// Multiplier applied to all Antimatter Dimensions, regardless of tier. This is cached using a Lazy
// and invalidated every update.
@ -9,7 +10,6 @@ export function antimatterDimensionCommonMultiplier() {
multiplier = multiplier.times(Achievements.power);
multiplier = multiplier.times(ShopPurchase.dimPurchases.currentMult);
multiplier = multiplier.times(ShopPurchase.allDimPurchases.currentMult);
multiplier = multiplier.times(NG.multiplier);
if (!EternityChallenge(9).isRunning) {
multiplier = multiplier.times(Currency.infinityPower.value.pow(InfinityDimensions.powerConversionRate).max(1));
@ -54,7 +54,6 @@ export function antimatterDimensionCommonMultiplier() {
export function getDimensionFinalMultiplierUncached(tier) {
if (tier < 1 || tier > 8) throw new Error(`Invalid Antimatter Dimension tier ${tier}`);
if (Laitela.isRunning && tier > Laitela.maxAllowedDimension) return DC.D0;
if (NormalChallenge(10).isRunning && tier > 6) return DC.D1;
if (EternityChallenge(11).isRunning) {
return Currency.infinityPower.value.pow(
@ -82,7 +81,7 @@ export function getDimensionFinalMultiplierUncached(tier) {
}
// This power effect goes intentionally after all the nerf effects and shouldn't be moved before them
if (Ra.has(RA_UNLOCKS.EFFARIG_UNLOCK) && multiplier.gte(AlchemyResource.inflation.effectValue)) {
if (AlchemyResource.inflation.isUnlocked && multiplier.gte(AlchemyResource.inflation.effectValue)) {
multiplier = multiplier.pow(1.05);
}
@ -147,8 +146,6 @@ function applyNDPowers(mult, tier) {
const glyphPowMultiplier = getAdjustedGlyphEffect("powerpow");
const glyphEffarigPowMultiplier = getAdjustedGlyphEffect("effarigdimensions");
multiplier = multiplier.pow(NG.power);
if (InfinityChallenge(4).isRunning && player.postC4Tier !== tier) {
multiplier = multiplier.pow(InfinityChallenge(4).effectValue);
}
@ -165,14 +162,12 @@ function applyNDPowers(mult, tier) {
InfinityUpgrade.thisInfinityTimeMult.chargedEffect,
AlchemyResource.power,
Achievement(183),
PelleRifts.death
PelleRifts.paradox
);
multiplier = multiplier.pow(getAdjustedGlyphEffect("curseddimensions"));
if (V.has(V_UNLOCKS.ND_POW) && !Pelle.isDoomed) {
multiplier = multiplier.pow(V_UNLOCKS.ND_POW.effect());
}
multiplier = multiplier.pow(VUnlocks.adPow.effectOrDefault(1));
if (PelleStrikes.infinity.hasStrike) {
multiplier = multiplier.pow(0.5);
@ -183,6 +178,8 @@ function applyNDPowers(mult, tier) {
}
function onBuyDimension(tier) {
if (tier === 1) Tutorial.turnOffEffect(TUTORIAL_STATE.DIM1);
if (tier === 2) Tutorial.turnOffEffect(TUTORIAL_STATE.DIM2);
Achievement(10 + tier).unlock();
Achievement(23).tryUnlock();
@ -464,7 +461,7 @@ class AntimatterDimensionState extends DimensionState {
*/
get continuumValue() {
if (!this.isAvailableForPurchase) return 0;
// Enslaved limits dim 8 purchases to 1 only
// Nameless limits dim 8 purchases to 1 only
// Continuum should be no different
if (this.tier === 8 && Enslaved.isRunning) return 1;
return this.costScale.getContinuumValue(Currency.antimatter.value, 10) * Laitela.matterExtraPurchaseFactor;
@ -568,6 +565,7 @@ class AntimatterDimensionState extends DimensionState {
get productionPerSecond() {
const tier = this.tier;
if (Laitela.isRunning && tier > Laitela.maxAllowedDimension) return DC.D0;
let amount = this.totalAmount;
if (NormalChallenge(12).isRunning) {
if (tier === 2) amount = amount.pow(1.6);

View File

@ -1,5 +1,6 @@
import { DimensionState } from "./dimension.js";
import { DC } from "../constants.js";
import { DC } from "../constants";
import { DimensionState } from "./dimension";
export function infinityDimensionCommonMultiplier() {
let mult = new Decimal(ShopPurchase.allDimPurchases.currentMult)
@ -17,12 +18,9 @@ export function infinityDimensionCommonMultiplier() {
EternityUpgrade.idMultICRecords,
AlchemyResource.dimensionality,
ImaginaryUpgrade(8),
PelleRifts.war.milestones[1]
PelleRifts.recursion.milestones[1]
);
mult = mult.times(NG.multiplier);
if (Replicanti.areUnlocked && Replicanti.amount.gt(1)) {
mult = mult.times(replicantiMult());
}
@ -99,7 +97,7 @@ class InfinityDimensionState extends DimensionState {
}
get canUnlock() {
return ((Perk.bypassIDAntimatter.isBought && !Pelle.isDoomed) || this.antimatterRequirementReached) &&
return (Perk.bypassIDAntimatter.canBeApplied || this.antimatterRequirementReached) &&
this.ipRequirementReached;
}
@ -127,6 +125,10 @@ class InfinityDimensionState extends DimensionState {
}
get productionPerSecond() {
if (EternityChallenge(2).isRunning || EternityChallenge(10).isRunning ||
(Laitela.isRunning && this.tier > Laitela.maxAllowedDimension)) {
return DC.D0;
}
let production = this.amount;
if (EternityChallenge(11).isRunning) {
return production;
@ -139,11 +141,6 @@ class InfinityDimensionState extends DimensionState {
get multiplier() {
const tier = this.tier;
if (EternityChallenge(2).isRunning || EternityChallenge(10).isRunning ||
(Laitela.isRunning && this.tier > Laitela.maxAllowedDimension)) {
return DC.D0;
}
if (EternityChallenge(11).isRunning) return DC.D1;
let mult = GameCache.infinityDimensionCommonMultiplier.value
.timesEffectsOf(
@ -155,18 +152,16 @@ class InfinityDimensionState extends DimensionState {
if (tier === 1) {
mult = mult.times(PelleRifts.pestilence.milestones[0].effectOrDefault(1));
mult = mult.times(PelleRifts.decay.milestones[0].effectOrDefault(1));
}
mult = mult.pow(NG.power);
mult = mult.pow(getAdjustedGlyphEffect("infinitypow"));
mult = mult.pow(getAdjustedGlyphEffect("effarigdimensions"));
mult = mult.pow(getAdjustedGlyphEffect("curseddimensions"));
mult = mult.powEffectOf(AlchemyResource.infinity);
mult = mult.pow(Ra.momentumValue);
mult = mult.powEffectOf(PelleRifts.death);
mult = mult.powEffectOf(PelleRifts.paradox);
if (player.dilation.active || PelleStrikes.dilation.hasStrike) {
mult = dilatedValueOf(mult);
@ -388,7 +383,7 @@ export const InfinityDimensions = {
},
get powerConversionRate() {
const multiplier = PelleRifts.death.milestones[2].effectOrDefault(1);
const multiplier = PelleRifts.paradox.milestones[2].effectOrDefault(1);
return (7 + getAdjustedGlyphEffect("infinityrate") + PelleUpgrade.infConversion.effectOrDefault(0)) * multiplier;
}
};

View File

@ -1,5 +1,6 @@
import { DimensionState } from "./dimension.js";
import { DC } from "../constants.js";
import { DC } from "../constants";
import { DimensionState } from "./dimension";
export function buySingleTimeDimension(tier) {
const dim = TimeDimension(tier);
@ -102,9 +103,6 @@ export function timeDimensionCommonMultiplier() {
PelleRifts.chaos
);
mult = mult.times(NG.multiplier);
if (EternityChallenge(9).isRunning) {
mult = mult.times(
Decimal.pow(
@ -146,7 +144,7 @@ class TimeDimensionState extends DimensionState {
nextCost(bought) {
if (this._tier > 4 && bought < this.e6000ScalingAmount) {
const cost = Decimal.pow(this.costMultiplier, bought).times(this.baseCost);
if (PelleRifts.death.milestones[0].canBeApplied) {
if (PelleRifts.paradox.milestones[0].canBeApplied) {
return cost.div("1e2250").pow(0.5);
}
return cost;
@ -163,7 +161,7 @@ class TimeDimensionState extends DimensionState {
const exponent = this.e6000ScalingAmount + (bought - this.e6000ScalingAmount) * TimeDimensions.scalingPast1e6000;
const cost = Decimal.pow(base, exponent).times(this.baseCost);
if (PelleRifts.death.milestones[0].canBeApplied && this._tier > 4) {
if (PelleRifts.paradox.milestones[0].canBeApplied && this._tier > 4) {
return cost.div("1e2250").pow(0.5);
}
return cost;
@ -184,11 +182,6 @@ class TimeDimensionState extends DimensionState {
get multiplier() {
const tier = this._tier;
if (EternityChallenge(1).isRunning || EternityChallenge(10).isRunning ||
(Laitela.isRunning && tier > Laitela.maxAllowedDimension)) {
return DC.D0;
}
if (EternityChallenge(11).isRunning) return DC.D1;
let mult = GameCache.timeDimensionCommonMultiplier.value
.timesEffectsOf(
@ -207,9 +200,7 @@ class TimeDimensionState extends DimensionState {
mult = mult.powEffectOf(AlchemyResource.time);
mult = mult.pow(Ra.momentumValue);
mult = mult.pow(ImaginaryUpgrade(11).effectOrDefault(1));
mult = mult.powEffectOf(PelleRifts.death);
mult = mult.pow(NG.power);
mult = mult.powEffectOf(PelleRifts.paradox);
if (player.dilation.active || PelleStrikes.dilation.hasStrike) {
mult = dilatedValueOf(mult);
@ -225,6 +216,10 @@ class TimeDimensionState extends DimensionState {
}
get productionPerSecond() {
if (EternityChallenge(1).isRunning || EternityChallenge(10).isRunning ||
(Laitela.isRunning && this.tier > Laitela.maxAllowedDimension)) {
return DC.D0;
}
if (EternityChallenge(11).isRunning) {
return this.amount;
}

View File

@ -1,5 +1,6 @@
import { GameMechanicState, SetPurchasableMechanicState } from "./game-mechanics/index.js";
import { DC } from "./constants.js";
import { GameMechanicState, SetPurchasableMechanicState } from "./game-mechanics/index";
import { DC } from "./constants";
import FullScreenAnimationHandler from "./full-screen-animation-handler";
function giveEternityRewards(auto) {
player.records.bestEternity.time = Math.min(player.records.thisEternity.time, player.records.bestEternity.time);
@ -7,7 +8,7 @@ function giveEternityRewards(auto) {
const newEternities = Pelle.isDisabled("eternityMults")
? new Decimal(1)
: new Decimal(RealityUpgrade(3).effectOrDefault(1)).times(getAdjustedGlyphEffect("timeetermult"));
: new Decimal(getAdjustedGlyphEffect("timeetermult")).timesEffectsOf(RealityUpgrade(3), Achievement(113));
if (Currency.eternities.eq(0) && newEternities.lte(10)) {
Tab.dimensions.time.show();
@ -24,17 +25,16 @@ function giveEternityRewards(auto) {
if (EternityChallenge.isRunning) {
const challenge = EternityChallenge.current;
challenge.addCompletion();
challenge.addCompletion(false);
if (Perk.studyECBulk.isBought) {
let completionCount = 0;
while (!challenge.isFullyCompleted && challenge.canBeCompleted) {
challenge.addCompletion();
challenge.addCompletion(false);
completionCount++;
}
AutomatorData.lastECCompletionCount = completionCount;
if (Enslaved.isRunning && completionCount > 5) EnslavedProgress.ec1.giveProgress();
}
// eslint-disable-next-line no-bitwise
player.challenge.eternity.requirementBits &= ~(1 << challenge.id);
respecTimeStudies(auto);
}
@ -57,14 +57,12 @@ function giveEternityRewards(auto) {
}
export function eternityAnimation() {
document.body.style.animation = "eternify 3s 1";
setTimeout(() => {
document.body.style.animation = "";
}, 3000);
FullScreenAnimationHandler.display("a-eternify", 3);
}
export function eternityResetRequest() {
if (!Player.canEternity) return;
if (GameEnd.creditsEverClosed) return;
askEternityConfirmation();
}
@ -73,6 +71,11 @@ export function eternity(force, auto, specialConditions = {}) {
// eslint-disable-next-line no-param-reassign
force = true;
}
// We define this variable so we can use it in checking whether to give
// the secret achievement for respec without studies.
// Annoyingly, we need to check for studies right here; giveEternityRewards removes studies if we're in an EC,
// so doing the check later doesn't give us the initial state of having studies or not.
const noStudies = player.timestudy.studies.length === 0;
if (force) {
player.challenge.eternity.current = 0;
} else {
@ -82,20 +85,18 @@ export function eternity(force, auto, specialConditions = {}) {
player.requirementChecks.reality.noEternities = false;
}
if (player.dilation.active && (!force || Currency.infinityPoints.gte(Number.MAX_VALUE))) {
rewardTP();
}
if (player.dilation.active && (!force || Currency.infinityPoints.gte(Number.MAX_VALUE))) rewardTP();
initializeChallengeCompletions();
initializeResourcesAfterEternity();
if (!EternityMilestone.keepAutobuyers.isReached) {
if (!EternityMilestone.keepAutobuyers.isReached && !(Pelle.isDoomed && PelleUpgrade.keepAutobuyers.canBeApplied)) {
// Fix infinity because it can only break after big crunch autobuyer interval is maxed
player.break = false;
}
player.challenge.eternity.current = 0;
if (!specialConditions.enteringEC) {
if (!specialConditions.enteringEC && !Pelle.isDoomed) {
player.dilation.active = false;
}
resetInfinityRuns();
@ -105,6 +106,9 @@ export function eternity(force, auto, specialConditions = {}) {
AntimatterDimensions.reset();
if (!specialConditions.enteringEC && player.respec) {
if (noStudies) {
SecretAchievement(34).unlock();
}
respecTimeStudies(auto);
player.respec = false;
}
@ -132,6 +136,24 @@ export function eternity(force, auto, specialConditions = {}) {
return true;
}
export function animateAndEternity() {
if (!Player.canEternity) return;
const hasAnimation = !FullScreenAnimationHandler.isDisplaying &&
((player.dilation.active && player.options.animations.dilation) ||
(!player.dilation.active && player.options.animations.eternity));
if (hasAnimation) {
if (player.dilation.active) {
animateAndUndilate();
} else {
eternityAnimation();
setTimeout(eternity, 2250);
}
} else {
eternity();
}
}
export function initializeChallengeCompletions(isReality) {
NormalChallenges.clearCompletions();
if (!PelleUpgrade.keepInfinityChallenges.canBeApplied) InfinityChallenges.clearCompletions();
@ -167,20 +189,18 @@ export function initializeResourcesAfterEternity() {
}
function applyRealityUpgradesAfterEternity() {
if (Pelle.isDoomed) return;
if (player.eternityUpgrades.size < 3 && Perk.autounlockEU1.isBought) {
if (player.eternityUpgrades.size < 3 && Perk.autounlockEU1.canBeApplied) {
for (const id of [1, 2, 3]) player.eternityUpgrades.add(id);
}
}
function askEternityConfirmation() {
if (player.options.confirmations.eternity) {
if (player.dilation.active && player.options.confirmations.dilation) {
Modal.exitDilation.show();
} else if (player.options.confirmations.eternity) {
Modal.eternity.show();
} else if (player.options.animations.eternity && document.body.style.animation === "") {
eternityAnimation();
setTimeout(eternity, 2250);
} else {
eternity();
animateAndEternity();
}
}
@ -190,50 +210,18 @@ export class EternityMilestoneState {
}
get isReached() {
if (Pelle.isDoomed && this.config.pelleObsolete) {
return this.config.pelleObsolete();
if (Pelle.isDoomed && this.config.givenByPelle) {
return this.config.givenByPelle();
}
return Currency.eternities.gte(this.config.eternities);
}
}
export const EternityMilestone = (function() {
const db = GameDatabase.eternity.milestones;
const infinityDims = Array.dimensionTiers
.map(tier => new EternityMilestoneState(db[`autobuyerID${tier}`]));
return {
autobuyerIPMult: new EternityMilestoneState(db.autobuyerIPMult),
keepAutobuyers: new EternityMilestoneState(db.keepAutobuyers),
autobuyerReplicantiGalaxy: new EternityMilestoneState(db.autobuyerReplicantiGalaxy),
keepInfinityUpgrades: new EternityMilestoneState(db.keepInfinityUpgrades),
bigCrunchModes: new EternityMilestoneState(db.bigCrunchModes),
autoEP: new EternityMilestoneState(db.autoEP),
autoIC: new EternityMilestoneState(db.autoIC),
autobuyMaxGalaxies: new EternityMilestoneState(db.autobuyMaxGalaxies),
unlockReplicanti: new EternityMilestoneState(db.unlockReplicanti),
autobuyerID: tier => infinityDims[tier - 1],
keepBreakUpgrades: new EternityMilestoneState(db.keepBreakUpgrades),
autoUnlockID: new EternityMilestoneState(db.autoUnlockID),
unlockAllND: new EternityMilestoneState(db.unlockAllND),
replicantiNoReset: new EternityMilestoneState(db.replicantiNoReset),
autobuyerReplicantiChance: new EternityMilestoneState(db.autobuyerReplicantiChance),
autobuyerReplicantiInterval: new EternityMilestoneState(db.autobuyerReplicantiInterval),
autobuyerReplicantiMaxGalaxies: new EternityMilestoneState(db.autobuyerReplicantiMaxGalaxies),
autobuyerEternity: new EternityMilestoneState(db.autobuyerEternity),
autoEternities: new EternityMilestoneState(db.autoEternities),
autoInfinities: new EternityMilestoneState(db.autoInfinities),
};
}());
export const EternityMilestones = {
// This is a bit of a hack because autobuyerID is a function that returns EternityMilestoneState objects instead of a
// EternityMilestoneState object itself
all: Object.values(EternityMilestone)
.filter(m => typeof m !== "function")
.concat(Array.dimensionTiers
.map(tier => new EternityMilestoneState(GameDatabase.eternity.milestones[`autobuyerID${tier}`]))
)
};
export const EternityMilestone = mapGameDataToObject(
GameDatabase.eternity.milestones,
config => (config.isBaseResource
? new EternityMilestoneState(config)
: new EternityMilestoneState(config))
);
class EternityUpgradeState extends SetPurchasableMechanicState {
get currency() {
@ -321,16 +309,9 @@ class EPMultiplierState extends GameMechanicState {
}
}
export const EternityUpgrade = mapGameDataToObject(
GameDatabase.eternity.upgrades,
config => new EternityUpgradeState(config)
);
export const EternityUpgrade = (function() {
const db = GameDatabase.eternity.upgrades;
return {
idMultEP: new EternityUpgradeState(db.idMultEP),
idMultEternities: new EternityUpgradeState(db.idMultEternities),
idMultICRecords: new EternityUpgradeState(db.idMultICRecords),
tdMultAchs: new EternityUpgradeState(db.tdMultAchs),
tdMultTheorems: new EternityUpgradeState(db.tdMultTheorems),
tdMultRealTime: new EternityUpgradeState(db.tdMultRealTime),
epMult: new EPMultiplierState(),
};
}());
EternityUpgrade.epMult = new EPMultiplierState();

View File

@ -1,6 +1,6 @@
import { GameMechanicState } from "./game-mechanics/index.js";
import { DC } from "./constants.js";
import { DC } from "./constants";
import { deepmergeAll } from "@/utility/deepmerge";
import { GameMechanicState } from "./game-mechanics/index";
export function startEternityChallenge() {
initializeChallengeCompletions();
@ -158,8 +158,11 @@ export class EternityChallengeState extends GameMechanicState {
return Math.min(Math.floor(completions), this.maxCompletions);
}
addCompletion() {
addCompletion(auto = false) {
this.completions++;
if ((this.id === 4 || this.id === 12) && auto) {
this.tryFail(true);
}
if (this.id === 6) {
GameCache.dimensionMultDecrease.invalidate();
}
@ -170,6 +173,7 @@ export class EternityChallengeState extends GameMechanicState {
requestStart() {
if (!Tab.challenges.eternity.isUnlocked || this.isRunning) return;
if (GameEnd.creditsEverClosed) return;
if (!player.options.confirmations.challenges) {
this.start();
return;
@ -194,7 +198,7 @@ export class EternityChallengeState extends GameMechanicState {
}
if (Enslaved.isRunning) {
if (this.id === 6 && this.completions === 5) EnslavedProgress.ec6.giveProgress();
if (EnslavedProgress.challengeCombo.hasProgress) Tab.challenges.normal.show();
if (!auto && EnslavedProgress.challengeCombo.hasProgress) Tab.challenges.normal.show();
}
startEternityChallenge();
return true;
@ -224,22 +228,37 @@ export class EternityChallengeState extends GameMechanicState {
eternity(true);
}
fail() {
fail(auto = false) {
this.exit();
let reason;
if (auto) {
if (this.id === 4) {
reason = restriction => `having more than ${quantifyInt("Infinity", restriction)}`;
reason = restriction => `Auto Eternity Challenge completion completed ` +
`Eternity Challenge ${this.id} and made the next tier ` +
`require having less Infinities (${quantifyInt("Infinity", restriction)} ` +
`or less) than you had`;
} else if (this.id === 12) {
reason = restriction => `spending more than ${quantify("in-game second", restriction, 0, 1)} in it`;
reason = restriction => `Auto Eternity Challenge completion completed ` +
`Eternity Challenge ${this.id} and made the next tier ` +
`require spending less time in it (${quantify("in-game second", restriction, 0, 1)} ` +
`or less) than you had spent`;
}
Modal.message.show(`You failed Eternity Challenge ${this.id} due to
${reason(this.config.restriction(this.completions))}; you have now exited it.`);
} else if (this.id === 4) {
reason = restriction => `You failed Eternity Challenge ${this.id} due to ` +
`having more than ${quantifyInt("Infinity", restriction)}`;
} else if (this.id === 12) {
reason = restriction => `You failed Eternity Challenge ${this.id} due to ` +
`spending more than ${quantify("in-game second", restriction, 0, 1)} in it`;
}
Modal.message.show(`${reason(this.config.restriction(this.completions))}, ` +
`which has caused you to exit it.`,
{ closeEvent: GAME_EVENT.REALITY_RESET_AFTER }, 1);
EventHub.dispatch(GAME_EVENT.CHALLENGE_FAILED);
}
tryFail() {
tryFail(auto = false) {
if (this.isRunning && !this.isWithinRestriction) {
this.fail();
this.fail(auto);
return true;
}
return false;
@ -292,11 +311,11 @@ export const EternityChallenges = {
autoComplete: {
tick() {
if (!player.reality.autoEC || Pelle.isDisabled("autoec")) return;
if (Ra.has(RA_UNLOCKS.AUTO_RU_AND_INSTANT_EC)) {
if (Ra.unlocks.instantECAndRealityUpgradeAutobuyers.canBeApplied) {
let next = this.nextChallenge;
while (next !== undefined) {
while (!next.isFullyCompleted) {
next.addCompletion();
next.addCompletion(true);
}
next = this.nextChallenge;
}
@ -306,7 +325,7 @@ export const EternityChallenges = {
let next = this.nextChallenge;
while (player.reality.lastAutoEC - interval > 0 && next !== undefined) {
player.reality.lastAutoEC -= interval;
next.addCompletion();
next.addCompletion(true);
next = this.nextChallenge;
}
player.reality.lastAutoEC %= interval;
@ -317,14 +336,14 @@ export const EternityChallenges = {
},
get interval() {
if (!Perk.autocompleteEC1.isBought || Pelle.isDisabled("autoec")) return Infinity;
if (!Perk.autocompleteEC1.canBeApplied) return Infinity;
let minutes = Effects.min(
Number.MAX_VALUE,
Perk.autocompleteEC1,
Perk.autocompleteEC2,
Perk.autocompleteEC3
);
if (V.has(V_UNLOCKS.FAST_AUTO_EC)) minutes /= V_UNLOCKS.FAST_AUTO_EC.effect();
minutes /= VUnlocks.fastAutoEC.effectOrDefault(1);
return TimeSpan.fromMinutes(minutes).totalMilliseconds;
}
}

View File

@ -19,7 +19,6 @@ window.EventHub = class EventHub {
}
}
// eslint-disable-next-line max-params
dispatch(event, args) {
const handlers = this._handlers[event];
if (handlers === undefined) return;
@ -28,7 +27,6 @@ window.EventHub = class EventHub {
}
}
// eslint-disable-next-line max-params
static dispatch(event, ...args) {
EventHub.logic.dispatch(event, args);
GameUI.dispatch(event, args);
@ -78,8 +76,8 @@ window.GAME_EVENT = {
GLYPHS_EQUIPPED_CHANGED: "GLYPHS_EQUIPPED_CHANGED",
GLYPHS_CHANGED: "GLYPHS_CHANGED",
GLYPH_SACRIFICED: "GLYPH_SACRIFICED",
GLYPH_CHOICES_GENERATED: "GLYPH_CHOICES_GENERATED",
GLYPH_SET_SAVE_CHANGE: "GLYPH_SET_SAVE_CHANGE",
GLYPH_VISUAL_CHANGE: "GLYPH_VISUAL_CHANGE",
// Break Infinity
BREAK_INFINITY: "BREAK_INFINITY",
@ -89,14 +87,17 @@ window.GAME_EVENT = {
INFINITY_DIMENSION_UNLOCKED: "INFINITY_DIMENSION_UNLOCKED",
INFINITY_CHALLENGE_COMPLETED: "INFINITY_CHALLENGE_COMPLETED",
INFINITY_UPGRADE_BOUGHT: "INFINITY_UPGRADE_BOUGHT",
INFINITY_UPGRADE_CHARGED: "INFINITY_UPGRADE_CHARGED",
INFINITY_UPGRADES_DISCHARGED: "INFINITY_UPGRADES_DISCHARGED",
ACHIEVEMENT_UNLOCKED: "ACHIEVEMENT_UNLOCKED",
CHALLENGE_FAILED: "CHALLENGE_FAILED",
REALITY_UPGRADE_BOUGHT: "REALITY_UPGRADE_BOUGHT",
REALITY_UPGRADE_TEN_BOUGHT: "REALITY_UPGRADE_TEN_BOUGHT",
PERK_BOUGHT: "PERK_BOUGHT",
BLACK_HOLE_UNLOCKED: "BLACK_HOLE_UNLOCKED",
BLACK_HOLE_UPGRADE_BOUGHT: "BLACK_HOLE_UPGRADE_BOUGHT",
GAME_LOAD: "GAME_LOAD",
CELESTIAL_UPGRADE_UNLOCKED: "CELESTIAL_UPGRADE_UNLOCKED",
OFFLINE_CURRENCY_GAINED: "OFFLINE_CURRENCY_GAINED",
SAVE_CONVERTED_FROM_PREVIOUS_VERSION: "SAVE_CONVERTED_FROM_PREVIOUS_VERSION",
REALITY_FIRST_UNLOCKED: "REALITY_FIRST_UNLOCKED",
AUTOMATOR_SAVE_CHANGED: "AUTOMATOR_SAVE_CHANGED",
@ -107,6 +108,7 @@ window.GAME_EVENT = {
ACHIEVEMENT_EVENT_OTHER: "ACHIEVEMENT_EVENT_OTHER",
ENTER_PRESSED: "ENTER_PRESSED",
ARROW_KEY_PRESSED: "ARROW_KEY_PRESSED",
// UI Events
UPDATE: "UPDATE",

View File

@ -265,13 +265,11 @@ Array.prototype.compact = function() {
};
Array.prototype.toBitmask = function() {
// eslint-disable-next-line no-bitwise
return this.reduce((prev, val) => prev | (1 << val), 0);
};
Set.prototype.toBitmask = function() {
let mask = 0;
// eslint-disable-next-line no-bitwise
for (const id of this) mask |= (1 << id);
return mask;
};
@ -280,9 +278,8 @@ Array.fromBitmask = function(mask) {
const bitIndices = [];
let currentIndex = 0;
while (mask !== 0) {
// eslint-disable-next-line no-bitwise
if (mask & 1) bitIndices.push(currentIndex);
// eslint-disable-next-line no-bitwise, no-param-reassign
// eslint-disable-next-line no-param-reassign
mask >>= 1;
++currentIndex;
}

View File

@ -1,18 +1,26 @@
window.format = function format(value, places, placesUnder1000) {
if (Pelle.isDoomed) {
if ((Pelle.endState - 2.5) / 2 > Math.random()) return "END";
function isEND() {
const threshold = GameEnd.endState > END_STATE_MARKERS.END_NUMBERS
? 1
: (GameEnd.endState - END_STATE_MARKERS.FADE_AWAY) / 2;
// Using the Pelle.isDoomed getter here causes this to not update properly after a game restart
return player.celestials.pelle.doomed && Math.random() < threshold;
}
return Notations.current.format(value, places, placesUnder1000);
window.format = function format(value, places = 0, placesUnder1000 = 0) {
if (isEND()) return "END";
return Notations.current.format(value, places, placesUnder1000, 3);
};
window.formatInt = function formatInt(value) {
if (isEND()) return "END";
if (Notations.current.isPainful) {
return format(value, 2, 0);
return format(value, 2);
}
return formatWithCommas(typeof value === "number" ? value.toFixed(0) : value.toNumber().toFixed(0));
};
window.formatFloat = function formatFloat(value, digits) {
if (isEND()) return "END";
if (Notations.current.isPainful) {
return format(value, Math.max(2, digits), digits);
}
@ -20,6 +28,7 @@ window.formatFloat = function formatFloat(value, digits) {
};
window.formatPostBreak = function formatPostBreak(value, places, placesUnder1000) {
if (isEND()) return "END";
const notation = Notations.current;
// This is basically just a copy of the format method from notations library,
// with the pre-break case removed.
@ -66,11 +75,15 @@ window.formatRarity = function formatRarity(value) {
return `${format(value, 2, places)}%`;
};
// We assume 2/2 decimal places to keep parameter count sensible; this is used very rarely
// We assume 2/0, 2/2 decimal places to keep parameter count sensible; this is used very rarely
window.formatMachines = function formatMachines(realPart, imagPart) {
if (isEND()) return "END";
const parts = [];
if (Decimal.neq(realPart, 0)) parts.push(format(realPart, 2, 2));
if (Decimal.neq(realPart, 0)) parts.push(format(realPart, 2));
if (Decimal.neq(imagPart, 0)) parts.push(`${format(imagPart, 2, 2)}i`);
// This function is used for just RM and just iM in a few spots, so we have to push both parts conditionally
// Nonetheless, we also need to special-case both zero so that it doesn't end up displaying as an empty string
if (Decimal.eq(realPart, 0) && Decimal.eq(imagPart, 0)) return format(0);
return parts.join(" + ");
};

View File

@ -0,0 +1,16 @@
export default {
isDisplaying: false,
displayForce(name, duration) {
document.body.style.animation = `${name} ${duration}s 1`;
this.isDisplaying = true;
setTimeout(() => {
document.body.style.animation = "";
this.isDisplaying = false;
}, duration * 1000);
},
display(name, duration) {
if (!this.isDisplaying) {
this.displayForce(name, duration);
}
}
};

View File

@ -121,7 +121,9 @@ export class Galaxy {
function galaxyReset() {
EventHub.dispatch(GAME_EVENT.GALAXY_RESET_BEFORE);
player.galaxies++;
if (!Achievement(143).isUnlocked) player.dimensionBoosts = 0;
if (!Achievement(143).isUnlocked || (Pelle.isDoomed && !PelleUpgrade.galaxyNoResetDimboost.canBeApplied)) {
player.dimensionBoosts = 0;
}
softReset(0);
if (Notations.current === Notation.emoji) player.requirementChecks.permanent.emojiGalaxies++;
// This is specifically reset here because the check is actually per-galaxy and not per-infinity
@ -129,6 +131,16 @@ function galaxyReset() {
EventHub.dispatch(GAME_EVENT.GALAXY_RESET_AFTER);
}
export function manualRequestGalaxyReset(bulk) {
if (!Galaxy.canBeBought || !Galaxy.requirement.isSatisfied) return;
if (GameEnd.creditsEverClosed) return;
if (player.options.confirmations.antimatterGalaxy) {
Modal.antimatterGalaxy.show({ bulk });
return;
}
requestGalaxyReset(bulk);
}
export function requestGalaxyReset(bulk, limit = Number.MAX_VALUE) {
if (EternityMilestone.autobuyMaxGalaxies.isReached && bulk) return maxBuyGalaxies(limit);
if (player.galaxies >= limit || !Galaxy.canBeBought || !Galaxy.requirement.isSatisfied) return false;

View File

@ -1,4 +1,4 @@
import { PurchasableMechanicState } from "./puchasable.js";
import { PurchasableMechanicState } from "./puchasable";
/**
* @abstract
@ -20,16 +20,13 @@ export class BitPurchasableMechanicState extends PurchasableMechanicState {
get bitIndex() { throw new NotImplementedError(); }
get isBought() {
// eslint-disable-next-line no-bitwise
return (this.bits & (1 << this.bitIndex)) !== 0;
}
set isBought(value) {
if (value) {
// eslint-disable-next-line no-bitwise
this.bits |= (1 << this.bitIndex);
} else {
// eslint-disable-next-line no-bitwise
this.bits &= ~(1 << this.bitIndex);
}
}

View File

@ -0,0 +1,38 @@
import { GameMechanicState } from "./game-mechanic";
/**
* @abstract
*/
export class BitUpgradeState extends GameMechanicState {
constructor(config) {
super(config);
if (this.id < 0 || this.id > 31) throw new Error(`Id ${this.id} out of bit range`);
}
/**
* @abstract
*/
get bits() { throw new NotImplementedError(); }
set bits(value) { throw new NotImplementedError(); }
get isUnlocked() {
return Boolean(this.bits & (1 << this.id));
}
get canBeApplied() {
return this.isUnlocked && this.isEffectActive;
}
get canBeUnlocked() {
return !this.isUnlocked;
}
// eslint-disable-next-line no-empty-function
onUnlock() { }
unlock() {
if (!this.canBeUnlocked) return;
this.bits |= (1 << this.id);
this.onUnlock();
}
}

View File

@ -1,4 +1,4 @@
import { Effect } from "./effect.js";
import { Effect } from "./effect";
/**
* @abstract
@ -15,7 +15,7 @@ export class GameMechanicState extends Effect {
for (const key in config.effects) {
const nested = config.effects[key];
let effect;
if (typeof nested === "number" || nested instanceof Decimal) {
if (typeof nested === "number" || typeof nested === "function" || nested instanceof Decimal) {
effect = new Effect(nested);
} else {
effect = new Effect(nested.effect, nested.cap, nested.effectCondition);

View File

@ -1,7 +1,8 @@
export * from "./effect.js";
export * from "./effects.js";
export * from "./game-mechanic.js";
export * from "./puchasable.js";
export * from "./set-purchasable.js";
export * from "./bit-purchasable.js";
export * from "./rebuyable.js";
export * from "./effect";
export * from "./effects";
export * from "./game-mechanic";
export * from "./bit-upgrade-state";
export * from "./puchasable";
export * from "./set-purchasable";
export * from "./bit-purchasable";
export * from "./rebuyable";

View File

@ -1,4 +1,4 @@
import { GameMechanicState } from "./game-mechanic.js";
import { GameMechanicState } from "./game-mechanic";
/**
* @abstract

View File

@ -1,4 +1,4 @@
import { GameMechanicState } from "./game-mechanic.js";
import { GameMechanicState } from "./game-mechanic";
/**
* @abstract
@ -49,6 +49,7 @@ export class RebuyableMechanicState extends GameMechanicState {
purchase() {
if (!this.canBeBought) return false;
if (GameEnd.creditsEverClosed) return false;
this.currency.subtract(this.cost);
this.boughtAmount++;
this.onPurchased();

View File

@ -1,4 +1,4 @@
import { PurchasableMechanicState } from "./puchasable.js";
import { PurchasableMechanicState } from "./puchasable";
/**
* @abstract

View File

@ -1,89 +1,93 @@
export * from "./glyph-effects.js";
export * from "./player.js";
export * from "./glyph-effects";
export * from "./player";
export * from "./automator/automator-backend.js";
export * from "./performance-stats.js";
export * from "./currency.js";
export * from "./cache.js";
export * from "./intervals.js";
export * from "./keyboard.js";
export * from "./hotkeys.js";
export * from "./galaxy.js";
export * from "./away-progress.js";
export * from "./confirmations.js";
export * from "./automator/automator-backend";
export * from "./performance-stats";
export * from "./currency";
export * from "./cache";
export * from "./intervals";
export * from "./keyboard";
export * from "./hotkeys";
export * from "./galaxy";
export * from "./away-progress";
export * from "./confirmations";
export * from "./autobuyers/index.js";
export * from "./storage/index.js";
export * from "./autobuyers/index";
export * from "./storage/index";
export * from "./notations.js";
export * from "./tutorial.js";
export * from "./notations";
export * from "./tutorial";
export * from "./new-game.js";
export * from "./new-game";
export * from "./celestials/quotes.js";
export * from "./celestials/teresa.js";
export * from "./celestials/effarig.js";
export * from "./celestials/enslaved.js";
export * from "./celestials/V.js";
export * from "./celestials/ra/ra.js";
export * from "./celestials/ra/alchemy.js";
export * from "./celestials/laitela/laitela.js";
export * from "./celestials/laitela/dark-matter-dimension.js";
export * from "./celestials/laitela/singularity.js";
export * from "./celestials/pelle/pelle.js";
export * from "./celestials/pelle/strikes.js";
export * from "./celestials/pelle/rifts.js";
export * from "./celestials/pelle/galaxy-generator.js";
export * from "./celestials/celestials.js";
export * from "./celestials/quotes";
export * from "./celestials/teresa";
export * from "./celestials/effarig";
export * from "./celestials/enslaved";
export * from "./celestials/V";
export * from "./celestials/ra/ra";
export * from "./celestials/ra/alchemy";
export * from "./celestials/laitela/laitela";
export * from "./celestials/laitela/dark-matter-dimension";
export * from "./celestials/laitela/singularity";
export * from "./celestials/pelle/pelle";
export * from "./celestials/pelle/strikes";
export * from "./celestials/pelle/rifts";
export * from "./celestials/pelle/galaxy-generator";
export * from "./celestials/pelle/game-end";
export * from "./celestials/celestials";
export * from "./automator/index.js";
export * from "./automator/automator-points.js";
export * from "./automator/index";
export * from "./automator/automator-points";
export * from "./app/player-progress.js";
export * from "./app/modal.js";
export * from "./app/themes.js";
export * from "./app/options.js";
export * from "./app/ui.js";
export * from "./app/player-progress";
export * from "./app/modal";
export * from "./app/themes";
export * from "./app/options";
export * from "./app/ui";
export * from "./achievements/normal-achievement.js";
export * from "./achievements/secret-achievement.js";
export * from "./achievements/achievement-timer.js";
export * from "./achievements/normal-achievement";
export * from "./achievements/secret-achievement";
export * from "./achievements/achievement-timer";
export * from "./glyphs/glyph-core.js";
export * from "./glyphs/glyph-effects.js";
export * from "./glyphs/glyph-generator.js";
export * from "./glyphs/glyph-purge-handler.js";
export * from "./glyphs/auto-glyph-processor.js";
export * from "./glyphs/glyph-core";
export * from "./glyphs/glyph-effects";
export * from "./glyphs/glyph-generator";
export * from "./glyphs/glyph-purge-handler";
export * from "./glyphs/auto-glyph-processor";
export * from "./time.js";
export * from "./tickspeed.js";
export * from "./time";
export * from "./tickspeed";
export * from "./dimensions/antimatter-dimension.js";
export * from "./dimensions/infinity-dimension.js";
export * from "./dimensions/time-dimension.js";
export * from "./dimensions/antimatter-dimension";
export * from "./dimensions/infinity-dimension";
export * from "./dimensions/time-dimension";
export * from "./time-studies/index.js";
export * from "./time-studies/index";
export * from "./dimboost.js";
export * from "./sacrifice.js";
export * from "./big_crunch.js";
export * from "./challenge.js";
export * from "./eternity.js";
export * from "./eternity_challenge.js";
export * from "./reality.js";
export * from "./replicanti.js";
export * from "./time-theorems.js";
export * from "./reality-upgrades.js";
export * from "./imaginary-upgrades.js";
export * from "./perks.js";
export * from "./dilation.js";
export * from "./black_hole.js";
export * from "./machines.js";
export * from "./devtools.js";
export * from "./news-ticker.js";
export * from "./kong.js";
export * from "./ui/tabs.js";
export * from "./ui/tab-notifications.js";
export * from "./speedrun.js";
export * from "./dimboost";
export * from "./sacrifice";
export * from "./big_crunch";
export * from "./infinity-upgrades";
export * from "./break-infinity-upgrades";
export * from "./normal-challenges";
export * from "./infinity-challenges";
export * from "./eternity";
export * from "./eternity_challenge";
export * from "./reality";
export * from "./replicanti";
export * from "./time-theorems";
export * from "./reality-upgrades";
export * from "./imaginary-upgrades";
export * from "./perks";
export * from "./dilation";
export * from "./black_hole";
export * from "./machines";
export * from "./devtools";
export * from "./news-ticker";
export * from "./kong";
export * from "./ui/tabs";
export * from "./ui/tab-notifications";
export * from "./speedrun";
export * from "./automator/script-templates.js";
export * from "./automator/script-templates";

View File

@ -1,28 +1,4 @@
import { GameDatabase } from "./secret-formula/game-database.js";
import { DC } from "./constants.js";
// There is a little too much stuff about glyph effects to put in constants.
// The last glyph type you can only get if you got effarig reality
export const GLYPH_TYPES = ["power", "infinity", "replication", "time", "dilation", "effarig",
"reality", "cursed", "companion"];
export const BASIC_GLYPH_TYPES = ["power", "infinity", "replication", "time", "dilation"];
export const ALCHEMY_BASIC_GLYPH_TYPES = ["power", "infinity", "replication", "time", "dilation", "effarig"];
export const GLYPH_SYMBOLS = { power: "Ω", infinity: "∞", replication: "Ξ", time: "Δ", dilation: "Ψ",
effarig: "Ϙ", reality: "Ϟ", cursed: "⸸", companion: "♥" };
export const CANCER_GLYPH_SYMBOLS = { power: "⚡", infinity: "8", replication: "⚤", time: "🕟", dilation: "☎",
effarig: "🦒", reality: "⛧", cursed: "☠", companion: "³" };
export const GlyphCombiner = Object.freeze({
add: x => x.reduce(Number.sumReducer, 0),
multiply: x => x.reduce(Number.prodReducer, 1),
// For exponents, the base value is 1, so when we add two exponents a and b we want to get a + b - 1,
// so that if a and b are both close to 1 so is their sum. In general, when we add a list x of exponents,
// we have to add 1 - x.length to the actual sum, so that if all the exponents are close to 1 the result
// is also close to 1 rather than close to x.length.
addExponents: x => x.reduce(Number.sumReducer, 1 - x.length),
multiplyDecimal: x => x.reduce(Decimal.prodReducer, DC.D1)
});
import { GameDatabase } from "./secret-formula/game-database";
/**
* Multiple glyph effects are combined into a summary object of this type.
@ -41,89 +17,117 @@ class GlyphEffectConfig {
* glyphs.
* @param {string} [setup.genericDesc] (Defaults to singleDesc with {value} replaced with "x") Generic
* description of the glyph's effect
* @param {string} [setup.shortDesc] Short and condensed version of the glyph's effect for use in the Modal
* @param {(function(number, number): number) | function(number, number): Decimal} [setup.effect] Calculate effect
* value from level and strength
* @param {NumericToString<number | Decimal>} [setup.formatEffect] Format the effect's value into a string. Defaults
* @param {function(number | Decimal): string} [setup.formatEffect] Format the effect's value into a string. Defaults
* to format(x, 3, 3)
* @param {NumericToString<number | Decimal>} [setup.formatSingleEffect] Format the effect's value into a string, used
* @param {function(number | Decimal): string} [setup.formatSingleEffect] Format the effect's value into a string, used
* for effects which need to display different values in single values versus combined values (eg. power effects)
* @param {NumericFunction<number | Decimal>} [setup.softcap] An optional softcap to be applied after glyph
* @param {function(number | Decimal): number | Decimal} [setup.softcap] An optional softcap to be applied after glyph
* effects are combined.
* @param {((function(number[]): GlyphEffectConfig__combine_result) | function(number[]): number)} setup.combine
* Specification of how multiple glyphs combine. Can be GlyphCombiner.add or GlyphCombiner.multiply for most glyphs.
* Otherwise, should be a function that takes a potentially empty array of numbers (each glyph's effect value)
* and returns a combined effect or an object with the combined effect amd a capped indicator.
*
* @param {boolean} [setup.enabledInDoomed] Determines if this effect is enabled while doomed. Defaults to false
*/
constructor(setup) {
GlyphEffectConfig.checkInputs(setup);
/** @member{string} unique key for the effect -- powerpow, etc */
/** @type {string} unique key for the effect -- powerpow, etc */
this.id = setup.id;
/** @member{number} bit position for the effect in the effect bitmask */
/** @type {number} bit position for the effect in the effect bitmask */
this.bitmaskIndex = setup.bitmaskIndex;
/** @member{boolean} flag to separate "basic"/effarig glyphs from cursed/reality glyphs */
/** @type {boolean} flag to separate "basic"/effarig glyphs from cursed/reality glyphs */
this.isGenerated = setup.isGenerated;
/** @member{string[]} the types of glyphs this effect can occur on */
/** @type {string[]} the types of glyphs this effect can occur on */
this.glyphTypes = setup.glyphTypes;
/** @member{string} See info about setup, above*/
this.singleDesc = setup.singleDesc;
/** @member{string} See info about setup, above*/
this.totalDesc = setup.totalDesc || setup.singleDesc;
/** @member {string} genericDesc description of the effect without a specific value */
this.genericDesc = setup.genericDesc || setup.singleDesc.replace("{value}", "x");
/** @member {string} shortDesc shortened description for use in glyph choice info modal */
this.shortDesc = setup.shortDesc;
/** @type {string} See info about setup, above */
this._singleDesc = setup.singleDesc;
/** @type {string} See info about setup, above */
this._totalDesc = setup.totalDesc ?? setup.singleDesc;
/** @type {string} description of the effect without a specific value */
this._genericDesc = setup.genericDesc ?? setup.singleDesc.replace("{value}", "x");
/** @type {string} shortened description for use in glyph choice info modal */
this._shortDesc = setup.shortDesc;
/**
* @member {(function(number, number): number) | function(number, number): Decimal} effect Calculate effect
* @type {(function(number, number): number) | function(number, number): Decimal} Calculate effect
* value from level and strength
*/
this.effect = setup.effect;
/**
* @member {NumericToString<number | Decimal>} formatEffect formatting function for the effect
* @type {function(number | Decimal): string} formatting function for the effect
* (just the number conversion). Combined with the description strings to make descriptions
*/
this.formatEffect = setup.formatEffect || (x => format(x, 3, 3));
/** @member{NumericToString<number | Decimal>} See info about setup, above*/
this.formatEffect = setup.formatEffect ?? (x => format(x, 3, 3));
/** @type {function(number | Decimal): string} See info about setup, above */
this.formatSingleEffect = setup.formatSingleEffect || this.formatEffect;
/**
* @member {function(number[]): GlyphEffectConfig__combine_result} combine Function that combines
* @type {function(number[]): GlyphEffectConfig__combine_result} combine Function that combines
* multiple glyph effects into one value (adds up, applies softcaps, etc)
*/
this.combine = GlyphEffectConfig.setupCombine(setup);
/** @member{function(number)} conversion function to produce altered glyph effect */
/** @type {function(number)} conversion function to produce altered glyph effect */
this.conversion = setup.conversion;
/**
* @member {NumericToString<number | Decimal>} formatSecondaryEffect formatting function for
* @type {function(number | Decimal): string} formatSecondaryEffect formatting function for
* the secondary effect (if there is one)
*/
this.formatSecondaryEffect = setup.formatSecondaryEffect || (x => format(x, 3, 3));
/** @member{NumericToString<number | Decimal>} See info about setup, above*/
/** @type {function(number | Decimal): string} See info about setup, above */
this.formatSingleSecondaryEffect = setup.formatSingleSecondaryEffect || this.formatSecondaryEffect;
/** @member{string} color to show numbers in glyph tooltips if boosted */
/** @type {string} color to show numbers in glyph tooltips if boosted */
this.alteredColor = setup.alteredColor;
/** @member{number} string passed along to tooltip code to ensure proper formatting */
/** @type {number} string passed along to tooltip code to ensure proper formatting */
this.alterationType = setup.alterationType;
/** @member{Boolean} Indicates whether the effect grows with level or shrinks */
/** @type {boolean} Indicates whether the effect grows with level or shrinks */
this._biggerIsBetter = undefined;
/** @type {boolean} Determines if effect is disabled while in doomed */
this._enabledInDoomed = setup.enabledInDoomed ?? false;
}
/**
* @returns{Boolean}
* @returns {boolean}
*/
get biggerIsBetter() {
if (this._biggerIsBetter === undefined) this._biggerIsBetter = this.checkBiggerIsBetter();
return this._biggerIsBetter;
}
/**
* @returns{Number}
*/
get singleDesc() {
const singleDesc = this._singleDesc;
return typeof singleDesc === "function" ? singleDesc() : singleDesc;
}
get totalDesc() {
const totalDesc = this._totalDesc;
return typeof totalDesc === "function" ? totalDesc() : totalDesc;
}
get genericDesc() {
const genericDesc = this._genericDesc;
return typeof genericDesc === "function" ? genericDesc() : genericDesc;
}
get shortDesc() {
const shortDesc = this._shortDesc;
return typeof shortDesc === "function" ? shortDesc() : shortDesc;
}
get isDisabledByDoomed() {
return Pelle.isDoomed && !this._enabledInDoomed;
}
/** @returns {number} */
compareValues(effectValueA, effectValueB) {
const result = Decimal.compare(effectValueA, effectValueB);
return this.biggerIsBetter ? result : -result;
}
/** @private */
/**
* @private
* @returns {boolean}
*/
checkBiggerIsBetter() {
const baseEffect = new Decimal(this.effect(1, 1.01));
const biggerEffect = new Decimal(this.effect(100, 2));
@ -134,7 +138,7 @@ class GlyphEffectConfig {
static checkInputs(setup) {
const KNOWN_KEYS = ["id", "bitmaskIndex", "glyphTypes", "singleDesc", "totalDesc", "genericDesc", "effect",
"formatEffect", "formatSingleEffect", "combine", "softcap", "conversion", "formatSecondaryEffect",
"formatSingleSecondaryEffect", "alteredColor", "alterationType", "isGenerated", "shortDesc"];
"formatSingleSecondaryEffect", "alteredColor", "alterationType", "isGenerated", "shortDesc", "enabledInDoomed"];
const unknownField = Object.keys(setup).find(k => !KNOWN_KEYS.includes(k));
if (unknownField !== undefined) {
throw new Error(`Glyph effect "${setup.id}" includes unrecognized field "${unknownField}"`);
@ -157,9 +161,7 @@ class GlyphEffectConfig {
}
}
/**
* @private
*/
/** @private */
static setupCombine(setup) {
let combine = setup.combine;
const softcap = setup.softcap;
@ -187,629 +189,24 @@ class GlyphEffectConfig {
}
}
export const ALTERATION_TYPE = {
ADDITION: 1,
EMPOWER: 2,
BOOST: 3
};
export const realityGlyphEffectLevelThresholds = [0, 9000, 15000, 25000];
GameDatabase.reality.glyphEffects = [
{
id: "timepow",
bitmaskIndex: 0,
isGenerated: true,
glyphTypes: ["time"],
singleDesc: "Time Dimension power +{value}",
totalDesc: "Time Dimension multipliers ^{value}",
shortDesc: "TD power +{value}",
effect: (level, strength) => 1.01 + Math.pow(level, 0.32) * Math.pow(strength, 0.45) / 75,
formatEffect: x => format(x, 3, 3),
formatSingleEffect: x => format(x - 1, 3, 3),
combine: GlyphCombiner.addExponents,
}, {
id: "timespeed",
bitmaskIndex: 1,
isGenerated: true,
glyphTypes: ["time"],
singleDesc: "Multiply game speed by {value}",
totalDesc: "Game runs ×{value} faster",
genericDesc: "Game speed multiplier",
shortDesc: "Game speed ×{value}",
effect: (level, strength) => (GlyphAlteration.isEmpowered("time")
? 1 + Math.pow(level, 0.35)
: 1 + Math.pow(level, 0.3) * Math.pow(strength, 0.65) / 20),
formatEffect: x => format(x, 3, 3),
combine: GlyphCombiner.multiply,
alteredColor: () => GlyphAlteration.getEmpowermentColor("time"),
alterationType: ALTERATION_TYPE.EMPOWER
}, {
id: "timeetermult",
bitmaskIndex: 2,
isGenerated: true,
glyphTypes: ["time"],
singleDesc: "Multiply Eternity gain by {value}",
totalDesc: "Eternity gain ×{value}",
genericDesc: "Eternity gain multiplier",
shortDesc: "Eternities ×{value}",
effect: (level, strength) => Math.pow((strength + 3) * level, 0.9) *
Math.pow(3, GlyphAlteration.sacrificeBoost("time")),
formatEffect: x => format(x, 2, 2),
combine: GlyphCombiner.multiply,
alteredColor: () => GlyphAlteration.getBoostColor("time"),
alterationType: ALTERATION_TYPE.BOOST
}, {
id: "timeEP",
bitmaskIndex: 3,
isGenerated: true,
glyphTypes: ["time"],
singleDesc: () => (GlyphAlteration.isAdded("time")
? "Eternity Point gain ×{value} [and ^]{value2}"
: "Multiply Eternity Point gain by {value}"),
totalDesc: () => (GlyphAlteration.isAdded("time")
? "Eternity Point gain ×{value} and ^{value2}"
: "Eternity Point gain ×{value}"),
genericDesc: () => (GlyphAlteration.isAdded("time")
? "Eternity Point gain multiplier and power"
: "Eternity Point gain multiplier"),
shortDesc: () => (GlyphAlteration.isAdded("time")
? "EP ×{value} and ^{value2}"
: "EP ×{value}"),
effect: (level, strength) => Math.pow(level * strength, 3) * 100,
formatEffect: x => format(x, 2, 3),
combine: GlyphCombiner.multiply,
conversion: x => 1 + Math.log10(x) / 1000,
formatSecondaryEffect: x => format(x, 4, 4),
alteredColor: () => GlyphAlteration.getAdditionColor("time"),
alterationType: ALTERATION_TYPE.ADDITION
}, {
id: "dilationDT",
bitmaskIndex: 4,
isGenerated: true,
glyphTypes: ["dilation"],
singleDesc: "Multiply Dilated Time gain by {value}",
totalDesc: "Dilated Time gain ×{value}",
shortDesc: "DT ×{value}",
effect: (level, strength) => (GlyphAlteration.isEmpowered("dilation")
? DC.D1_005.pow(level).times(15)
: Decimal.pow(level * strength, 1.5).times(2)),
formatEffect: x => format(x, 2, 1),
combine: GlyphCombiner.multiplyDecimal,
alteredColor: () => GlyphAlteration.getEmpowermentColor("dilation"),
alterationType: ALTERATION_TYPE.EMPOWER
}, {
id: "dilationgalaxyThreshold",
bitmaskIndex: 5,
isGenerated: true,
glyphTypes: ["dilation"],
singleDesc: "Tachyon Galaxy threshold multiplier ×{value}",
genericDesc: "Tachyon Galaxy cost multiplier",
shortDesc: "TG threshold ×{value}",
effect: (level, strength) => 1 - Math.pow(level, 0.17) * Math.pow(strength, 0.35) / 100 -
GlyphAlteration.sacrificeBoost("dilation") / 50,
formatEffect: x => format(x, 3, 3),
alteredColor: () => GlyphAlteration.getBoostColor("dilation"),
alterationType: ALTERATION_TYPE.BOOST,
combine: effects => {
const prod = effects.reduce(Number.prodReducer, 1);
return prod < 0.4
? { value: 0.4 - Math.pow(0.4 - prod, 1.7), capped: true }
: { value: prod, capped: false };
},
}, {
// TTgen slowly generates TT, value amount is per second, displayed per hour
id: "dilationTTgen",
bitmaskIndex: 6,
isGenerated: true,
glyphTypes: ["dilation"],
singleDesc: () => (GlyphAlteration.isAdded("dilation")
? "Generates {value} Time Theorems/hour [and\nmultiplies Time Theorem generation by] {value2}"
: "Generates {value} Time Theorems per hour"),
totalDesc: () => (GlyphAlteration.isAdded("dilation")
? "Generating {value} Time Theorems/hour and Time Theorem generation ×{value2}"
: "Generating {value} Time Theorems per hour"),
genericDesc: () => (GlyphAlteration.isAdded("dilation")
? "Time Theorem generation and multiplier"
: "Time Theorem generation"),
shortDesc: () => (GlyphAlteration.isAdded("dilation")
? "{value} TT/hr and TTgen ×{value2}"
: "{value} TT/hr"),
effect: (level, strength) => Math.pow(level * strength, 0.5) / 10000,
/** @type {function(number): string} */
formatEffect: x => format(3600 * x, 2, 2),
combine: GlyphCombiner.add,
conversion: x => Math.clampMin(Math.pow(10000 * x, 1.6), 1),
formatSecondaryEffect: x => format(x, 2, 2),
alteredColor: () => GlyphAlteration.getAdditionColor("dilation"),
alterationType: ALTERATION_TYPE.ADDITION
}, {
id: "dilationpow",
bitmaskIndex: 7,
isGenerated: true,
glyphTypes: ["dilation"],
singleDesc: "Antimatter Dimension power +{value} while Dilated",
totalDesc: "Antimatter Dimension multipliers ^{value} while Dilated",
genericDesc: "Antimatter Dimensions ^x while Dilated",
shortDesc: "Dilated AD power +{value}",
effect: (level, strength) => 1.1 + Math.pow(level, 0.7) * Math.pow(strength, 0.7) / 25,
formatEffect: x => format(x, 2, 2),
formatSingleEffect: x => format(x - 1, 2, 2),
combine: GlyphCombiner.addExponents,
}, {
id: "replicationspeed",
bitmaskIndex: 8,
isGenerated: true,
glyphTypes: ["replication"],
singleDesc: "Multiply Replication speed by {value}",
totalDesc: "Replication speed ×{value}",
genericDesc: "Replication speed multiplier",
shortDesc: "Replication speed ×{value}",
effect: (level, strength) => (GlyphAlteration.isEmpowered("replication")
? DC.D1_007.pow(level).times(10)
: Decimal.times(level, strength).times(3)),
formatEffect: x => format(x, 2, 1),
combine: GlyphCombiner.multiplyDecimal,
alteredColor: () => GlyphAlteration.getEmpowermentColor("replication"),
alterationType: ALTERATION_TYPE.EMPOWER
}, {
id: "replicationpow",
bitmaskIndex: 9,
isGenerated: true,
glyphTypes: ["replication"],
singleDesc: "Replicanti multiplier power +{value}",
totalDesc: "Replicanti multiplier ^{value}",
shortDesc: "Replicanti mult. power +{value}",
effect: (level, strength) => 1.1 + Math.pow(level, 0.5) * strength / 25 +
GlyphAlteration.sacrificeBoost("replication") * 3,
formatEffect: x => format(x, 2, 2),
formatSingleEffect: x => format(x - 1, 2, 2),
combine: GlyphCombiner.addExponents,
alteredColor: () => GlyphAlteration.getBoostColor("replication"),
alterationType: ALTERATION_TYPE.BOOST
}, {
id: "replicationdtgain",
bitmaskIndex: 10,
isGenerated: true,
glyphTypes: ["replication"],
singleDesc: () => (GlyphAlteration.isAdded("replication")
? "Multiply Dilated Time [and Replicanti speed] by \nlog₁₀(replicanti)×{value}"
: "Multiply Dilated Time gain by \nlog₁₀(replicanti)×{value}"),
totalDesc: () => (GlyphAlteration.isAdded("replication")
? "Dilated Time gain and Replication speed ×(log₁₀(replicanti)×{value})"
: "Dilated Time gain ×(log₁₀(replicanti)×{value})"),
genericDesc: () => (GlyphAlteration.isAdded("replication")
? "Dilated Time+Replicanti mult (log₁₀(replicanti))"
: "Dilated Time gain multiplier (log₁₀(replicanti))"),
shortDesc: () => (GlyphAlteration.isAdded("replication")
? "DT and repl. ×log₁₀(repl.)×{value}"
: "DT ×log₁₀(repl.)×{value}"),
effect: (level, strength) => 0.0003 * Math.pow(level, 0.3) * Math.pow(strength, 0.65),
formatEffect: x => format(x, 5, 5),
formatSingleEffect: x => format(x, 5, 5),
// It's bad to stack this one additively (N glyphs acts as a DT mult of N) or multiplicatively (the raw number is
// less than 1), so instead we do a multiplicative stacking relative to the "base" effect of a level 1, 0% glyph.
// We also introduce a 3x mult per glyph after the first, so that stacking level 1, 0% glyphs still has an effect.
// This is still just a flat DT mult when stacking multiple glyphs, but at least it's bigger than 2 or 3.
combine: effects => ({
value: effects.length === 0 ? 0 : effects.reduce(Number.prodReducer, Math.pow(0.0001, 1 - effects.length)),
capped: false
}),
conversion: x => x,
formatSecondaryEffect: x => format(x, 2, 3),
formatSingleSecondaryEffect: x => format(x, 5, 5),
alteredColor: () => GlyphAlteration.getAdditionColor("replication"),
alterationType: ALTERATION_TYPE.ADDITION
}, {
id: "replicationglyphlevel",
bitmaskIndex: 11,
isGenerated: true,
glyphTypes: ["replication"],
singleDesc: () => `Replicanti scaling for next Glyph level: \n^${format(0.4, 1, 1)}
^(${format(0.4, 1, 1)} + {value})`,
totalDesc: () => `Replicanti scaling for next Glyph level: ^${format(0.4, 1, 1)}
^(${format(0.4, 1, 1)} + {value})`,
genericDesc: "Replicanti scaling for Glyph level",
shortDesc: "Replicanti pow. for level +{value}",
effect: (level, strength) => Math.pow(Math.pow(level, 0.25) * Math.pow(strength, 0.4), 0.5) / 50,
formatEffect: x => format(x, 3, 3),
combine: effects => {
let sum = effects.reduce(Number.sumReducer, 0);
if (effects.length > 2) sum *= 6 / (effects.length + 4);
return sum > 0.1
? { value: 0.1 + 0.2 * (sum - 0.1), capped: true }
: { value: sum, capped: effects.length > 2 };
}
}, {
id: "infinitypow",
bitmaskIndex: 12,
isGenerated: true,
glyphTypes: ["infinity"],
singleDesc: "Infinity Dimension power +{value}",
totalDesc: "Infinity Dimension multipliers ^{value}",
shortDesc: "ID power +{value}",
effect: (level, strength) => 1.007 + Math.pow(level, 0.21) * Math.pow(strength, 0.4) / 75 +
GlyphAlteration.sacrificeBoost("infinity") / 50,
formatEffect: x => format(x, 3, 3),
formatSingleEffect: x => format(x - 1, 3, 3),
combine: GlyphCombiner.addExponents,
alteredColor: () => GlyphAlteration.getBoostColor("infinity"),
alterationType: ALTERATION_TYPE.BOOST
}, {
id: "infinityrate",
bitmaskIndex: 13,
isGenerated: true,
glyphTypes: ["infinity"],
singleDesc: () => `Infinity Power conversion rate: \n^${formatInt(7)}
^(${formatInt(7)} + {value})`,
totalDesc: () => `Infinity Power conversion rate: ^${formatInt(7)}
^(${formatInt(7)} + {value})`,
genericDesc: "Infinity Power conversion rate",
shortDesc: "Infinity Power conversion +{value}",
effect: (level, strength) => Math.pow(level, 0.2) * Math.pow(strength, 0.4) * 0.04,
formatEffect: x => format(x, 2, 2),
combine: GlyphCombiner.add,
}, {
id: "infinityIP",
bitmaskIndex: 14,
isGenerated: true,
glyphTypes: ["infinity"],
singleDesc: () => (GlyphAlteration.isAdded("infinity")
? "Infinity Point gain ×{value} [and ^]{value2}"
: "Multiply Infinity Point gain by {value}"),
totalDesc: () => (GlyphAlteration.isAdded("infinity")
? "Infinity Point gain ×{value} and ^{value2}"
: "Infinity Point gain ×{value}"),
genericDesc: () => (GlyphAlteration.isAdded("infinity")
? "Infinity Point gain multiplier and power"
: "Infinity Point gain multiplier"),
shortDesc: () => (GlyphAlteration.isAdded("infinity")
? "IP ×{value} and ^{value2}"
: "IP ×{value}"),
effect: (level, strength) => Math.pow(level * (strength + 1), 6) * 10000,
formatEffect: x => format(x, 2, 3),
combine: GlyphCombiner.multiply,
// eslint-disable-next-line no-negated-condition
softcap: value => ((Effarig.eternityCap !== undefined) ? Math.min(value, Effarig.eternityCap.toNumber()) : value),
conversion: x => 1 + Math.log10(x) / 1800,
formatSecondaryEffect: x => format(x, 4, 4),
alteredColor: () => GlyphAlteration.getAdditionColor("infinity"),
alterationType: ALTERATION_TYPE.ADDITION
}, {
id: "infinityinfmult",
bitmaskIndex: 15,
isGenerated: true,
glyphTypes: ["infinity"],
singleDesc: "Multiply Infinity gain by {value}",
totalDesc: "Infinity gain ×{value}",
genericDesc: "Infinity gain multiplier",
shortDesc: "Infinities ×{value}",
effect: (level, strength) => (GlyphAlteration.isEmpowered("infinity")
? DC.D1_02.pow(level)
: Decimal.pow(level * strength, 1.5).times(2)),
formatEffect: x => format(x, 2, 1),
combine: GlyphCombiner.multiplyDecimal,
alteredColor: () => GlyphAlteration.getEmpowermentColor("infinity"),
alterationType: ALTERATION_TYPE.EMPOWER
}, {
id: "powerpow",
bitmaskIndex: 16,
isGenerated: true,
glyphTypes: ["power"],
singleDesc: () => (GlyphAlteration.isAdded("power")
? "Antimatter Dimension power +{value}\n[and Antimatter Galaxy cost ×]{value2}"
: "Antimatter Dimension power +{value}"),
totalDesc: () => (GlyphAlteration.isAdded("power")
? "Antimatter Dimension multipliers ^{value} and Antimatter Galaxy cost ×{value2}"
: "Antimatter Dimension multipliers ^{value}"),
genericDesc: () => (GlyphAlteration.isAdded("power")
? "Antimatter Dimensions multipliers ^x and Antimatter Galaxy cost multiplier"
: "Antimatter Dimension multipliers ^x"),
shortDesc: () => (GlyphAlteration.isAdded("power")
? "AD power +{value} and AG cost ×{value2}"
: "AD power +{value}"),
effect: (level, strength) => 1.015 + Math.pow(level, 0.2) * Math.pow(strength, 0.4) / 75,
formatEffect: x => format(x, 3, 3),
formatSingleEffect: x => format(x - 1, 3, 3),
combine: GlyphCombiner.addExponents,
conversion: x => 2 / (x + 1),
formatSecondaryEffect: x => format(x, 3, 3),
alteredColor: () => GlyphAlteration.getAdditionColor("power"),
alterationType: ALTERATION_TYPE.ADDITION
}, {
id: "powermult",
bitmaskIndex: 17,
isGenerated: true,
glyphTypes: ["power"],
singleDesc: "Antimatter Dimension multipliers ×{value}",
shortDesc: "AD ×{value}",
effect: (level, strength) => (GlyphAlteration.isEmpowered("power")
? DC.D11111.pow(level * 220)
: Decimal.pow(level * strength * 10, level * strength * 10)),
formatEffect: x => formatPostBreak(x, 2, 0),
combine: GlyphCombiner.multiplyDecimal,
alteredColor: () => GlyphAlteration.getEmpowermentColor("power"),
alterationType: ALTERATION_TYPE.EMPOWER
}, {
id: "powerdimboost",
bitmaskIndex: 18,
isGenerated: true,
glyphTypes: ["power"],
singleDesc: "Dimension Boost multiplier ×{value}",
genericDesc: "Dimension Boost multiplier",
shortDesc: "Dimboost mult. ×{value}",
effect: (level, strength) => Math.pow(level * strength, 0.5) *
Math.pow(1 + GlyphAlteration.sacrificeBoost("power"), 3),
formatEffect: x => format(x, 2, 2),
combine: GlyphCombiner.multiply,
alteredColor: () => GlyphAlteration.getBoostColor("power"),
alterationType: ALTERATION_TYPE.BOOST
}, {
id: "powerbuy10",
bitmaskIndex: 19,
isGenerated: true,
glyphTypes: ["power"],
singleDesc: () => `Increase the bonus from buying ${formatInt(10)} Antimatter Dimensions by {value}`,
totalDesc: () => `Multiplier from "Buy ${formatInt(10)}" ×{value}`,
genericDesc: () => `"Buy ${formatInt(10)}" bonus increase`,
shortDesc: () => `AD Buy ${formatInt(10)} mult. ×{value}`,
effect: (level, strength) => 1 + level * strength / 12,
formatEffect: x => format(x, 2, 2),
combine: GlyphCombiner.addExponents,
}, {
id: "effarigblackhole",
bitmaskIndex: 20,
isGenerated: true,
glyphTypes: ["effarig"],
singleDesc: "Game speed power +{value}",
totalDesc: "Game speed ^{value}",
genericDesc: "Game speed ^x",
shortDesc: "Game speed power +{value}",
effect: (level, strength) => 1 + Math.pow(level, 0.25) * Math.pow(strength, 0.4) / 75,
formatEffect: x => format(x, 3, 3),
formatSingleEffect: x => format(x - 1, 3, 3),
combine: GlyphCombiner.addExponents,
}, {
id: "effarigrm",
bitmaskIndex: 21,
isGenerated: true,
glyphTypes: ["effarig"],
singleDesc: "Reality Machine multiplier ×{value}",
genericDesc: "Reality Machine multiplier",
shortDesc: "RM ×{value}",
effect: (level, strength) => (GlyphAlteration.isEmpowered("effarig")
? Math.pow(level, 1.5)
: Math.pow(level, 0.6) * strength),
formatEffect: x => format(x, 2, 2),
combine: GlyphCombiner.multiply,
alteredColor: () => GlyphAlteration.getEmpowermentColor("effarig"),
alterationType: ALTERATION_TYPE.EMPOWER
}, {
id: "effarigglyph",
bitmaskIndex: 22,
isGenerated: true,
glyphTypes: ["effarig"],
singleDesc: "Glyph Instability starting level +{value}",
genericDesc: "Glyph Instability delay",
shortDesc: "Instability delay +{value}",
effect: (level, strength) => Math.floor(10 * Math.pow(level * strength, 0.5)),
formatEffect: x => formatInt(x),
combine: GlyphCombiner.add,
}, {
id: "effarigachievement",
bitmaskIndex: 23,
isGenerated: true,
glyphTypes: ["effarig"],
singleDesc: "Achievement multiplier power +{value}",
totalDesc: "Achievement multiplier ^{value}",
genericDesc: "Achievement multiplier ^x",
shortDesc: "Achievement mult. power +{value}",
effect: (level, strength) => 1 + Math.pow(level, 0.4) * Math.pow(strength, 0.6) / 60 +
GlyphAlteration.sacrificeBoost("effarig") / 10,
formatEffect: x => format(x, 3, 3),
formatSingleEffect: x => format(x - 1, 3, 3),
combine: GlyphCombiner.addExponents,
alteredColor: () => GlyphAlteration.getBoostColor("effarig"),
alterationType: ALTERATION_TYPE.BOOST
}, {
id: "effarigforgotten",
bitmaskIndex: 24,
isGenerated: true,
glyphTypes: ["effarig"],
singleDesc: () => (GlyphAlteration.isAdded("effarig")
? `Buy ${formatInt(10)} multiplier ^{value} [and\nDimension Boost multiplier ^]{value2}`
: `Bonus from buying ${formatInt(10)} Dimensions ^{value}`),
totalDesc: () => (GlyphAlteration.isAdded("effarig")
? `Multiplier from "Buy ${formatInt(10)}" ^{value} and Dimension Boost multiplier ^{value2}`
: `Multiplier from "Buy ${formatInt(10)}" ^{value}`),
genericDesc: () => (GlyphAlteration.isAdded("effarig")
? `"Buy ${formatInt(10)}" and Dimension Boost multipliers ^x`
: `"Buy ${formatInt(10)}" multiplier ^x`),
shortDesc: () => (GlyphAlteration.isAdded("effarig")
? `Buy ${formatInt(10)} mult. ^{value}, Dimboost mult. ^{value2}`
: `Buy ${formatInt(10)} mult. ^{value}`),
effect: (level, strength) => 1 + 2 * Math.pow(level, 0.25) * Math.pow(strength, 0.4),
formatEffect: x => format(x, 2, 2),
combine: GlyphCombiner.multiply,
conversion: x => Math.pow(x, 0.4),
formatSecondaryEffect: x => format(x, 2, 2),
alteredColor: () => GlyphAlteration.getAdditionColor("effarig"),
alterationType: ALTERATION_TYPE.ADDITION
}, {
id: "effarigdimensions",
bitmaskIndex: 25,
isGenerated: true,
glyphTypes: ["effarig"],
singleDesc: "All dimension power +{value}",
totalDesc: "All dimension multipliers ^{value}",
genericDesc: "All dimension multipliers ^x",
shortDesc: "All Dimension power +{value}",
effect: (level, strength) => 1 + Math.pow(level, 0.25) * Math.pow(strength, 0.4) / 500,
formatEffect: x => format(x, 3, 3),
formatSingleEffect: x => format(x - 1, 3, 3),
combine: GlyphCombiner.addExponents,
}, {
id: "effarigantimatter",
bitmaskIndex: 26,
isGenerated: true,
glyphTypes: ["effarig"],
singleDesc: () => `Antimatter production: ${formatInt(10)}^x ➜ ${formatInt(10)}^(x^{value})`,
genericDesc: "Antimatter production exponent",
shortDesc: "AM production exponent ^{value}",
effect: (level, strength) => 1 + Math.pow(level, 0.25) * Math.pow(strength, 0.4) / 5000,
formatEffect: x => format(x, 4, 4),
combine: GlyphCombiner.multiply,
}, {
id: "timeshardpow",
bitmaskIndex: 27,
isGenerated: true,
// This gets explicitly added to time glyphs elsewhere (once unlocked)
glyphTypes: [],
singleDesc: "Time Shard power +{value}",
totalDesc: "Time Shard gain ^{value}",
genericDesc: "Time Shards ^x",
shortDesc: "Time Shard power +{value}",
effect: (level, strength) => 1 + (strength / 3.5) * Math.pow(level, 0.35) / 400,
formatEffect: x => format(x, 3, 3),
formatSingleEffect: x => format(x - 1, 3, 3),
combine: GlyphCombiner.addExponents,
}, {
id: "cursedgalaxies",
bitmaskIndex: 0,
isGenerated: false,
glyphTypes: ["cursed"],
singleDesc: `All Galaxies are {value} weaker`,
totalDesc: "All Galaxy strength -{value}",
shortDesc: "Galaxy Strength -{value}",
// Multiplies by 0.768 per glyph
effect: (level, strength) => Math.pow((strength / 3.5) * level, -0.03),
formatEffect: x => formatPercents(1 - x, 2),
combine: GlyphCombiner.multiply,
}, {
id: "curseddimensions",
bitmaskIndex: 1,
isGenerated: false,
glyphTypes: ["cursed"],
singleDesc: "All Dimension multipliers ^{value}",
shortDesc: "All Dimensions ^{value}",
// Multiplies by 0.734 per glyph
effect: (level, strength) => Math.pow((strength / 3.5) * level, -0.035),
formatEffect: x => format(x, 3, 3),
combine: GlyphCombiner.multiply,
}, {
id: "cursedtickspeed",
bitmaskIndex: 2,
isGenerated: false,
glyphTypes: ["cursed"],
singleDesc: "The threshold for Tickspeed Upgrades from Time Dimensions is multiplied by ×{value}",
totalDesc: "The threshold for Tickspeed Upgrades from Time Dimensions is increased by ×{value}",
shortDesc: "TD Tickspeed threshold ×{value}",
// Additive 3.82 per glyph
effect: (level, strength) => Math.log10(level) * (strength / 3.5),
formatEffect: x => format(x, 3, 3),
combine: GlyphCombiner.add,
}, {
id: "cursedEP",
bitmaskIndex: 3,
isGenerated: false,
glyphTypes: ["cursed"],
singleDesc: "Divide Eternity Point gain by {value}",
totalDesc: "Eternity Point gain / {value}",
shortDesc: "EP / {value}",
// Divides e666.6 per glyph
effect: (level, strength) => Decimal.pow10(-level / 10 * (strength / 3.5)),
formatEffect: x => format(x.reciprocal()),
combine: GlyphCombiner.multiplyDecimal,
}, {
id: "realityglyphlevel",
bitmaskIndex: 4,
isGenerated: false,
glyphTypes: ["reality"],
singleDesc: "Increase the effective level of equipped basic Glyphs by {value}",
totalDesc: "Equipped basic Glyph level +{value}",
shortDesc: "Basic Glyph Level +{value}",
effect: level => Math.floor(Math.sqrt(level * 90)),
formatEffect: x => formatInt(x),
combine: GlyphCombiner.add,
}, {
id: "realitygalaxies",
bitmaskIndex: 5,
isGenerated: false,
glyphTypes: ["reality"],
singleDesc: "All Galaxies are {value} stronger",
totalDesc: "All Galaxy strength +{value}",
shortDesc: "Galaxy Strength +{value}",
effect: level => 1 + Math.pow(level / 100000, 0.5),
formatEffect: x => formatPercents(x - 1, 2),
combine: GlyphCombiner.multiply,
}, {
id: "realityrow1pow",
bitmaskIndex: 6,
isGenerated: false,
glyphTypes: ["reality"],
singleDesc: "Multiplier from Reality Upgrade Amplifiers ^{value}",
totalDesc: "Reality Upgrade Amplifier multiplier ^{value}",
shortDesc: "Amplifier Multiplier ^{value}",
effect: level => 1 + level / 125000,
formatEffect: x => format(x, 3, 3),
combine: GlyphCombiner.addExponents,
}, {
id: "realityDTglyph",
bitmaskIndex: 7,
isGenerated: false,
glyphTypes: ["reality"],
singleDesc: () => `Dilated Time scaling for next Glyph level: \n^${format(1.3, 1, 1)}
^(${format(1.3, 1, 1)} + {value})`,
totalDesc: () => `Dilated Time scaling for next Glyph level: ^${format(1.3, 1, 1)}
^(${format(1.3, 1, 1)} + {value})`,
genericDesc: "Dilated Time scaling for Glyph level",
shortDesc: "DT pow. for level +{value}",
// You can only get this effect on level 25000 reality glyphs anyway, might as well make it look nice
effect: () => 0.1,
formatEffect: x => format(x, 2, 2),
combine: GlyphCombiner.add,
}, {
id: "companiondescription",
bitmaskIndex: 8,
isGenerated: false,
glyphTypes: ["companion"],
singleDesc: "It does nothing but sit there and cutely smile at you, whisper into your dreams politely, " +
"and plot the demise of all who stand against you. This one-of-a-kind Glyph will never leave you.",
totalDesc: "+{value} happiness",
shortDesc: "Doesn't want to kill you",
effect: () => (Enslaved.isRunning ? 0 : (0.4 + 0.6 * Math.random())),
formatEffect: x => formatPercents(x, 2, 2),
combine: GlyphCombiner.add,
}, {
id: "companionEP",
bitmaskIndex: 9,
isGenerated: false,
glyphTypes: ["companion"],
singleDesc: "Thanks for your dedication for the game! You reached {value} Eternity Points on your first Reality.",
shortDesc: "It loves you very, very much",
totalDesc: () => (Enslaved.isRunning ? "Help me" : "Yay!"),
// The EP value for this is entirely encoded in rarity, but level needs to be present to
// make sure the proper parameter is being used. The actual glyph level shouldn't do anything.
// eslint-disable-next-line no-unused-vars
effect: (level, strength) => Decimal.pow10(1e6 * strengthToRarity(strength)),
formatEffect: x => formatPostBreak(x, 2),
combine: GlyphCombiner.multiplyDecimal,
}
].mapToObject(effect => effect.id, effect => new GlyphEffectConfig(effect));
export const GlyphEffects = mapGameDataToObject(
GameDatabase.reality.glyphEffects,
config => new GlyphEffectConfig(config)
);
export function findGlyphTypeEffects(glyphType) {
return Object.values(GameDatabase.reality.glyphEffects).filter(e => e.glyphTypes.includes(glyphType));
return GlyphEffects.all.filter(e => e.glyphTypes.includes(glyphType));
}
export function makeGlyphEffectBitmask(effectList) {
// eslint-disable-next-line no-bitwise
return effectList.reduce((mask, eff) => mask + (1 << GameDatabase.reality.glyphEffects[eff].bitmaskIndex), 0);
return effectList.reduce((mask, eff) => mask + (1 << GlyphEffects[eff].bitmaskIndex), 0);
}
export function getGlyphEffectsFromBitmask(bitmask) {
return orderedEffectList
.map(effectName => GameDatabase.reality.glyphEffects[effectName])
// eslint-disable-next-line no-bitwise
.map(effectName => GlyphEffects[effectName])
.filter(effect => (bitmask & (1 << effect.bitmaskIndex)) !== 0);
}
@ -829,142 +226,47 @@ class GlyphType {
* @param {string} setup.id
* @param {string} setup.symbol
* @param {string} setup.color
* @param {function} [setup.primaryEffect] All glyphs generated will have this effect, if specified
* @param {function} [setup.unlockedFn] If this glyph type is not available initially, this specifies
* @param {function(): string} [setup.primaryEffect] All glyphs generated will have this effect, if specified
* @param {function(): boolean} [setup.isUnlocked] If this glyph type is not available initially, this specifies
* how to check to see if it is available
* @param {function(string):boolean} [setup.effectUnlockedFn] If certain effects of this glyph are not
* initially available, this is a function of the effect id that returns whether one is
* @param {number} setup.alchemyResource Alchemy resource generated by sacrificing this glyph
* @param {boolean} setup.hasRarity If the glyph can have rarity or not
*/
constructor(setup) {
/** @member {string} id identifier for this type (time, power, etc)*/
/** @type {string} identifier for this type (time, power, etc)*/
this.id = setup.id;
/** @member {string} symbol used to display glyphs of this type and as a UI shorthand */
/** @type {string} used to display glyphs of this type and as a UI shorthand */
this.symbol = setup.symbol;
/** @member {GlyphEffectConfig[]} effects list of effects that this glyph can have */
/** @type {GlyphEffectConfig[]} list of effects that this glyph can have */
this.effects = findGlyphTypeEffects(setup.id);
/** @member {string} color used for glyph borders and other places where color coding is needed */
/** @type {string} used for glyph borders and other places where color coding is needed */
this.color = setup.color;
/** @member {string?} primaryEffect all glyphs generated will have at least this effect */
/** @type {string?} all glyphs generated will have at least this effect */
this.primaryEffect = setup.primaryEffect;
/** @private @member {function?} unlockedFn */
this.unlockedFn = setup.unlockedFn;
/** @private @member {function(string):boolean?} effectUnlockedFn */
this.effectUnlockedFn = setup.effectUnlockedFn;
/** @type {undefined | function(): boolean} */
this._isUnlocked = setup.isUnlocked;
/** @type {number} */
this.alchemyResource = setup.alchemyResource;
/** @type {boolean} */
this.hasRarity = setup.hasRarity;
if (!GLYPH_TYPES.includes(this.id)) {
throw new Error(`Id ${this.id} not found in GLYPH_TYPES`);
}
}
/** @property {boolean} */
/** @returns {boolean} */
get isUnlocked() {
// eslint-disable-next-line no-negated-condition
return this.unlockedFn !== undefined ? this.unlockedFn() : true;
return this._isUnlocked?.() ?? true;
}
}
/**
* @param {string} id
* @returns {boolean}
*/
isEffectUnlocked(id) {
// eslint-disable-next-line no-negated-condition
return this.effectUnlockedFn !== undefined ? this.effectUnlockedFn(id) : true;
}
/**
* @param {function(): number} rng Random number source (0..1)
* @param {string[]} [blacklist] Do not return the specified effects
* @returns {string | null}
*/
randomEffect(rng, blacklist = []) {
const available = this.effects
.map(e => e.id)
.filter(id => !blacklist.includes(id) && this.isEffectUnlocked(id));
if (available.length === 0) return null;
return available[Math.floor(rng.uniform() * available.length)];
}
}
const allGlyphTypes = mapGameDataToObject(
GameDatabase.reality.glyphTypes,
config => new GlyphType(config)
);
export const GlyphTypes = {
time: new GlyphType({
id: "time",
symbol: GLYPH_SYMBOLS.time,
effects: findGlyphTypeEffects("time"),
color: "#b241e3",
primaryEffect: "timepow",
alchemyResource: ALCHEMY_RESOURCE.TIME,
hasRarity: true
}),
dilation: new GlyphType({
id: "dilation",
symbol: GLYPH_SYMBOLS.dilation,
effects: findGlyphTypeEffects("dilation"),
color: "#64dd17",
alchemyResource: ALCHEMY_RESOURCE.DILATION,
hasRarity: true
}),
replication: new GlyphType({
id: "replication",
symbol: GLYPH_SYMBOLS.replication,
effects: findGlyphTypeEffects("replication"),
color: "#03a9f4",
alchemyResource: ALCHEMY_RESOURCE.REPLICATION,
hasRarity: true
}),
infinity: new GlyphType({
id: "infinity",
symbol: GLYPH_SYMBOLS.infinity,
effects: findGlyphTypeEffects("infinity"),
color: "#b67f33",
primaryEffect: "infinitypow",
alchemyResource: ALCHEMY_RESOURCE.INFINITY,
hasRarity: true
}),
power: new GlyphType({
id: "power",
symbol: GLYPH_SYMBOLS.power,
effects: findGlyphTypeEffects("power"),
color: "#22aa48",
primaryEffect: "powerpow",
alchemyResource: ALCHEMY_RESOURCE.POWER,
hasRarity: true
}),
effarig: new GlyphType({
id: "effarig",
symbol: GLYPH_SYMBOLS.effarig,
effects: findGlyphTypeEffects("effarig"),
color: "#e21717",
unlockedFn: () => EffarigUnlock.reality.isUnlocked,
alchemyResource: ALCHEMY_RESOURCE.EFFARIG,
hasRarity: true
// Effarig glyphs have no primary effect; all are equally likely
}),
reality: new GlyphType({
id: "reality",
symbol: GLYPH_SYMBOLS.reality,
effects: findGlyphTypeEffects("reality"),
color: "#555555",
unlockedFn: () => false,
alchemyResource: ALCHEMY_RESOURCE.REALITY
// Refining a reality glyph is pretty wasteful anyway, but might as well have this here
}),
cursed: new GlyphType({
id: "cursed",
symbol: GLYPH_SYMBOLS.cursed,
effects: findGlyphTypeEffects("cursed"),
color: "black",
unlockedFn: () => false,
}),
companion: new GlyphType({
id: "companion",
symbol: GLYPH_SYMBOLS.companion,
effects: findGlyphTypeEffects("companion"),
color: "#feaec9",
unlockedFn: () => false,
}),
...allGlyphTypes,
/**
* @param {function(): number} rng Random number source (0..1)
* @param {string} [blacklisted] Do not return the specified type

View File

@ -23,8 +23,11 @@ export const AutoGlyphProcessor = {
if (glyph.type === "cursed") return -Infinity;
switch (this.scoreMode) {
case AUTO_GLYPH_SCORE.LOWEST_SACRIFICE:
// Picked glyphs are never kept in this mode
return -player.reality.glyphs.sac[glyph.type];
// Picked glyphs are never kept in this mode. Sacrifice cap needs to be checked since effarig caps
// at a lower value than the others and we don't want to uselessly pick that to sacrifice all the time
return player.reality.glyphs.sac[glyph.type] >= GlyphSacrifice[glyph.type].cap
? -Infinity
: -player.reality.glyphs.sac[glyph.type];
case AUTO_GLYPH_SCORE.EFFECT_COUNT:
// Effect count, plus a very small rarity term to break ties in favor of rarer glyphs
return strengthToRarity(glyph.strength) / 1000 + getGlyphEffectsFromBitmask(glyph.effects, 0, 0)
@ -51,7 +54,7 @@ export const AutoGlyphProcessor = {
const effectList = getGlyphEffectsFromBitmask(glyph.effects, 0, 0)
.filter(effect => effect.isGenerated)
.map(effect => effect.id);
// This ternary check is required to filter out the additional effects given by Ra-Enslaved 25, which don't
// This ternary check is required to filter out the additional effects given by Ra-Nameless 25, which don't
// exist in the glyph filter settings. It can be safely ignored since the effect is always given.
const effectScore = effectList.map(e => (typeCfg.effectScores[e] ? typeCfg.effectScores[e] : 0)).sum();
return strengthToRarity(glyph.strength) + effectScore;
@ -61,7 +64,8 @@ export const AutoGlyphProcessor = {
// to make them picked last, because we can't refine them.
case AUTO_GLYPH_SCORE.LOWEST_ALCHEMY: {
const resource = AlchemyResource[glyph.type];
return resource.isUnlocked && !resource.capped
const refinementGain = GlyphSacrificeHandler.glyphRefinementGain(glyph);
return resource.isUnlocked && refinementGain > 0
? -resource.amount
: Number.NEGATIVE_INFINITY;
}
@ -123,6 +127,12 @@ export const AutoGlyphProcessor = {
default:
throw new Error("Unknown auto Glyph Sacrifice mode");
}
},
// Generally only used for UI in order to notify the player that they might end up retroactively getting rid of
// some glyphs they otherwise want to keep
hasNegativeEffectScore() {
return this.scoreMode === AUTO_GLYPH_SCORE.EFFECT_SCORE &&
Object.values(this.types).map(t => Object.values(t.effectScores)).flat().some(v => v < 0);
}
};
@ -130,10 +140,27 @@ export function autoAdjustGlyphWeights() {
const sources = getGlyphLevelSources();
const f = x => Math.pow(Math.clampMin(1, Math.log(5 * x)), 3 / 2);
const totalWeight = Object.values(sources).map(s => f(s.value)).sum();
player.celestials.effarig.glyphWeights.ep = 100 * f(sources.ep.value) / totalWeight;
player.celestials.effarig.glyphWeights.repl = 100 * f(sources.repl.value) / totalWeight;
player.celestials.effarig.glyphWeights.dt = 100 * f(sources.dt.value) / totalWeight;
player.celestials.effarig.glyphWeights.eternities = 100 * f(sources.eternities.value) / totalWeight;
const scaledWeight = key => 100 * f(sources[key].value) / totalWeight;
// Adjust all weights to be integer, while maintaining that they must sum to 100. We ensure it's within 1 on the
// weights by flooring and then taking guesses on which ones would give the largest boost when adding the lost
// amounts. This isn't necessarily the best integer weighting, but gives a result that's quite literally within
// 99.97% of the non-integer optimal settings and prevents the total from exceeding 100.
const weightKeys = ["ep", "repl", "dt", "eternities"];
const weights = [];
for (const key of weightKeys) {
weights.push({
key,
percent: scaledWeight(key)
});
}
const fracPart = x => x - Math.floor(x);
const priority = weights.sort((a, b) => fracPart(b.percent) - fracPart(a.percent)).map(w => w.key);
const missingPercent = 100 - weights.map(w => Math.floor(w.percent)).reduce((a, b) => a + b);
for (let i = 0; i < weightKeys.length; i++) {
const key = priority[i];
player.celestials.effarig.glyphWeights[key] = Math.floor(scaledWeight(key)) + (i < missingPercent ? 1 : 0);
}
}
function getGlyphLevelSources() {
@ -183,6 +210,7 @@ function getGlyphLevelSources() {
export function getGlyphLevelInputs() {
const sources = getGlyphLevelSources();
const staticFactors = GameCache.staticGlyphWeights.value;
// If the nomial blend of inputs is a * b * c * d, then the contribution can be tuend by
// changing the exponents on the terms: aⁿ¹ * bⁿ² * cⁿ³ * dⁿ⁴
// If n1..n4 just add up to 4, then the optimal strategy is to just max out the one over the
@ -221,55 +249,65 @@ export function getGlyphLevelInputs() {
adjustFactor(sources.repl, weights.repl / 100);
adjustFactor(sources.dt, weights.dt / 100);
adjustFactor(sources.eternities, weights.eternities / 100);
const perkShopEffect = Effects.max(1, PerkShopUpgrade.glyphLevel);
const shardFactor = Ra.has(RA_UNLOCKS.SHARD_LEVEL_BOOST) ? RA_UNLOCKS.SHARD_LEVEL_BOOST.effect() : 0;
const shardFactor = Ra.unlocks.relicShardGlyphLevelBoost.effectOrDefault(0);
let baseLevel = sources.ep.value * sources.repl.value * sources.dt.value * sources.eternities.value *
perkShopEffect + shardFactor;
staticFactors.perkShop + shardFactor;
const singularityEffect = SingularityMilestone.glyphLevelFromSingularities.isUnlocked
? SingularityMilestone.glyphLevelFromSingularities.effectValue
: 1;
const singularityEffect = SingularityMilestone.glyphLevelFromSingularities.effectOrDefault(1);
baseLevel *= singularityEffect;
let scaledLevel = baseLevel;
// With begin = 1000 and rate = 250, a base level of 2000 turns into 1500; 4000 into 2000
const instabilityScaleBegin = Glyphs.instabilityThreshold;
const instabilityScaleRate = 500;
if (scaledLevel > instabilityScaleBegin) {
const excess = (scaledLevel - instabilityScaleBegin) / instabilityScaleRate;
scaledLevel = instabilityScaleBegin + 0.5 * instabilityScaleRate * (Math.sqrt(1 + 4 * excess) - 1);
}
const hyperInstabilityScaleBegin = Glyphs.hyperInstabilityThreshold;
const hyperInstabilityScaleRate = 400;
if (scaledLevel > hyperInstabilityScaleBegin) {
const excess = (scaledLevel - hyperInstabilityScaleBegin) / hyperInstabilityScaleRate;
scaledLevel = hyperInstabilityScaleBegin + 0.5 * hyperInstabilityScaleRate * (Math.sqrt(1 + 4 * excess) - 1);
}
// The softcap starts at begin and rate determines how quickly level scales after the cap, turning a linear pre-cap
// increase to a quadratic post-cap increase with twice the scaling. For example, with begin = 1000 and rate = 400:
// - Scaled level 1400 requires +800 more base levels from the start of the cap (ie. level 1800)
// - Scaled level 1800 requires +1600 more base levels from scaled 1400 (ie. level 3400)
// - Each additional 400 scaled requires another +800 on top of the already-existing gap for base
// This is applied twice in a stacking way, using regular instability first and then again with hyperinstability
// if the newly reduced level is still above the second threshold
const instabilitySoftcap = (level, begin, rate) => {
if (level < begin) return level;
const excess = (level - begin) / rate;
return begin + 0.5 * rate * (Math.sqrt(1 + 4 * excess) - 1);
};
scaledLevel = instabilitySoftcap(scaledLevel, staticFactors.instability, 500);
scaledLevel = instabilitySoftcap(scaledLevel, staticFactors.hyperInstability, 400);
const scalePenalty = scaledLevel > 0 ? baseLevel / scaledLevel : 1;
const rowFactor = [Array.range(1, 5).every(x => RealityUpgrade(x).boughtAmount > 0)]
.concat(Array.range(1, 4).map(x => Array.range(1, 5).every(y => RealityUpgrade(5 * x + y).isBought)))
.filter(x => x)
.length;
const achievementFactor = Effects.sum(Achievement(148), Achievement(166));
baseLevel += rowFactor + achievementFactor;
scaledLevel += rowFactor + achievementFactor;
// Temporary runaway prevention (?)
const levelHardcap = 1000000;
const levelCapped = scaledLevel > levelHardcap;
scaledLevel = Math.min(scaledLevel, levelHardcap);
const incAfterInstability = staticFactors.realityUpgrades + staticFactors.achievements;
baseLevel += incAfterInstability;
scaledLevel += incAfterInstability;
return {
ep: sources.ep,
repl: sources.repl,
dt: sources.dt,
eter: sources.eternities,
perkShop: perkShopEffect,
perkShop: staticFactors.perkShop,
scalePenalty,
rowFactor,
achievementFactor,
rowFactor: staticFactors.realityUpgrades,
achievementFactor: staticFactors.achievements,
shardFactor,
singularityEffect,
rawLevel: baseLevel,
actualLevel: Math.max(1, scaledLevel),
capped: levelCapped
};
}
// Calculates glyph weights which don't change over the course of a reality unless particular events occur; this is
// stored in the GameCache and only invalidated as needed
export function staticGlyphWeights() {
const perkShop = Effects.max(1, PerkShopUpgrade.glyphLevel);
const instability = Glyphs.instabilityThreshold;
const hyperInstability = Glyphs.hyperInstabilityThreshold;
const realityUpgrades = [Array.range(1, 5).every(x => RealityUpgrade(x).boughtAmount > 0)]
.concat(Array.range(1, 4).map(x => Array.range(1, 5).every(y => RealityUpgrade(5 * x + y).isBought)))
.filter(x => x)
.length;
const achievements = Effects.sum(Achievement(148), Achievement(166));
return {
perkShop,
instability,
hyperInstability,
realityUpgrades,
achievements
};
}

View File

@ -1,10 +1,11 @@
import { GameMechanicState } from "../game-mechanics/index.js";
import { GameMechanicState } from "../game-mechanics/index";
export const orderedEffectList = ["powerpow", "infinitypow", "replicationpow", "timepow",
"dilationpow", "timeshardpow", "powermult", "powerdimboost", "powerbuy10",
"dilationTTgen", "infinityinfmult", "infinityIP", "timeEP",
"dilationDT", "replicationdtgain", "replicationspeed", "timespeed",
"dilationDT", "replicationdtgain", "replicationspeed",
"timeetermult", "dilationgalaxyThreshold", "infinityrate", "replicationglyphlevel",
"timespeed",
"effarigblackhole", "effarigrm", "effarigglyph", "effarigachievement",
"effarigforgotten", "effarigdimensions", "effarigantimatter",
"cursedgalaxies", "cursedtickspeed", "curseddimensions", "cursedEP",
@ -51,13 +52,15 @@ export const Glyphs = {
const isUsableIndex = index => (useProtectedSlots ? index < this.protectedSlots : index >= this.protectedSlots);
return this.inventory.findIndex((slot, index) => slot === null && isUsableIndex(index));
},
// This is stored in GameCache and only invalidated if glyphs change; we check for free inventory space often in
// lots of places and this is an expensive operation
get freeInventorySpace() {
this.validate();
return this.inventory.filter((e, idx) => e === null && idx >= this.protectedSlots).length;
},
get activeSlotCount() {
if (Pelle.isDoomed) {
if (PelleRifts.famine.milestones[0].canBeApplied) return 1;
if (PelleRifts.vacuum.milestones[0].canBeApplied) return 1;
return 0;
}
return 3 + Effects.sum(RealityUpgrade(9), RealityUpgrade(24));
@ -115,6 +118,7 @@ export const Glyphs = {
player.reality.glyphs.protectedRows = newRows;
this.validate();
GameCache.glyphInventorySpace.invalidate();
},
// Move all glyphs from the origin row to the destination row, does nothing if a column-preserving move operation
// isn't possible. Returns a boolean indicating success/failure on glyph moving. Row is 0-indexed
@ -183,15 +187,38 @@ export const Glyphs = {
this.validate();
EventHub.dispatch(GAME_EVENT.GLYPHS_CHANGED);
},
findByValues(finding, ignore = { level, strength, effects }) {
for (const glyph of this.sortedInventoryList) {
const type = glyph.type === finding.type;
const effects = glyph.effects === finding.effects ||
(ignore.effects && hasAtLeastGlyphEffects(glyph.effects, finding.effects));
const str = ignore.strength || glyph.strength === finding.strength;
const lvl = ignore.level || glyph.level === finding.level;
const sym = Boolean(glyph.symbol) || glyph.symbol === finding.symbol;
if (type && effects && str && lvl && sym) return glyph;
findByValues(targetGlyph, searchList, fuzzyMatch = { level, strength, effects }) {
// We need comparison to go both ways for normal matching and subset matching for partially-equipped sets
const compFn = (op, comp1, comp2) => {
switch (op) {
case -1:
return comp1 <= comp2;
case 0:
return comp1 === comp2;
case 1:
return comp1 >= comp2;
}
return false;
};
for (const glyph of searchList) {
const type = glyph.type === targetGlyph.type;
let eff = false;
switch (fuzzyMatch.effects) {
case -1:
eff = hasAtLeastGlyphEffects(targetGlyph.effects, glyph.effects);
break;
case 0:
eff = glyph.effects === targetGlyph.effects;
break;
case 1:
eff = hasAtLeastGlyphEffects(glyph.effects, targetGlyph.effects);
break;
}
const str = compFn(fuzzyMatch.strength, glyph.strength, targetGlyph.strength);
const lvl = compFn(fuzzyMatch.level, glyph.level, targetGlyph.level);
const sym = glyph.symbol === targetGlyph.symbol;
if (type && eff && str && lvl && sym) return glyph;
}
return undefined;
},
@ -216,6 +243,8 @@ export const Glyphs = {
)
) return;
if (GameEnd.creditsEverClosed) return;
this.validate();
if (this.findByInventoryIndex(glyph.idx) !== glyph) {
throw new Error("Inconsistent inventory indexing");
@ -226,7 +255,8 @@ export const Glyphs = {
}
if (this.active[targetSlot] === null) {
if (sameSpecialTypeIndex >= 0) {
Modal.message.show(`You may only have one ${glyph.type.capitalize()} Glyph equipped`);
Modal.message.show(`You may only have one ${glyph.type.capitalize()} Glyph equipped!`,
{ closeEvent: GAME_EVENT.GLYPHS_CHANGED });
return;
}
this.removeFromInventory(glyph);
@ -242,7 +272,8 @@ export const Glyphs = {
} else {
// We can only replace effarig/reality glyph
if (sameSpecialTypeIndex >= 0 && sameSpecialTypeIndex !== targetSlot) {
Modal.message.show(`You may only have one ${glyph.type.capitalize()} Glyph equipped`);
Modal.message.show(`You may only have one ${glyph.type.capitalize()} Glyph equipped!`,
{ closeEvent: GAME_EVENT.GLYPHS_CHANGED });
return;
}
if (!player.options.confirmations.glyphReplace) {
@ -254,9 +285,10 @@ export const Glyphs = {
// Loading glyph sets might choose NEW! glyphs, in which case the hover-over flag clearing never got triggered
this.removeNewFlag(glyph);
},
unequipAll() {
unequipAll(forceToUnprotected = false) {
const targetRegion = forceToUnprotected ? false : player.options.respecIntoProtected;
while (player.reality.glyphs.active.length) {
const freeIndex = this.findFreeIndex(player.options.respecIntoProtected);
const freeIndex = this.findFreeIndex(targetRegion);
if (freeIndex < 0) break;
const glyph = player.reality.glyphs.active.pop();
this.active[glyph.idx] = null;
@ -323,7 +355,7 @@ export const Glyphs = {
},
addToInventory(glyph, requestedInventoryIndex, isExistingGlyph = false) {
this.validate();
glyph.id = GlyphGenerator.makeID();
if (!isExistingGlyph) glyph.id = GlyphGenerator.makeID();
const isProtectedIndex = requestedInventoryIndex < this.protectedSlots;
let index = this.findFreeIndex(isProtectedIndex);
if (index < 0) return;
@ -380,7 +412,7 @@ export const Glyphs = {
},
sort(sortFunction) {
const glyphsToSort = player.reality.glyphs.inventory.filter(g => g.idx >= this.protectedSlots);
const freeSpace = this.freeInventorySpace;
const freeSpace = GameCache.glyphInventorySpace.value;
const sortOrder = ["power", "infinity", "replication", "time", "dilation", "effarig",
"reality", "cursed", "companion"];
const byType = sortOrder.mapToObject(g => g, () => ({ glyphs: [], padding: 0 }));
@ -421,7 +453,6 @@ export const Glyphs = {
},
sortByEffect() {
function reverseBitstring(eff) {
// eslint-disable-next-line no-bitwise
return parseInt(((1 << 30) + (eff >>> 0)).toString(2).split("").reverse().join(""), 2);
}
// The bitwise reversal is so that the effects with the LOWER id are valued higher in the sorting.
@ -431,6 +462,7 @@ export const Glyphs = {
// If there are enough glyphs that are better than the specified glyph, in every way, then
// the glyph is objectively a useless piece of garbage.
isObjectivelyUseless(glyph, threshold) {
if (player.reality.applyFilterToPurge && AutoGlyphProcessor.wouldKeep(glyph)) return false;
function hasSomeBetterEffects(glyphA, glyphB, comparedEffects) {
for (const effect of comparedEffects) {
const c = effect.compareValues(
@ -446,7 +478,6 @@ export const Glyphs = {
g.type === glyph.type &&
g.id !== glyph.id &&
(g.level >= glyph.level || g.strength >= glyph.strength) &&
// eslint-disable-next-line no-bitwise
((g.effects & glyph.effects) === glyph.effects));
let compareThreshold = glyph.type === "effarig" || glyph.type === "reality" ? 1 : 5;
compareThreshold = Math.clampMax(compareThreshold, threshold);
@ -455,6 +486,7 @@ export const Glyphs = {
const betterCount = toCompare.countWhere(other => !hasSomeBetterEffects(glyph, other, comparedEffects));
return betterCount >= compareThreshold;
},
// Note that this same function is called with different parameters for purge (5), harsh purge (1), and sac all (0)
autoClean(threshold = 5, deleteGlyphs = true) {
const isHarsh = threshold < 5;
let toBeDeleted = 0;
@ -463,7 +495,8 @@ export const Glyphs = {
// We look in backwards order so that later glyphs get cleaned up first
for (let inventoryIndex = this.totalSlots - 1; inventoryIndex >= this.protectedSlots; --inventoryIndex) {
const glyph = this.inventory[inventoryIndex];
if (glyph === null || glyph.type === "companion") continue;
// Never clean companion, and only clean cursed if we choose to sacrifice all
if (glyph === null || glyph.type === "companion" || (glyph.type === "cursed" && threshold !== 0)) continue;
// Don't auto-clean custom glyphs (eg. music glyphs) unless it's harsh or delete all
const isCustomGlyph = glyph.color !== undefined || glyph.symbol !== undefined;
if (isCustomGlyph && !isHarsh) continue;
@ -503,7 +536,7 @@ export const Glyphs = {
}
},
processSortingAfterReality() {
if (V.has(V_UNLOCKS.AUTO_AUTOCLEAN) && player.reality.autoAutoClean) this.autoClean();
if (VUnlocks.autoAutoClean.canBeApplied && player.reality.autoAutoClean) this.autoClean();
switch (player.reality.autoSort) {
case AUTO_SORT_MODE.NONE:
break;
@ -609,13 +642,11 @@ export const Glyphs = {
},
// Modifies a basic glyph to have timespeed, and adds the new effect to time glyphs
applyGamespeed(glyph) {
if (!Ra.has(RA_UNLOCKS.ALWAYS_GAMESPEED)) return;
if (!Ra.unlocks.allGamespeedGlyphs.canBeApplied) return;
if (BASIC_GLYPH_TYPES.includes(glyph.type)) {
// eslint-disable-next-line no-bitwise
glyph.effects |= (1 << GameDatabase.reality.glyphEffects.timespeed.bitmaskIndex);
glyph.effects |= (1 << GlyphEffects.timespeed.bitmaskIndex);
if (glyph.type === "time") {
// eslint-disable-next-line no-bitwise
glyph.effects |= (1 << GameDatabase.reality.glyphEffects.timeshardpow.bitmaskIndex);
glyph.effects |= (1 << GlyphEffects.timeshardpow.bitmaskIndex);
}
}
},
@ -635,23 +666,28 @@ export const Glyphs = {
EventHub.dispatch(GAME_EVENT.GLYPHS_EQUIPPED_CHANGED);
EventHub.dispatch(GAME_EVENT.GLYPHS_CHANGED);
this.validate();
},
// Mostly used for key-swapping glyph set UI elements; composites the entire glyph set together in a way which is
// relatively unlikely to cause collisions between different glyph sets unless they're actually the same glyphs.
// Different permutations of the same glyphs should produce the same hash, but aren't guaranteed to
hash(glyphSet) {
let hash = 1;
for (const glyph of glyphSet) {
// This should be at most around e23 or so in practice
const singleGlyphHash = Math.pow(glyph.level, 2) * Math.pow(glyph.strength, 4) * glyph.effects *
glyph.type.charCodeAt(0);
hash *= singleGlyphHash;
}
return hash;
}
};
class GlyphSacrificeState extends GameMechanicState { }
export const GlyphSacrifice = (function() {
const db = GameDatabase.reality.glyphSacrifice;
return {
time: new GlyphSacrificeState(db.time),
dilation: new GlyphSacrificeState(db.dilation),
replication: new GlyphSacrificeState(db.replication),
infinity: new GlyphSacrificeState(db.infinity),
power: new GlyphSacrificeState(db.power),
effarig: new GlyphSacrificeState(db.effarig),
reality: new GlyphSacrificeState(db.reality),
};
}());
export const GlyphSacrifice = mapGameDataToObject(
GameDatabase.reality.glyphSacrifice,
config => new GlyphSacrificeState(config)
);
export function recalculateAllGlyphs() {
for (let i = 0; i < player.reality.glyphs.active.length; i++) {
@ -687,18 +723,25 @@ export function getRarity(x) {
return GlyphRarities.find(e => x >= e.minStrength);
}
export function getAdjustedGlyphLevel(glyph, realityGlyphBoost = Glyphs.levelBoost) {
export function getColor(strength) {
return getRarity(strength)[(player.options.forceDarkGlyphs || Theme.current().isDark()) ? "darkColor" : "lightColor"];
}
export function getAdjustedGlyphLevel(glyph, realityGlyphBoost = Glyphs.levelBoost, ignoreCelestialEffects = false) {
const level = glyph.level;
if (!ignoreCelestialEffects) {
if (Pelle.isDoomed) return Math.min(level, Pelle.glyphMaxLevel);
if (Enslaved.isRunning) return Math.max(level, Enslaved.glyphLevelMin);
if (Effarig.isRunning) return Math.min(level, Effarig.glyphLevelCap);
}
if (BASIC_GLYPH_TYPES.includes(glyph.type)) return level + realityGlyphBoost;
return level;
}
export function respecGlyphs() {
if (!Glyphs.unequipAll()) {
Modal.message.show("Some of your Glyphs could not be unequipped due to lack of inventory space.");
Modal.message.show("Some of your Glyphs could not be unequipped due to lack of inventory space.",
{ closeEvent: GAME_EVENT.GLYPHS_CHANGED });
}
player.reality.respec = false;
}

View File

@ -53,7 +53,7 @@ export function getAdjustedGlyphEffect(effectKey) {
* @return {number | Decimal}
*/
export function getSecondaryGlyphEffect(effectKey) {
return GameDatabase.reality.glyphEffects[effectKey].conversion(getAdjustedGlyphEffect(effectKey));
return GlyphEffects[effectKey].conversion(getAdjustedGlyphEffect(effectKey));
}
/**
@ -66,15 +66,14 @@ export function getGlyphEffectValues(effectKey) {
throw new Error(`Unknown Glyph effect requested "${effectKey}"'`);
}
return player.reality.glyphs.active
// eslint-disable-next-line no-bitwise
.filter(glyph => ((1 << GameDatabase.reality.glyphEffects[effectKey].bitmaskIndex) & glyph.effects) !== 0)
.filter(glyph => generatedTypes.includes(glyph.type) === GameDatabase.reality.glyphEffects[effectKey].isGenerated)
.filter(glyph => ((1 << GlyphEffects[effectKey].bitmaskIndex) & glyph.effects) !== 0)
.filter(glyph => generatedTypes.includes(glyph.type) === GlyphEffects[effectKey].isGenerated)
.map(glyph => getSingleGlyphEffectFromBitmask(effectKey, glyph));
}
// Combines all specified glyph effects, reduces some boilerplate
function getTotalEffect(effectKey) {
return GameDatabase.reality.glyphEffects[effectKey].combine(getGlyphEffectValues(effectKey));
return GlyphEffects[effectKey].combine(getGlyphEffectValues(effectKey));
}
/**
@ -109,8 +108,7 @@ export function getGlyphEffectValuesFromBitmask(bitmask, level, baseStrength, ty
// Pulls out a single effect value from a glyph's bitmask, returning just the value (nothing for missing effects)
export function getSingleGlyphEffectFromBitmask(effectName, glyph) {
const glyphEffect = GameDatabase.reality.glyphEffects[effectName];
// eslint-disable-next-line no-bitwise
const glyphEffect = GlyphEffects[effectName];
if ((glyph.effects & (1 << glyphEffect.bitmaskIndex)) === 0) {
return undefined;
}
@ -122,9 +120,7 @@ export function countValuesFromBitmask(bitmask) {
let numEffects = 0;
let bits = bitmask;
while (bits !== 0) {
// eslint-disable-next-line no-bitwise
numEffects += bits & 1;
// eslint-disable-next-line no-bitwise
bits >>= 1;
}
return numEffects;
@ -137,7 +133,7 @@ export function getActiveGlyphEffects() {
.filter(ev => ev.values.length > 0)
.map(ev => ({
id: ev.effect,
value: GameDatabase.reality.glyphEffects[ev.effect].combine(ev.values),
value: GlyphEffects[ev.effect].combine(ev.values),
}));
const effectNames = effectValues.map(e => e.id);

View File

@ -151,10 +151,9 @@ export const GlyphGenerator = {
// These Glyphs are given on entering Doomed to prevent the player
// from having none of each basic glyphs which are requied to beat pelle
doomedGlyph(type) {
const effectList = Object.values(GameDatabase.reality.glyphEffects).filter(e => e.id.startsWith(type));
effectList.push(GameDatabase.reality.glyphEffects.timespeed);
const effectList = GlyphEffects.all.filter(e => e.id.startsWith(type));
effectList.push(GlyphEffects.timespeed);
let bitmask = 0;
// eslint-disable-next-line no-bitwise
for (const effect of effectList) bitmask |= 1 << effect.bitmaskIndex;
const glyphLevel = Math.max(player.records.bestReality.glyphLevel, 5000);
return {
@ -213,9 +212,9 @@ export const GlyphGenerator = {
randomStrength(rng) {
// Technically getting this upgrade really changes glyph gen but at this point almost all
// the RNG is gone anyway.
if (Ra.has(RA_UNLOCKS.MAX_RARITY_AND_SHARD_SACRIFICE_BOOST)) return rarityToStrength(100);
if (Ra.unlocks.maxGlyphRarityAndShardSacrificeBoost.canBeApplied) return rarityToStrength(100);
let result = GlyphGenerator.gaussianBellCurve(rng) * GlyphGenerator.strengthMultiplier;
const relicShardFactor = Ra.has(RA_UNLOCKS.EXTRA_CHOICES_AND_RELIC_SHARD_RARITY_ALWAYS_MAX) ? 1 : rng.uniform();
const relicShardFactor = Ra.unlocks.extraGlyphChoicesAndRelicShardRarityAlwaysMax.canBeApplied ? 1 : rng.uniform();
const increasedRarity = relicShardFactor * Effarig.maxRarityBoost +
Effects.sum(Achievement(146), GlyphSacrifice.effarig);
// Each rarity% is 0.025 strength.
@ -232,25 +231,25 @@ export const GlyphGenerator = {
// as preventing all of the glyphs changing drastically when RU17 is purchased.
const random1 = rng.uniform();
const random2 = rng.uniform();
if (type !== "effarig" && Ra.has(RA_UNLOCKS.GLYPH_EFFECT_COUNT)) return 4;
const maxEffects = Ra.has(RA_UNLOCKS.GLYPH_EFFECT_COUNT) ? 7 : 4;
if (type !== "effarig" && Ra.unlocks.glyphEffectCount.canBeApplied) return 4;
const maxEffects = Ra.unlocks.glyphEffectCount.canBeApplied ? 7 : 4;
let num = Math.min(
maxEffects,
Math.floor(Math.pow(random1, 1 - (Math.pow(level * strength, 0.5)) / 100) * 1.5 + 1));
Math.floor(Math.pow(random1, 1 - (Math.pow(level * strength, 0.5)) / 100) * 1.5 + 1)
);
// If we do decide to add anything else that boosts chance of an extra effect, keeping the code like this
// makes it easier to do (add it to the Effects.max).
if (RealityUpgrade(17).isBought && random2 < Effects.max(0, RealityUpgrade(17))) {
num = Math.min(num + 1, maxEffects);
}
if (Ra.has(RA_UNLOCKS.GLYPH_EFFECT_COUNT)) num = Math.max(num, 4);
return num;
return Ra.unlocks.glyphEffectCount.canBeApplied ? Math.max(num, 4) : num;
},
// Populate a list of reality glyph effects based on level
generateRealityEffects(level) {
const numberOfEffects = realityGlyphEffectLevelThresholds.filter(lv => lv <= level).length;
const sortedRealityEffects = Object.values(GameDatabase.reality.glyphEffects)
.filter(eff => eff.id.match("reality*"))
const sortedRealityEffects = GlyphEffects.all
.filter(eff => eff.glyphTypes.includes("reality"))
.sort((a, b) => a.bitmaskIndex - b.bitmaskIndex)
.map(eff => eff.id);
return sortedRealityEffects.slice(0, numberOfEffects);

View File

@ -2,11 +2,21 @@
export const GlyphSacrificeHandler = {
// Anything scaling on sacrifice caps at this value, even though the actual sacrifice values can go higher
maxSacrificeForEffects: 1e100,
// This is used for glyph UI-related things in a few places, but is handled here as a getter which is only called
// sparingly - that is, whenever the cache is invalidated after a glyph is sacrificed. Thus it only gets recalculated
// when glyphs are actually sacrificed, rather than every render cycle.
get logTotalSacrifice() {
// We check elsewhere for this equalling zero to determine if the player has ever sacrificed. Technically this
// should check for -Infinity, but the clampMin works in practice because the minimum possible sacrifice
// value is greater than 1 for even the weakest possible glyph
return BASIC_GLYPH_TYPES.reduce(
(tot, type) => tot + Math.log10(Math.clampMin(player.reality.glyphs.sac[type], 1)), 0);
},
get canSacrifice() {
return RealityUpgrade(19).isBought;
},
get isRefining() {
return Ra.has(RA_UNLOCKS.GLYPH_ALCHEMY) && AutoGlyphProcessor.sacMode !== AUTO_GLYPH_REJECT.SACRIFICE;
return Ra.unlocks.unlockGlyphAlchemy.canBeApplied && AutoGlyphProcessor.sacMode !== AUTO_GLYPH_REJECT.SACRIFICE;
},
handleSpecialGlyphTypes(glyph) {
switch (glyph.type) {
@ -35,7 +45,7 @@ export const GlyphSacrificeHandler = {
if (glyph.type === "reality") return 0.01 * glyph.level * Achievement(171).effectOrDefault(1);
const pre10kFactor = Math.pow(Math.clampMax(glyph.level, 10000) + 10, 2.5);
const post10kFactor = 1 + Math.clampMin(glyph.level - 10000, 0) / 100;
const power = Ra.has(RA_UNLOCKS.MAX_RARITY_AND_SHARD_SACRIFICE_BOOST) ? 1 + Effarig.maxRarityBoost / 100 : 1;
const power = Ra.unlocks.maxGlyphRarityAndShardSacrificeBoost.effectOrDefault(1);
return Math.pow(pre10kFactor * post10kFactor * glyph.strength *
Teresa.runRewardMultiplier * Achievement(171).effectOrDefault(1), power);
},
@ -50,6 +60,7 @@ export const GlyphSacrificeHandler = {
return;
}
player.reality.glyphs.sac[glyph.type] += toGain;
GameCache.logTotalGlyphSacrifice.invalidate();
Glyphs.removeFromInventory(glyph);
EventHub.dispatch(GAME_EVENT.GLYPH_SACRIFICED, glyph);
},
@ -64,13 +75,13 @@ export const GlyphSacrificeHandler = {
// Refined glyphs give this proportion of their maximum attainable value from their level
glyphRefinementEfficiency: 0.05,
glyphRawRefinementGain(glyph) {
if (!Ra.has(RA_UNLOCKS.GLYPH_ALCHEMY)) return 0;
if (!Ra.unlocks.unlockGlyphAlchemy.canBeApplied) return 0;
const glyphMaxValue = this.levelRefinementValue(glyph.level);
const rarityModifier = strengthToRarity(glyph.strength) / 100;
return this.glyphRefinementEfficiency * glyphMaxValue * rarityModifier;
},
glyphRefinementGain(glyph) {
if (!Ra.has(RA_UNLOCKS.GLYPH_ALCHEMY) || !generatedTypes.includes(glyph.type)) return 0;
if (!Ra.unlocks.unlockGlyphAlchemy.canBeApplied || !generatedTypes.includes(glyph.type)) return 0;
const resource = this.glyphAlchemyResource(glyph);
const glyphActualValue = this.glyphRawRefinementGain(glyph);
if (resource.cap === 0) return glyphActualValue;
@ -96,7 +107,7 @@ export const GlyphSacrificeHandler = {
return;
}
const decoherence = AlchemyResource.decoherence.isUnlocked;
if (!Ra.has(RA_UNLOCKS.GLYPH_ALCHEMY) ||
if (!Ra.unlocks.unlockGlyphAlchemy.canBeApplied ||
(this.glyphRefinementGain(glyph) === 0 && !decoherence) ||
(decoherence && AlchemyResources.base.every(x => x.data.amount >= Ra.alchemyResourceCap))) {
this.sacrificeGlyph(glyph, force);

View File

@ -1,6 +1,7 @@
import { GameKeyboard } from "./keyboard.js";
import Mousetrap from "mousetrap";
import { GameKeyboard } from "./keyboard";
// Add your hotkeys and combinations here
// GameKeyboard.bind for single press combinations
// GameKeyboard.bindRepeatable for repeatable combinations
@ -52,37 +53,40 @@ export const shortcuts = [
name: "Dimension Boost",
keys: ["d"],
type: "bindRepeatableHotkey",
function: () => requestDimensionBoost(true),
function: () => manualRequestDimensionBoost(true),
visible: true
}, {
name: "Single Dimension Boost",
keys: ["shift", "d"],
type: "bindRepeatableHotkey",
function: () => requestDimensionBoost(false),
function: () => manualRequestDimensionBoost(false),
visible: false
}, {
name: "Antimatter Galaxy",
keys: ["g"],
type: "bindRepeatableHotkey",
function: () => requestGalaxyReset(true),
function: () => manualRequestGalaxyReset(true),
visible: true
}, {
name: "Single Antimatter Galaxy",
keys: ["shift", "g"],
type: "bindRepeatableHotkey",
function: () => requestGalaxyReset(false),
function: () => manualRequestGalaxyReset(false),
visible: false
}, {
name: "Big Crunch",
keys: ["c"],
type: "bindRepeatableHotkey",
function: () => bigCrunchResetRequest(),
function: () => manualBigCrunchResetRequest(),
visible: true
}, {
name: "Replicanti Galaxy",
keys: ["r"],
type: "bindRepeatableHotkey",
function: () => replicantiGalaxyRequest(),
type: "bindHotkey",
function: () => {
replicantiGalaxyRequest();
setHoldingR(true);
},
visible: () => Replicanti.areUnlocked || PlayerProgress.eternityUnlocked()
}, {
name: "Eternity",
@ -143,7 +147,6 @@ export const shortcuts = [
keys: ["mod", "s"],
type: "bind",
function: () => {
if (Pelle.endState >= 4.5) return false;
GameStorage.save(false, true);
return false;
},
@ -215,25 +218,37 @@ export const shortcuts = [
name: "Change Tab",
keys: ["up"],
type: "bind",
function: () => keyboardTabChange("up"),
function: () => {
EventHub.dispatch(GAME_EVENT.ARROW_KEY_PRESSED, "up");
return false;
},
visible: false
}, {
name: "Change Tab",
keys: ["down"],
type: "bind",
function: () => keyboardTabChange("down"),
function: () => {
EventHub.dispatch(GAME_EVENT.ARROW_KEY_PRESSED, "down");
return false;
},
visible: false
}, {
name: "Change Subtab",
keys: ["left"],
type: "bind",
function: () => keyboardTabChange("left"),
function: () => {
EventHub.dispatch(GAME_EVENT.ARROW_KEY_PRESSED, "left");
return false;
},
visible: false
}, {
name: "Change Subtab",
keys: ["right"],
type: "bind",
function: () => keyboardTabChange("right"),
function: () => {
EventHub.dispatch(GAME_EVENT.ARROW_KEY_PRESSED, "right");
return false;
},
visible: false
}, {
name: "Doesn't exist",
@ -254,9 +269,8 @@ for (const hotkey of shortcuts) {
GameKeyboard[hotkey.type](keys, hotkey.function);
}
// We need to know whether the player is holding R or not for the
// replicanti galaxy
GameKeyboard.bind("r", () => setHoldingR(true), "keydown");
// We need to know whether the player is holding R or not for the replicanti galaxy
// The keydown version is above, with the replicantiGalaxyRequest, as otherwise it would be overridden
GameKeyboard.bind("r", () => setHoldingR(false), "keyup");
// Same thing with Shift; we need to double-up on ctrl-shift as well since they're technically different keybinds
@ -319,7 +333,7 @@ function toggleBuySingles(buyer) {
function keyboardToggleAutobuyers() {
Autobuyers.toggle();
GameUI.notify.info(`Autobuyers ${(player.auto.autobuyersOn) ? "enabled" : "disabled"}`);
GameUI.notify.info(`Autobuyers ${(player.auto.autobuyersOn) ? "resumed" : "paused"}`);
}
function keyboardToggleContinuum() {
@ -340,10 +354,9 @@ function keyboardAutomatorToggle() {
} else {
// Only attempt to start the visible script instead of the existing script if it isn't already running
const visibleIndex = player.reality.automator.state.editorScript;
const visibleScript = player.reality.automator.scripts[visibleIndex].content;
AutomatorBackend.restart();
AutomatorBackend.start(visibleIndex);
if (AutomatorData.currentErrors(AutomatorData.currentScriptText(visibleScript)).length === 0) {
if (AutomatorData.currentErrors().length === 0) {
GameUI.notify.info(`Starting script "${AutomatorBackend.scriptName}"`);
} else {
GameUI.notify.error(`Cannot start script "${AutomatorBackend.scriptName}" (has errors)`);
@ -372,11 +385,9 @@ function armageddonRequest() {
}
function keyboardPressEscape() {
if (ui.view.modal.queue.length === 0) {
Tab.options.show(true);
} else {
Modal.hideAll();
}
if (Quote.isOpen || Quote.isHistoryOpen) Quote.clearAll();
else if (Modal.isOpen) Modal.hideAll();
else Tab.options.show(true);
}
function keyboardPressQuestionMark() {
@ -406,22 +417,23 @@ function keyboardVisibleTabsToggle() {
Modal.hiddenTabs.show();
}
function keyboardTabChange(direction) {
EventHub.logic.on(GAME_EVENT.ARROW_KEY_PRESSED, direction => {
if (Quote.isOpen || Quote.isHistoryOpen) return;
// Current tabs. Defined here as both tab and subtab movements require knowing your current tab.
const currentTab = Tabs.current.key;
if (direction === "up" || direction === "down") {
if (direction[0] === "up" || direction[0] === "down") {
// Make an array of the keys of all the unlocked and visible tabs
const tabs = Tabs.currentUIFormat.flatMap(i => (i.isAvailable ? [i.key] : []));
// Find the index of the tab we are on
let top = tabs.indexOf(currentTab);
// Move in the desired direction
if (direction === "up") top--;
if (direction[0] === "up") top--;
else top++;
// Loop around if needed
top = (top + tabs.length) % tabs.length;
// And now we go there.
Tab[tabs[top]].show(true);
} else if (direction === "left" || direction === "right") {
} else if (direction[0] === "left" || direction[0] === "right") {
// Current subtabs
const currentSubtab = Tabs.current._currentSubtab.key;
// Make an array of the keys of all the unlocked and visible subtabs
@ -429,16 +441,14 @@ function keyboardTabChange(direction) {
// Find the index of the subtab we are on
let sub = subtabs.indexOf(currentSubtab);
// Move in the desired direction
if (direction === "left") sub--;
if (direction[0] === "left") sub--;
else sub++;
// Loop around if needed
sub = (sub + subtabs.length) % subtabs.length;
// And now we go there.
Tab[currentTab][subtabs[sub]].show(true);
}
// Return false so the arrow keys don't do anything else
return false;
}
});
const konamiCode = ["up", "up", "down", "down", "left", "right", "left", "right", "b", "a", "enter"];
let konamiStep = 0;

View File

@ -1,5 +1,5 @@
import { BitPurchasableMechanicState, RebuyableMechanicState } from "./game-mechanics/index.js";
import { DC } from "./constants.js";
import { BitPurchasableMechanicState, RebuyableMechanicState } from "./game-mechanics/index";
import { DC } from "./constants";
class ImaginaryUpgradeState extends BitPurchasableMechanicState {
constructor(config) {
@ -24,7 +24,6 @@ class ImaginaryUpgradeState extends BitPurchasableMechanicState {
}
get isAvailableForPurchase() {
// eslint-disable-next-line no-bitwise
return (player.reality.imaginaryUpgReqs & (1 << this.id)) !== 0;
}
@ -33,12 +32,15 @@ class ImaginaryUpgradeState extends BitPurchasableMechanicState {
}
get canBeApplied() {
return super.canBeApplied && !Pelle.isDisabled("imaginaryUpgrades");
return super.canBeApplied && !this.pelleDisabled;
}
get pelleDisabled() {
return Pelle.isDoomed && this.config.isDisabledInDoomed;
}
tryUnlock() {
if (!MachineHandler.isIMUnlocked || this.isAvailableForPurchase || !this.config.checkRequirement()) return;
// eslint-disable-next-line no-bitwise
player.reality.imaginaryUpgReqs |= (1 << this.id);
GameUI.notify.reality(`You've unlocked an Imaginary Upgrade: ${this.config.name}`);
}
@ -71,12 +73,22 @@ class RebuyableImaginaryUpgradeState extends RebuyableMechanicState {
}
get canBeApplied() {
return super.canBeApplied && !Pelle.isDisabled("imaginaryUpgrades");
return super.canBeApplied && !this.pelleDisabled;
}
get pelleDisabled() {
return Pelle.isDoomed;
}
set boughtAmount(value) {
player.reality.imaginaryRebuyables[this.id] = value;
}
onPurchased() {
if (this.id === 7) {
GameCache.staticGlyphWeights.invalidate();
}
}
}
ImaginaryUpgradeState.index = mapGameData(
@ -100,7 +112,6 @@ export const ImaginaryUpgrades = {
return this.all.countWhere(u => u.isBought);
},
get allBought() {
// eslint-disable-next-line no-bitwise
return (player.reality.imaginaryUpgradeBits >> 6) + 1 === 1 << (GameDatabase.reality.imaginaryUpgrades.length - 5);
}
};

View File

@ -0,0 +1,163 @@
import { GameMechanicState } from "./game-mechanics/index";
export function tryCompleteInfinityChallenges() {
if (EternityMilestone.autoIC.isReached) {
const toComplete = InfinityChallenges.all.filter(x => x.isUnlocked && !x.isCompleted);
for (const challenge of toComplete) challenge.complete();
}
}
class InfinityChallengeRewardState extends GameMechanicState {
constructor(config, challenge) {
super(config);
this._challenge = challenge;
}
get isEffectActive() {
return this._challenge.isCompleted;
}
}
class InfinityChallengeState extends GameMechanicState {
constructor(config) {
super(config);
this._reward = new InfinityChallengeRewardState(config.reward, this);
}
get unlockAM() {
return this.config.unlockAM;
}
get isUnlocked() {
return player.records.thisEternity.maxAM.gte(this.unlockAM) || (Achievement(133).isUnlocked && !Pelle.isDoomed) ||
(PelleUpgrade.keepInfinityChallenges.canBeApplied && Pelle.cel.records.totalAntimatter.gte(this.unlockAM));
}
get isRunning() {
return player.challenge.infinity.current === this.id;
}
requestStart() {
if (!this.isUnlocked) return;
if (GameEnd.creditsEverClosed) return;
if (!player.options.confirmations.challenges) {
this.start();
return;
}
Modal.startInfinityChallenge.show(this.id);
}
start() {
if (!this.isUnlocked || this.isRunning) return;
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();
}
get isCompleted() {
return (player.challenge.infinity.completedBits & (1 << this.id)) !== 0;
}
complete() {
player.challenge.infinity.completedBits |= 1 << this.id;
EventHub.dispatch(GAME_EVENT.INFINITY_CHALLENGE_COMPLETED);
}
get isEffectActive() {
return this.isRunning;
}
/**
* @return {InfinityChallengeRewardState}
*/
get reward() {
return this._reward;
}
get isQuickResettable() {
return this.config.isQuickResettable;
}
get goal() {
return this.config.goal;
}
updateChallengeTime() {
const bestTimes = player.challenge.infinity.bestTimes;
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);
GameCache.infinityChallengeTimeSum.invalidate();
}
exit() {
player.challenge.infinity.current = 0;
bigCrunchResetValues();
if (!Enslaved.isRunning) Tab.dimensions.antimatter.show();
}
}
/**
* @param {number} id
* @return {InfinityChallengeState}
*/
export const InfinityChallenge = InfinityChallengeState.createAccessor(GameDatabase.challenges.infinity);
/**
* @returns {InfinityChallengeState}
*/
Object.defineProperty(InfinityChallenge, "current", {
get: () => (player.challenge.infinity.current > 0
? InfinityChallenge(player.challenge.infinity.current)
: undefined),
});
Object.defineProperty(InfinityChallenge, "isRunning", {
get: () => InfinityChallenge.current !== undefined,
});
export const InfinityChallenges = {
/**
* @type {InfinityChallengeState[]}
*/
all: InfinityChallenge.index.compact(),
completeAll() {
for (const challenge of InfinityChallenges.all) challenge.complete();
},
clearCompletions() {
player.challenge.infinity.completedBits = 0;
},
get nextIC() {
return InfinityChallenges.all.find(x => !x.isUnlocked);
},
get nextICUnlockAM() {
return this.nextIC?.unlockAM;
},
/**
* Displays a notification if the antimatter gained will surpass the next unlockAM requirement.
* @param value {Decimal} - total antimatter
*/
notifyICUnlock(value) {
// Disable the popup if the user will automatically complete the IC.
if (EternityMilestone.autoIC.isReached) return;
if (InfinityChallenges.nextIC === undefined) return;
for (const ic of InfinityChallenges.all) {
if (ic.isUnlocked || ic.isCompleted) continue;
if (value.lt(ic.unlockAM)) break;
// This has a reasonably high likelihood of happening when the player isn't looking at the game, so
// we leave it there for 5 minutes unless they click it away early
GameUI.notify.infinity(`You have unlocked Infinity Challenge ${ic.id}`, 300000);
}
},
/**
* @returns {InfinityChallengeState[]}
*/
get completed() {
return InfinityChallenges.all.filter(ic => ic.isCompleted);
}
};

View File

@ -0,0 +1,215 @@
import { GameMechanicState, SetPurchasableMechanicState } from "./game-mechanics/index";
import { DC } from "./constants";
class ChargedInfinityUpgradeState extends GameMechanicState {
constructor(config, upgrade) {
super(config);
this._upgrade = upgrade;
}
get isEffectActive() {
return this._upgrade.isBought && this._upgrade.isCharged;
}
}
export class InfinityUpgradeState extends SetPurchasableMechanicState {
constructor(config) {
super(config);
if (config.charged) {
this._chargedEffect = new ChargedInfinityUpgradeState(config.charged, this);
}
}
get currency() {
return Currency.infinityPoints;
}
get set() {
return player.infinityUpgrades;
}
get isAvailableForPurchase() {
return this.config.checkRequirement?.() ?? true;
}
get isEffectActive() {
return this.isBought && !this.isCharged;
}
get chargedEffect() {
return this._chargedEffect;
}
purchase() {
if (super.purchase()) {
// This applies the 4th column of infinity upgrades retroactively
if (this.config.id.includes("skip")) skipResetsIfPossible();
EventHub.dispatch(GAME_EVENT.INFINITY_UPGRADE_BOUGHT);
return true;
}
if (this.canCharge) {
this.charge();
EventHub.dispatch(GAME_EVENT.INFINITY_UPGRADE_CHARGED);
return true;
}
return false;
}
get hasChargeEffect() {
return this.config.charged !== undefined;
}
get isCharged() {
return player.celestials.ra.charged.has(this.id);
}
get canCharge() {
return this.isBought &&
this.hasChargeEffect &&
!this.isCharged &&
Ra.chargesLeft !== 0 &&
!Pelle.isDisabled("chargedInfinityUpgrades");
}
charge() {
player.celestials.ra.charged.add(this.id);
}
disCharge() {
player.celestials.ra.charged.delete(this.id);
}
}
export function totalIPMult() {
if (Effarig.isRunning && Effarig.currentStage === EFFARIG_STAGES.INFINITY) {
return DC.D1;
}
let ipMult = DC.D1
.times(ShopPurchase.IPPurchases.currentMult)
.timesEffectsOf(
TimeStudy(41),
TimeStudy(51),
TimeStudy(141),
TimeStudy(142),
TimeStudy(143),
Achievement(85),
Achievement(93),
Achievement(116),
Achievement(125),
Achievement(141).effects.ipGain,
InfinityUpgrade.ipMult,
DilationUpgrade.ipMultDT,
GlyphEffect.ipMult
);
ipMult = ipMult.times(Replicanti.amount.powEffectOf(AlchemyResource.exponential));
return ipMult;
}
export function disChargeAll() {
const upgrades = [
InfinityUpgrade.totalTimeMult,
InfinityUpgrade.dim18mult,
InfinityUpgrade.dim36mult,
InfinityUpgrade.resetBoost,
InfinityUpgrade.buy10Mult,
InfinityUpgrade.dim27mult,
InfinityUpgrade.dim45mult,
InfinityUpgrade.galaxyBoost,
InfinityUpgrade.thisInfinityTimeMult,
InfinityUpgrade.unspentIPMult,
InfinityUpgrade.dimboostMult,
InfinityUpgrade.ipGen
];
for (const upgrade of upgrades) {
if (upgrade.isCharged) {
upgrade.disCharge();
}
}
player.celestials.ra.disCharge = false;
EventHub.dispatch(GAME_EVENT.INFINITY_UPGRADES_DISCHARGED);
}
// The repeatable 2xIP upgrade has an odd cost structure - it follows a shallow exponential (step *10) up to e3M, at
// which point it follows a steeper one (step *1e10) up to e6M before finally hardcapping. At the hardcap, there's
// an extra bump that increases the multipler itself from e993k to e1M. All these numbers are specified in
// GameDatabase.infinity.upgrades.ipMult
class InfinityIPMultUpgrade extends GameMechanicState {
get cost() {
if (this.purchaseCount >= this.purchasesAtIncrease) {
return this.config.costIncreaseThreshold
.times(Decimal.pow(this.costIncrease, this.purchaseCount - this.purchasesAtIncrease));
}
return Decimal.pow(this.costIncrease, this.purchaseCount + 1);
}
get purchaseCount() {
return player.IPMultPurchases;
}
get purchasesAtIncrease() {
return this.config.costIncreaseThreshold.log10() - 1;
}
get hasIncreasedCost() {
return this.purchaseCount >= this.purchasesAtIncrease;
}
get costIncrease() {
return this.hasIncreasedCost ? 1e10 : 10;
}
get isCapped() {
return this.cost.gte(this.config.costCap);
}
get isBought() {
return this.isCapped;
}
get isRequirementSatisfied() {
return Achievement(41).isUnlocked;
}
get canBeBought() {
return !Pelle.isDoomed && !this.isCapped && Currency.infinityPoints.gte(this.cost) && this.isRequirementSatisfied;
}
// This is only ever called with amount = 1 or within buyMax under conditions that ensure the scaling doesn't
// change mid-purchase
purchase(amount = 1) {
if (!this.canBeBought) return;
if (!TimeStudy(181).isBought) {
Autobuyer.bigCrunch.bumpAmount(DC.D2.pow(amount));
}
Currency.infinityPoints.subtract(Decimal.sumGeometricSeries(amount, this.cost, this.costIncrease, 0));
player.IPMultPurchases += amount;
GameUI.update();
}
buyMax() {
if (!this.canBeBought) return;
if (!this.hasIncreasedCost) {
// Only allow IP below the softcap to be used
const availableIP = Currency.infinityPoints.value.clampMax(this.config.costIncreaseThreshold);
const purchases = Decimal.affordGeometricSeries(availableIP, this.cost, this.costIncrease, 0).toNumber();
if (purchases <= 0) return;
this.purchase(purchases);
}
// Do not replace it with `if else` - it's specifically designed to process two sides of threshold separately
// (for example, we have 1e4000000 IP and no mult - first it will go to (but not including) 1e3000000 and then
// it will go in this part)
if (this.hasIncreasedCost) {
const availableIP = Currency.infinityPoints.value.clampMax(this.config.costCap);
const purchases = Decimal.affordGeometricSeries(availableIP, this.cost, this.costIncrease, 0).toNumber();
if (purchases <= 0) return;
this.purchase(purchases);
}
}
}
export const InfinityUpgrade = mapGameDataToObject(
GameDatabase.infinity.upgrades,
config => (config.id === "ipMult"
? new InfinityIPMultUpgrade(config)
: new InfinityUpgradeState(config))
);

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