Action RPG
Real-time combat meets character progression — you died, but your gear survived.
"The only way to do great work is to love what you do." — Steve Jobs. "You Died." — Dark Souls.
Prerequisites
| Module | What You Used From It |
|---|---|
| Module 03 - Top-Down Shooter (or Module 08 - Fighting Game) | Real-time input handling, collision detection, frame-rate-independent movement, hit detection |
| Module 07 - Roguelike | Item/inventory systems, procedural generation concepts, entity management |
Week 1: History & Design Theory
The Origin
The action RPG was born from a desire to keep the progression systems of turn-based RPGs — levels, loot, stats — while replacing menu selection with real-time, skill-based combat. Early entries like Hydlide (1984) and The Legend of Zelda (1986) proved that exploration and combat could happen on the same screen without pausing for menus. The genre truly crystallized when developers realized the magic formula: combine the dopamine of finding better gear with the satisfaction of skillful play, so that the player's character AND the player themselves grow more powerful over time.
How the Genre Evolved
Diablo (Blizzard, 1997): Blizzard North's dungeon crawler made loot the star of the show. Procedurally generated dungeons, randomized item properties, and tiered rarity (common through unique) created an addictive loop: kill monsters, find better gear, kill harder monsters. Diablo proved that loot tables — the weighted probability distributions behind item drops — were as important as level design.
Dark Souls (FromSoftware, 2011): FromSoftware inverted the power fantasy. Instead of showering the player with loot, Dark Souls made every encounter dangerous and every victory earned. Its combat system — built on stamina management, dodge timing (invincibility frames), and reading enemy telegraph animations — demanded that player skill matter more than stats.
Elden Ring (FromSoftware, 2022): The synthesis. Elden Ring merged Dark Souls' precise combat with an open world full of optional bosses, hidden dungeons, and build variety. Its character build system showed that meaningful build diversity and challenging real-time combat could coexist.
What Makes an Action RPG "Great"
A great action RPG makes the player feel two kinds of growth simultaneously. Statistical growth comes from levels, gear, and build choices. Skill growth comes from the player learning enemy patterns, mastering dodge timing, managing stamina, and making split-second decisions. The best games ensure that neither growth alone is sufficient: a perfectly built character piloted by a careless player will still die, and a skillful player with terrible gear will hit a wall.
The Essential Mechanic
Real-time combat where stats, gear, and player skill both matter.
Week 2: Build the MVP
What You're Building
A top-down (or side-view) action game where the player character has stats (HP, attack, defense, stamina), fights enemies in real-time, collects loot drops with randomized properties, and can allocate stat points on level-up. The player should face 3-4 enemy types across 2-3 small zones, with at least one mini-boss that requires learning its attack patterns.
Core Concepts (Must Implement)
1. Loot / Drop Table System
When an enemy dies, it rolls against a weighted probability table to determine what it drops. Items have rarity tiers, and rarer items have better stats.
rarity_tiers = {
"common": { color: "white", stat_range: [1, 3], weight: 60 },
"uncommon": { color: "green", stat_range: [3, 6], weight: 25 },
"rare": { color: "blue", stat_range: [6, 10], weight: 10 },
"legendary": { color: "orange", stat_range: [10, 15], weight: 5 }
}
drop_tables = {
"skeleton": [
{ item_type: "sword", drop_chance: 30 },
{ item_type: "shield", drop_chance: 15 },
{ item_type: "potion", drop_chance: 50 },
{ item_type: "nothing", drop_chance: 5 }
]
}
function roll_drop(enemy_type):
table = drop_tables[enemy_type]
roll = random(0, 100)
cumulative = 0
for entry in table:
cumulative += entry.drop_chance
if roll < cumulative:
if entry.item_type == "nothing": return null
rarity = roll_rarity()
return generate_item(entry.item_type, rarity)
function roll_rarity():
roll = random(0, 100)
cumulative = 0
for tier_name, tier_data in rarity_tiers:
cumulative += tier_data.weight
if roll < cumulative:
return tier_name Why it matters: The drop table is the core reward system. It controls the player's power curve and creates the "just one more" loop that keeps players engaged.
Click "Drop Loot" to roll the drop table. Each drop shows the item and rarity. The bar chart on the right tracks actual vs. expected distribution over many drops. Click rapidly to see the distribution converge!
2. Stat System with Equipment
The player has base stats that are modified by equipped gear. Every piece of equipment adds to one or more stats.
player = {
base_stats: { hp_max: 100, attack: 10, defense: 8, speed: 5, stamina_max: 100 },
equipment: { weapon: null, armor: null, accessory: null },
level: 1, xp: 0
}
function get_effective_stat(stat_name):
base = player.base_stats[stat_name]
gear_bonus = 0
for slot, item in player.equipment:
if item and stat_name in item.stat_bonuses:
gear_bonus += item.stat_bonuses[stat_name]
return base + gear_bonus Why it matters: The stat system is what makes loot meaningful. Equipment creates a constant stream of micro-decisions: is +3 attack better than +5 defense?
3. Stamina / Resource Management in Combat
Attacks, dodges, and blocks cost stamina. When stamina is depleted, the player is vulnerable.
STAMINA_REGEN_RATE = 20 # per second
ATTACK_STAMINA_COST = 25
DODGE_STAMINA_COST = 30
BLOCK_STAMINA_COST = 15 # per second while blocking
function update_stamina(dt):
if player.state == "blocking":
player.stamina -= BLOCK_STAMINA_COST * dt
if player.stamina <= 0:
player.stamina = 0
break_block()
elif player.state == "idle" or player.state == "moving":
player.stamina = min(player.stamina + STAMINA_REGEN_RATE * dt,
get_effective_stat("stamina_max"))
function try_attack():
if player.stamina >= ATTACK_STAMINA_COST:
player.stamina -= ATTACK_STAMINA_COST
start_attack_animation()
return true
else:
show_feedback("Not enough stamina!")
return false Why it matters: Stamina prevents optimal play from being "attack as fast as possible." It forces the player to create rhythms — attack, attack, back off, regenerate.
4. Dodge / Invincibility Frames (I-Frames)
During a dodge animation, there is a brief window where the player cannot be hit. This rewards precise timing.
DODGE_DURATION = 0.4 # seconds
IFRAME_START = 0.05 # i-frames begin 50ms into dodge
IFRAME_END = 0.25 # i-frames end 250ms into dodge
DODGE_SPEED = 300 # pixels/second during dodge
function start_dodge(direction):
player.state = "dodging"
player.dodge_timer = 0
player.dodge_direction = direction
function update_dodge(dt):
if player.state != "dodging": return
player.dodge_timer += dt
player.position += player.dodge_direction * DODGE_SPEED * dt
player.invulnerable = (player.dodge_timer >= IFRAME_START and
player.dodge_timer <= IFRAME_END)
if player.dodge_timer >= DODGE_DURATION:
player.state = "idle"
player.invulnerable = false
function try_damage_player(damage, source):
if player.invulnerable:
return 0 # Dodged!
actual = calculate_damage(damage, get_effective_stat("defense"))
player.hp -= actual
return actual Why it matters: I-frames are the purest expression of skill-based defense in action RPGs. A player who masters dodge timing can fight enemies far above their stat level.
A sweeping attack moves across the arena. Press SPACE to dodge. During the i-frame window (shown in green on the timeline), the attack passes through harmlessly. Mistiming means damage! The timeline below shows the dodge frame window.
5. Enemy Telegraphs and Attack Windows
Enemies signal their attacks with visible wind-up animations before the damage lands.
enemy_attacks = {
"overhead_smash": {
telegraph_duration: 0.8, # Wind-up: DODGE NOW
active_duration: 0.2, # Damage frames
recovery_duration: 0.6, # Vulnerable: ATTACK NOW
damage: 30,
hitbox: { width: 40, height: 60, offset_y: -30 }
}
}
function update_enemy_attack(enemy, dt):
enemy.attack_timer += dt
attack = enemy_attacks[enemy.current_attack]
if enemy.attack_phase == "telegraph":
show_telegraph_indicator(enemy)
if enemy.attack_timer >= attack.telegraph_duration:
enemy.attack_phase = "active"
enemy.attack_timer = 0
elif enemy.attack_phase == "active":
if not player.invulnerable and overlaps(attack.hitbox, player.hitbox):
try_damage_player(attack.damage, enemy)
if enemy.attack_timer >= attack.active_duration:
enemy.attack_phase = "recovery"
enemy.attack_timer = 0
elif enemy.attack_phase == "recovery":
enemy.can_be_staggered = true
if enemy.attack_timer >= attack.recovery_duration:
enemy.attack_phase = "idle" Why it matters: Telegraphs are the language of action RPG combat. They transform fights from reaction-speed tests into pattern-recognition puzzles.
6. Area / Zone Transitions
zones = {
"village": {
enemies: [],
exits: [{ position: [15, 0], target_zone: "forest", entry_point: [1, 10] }],
},
"forest": {
enemies: ["skeleton", "wolf"],
exits: [
{ position: [0, 10], target_zone: "village", entry_point: [14, 0] },
{ position: [15, 5], target_zone: "dungeon", entry_point: [1, 5] }
],
}
} Why it matters: Zone transitions create the feeling of a larger world from small, manageable pieces.
7. Character Build / Skill System
STAT_POINTS_PER_LEVEL = 3
build_stats = {
"strength": { description: "Increases attack damage", affects: "attack", per_point: 2 },
"dexterity": { description: "Increases speed and stamina", affects: ["speed", "stamina_max"],
per_point: [1, 5] },
"vitality": { description: "Increases HP", affects: "hp_max", per_point: 10 },
"luck": { description: "Increases crit and drop rates", affects: "luck", per_point: 1 }
}
# Example builds after 10 levels (30 points):
# "Glass Cannon": 20 STR, 5 DEX, 0 VIT, 5 LCK — hits hard, dies fast
# "Tank": 5 STR, 5 DEX, 20 VIT, 0 LCK — survives everything, slow kills Why it matters: Build allocation gives the player ownership over their character's identity. Two players can fight the same boss with completely different strategies.
Stretch Goals
- Weapon types with different movesets: Swords swing fast, hammers swing slow but stagger enemies.
- Status effects on gear: A "Burning Sword" that applies damage-over-time.
- Boss with multiple phases: At 50% HP, the boss changes attack patterns.
- NPC shop: Spend gold to buy potions, basic gear, or stat resets.
MVP Spec
| Element | Minimum Viable Version |
|---|---|
| Player | Character with HP, stamina, attack, defense, speed stats |
| Equipment | Weapon and armor slots; equipping items modifies stats |
| Combat | Real-time attack, dodge (with i-frames), and block |
| Enemies | 3-4 types with different telegraph patterns and drop tables |
| Loot | Random drops with at least 3 rarity tiers |
| Zones | 2-3 connected areas with zone transition |
| Leveling | XP from kills, stat point allocation on level-up |
| Boss | 1 mini-boss with distinct attack patterns and guaranteed rare drop |
| HUD | HP bar, stamina bar, equipped item display |
Deliverable
A playable action RPG where the player moves through 2-3 zones, fights enemies in real-time using attacks, dodges, and blocks, collects randomized loot, equips gear that changes their stats, levels up and allocates stat points, and defeats a mini-boss. The player should feel both kinds of growth: statistical (better gear, higher stats) and personal (learning enemy patterns, mastering dodge timing).
Analogies by Background
These analogies map game dev concepts to patterns you already know.
For Backend Developers
| Core Concept | Analogy |
|---|---|
| Loot / Drop Tables | Like weighted load balancing — requests are distributed across servers according to configured weights |
| Stat System with Equipment | Like environment variable overrides — base configuration is merged with deployment-specific overrides |
| Stamina Management | Like rate limiting with a token bucket — each action consumes tokens, tokens regenerate over time |
| Dodge / I-Frames | Like a circuit breaker in "open" state — during the brief window, all incoming requests are rejected |
| Enemy Telegraphs | Like health check probes with predictable timing |
| Zone Transitions | Like microservice boundaries — each zone is an independent service with its own data |
| Character Build System | Like compile-time feature flags — choices made at build time determine runtime behavior |
For Frontend Developers
| Core Concept | Analogy |
|---|---|
| Loot / Drop Tables | Like A/B test variant allocation — users are assigned to variants based on configured weights |
| Stat System with Equipment | Like CSS cascade — base styles are overridden by more specific rules |
| Stamina Management | Like debouncing — rapid inputs are throttled |
| Dodge / I-Frames | Like pointer-events: none applied temporarily |
| Enemy Telegraphs | Like CSS transition delays — the visual change begins, a delay passes, then the final state resolves |
| Zone Transitions | Like client-side route changes — the component tree is replaced but global state persists |
| Character Build System | Like theming choices at app initialization |
For Data / ML Engineers
| Core Concept | Analogy |
|---|---|
| Loot / Drop Tables | Like sampling from a categorical distribution — each category has a probability weight |
| Stat System with Equipment | Like feature vectors with additive components |
| Stamina Management | Like GPU memory budgeting — each operation consumes memory, stalling when depleted |
| Dodge / I-Frames | Like dropout during training — for a subset of frames, incoming signals are ignored |
| Enemy Telegraphs | Like data pipeline latency — a known delay between data arriving and processing completing |
| Zone Transitions | Like switching between dataset partitions |
| Character Build System | Like hyperparameter tuning — allocating stat points is choosing hyperparameters |
Discussion Questions
- Dark Souls is famous for being "hard but fair." How do i-frames and enemy telegraphs create a sense of fairness that pure stat-based difficulty does not?
- Diablo's loot system is often compared to slot machines. At what point does a random reward system cross from "engaging" to "exploitative"?
- In most action RPGs, the player can eventually out-level any challenge. Should difficulty scale with the player? What are the emotional tradeoffs?
- Stamina systems force players to not act. Why is "forced inaction" a valuable design tool?