# Building a Paper Mario Action Command System

> How to build a Power Bounce combat system with state machines, timing windows, and difficulty scaling.

- Role: Combat prototype
- Status: Playable technical demo
- Stack: JavaScript, Canvas, state machines, timing windows
- Page: https://thomasjvu.com/projects/paper-mario-combat
- Markdown: https://thomasjvu.com/projects/paper-mario-combat.md

## Links

- [Play](https://thomasjvu.github.io/paper-mario-combat/)
- [GitHub](https://github.com/thomasjvu/paper-mario-combat)

## Metrics

- Base window: 320ms — Starting action-command timing window.
- Minimum window: 75ms — Late-combo timing floor after difficulty scaling.

## Writeup

A browser-based recreation of Paper Mario's Power Bounce combat system—the repeated jump attack where you time button presses to keep bouncing and dealing damage.

---

## The Big Picture

The Power Bounce is a repeated jump attack where Mario bounces off enemies, dealing damage each time. The twist? You have to press a button at the right moment to maximize damage and keep bouncing. Miss the timing, and Mario falls to the ground—game over for that combo.

Three core systems make this work:

1. **State Machine** — organizing game flow
2. **Action Command** — the timing-based input system
3. **Difficulty Scaling** — making it get harder as you improve

Let's dive into each one.

---

## 1. The State Machine

At its core, a game is just a state machine—a set of "modes" the game can be in, with rules for how to transition between them. Here's how we defined the states for Power Bounce:

```javascript
const STATE = {
  TITLE: 0,         // Waiting for player to start
  IDLE: 1,          // Mario standing still, waiting for input
  RUN_TO_ENEMY: 2,  // Mario running toward the Goomba
  JUMP_UP: 3,       // Mario's initial jump arc
  FALL_TO_STOMP: 4, // Mario falling toward the enemy
  ACTION_WINDOW: 5, // Timing window is active (we'll cover this)
  STOMP_HIT: 6,     // Successful hit—deal damage, bounce up
  BOUNCE_UP: 7,     // Rising after a successful hit
  BOUNCE_FALL: 8,   // Falling for the next potential hit
  MISS_BOUNCE: 9,   // Weak bounce after missing timing
  MISS_FALL: 10,    // Falling to the ground after missing
  RESULTS: 11,      // Show final score
};
```

Each frame, the game checks `state` and runs different logic. For example, in `JUMP_UP`, we apply upward velocity to Mario. In `BOUNCE_FALL`, we check if the player pressed the button at the right time.

The `update()` function is the heart of this:

```javascript
function update(dt) {
  stateTimer += dt;

  switch (state) {
    case STATE.TITLE:
      if (inputJustPressed) startGame();
      break;

    case STATE.IDLE:
      mario.sprite = 'idle';
      if (inputJustPressed) {
        state = STATE.RUN_TO_ENEMY;
        sfxJump();
      }
      break;

    case STATE.RUN_TO_ENEMY: {
      mario.sprite = 'run';
      mario.x += runSpeed;
      if (mario.x >= targetX) {
        state = STATE.JUMP_UP;
        jumpVelY = -12;
      }
      break;
    }
    // ... more states
  }
}
```

This pattern—checking "what mode are we in, then do the right thing"—is how virtually every game works. It's clean, organized, and easy to debug.

---

## 2. The Action Command System

Here's where the skill expression comes in. As Mario falls toward the enemy, a ring shrinks around the target. You need to press your button when the ring is in the "sweet spot."

### Drawing the Ring

We track progress from 0 to 1 as the ring shrinks:

```javascript
function drawActionRing(x, y) {
  if (!ringActive) return;

  // Ring starts at outer radius, shrinks to inner radius
  const outerR = 40;
  const innerR = 14;
  const currentR = outerR - (outerR - innerR) * ringProgress;

  // Determine color based on timing zone
  // Green = perfect zone (Nice), Yellow = okay zone (Good), Red = too late
  const inZone = ringProgress >= 0.70 && ringProgress <= 0.95;
  const ringColor = inZone ? '#3DF53D' : (ringProgress > 0.95 ? '#FF4444' : '#FFD700');

  // Draw the shrinking ring
  ctx.beginPath();
  ctx.arc(x, y, currentR, 0, Math.PI * 2);
  ctx.strokeStyle = ringColor;
  ctx.lineWidth = 4;
  ctx.stroke();

  // Add a glow effect when in the perfect zone
  if (inZone) {
    ctx.beginPath();
    ctx.arc(x, y, currentR, 0, Math.PI * 2);
    ctx.strokeStyle = 'rgba(61, 245, 61, 0.3)';
    ctx.lineWidth = 10;
    ctx.stroke();
  }
}
```

### Detecting the Input

When the player presses a button, we check where `ringProgress` is:

```javascript
if (inputJustPressed && !actionPressed) {
  actionPressed = true;
  const accuracy = ringProgress;

  // Determine result based on timing
  // Nice: 80-95% (the green zone)
  // Good: 70-100% (the yellow zone)
  // Miss: anything outside that range
  if (accuracy >= 0.70 && accuracy <= 1.0) {
    actionResult = accuracy >= 0.80 && accuracy <= 0.95 ? 'nice' : 'good';
  } else {
    actionResult = 'miss';
  }
}

// If ring completes without input, it's a miss
if (ringProgress >= 1 && !actionPressed) {
  actionResult = 'miss';
  actionPressed = true;
}
```

The timing windows are:
- **Nice** (best): 80-95% — requires precise timing
- **Good** (okay): 70-100% — more forgiving
- **Miss** (fail): outside those ranges

---

## 3. Combo & Difficulty Scaling

This is what makes the system engaging. As you build a combo, two things happen:

1. **The timing window shrinks** — making it harder to land the next hit
2. **Mario bounces higher** — giving you less time to react

Here's how we calculate the timing window:

```javascript
// Timing - gets tighter with each hit
const BASE_WINDOW = 320;      // 320ms at the start (very generous)
const WINDOW_DECREASE = 18;    // Lose 18ms per combo
const MIN_WINDOW = 75;        // Never go below 75ms (human limit)

function getWindowDuration() {
  return Math.max(MIN_WINDOW, BASE_WINDOW - combo * WINDOW_DECREASE);
}
```

So after 10 successful hits, the window is down to 140ms—requiring genuine reflexes.

The damage also scales:

```javascript
function getDamage() {
  if (combo === 0) return 2;  // First hit always deals 2 damage
  return 1;                    // Subsequent hits deal 1
}
```

And the bounce height increases:

```javascript
// Bounce velocity increases with combo, capped at +3
jumpVelY = -9 - Math.min(combo * 0.3, 3);
```

This creates a satisfying progression: early hits are easy (build your confidence), but as you chain more together, the system demands more precision while rewarding you with higher bounces and flashier effects.
