← Devlog
v0.3.3-pre-alpha

Ultraviolet v0.3.3-pre-alpha: a real boss, and a swing you can read

Two builds in one post. v0.3.1 put a referee between the enemies so a crowd fight could be read. These two make the enemies — and the boss — worth reading.

The correctness criterion for v0.3.3, in the same spirit as v0.3.0 (“plays the same”) and v0.3.1 (“plays better”):

it plays harder, but every death can be explained.

Not “more HP and faster swings”. The difficulty is meant to come from decisions you make wrong — you didn’t read the colour, you panicked and dashed, you stood in block — not from information you weren’t given.


The boss splits instead of swinging

Legion (Koncept §8) has one rule: he never attacks you. He stands in the arena with an HP bar across the bottom of the screen and a name plate above him, and every few seconds he flashes for about 0.6 s, plants himself, and a brood peels out of his body. The whole fight is the crowd.

Four design decisions drove it, and the first two resolve each other. Classic HP bar, damageable at all times — which normally invites “ignore the adds, hug the boss”. But the brood spawns from him, so standing on the bar is standing in the swarm, and the split shoves you back out of katana range on every wave. Crowd management isn’t a gate on the bar; it’s the price of admission to it.

The rest:

One bug worth the retelling: Health.invincibility_time defaults to 0.4 s and nothing had ever overridden it. Fine for a 3-HP spawn; on anything with an HP bar it caps you at ~2.5 landed hits a second and eats hits 2 and 3 of the katana combo. A boss with default i-frames reads as long, not hard. Legion runs 0.1 s, pinned by an assertion.


Three moves, three colours

Every ordinary enemy now carries a small list of AttackDef resources instead of three exported numbers. An enemy with an empty list behaves exactly as it did in v0.3.1 — that equality is the whole first phase.

The spawn gets two of them:

The blocker gets jab + heavy (it punishes turtling). Legion’s thralls get only a jab, faster — a swarm with a mix-up is unreadable, and phase III needs to stay readable. Elites get all three, plus the trick below.

The colour of the telegraph is the entire read. Pick the move by which band of distance you’re in, weighted, and let the weights bend under conditions.


Timing you can’t fully predict


Conditions — the enemy reads what it can see

All of these read visible player state, never an internal flag (a rule v0.3.1 broke once and fixed):


The referee, version 2

Two additions to EnemyDirector:


One new tool for the player: the riposte

After a successful parry, a 0.4 s window opens in which your next hit on that same enemy:

Why this and nothing else: the parry’s only reward was posture, so against tougher enemies you’d rationally skip it for the dash. The riposte closes that loop and gives an aggressive player an answer to pressure — no new button, no new system. The window is tied to the specific enemy you parried, so you can’t parry one and cash it on another.

And aggression chips posture now, not just the parry (34) and block-hit (22): katana 10, combo finisher 20, charged attack 30, hot shot 12 (a cold shot doesn’t chip). A full combo is 40 posture in about 0.9 s with regen suppressed. The parry route is faster (three parries); the aggression route is surer (it doesn’t need you to hit a 0.15 s window). That ordering is pinned by an assertion so tuning can’t quietly invert it.


Legion v2

§8 is untouched — he still never attacks. The depth went into what and how he summons:

One late addition, for elites only: the late reveal. An elite’s swing carries the enemy’s own colour first and only switches to the move colour (amber or violet) 0.15 s before it lands — exactly one parry window. It’s the honest version of a feint: you can’t resolve it early, but you can react. The animation had to keep the same secret — current_attack() withholds the move until the reveal, or the clip would give it away while the colour is still silent.


Controls

Unchanged, with one addition: the riposte is automatic — parry, then land a normal hit on the same enemy within 0.4 s.

ActionKeyboardPad (Xbox)
Move← / →left stick / d-pad
Jump (double)Space / EnterA
Melee (3-hit combo)A — tapRB
Charged attackA — hold ≥ ~0.5 sRB — hold
Third-hit varianthold ↓ heavy · nothing = fast · ↑ = air launchersame
Block / parryD — hold / tap just before the hitLT
Riposteautomatic — hit within 0.4 s of a parry, same enemysame
Dodge (dash, i-frames)ShiftLB
Shoot (8-way)SRT
ReloadR (auto on empty)Y
Interact / throw-outEX
Pause / menuEscStart
Overview (relic + tattoos + souls, read-only)Tab — holdSelect — hold
Ledge grabautomatic; climb = jump / ↑, drop = ↓same

How to run it

SystemFileNotes
Windowsultraviolet.exe (+ ultraviolet.pck beside it)SmartScreen may warn about an unsigned file — “More info” → “Run anyway”.
Linuxultraviolet.x86_64 (+ .pck beside it)chmod +x, then ./ultraviolet.x86_64.
macOSUltraviolet.appUnsigned: right-click → Open → confirm, or xattr -dr com.apple.quarantine Ultraviolet.app.

Quitting: Esc in the main menu closes the game. In a run, Esc opens the pause menu — quit to the main menu from there.


Still pre-alpha — what’s not here


How to report feedback

BUG — what happened, where (menu / hub / which room / boss room), how to reproduce, which platform. For a fight, say how many enemies, which body colours, and what colour the swing was that got you.

FEEL — it works, but how does it play? These builds are almost entirely feel, and the one question above all others:

Leave feedback in the comments under the game or this post. Testers with the full guide have a focused checklist there.

Thanks for playing something that’s still mostly rectangles.

— SolarNine · v0.3.3-pre-alpha