<aside> <img src="/icons/reorder_gray.svg" alt="/icons/reorder_gray.svg" width="40px" />
Navigation
</aside>
The core clicker loop is a tap-to-spawn, tap-to-destroy cycle: tapping the main button spawns chests on the field, and tapping a chest damages it (with a chance to crit or one-shot it) until it breaks open and drops an item or gem. Four scripts drive the button/hit side of it:
IClickableObject on the main button; on tap, spawns chests and plays the press animation, particles, and click sound.ChestsPerSpawn chests (scaled by the Chests effector multiplier) at a random field point on every button tap.
MainButtonBehavior.OnObjectClicked (invoked by the input system on tap) spawns chests first, then plays the press animation and feedback:
public void OnObjectClicked()
{
ChestSpawnController.SpawnOnButtonClick();
pressBehavior.Press();
clickParticles.Play();
if (Time.time - lastClickSoundTime >= CLICK_SOUND_COOLDOWN)
{
lastClickSoundTime = Time.time;
AudioController.PlaySound(AudioController.GetClip("main_button_click"), pitch: Random.Range(0.95f, 1.05f));
}
}
ChestSpawnController.SpawnOnButtonClick spawns Mathf.Max(1, UpgradeEffects.GetChestsPerSpawn(GameData.ChestsPerSpawn) * EffectorController.GetChestsMultiplier()) chests, each at a random point from ChestSpawnAreaController, with a tier rolled by ChestDatabase.RollTier() and effects rolled by ChestEffectDatabase.RollEffects() - see Chests & Loot and Effectors for the roll details. CanBeClicked() returns !pressBehavior.IsBusy, so a tap is ignored while the press animation is still resolving.
MainButtonPressBehavior.Press() swaps to the inactive sprite, flashes a SpriteOverlay shader highlight, and tweens the button down then back up. The return duration is scaled by the Button Return Speed upgrade, so this is the only piece of click feedback tunable from a purchasable upgrade rather than the Inspector alone:
public void Press()
{
if (IsBusy) return;
IsBusy = true;
moveTween.KillActive();
visualRenderer.sprite = inactiveSprite;
PlayHighlight();
float pressedY = initialLocalY - pressDepth;
moveTween = visualTransform.DOLocalMoveY(pressedY, pressDuration).SetEasing(pressEasing).OnComplete(() =>
{
GameData gameData = GameData.Data;
float returnDuration = UpgradeEffects.GetButtonReturnDuration(gameData != null ? gameData.ButtonReturnDuration : pressDuration);
moveTween = visualTransform.DOLocalMoveY(initialLocalY, returnDuration).SetEasing(returnEasing).OnComplete(() =>
{
activationBounce.Bounce();
visualRenderer.sprite = activeSprite;
IsBusy = false;
buttonBehavior.OnButtonActivated();
});
});
}
On return completion, activationBounce (a plain SimpleBounce, not a component) scales the button's transform, the sprite swaps back to activeSprite, IsBusy clears, and MainButtonBehavior.OnButtonActivated() fires (currently an empty hook, a natural place to react to the press finishing).
Tapping a chest goes through ChestBehavior.Interact(), not the main button - ClickerController resolves the damage roll, ChestBehavior applies it and reacts visually. See Chests & Loot for the chest-side health/open flow; this is the roll itself:
public static float RegisterChestHit(out bool isCritical)
{
if (!IsInitialized)
{
isCritical = false;
return 0;
}
isCritical = FrenzyController.IsActive || Random.value < UpgradeEffects.GetCritChance(gameData.CritChance);
float damage = UpgradeEffects.GetChestDamage(gameData.BaseChestDamage) + (isCritical ? gameData.CritBonusDamage : 0);
damage *= EffectorController.GetDamageMultiplier();
return damage;
}
// Independent of the crit roll; only checked while the chest is above GameData.OneShotHealthThreshold.
public static bool RollOneShot()
{
return IsInitialized && Random.value < UpgradeEffects.GetOneShotChance();
}
A one-shot roll is checked first, and only while the chest's health is still above OneShotHealthThreshold (default 70%) - if it hits, the chest takes its full remaining health in one blow instead of a normal roll. Otherwise RegisterChestHit resolves a normal hit: crit is guaranteed during Frenzy Mode, otherwise rolled against UpgradeEffects.GetCritChance; damage is UpgradeEffects.GetChestDamage (Chest Damage upgrade) plus GameData.CritBonusDamage on a crit, then scaled by the Effectors damage multiplier.
EffectorController holds three temporary multipliers - money, chest damage, and chests-per-spawn - each activated by picking up a timed offer. See the Effectors page for the offer flow; the core loop only reads the getters:
public static float GetMoneyMultiplier() => ActiveEffectController.IsActive(MoneyMultiplierEffectorData.ID) ? moneyMultiplier : 1f;
public static float GetDamageMultiplier() => ActiveEffectController.IsActive(DamageMultiplierEffectorData.ID) ? damageMultiplier : 1f;
public static float GetChestsMultiplier() => ActiveEffectController.IsActive(ChestsMultiplierEffectorData.ID) ? chestsMultiplier : 1f;
GetDamageMultiplier feeds RegisterChestHit above; GetChestsMultiplier feeds ChestSpawnController.SpawnOnButtonClick's spawn count. The money multiplier applies wherever coin rewards are granted (item pickup, chest value), outside this page's scope.
Frenzy Mode no longer scales a fillbar - its only effect on the core loop is forcing every chest hit to crit for its duration, via the isCritical = FrenzyController.IsActive || ... short-circuit in RegisterChestHit above. See Frenzy Mode for how it's triggered and how long it lasts.
ClickerController.ResetRun() calls ClickerSave.ResetRunState() and fires OnRunReset:
public static void ResetRun()
{
if (!IsInitialized) return;
save.ResetRunState();
SaveController.MarkAsSaveIsRequired();
OnRunReset?.Invoke();
}
This is called as part of RebirthController.PerformRebirth(), alongside despawning every live chest and item/effect drop on the field - see Skilltree & Prestige for the full Rebirth flow.