Ex­po­nen­tial Idle Guides

Day 7: Pol­ish­ing

Guide writ­ten by prop. Con­tri­bu­tions from the Amaz­ing Com­munity.

Feel free to use the gloss­ary as needed.

Hey. We’re nearly there. Let’s fin­ish this the­ory today with some pol­ish­ing touches. Today we’re go­ing to learn how to im­ple­ment achieve­ments, dis­play story chapters, switch screens, and more.

Achieve­ments #

Achieve­ments are ways to mark your pro­gress, as well as provide the player with ex­tra chal­lenges. Achieve­ments in a cus­tom the­ory are ac­cess­ible through the the­ory’s in­form­a­tion panel. Click the ‘two ar­rows’ icon on the top-left, scroll down un­til you see ‘My The­ory’, then click on the cog. You will see the the­ory’s de­scrip­tion, au­thor, and source. Right be­low the in­form­a­tion panel are two but­tons: De­lete and Re­set. The Achieve­ments but­ton will ap­pear here after we im­ple­ment our first.

An achieve­ment is defined us­ing the cre­ateAchieve­ment() func­tion, and its ana­tomy goes as fol­lows:

Let’s define our first one in­side of init(), be­fore the call to up­dateAvail­ab­il­ity(). This one will make us wait 20 minutes be­fore it un­locks auto­mat­ic­ally, by check­ing our t vari­able:

let patienceAch;

let init = () =>
{
    ...

    patienceAch = theory.createAchievement(0, null, 'Patience', 'Wait for 20 minutes.', () => t >= 1200, () => Math.max(0, Math.min(t / 1200, 1)));

    ...
}

Now, let’s re­load our the­ory, and then wait, wait,… Wait. It’s triggered already? Well I sup­pose us hu­mans are quite pa­tient in­deed. If you now go to the the­ory’s in­form­a­tion panel, you will be able to check out this new achieve­ment. To the right, you can see a star if you’ve un­locked it, and a pro­gress per­cent­age if you haven’t. If you click on it, you can also see the un­lock date (in case it’s un­locked).

Next, let’s im­ple­ment a secret achieve­ment, ‘Lewd’, this time within a cat­egory, ‘Lewd’. First, let’s cre­ate the cat­egory, spe­cify­ing its iden­ti­fier and name:

let lewdAchCat;

let init = () =>
{
    ...

    lewdAchCat = theory.createAchievementCategory(0, 'Lewd');

    ...
}

With the cat­egory defined, let’s define our secret achieve­ment be­long­ing to the cat­egory. Note that in­stead of a pro­gress in­dic­ator, secret achieve­ments can have a hint when you check it out, and this para­meter sits between the de­scrip­tion and the un­lock con­di­tion. Let’s write the hint as ‘Nice’, be­cause every­one will un­der­stand the ref­er­ence. I know, this won’t be much of a secret then.

let lewdAch;

let init = () =>
{
    ...

    lewdAch = theory.createSecretAchievement(1, lewdAchCat, 'Lewd', 'Reach sixty and nine levels on the clicker.', 'Nice', () => clicker.level >= 69);

    ...
}

You know what to do with this one. Re­mem­ber that the un­lock con­di­tion is checked only once per second, so set­ting it up ex­actly as clicker.level == 69 makes it much harder to un­lock, due to auto­buy and tim­ing is­sues.

Now, if you go to the SDK’s com­mand line and type in this, you will be able to check whether or not this achieve­ment is un­locked:

log(lewdAch.isUnlocked);

By us­ing the isUn­locked prop­erty, we can also add more re­quire­ments to an achieve­ment, by mak­ing it de­pend on an­other achieve­ment to be un­locked. Go wild.

Story Chapters #

Story chapters are dia­logues that can pop up throughout the game ac­cord­ing to your pro­gres­sion. They’re quite sim­ilar to achieve­ments in this sense, and use cre­ateSt­oryChapter() to de­clare. Com­pared to achieve­ments, they are much sim­pler to de­clare, and can’t be made secret:

Let’s cre­ate an ex­ample achieve­ment that un­locks when the q un­lock mile­stone is pur­chased:

let qChap;

let init = () =>
{
    ...

    qChap = theory.createStoryChapter(0, 'Queued up', 'The theory is now queued up.\n\nGet it?\nQ, the letter?\n\nAnyway.', () => qMs.level > 0);

    ...
}

Voilà! Now if you check the game… Be­hold, pass­able writ­ing! Okay, now check out the new but­ton for Story Chapters that just popped up in the the­ory’s in­form­a­tion panel. Click on the chapter to re­play it. Be­hold, pass­able writ­ing.

Sim­ilar to achieve­ments, you can also check whether or not this story chapter is un­locked from the SDK:

log(qChap.isUnlocked);

Stages #

To re­cap what we’ve known about the UI up to this point, the equa­tion screen con­sists of four parts: the primary, sec­ond­ary, and ter­tiary equa­tions, and the qua­tern­ary entries. But what if we wanted more space to write in­form­a­tion? The The­ory API gives us a tool that al­lows us to cycle between dif­fer­ent screens, so we can dis­play all the in­form­a­tion we want. These screens are called stages. In the base game, stages are be­ing used ex­tens­ively in the Con­ver­gence Test, not only to switch between the seven tests, but even ad­apt the tick() func­tion to run the cur­rently dis­played test. However, in this guide, we will only be us­ing stages to ma­nip­u­late dis­played equa­tions.

First, let’s define our stage counter, and re­gister it in the in­ternal state:

let stage = 0;

var getInternalState = () => JSON.stringify
({
    ...
    stage
});

var setInternalState = (stateStr) =>
{
    ...
    stage = state.stage ?? stage;
}

This won’t do any­thing just yet. In the The­ory API, there are four func­tions to con­trol the stages: can­Go­To­Previ­ousStage(), go­To­Previ­ousStage(), can­Go­ToNextStage(), and go­ToNextStage(). The can­GoTo func­tions con­trol the vis­ib­il­ity of the stage switch­ing ar­rows on screen, while the goTo func­tions con­trol their be­ha­viour when clicked. Let’s cre­ate a stage sys­tem us­ing two stages, 0 and 1:

var canGoToPreviousStage = () => stage > 0;

var goToPreviousStage = () => stage -= 1;

var canGoToNextStage = () => stage < 1;

var goToNextStage = () => stage += 1;

In this ex­ample, when you’re on stage 0, the left ar­row is un­avail­able, and click­ing the right ar­row will add 1 to the stage counter. And on stage 1, like­wise, the right ar­row is un­avail­able, and click­ing the left ar­row will sub­tract 1 from the stage counter.

Click­ing these ar­rows do noth­ing to af­fect the equa­tions yet, so let’s im­ple­ment that. Since we don’t ac­tu­ally need space for new equa­tions, let’s mangle our ex­ist­ing ones by swap­ping their sides. To ad­apt equa­tions ac­cord­ing to the stage counter, we can use a simple if-else clause within the equa­tions:

var getPrimaryEquation = () =>
{
    if(stage == 0)
        return `\\dot{\\rho} = c_1${c1ExpMs.level ? `^{${getc1Exp(c1ExpMs.level)}}` : ''}c_2(1+f)${qMs.level ? 'q' : ''}`;
    else
        return `c_1${c1ExpMs.level ? `^{${getc1Exp(c1ExpMs.level)}}` : ''}c_2(1+f)${qMs.level ? 'q' : ''} = \\dot{\\rho}`;
}

var getSecondaryEquation = () =>
{
    if(stage == 0)
        return `${theory.latexSymbol} = \\max\\rho`;
    else
        return `\\max\\rho = ${theory.latexSymbol}`;
}

var getTertiaryEquation = () =>
{
    if(stage == 0)
        return qMs.level ? `q = ${q.toString()}` : '';
    else
        return qMs.level ? `${q.toString()} = q` : '';
}

But wait. We no­tice that aside from the ter­tiary equa­tion, the other two do not get up­dated when we switch stages. Re­mem­ber that these equa­tions don’t up­date by them­selves, and we must manu­ally in­val­id­ate them. The best way to do so is from within the two goTo func­tions, so that we only do it once when the ar­rows are clicked:


var goToPreviousStage = () =>
{
    stage -= 1;
    theory.invalidatePrimaryEquation();
    theory.invalidateSecondaryEquation();
}

...

var goToNextStage = () =>
{
    stage += 1;
    theory.invalidatePrimaryEquation();
    theory.invalidateSecondaryEquation();
}

Yes! We did it! We man­aged to make this the­ory so much more sil­lier[^1].

Sin­gu­lar Up­grades #

An­other API fea­ture that’s in­tro­duced within the Con­ver­gence Test is the sin­gu­lar up­grade. This takes the form of the Prove lemma but­ton, and al­lows us to pin some up­grades to the top for con­veni­ence[^2].

Set­ting up a sin­gu­lar up­grade is easy - it is done in the same way as nor­mal up­grades, us­ing the cre­ateSin­gu­larUp­grade() func­tion. For ex­ample, we would like to con­vert the clicker up­grade to a sin­gu­lar up­grade. Simply change cre­ateUp­grade to cre­ateSin­gu­larUp­grade like so:

let init = () =>
{
    ...

    {
        clicker = theory.createSingularUpgrade(0, currency, new FreeCost);
        ...
    }

    ...
}

Note that sin­gu­lar, nor­mal, per­man­ent, and mile­stone up­grades don’t share any com­mon iden­ti­fier space, and you can use the same num­ber for dif­fer­ent kinds of up­grades. We don’t need to change any­thing on this front.

Now, re­load the game. We no­tice that the clicker has moved above the reg­u­lar up­grades. The auto­buyer also no longer levels the clicker, nor can pub­lish­ing re­set its level. This makes it use­ful if you want up­grades that don’t get tampered by the auto­buyer or pub­lic­a­tions.

Ex­tra As­sign­ments #

Let’s do something with our newly ac­quired know­ledge:

  1. Cre­ate an achieve­ment with its own cat­egory. It can be about any­thing, but one of the re­quire­ments is that the ‘Lewd’ achieve­ment has to be un­locked.
  2. Cre­ate a story chapter that re­quires the first chapter to be un­locked.
  3. Im­ple­ment a new sin­gu­lar up­grade that re­sets the qua­tern­ary timer.

Af­ter­math #

Today, we have learned about what makes a the­ory silly and pol­ished, with a story, goals to achieve, and proper Qual­ity of Life™ im­prove­ments. Thank you for par­ti­cip­at­ing in this ritual. Don’t for­get to give your of­fer­ings to Io­sui, and I hope you will join us an­other week for more ad­vanced rituals.

Mean­while, the source code after today’s work can be found here. This code does not con­tain solu­tions to as­sign­ments.

import { BigNumber } from '../api/BigNumber';
import { CompositeCost, ExponentialCost, FreeCost, LinearCost } from '../api/Costs';
import { Localization } from '../api/Localization';
import { QuaternaryEntry, theory } from '../api/Theory';
import { Utils } from '../api/Utils';

var id = 'my_theory';
var name = 'My Theory';
var description = 'The one and only.';
var authors = 'Stuart Clickus';

let currency;
let clicker;
let c1, c2;
let f;
let qdot;
let c1ExpMs, qMs, qdotMs;
let lewdAchCat;
let patienceAch, lewdAch;
let qChap;

let q = BigNumber.ONE;
let t = 0;
let stage = 0;

let quaternary =
[
    new QuaternaryEntry('h', null),
    new QuaternaryEntry('m', null),
    new QuaternaryEntry('s', null)
];

let init = () =>
{
    currency = theory.createCurrency();

    {
        clicker = theory.createSingularUpgrade(0, currency, new FreeCost);
        clicker.description = Utils.getMath('\\rho \\leftarrow \\rho + 1');
        clicker.info = 'Increases currency by 1';
        clicker.bought = (amount) => currency.value += 1;
    }

    {
        c1 = theory.createUpgrade(1, currency, new ExponentialCost(10, 1));
        let getDesc = (level) => `c_1 = ${getc1(level).toString(0)}`;
        let getInfo = (level) =>
        {
            if(c1ExpMs.level)
                return `c_1^{${getc1Exp(c1ExpMs.level)}}=
                ${getc1(level).pow(getc1Exp(c1ExpMs.level)).toString()}`;
            return getDesc(level);
        }

        c1.getDescription = (amount) => Utils.getMath(getDesc(c1.level));
        c1.getInfo = (amount) => Utils.getMathTo(getInfo(c1.level),
        getInfo(c1.level + amount));
    }

    {
        c2 = theory.createUpgrade(3, currency, new ExponentialCost(500, 3));
        let getDesc = (level) => `c_2 = ${getc2(level).toString(0)}`;
        c2.getDescription = (amount) => Utils.getMath(`c_2 = 2^{${c2.level}}`);
        c2.getInfo = (amount) => Utils.getMathTo(getDesc(c2.level),
        getDesc(c2.level + amount));
    }

    {
        f = theory.createUpgrade(2, currency, new CompositeCost(30,
        new ExponentialCost(100, 1.618034),
        new ExponentialCost(1e16, 1.618034 * 1.5)));
        let getDesc = (level) => `f = ${getf(level).toString(0)}`;
        f.getDescription = (amount) => Utils.getMath(getDesc(f.level));
        f.getInfo = (amount) => Utils.getMathTo(getDesc(f.level),
        getDesc(f.level + amount));
    }

    {
        qdot = theory.createUpgrade(4, currency, new ExponentialCost(1e45, 2.5));
        let getDesc = (level) => `\\dot{q} = ${getqdot(level).toString(0)}`;
        qdot.getDescription = (amount) => Utils.getMath(getDesc(qdot.level));
        qdot.getInfo = (amount) => Utils.getMathTo(getDesc(qdot.level),
        getDesc(qdot.level + amount));
    }

    theory.createPublicationUpgrade(0, currency, BigNumber.from('1e7'));
    theory.createBuyAllUpgrade(1, currency, BigNumber.from('1e12'));
    theory.createAutoBuyerUpgrade(2, currency, BigNumber.from('1e17'));
    
    theory.setMilestoneCost(new LinearCost(15, 15));

    {
        c1ExpMs = theory.createMilestoneUpgrade(0, 5);
        c1ExpMs.description = Localization.getUpgradeIncCustomExpDesc('c_1', '0.03');
        c1ExpMs.info = Localization.getUpgradeIncCustomExpInfo('c_1', '0.03');
        c1ExpMs.boughtOrRefunded = (_) => theory.invalidatePrimaryEquation();
    }

    {
        qMs = theory.createMilestoneUpgrade(1, 1);
        qMs.description = Localization.getUpgradeAddTermDesc('q');
        qMs.info = Localization.getUpgradeAddTermInfo('q');
        qMs.boughtOrRefunded = (_) =>
        {
            theory.invalidatePrimaryEquation();
            updateAvailability();
        }
        qMs.canBeRefunded = () => qdotMs.level == 0;
    }

    {
        qdotMs = theory.createMilestoneUpgrade(2, 3);
        qdotMs.description = Localization.getUpgradeMultCustomDesc('\\dot{q}', '2');
        qdotMs.info = Localization.getUpgradeMultCustomInfo('\\dot{q}', '2');
        qdotMs.isAvailable = false;
    }

    patienceAch = theory.createAchievement(0, null, 'Patience', 'Wait for 20 minutes.', () => t >= 1200, () => Math.max(0, Math.min(t / 1200, 1)));

    lewdAchCat = theory.createAchievementCategory(0, 'Lewd');
    lewdAch = theory.createSecretAchievement(1, lewdAchCat, 'Lewd', 'Reach sixty and nine levels on the clicker.', 'Nice', () => clicker.level >= 69);

    qChap = theory.createStoryChapter(0, 'Queued up', 'The theory is now queued up.\n\nGet it?\nQ, the letter?\n\nAnyway.', () => qMs.level > 0);

    updateAvailability();
}

var updateAvailability = () =>
{
    qdot.isAvailable = qMs.level > 0;
    qdotMs.isAvailable = qMs.level > 0;
};

let getc1 = (level) => Utils.getStepwisePowerSum(level, 2, 5, 0);
let getc1Exp = (level) => 1 + 0.03 * level;

let getc2 = (level) => BigNumber.TWO.pow(level);

let getqdot = (level) => Utils.getStepwisePowerSum(level, 2, 10, 0) * Math.pow(2, qdotMs.level);

const fibSqrt5 = BigNumber.FIVE.sqrt();
const fibA = (BigNumber.ONE + fibSqrt5) / BigNumber.TWO;
const fibB = (fibSqrt5 - BigNumber.ONE) / BigNumber.TWO;

let getf = (level) =>
{
    if(level % 2 == 0)
        return (fibA.pow(level) - fibB.pow(level)) / fibSqrt5;
    return (fibA.pow(level) + fibB.pow(level)) / fibSqrt5;
};

var tick = (elapsedTime, multiplier) =>
{
    t += elapsedTime;
    let dt = BigNumber.from(elapsedTime * multiplier);
    let bonus = theory.publicationMultiplier;

    let dq = qMs.level ? dt * getqdot(qdot.level) : BigNumber.ZERO;
    q += dq;

    currency.value += dt * bonus * getc1(c1.level).pow(getc1Exp(c1ExpMs.level)) * getc2(c2.level) * (BigNumber.ONE + getf(f.level)) * (qMs.level ? q : BigNumber.ONE);

    theory.invalidateTertiaryEquation();
    theory.invalidateQuaternaryValues();
}

var getPrimaryEquation = () =>
{
    if(stage == 0)
        return `\\dot{\\rho} = c_1${c1ExpMs.level ? `^{${getc1Exp(c1ExpMs.level)}}` : ''}c_2(1+f)${qMs.level ? 'q' : ''}`;
    else
        return `c_1${c1ExpMs.level ? `^{${getc1Exp(c1ExpMs.level)}}` : ''}c_2(1+f)${qMs.level ? 'q' : ''} = \\dot{\\rho}`;
}

var getSecondaryEquation = () =>
{
    if(stage == 0)
        return `${theory.latexSymbol} = \\max\\rho`;
    else
        return `\\max\\rho = ${theory.latexSymbol}`;
}

var getTertiaryEquation = () =>
{
    if(stage == 0)
        return qMs.level ? `q = ${q.toString()}` : '';
    else
        return qMs.level ? `${q.toString()} = q` : '';
}

var getQuaternaryEntries = () =>
{
    let minutes = Math.floor(t / 60);
    let seconds = t - minutes * 60;
    let hours = Math.floor(minutes / 60);
    minutes -= hours * 60;

    quaternary[0].value = hours;
    quaternary[1].value = minutes;
    quaternary[2].value = seconds.toFixed(1);
    return quaternary;
}

var canGoToPreviousStage = () => stage > 0;

var goToPreviousStage = () =>
{
    stage -= 1;
    theory.invalidatePrimaryEquation();
    theory.invalidateSecondaryEquation();
}

var canGoToNextStage = () => stage < 1;

var goToNextStage = () =>
{
    stage += 1;
    theory.invalidatePrimaryEquation();
    theory.invalidateSecondaryEquation();
}

var postPublish = () =>
{
    t = 0;
    q = BigNumber.ONE;
}

var get2DGraphValue = () => currency.value.sign *
(BigNumber.ONE + currency.value.abs()).log10().toNumber();

const pubPower = 0.1;

var getPublicationMultiplier = (tau) => tau.pow(pubPower);

var getPublicationMultiplierFormula = (symbol) => `{${symbol}}^{${pubPower}}`;

var getTau = () => currency.value;

var getCurrencyFromTau = (tau) =>
[
    tau.max(BigNumber.ONE),
    currency.symbol
];

var getInternalState = () => JSON.stringify
({
    t,
    q: q.toBase64String(),
    stage
});

var setInternalState = (stateStr) =>
{
    if(!stateStr)
        return;

    let state = JSON.parse(stateStr);
    t = state.t ?? t;
    q = BigNumber.fromBase64String(state.q) ?? q;
    stage = state.stage ?? stage;
}

init();

[^1] We can’t ac­tu­ally af­ford to swap the qua­tern­ary entries’ sides. The left side is ex­tremely tiny and can con­tain only 1 let­ter be­fore clip­ping.

[^2] As it stands (v1.4.40), there is a bug in the iOS ver­sion of the game, where if a sin­gu­lar up­grade is not vis­ible, the en­tire equa­tion area will be dwarfed by the phantom area that this in­vis­ible up­grade had al­loc­ated, mak­ing the the­ory un­play­able. It is ad­vised that your sin­gu­lar up­grades be vis­ible at all times (is­Avail­able should be true).