Repository navigation
Upgrades & Perks
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.
- 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.
| 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 |
-
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.
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").
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);
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();
}
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
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.
- 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:
PerksMenuDisplayshows every defined Perk with its current progress (e.g. "23/50") or "Unlocked" once complete.
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.
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.
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());
-
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
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:
- press
F1to open the terminal - Type: perk timeLord
- Die -> try again -> perk selection display -> choose time freeze (it will be shown unlocked)
- Use the key defined for time freeze in the keybinds setting to use the perk