NewAlerts at Digibee — from proactive-monitoring research to a released feature.Read the case study

Solo project · design, code and art

Hollowmere

A grimdark top-down RPG where the only light that shows you the dead runs out while you hold it.

  • playtest coming soon
  • In progress
  • 576×360 internal
  • Canvas 2D · no engine
  • Godot 4.7 port underway
Hollowmere screenshot

The core loop

Two lights, and only one of them tells the truth

Every design decision in the game hangs off one rule: the safe light is useless against the thing that kills you.

The pitch torch. Wide, warm, lasts forever — and it will never show you a wraith. Radius 117px.
The pitch torch. Wide, warm, lasts forever — and it will never show you a wraith. Radius 117px.
The dark iron lamp. Narrower, green, and it drains while lit. Radius 93px. The only way to see the dead.
The dark iron lamp. Narrower, green, and it drains while lit. Radius 93px. The only way to see the dead.
Walking the village with the lamp raised. The pool of light travels with you and the ember trail follows a step behind, so the world is only ever revealed at walking pace.
Walking the village with the lamp raised. The pool of light travels with you and the ember trail follows a step behind, so the world is only ever revealed at walking pace.Recorded from the running build.

The wager is the whole game

Walk lit and burn charge, or walk blind and save it. Because the lamp both reveals and banishes, every encounter spends the same resource twice.

The HUD reads in peripheral vision

Health and lamp are liquid orbs, not bars — a moving surface catches the eye where a rectangle doesn't. You watch them while walking into the dark, not by looking at them.

Feedback is always diegetic

A refused action flashes the slot red. The carried light pulses its own belt slot. Exhaustion makes the stamina bar breathe. No text tells you any of it.

Torch radius117 px
Lamp radius93 px
Lamp drain2 / sec
Lamp regen1 / sec
Banish time3.6 sec

Combat

Committed swings, and a bar that punishes panic

Attacks run wind → strike → recover and cannot be cancelled. Facing follows the cursor, so aiming and moving are separate decisions.

The graveyard ambush. Five skeletons rise the moment you cross it. Watch the stamina squares drain as the swings land, and the screen flash when one connects with you.
The graveyard ambush. Five skeletons rise the moment you cross it. Watch the stamina squares drain as the swings land, and the screen flash when one connects with you.Recorded from the running build.
Mid-strike. Movement drops to 42% during a swing, so committing to an attack is a real position choice.
Mid-strike. Movement drops to 42% during a swing, so committing to an attack is a real position choice.
The Fallen Hunter, a mini-boss with a named health bar and a 0.37s telegraph. Stamina is down to four squares, health to 94/150.
The Fallen Hunter, a mini-boss with a named health bar and a 0.37s telegraph. Stamina is down to four squares, health to 94/150.

Stamina is ten squares, and no numbers

A swing costs a third of a square. Sprinting costs one per second. Empty it and you move and swing 30% slower until a whole square returns — the bar pulses the entire time, which is the only signal the penalty is on.

Never take the sword away

Running out of stamina makes you slow, not helpless. Attacks are still allowed at zero. The penalty stays legible without ever removing the player's only verb.

Stamina10 squares
Swing cost1/3
Sprint1 / sec · +50%
Exhausted−30%
Recovery3s, then 1.5s

Atmosphere

Weather, grain and a vignette, drawn every frame

320 raindrops, each with its own landing height so the rain strikes the ground all over the screen rather than at the bottom edge. A film grain and a vignette sit over everything.

Rain over the village square, lit by a torch. Every drop splashes where it lands; the grain and vignette are composited over the finished frame.
Rain over the village square, lit by a torch. Every drop splashes where it lands; the grain and vignette are composited over the finished frame.Recorded from the running build.

Lighting order is a design decision

Anything drawn before the lighting pass obeys the darkness; anything after is self-luminous. Every element is deliberately assigned to one side — which is why the HUD stays readable in a pitch-black cellar.

Audio is synthesised, not sampled

There are no sound files. Rain, wind, crickets, the sewer hum and every hit are generated with Web Audio at runtime, which is what keeps the whole game inside one file.

Systems

One surface language, four panels

Inventory, journal, skills and shop are the same leather-and-brass frame. Nothing in the UI uses a second visual vocabulary, so a new panel is never a new thing to learn.

50-slot pack with equipment on the left and a live portrait. Slots 1–9 double as the hotbar, so what you see here is what your number keys do. Equipping swaps rather than discards.
50-slot pack with equipment on the left and a live portrait. Slots 1–9 double as the hotbar, so what you see here is what your number keys do. Equipping swaps rather than discards.
Vows and found notes on two tabs. Progress is shown against the requirement (4 / 7), not as a percentage — the player counts the same things the game counts.
Vows and found notes on two tabs. Progress is shown against the requirement (4 / 7), not as a percentage — the player counts the same things the game counts.
Three skills bought with soul embers, earned by killing. Cost is 30 base and compounds 20% per rank already owned, so breadth is cheaper than depth.
Three skills bought with soul embers, earned by killing. Cost is 30 base and compounds 20% per rank already owned, so breadth is cheaper than depth.
Buy and sell in one frame. Stock is finite and leaves the shelf when bought. Soul powder is deliberately unsellable — it is the currency of the darkness wager and cashing it out would defuse the loop.
Buy and sell in one frame. Stock is finite and leaves the shelf when bought. Soul powder is deliberately unsellable — it is the currency of the darkness wager and cashing it out would defuse the loop.

Text below 8px is hand-plotted

Anything small is drawn from a 3×5 bitmap font rather than fillText, which turns to mush under the pixelated upscale. Counters and quantities are all bitmap glyphs.

Quest markers cannot lie

The ? above an NPC is computed from the same branch chain as their dialogue, in the same order. If he has something to say, he wears a mark — the two can never disagree.

World and story

Six areas, told through what people left behind

Most of the story is optional and physical: letters on bodies, notes in the mud, a key that belonged to somebody's brother.

Herbert, the quest giver, on the thing that walked into the village ten years ago. Dialogue scrolls and pages; choices are numbered so they can be taken by keyboard or mouse.
Herbert, the quest giver, on the thing that walked into the village ten years ago. Dialogue scrolls and pages; choices are numbered so they can be taken by keyboard or mouse.
The undercroft. Torchlight only reaches so far, and the chest is a landmark you find by its glinting lock.
The undercroft. Torchlight only reaches so far, and the chest is a landmark you find by its glinting lock.
A ruined house on the road north, its door nailed shut and marked with wards. Placement of every prop is generated and validated by flood fill, never hand-typed.
A ruined house on the road north, its door nailed shut and marked with wards. Placement of every prop is generated and validated by flood fill, never hand-typed.
The end of current content. Two honest options and an address for feedback — it says the game is unfinished rather than pretending otherwise.
The end of current content. Two honest options and an address for feedback — it says the game is unfinished rather than pretending otherwise.

Migration

Porting to Godot, one system at a time

The plan is deliberately unglamorous: finish the design in the fast build, then port it once the design has stopped moving.

HTML — the playable build. Same village, same spot.
HTML — the playable build. Same village, same spot.
Godot 4.7 — the port. Village geometry, collision, the figure, lighting, weather and HUD all reproduced.
Godot 4.7 — the port. Village geometry, collision, the figure, lighting, weather and HUD all reproduced.
Torch light in Godot, using CanvasModulate and PointLight2D rather than a composited lighting canvas. The radii are the same numbers as the original.
Torch light in Godot, using CanvasModulate and PointLight2D rather than a composited lighting canvas. The radii are the same numbers as the original.
Pulled back. Houses, trees, braziers, walls and the 520 scattered props all land in the same places as the HTML build — the random number generator was ported bit-exactly so the layout matches rather than merely resembling.
Pulled back. Houses, trees, braziers, walls and the 520 scattered props all land in the same places as the HTML build — the random number generator was ported bit-exactly so the layout matches rather than merely resembling.

The art is the code.

There are no sprite files to import. A character is a function that draws a body from parameters, so porting the art means re-deriving the renderer, not copying assets. One shared function draws the player, four villagers, skeletons, the zombie, the boss and the guards, separated only by palette.

Two different lighting models.

The original composites a lighting canvas over the world, so what obeys the darkness is decided by draw order. Godot uses CanvasModulate with 2D lights, where it is decided per-node. Every element had to be re-classified as darkened or self-luminous, deliberately.

Determinism had to be preserved.

Prop placement is generated from a seeded RNG. To get the same village rather than a similar one, the generator was ported bit-for-bit, matching JavaScript's 32-bit integer semantics exactly.

Nothing is trusted without a comparison.

Both builds gained a screenshot harness that poses a scene and renders it headlessly. Ported output is checked against the original frame by frame — the shared character renderer came back identical on 2,488 of 2,496 sampled pixels, and the eight that differed were the pulsing eyes, which are time-based.

Where the port actually stands

Ported: world geometry, collision, movement, the character renderer, the lighting model, weather, and the HUD. Not ported: combat, enemy AI, inventory, dialogue, quests and audio.

That split is the honest one. The Godot build is a proven feasibility spike, not a second game — it answered the expensive question (can this be reproduced faithfully?) and the answer was yes. Continuing it now would mean designing and porting at the same time, so the design gets finished in the build where iteration is fastest, and the port resumes when saves, controllers and a desktop release actually matter.

Status

Still being built

6Areas
4Quests
5Enemy types
0Dependencies
noneBuild step

Hollowmere — solo project, in progress. Design, code, art and audio.