@kitten-science/kitten-scientists
Version:
Add-on for the wonderful incremental browser game: https://kittensgame.com/web/
387 lines • 17.9 kB
JavaScript
import { isNil, mustExist } from "@oliversalzburg/js-utils/data/nil.js";
import { TimeControlSettings } from "./settings/TimeControlSettings.js";
import { objectEntries } from "./tools/Entries.js";
import { negativeOneToInfinity } from "./tools/Format.js";
import { Cycles, TimeItemVariant, } from "./types/index.js";
export class TimeControlManager {
_host;
settings;
_religionManager;
_workshopManager;
constructor(host, _bonfireManager, religionManager, workshopManager, settings = new TimeControlSettings()) {
this._host = host;
this.settings = settings;
this._religionManager = religionManager;
this._workshopManager = workshopManager;
}
async tick(_context) {
if (!this.settings.enabled) {
return;
}
if (this.settings.accelerateTime.enabled) {
this.accelerateTime();
}
if (this.settings.timeSkip.enabled) {
this.timeSkip();
}
if (this.settings.reset.enabled) {
await this.autoReset(this._host.engine);
}
}
async autoReset(engine) {
// Don't reset if there's a challenge running.
if (this._host.game.challenges.currentChallenge) {
return;
}
const checkedList = [];
// check building
for (const [name, entry] of objectEntries(this.settings.reset.bonfire.buildings)) {
if (!entry.enabled) {
continue;
}
// If the trigger for an item is set to infinity, it basically disables the entire feature.
if (entry.trigger < 0) {
return;
}
// TODO: Obvious error here. For upgraded buildings, it needs special handling.
let bld;
try {
// @ts-expect-error Obvious error here. For upgraded buildings, it needs special handling.
bld = this._host.game.bld.getBuildingExt(name);
}
catch (_error) {
bld = null;
}
if (isNil(bld)) {
continue;
}
checkedList.push({
name: bld.meta.label ?? mustExist(bld.meta.stages)[mustExist(bld.meta.stage)].label,
trigger: entry.trigger,
val: bld.meta.val,
});
// If the required amount of buildings hasn't been built yet, bail out.
if (bld.meta.val < entry.trigger) {
return;
}
}
// unicornPasture
// Special handling for unicorn pasture. As it's listed under religion, but is
// actually a bonfire item.
const unicornPasture = this.settings.reset.religion.buildings.unicornPasture;
if (unicornPasture.enabled) {
// If the trigger for an item is set to infinity, it basically disables the entire feature.
if (unicornPasture.trigger < 0) {
return;
}
const bld = this._host.game.bld.getBuildingExt("unicornPasture");
checkedList.push({
name: mustExist(bld.meta.label),
trigger: unicornPasture.trigger,
val: bld.meta.val,
});
if (bld.meta.val < unicornPasture.trigger) {
return;
}
}
// check space
// This is identical to regular buildings.
for (const [name, entry] of objectEntries(this.settings.reset.space.buildings)) {
if (!entry.enabled) {
continue;
}
// If the trigger for an item is set to infinity, it basically disables the entire feature.
if (entry.trigger < 0) {
return;
}
const bld = this._host.game.space.getBuilding(name);
checkedList.push({ name: bld.label, trigger: entry.trigger, val: bld.val });
if (bld.val < entry.trigger) {
return;
}
}
// check religion
for (const [name, entry] of objectEntries(this.settings.reset.religion.buildings)) {
if (!entry.enabled) {
continue;
}
// If the trigger for an item is set to infinity, it basically disables the entire feature.
if (entry.trigger < 0) {
return;
}
const bld = mustExist(this._religionManager.getUpgradeMeta(name, entry.variant));
checkedList.push({ name: bld.label, trigger: entry.trigger, val: bld.val });
if (bld.val < entry.trigger) {
return;
}
}
// check time
for (const [name, entry] of objectEntries(this.settings.reset.time.buildings)) {
if (!entry.enabled) {
continue;
}
// If the trigger for an item is set to infinity, it basically disables the entire feature.
if (entry.trigger < 0) {
return;
}
const bld = mustExist(this.getBuild(name, entry.variant));
checkedList.push({ name: bld.label, trigger: entry.trigger, val: bld.val });
if (bld.val < entry.trigger) {
return;
}
}
// check resources
for (const [name, entry] of objectEntries(this.settings.reset.resources.resources)) {
if (!entry.enabled) {
continue;
}
// If the trigger for an item is set to infinity, it basically disables the entire feature.
if (entry.trigger < 0) {
return;
}
const res = mustExist(this._host.game.resPool.get(name));
checkedList.push({
name: this._host.engine.i18n(`$resources.${entry.resource}.title`),
trigger: entry.trigger,
val: res.value,
});
if (res.value < entry.trigger) {
return;
}
}
// Check Workshop upgrades
for (const [, entry] of objectEntries(this.settings.reset.upgrades.upgrades)) {
if (entry.enabled) {
const upgrade = mustExist(this._host.game.workshop.upgrades.find(subject => subject.name === entry.upgrade));
checkedList.push({ name: upgrade.label, trigger: 1, val: upgrade.researched ? 1 : 0 });
if (!upgrade.researched) {
return;
}
}
}
if (checkedList.length === 0) {
return;
}
// We have now determined that we either have all items or could buy all items.
// stop!
engine.stop(false);
const sleep = async (time = 1500) => {
return new Promise((resolve, reject) => {
if (!this._host.engine.settings.enabled) {
reject(new Error("canceled by player"));
return;
}
setTimeout(resolve, time);
});
};
try {
for (const checked of checkedList) {
await sleep(500);
this._host.engine.imessage("reset.check", [
checked.name,
this._host.game.getDisplayValueExt(checked.trigger),
this._host.game.getDisplayValueExt(checked.val),
]);
}
await sleep(0);
this._host.engine.imessage("reset.checked");
await sleep();
this._host.engine.iactivity("reset.tip");
await sleep();
this._host.engine.imessage("reset.countdown.10");
await sleep(2000);
this._host.engine.imessage("reset.countdown.9");
await sleep();
this._host.engine.imessage("reset.countdown.8");
await sleep();
this._host.engine.imessage("reset.countdown.7");
await sleep();
this._host.engine.imessage("reset.countdown.6");
await sleep();
this._host.engine.imessage("reset.countdown.5");
await sleep();
this._host.engine.imessage("reset.countdown.4");
await sleep();
this._host.engine.imessage("reset.countdown.3");
await sleep();
this._host.engine.imessage("reset.countdown.2");
await sleep();
this._host.engine.imessage("reset.countdown.1");
await sleep();
this._host.engine.imessage("reset.countdown.0");
await sleep();
this._host.engine.iactivity("reset.last.message");
await sleep();
}
catch (_error) {
this._host.engine.imessage("reset.cancel.message");
this._host.engine.iactivity("reset.cancel.activity");
return;
}
//=============================================================
for (let challengeIndex = 0; challengeIndex < this._host.game.challenges.challenges.length; challengeIndex++) {
this._host.game.challenges.challenges[challengeIndex].pending = false;
}
this._host.game.resetAutomatic();
//=============================================================
}
accelerateTime() {
const temporalFluxAvailable = this._workshopManager.getValueAvailable("temporalFlux");
// If there's no available flux (we went below the limit)
if (temporalFluxAvailable <= 0) {
if (this._host.game.time.isAccelerated) {
// Stop the acceleration
this._host.game.time.isAccelerated = false;
}
return;
}
if (this._host.game.time.isAccelerated) {
return;
}
const temporalFlux = this._host.game.resPool.get("temporalFlux");
if (temporalFlux.maxValue * this.settings.accelerateTime.trigger <= temporalFlux.value) {
this._host.game.time.isAccelerated = true;
this._host.engine.iactivity("act.accelerate", [], "ks-accelerate");
this._host.engine.storeForSummary("accelerate", 1);
}
}
timeSkip() {
if (!this._host.game.workshop.get("chronoforge").researched) {
return;
}
// Don't time skip while we're in a temporal paradox.
if (this._host.game.calendar.day < 0) {
return;
}
// If we have less time crystals than our required trigger value, bail out.
const shatterCostIncreaseChallenge = this._host.game.getEffect("shatterCostIncreaseChallenge");
const timeCrystalsAvailable = this._workshopManager.getValueAvailable("timeCrystal");
if (timeCrystalsAvailable < this.settings.timeSkip.trigger ||
timeCrystalsAvailable < 1 + shatterCostIncreaseChallenge) {
return;
}
const shatterVoidCost = this._host.game.getEffect("shatterVoidCost");
const voidAvailable = this._workshopManager.getValueAvailable("void");
if (voidAvailable < shatterVoidCost) {
return;
}
// If skipping during this season was disabled, bail out.
const season = this._host.game.calendar.season;
if (!this.settings.timeSkip.seasons[this._host.game.calendar.seasons[season].name].enabled) {
return;
}
// If skipping during this cycle was disabled, bail out.
const currentCycle = this._host.game.calendar.cycle;
if (!this.settings.timeSkip.cycles[Cycles[currentCycle]].enabled) {
return;
}
// If we have too much stored heat, wait for it to cool down.
const heatMax = this._host.game.getEffect("heatMax");
const heatNow = this._host.game.time.heat;
if (!this.settings.timeSkip.ignoreOverheat.enabled) {
if (heatMax <= heatNow) {
return;
}
}
const factor = this._host.game.challenges.getChallenge("1000Years").researched ? 5 : 10;
let maxSkipsActiveHeatTransfer = Number.POSITIVE_INFINITY;
// Active Heat Transfer
if (!this.settings.timeSkip.ignoreOverheat.enabled &&
this.settings.timeSkip.activeHeatTransfer.enabled) {
const heatPerTick = this._host.game.getEffect("heatPerTick");
const ticksPerSecond = this._host.game.ticksPerSecond;
if (this.settings.timeSkip.activeHeatTransfer.activeHeatTransferStatus.enabled) {
// Heat Transfer to specified value
if (heatNow <= heatMax * this.settings.timeSkip.activeHeatTransfer.trigger) {
this.settings.timeSkip.activeHeatTransfer.activeHeatTransferStatus.enabled = false;
this._host.refreshEntireUserInterface();
this._host.engine.iactivity("act.time.activeHeatTransferEnd", [], "ks-timeSkip");
}
// Get temporalFlux
// TODO: More judgment(e.g. determining crystal cost)? Or should the players decide for themselves(Add options)?
const temporalFluxProduction = this._host.game.getEffect("temporalFluxProduction");
const daysPerYear = (this._host.game.calendar.daysPerSeason +
10 +
this._host.game.getEffect("temporalParadoxDay")) *
this._host.game.calendar.seasonsPerYear;
const ticksPerDay = this._host.game.calendar.ticksPerDay;
const daysPerTicks = (1 + this._host.game.timeAccelerationRatio()) / ticksPerDay;
const ticksPerYear = daysPerYear / daysPerTicks;
const temporalFlux = this._host.game.resPool.get("temporalFlux");
const fluxEnabled = temporalFlux.maxValue > ticksPerYear;
const flux = temporalFlux.value < ticksPerYear;
if (!season &&
this._host.game.calendar.day < 10 &&
temporalFluxProduction > factor / heatPerTick &&
this.settings.accelerateTime.enabled &&
fluxEnabled &&
flux) {
maxSkipsActiveHeatTransfer = Math.ceil((ticksPerYear + ticksPerDay * 10 - temporalFlux.value) / temporalFluxProduction);
this._host.engine.iactivity("act.time.getTemporalFlux", [], "ks-timeSkip");
this._host.engine.storeForSummary("time.getTemporalFlux", 1);
}
else if (this.settings.timeSkip.activeHeatTransfer.cycles[Cycles[currentCycle]].enabled) {
// Heat Transfer during selected cycles
return;
}
else {
maxSkipsActiveHeatTransfer =
this._host.game.calendar.yearsPerCycle - this._host.game.calendar.cycleYear;
}
}
else if (heatNow >= heatMax - heatPerTick * ticksPerSecond * 10) {
this.settings.timeSkip.activeHeatTransfer.activeHeatTransferStatus.enabled = true;
this._host.refreshEntireUserInterface();
this._host.engine.iactivity("act.time.activeHeatTransferStart", [], "ks-timeSkip");
this._host.engine.storeForSummary("time.activeHeatTransferStart", 1);
}
}
// The maximum years to skip, based on the user configuration.
const maxSkips = negativeOneToInfinity(this.settings.timeSkip.max);
// The amount of skips we can perform.
let canSkip = Math.floor(Math.min(this.settings.timeSkip.ignoreOverheat.enabled
? Number.POSITIVE_INFINITY
: (heatMax - heatNow) / factor, maxSkips, maxSkipsActiveHeatTransfer, timeCrystalsAvailable / (1 + shatterCostIncreaseChallenge), 0 < shatterVoidCost ? voidAvailable / shatterVoidCost : Number.POSITIVE_INFINITY));
// The amount of skips to perform.
let willSkip = 0;
const yearsPerCycle = this._host.game.calendar.yearsPerCycle;
const remainingYearsCurrentCycle = yearsPerCycle - this._host.game.calendar.cycleYear;
const cyclesPerEra = this._host.game.calendar.cyclesPerEra;
// If the cycle has more years remaining than we can even skip, skip all of them.
// I guess the idea here is to not skip through years of another cycle, if that
// cycle may not be enabled for skipping.
if (canSkip < remainingYearsCurrentCycle) {
willSkip = canSkip;
}
else {
willSkip += remainingYearsCurrentCycle;
canSkip -= remainingYearsCurrentCycle;
let skipCycles = 1;
while (yearsPerCycle < canSkip &&
this.settings.timeSkip.cycles[Cycles[((currentCycle + skipCycles) % cyclesPerEra)]].enabled) {
willSkip += yearsPerCycle;
canSkip -= yearsPerCycle;
skipCycles += 1;
}
if (this.settings.timeSkip.cycles[Cycles[((currentCycle + skipCycles) % cyclesPerEra)]].enabled &&
0 < canSkip) {
willSkip += canSkip;
}
}
// If we found we can skip any years, do so now.
if (0 < willSkip) {
const controller = new classes.ui.time.ShatterTCBtnController(this._host.game);
const model = controller.fetchModel({});
controller.doShatterAmt(model, willSkip);
this._host.engine.iactivity("act.time.skip", [willSkip], "ks-timeSkip");
this._host.engine.storeForSummary("time.skip", willSkip);
}
}
getBuild(name, variant) {
if (variant === TimeItemVariant.Chronoforge) {
return this._host.game.time.getCFU(name);
}
return this._host.game.time.getVSU(name);
}
}
//# sourceMappingURL=TimeControlManager.js.map