← Back to projects

Game · Godot

Momo: The Curse

GodotGDScript
Momo: The Curse screenshot

Why a Horror Roguelike

After A Real Man's Game, a fighting game, I wanted to try a completely different genre: 2D roguelikes. The Binding of Isaac was the biggest inspiration, a randomly generated level for every run, powerups, bosses, and "Cobb Can Move", another 2D horror game built around completing tasks in a level while avoiding a stalking monster. I combined both ideas into Momo: The Curse, themed around the Taiwanese found-footage horror film Incantation.

I picked Godot specifically to try an engine other than Unity. Unreal was too heavy for my computer and overkill for a game that didn't need advanced graphics, and Godot's own language, GDScript, reads a lot like Python, which made the switch easier than I expected.

Pixel Art From Scratch

Godot doesn't have anything like the Unity Asset Store's flood of free character and environment packs, so almost every visual in this game, the monster, the interactable items, the player, the tileset, had to be made rather than imported. I worked across a few different browser-based pixel art tools, mainly Pixilart and Piskel, used AI to help rough out sprite designs, and hand-assembled the frames into spritesheets for animation myself. This was by a wide margin the most time-consuming and frustrating part of the whole project, and also the part I had the least prior experience with.

From a Hand-Drawn Map to a Real Generator

Player standing among torches and a battery pickup in a generated cave room, lit by flickering torchlight

My first version of the map was just me drawing directly onto the tilemap canvas. It looked fine once, but it meant every run was identical, which defeats the entire point of building something in the spirit of a roguelike. So I wrote a LevelGenerator script instead: it places a random number of non-overlapping rooms, connects each room to the next with a corridor carved between their centers, and then marks every empty tile touching a floor tile as a wall, which is what keeps the map fully enclosed without needing a separate pathfinding pass to check for unreachable rooms. Connectivity falls out of how the rooms are carved and linked in the first place, rather than being verified after the fact.

Godot's tile placement also reads the four neighboring tiles around every wall cell to pick the correct edge or corner sprite, so the map reads as a proper enclosed cave instead of a grid of identical square tiles.

Seven Levels, Hand-Tuned

Each of the seven levels is its own row in a config table: map size, room count range, and exactly how many batteries, torches, rocks, fuel canisters, and monsters to place, plus whether that level has a generator objective at all. Level 2 is where sound detection turns on for the monster, and level 4 is where the fuel and generator objective is introduced. Rather than one generator that scales smoothly, each level is a deliberately hand-picked difficulty step.

LevelMap SizeRoomsMonstersNew This Level
135 × 354–51Base loop: batteries, torches, escape
238 × 384–61Monster can hear you
341 × 415–62Generator objective introduced
444 × 445–72Fuel canisters required to power it
753 × 537–84Largest map, fastest and farthest-sighted monster

Placing Things Without Breaking the Level

Every item spawns from a shuffled pool of valid floor tiles, popped one at a time, so nothing ever lands on a wall or on top of something else. The exit gets placed in the last room generated, and every floor tile within 5 tiles of it is then removed from the pool so nothing spawns suspiciously close to the way out. On levels with a generator, it deliberately spawns as far from the exit as possible rather than randomly, which is what turns fetching fuel into an actual trek back across the map instead of a quick errand.

The monster is the pickiest to place. It retries up to 200 times, picking a random floor tile and rejecting it unless it's at least 10 tiles from where the player spawns, so a run can never open with the monster standing on top of you. Getting every one of these constraints, batteries spread out, generator and exit kept apart, monster far enough away but not on an unreachable tile, to hold at once across seven differently sized maps took a lot of trial and error.

Momo Doesn't Walk, It Drags Itself

The monster isn't a sprite that slides around, it's a two-bone inverted-kinematics rig: each hand has a "planted" world position, and every so often the game picks a new ideal spot for whichever hand is due to move, eases it there over a fifth of a second, and solves the elbow angle with the law of cosines so the arm bends naturally to reach it. The body itself just drags toward the midpoint of both planted hands. Every time a hand plants, it triggers a small camera shake and a drag sound whose volume and pitch depend on how close the monster is and whether it's chasing, which is a small detail that does most of the work in making it feel like something is actually crawling toward you rather than an animation playing.

Three Ways to Get Caught: Sight, Sound, and a Thrown Rock

The monster runs a small state machine: patrol, chase, and investigate. In patrol it wanders in a random direction for a few seconds at a time. It switches to chase the moment a raycast to the player is unobstructed and the player is within sight range, and drops back to patrol once the player gets far enough away, with a wider drop-out range than the spot-range so it doesn't flicker between the two states at the boundary. From level 2 onward it can also hear: the player's noise level scales with walking versus running, and if the monster is within hearing range of that noise, it starts chasing without ever needing a clean sightline.

Investigate is the one built around player agency rather than the monster's own senses: throwing a carried rock sends it flying in an arc, and when it lands, any monster within its noise radius gets sent to investigate that exact spot for a few seconds before giving up and returning to patrol. It's a distraction tool in the traditional stealth-game sense, and it was worth building because it gives a cornered player an option besides running.

One Base Class for Everything You Can Pick Up

Torches, rocks, batteries, fuel canisters, and the generator all extend the same Interactable base class, which owns the shared hold-to-interact logic: how long you need to hold the key, cancelling if you let go or walk away, and a progress callback each subclass can hook into for its own UI. Each item then only implements what's actually unique to it, a torch flickers and lights the area around it, a rock can be picked up or thrown, fuel gets carried to the generator. Rocks and torches even override the hold time down to zero so they interact instantly instead of needing a hold, without needing to duplicate any of the surrounding input handling to do it.

Sound and Light

Torches flicker by nudging their light energy up and down slightly every frame rather than staying at a flat brightness, which reads as fire instead of a static lamp. Almost all of the sound design, footsteps, the monster's drag, ambience, came from freesound.org, and it's the single thing that did the most to make the game feel unsettling rather than just looking unsettling. A jumpscare on capture pauses the game, plays out, and only then hands off to a game over screen, rather than cutting straight there.

Shipping to the Web

Unlike A Real Man's Game, which only ever shipped as a Windows download, I wanted this one playable directly in the browser on itch.io. Godot's web export brought its own round of issues I hadn't dealt with before, mismatched paths, things that behaved differently running in a browser sandbox than they did in the editor, that took real debugging to work through. It was worth it: a horror game people can just click and play, with no download and no convincing anyone to trust an .exe from a stranger on the internet, reaches a very different audience than a Windows-only build does.

What I'd Do Again

What held up across seven levels

  • Switching from a hand-drawn map to a real generator early. A repeated, memorized layout kills a roguelike before it starts, no matter how good the rest of the game is.
  • Keeping the monster's behavior to three named states (patrol, chase, investigate) instead of one tangle of conditionals. Every new detection mechanic, hearing, rock throwing, was just one more way to trigger a state that already existed.
  • One shared Interactable base class instead of five copies of the same hold-to-interact input code. Every new pickup item was a small override, not a rewrite.

Tech Stack

LayerTechnology
EngineGodot, GDScript
Procedural generationCustom room-and-corridor level generator
Character animationSkeleton2D / Bone2D inverse kinematics (monster), AnimatedSprite2D (player)
ArtHand-drawn pixel art (Pixilart, Piskel), AI-assisted sprite design
Audiofreesound.org sound effects
Distributionitch.io, HTML5 web export

Conclusion

Momo: The Curse is free to play in the browser on itch.io. It's a very different game from the fighting game I made right before it, procedurally generated instead of hand-built, atmosphere-driven instead of action-driven, and almost entirely hand-drawn instead of leaning on an asset store. Between the pixel art, the level generator, and getting a monster to convincingly drag itself across the floor, it took a long time and a lot of bugs to get here, but it's the project that taught me the most about building systems rather than just building a single scene.

Li Junyu

Solo build.