# Making the Game *Fun* — A Deep Dive on Platformer Game Feel & "Juice"

This document is two things:

1. **Research** — a deep dive into *what actually makes a 2D platformer fun*, broken
   into a big checklist of the techniques pros use ("game feel" + "juice").
2. **Implementation log** — exactly which of those techniques were added to *this*
   game, where they live in the code, and how to tune them.

The game started as the classic Brackeys/Godot platformer tutorial: move, jump,
collect coins, avoid a slime, fall = die. It worked, but it felt *flat* — the jump
was rigid, nothing reacted to you, and the only feedback was a number going up. The
changes below are about closing the gap between "it functions" and "it feels good."

---

## Part 1 — What Makes a Platformer Fun?

Fun in an action-platformer comes from a tight loop:

> **You press a button → the character does something that feels good → the world
> reacts in a way that feels good → you want to press the button again.**

Almost everything below is in service of that loop. There are three big pillars.

### Pillar 1 — Controls that feel *responsive and fair* (the most important)

This is the single biggest factor. A platformer can have gorgeous art and still feel
awful if the jump is wrong. The trick is that "realistic" physics feels *bad*; good
platformers fake the physics to flatter the player. Key techniques:

| Technique | What it does | Why it's fun |
|---|---|---|
| **Coyote time** | Lets you jump for a few frames *after* walking off a ledge | Removes "but I pressed jump!" deaths; the game feels fair |
| **Jump buffering** | A jump pressed slightly *before* landing still fires | Lets you mash confidently; landing→jumping feels seamless |
| **Variable jump height** | Tap = small hop, hold = full jump | Gives the player fine analog control over a digital button |
| **Asymmetric gravity** | You fall *faster* than you rise | Kills the "floaty moon-jump" feeling; adds weight & snap |
| **Apex hang time** | Slightly reduced gravity at the top of the arc | A beat of "hang" at the peak makes jumps readable & graceful |
| **Acceleration / friction** | Speed ramps up & down instead of snapping on/off | Gives the character momentum and weight, but stays snappy |
| **Air control** | Weaker steering + more momentum while airborne | Makes ground and air feel distinct; rewards commitment |
| **Terminal velocity** | A cap on fall speed | Keeps long falls controllable and survivable |
| **Corner correction / ledge nudge** | Nudges you past corners you *almost* cleared | Removes cheap bonks on tile edges *(noted as future work)* |

The rule of thumb (from designers like Mark Brown / GMTK and the Celeste devs): **be
generous to the player**. If it's ambiguous whether a jump should have worked, let it
work.

### Pillar 2 — "Juice": feedback that makes actions feel *impactful*

"Juice" = the disproportionate amount of audio-visual feedback an action gets. A coin
isn't just removed; it *pops, sparkles, sings, and floats a number*. Juice doesn't
change what the game *does* — it changes how it *feels*. The canonical techniques:

- **Squash & stretch** — the character/objects deform on jump, land, and impact.
  Borrowed straight from classical animation; sells weight and energy.
- **Anticipation & follow-through** — a tiny wind-up before an action and an
  overshoot after (e.g. an elastic settle) make motion feel alive.
- **Screen shake** — camera kicks on impacts. Tiny for small hits, big for deaths.
  Sells force. (Reference: Vlambeer's *"The Art of Screenshake"*.)
- **Hit-stop / freeze frames** — the game freezes for a few milliseconds on a big
  impact, then resumes. Makes hits feel like they *connect*. (Used heavily in fighting
  games, *Celeste*, *Dead Cells*.)
- **Particles** — dust on landing/running, sparkles on pickups, a splat on enemy
  death, a poof on dying. Cheap and enormously effective.
- **Tweening & easing** — nothing should move linearly. Ease-out, elastic, and "back"
  curves make UI and objects feel springy and intentional.
- **Camera craft** — smoothing/lag, look-ahead in the move direction, and a small
  zoom "punch" on big events. The camera is a character too.
- **Sound design** —
  - *layered* and *varied* sounds (pitch randomization stops repetition fatigue),
  - *rising pitch on combos* (each coin a semitone higher = a satisfying musical ramp),
  - a sound for *every* meaningful action (jump, land, double-jump, stomp, hurt).
- **Flashes & color** — a quick tint/flash on hit or pickup draws the eye.
- **The "+1" popup** — floating score numbers turn an abstract counter into a reward.

The mantra (from *Juice it or lose it* by Martin Jonasson & Petri Purho): **add way
more feedback than feels reasonable, then dial it back.**

### Pillar 3 — Reward, risk, and clear goals

Feel gets you to "satisfying"; design gets you to "I want to keep playing."

- **Risk/reward** — make the fun action also the useful one. Stomping an enemy is more
  exciting *and* faster than avoiding it.
- **Combos & chains** — reward skillful, fast, continuous play (coin combos here).
- **Readable goals & feedback** — the player should always know what to do and whether
  they're succeeding. On-screen score, teaching labels, obvious hazards.
- **Generous, low-friction failure** — fast restarts and "my fault, not the game's"
  deaths keep players in *flow* instead of frustrated.
- **Expressive movement** — give players more verbs (double jump, bounce) so *how* they
  move becomes self-expression, not just a means to an end.

---

## Part 2 — The Big Checklist (and what we added)

Legend: ✅ implemented · 🔜 great future addition

### A. Movement & control
- ✅ **Acceleration & friction** (separate ground/air values)
- ✅ **Coyote time** (0.10s)
- ✅ **Jump buffering** (0.10s)
- ✅ **Variable jump height** (release early to cut the jump)
- ✅ **Asymmetric gravity** (fall 1.5×, rise 0.9×)
- ✅ **Terminal velocity** (max fall speed clamp)
- ✅ **Double jump** (a new movement verb + recovery tool)
- ✅ **Stomp bounce** (refreshes the air jump for chaining)
- ✅ **Apex hang time** (gravity eases near the peak for a beat of "hang")
- ✅ **Corner correction** (nudge past ceiling corners you *almost* cleared)
- 🔜 Wall slide / wall jump, dash

### B. Juice & feedback
- ✅ **Squash & stretch** on jump, land, bounce — a velocity-driven **damped spring**
  with an overshoot bounce (Mario-style), plus subtle tall-stretch in the air
- ✅ **Screen shake** scaled to impact (land, stomp, death)
- ✅ **Hit-stop / freeze frame** on enemy stomp
- ✅ **Particles**: running/landing/jump **dust**, coin **sparkles**, slime **splat**,
  death **poof**, double-jump **puff**
- ✅ **Sound for every action**: jump, double-jump, land, stomp, hurt (previously many
  sound assets were unused!)
- ✅ **Pitch-rising combo audio** on coins
- ✅ **Pitch-randomised SFX** on jump/double-jump/land/stomp/hurt (kills repetition fatigue)
- ✅ **Music fade-in** on level start (no more slamming on at full volume)
- ✅ **Music ducking** on death — volume drops *and* a low-pass filter muffles it during
  the slow-mo, then it swells back as you respawn
- ✅ **Floating "+1" score popups** with an ease-out + fade tween
- ✅ **Dramatic slow-motion death** + poof + shake + screen flash + music duck
- ✅ **Camera look-ahead** (leads in your move direction) **+ zoom "punch"** on land/stomp/death
- ✅ **Hit-flash**: white blow-out on a stomped slime + a red full-screen flash on death
- ✅ **Motion-trail after-images** streaking off the double jump and stomp bounce
- ✅ **Spawn pop-in** (the player squashes into the level on start)
- ✅ **Combo HUD pop** (score label springs + warms from white → orange as the combo climbs)
- ✅ **Coin idle bob** (a gentle float so pickups feel alive)
- 🔜 Parallax background layers

### C. Reward & progression
- ✅ **Coin combo system** (chain coins within 1.5s; combo shown in the HUD)
- ✅ **Persistent on-screen HUD** (score + combo always visible — it used to scroll
  off-screen as a world-space label)
- ✅ **Stompable enemies** (the fun action is now also the powerful one)
- ✅ **Teaching labels** updated to surface the new verbs (double-jump, stomping)
- 🔜 Win/goal flag with a celebration, level timer, high-score save

---

## Part 3 — Implementation Details (file by file)

### `scripts/juice.gd` *(registered as an autoload singleton `Juice`)*
Global "feel" helpers any object can call:
- `Juice.hit_stop(duration)` — freezes the game (`Engine.time_scale = 0`) for a real
  time duration, then restores it. The freeze-frame on impacts.
- `Juice.spawn_burst(pos, color, amount, speed)` — spawns a self-cleaning one-shot
  `CPUParticles2D` burst. Used for coins, slime splats, the death poof, and the
  double-jump puff.
- `Juice.flash_screen(color, duration)` — flashes a colour across the whole screen on
  its own top-most `CanvasLayer`, then fades it out. Used for the red death flash; a
  reusable "that mattered" cue for any big event.

### `scripts/player.gd` *(rewritten — the core of the feel)*
All tuning constants live at the top of the file. Highlights:
- **Acceleration/friction**: `ACCELERATION`, `FRICTION`, `AIR_ACCELERATION`,
  `AIR_FRICTION` via `move_toward` instead of snapping `velocity.x`.
- **Coyote time / jump buffer**: `coyote_timer` & `jump_buffer_timer` countdowns; a
  jump fires when both a buffered press and ground-grace exist.
- **Variable jump height**: releasing jump while rising multiplies `velocity.y` by
  `JUMP_CUT` (0.45).
- **Asymmetric gravity**: `RISE_GRAVITY_MULT` (0.9) vs `FALL_GRAVITY_MULT` (1.5),
  clamped to `MAX_FALL_SPEED`.
- **Double jump**: `MAX_AIR_JUMPS`; refreshed on landing and on a stomp.
- **Apex hang time**: in `_apply_gravity()`, when `|velocity.y|` is under
  `APEX_SPEED_THRESHOLD` the gravity multiplier is scaled by `APEX_GRAVITY_MULT` for a
  beat of float at the top of the arc.
- **Corner correction**: `_corner_correction()` runs just before `move_and_slide()`. If
  you're rising into a ceiling, it uses `test_move()` to probe a few pixels left/right
  (`CORNER_CORRECTION_PIXELS`) and nudges you past the corner instead of bonking.
- **Squash & stretch** *(Mario-style spring)*: instead of a one-shot tween, a damped
  spring drives the sprite scale every frame in `_update_squash()`. `_squash_impulse()`
  just "kicks" the spring — positive = squash (wide/short, for impacts), negative =
  stretch (tall/thin, for take-offs) — and the spring carries it back *past* neutral
  with a lively overshoot (squash → stretch → settle), which is what sells the feel.
  Landing squash scales with fall speed (`LAND_SQUASH_MIN`..`MAX`); airborne, the rest
  pose is a subtle tall-stretch that grows with vertical speed (`AIR_STRETCH_MAX`); and
  the sprite slides down by `SQUASH_PIVOT` while squashing so the feet stay planted.
  Driven on spawn (pop-in), jump (stretch), land (impact squash), and stomp bounce.
- **Landing feedback**: `_check_landing()` reads the fall speed *before*
  `move_and_slide()` zeroes it, then scales dust + shake + a tiny zoom punch to impact.
- **Camera craft** (`_process`): layers a directional **look-ahead** (`LOOK_AHEAD`),
  a decaying **screen shake** (`add_shake()`), and a settling **zoom punch**
  (`add_zoom_punch()`) onto the camera. Runs in `_process` so it keeps animating even
  during the death freeze.
- **Motion trail**: `_update_trail()` / `_spawn_afterimage()` drop fading ghost sprites
  on a cadence while `trail_timer` is active — triggered by the double jump and bounce.
- **Varied SFX**: `_play_varied()` adds a little random `pitch_scale` to each sound so
  repeated jumps/lands never sound identical.
- **Stomp**: enemies call `player.bounce()`, which kicks the player up and adds the
  stomp sound, shake, zoom punch, a trail, a squash, and a hit-stop.
- **Death**: `die()` — a single, reusable death used by *both* falling off the map and
  touching an enemy: hurt sound, red poof, **red screen flash**, **music duck**, shake,
  zoom punch, slow-mo, then reload.

### `scenes/player.tscn`
Added child nodes the script drives: `DustParticles` (CPUParticles2D) and five
`AudioStreamPlayer`s — `JumpSound`, `DoubleJumpSound`, `LandSound`, `HurtSound`,
`StompSound` — wired to previously-unused sound assets.

### `scripts/slime.gd` + `scenes/slime.tscn`
The slime is now **stompable**. Its deadly area was replaced with a single `Hitbox`
`Area2D` whose `body_entered` decides:
- **Stomp** (player falling *and* above the slime's middle) → juicy squash-death
  (a white **hit-flash** that fades as it flattens + goo `spawn_burst`) and
  `player.bounce()`.
- **Otherwise** (side/below hit) → `player.die()`.

### `scripts/coin.gd` + `scripts/game_manager.gd`
- Coins now: a gentle **idle bob** (looping tween in `_ready`), a sparkle burst,
  **rising pickup pitch** with the combo, a floating **"+1" popup**, and a tiny camera nudge.
- `GameManager` tracks a **combo** that decays after `COMBO_WINDOW` (1.5s) and renders
  `Coins: N   xM combo!` to the HUD. Each pickup gives the HUD label a springy
  **scale-pop** (`_pop_label`) and **warms its colour** from white → hot orange as the
  combo climbs (settling back to white when the combo drops). Its `_ready()` also calls
  `Music.restore()` so the music fades in whenever a level (re)starts.

### `scripts/music.gd` *(new — attached to the `Music` autoload in `scenes/music.tscn`)*
Gives the background track dynamic behaviour instead of a fixed volume:
- **Fade-in**: starts near-silent (`START_VOLUME`) and `restore()` swells it to full when
  a level begins (called from `GameManager._ready()`, so it also covers post-death recovery).
- **Ducking**: `duck()` (called from `player.die()`) drops the volume to `DUCK_VOLUME` and
  sweeps a runtime-added **low-pass filter** on the Music bus down to `DUCK_CUTOFF`, muffling
  the track during the slow-mo death beat; `restore()` opens it back up on respawn.
- Sweeps use `set_ignore_time_scale(true)` so they stay crisp even while the game is in slow-mo.
- As an autoload, it persists across scene reloads — the music never restarts, it just
  fades/ducks/recovers seamlessly.

### `scripts/killzone.gd`
Now simply routes any falling body to the shared `player.die()`, so every death looks
and feels identical.

### `scenes/game.tscn`
- The world-space score label was promoted to a proper **HUD** on a `CanvasLayer`
  (top-left, outlined for readability) so score/combo are always visible.
- Teaching labels updated to surface the new verbs (double-jump, stomping).

### `project.godot`
Registers the `Juice` and `Music` autoload singletons (the `Music` autoload scene now
carries `scripts/music.gd` for the fade-in/duck behaviour).

---

## Part 4 — How to Tune It Yourself

The feel is deliberately concentrated in named constants. Want a floatier game?
Lower `FALL_GRAVITY_MULT`. Snappier? Raise `ACCELERATION`/`FRICTION`. More forgiving?
Raise `COYOTE_TIME`/`JUMP_BUFFER`. Bouncier stomps? Raise `STOMP_BOUNCE`. More
explosive juice? Raise the `amount`/`speed` args on `Juice.spawn_burst` and the
`add_shake()` values.

More of the newer knobs (all in `player.gd`):
- **Apex hang**: a longer "hang" at the peak → raise `APEX_SPEED_THRESHOLD` or lower
  `APEX_GRAVITY_MULT`.
- **Camera look-ahead**: how far it leads → `LOOK_AHEAD`; how snappily → `LOOK_AHEAD_SPEED`.
- **Zoom punch**: how hard impacts push the camera in → the `add_zoom_punch()` args;
  how fast it settles → `ZOOM_PUNCH_DECAY`.
- **Motion trail**: length → `TRAIL_DURATION`; density → `AFTERIMAGE_INTERVAL`; colour →
  `TRAIL_COLOR`.
- **Corner correction**: how generous the ceiling nudge is → `CORNER_CORRECTION_PIXELS`.
- **SFX variety**: the pitch spread → the `low`/`high` args of `_play_varied()`.
- **Squash & stretch**: bouncier recovery → lower `SQUASH_DAMPING` (more wobble) or
  raise `SQUASH_STIFFNESS` (snappier). Flatter landings → raise `LAND_SQUASH_MAX`.
  Taller take-offs → raise `JUMP_STRETCH`. More air stretch → raise `AIR_STRETCH_MAX`.
- **Screen shake**: the per-event strength → the `add_shake()` args (landing scales with
  fall speed; a normal hop is tuned to *not* shake); how fast it settles → `SHAKE_DECAY`.
- **Music** (in `music.gd`): fade-in length → `FADE_IN_TIME`; how quiet/muffled the death
  duck is → `DUCK_VOLUME` / `DUCK_CUTOFF`; how fast it ducks → `DUCK_TIME`.

**Golden tuning rule:** change *one* number at a time and *feel* it. Game feel is
subjective and lives in your hands, not on paper.

---

## References / Further Reading
- Steve Swink — *Game Feel: A Game Designer's Guide to Virtual Sensation* (the
  foundational book on this topic).
- Jan Willem Nijman (Vlambeer) — *"The Art of Screenshake"* (talk).
- Martin Jonasson & Petri Purho — *"Juice it or lose it"* (talk).
- Mark Brown (Game Maker's Toolkit) — *"Why Does Celeste Feel So Good to Play?"* and
  *"The Secret of Mario's Jump"* (videos).
- Celeste & Towerfall (Maddy Thorson/Noel Berry) — public talks/posts on coyote time,
  jump buffering, and corner correction.
