Further TimeStudyTree code restructuring and standardization

This commit is contained in:
SpectralFlame 2021-11-18 13:57:03 -06:00
parent 83fedc02b2
commit bf826aa267
7 changed files with 285 additions and 328 deletions

View File

@ -31,7 +31,7 @@ Vue.component("ec-time-study", {
return typeof this.study.requirementCurrent === "number";
},
formatValue() {
return this.config.requirement.formatValue;
return this.config.secondary.formatValue;
},
// Linebreaks added to avoid twitching in scientific notation
needsFirstLinebreak() {
@ -69,12 +69,11 @@ Vue.component("ec-time-study", {
<br>
Requirement:
<br v-if="needsFirstLinebreak">
<span v-if="id === 12">Use only the Time Dimension path</span>
<span v-else-if="id === 11">Use only the Antimatter Dimension path</span>
<span v-if="config.secondary.path">Use only the {{ config.secondary.path }} path</span>
<span v-else>
{{ formatValue(requirement.current) }}/{{ formatValue(requirement.total) }}
<br v-if="needsSecondLinebreak">
{{ config.requirement.resource }}
{{ config.secondary.resource }}
</span>
</template>
<span v-if="isUnlocked && !isRunning"><br>Double click to start</span>

View File

@ -22,17 +22,9 @@ Vue.component("normal-time-study", {
},
hintText() {
const id = this.study.id;
switch (this.setup.path) {
case TIME_STUDY_PATH.ANTIMATTER_DIM: return `${id} Antimatter Dims`;
case TIME_STUDY_PATH.INFINITY_DIM: return `${id} Infinity Dims`;
case TIME_STUDY_PATH.TIME_DIM: return `${id} Time Dims`;
case TIME_STUDY_PATH.ACTIVE: return `${id} Active`;
case TIME_STUDY_PATH.PASSIVE: return `${id} Passive`;
case TIME_STUDY_PATH.IDLE: return `${id} Idle`;
case TIME_STUDY_PATH.LIGHT: return `${id} Light`;
case TIME_STUDY_PATH.DARK: return `${id} Dark`;
}
return id;
if (!this.setup.path) return id;
const pathEntry = NormalTimeStudies.pathList.find(p => p.path === this.setup.path);
return `${id} ${pathEntry.name}`;
}
},
methods: {

View File

@ -9,65 +9,28 @@ Vue.component("modal-edit-tree", {
};
},
computed: {
tree() {
importedTree() {
if (!this.inputIsValidTree) return false;
const formattedInput = this.input.split("|")[0].split(",");
const eternityChallenge = TimeStudy.eternityChallenge(this.input.split("|")[1]);
const hasEternityChallenge = eternityChallenge !== undefined;
const studies = new Set();
for (const study of formattedInput) {
studies.add(TimeStudy(study));
}
let totalCost = 0;
let missingCost = 0;
if (hasEternityChallenge) {
totalCost += eternityChallenge.cost;
if (player.challenge.eternity.unlocked !== eternityChallenge.id) {
missingCost += eternityChallenge.cost;
}
}
const firstSplitPaths = new Set();
const secondSplitPaths = new Set();
for (const study of studies) {
if (study.cost) {
totalCost += study.cost;
if (!study.isBought) {
missingCost += study.cost;
}
}
switch (study.path) {
case TIME_STUDY_PATH.ANTIMATTER_DIM:
firstSplitPaths.add("Antimatter Dims");
break;
case TIME_STUDY_PATH.INFINITY_DIM:
firstSplitPaths.add("Infinity Dims");
break;
case TIME_STUDY_PATH.TIME_DIM:
firstSplitPaths.add("Time Dims");
break;
case TIME_STUDY_PATH.ACTIVE:
secondSplitPaths.add("Active");
break;
case TIME_STUDY_PATH.PASSIVE:
secondSplitPaths.add("Passive");
break;
case TIME_STUDY_PATH.IDLE:
secondSplitPaths.add("Idle");
}
}
const totalST = this.calculateMissingSTCost([...studies], true);
const missingST = this.calculateMissingSTCost([...studies], false);
const importedTree = new TimeStudyTree(this.input, Currency.timeTheorems.value, V.spaceTheorems);
return {
totalST,
missingST,
totalCost,
missingCost,
firstSplitPaths,
secondSplitPaths,
eternityChallenge,
hasEternityChallenge
totalTT: importedTree.runningCost[0],
totalST: importedTree.runningCost[1],
newStudies: makeEnumeration(importedTree.purchasedStudies),
invalidStudies: importedTree.invalidStudies,
firstPaths: makeEnumeration(importedTree.firstSplitPaths),
secondPaths: makeEnumeration(importedTree.secondSplitPaths),
ec: importedTree.ec,
};
},
invalidMessage() {
if (!this.inputIsValidTree || this.importedTree.invalidStudies.length === 0) return null;
let coloredString = this.input;
for (const id of this.importedTree.invalidStudies) {
coloredString = coloredString.replaceAll(new RegExp(`(,)?(${id})(,)?`, "gu"),
`$1<span style="color: var(--color-bad);">$2</span>$3`);
}
return `Your import string has invalid study IDs: ${coloredString}`;
},
editLabel() {
return `Editing ${this.name}`;
},
@ -89,47 +52,11 @@ Vue.component("modal-edit-tree", {
this.emitClose();
}
},
formatPaths(paths) {
return Array.from(paths).join(", ");
},
calculateMissingSTCost(studiesToBuy, ignoreCurrentStudies) {
// Explicitly hardcoding how the study tree affects total ST should be fine here, as it massively simplifies
// the code and the study tree structure is very unlikely to change. Note that all studies within the same
// set also have identical ST costs. Triads also have identical costs too.
const conflictingStudySets = [
[121, 122, 123],
[131, 132, 133],
[141, 142, 143],
[221, 222],
[223, 224],
[225, 226],
[227, 228],
[231, 232],
[233, 234],
];
let totalSTSpent = 0;
for (const studySet of conflictingStudySets) {
const studiesInSet = studiesToBuy.filter(study => studySet.includes(study.id));
if (studiesInSet.length > 1) {
totalSTSpent += TimeStudy(studySet[0]).STCost * (studiesInSet.length - 1);
if (!ignoreCurrentStudies) {
const currStudies = player.timestudy.studies;
const alreadyBought = studiesInSet.filter(study => currStudies.includes(study.id));
totalSTSpent -= TimeStudy(studySet[0]).STCost * Math.clampMin(alreadyBought.length - 1, 0);
}
}
}
// Triad studies don't have .cost
const triads = studiesToBuy.filter(study => !study.cost);
if (ignoreCurrentStudies) {
totalSTSpent += triads.length * TriadStudy(1).STCost;
} else {
totalSTSpent += TriadStudy(1).STCost * triads
.filter(study => !player.celestials.v.triadStudies.includes(study))
.length;
}
return totalSTSpent;
},
formatTheoremCost(tt, st) {
const strTT = `${formatWithCommas(tt)} TT`;
const strST = `${formatWithCommas(st)} ST`;
return st === 0 ? strTT : `${strTT} + ${strST}`;
}
},
template: `
<div class="c-modal-import-tree l-modal-content--centered">
@ -147,29 +74,27 @@ Vue.component("modal-edit-tree", {
<div v-if="!inputIsValid">Invalid tree</div>
<template v-if="inputIsValidTree">
<div class="l-modal-import-tree__tree-info-line">
Total tree cost:
{{ quantify("Time Theorem", tree.totalCost, 0, 0, formatWithCommas) }}
<span v-if="tree.totalST !== 0">
and {{ quantify("Space Theorem", tree.totalST, 0, 0, formatWithCommas) }}
</span>
</div>
<div class="l-modal-import-tree__tree-info-line">
Cost of missing studies:
{{ quantify("Time Theorem", tree.missingCost, 0, 0, formatWithCommas) }}
<span v-if="tree.missingST !== 0">
and {{ quantify("Space Theorem", tree.missingST, 0, 0, formatWithCommas) }}
</span>
</div>
<div v-if="tree.firstSplitPaths.size > 0" class="l-modal-import-tree__tree-info-line">
{{ pluralize("First split path", tree.firstSplitPaths.size) }}:
{{ formatPaths(tree.firstSplitPaths) }}
</div>
<div v-if="tree.secondSplitPaths.size > 0" class="l-modal-import-tree__tree-info-line">
{{ pluralize("Second split path", tree.secondSplitPaths.size) }}:
{{ formatPaths(tree.secondSplitPaths) }}
<div v-if="importedTree.totalTT === 0">
<i>Importing this into an empty tree will not purchase anything.</i>
</div>
<div v-if="tree.hasEternityChallenge" class="l-modal-import-tree__tree-info-line">
Eternity challenge: {{ tree.eternityChallenge.id }}
<div v-else>
Importing into an empty tree will purchase:
<br>
{{ importedTree.newStudies }}
(Cost: {{ formatTheoremCost(importedTree.totalTT, importedTree.totalST) }})
</div>
</div>
<br>
<div v-if="invalidMessage" class="l-modal-import-tree__tree-info-line" v-html="invalidMessage" />
<br>
<div v-if="importedTree.firstPaths" class="l-modal-import-tree__tree-info-line">
First split: {{ importedTree.firstPaths }}
</div>
<div v-if="importedTree.secondPaths" class="l-modal-import-tree__tree-info-line">
Second split: {{ importedTree.secondPaths }}
</div>
<div v-if="importedTree.ec > 0" class="l-modal-import-tree__tree-info-line">
Eternity challenge: {{ importedTree.ec }}
</div>
</template>
</div>

View File

@ -8,65 +8,48 @@ Vue.component("modal-import-tree", {
this.$refs.input.select();
},
computed: {
tree() {
importedTree() {
if (!this.inputIsValidTree) return false;
const formattedInput = this.truncatedInput.split("|")[0].split(",");
const eternityChallenge = TimeStudy.eternityChallenge(this.truncatedInput.split("|")[1]);
const hasEternityChallenge = eternityChallenge !== undefined;
const studies = new Set();
for (const study of formattedInput) {
studies.add(TimeStudy(study));
}
let totalCost = 0;
let missingCost = 0;
if (hasEternityChallenge) {
totalCost += eternityChallenge.cost;
if (player.challenge.eternity.unlocked !== eternityChallenge.id) {
missingCost += eternityChallenge.cost;
}
}
const firstSplitPaths = new Set();
const secondSplitPaths = new Set();
for (const study of studies) {
if (study.cost) {
totalCost += study.cost;
if (!study.isBought) {
missingCost += study.cost;
}
}
switch (study.path) {
case TIME_STUDY_PATH.ANTIMATTER_DIM:
firstSplitPaths.add("Antimatter Dims");
break;
case TIME_STUDY_PATH.INFINITY_DIM:
firstSplitPaths.add("Infinity Dims");
break;
case TIME_STUDY_PATH.TIME_DIM:
firstSplitPaths.add("Time Dims");
break;
case TIME_STUDY_PATH.ACTIVE:
secondSplitPaths.add("Active");
break;
case TIME_STUDY_PATH.PASSIVE:
secondSplitPaths.add("Passive");
break;
case TIME_STUDY_PATH.IDLE:
secondSplitPaths.add("Idle");
}
}
const totalST = this.calculateMissingSTCost([...studies], true);
const missingST = this.calculateMissingSTCost([...studies], false);
const importedTree = new TimeStudyTree(this.truncatedInput, Currency.timeTheorems.value, V.spaceTheorems);
return {
totalST,
missingST,
totalCost,
missingCost,
firstSplitPaths,
secondSplitPaths,
eternityChallenge,
hasEternityChallenge
totalTT: importedTree.runningCost[0],
totalST: importedTree.runningCost[1],
newStudies: makeEnumeration(importedTree.purchasedStudies),
invalidStudies: importedTree.invalidStudies,
};
},
combinedTree() {
if (!this.inputIsValidTree) return false;
const currentStudies = player.timestudy.studies.map(s => `${s}`)
.concat(player.celestials.v.triadStudies.map(s => `T${s}`));
if (player.challenge.eternity.current !== 0) currentStudies.push(`EC${player.challenge.eternity.current}`);
// We know that we have enough for all existing studies because we actually purchased them, so setting initial
// theorem values to e308 ensures we have enough to actually properly initialize a Tree object with all the
// current studies. Then we set theorem totals to their proper values immediately AFTER everything is bought
const currentStudyTree = new TimeStudyTree(currentStudies, Number.MAX_VALUE, Number.MAX_VALUE);
currentStudyTree.remainingTheorems = [Currency.timeTheorems.value, V.spaceTheorems];
const importedTree = new TimeStudyTree(this.truncatedInput, Currency.timeTheorems.value, V.spaceTheorems);
const compositeTree = currentStudyTree.createCombinedTree(importedTree);
return {
missingTT: compositeTree.runningCost[0] - currentStudyTree.runningCost[0],
missingST: compositeTree.runningCost[1] - currentStudyTree.runningCost[1],
newStudies: makeEnumeration(compositeTree.purchasedStudies
.filter(s => !currentStudyTree.purchasedStudies.includes(s))),
firstPaths: makeEnumeration(compositeTree.firstSplitPaths),
secondPaths: makeEnumeration(compositeTree.secondSplitPaths),
ec: compositeTree.ec,
};
},
invalidMessage() {
if (!this.inputIsValidTree || this.importedTree.invalidStudies.length === 0) return null;
let coloredString = this.truncatedInput;
for (const id of this.importedTree.invalidStudies) {
coloredString = coloredString.replaceAll(new RegExp(`(,)?(${id})(,)?`, "gu"),
`$1<span style="color: var(--color-bad);">$2</span>$3`);
}
return `Your import string has invalid study IDs: ${coloredString}`;
},
truncatedInput() {
// If last character is "," remove it
return this.input.replace(/,$/u, "");
@ -97,47 +80,11 @@ Vue.component("modal-import-tree", {
formatCost(cost) {
return formatWithCommas(cost);
},
formatPaths(paths) {
return Array.from(paths).join(", ");
},
calculateMissingSTCost(studiesToBuy, ignoreCurrentStudies) {
// Explicitly hardcoding how the study tree affects total ST should be fine here, as it massively simplifies
// the code and the study tree structure is very unlikely to change. Note that all studies within the same
// set also have identical ST costs. Triads also have identical costs too.
const conflictingStudySets = [
[121, 122, 123],
[131, 132, 133],
[141, 142, 143],
[221, 222],
[223, 224],
[225, 226],
[227, 228],
[231, 232],
[233, 234],
];
let totalSTSpent = 0;
for (const studySet of conflictingStudySets) {
const studiesInSet = studiesToBuy.filter(study => studySet.includes(study.id));
if (studiesInSet.length > 1) {
totalSTSpent += TimeStudy(studySet[0]).STCost * (studiesInSet.length - 1);
if (!ignoreCurrentStudies) {
const currStudies = player.timestudy.studies;
const alreadyBought = studiesInSet.filter(study => currStudies.includes(study.id));
totalSTSpent -= TimeStudy(studySet[0]).STCost * Math.clampMin(alreadyBought.length - 1, 0);
}
}
}
// Triad studies don't have .cost
const triads = studiesToBuy.filter(study => !study.cost);
if (ignoreCurrentStudies) {
totalSTSpent += triads.length * TriadStudy(1).STCost;
} else {
totalSTSpent += TriadStudy(1).STCost * triads
.filter(study => !player.celestials.v.triadStudies.includes(study))
.length;
}
return totalSTSpent;
},
formatTheoremCost(tt, st) {
const strTT = `${formatWithCommas(tt)} TT`;
const strST = `${formatWithCommas(st)} ST`;
return st === 0 ? strTT : `${strTT} + ${strST}`;
}
},
template: `
<div class="c-modal-import-tree l-modal-content--centered">
@ -155,29 +102,38 @@ Vue.component("modal-import-tree", {
<div v-if="inputIsSecret">???</div>
<template v-else-if="inputIsValidTree">
<div class="l-modal-import-tree__tree-info-line">
Total tree cost:
{{ quantify("Time Theorem", tree.totalCost, 0, 0, formatWithCommas) }}
<span v-if="tree.totalST !== 0">
and {{ quantify("Space Theorem", tree.totalST, 0, 0, formatWithCommas) }}
</span>
<div v-if="combinedTree.missingTT === 0">
<i>Importing this with your current studies will not purchase anything.</i>
</div>
<div v-else>
Importing with your current studies will purchase:
<br>
{{ combinedTree.newStudies }}
(Cost: {{ formatTheoremCost(combinedTree.missingTT, combinedTree.missingST) }})
</div>
</div>
<div class="l-modal-import-tree__tree-info-line">
Cost of missing studies:
{{ quantify("Time Theorem", tree.missingCost, 0, 0, formatWithCommas) }}
<span v-if="tree.missingST !== 0">
and {{ quantify("Space Theorem", tree.missingST, 0, 0, formatWithCommas) }}
</span>
</div>
<div v-if="tree.firstSplitPaths.size > 0" class="l-modal-import-tree__tree-info-line">
{{ pluralize("First split path", tree.firstSplitPaths.size) }}:
{{ formatPaths(tree.firstSplitPaths) }}
</div>
<div v-if="tree.secondSplitPaths.size > 0" class="l-modal-import-tree__tree-info-line">
{{ pluralize("Second split path", tree.secondSplitPaths.size) }}:
{{ formatPaths(tree.secondSplitPaths) }}
<div v-if="importedTree.totalTT === 0">
<i>Importing this into an empty tree will not purchase anything.</i>
</div>
<div v-if="tree.hasEternityChallenge" class="l-modal-import-tree__tree-info-line">
Eternity challenge: {{ tree.eternityChallenge.id }}
<div v-else>
Importing into an empty tree will purchase:
<br>
{{ importedTree.newStudies }}
(Cost: {{ formatTheoremCost(importedTree.totalTT, importedTree.totalST) }})
</div>
</div>
<br>
<div v-if="invalidMessage" class="l-modal-import-tree__tree-info-line" v-html="invalidMessage" />
<br>
<div v-if="combinedTree.firstPaths" class="l-modal-import-tree__tree-info-line">
First split: {{ combinedTree.firstPaths }}
</div>
<div v-if="combinedTree.secondPaths" class="l-modal-import-tree__tree-info-line">
Second split: {{ combinedTree.secondPaths }}
</div>
<div v-if="combinedTree.ec > 0" class="l-modal-import-tree__tree-info-line">
Eternity challenge: {{ combinedTree.ec }}
</div>
</template>
<div v-else-if="hasInput">Not a valid tree</div>

View File

@ -5,7 +5,9 @@ GameDatabase.eternity.timeStudies.ec = [
{
id: 1,
cost: 30,
requirement: {
requirement: [171],
reqType: TS_REQUIREMENT_TYPE.AT_LEAST_ONE,
secondary: {
resource: "Eternities",
current: () => Currency.eternities.value,
required: completions => new Decimal(20000 + completions * 20000),
@ -15,7 +17,9 @@ GameDatabase.eternity.timeStudies.ec = [
{
id: 2,
cost: 35,
requirement: {
requirement: [171],
reqType: TS_REQUIREMENT_TYPE.AT_LEAST_ONE,
secondary: {
resource: "Tickspeed upgrades from Time Dimensions",
current: () => player.totalTickGained,
required: completions => 1300 + completions * 150,
@ -25,7 +29,9 @@ GameDatabase.eternity.timeStudies.ec = [
{
id: 3,
cost: 40,
requirement: {
requirement: [171],
reqType: TS_REQUIREMENT_TYPE.AT_LEAST_ONE,
secondary: {
resource: "8th Antimatter Dimensions",
current: () => AntimatterDimension(8).totalAmount,
required: completions => new Decimal(17300 + completions * 1250),
@ -35,7 +41,9 @@ GameDatabase.eternity.timeStudies.ec = [
{
id: 4,
cost: 70,
requirement: {
requirement: [143],
reqType: TS_REQUIREMENT_TYPE.AT_LEAST_ONE,
secondary: {
resource: "Infinities",
current: () => Currency.infinitiesTotal.value,
required: completions => new Decimal(1e8 + completions * 5e7),
@ -45,7 +53,9 @@ GameDatabase.eternity.timeStudies.ec = [
{
id: 5,
cost: 130,
requirement: {
requirement: [42],
reqType: TS_REQUIREMENT_TYPE.AT_LEAST_ONE,
secondary: {
resource: "Antimatter Galaxies",
current: () => player.galaxies,
required: completions => 160 + completions * 14,
@ -55,7 +65,9 @@ GameDatabase.eternity.timeStudies.ec = [
{
id: 6,
cost: 85,
requirement: {
requirement: [121],
reqType: TS_REQUIREMENT_TYPE.AT_LEAST_ONE,
secondary: {
resource: "Replicanti Galaxies",
current: () => player.replicanti.galaxies,
required: completions => 40 + completions * 5,
@ -65,7 +77,9 @@ GameDatabase.eternity.timeStudies.ec = [
{
id: 7,
cost: 115,
requirement: {
requirement: [111],
reqType: TS_REQUIREMENT_TYPE.AT_LEAST_ONE,
secondary: {
resource: "antimatter",
current: () => Currency.antimatter.value,
required: completions => DC.E300000.pow(completions).times(DC.E500000),
@ -75,7 +89,9 @@ GameDatabase.eternity.timeStudies.ec = [
{
id: 8,
cost: 115,
requirement: {
requirement: [123],
reqType: TS_REQUIREMENT_TYPE.AT_LEAST_ONE,
secondary: {
resource: "Infinity Points",
current: () => Currency.infinityPoints.value,
required: completions => DC.E1000.pow(completions).times(DC.E4000),
@ -85,7 +101,9 @@ GameDatabase.eternity.timeStudies.ec = [
{
id: 9,
cost: 415,
requirement: {
requirement: [151],
reqType: TS_REQUIREMENT_TYPE.AT_LEAST_ONE,
secondary: {
resource: "Infinity Power",
current: () => Currency.infinityPower.value,
required: completions => DC.E2000.pow(completions).times(DC.E17500),
@ -95,7 +113,9 @@ GameDatabase.eternity.timeStudies.ec = [
{
id: 10,
cost: 550,
requirement: {
requirement: [181],
reqType: TS_REQUIREMENT_TYPE.AT_LEAST_ONE,
secondary: {
resource: "Eternity Points",
current: () => Currency.eternityPoints.value,
required: completions => DC.E20.pow(completions).times(DC.E100),
@ -104,10 +124,20 @@ GameDatabase.eternity.timeStudies.ec = [
},
{
id: 11,
cost: 1
cost: 1,
requirement: [231, 232],
reqType: TS_REQUIREMENT_TYPE.AT_LEAST_ONE,
secondary: {
path: "Antimatter Dimension",
}
},
{
id: 12,
cost: 1
cost: 1,
requirement: [233, 234],
reqType: TS_REQUIREMENT_TYPE.AT_LEAST_ONE,
secondary: {
path: "Time Dimension",
}
}
];

View File

@ -3,14 +3,14 @@ import { GameMechanicState } from "./game-mechanics/index.js";
export const NormalTimeStudies = {};
NormalTimeStudies.pathList = [
{ path: TIME_STUDY_PATH.ANTIMATTER_DIM, studies: [71, 81, 91, 101] },
{ path: TIME_STUDY_PATH.INFINITY_DIM, studies: [72, 82, 92, 102] },
{ path: TIME_STUDY_PATH.TIME_DIM, studies: [73, 83, 93, 103] },
{ path: TIME_STUDY_PATH.ACTIVE, studies: [121, 131, 141] },
{ path: TIME_STUDY_PATH.PASSIVE, studies: [122, 132, 142] },
{ path: TIME_STUDY_PATH.IDLE, studies: [123, 133, 143] },
{ path: TIME_STUDY_PATH.LIGHT, studies: [221, 223, 225, 227, 231, 233] },
{ path: TIME_STUDY_PATH.DARK, studies: [222, 224, 226, 228, 232, 234] }
{ path: TIME_STUDY_PATH.ANTIMATTER_DIM, studies: [71, 81, 91, 101], name: "Antimatter Dims" },
{ path: TIME_STUDY_PATH.INFINITY_DIM, studies: [72, 82, 92, 102], name: "Infinity Dims" },
{ path: TIME_STUDY_PATH.TIME_DIM, studies: [73, 83, 93, 103], name: "Time Dims" },
{ path: TIME_STUDY_PATH.ACTIVE, studies: [121, 131, 141], name: "Active" },
{ path: TIME_STUDY_PATH.PASSIVE, studies: [122, 132, 142], name: "Passive" },
{ path: TIME_STUDY_PATH.IDLE, studies: [123, 133, 143], name: "Idle" },
{ path: TIME_STUDY_PATH.LIGHT, studies: [221, 223, 225, 227, 231, 233], name: "Light" },
{ path: TIME_STUDY_PATH.DARK, studies: [222, 224, 226, 228, 232, 234], name: "Dark" }
];
NormalTimeStudies.paths = NormalTimeStudies.pathList.mapToObject(e => e.path, e => e.studies);
@ -210,10 +210,6 @@ class TimeStudyState extends GameMechanicState {
constructor(config, type) {
super(config);
this.type = type;
/**
* @type {TimeStudyConnection[]}
*/
this.incomingConnections = [];
}
get cost() {
@ -331,36 +327,45 @@ NormalTimeStudyState.all = NormalTimeStudyState.studies.filter(e => e !== undefi
* in any TimeStudyState objects. During parsing, additional info is stored in order to improve user feedback when
* attempting to import other study trees.
*
* @member {Number} usableTT Time theorems remaining after attempting import
* @member {Number} usableST Space theorems remaining after attempting import
* @member {Number} totalTT Total time theorem cost for importing all valid studies
* @member {Number} totalST Total space theorem cost for importing all valid studies
* @member {Array: Number} initialTheorems Two-element array containing starting totals of TT/ST before buying
* @member {Array: Number} remainingTheorems Two-element array containing leftover TT/ST totals after buying
* @member {Array: Number} runningCost Two-element array containing the total cost of buying all valid,
* buyable studies whether or not they are actually affordable
* @member {Array: String} plannedStudies Array of valid studies to be purchased from the initial import string;
* all entries are Strings because both numbers (normal TS) and T# (triads) need to be supported
* all entries are Strings because numbers (normal TS), T# (triads), and EC# (ECs) need to be supported
* @member {Array: String} invalidStudies Array of studies from the initial string which are correctly formatted
* but don't actually exist; used for informational purposes elsewhere
* @member {Array: String} purchasedStudies Array of studies which were actually purchased, using the given amount
* of available theorems. Will always be a subset of plannedStudies
* @member {Array: String} errors Array of Strings indicating possible reasons the import string is invalid
* @member {Number} ec Numerical ID of the EC which is unlocked from the imported tree, zero if none
* @member {Number} ec Numerical ID of the EC which is unlocked from the imported tree. Equal to 0 if no
* EC has been attempted, or the negative of the ID if purchasing was attempted but failed (needed for merging logic)
*/
export class TimeStudyTree {
constructor(importString, usableTT, usableST) {
this.usableTT = usableTT;
this.usableST = usableST;
this.totalTT = 0;
this.totalST = 0;
// The first parameter will either be an import string or an array of studies (possibly with an EC at the end)
constructor(studies, initialTT, initialST) {
// If we have above e308 TT there's no way buying studies will put a dent in our total anyway
this.initialTheorems = [Decimal.min(initialTT, Number.MAX_VALUE).toNumber(), initialST];
this.remainingTheorems = [...this.initialTheorems];
this.runningCost = [0, 0];
this.plannedStudies = [];
this.invalidStudies = [];
this.errors = [];
if (TimeStudyTree.isValidImportString(importString)) {
this.parseValidStudies(importString);
this.parseEC(importString);
this.attemptBuyAll();
} else {
this.plannedStudies = [];
this.invalidStudies = [];
this.ec = 0;
this.purchasedStudies = [];
this.ec = 0;
switch (typeof studies) {
case "string":
// Input parameter is an unparsed study import string
if (TimeStudyTree.isValidImportString(studies)) {
this.parseStudyImport(studies);
this.attemptBuyAll();
}
break;
case "object":
// Input parameter is an array of strings assumed to be already formatted as plannedStudies would be after
// import parsing.
this.plannedStudies = [...studies];
this.attemptBuyAll();
break;
}
}
@ -371,9 +376,21 @@ export class TimeStudyTree {
return /^(\d+|T\d+)(,(\d+|T\d+))*(\|\d+)?$/u.test(input);
}
// Takes this tree and imports the other tree on top of its state, returning a new composite tree with the studies of
// both trees together. Note the order of combination matters since one tree may fulfill requirements in the other.
// The only information taken from the other tree is studies; TT/ST counts are taken from this tree.
createCombinedTree(otherTree) {
const thisStudyList = [...this.purchasedStudies];
if (this.ec > 0) currentStudies.push(`EC${this.ec}`);
const compositeTree = new TimeStudyTree(thisStudyList, this.initialTheorems[0], this.initialTheorems[1]);
compositeTree.plannedStudies = compositeTree.plannedStudies.concat(otherTree.plannedStudies);
compositeTree.attemptBuyAll();
return compositeTree;
}
// This reads off all the studies in the import string and splits them into invalid and valid study IDs. We hold on
// to invalid studies for additional information to present to the player
parseValidStudies(input) {
parseStudyImport(input) {
const treeStudies = input.split("|")[0].split(",");
const normalIDs = GameDatabase.eternity.timeStudies.normal.map(s => s.id);
const triadIDs = GameDatabase.celestials.v.triadStudies.map(s => s.id);
@ -383,45 +400,44 @@ export class TimeStudyTree {
: normalIDs.includes(parseInt(str, 10))
);
let hasInvalidStudies = false;
for (const study of treeStudies) {
if (doesStudyExist(study)) this.plannedStudies.push(study);
else {
hasInvalidStudies = true;
this.invalidStudies.push(study);
}
else this.invalidStudies.push(study);
}
if (hasInvalidStudies) this.errors.push("String has invalid study IDs.");
}
// Parses out the EC from the import string, pushing an error message if it doesn't exist
parseEC(input) {
// If the string has an EC indicated in it, append that to the end of the study array
const ecString = input.split("|")[1];
if (!ecString) {
// Study strings without an ending "|##" are still valid, but will result in ecString being undefined
this.ec = 0;
return;
}
const ecID = parseInt(ecString, 10);
if (!GameDatabase.challenges.eternity.map(c => c.id).includes(ecID)) {
this.ec = 0;
this.errors.push(`Eternity Challenge ${ecID} does not exist.`);
const ecDB = GameDatabase.eternity.timeStudies.ec;
// Specifically exclude 0 because saved presets will contain it by default
if (!ecDB.map(c => c.id).includes(ecID) && ecID !== 0) {
this.invalidStudies.push(`${ecID}$`);
return;
}
this.ec = ecID;
this.plannedStudies.push(`EC${ecID}`);
}
// Attempt to purchase all studies specified in the initial import string
attemptBuyAll() {
const normalDB = GameDatabase.eternity.timeStudies.normal;
const ecDB = GameDatabase.eternity.timeStudies.ec;
const triadDB = GameDatabase.celestials.v.triadStudies;
const getStudyEntry = str => (
/^T\d+$/u.test(str)
? triadDB.find(s => s.id === parseInt(str.substr(1), 10))
: normalDB.find(s => s.id === parseInt(str, 10))
);
const getStudyEntry = str => {
const id = `${str}`.match(/(T|EC)?(\d+)/u);
switch (id[1]) {
case "T":
return triadDB.find(s => s.id === parseInt(id[2].substr(1), 10));
case "EC":
return ecDB.find(s => s.id === parseInt(id[2].substr(1), 10));
default:
return normalDB.find(s => s.id === parseInt(id[2], 10));
}
};
this.purchasedStudies = [];
for (const study of this.plannedStudies) {
const toBuy = getStudyEntry(study);
if (this.attemptBuySingle(toBuy)) this.purchasedStudies.push(study);
@ -432,6 +448,9 @@ export class TimeStudyTree {
// requirement is satisfied, then the running theorem costs will be updated (always) and the remaining usable
// theorems will be decremented (only if there are enough left to actually purchase)
attemptBuySingle(dbEntry) {
// Import strings can contain repeated or undefined entries
if (!dbEntry || this.purchasedStudies.includes(`${dbEntry.id}`)) return false;
const check = req => (typeof req === "number"
? this.purchasedStudies.includes(`${req}`)
: req());
@ -454,11 +473,19 @@ export class TimeStudyTree {
const stNeeded = dbEntry.STCost && dbEntry.requiresST.some(id => this.purchasedStudies.includes(id))
? dbEntry.STCost
: 0;
this.totalTT += dbEntry.cost;
this.totalST += stNeeded;
if (dbEntry.cost > this.usableTT || stNeeded > this.usableST) return false;
this.usableTT -= dbEntry.cost;
this.usableST -= stNeeded;
const canAfford = this.remainingTheorems[0] >= dbEntry.cost && this.remainingTheorems[1] >= stNeeded;
// We have to handle ECs slightly differently because you can only have one at once and merging trees may attempt
// to buy another. To distinguish between successful and failed purchases, we give failed ones a negative sign
if (dbEntry.secondary) {
if (this.ec !== 0) return false;
this.ec = canAfford ? dbEntry.id : -dbEntry.id;
}
this.runningCost[0] += dbEntry.cost;
this.runningCost[1] += stNeeded;
if (!canAfford) return false;
this.remainingTheorems[0] -= dbEntry.cost;
this.remainingTheorems[1] -= stNeeded;
return true;
}
@ -471,6 +498,36 @@ export class TimeStudyTree {
if (this.purchasedStudies.includes("201")) return 2;
return 1;
}
get firstSplitPaths() {
const pathSet = new Set();
const validPaths = [TIME_STUDY_PATH.ANTIMATTER_DIM, TIME_STUDY_PATH.INFINITY_DIM, TIME_STUDY_PATH.TIME_DIM];
for (const path of validPaths) {
const pathEntry = NormalTimeStudies.pathList.find(p => p.path === path);
for (const study of this.purchasedStudies) {
if (pathEntry.studies.includes(parseInt(study, 10))) {
pathSet.add(pathEntry.name);
break;
}
}
}
return Array.from(pathSet);
}
get secondSplitPaths() {
const pathSet = new Set();
const validPaths = [TIME_STUDY_PATH.ACTIVE, TIME_STUDY_PATH.PASSIVE, TIME_STUDY_PATH.IDLE];
for (const path of validPaths) {
const pathEntry = NormalTimeStudies.pathList.find(p => p.path === path);
for (const study of this.purchasedStudies) {
if (pathEntry.studies.includes(parseInt(study, 10))) {
pathSet.add(pathEntry.name);
break;
}
}
}
return Array.from(pathSet);
}
}
/**
@ -577,9 +634,9 @@ export class ECTimeStudyState extends TimeStudyState {
if (player.challenge.eternity.unlocked !== 0) {
return false;
}
const isConnectionSatisfied = this.incomingConnections
.some(connection => connection.isSatisfied);
if (!isConnectionSatisfied) {
// We'd have a switch case here if we wanted to generalize, but in our case it doesn't matter because all ECs have
// the same study restriction of type TS_REQUIREMENT_TYPE.AT_LEAST_ONE - so we just assume that behavior instead
if (!this.config.requirement.some(s => TimeStudy(s).isBought)) {
return false;
}
if (player.etercreq === this.id && this.id !== 11 && this.id !== 12) {
@ -599,11 +656,11 @@ export class ECTimeStudyState extends TimeStudyState {
}
get requirementTotal() {
return this.config.requirement.required(this.challenge.completions);
return this.config.secondary.required(this.challenge.completions);
}
get requirementCurrent() {
const current = this.config.requirement.current();
const current = this.config.secondary.current();
if (this.cachedCurrentRequirement === undefined) {
this.cachedCurrentRequirement = current;
} else if (typeof current === "number") {
@ -931,8 +988,5 @@ TimeStudy.allConnections = (function() {
[TimeStudy.timeDimension(8), TimeStudy.reality]
].map(props => new TimeStudyConnection(props[0], props[1], props[2]));
for (const connection of connections) {
connection.to.incomingConnections.push(connection);
}
return connections;
}());

View File

@ -5214,6 +5214,7 @@ screen and (max-width: 480px) {
.c-modal-import-tree {
width: 48rem;
word-break: break-word;
}
.l-modal-import-tree__tree-info-line {