mirror of
https://github.com/IvarK/AntimatterDimensionsSourceCode.git
synced 2024-11-25 05:32:17 +00:00
Merge branch 'master' into Steam-Webpack
This commit is contained in:
commit
0dddaa6b90
@ -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
3
.stylelintignore
Normal file
@ -0,0 +1,3 @@
|
||||
public/stylesheets/fontawesome/**/*.css
|
||||
public/stylesheets/codemirror/*.css
|
||||
public/stylesheets/vis-network.css
|
425
.stylelintrc.json
Normal file
425
.stylelintrc.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
}),
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
@ -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),
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
|
||||
import { Autobuyer, AutobuyerState } from "./autobuyer";
|
||||
|
||||
Autobuyer.eternity = new class EternityAutobuyerState extends AutobuyerState {
|
||||
get data() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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";
|
||||
|
@ -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();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
|
||||
import { Autobuyer, AutobuyerState } from "./autobuyer";
|
||||
|
||||
Autobuyer.ipMult = new class IPMultAutobuyerState extends AutobuyerState {
|
||||
get data() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
|
||||
import { Autobuyer, AutobuyerState } from "./autobuyer";
|
||||
|
||||
Autobuyer.reality = new class RealityAutobuyerState extends AutobuyerState {
|
||||
get data() {
|
||||
|
@ -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();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
|
||||
import { Autobuyer, AutobuyerState } from "./autobuyer";
|
||||
|
||||
Autobuyer.replicantiGalaxy = new class ReplicantiGalaxyAutobuyerState extends AutobuyerState {
|
||||
get data() {
|
||||
|
@ -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();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Autobuyer, AutobuyerState } from "./autobuyer.js";
|
||||
import { Autobuyer, AutobuyerState } from "./autobuyer";
|
||||
|
||||
Autobuyer.sacrifice = new class SacrificeAutobuyerState extends AutobuyerState {
|
||||
get data() {
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
@ -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() {
|
||||
|
@ -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) {
|
||||
|
@ -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 },
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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>",
|
||||
},
|
||||
];
|
||||
}());
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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];
|
||||
}
|
||||
};
|
||||
|
48
javascripts/core/break-infinity-upgrades.js
Normal file
48
javascripts/core/break-infinity-upgrades.js
Normal 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))
|
||||
);
|
@ -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() {
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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.`;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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)
|
||||
);
|
||||
}());
|
||||
|
48
javascripts/core/celestials/pelle/game-end.js
Normal file
48
javascripts/core/celestials/pelle/game-end.js
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
);
|
||||
}());
|
||||
|
@ -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")
|
||||
),
|
||||
};
|
||||
|
@ -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
|
||||
}));
|
||||
}
|
||||
|
@ -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 Teresa’s 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();
|
||||
});
|
||||
|
@ -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());
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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))
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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(" + ");
|
||||
};
|
||||
|
||||
|
16
javascripts/core/full-screen-animation-handler.js
Normal file
16
javascripts/core/full-screen-animation-handler.js
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
38
javascripts/core/game-mechanics/bit-upgrade-state.js
Normal file
38
javascripts/core/game-mechanics/bit-upgrade-state.js
Normal 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { GameMechanicState } from "./game-mechanic.js";
|
||||
import { GameMechanicState } from "./game-mechanic";
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
|
@ -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();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PurchasableMechanicState } from "./puchasable.js";
|
||||
import { PurchasableMechanicState } from "./puchasable";
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
163
javascripts/core/infinity-challenges.js
Normal file
163
javascripts/core/infinity-challenges.js
Normal 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);
|
||||
}
|
||||
};
|
215
javascripts/core/infinity-upgrades.js
Normal file
215
javascripts/core/infinity-upgrades.js
Normal 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
Loading…
Reference in New Issue
Block a user