When I first designed Mirador's progression system, I wanted to create meaningful choices that would affect how players approach each run. The upgrade system provides this depth through a collection of abilities that modify player capabilities and create strategic decisions. This post explores how the upgrade system works and how it integrates with the game's progression mechanics.
The upgrade system is built around three core components: the upgrade definitions, the rarity system, and the upgrade manager. This design allows for easy extension while maintaining type safety and clear progression mechanics. Upgrades are defined as an enum with associated display information. This approach keeps the system extensible while maintaining type safety:
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AvailableUpgrade {
SpeedUp,
SlowTime,
SilentStep,
TallBoots,
HeadStart,
Dash,
Unknown,
}
This design allows me to add new upgrades easily while ensuring that each one has consistent display information.
I wanted to create excitement around upgrades while maintaining game balance. The rarity system achieves this by making some upgrades more valuable than others:
#[derive(Debug, Clone, PartialEq)]
pub enum UpgradeRarity {
Common, // 40% chance
Uncommon, // 30% chance
Rare, // 20% chance
Epic, // 8% chance
Legendary, // 2% chance
}
This creates a progression where common upgrades appear frequently while rare ones become exciting discoveries. The weighted selection ensures that players experience a mix of upgrades while still having those special moments when a powerful upgrade appears.
The UpgradeManager
tracks which upgrades the player has collected throughout their current run. This simple structure allows the system to track how many times each upgrade has been collected, enabling level-based progression where upgrades become more effective with each collection.
The upgrade system triggers at specific points during gameplay, based on level progression. I chose to offer upgrades every 3 levels to provide regular progression without overwhelming the player.
When the player reaches the maze exit, the game transitions to the ExitReached
screen where the player moves upward for 1 second, then either shows the upgrade menu or continues to the next level:
} else if state.game_state.current_screen == CurrentScreen::ExitReached {
state.game_state.exit_reached_timer += state.game_state.delta_time;
if state.game_state.exit_reached_timer < 1.0 {
state.game_state.player.move_up(state.game_state.delta_time);
} else {
let current_level = state.game_state.game_ui.level;
if current_level > 0 && current_level % 3 == 0 {
state.game_state.current_screen = CurrentScreen::UpgradeMenu;
state.upgrade_menu.show();
} else {
self.new_level(false);
}
}
}
This creates a natural rhythm where players complete levels, occasionally receive upgrades, and continue their journey with enhanced abilities.
When upgrades are offered, the system presents three random upgrade options. The weighted selection ensures that rarer upgrades appear less frequently, maintaining game balance while providing exciting moments when powerful upgrades are available.
Player upgrades are reset at specific points to maintain game balance and provide fresh progression opportunities. This was an important design decision to prevent the game from becoming too easy over multiple runs. When a player starts a new game (either through the pause menu or by completing a run), all upgrades are reset. When returning to the title screen or starting a new game, upgrades are explicitly cleared. The reset process is intentionally simple - it clears all collected upgrades, returning the player to their base abilities for the next run. This ensures that each run feels fresh and challenging.
The game currently includes seven different upgrades, each with unique effects and visual representations. I designed these to provide meaningful choices while maintaining game balance. | Upgrade | Icon | Rarity | Effect | Tooltip | |---------|------|--------|--------|---------| | Speed Up |  | Common | Increases movement speed by 10% per level | Increases your movement speed, making you faster and more agile. | | Slow Time |  | Uncommon | Makes time pass slower, giving more time to navigate | Each second lasts longer, giving you more time to navigate the maze. | | Silent Step |  | Rare | Reduces noise made while moving | Reduces the noise you make while moving, making you harder to detect. | | Tall Boots |  | Uncommon | Increases player height for better visibility | Makes you taller, allowing you to better see over the walls of the maze. | | Head Start |  | Rare | Prevents enemy movement at level start | Prevents the enemy from moving for a short time at the start of each level. | | Dash |  | Epic | Increases maximum stamina for longer sprinting | Increase your maximum stamina, allowing you to sprint for longer. | The effects are applied when upgrades are collected and persist throughout the current run until the game is restarted or the player returns to the title screen.
The upgrade system affects player abilities through the Player
struct. This integration was straightforward since the player already had the necessary fields to modify.
When an upgrade is selected, it's applied to the player. The actual game effects are implemented in the player movement and game logic systems. For example, speed upgrades modify the player's base_speed
field, while stamina upgrades would affect max_stamina
.
The system tracks how many times each upgrade has been collected, allowing for level-based progression where upgrades become more effective with each collection. This encourages players to choose upgrades strategically based on their playstyle and current situation.
The upgrade effects are applied through a systematic process that ensures proper stacking and balance. The system resets affected player fields to base values, then applies all owned upgrades with appropriate stacking mechanics:
pub fn apply_upgrade_effects(&self, game_state: &mut crate::game::GameState) {
// Reset affected player fields to base values
game_state.player.base_speed = 100.0;
game_state.player.max_stamina = 2.0;
game_state.player.position[1] = crate::math::coordinates::constants::PLAYER_HEIGHT;
// Apply stacking upgrades
for (upgrade, count) in self.upgrade_manager.player_upgrades.iter() {
match upgrade {
AvailableUpgrade::SpeedUp => {
// +10% movement and sprint speed per instance
game_state.player.base_speed <em>= 1.1_f32.powi(</em>count as i32);
}
AvailableUpgrade::Dash => {
// +10% max stamina per instance
game_state.player.max_stamina <em>= 1.1_f32.powi(</em>count as i32);
}
AvailableUpgrade::TallBoots => {
// +3 height per instance
game_state.player.position[1] =
crate::math::coordinates::constants::PLAYER_HEIGHT + 3.0 <em> (</em>count as f32);
}
AvailableUpgrade::SlowTime => {
// +5 seconds per instance to timer (handled at level start)
if let Some(timer) = &mut game_state.game_ui.timer {
let extra = 5 <em> </em>count as u64;
timer.config.duration += std::time::Duration::from_secs(extra);
}
}
// ... other upgrades
}
}
// After applying, update current speed to base
game_state.player.speed = game_state.player.base_speed;
}
This approach ensures that:
The upgrade system provides a simple but effective progression mechanism where players collect upgrades that modify their abilities. The rarity system ensures balanced gameplay while the level-based progression creates meaningful choices that affect how players approach each run. This approach of building complex progression on top of simple, tested components allows for consistent behavior across different parts of the game while keeping the code manageable. The upgrade system shows that good architecture pays dividends as the project grows in complexity. The system's design makes it easy to add new upgrades or modify existing ones, while the rarity system ensures that each upgrade choice feels meaningful. The reset mechanics ensure that each run remains challenging and fresh, preventing the game from becoming too easy over multiple playthroughs.