
What I built
my contribution — nothing here is third-party work
- Foundation (October 2026): a fresh Unity 6 project started from the empty Universal 2D template, so every later commit is diffable against a known-empty start. Assembly definitions for runtime and editor code, an Input System actions asset (move, attack, interact, pause; keyboard and gamepad), the Kenney Tiny Dungeon sheet sliced into 132 sprites with Tile assets and a palette, a playground room on Tilemaps with a merged composite wall collider, a pixel-perfect camera (320×180 reference, 16 px per unit) and a Rigidbody2D-driven player mover.
- Reproducible setup: the whole foundation is created by one idempotent editor script that runs headlessly from the command line — no hand-edited scene or project YAML.
- Combat core: weapons are ScriptableObjects (damage, knockback, hit-stop, windup / active / recovery / cooldown, reach, hitbox, swing arc). A `MeleeAttacker` swings any of them: after the windup it checks an oriented overlap box every frame of the active window and hits each `IDamageable` once per swing. Health lives in a plain C# `HealthPool` (clamping, invulnerability frames, death exactly once) wrapped by a thin component, so the rules are unit-tested without Unity. Feedback without extra assets: 50 ms hit-stop on the unscaled clock, colour flash and squash, ease-out knockback that movement scripts yield to, and a weapon sprite sweeping the arc. Attack input is buffered for 150 ms.
- Enemy AI: each enemy type is an `EnemyDefinition` asset (look, health, speeds, sight, attack pattern, cowardice) applied to one generic prefab at spawn. Behaviour is a small state machine whose states — Idle, Patrol, Chase, Attack, Flee, Recover — are plain C# objects that only see an interface: senses (sight range with hysteresis, line of sight against walls, a short memory of the last seen position, an alert after being hit) and actuators (move, stop, swing). Attacks are telegraphed on the floor from the weapon's hitbox during the windup, followed by a pause that is the player's punish window. Cowards flee at low health, recover out of sight, and cannot flee again for a while so they end up fighting. The boss, Cinderclaw, is the same brain with a cycling attack pattern (slam, slam, sweep); no boss-specific code.
- Progression and persistence: enemies carry XP and coin rewards as data and drop pickups that fly to the player; a plain C# progress model handles levels (each level adds max health and refills), XP carry-over and coins; a smith's station in the room sells better weapons through a small interaction layer (nearest usable object in range, one Interact action). Saving is a version plus a list of named JSON blobs — every component with state worth keeping writes its own small record — behind a swappable store: PlayerPrefs in the browser build, memory in tests. One session object decides when to load and autosave, so gameplay code never writes files.
- Interface and feel: a screen-space HUD (health pips, XP bar and level, coins, equipped weapon, interaction prompt) driven by the same events the gameplay raises, a death screen that restarts from the last autosave, pause on a single game clock that also owns hit-stop so the two can never cancel each other, pooled floating damage numbers, camera shake scaled by what happened, and ten placeholder sound effects synthesised from square, saw, sine and noise waves at start — no audio files, no licences to track, and one class to swap when recorded sounds arrive. None of the effects are known to the gameplay code; they subscribe to global events.
- Tests: EditMode tests cover the health pool, cooldown, hitbox geometry, every AI state transition through a fake context, the attack pattern, the level curve and progress model, the shop rules, the save format, the game clock and the sound synthesis (70 tests); PlayMode tests instantiate the real prefabs and data and check the sword kill loop, enemy chasing and biting, the bat's flee-and-recover, loot collection after a kill, level-up healing, shop purchases, a full save/load round trip, and the HUD, death screen, pause and damage popups against the real prefabs (17 tests). Both suites run headlessly from the command line like the builds.
- Floors and the run: two hand-made floors authored as text maps and built into scenes by the same editor script, a smith on each, an exit sealed by the guardian on the second floor, a win screen, and checkpoints on arrival so the build can boot into the floor a save belongs to.
- First playtest (a recorded run with commentary) drove a revision pass: the walls had no collision in the web build because a composite collider built by script produced no geometry at runtime, which a scene-level test now guards; mouse attacks aim at the cursor; damage numbers moved from world space to the HUD so they are legible at the pixel scale; enemies got health bars; health, enemy pacing and heart drops were tuned after a 36-second first death; a short controls hint appears on each floor.
- Most of the code was written with an AI assistant (Claude Code) under my direction: I set the scope and the order of the systems, decided the design rules (telegraphed attacks, data over code, one clock), reviewed every change and its tests, and chose what to cut. The commits say so.
- The earlier prototype (June–July 2025) followed a beginner tutorial and is not presented as my work; it is kept only as a frozen reference.
The game
Cinderhold is a short top-down dungeon crawler. You fight down two floors of slimes, bats and rats, level up for more health, buy a heavier weapon from the smith with the coins they drop, and face Cinderclaw, the guardian that seals the last exit. Every enemy attack is marked on the floor before it lands: step out of the red mark, then punish. It is small on purpose, a complete loop that runs in the browser.
The first version of this project, in 2025, was a tutorial follow-along. It is kept only as a frozen reference and nothing from it is reused; the current game started in October 2026 from Unity’s empty 2D template.
Technical breakdown
1. Enemies as small state machines
Each enemy type is an EnemyDefinition asset (sprite, health, speeds, sight range, an attack pattern and how cowardly it is) applied to one generic prefab at spawn. Behaviour is six states. They are plain C# objects that only see an interface of senses and actions, so every transition is tested against a fake instead of a scene.
The attack state carries the pacing rule that makes fights readable: swing, wait the swing out, then pause before deciding again. That pause is the player’s window to hit back.
public override EnemyState Tick(IEnemyContext ctx, float dt)
{
EnemyStats s = ctx.Stats;
if (!ctx.HasTarget) return new IdleState(s.RandomIdleTime(ctx.Random01()));
if (ctx.IsAttacking) return null;
if (_swung)
{
_pause += dt;
if (_pause < s.AttackPause) return null;
if (ctx.CanFlee) return new FleeState();
_swung = false;
_pause = 0f;
}
if (ctx.DistanceToTarget > s.AttackRange * LeaveRangeFactor) return new ChaseState();
Vector2 direction = ctx.TargetPosition - ctx.Position;
if (ctx.TryAttack(direction)) _swung = true;
return null; // weapon on cooldown: hold position and try again next tick
}
AttackState.cs — Tick, unedited. A state returns the next state, or null to keep running.
The boss is the same brain with a longer weapon list (slam, slam, sweep). There is no boss-specific code.
2. Floors written as text
Both floors are text maps inside an editor script, built into scenes from the command line: walls, doorways, props, the spawn, the smith, the exit and every enemy. Changing a floor means editing a few characters and re-running one build step.
####################
#.........#........#
#..S......#...B....#
#.........D........#
#....P....#........#
#.........#....S...#
#######D########...#
#.........$........#
#..B...............#
#........x...S....E#
#........b.........#
####################
Floor 1 as authored. # wall · D doorway · P spawn · $ smith · E exit · S slime · B bat · x crate · b barrel.
3. A playtest that became a test
The first recorded playtest showed the player walking straight through walls. The cause was a composite collider that, built by script, produced no geometry at runtime, and the unit tests could not see it because they never loaded a floor. The fix shipped with a test that loads Floor 1 and drives the player and an enemy into a wall, so the same failure cannot return unnoticed.
What’s next
Recorded sound in place of the synthesised placeholders, and a room generator that stitches hand-made rooms into floors.
Gallery


Credits
third-party assets used — with thanks
| Asset | Author | Licence |
|---|---|---|
| Tiny Dungeon tileset and characters | Kenney | CC0 |
| Kenney Fonts: Kenney Mini and Kenney Mini Square (UI) | Kenney | CC0 |
| Liberation Sans (TextMesh Pro's bundled default font, shipped but not used for display) | Red Hat | SIL Open Font License |
| Sound effects | generated procedurally in code — no recorded audio | n/a (own code) |