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.
v0.3.2replaces the stub boss with Legion: he doesn’t attack, he splits into a brood that comes out of his body. (Tagged separately, but this is the first itch build to carry him.)v0.3.3is enemy AI v3: every swing used to be a fixed 0.35 s wind-up. Now it’s a choice — three moves you tell apart by the colour of the telegraph — plus timing you can’t fully predict and conditions that read your posture, your jump, your retreat.
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:
- Posture yes, execution no. Parrying and blocking break his posture and open a long damage window, but the
[E]throw-out prompt never appears — you don’t finish a prince by hand. His death is a throw-out, and it drops the first soul this game has ever produced (§7.1: one soul, one tattoo). - A hard cap on the brood — 4 in phase I, 6 in II, 7 in III. At the cap he doesn’t flash on an empty summon.
- The push is the first unparryable move in the game (§6.3 says princes get them). It covers his wind-up and hands control straight back; a dash still beats it, which keeps “the dash is the dodge” intact.
- The whole §4.2 hierarchy shipped alongside him. Spawn / named / prince is one field,
rank, on the base enemy — it governs exactly three things (the name plate, whether a throw-out is allowed, and poise). Everything else that makes an elite an elite is the@exportprofile fromv0.3.1, so an elite is a scene with different numbers, not a new class. Poise scales only what a plain hit buys — flinch and knockback — so a boss still opens up to a parry at full strength. Scaling that “in the obvious place” would have quietly deleted the posture game from every boss fight with the test suite still green.
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:
- Jab — fast, short, its type’s colour. Block or parry.
- Heavy — amber. Not unblockable: it costs 45 stamina to hold-block instead of 15, so two into your guard breaks it. The answer is the parry (its telegraph is 0.55 s, the widest window in the game) or a dash.
- Low — violet. The one move a held guard doesn’t catch. It has a positional answer — jump — which is why it isn’t a “prince-only unparryable”. Violet means get out of the way, and the same colour carries through to prince moves later.
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
- Held swings. Sometimes an enemy holds a fully-drawn wind-up an extra 0.15–0.35 s. The rule is absolute: the telegraph has to visibly continue — the swing polygon stays lit, the sprite holds the drawn frame. “He’s still winding up”, not “a timer quietly moved”. That’s the line between a delay and a lie.
- Whiff ≠ hit. An active phase that touches nothing — no hit, no block, no parry — gets
recovery × 1.4. One rule, and it opens footsies: baiting a heavy and punishing the whiff becomes a basic skill, and an enemy that swung at air looks punished. - Chains. After a hit or a block, an enemy may carry into a second (rarely third) swing instead of recovering. The chain link has a short 0.20 s wind-up, so you have to recognise “he isn’t done” immediately. Max three links, never two heavies in one chain, and no chain without an attack token. The 0.20 s is also a natural dash-catch — dash i-frames last 0.18 s — and nobody had to code that or read your input; the geometry does it.
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):
- Held guard over 1.2 s → the heavy’s weight ×3. Your guard is on screen, so the reaction is fair.
- You’re airborne and in range → the enemy holds the swing and hits on your landing. Kills the jump as a free escape without adding an anti-air animation.
- You whiffed a charged attack → the enemy in range engages immediately with its heaviest move — but never skips its own attack cooldown. Symmetrical to the whiff penalty above.
- You’ve been backing away for 2 s → the standoff ring tightens to 80%. Anti-kiting; the pressure rises out of the situation, not off a timer. (This one had a real bug: the “backing away” test was a per-frame threshold, 0.5 px, which at 60 fps rejected any retreat slower than 30 px/s and would have behaved differently at another frame rate. It’s a speed now.)
- You’re guard-broken → the enemy picks its fastest move, not its biggest. The break is a punish, not a surprise execution.
- The enemy itself is under 30% HP → heavier, and it stops holding distance. “Desperate” reads as a change of character, not a buff.
The referee, version 2
Two additions to EnemyDirector:
- A second, scarcer floor for heavies — at most one guard-breaking swing in the air across the whole fight. Two amber wind-ups at once are unreadable, and readability wins over depth.
- A flanker. With three or more enemies, one holds the ring on the far side of you. The ring is a position now, not a distance, so the same code walks around instead of a second movement mode. Your back used to be safe, which made a four-enemy fight a fight with a queue; now retreating isn’t automatically the right answer and position is a resource. (It took four separate conditions before a crossing would even finish — the flanker phases through allied bodies, doesn’t yield on the way, doesn’t stop to swing. And
min_enemies_to_flank = 3came out of a failing test that turned out to be a missing design rule: with two enemies, going around just leaves one in front of you, which is easier.)
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:
- deals double damage,
- and dumps a big chunk of resonance (until now a parry only fed resonance slowly, through posture).
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:
- Wave composition per phase — phase I is fast pressure, II adds a shieldbearer (a slow blocker that leads with its guard), III adds a ranged thrall so you can’t stand still.
- Formations instead of an even fan — a wall between you and him, or pincers on both sides of you.
- Reactive summoning — break through to him while his retinue is thinned and he splits immediately, wall formation, on its own shorter cooldown. “Hide behind the retinue” becomes an active behaviour, not just a ring.
- Phase III presses — he stops retreating and comes forward, waves enter as pincers, and a named elite comes out once.
- The retinue defends the broken boss. When his posture cracks and the damage window opens, the whole living retinue gets attack priority and converges on him. The window stops being free: you take three seconds on the HP bar and pay for them with a crowd on your back, or you give it up and clear the swarm first. That’s the shape of decision this whole version is chasing.
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.
| Action | Keyboard | Pad (Xbox) |
|---|---|---|
| Move | ← / → | left stick / d-pad |
| Jump (double) | Space / Enter | A |
| Melee (3-hit combo) | A — tap | RB |
| Charged attack | A — hold ≥ ~0.5 s | RB — hold |
| Third-hit variant | hold ↓ heavy · nothing = fast · ↑ = air launcher | same |
| Block / parry | D — hold / tap just before the hit | LT |
| Riposte | automatic — hit within 0.4 s of a parry, same enemy | same |
| Dodge (dash, i-frames) | Shift | LB |
| Shoot (8-way) | S | RT |
| Reload | R (auto on empty) | Y |
| Interact / throw-out | E | X |
| Pause / menu | Esc | Start |
| Overview (relic + tattoos + souls, read-only) | Tab — hold | Select — hold |
| Ledge grab | automatic; climb = jump / ↑, drop = ↓ | same |
How to run it
| System | File | Notes |
|---|---|---|
| Windows | ultraviolet.exe (+ ultraviolet.pck beside it) | SmartScreen may warn about an unsigned file — “More info” → “Run anyway”. |
| Linux | ultraviolet.x86_64 (+ .pck beside it) | chmod +x, then ./ultraviolet.x86_64. |
| macOS | Ultraviolet.app | Unsigned: 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
- Placeholder art. The player has a stand-in sprite pack; enemies, terrain and UI are coloured rectangles (red melee / yellow archer / blue blocker / big purple Legion; an elite is its own colour plus a name plate). The swing colours — jab / amber / violet — are a separate layer from the body colour.
- Placeholder sound. Rough stand-ins, two music tracks on loop.
- Nobody has played phases 4–8 of
v0.3.3yet. The heavy telegraph (0.55 s), held-swing chance (35%), whiff penalty (×1.4), riposte window (0.4 s), posture chips (10/20/30/12), reveal lead (0.15 s) — all tuning knobs, not final values. - Legion’s arena is a flat 1280×360 room with one ledge. Whether it can carry a 6–7 enemy crowd fight is an open question — if not, it gets two platforms, not a rebuild. 60 HP and the 4/6/7 brood caps are numbers off the ceiling.
- Balance is untuned — damage, enemy HP, costs, the per-room enemy-count ramp.
- No death screen, no attempt counter, no meta-progression beyond tattoos (and now the first soul from Legion).
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:
- Can you explain every death? “I didn’t read the colour”, “I panicked into a dash”, “I stood in block” — fine, that’s difficulty. “I had no way to see that” — that’s a bug. Tell me which deaths were which.
- The heavy — is a 0.55 s telegraph too slow (reads as an invitation) or right (the widest parry window in the game)? Is 45 stamina to block it a cost, or a sentence?
- The flanker — did a 3–4 enemy fight become different, or just harder? Did retreating stop being the automatic answer?
- The riposte — did the parry become worth doing, or is dodging still just better?
- Legion — can you ignore the brood and burn the boss down? Is the flat arena big enough to split the swarm, or is it one heap against a wall? Is the posture-break window a real decision or an obvious one-way call?
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