Skip to content

Upgrades & Perks

Rahul-18K edited this page Oct 5, 2026 · 21 revisions

Upgrades

Overview

Adds a purchasable, temporary upgrade system: 6 upgrades (Sword Damage, Attack Speed, Shield Durability, Regen on Kill, Player Speed+, Endurance) that the player buys from the Shop's Upgrades tab using real gold. Every upgrade is temporary and repurchasable — buying the same upgrade again while it's still active advances its tier (a stronger effect and a longer expiry window), and once it fully expires it resets and can be bought again from scratch.

Core Workflow

  • Purchase: Player opens the Shop, clicks the Upgrades tab, selects a card, and buys it from the detail panel — the only real way to purchase an upgrade in the finished game.
  • Tiering: Buying an upgrade for the first time activates it at Tier 1. Buying the same upgrade again while it's still active advances it to the next tier, both strengthening the effect and extending how long it lasts.
  • Expiry: Each upgrade expires one of two ways — a fixed duration (Defence/Movement upgrades) or a fixed number of kills while active (Action upgrades). Expiry freezes while the game is paused or the player is dead, and resumes from the exact same value once unpaused/alive.
  • Effect application: Each upgrade applies a real gameplay effect the moment it activates or changes tier, and reverses that effect the moment it expires.

The 6 Upgrades

Upgrade Category Tiers Effect per tier Expires by
Sword Damage Action 3 +5 / +10 / +15 base attack (replaces, doesn't stack) Kills (2 / 5 / 8)
Attack Speed Action 3 Attack cooldown ×0.8 / ×0.6 / ×0.4 Kills (2 / 5 / 8)
Shield Durability Defence 3 Absorbs the next 3 / 5 / 8 hits, no health lost Time (or absorbing all hits)
Regen on Kill Defence 2 Heals 5 / 10 HP on every kill, capped at max health Time
Player Speed+ Movement 3 Movement speed ×1.15 / ×1.3 / ×1.5 Time
Endurance Movement 3 Stamina refills ×1.6 / ×2.0 / ×2.5 as fast Time

Team Dependencies

  • Shop (Team 3): Owns the Shop window, the card grid, and the real currency (InventoryComponent). Upgrades' data plugs into their existing catalog/purchase system rather than a separate one.

Behind The Scenes

The tiered, temporary data model

Each upgrade is an UpgradeNode with a cost, a category (Action/Defence/ Movement), and one of two expiry types:

Buy once -> Tier 1, active for 5 kills.
Buy again before it expires -> Tier 2, active for 8 kills.
Kills run out -> resets fully to Tier 0, buyable again from Tier 1.

TIME-based upgrades work the same way, but with seconds instead of kills — and critically, each tier adds to whatever time is currently remaining:

Buy with 4s left, +10s increment -> 14s remaining (not a flat "10" or "20").

Applying real effects

Each UpgradeNode takes two callbacks — setOnTierChanged() and setOnExpired() — registered once when the upgrade is defined, fired automatically whenever purchaseNextTier() runs or the upgrade expires:

node.setOnTierChanged(() -> applySwordDamageEffect(node));
node.setOnExpired(this::removeSwordDamageEffect);

Most upgrades apply a standing effect this way — e.g. Sword Damage recomputes the player's baseAttack from a stored baseline plus the current tier's bonus, so stacking tiers replaces the bonus rather than adding it twice.

Regen on Kill is different — it needs to react to every kill while active, not apply once. Instead of a tier-changed callback, it registers a listener on the player entity for the "enemyKilled" event fired by CombatStatsComponent, and heals the player by a tier-scaling amount each time it fires:

player.getEvents().addListener("enemyKilled", this::onEnemyKilled);

Shop integration

ShopDisplay doesn't know anything about upgrade tiers or effects directly — it calls UpgradeNode.purchaseNextTier() on a successful real purchase (after deducting real gold via the Shop's existing buyUpgrade()), and the effect callbacks handle the rest:

boolean purchased = shop.buyUpgrade(catalogSlot);
if (purchased) {
  node.purchaseNextTier();
}

UML Diagram

Class Diagram

classDiagram
  class UpgradeNode {
    +purchaseNextTier() void
    +tickTime(float) void
    +onEnemyKilled() void
    +reset() void
    +getNextTierCost() int
    +getRemainingText() String
    +setOnTierChanged(Runnable) void
    +setOnExpired(Runnable) void
  }
  class UpgradesDisplay {
    +setPlayer(Entity) void
    +getAllUpgrades() List
    +getShieldHitsRemaining() int
    -onEnemyKilled() void
    clearAllUpgrades() void
  }
  class ActiveUpgradesHud {
    +draw(SpriteBatch) void
  }
  class ShopDisplay {
    -attemptUpgradePurchase(int, UpgradeNode) void
  }
  class ShopComponent {
    +buyUpgrade(int) boolean
  }
  class PlayerActions {
    +addSpeedModifier(Object, float) void
    +removeSpeedModifier(Object) void
    +setAttackSpeedMultiplier(float) void
  }
  class CombatStatsComponent {
    +setBaseAttack(int) void
    +setShieldHits(int) void
    +getShieldHits() int
    +hit(CombatStatsComponent, int) void
  }
  class StaminaComponent {
    +setRegenMultiplier(float) void
  }
  class UpgradeActivationFlashComponent {
    +draw(SpriteBatch) void
  }

  UpgradesDisplay "1" o-- "6" UpgradeNode : owns
  ActiveUpgradesHud ..> UpgradesDisplay : reads upgrades
  ShopDisplay ..> ShopComponent : buyUpgrade()
  ShopDisplay ..> UpgradeNode : purchaseNextTier()
  UpgradesDisplay ..> PlayerActions : speed, attack speed
  UpgradesDisplay ..> CombatStatsComponent : attack, shield hits
  UpgradesDisplay ..> StaminaComponent : regen multiplier
  CombatStatsComponent ..> UpgradesDisplay : enemyKilled event
  UpgradesDisplay ..> UpgradeActivationFlashComponent : upgradeActivated event
Loading

Perks

Overview

A permanent, milestone-earned achievement system, entirely separate from Upgrades. A Perk unlocks once and stays unlocked forever — there's no buying, no expiry, no currency involved. Progress will be tracked through the quest tracking system which will help in tracking statistics such as the player does things in the game (e.g. dashing, killing enemies) and persists across sessions, the same way a game console's achievements do. A player can have at most two active perks throughout the game and can change between which perks to activate whenever they die or in the main menu.

Core Workflow

  • Progress: Through the quest tracking system, every action is registered with system and calls the unlocking perk function for checking if the action amount has surpassed the threshold or not.
  • Unlocking: Once a Perk's progress reaches its threshold, it unlocks automatically and stays unlocked — there's no way to lose it again (other than the terminal command added for testing).
  • Persistence: Progress and unlock state are saved to disk immediately after every update, so they survive closing and reopening the game.
  • Viewing: PerksMenuDisplay shows every defined Perk with its current progress (e.g. "23/50") or "Unlocked" once complete.

Behind The Scenes

One call to report progress, from anywhere

PerkService is a static registry — any gameplay system can report progress toward a milestone without needing a reference to anything, and without needing to check whether a matching Perk even exists:

PerkService.recordEvent("dashUsed", 1);

If no Perk is currently listening for that event key, this is simply a no-op — gameplay code never needs to know or care whether a Perk system exists for what it just did. This is exactly what quest on completion calls to unlock a perk for the player to activate.

Adding a new Perk needs almost no code

Because Perks are just data (an id, name, description, an event key to listen for, and a threshold), adding a new one to an existing event is a single line in PerkDefinitions:

PerkService.register(
    new Perk("dash_master", "Dash Master", "Use dash 100 times", "dashUsed", 100));

Adding a Perk for a brand-new milestone (e.g. once wall jumps exist) only needs one additional recordEvent(...) call at the point that milestone happens, plus the same one-line definition above — nothing about PerkService itself ever needs to change.

Persistence

Unlock state is saved via libGDX's Preferences (a simple persistent key-value store, the same mechanism the Pause Menu already uses for audio settings) — not a save file the player manages, just automatic background persistence:

prefs.putInteger(perk.getId() + ".progress", perk.getProgress());
prefs.putBoolean(perk.getId() + ".unlocked", perk.isUnlocked());

Currently defined Perks

  • shieldMaster - collect shield 5 times: Extends the shield duration by 10 seconds whenever is set active.

  • thickSkin - kill 10 enemies: Increases player health points by 50 (150 total HP) whenever is set active.

  • snatcher - spend 120 gold coins in shop: Increases loot drop on killing enemies when set active.

  • timelord - find the tortoise: Gives player a special ability to pause the time for 5 seconds (with a 20 second refresh) when set active. (this perk application includes addition of a tortoise sprite which was extracted from an online source and is not my own creation. LINK to the online sprite: Tortoise sprite

Awarding/Resetting Perks through Terminal

To award perks through terminal, perk {perk id} command is to be used, similarly to reset a perk status, perk {perk id} reset is to be used. The perk IDs for all the perks are mentioned in the currently defined perks section of the wiki.

example of use:

  1. press F1 to open the terminal
  2. Type: perk timeLord
  3. Die -> try again -> perk selection display -> choose time freeze (it will be shown unlocked)
  4. Use the key defined for time freeze in the keybinds setting to use the perk

UML

UML PERKS

Clone this wiki locally