Refactor GMS class structure to allow multiple effects

This commit is contained in:
Andrei Andreev 2019-12-28 13:51:34 +03:00
parent 23b0f059a0
commit d79390e482
23 changed files with 408 additions and 292 deletions

View File

@ -108,7 +108,6 @@
"computed-property-spacing": "error",
"consistent-this": "error",
"func-call-spacing": "error",
"guard-for-in": "warn",
"id-blacklist": [
"error",
"ret",

View File

@ -94,14 +94,20 @@
<script type="text/javascript" src="javascripts/core/math.js"></script>
<script type="text/javascript" src="javascripts/core/async-utils.js"></script>
<script type="text/javascript" src="javascripts/core/game-mechanics/effect.js"></script>
<script type="text/javascript" src="javascripts/core/game-mechanics/effects.js"></script>
<script type="text/javascript" src="javascripts/core/game-mechanics/game-mechanic.js"></script>
<script type="text/javascript" src="javascripts/core/game-mechanics/puchasable.js"></script>
<script type="text/javascript" src="javascripts/core/game-mechanics/set-purchasable.js"></script>
<script type="text/javascript" src="javascripts/core/game-mechanics/bit-purchasable.js"></script>
<script type="text/javascript" src="javascripts/core/game-mechanics/rebuyable.js"></script>
<script type="text/javascript" src="javascripts/core/automator/automator-backend.js"></script>
<script type="text/javascript" src="javascripts/core/secret-formula/effects.js"></script>
<script type="text/javascript" src="javascripts/core/secret-formula/game-database.js"></script>
<script type="text/javascript" src="javascripts/core/glyph-effects.js"></script>
<script type="text/javascript" src="javascripts/core/player.js"></script>
<script type="text/javascript" src="javascripts/core/performance-stats.js"></script>
<script type="text/javascript" src="javascripts/core/currency.js"></script>
<script type="text/javascript" src="javascripts/core/game-mechanic.js"></script>
<script type="text/javascript" src="javascripts/core/event-hub.js"></script>
<script type="text/javascript" src="javascripts/core/cache.js"></script>
<script type="text/javascript" src="javascripts/core/intervals.js"></script>

View File

@ -3,13 +3,6 @@
class AchievementState extends GameMechanicState {
constructor(config) {
super(config);
if (config.secondaryEffect) {
const secondaryConfig = {
id: config.id,
effect: config.secondaryEffect
};
this._secondaryState = new AchievementState(secondaryConfig);
}
this._row = Math.floor(this.id / 10);
this._column = this.id % 10;
// eslint-disable-next-line no-bitwise
@ -73,16 +66,8 @@ class AchievementState extends GameMechanicState {
return this.isUnlocked;
}
get isEffectConditionSatisfied() {
return this.config.effectCondition === undefined || this.config.effectCondition();
}
get canBeApplied() {
return this.isEnabled && this.isEffectConditionSatisfied;
}
get secondaryEffect() {
return this._secondaryState;
get isEffectActive() {
return this.isUnlocked;
}
}

View File

@ -54,7 +54,7 @@ function bigCrunchReset() {
player.bestInfinitiesPerMs = player.bestInfinitiesPerMs.clampMin(
gainedInfinities().round().dividedBy(player.thisInfinityRealTime)
);
const earlyGame = player.bestInfinityTime > 60000 && !player.break;
const challenge = NormalChallenge.current || InfinityChallenge.current;
EventHub.dispatch(GameEvent.BIG_CRUNCH_BEFORE);
@ -139,7 +139,7 @@ class ChargedInfinityUpgradeState extends GameMechanicState {
this._upgrade = upgrade;
}
get canBeApplied() {
get isEffectActive() {
return this._upgrade.isBought && this._upgrade.isCharged;
}
}
@ -165,7 +165,7 @@ class InfinityUpgrade extends SetPurchasableMechanicState {
return this._requirement === undefined || this._requirement.isBought;
}
get canBeApplied() {
get isEffectActive() {
return this.isBought && !this.isCharged;
}
@ -215,7 +215,7 @@ function totalIPMult() {
Achievement(93),
Achievement(116),
Achievement(125),
Achievement(141),
Achievement(141).effects.ipGain,
InfinityUpgrade.ipMult,
DilationUpgrade.ipMultDT,
GlyphEffect.ipMult
@ -305,10 +305,6 @@ class InfinityIPMultUpgrade extends GameMechanicState {
return !this.isCapped && player.infinityPoints.gte(this.cost) && this.isRequirementSatisfied;
}
get canBeApplied() {
return true;
}
purchase(amount = 1) {
if (!this.canBeBought) return;
const costIncrease = this.costIncrease;

View File

@ -45,7 +45,7 @@ class AlchemyResourceState extends GameMechanicState {
return this.config.isUnlocked();
}
get canBeApplied() {
get hasCustomEffectValue() {
return true;
}

View File

@ -73,7 +73,7 @@ class CompressionUpgradeState extends BitPurchasableMechanicState {
return this.config.effectDisplay(this.config.effect());
}
get canBeApplied() {
get isEffectActive() {
// eslint-disable-next-line no-bitwise
const requirementFulfilled = new Decimal(this.config.resource()).gte(this.config.threshold()) ^
this.config.invertedCondition;

View File

@ -121,10 +121,6 @@ class PerkShopUpgradeState extends RebuyableMechanicState {
player.celestials.teresa.perkShop[this.id] = value;
}
get cap() {
return this.config.cap();
}
get isCapped() {
return this.cost === this.cap;
}

View File

@ -152,7 +152,7 @@ class InfinityChallengeRewardState extends GameMechanicState {
this._challenge = challenge;
}
get canBeApplied() {
get isEffectActive() {
return this._challenge.isCompleted;
}
}
@ -194,7 +194,7 @@ class InfinityChallengeState extends GameMechanicState {
player.challenge.infinity.completedBits |= 1 << this.id;
}
get canBeApplied() {
get isEffectActive() {
return this.isRunning;
}

View File

@ -26,7 +26,7 @@ function normalDimensionCommonMultiplier() {
Achievement(73),
Achievement(74),
Achievement(76),
Achievement(78),
Achievement(78).effects.dimensionMult,
Achievement(84),
Achievement(91),
Achievement(92),
@ -331,7 +331,7 @@ function buyMaxDimension(tier, bulk = Infinity, auto = false) {
return;
}
let buying = maxBought.quantity;
if (buying > bulkLeft) buying = bulkLeft;
if (buying > bulkLeft) buying = bulkLeft;
dimension.amount = dimension.amount.plus(10 * buying).round();
dimension.bought += 10 * buying;
dimension.currencyAmount = dimension.currencyAmount.minus(Decimal.pow10(maxBought.logPrice));
@ -596,9 +596,9 @@ const NormalDimensions = {
},
get buyTenMultiplier() {
if (NormalChallenge(7).isRunning) return new Decimal(2).min(1 + DimBoost.totalBoosts / 5);
let mult = new Decimal(2).plusEffectsOf(
Achievement(141).secondaryEffect,
Achievement(141).effects.buyTenMult,
EternityChallenge(3).reward
);
@ -606,9 +606,9 @@ const NormalDimensions = {
InfinityUpgrade.buy10Mult,
Achievement(58)
).times(getAdjustedGlyphEffect("powerbuy10"));
mult = mult.pow(getAdjustedGlyphEffect("effarigforgotten")).powEffectOf(InfinityUpgrade.buy10Mult.chargedEffect);
return mult;
}
};
@ -638,4 +638,4 @@ function produceAntimatter(diff) {
player.antimatter = player.antimatter.plus(amProduced);
player.totalAntimatter = player.totalAntimatter.plus(amProduced);
player.thisInfinityMaxAM = player.thisInfinityMaxAM.max(player.antimatter);
}
}

View File

@ -192,7 +192,7 @@ function eternityResetReplicanti() {
player.replicanti.gal = 0;
player.replicanti.galaxies = 0;
player.replicanti.galCost = new Decimal(1e170);
if (EternityMilestone.autobuyerReplicantiGalaxy.isReached &&
if (EternityMilestone.autobuyerReplicantiGalaxy.isReached &&
player.replicanti.galaxybuyer === undefined) player.replicanti.galaxybuyer = false;
}
@ -281,10 +281,6 @@ class EPMultiplierState extends GameMechanicState {
this.cachedEffectValue = new Lazy(() => Decimal.pow(5, player.epmultUpgrades));
}
get canBeApplied() {
return true;
}
get isAffordable() {
return player.eternityPoints.gte(this.cost);
}
@ -305,6 +301,10 @@ class EPMultiplierState extends GameMechanicState {
Autobuyer.eternity.bumpAmount(Decimal.pow(5, diff));
}
get hasCustomEffectValue() {
return true;
}
get effectValue() {
return this.cachedEffectValue.value;
}

View File

@ -31,11 +31,15 @@ class EternityChallengeRewardState extends GameMechanicState {
this._challenge = challenge;
}
get hasCustomEffectValue() {
return true;
}
get effectValue() {
return this.config.effect(this._challenge.completions);
}
get canBeApplied() {
get isEffectActive() {
return this._challenge.completions > 0;
}
}
@ -59,7 +63,7 @@ class EternityChallengeState extends GameMechanicState {
return player.challenge.eternity.current === this.id;
}
get canBeApplied() {
get isEffectActive() {
return this.isRunning;
}

View File

@ -1,229 +0,0 @@
"use strict";
/**
* @abstract
*/
class GameMechanicState {
constructor(config) {
if (!config) throw new Error("Must specify config for GameMechanicState");
this.config = config;
if (typeof this.config.effect === "number" || this.config.effect instanceof Decimal) {
Object.defineProperty(this, "effectValue", {
configurable: false,
writable: false,
value: this.config.effect,
});
if (this.config.cap === undefined) {
Object.defineProperty(this, "cappedEffectValue", {
configurable: false,
writable: false,
value: this.config.effect,
});
}
}
}
get id() {
return this.config.id;
}
get effectValue() {
return this.config.effect();
}
get cappedEffectValue() {
const effectValue = this.effectValue;
if (this.config.cap === undefined) return effectValue;
const cap = typeof this.config.cap === "function"
? this.config.cap()
: this.config.cap;
if (cap === undefined) return effectValue;
return typeof effectValue === "number"
? Math.min(effectValue, cap)
: Decimal.min(effectValue, cap);
}
effectOrDefault(defaultValue) {
return this.canBeApplied ? this.cappedEffectValue : defaultValue;
}
get canBeApplied() {
return false;
}
applyEffect(applyFn) {
if (this.canBeApplied) applyFn(this.cappedEffectValue);
}
static createAccessor(gameData) {
const index = mapGameData(gameData, config => new this(config));
const accessor = id => index[id];
accessor.index = index;
return accessor;
}
}
/**
* @abstract
*/
class PurchasableMechanicState extends GameMechanicState {
/**
* @abstract
*/
get currency() { throw new NotImplementedError(); }
get isAffordable() {
return this.currency.isAffordable(this.cost);
}
get isAvailable() {
return true;
}
get isRebuyable() {
return false;
}
get cost() {
return this.config.cost;
}
/**
* @abstract
*/
get isBought() { throw new NotImplementedError(); }
/**
* @abstract
*/
set isBought(value) { throw new NotImplementedError(); }
get canBeBought() {
return !this.isBought && this.isAffordable && this.isAvailable;
}
purchase() {
if (!this.canBeBought) return false;
this.currency.subtract(this.cost);
this.isBought = true;
GameUI.update();
return true;
}
get canBeApplied() {
return this.isBought;
}
}
/**
* @abstract
*/
class SetPurchasableMechanicState extends PurchasableMechanicState {
/**
* @abstract
*/
get set() { throw new NotImplementedError(); }
get isBought() {
return this.set.has(this.id);
}
set isBought(value) {
if (value) {
this.set.add(this.id);
} else {
this.set.delete(this.id);
}
}
}
/**
* @abstract
*/
class BitPurchasableMechanicState extends PurchasableMechanicState {
/**
* @abstract
*/
get bits() { throw new NotImplementedError(); }
/**
* @abstract
*/
set bits(value) { throw new NotImplementedError(); }
/**
* @abstract
*/
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);
}
}
}
/**
* @abstract
*/
class RebuyableMechanicState extends GameMechanicState {
/**
* @abstract
*/
get currency() { throw new NotImplementedError(); }
get isAffordable() {
return this.currency.isAffordable(this.cost);
}
get cost() {
return this.config.cost();
}
get isAvailable() {
return true;
}
get isCapped() {
return false;
}
get isRebuyable() {
return true;
}
/**
* @abstract
*/
get boughtAmount() { throw new NotImplementedError(); }
/**
* @abstract
*/
set boughtAmount(value) { throw new NotImplementedError(); }
get canBeApplied() {
return this.boughtAmount > 0;
}
get canBeBought() {
return this.isAffordable && this.isAvailable && !this.isCapped;
}
purchase() {
if (!this.canBeBought) return false;
this.currency.subtract(this.cost);
this.boughtAmount++;
GameUI.update();
return true;
}
}

View File

@ -0,0 +1,36 @@
"use strict";
/**
* @abstract
*/
class BitPurchasableMechanicState extends PurchasableMechanicState {
/**
* @abstract
*/
get bits() { throw new NotImplementedError(); }
/**
* @abstract
*/
set bits(value) { throw new NotImplementedError(); }
/**
* @abstract
*/
get bitIndex() { throw new NotImplementedError(); }
get isBought() {
// eslint-disable-next-line no-bitwise
return (this.bits & (1 << this.bitIndex)) !== 0;
}
set isBought(value) {
if (value) {
// eslint-disable-next-line no-bitwise
this.bits |= (1 << this.bitIndex);
} else {
// eslint-disable-next-line no-bitwise
this.bits &= ~(1 << this.bitIndex);
}
}
}

View File

@ -0,0 +1,135 @@
"use strict";
class Effect {
get effectValue() {
throw new Error("Effect is undefined.");
}
get uncappedEffectValue() {
throw new Error("Effect is undefined.");
}
get cap() {
throw new Error("Cap is undefined.");
}
get isEffectConditionSatisfied() {
return true;
}
get isEffectActive() {
return true;
}
get canBeApplied() {
return this.isEffectActive && this.isEffectConditionSatisfied;
}
effectOrDefault(defaultValue) {
return this.canBeApplied ? this.effectValue : defaultValue;
}
applyEffect(applyFn) {
if (this.canBeApplied) applyFn(this.effectValue);
}
// eslint-disable-next-line max-params
defineEffect(value, cap, condition) {
const isFunction = v => typeof v === "function";
const isNumber = v => typeof v === "number";
const isDecimal = v => v instanceof Decimal;
const isConstant = v => isNumber(v) || isDecimal(v);
if (!isFunction(value) && !isConstant(value)) {
throw new Error("Unknown effect value type.");
}
const createProperty = () => ({
configurable: false
});
const addGetter = (property, v) => {
if (isConstant(v)) {
property.writable = false;
property.value = v;
} else if (isFunction(v)) {
property.get = v;
} else {
throw new Error("Unknown getter type.");
}
};
if (condition !== undefined) {
if (!isFunction(condition)) {
throw new Error("Effect condition must be a function.");
}
const conditionProperty = createProperty();
conditionProperty.get = condition;
Object.defineProperty(this, "isEffectConditionSatisfied", conditionProperty);
}
const uncappedEffectValueProperty = createProperty();
addGetter(uncappedEffectValueProperty, value);
Object.defineProperty(this, "uncappedEffectValue", uncappedEffectValueProperty);
if (cap !== undefined) {
const capProperty = createProperty();
addGetter(capProperty, cap);
Object.defineProperty(this, "cap", capProperty);
}
const effectValueProperty = createProperty();
addGetter(effectValueProperty, value);
if (isConstant(cap)) {
if (isNumber(value)) {
effectValueProperty.get = () => Math.min(value, this.cap);
} else if (isDecimal(value)) {
effectValueProperty.get = () => Decimal.min(value, this.cap);
} else if (isFunction(value)) {
// Postpone effectValue specialization until the first call
effectValueProperty.configurable = true;
effectValueProperty.get = () => {
const effectValue = value();
const specializedProperty = createProperty();
if (isNumber(effectValue)) {
specializedProperty.get = () => Math.min(value(), this.cap);
} else if (isDecimal(effectValue)) {
effectValueProperty.get = () => Decimal.min(value(), this.cap);
} else {
throw new Error("Unknown effect value type.");
}
Object.defineProperty(this, "effectValue", specializedProperty);
return specializedProperty.get();
};
}
} else if (isFunction(cap)) {
if (isNumber(value)) {
effectValueProperty.get = () => {
const capValue = this.cap;
return capValue === undefined ? value : Math.min(value, capValue);
};
} else if (isDecimal(value)) {
effectValueProperty.get = () => {
const capValue = this.cap;
return capValue === undefined ? value : Decimal.min(value, capValue);
};
} else if (isFunction(value)) {
// Postpone effectValue specialization until the first call
effectValueProperty.configurable = true;
effectValueProperty.get = () => {
const effectValue = value();
const specializedProperty = createProperty();
if (isNumber(effectValue)) {
specializedProperty.get = () => {
const capValue = this.cap;
return capValue === undefined ? value() : Math.min(value(), capValue);
};
} else if (isDecimal(effectValue)) {
specializedProperty.get = () => {
const capValue = this.cap;
return capValue === undefined ? value() : Decimal.min(value(), capValue);
};
} else {
throw new Error("Unknown effect value type.");
}
Object.defineProperty(this, "effectValue", specializedProperty);
return specializedProperty.get();
};
}
}
Object.defineProperty(this, "effectValue", effectValueProperty);
}
}

View File

@ -0,0 +1,51 @@
"use strict";
/**
* @abstract
*/
class GameMechanicState extends Effect {
constructor(config) {
super();
if (!config) throw new Error("Must specify config for GameMechanicState");
this._config = config;
if (config.effect !== undefined && !this.hasCustomEffectValue) {
this.defineEffect(config.effect, config.cap, config.effectCondition);
}
if (config.effects !== undefined) {
this.effects = {};
for (const key in config.effects) {
const effect = new Effect();
const value = config.effects[key];
if (typeof value === "number" || value instanceof Decimal) {
effect.defineEffect(value);
} else {
effect.defineEffect(value.effect, value.cap, value.effectCondition);
}
Object.defineProperty(effect, "isEffectActive", {
configurable: false,
get: () => this.isEffectActive
});
this.effects[key] = effect;
}
}
}
get config() {
return this._config;
}
get id() {
return this.config.id;
}
get hasCustomEffectValue() {
return false;
}
static createAccessor(gameData) {
const index = mapGameData(gameData, config => new this(config));
const accessor = id => index[id];
accessor.index = index;
return accessor;
}
}

View File

@ -0,0 +1,53 @@
"use strict";
/**
* @abstract
*/
class PurchasableMechanicState extends GameMechanicState {
/**
* @abstract
*/
get currency() { throw new NotImplementedError(); }
get isAffordable() {
return this.currency.isAffordable(this.cost);
}
get isAvailable() {
return true;
}
get isRebuyable() {
return false;
}
get cost() {
return this.config.cost;
}
/**
* @abstract
*/
get isBought() { throw new NotImplementedError(); }
/**
* @abstract
*/
set isBought(value) { throw new NotImplementedError(); }
get canBeBought() {
return !this.isBought && this.isAffordable && this.isAvailable;
}
purchase() {
if (!this.canBeBought) return false;
this.currency.subtract(this.cost);
this.isBought = true;
GameUI.update();
return true;
}
get isEffectActive() {
return this.isBought;
}
}

View File

@ -0,0 +1,57 @@
"use strict";
/**
* @abstract
*/
class RebuyableMechanicState extends GameMechanicState {
/**
* @abstract
*/
get currency() { throw new NotImplementedError(); }
get isAffordable() {
return this.currency.isAffordable(this.cost);
}
get cost() {
return this.config.cost();
}
get isAvailable() {
return true;
}
get isCapped() {
return false;
}
get isRebuyable() {
return true;
}
/**
* @abstract
*/
get boughtAmount() { throw new NotImplementedError(); }
/**
* @abstract
*/
set boughtAmount(value) { throw new NotImplementedError(); }
get isEffectActive() {
return this.boughtAmount > 0;
}
get canBeBought() {
return this.isAffordable && this.isAvailable && !this.isCapped;
}
purchase() {
if (!this.canBeBought) return false;
this.currency.subtract(this.cost);
this.boughtAmount++;
GameUI.update();
return true;
}
}

View File

@ -0,0 +1,23 @@
"use strict";
/**
* @abstract
*/
class SetPurchasableMechanicState extends PurchasableMechanicState {
/**
* @abstract
*/
get set() { throw new NotImplementedError(); }
get isBought() {
return this.set.has(this.id);
}
set isBought(value) {
if (value) {
this.set.add(this.id);
} else {
this.set.delete(this.id);
}
}
}

View File

@ -574,7 +574,7 @@ const Player = {
Achievement(37),
Achievement(54),
Achievement(55),
Achievement(78).secondaryEffect
Achievement(78).effects.antimatter
).toDecimal();
},

View File

@ -726,9 +726,7 @@ const Glyphs = {
}
};
class GlyphSacrificeState extends GameMechanicState {
get canBeApplied() { return true; }
}
class GlyphSacrificeState extends GameMechanicState { }
const GlyphSacrifice = (function() {
const db = GameDatabase.reality.glyphSacrifice;

View File

@ -461,9 +461,13 @@ GameDatabase.achievements.normal = [
checkEvent: GameEvent.BIG_CRUNCH_BEFORE,
reward: () => `Start with ${shorten(2e25, 0, 0)} antimatter ` +
`and all Dimensions are stronger in the first ${shortenSmallInteger(300)}ms of Infinities.`,
effect: () => 330 / (Time.thisInfinity.totalMilliseconds + 30),
effectCondition: () => Time.thisInfinity.totalMilliseconds < 300,
secondaryEffect: () => 2e25
effects: {
dimensionMult: {
effect: () => 330 / (Time.thisInfinity.totalMilliseconds + 30),
effectCondition: () => Time.thisInfinity.totalMilliseconds < 300,
},
antimatter: 2e25
}
},
{
id: 81,
@ -902,8 +906,10 @@ GameDatabase.achievements.normal = [
checkEvent: GameEvent.REALITY_RESET_BEFORE,
reward: () => `${shortenSmallInteger(4)}x IP gain and boost from
buying ${shortenSmallInteger(10)} Dimensions +${shorten(0.1, 0, 1)}.`,
effect: 4,
secondaryEffect: () => 0.1
effects: {
ipGain: 4,
buyTenMult: 0.1
}
},
{
id: 142,

View File

@ -165,8 +165,8 @@ function canBuyStudy(id) {
}
function canBuyLocked(id) {
return V.availableST >= TimeStudy(id).STCost &&
TimeStudy(id) &&
return V.availableST >= TimeStudy(id).STCost &&
TimeStudy(id) &&
TimeStudy(id).checkVRequirement();
}
@ -392,7 +392,7 @@ class NormalTimeStudyState extends TimeStudyState {
return canBuyStudy(this.id) || canBuyLocked(this.id);
}
get canBeApplied() {
get isEffectActive() {
return this.isBought;
}
@ -655,7 +655,7 @@ class TriadStudyState extends TimeStudyState {
return this.config.description;
}
get canBeApplied() {
get isEffectActive() {
return this.isBought;
}