Lee Hardy Dydo

The Diablo Duck

October 16–17, 2026 · Diablo Duck, part 1 of 3

A yellow rubber duck with a sword and shield waddling, in Diablo's palette, from three angles
The finished duck, walking. These are the actual game sprites, decoded from the files the game loads.

As a kid I used game trainers for Diablo: little programs that found your gold in memory and let you change it. Last Friday I wanted to know how they worked. Two days later, Diablo's warrior was a rubber duck with eighty-five animations, a voice line in my own voice, and two menus inside the game that didn't exist on Thursday.

I did it with Claude Code, Anthropic's LLM coding tool, in my terminal. It generated nearly all of the code. I made the calls: what looked wrong, what mattered, when to stop and preview. This post walks through it in order, including the wrong turns, because the wrong turns are where the interesting parts are. It's long. It's also written so another LLM could pick this project up later; there's a structured summary at the end.

1A trainer the 1998 way

The game is DevilutionX, an open-source rebuild of Diablo that runs natively on a modern Mac, with the free shareware data. The original trainers had no map of where anything lived in memory. They searched for it.

You have 112 gold. Scan the whole game for the number 112: about seventeen thousand hits. Pick up some gold so you have 122, and scan only those hits for 122. Two addresses left. Write 5,000 to both, and the game shows 5,000.

Why two? One is the total; the other is the gold pile item in your inventory. The game recalculates the total from the piles, so writing only the total gets undone. That was the first lesson: the number on screen is often derived from something else.

From there the trainer became a small web page with live Gold, Health and Mana, a Lock button, and later commands to learn spells and max out stats. It reads and writes the game's memory directly. That needed a copy of the game re-signed so macOS allows another program to attach to it.

2The first duck, and why image generators failed

The warrior's sprites are pre-rendered: Blizzard built a 3D model in the 90s and rendered it from eight angles, every frame of every animation, then reduced the results to a 256-color palette. Each animation is a file of eight strips of frames.

The first try was the obvious one: ask image generators for a duck sprite sheet.

A sheet of duck sprites from an image generator, each frame a slightly different duck
A sheet from an image generator: a different duck in nearly every frame.
One duck frame from an image generator
One frame. Charming, and useless for animation.

Every frame was a slightly different duck: different bill, different eyes, different proportions. Animation needs the same duck in every frame. The fix was to do what Blizzard did: one 3D model in Blender, rendered from eight angles. Since it's always the same model, it's always the same duck.

3Waving, then slapping, then slicing

The first 3D attack looked fine in a still and wrong in motion. The duck swept his sword sideways across his front, as if waving it at the monster. The swing was turning the wrong way. Fixing that gave an overhead chop that hit with the flat of the blade: he was slapping enemies. The blade's wide side faced forward. Turning the blade a quarter turn on its own axis made him slice.

Widget: the attack, version by version

Attack animation

Each of these meant a full render, minutes long at the time. So the second thing built was a preview: a few key frames from a few angles at low quality, in seconds, with a red box showing the game's frame so you can see what gets clipped. The shadow running off the left edge got caught that way.

4The palette fights back

In the dungeon the duck glowed like a toy under studio lights. Diablo's art is painted dark. Darkening him turned him khaki, because the game's palette has almost no dark, saturated yellows; every darker yellow lands on the nearest color it has, which is olive.

The warrior next to the duck at four brightness and saturation settings
Choosing a tone: the warrior on top, then the duck at four brightness and saturation settings. The palette decides more than the renderer does.

Then the bill turned blue. An earlier experiment had added a palette swap that recolored the warrior's shield blue. The game applies it to every warrior sprite, including the duck, and it repainted a range of reds as blues, which included the duck's darker oranges. The fix was to drop the swap, since the duck's shield is blue in the model anyway. The lesson stuck: anything that recolors a sprite at load time recolors everything that uses those colors.

5Seeing changes immediately

Partway through I asked for a talk I'd seen years ago, Bret Victor's Inventing on Principle. His principle is that creators need an immediate connection to what they create: make a change, see the effect, nothing hidden. I'd adopted it as my own, and it became the test for the tools:

The animation previewer listing every warrior animation
The previewer: 85 animations, original and duck, with sound.
The 3D duck viewer in a browser
The 3D viewer, live as the model changes.

6From snowman to rubber duck, using a real duck

Seen next to the warrior at game size, the duck was a snowman: two stacked balls, a disc for a bill, boxes for feet. I took photos of an actual rubber duck I have, a black eight-ball one, from the side, the front and the top. The best ones were shot level and square-on, the way an artist would set up reference.

A black rubber duck photographed from the side
Side.
The duck from the front
Front.
The duck from above
Top.

Instead of eyeballing proportions, a script traced the duck's outline from each photo, rendered the 3D model from the same angle at the same height, and overlaid the two. White is where they agree, green is only the photo, magenta is only the model. Each round, Claude Code generated an edit to the model script and the overlay ran again.

Overlay maps comparing the model's outline with the photo's, side and front
The final fit: about 99% of the photo's width at equal height from the side, 96% from the front. The magenta notch at the top is the bill and eye, which aren't part of the traced outline.

The body is built from metaballs, blobby shapes that melt into each other, so the head joins the body without a seam. One trick: a "negative" blob that carves material away, used to cut the dip between the head and the tail.

Then came the details, each from a note of mine and a round of previews:

The earlier snowman-like duck walking
Before: the snowman.
The photo-fitted duck walking
After: fitted to the photos.
Close-ups of the duck's face from three angles
The face: decal eyes, spoon bill, and the tuft.

7Eighty-five animations

The warrior has one animation set per weapon setup: sword and shield, sword, shield, nothing, club and shield, club, bow, staff, axe. Each has a stand, a walk, an attack, getting hit, three spell casts, and a town stand and walk. The shield setups add a block, and only the unarmed set has a death. In the shareware data that's 85 files.

Widget: play the real sprites

Click a direction. These are the sheets the game uses: eight directions, every frame, in the game's palette, at game speed.

Some things only showed up when every animation sat side by side:

Repair cast with a dust tornado
Repair, with its dust tornado.
Fire cast
Fire.
Lightning cast
Lightning.
Staff sweep attack
The staff sweep.
Death: the duck goes belly-up
Belly-up.
Getting hit: a squash
Getting hit: a squeeze.
The start of the full reel: every animation, all eight directions, with the game's sounds. Made by a script from the actual sprite files.

8From eighty minutes to eight seconds

A full rebuild of every animation started at well over an hour. Measuring where the time went, instead of guessing, found most of it was overhead:

ChangeWhy it helped
Cycles to EEVEEPath tracing is wasted on a sprite that ends up 33 pixels tall in 256 colors. The real-time renderer was about 15 times faster per frame and looked the same at game size.
16 samples to 4Another 2.2 times; a side-by-side at game resolution showed no visible difference.
One Blender session per workerStarting Blender for every animation cost seconds each.
Parallel workersThe M3 had spare capacity.
numpy for the palette conversionThe pixel-by-pixel Python loop was the slow step once rendering got fast.
Skip what didn't changeMost edits touch one animation.

Changing one animation now takes about 8 seconds, start to finish, including the reload in the running game. Changing the whole duck takes about fifteen minutes. Getting that under a minute would mean renting GPUs in the cloud, or putting an old PC to work. That's a later problem.

9My voice as Cain

Cain the Elder greets you with his famous line. A small script records your own version: a beep, you speak, it trims the silence, matches the original's loudness, converts it to the game's format and installs it. The game streams voice lines from disk, so the next time you click Cain, he's you. The first takes were choppy because the recording tool was dropping audio. Switching recorders fixed it, and the beep that started recording got trimmed out of the take.

Take 4, the one in the game: my voice as Cain, trimmed and level-matched to the original by the script.

10Menus inside the game

A web page for the trainer works, but I wanted it in the game. That meant building DevilutionX from source with a patch. Pressing T opens a trainer in the quest log's panel: god mode, fill health and mana, gold, level up, max stats, learn all spells, repair, and give a bow, an axe and a staff. Actions go through the game's own functions, so derived numbers stay consistent and everything saves normally. The memory edits from outside didn't survive a restart; these do.

Before the patched game ran, a script (generated with Claude Code) drew the menu offline from the game's own files: the panel art, the font, the spinning pentagram selector, placed with the same coordinates the code uses. The mockup and the real thing match.

Mockup of the trainer menu drawn from game art
The mockup, drawn from the game's bytes.
The trainer menu in the running game
The real menu in the game.

Y opens the item cycler: the inventory panel with arrows beside the hand slots. The arrows preview every weapon and shield, and the backpack area becomes a readout of what's equipped against what's previewed: damage or armor, requirements, and which of the duck's animation sets it will use. Apply equips it. It's also the fastest way to check all nine weapon setups' animations.

The item cycler open in the game next to the trainer menu
The item cycler in the game: a Quarter Staff previewed against the equipped Short Staff.

11Side quests, parked

12Next installment

Diablo's warrior has three armor classes, light, medium and heavy, each with a complete set of animations. The game picks the set from your chest armor. That's 255 animations, 170 more than the duck has. The shareware data only includes light armor, so the other two need the full game's data. And the duck needs a design for it: perhaps a little helmet for medium and a breastplate for heavy. That's part two.


For the next LLM: structured summary

Project: ~/Projects/diablo-trainer (git). Game: DevilutionX 1.5.5, shareware spawn.mpq. setup.sh fetches pinned dependencies.

AreaFilesNotes
Traineragent.py, web/, scan.pymach memory access to a re-signed copy (get-task-allow). Player record found by name; offsets verified live. agent.py reload clears sprite slots so new files load.
Duck modelduck_render.pyBlender script: metaball body, sculpted bill, decal eyes, webbed feet, weapons on pivots, a guard pose per weapon, a pose() per animation, effects as a second pass (DUCK_PASS=fx).
Buildbuild_fast.py, render_batch.py, duck3d_np.pyEEVEE, 4 samples, parallel Blender sessions, numpy conversion to the game palette (player colors 128-255), skip-unchanged stamps.
Checkingpreview.sh, look.sh, fit_photo.sh, make_reel.pyPreviews, one frame at game resolution, photo outline overlays, MP4 reel with sound.
Live toolsweb/app/preview, web/app/duckNext.js: animation previewer (watches mods/), 3D glTF viewer (reloads on script edits).
Game patchpatches/devilutionx-1.5.5-trainer-menu.patch, build_game.shT trainer menu (quest log panel), Y item cycler (inventory panel), god mode hook. Build flags documented in the script.
Voicevoice.shSoX recording, cue trimming, loudness match, MP3 into mods/sfx/.
Docsdocs/animations.md, docs/pipeline.md, docs/todo.mdRunning list, pipeline decisions, parked ideas.

Gotchas worth knowing: image generators can't keep a character consistent across frames; palette swaps (TRN) recolor every warrior sprite; the warrior's spell effects are baked into his sprites; gold is items plus a derived total; memory edits don't persist across a restart, game-function edits do; check the screen is unlocked before sending keystrokes; ask "bow and arrow or bo staff?"