Module 21

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

ModuleWhat 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 - RoguelikeItem/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.

Demo: Loot Drop Table Visualizer

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.

Demo: I-Frames Dodge Timing

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

MVP Spec

ElementMinimum Viable Version
PlayerCharacter with HP, stamina, attack, defense, speed stats
EquipmentWeapon and armor slots; equipping items modifies stats
CombatReal-time attack, dodge (with i-frames), and block
Enemies3-4 types with different telegraph patterns and drop tables
LootRandom drops with at least 3 rarity tiers
Zones2-3 connected areas with zone transition
LevelingXP from kills, stat point allocation on level-up
Boss1 mini-boss with distinct attack patterns and guaranteed rare drop
HUDHP 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 ConceptAnalogy
Loot / Drop TablesLike weighted load balancing — requests are distributed across servers according to configured weights
Stat System with EquipmentLike environment variable overrides — base configuration is merged with deployment-specific overrides
Stamina ManagementLike rate limiting with a token bucket — each action consumes tokens, tokens regenerate over time
Dodge / I-FramesLike a circuit breaker in "open" state — during the brief window, all incoming requests are rejected
Enemy TelegraphsLike health check probes with predictable timing
Zone TransitionsLike microservice boundaries — each zone is an independent service with its own data
Character Build SystemLike compile-time feature flags — choices made at build time determine runtime behavior

For Frontend Developers

Core ConceptAnalogy
Loot / Drop TablesLike A/B test variant allocation — users are assigned to variants based on configured weights
Stat System with EquipmentLike CSS cascade — base styles are overridden by more specific rules
Stamina ManagementLike debouncing — rapid inputs are throttled
Dodge / I-FramesLike pointer-events: none applied temporarily
Enemy TelegraphsLike CSS transition delays — the visual change begins, a delay passes, then the final state resolves
Zone TransitionsLike client-side route changes — the component tree is replaced but global state persists
Character Build SystemLike theming choices at app initialization

For Data / ML Engineers

Core ConceptAnalogy
Loot / Drop TablesLike sampling from a categorical distribution — each category has a probability weight
Stat System with EquipmentLike feature vectors with additive components
Stamina ManagementLike GPU memory budgeting — each operation consumes memory, stalling when depleted
Dodge / I-FramesLike dropout during training — for a subset of frames, incoming signals are ignored
Enemy TelegraphsLike data pipeline latency — a known delay between data arriving and processing completing
Zone TransitionsLike switching between dataset partitions
Character Build SystemLike hyperparameter tuning — allocating stat points is choosing hyperparameters

Discussion Questions

  1. 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?
  2. Diablo's loot system is often compared to slot machines. At what point does a random reward system cross from "engaging" to "exploitative"?
  3. In most action RPGs, the player can eventually out-level any challenge. Should difficulty scale with the player? What are the emotional tradeoffs?
  4. Stamina systems force players to not act. Why is "forced inaction" a valuable design tool?