The Serpent Lies Still is a Dark Souls style action RPG for PC, being built as a vertical slice: one church, a handful of enemies, one boss.
This is the first dev update. The day went into the machinery that everything else will come out of, and into finding what that machinery gets wrong.
Nearly everything is built in code
The rule for the slice is that the project makes its own content. Textures, materials, meshes, animation clips and audio cues are generated by commandlets run against the engine. The characters ride the engine's stock mannequin skeleton, and the title screen is a painting. Almost nothing else you see is a file somebody sculpted. A level is a program that places things.
That makes every bug a generator bug, which is mostly good news: fix it once and every piece it produced is fixed.
Two crashes with one shape
Two different generators crashed the editor on the same assertion.
The weapon kit closes a looped tube by appending the first point of its path to the path. The architecture kit's profile helper did the same with P.Add(P.Last()). Both hand Add a reference to an element of the array they are growing. If the array reallocates, the reference dangles, and Unreal's TArray asserts on exactly that.
Both now copy the point first. The second crash only showed up once the first was fixed, so every source file was then searched for any Add, Emplace, Insert or Append of an element of the same array. None remain.
A material that never compiled
A review of the generated art found that 27 of the 34 material instances, every stone, wood, metal, cloth and leather one, were drawing as the engine's default grid.
The master material's mask parameter expects a linear texture and defaulted to an sRGB one. On the SM5 path that failed to compile, and every instance built on it fell back. The fix was one default texture. The build never reported it; looking at the pictures did.
Animation as code
Player and enemy animation is authored the same way. A pose solver distributes bend across spine and neck, runs two-bone IK with hinge-aligned elbows and knees, spreads twist along the forearm and calf, and curls the fingers. Keyed hand and foot placements are baked at 30 fps with a per-frame constraint that keeps a two-handed weapon's haft between the hands. Then every baked frame goes through a lint: reach, ankle, floor, blade through the body, hand in the torso, wrist bend, forearm twist.
The lint was wrong first. The forearm check compared the hand against a reference pose, which is ambiguous at 180 degrees. It was replaced with an anatomical measure: the grip's thumb side about the forearm, taken from the elbow's flexion side, where 0 is a handshake grip and a real forearm reaches about plus or minus 90. Calibrated, the reference arm reads 21 and the neutral pose reads minus 22.
Two fixes went into the bake itself, so they apply to every clip:
- Weapons are pitched about the grip just enough to keep both ends above the floor. Clips with a weapon below the floor went from 50 to 2.
- Follow-throughs that would carry the blade back through the legs or torso are eased toward the contact. Overhead chops now finish with the tip at the floor in front of the figure.
Most frames the lint still flags are wind-ups with the hand close behind the shoulder. Close-ups of three clips showed no candy-wrapper twisting, because the solver spreads the twist over the twist bones, so those cases are logged rather than forced.
A CPU sketch renderer turns each clip into a three-view filmstrip of capsule figures with the blade-tip path over it. All 306 clips sketched at that point took under a minute. A single GPU screenshot took minutes on the integrated GPU the game is being built to run on. By the end of the day the library stood at 317 clips and 17 weapon-family move sets with 259 moves, and the move-timing lint reported no problems.
A combat system that survives a slow frame
Several decisions in the first full combat pass are about frames that arrive late.
Input buffer. One slot, latest press wins, 0.20 seconds of life. An attack pressed before the combo window opens is dropped. A roll pressed mid-action is held and fires at the dodge-cancel point or the end of the action. Nothing fires during hit-stop, and a test later caught the remaining hole: a roll pressed just before a swing connected expired while the player was frozen, so the buffer no longer ages during hit-stop.
Hit sweeps. Each frame sweeps from the previous frame's blade position to the current one against every active hit window, including a window that opened and closed between two frames. Procedural blades are sub-stepped at 30 Hz. A 0.05 second window is still hit at 4 fps, and a test says so.
Poise.
| stagger | open to riposte | |
|---|---|---|
| ordinary enemy, poise broken | 1.2s | 1.2s |
| player, poise broken | 0.75s | none |
Enemies do not riposte, so the player's stagger has no window to open. A riposte out of a parry's recovery multiplies the weapon's critical ratio by 2.5, a backstab by 2.0, and a guard break opens the victim to a riposte.
The first data pass was written against the wrong numbers
An audit of the first data pass against the design number sheets found enemy health, poise and rewards at two to three times the specified values, class stats and starting gear that did not match, and six invented skill trees where the sheets specify four trees of eight.
The data was rewritten from the sheets. The importer also learned to delete assets whose ids no longer exist in the JSON, so a removed spell or skill node stops loading into the runtime. The first clean import after the rewrite loaded 179 definitions and removed 54 stale ones, among them the old Powers, 39 skill nodes, 5 spells, 4 weapons and 3 items.
One deliberate change from the sheet: the boss's Rotfire absorption went from 60 percent to 35. At 60, the Hexborn class's main damage did less than half damage against the only boss. At 35 he stays resistant to his own element without making a Rotfire build a bad pick for the fight.
What the balance simulator said
A re-runnable Monte Carlo simulator reads the same data files and mirrors the runtime's stat, combat, vitals and boss maths, 300 runs per class per fight.
| target | simulated | |
|---|---|---|
| boss fight, median, level 10 | 4 to 7 minutes | 1:09 to 1:16 |
| Falling Cleaves to kill a light or robe build | 3 to 4 | 2 to 3 |
At the specified 2,600 health the fight is a little over a minute. A 2.5 minute fight would need about 2.4 times the health. Reaching level 10 costs 14 to 20 farming loops, or 10 to 18 minutes.
The armour breakdown shows where the lever is. Falling Cleaves to die, level 10 to 12:
| armour weight | hits to die |
|---|---|
| heavy | 3.1 to 3.3 |
| medium | 2.9 to 3.1 |
| light | 2.2 to 2.4 |
| robe | 2.1 to 2.2 |
| cloth | 1.9 to 2.0 |
Only the heavy and medium classes land inside the 3 to 4 target, and chest absorption is the lever that moves the rest.
A later run with the boss's designed behaviour, its strings of attacks and phase-two openings, gave a median of 1:10 with a range of 0:55 to 1:33. An earlier hand estimate of 3.4 to 4.9 minutes was withdrawn. Arithmetic said three to five minutes and simulation said a little over one, and the simulation was right to disagree. The fight length itself is left as a design decision.
The simulator also caught a unit mismatch. Enemy absorption is stored as a fraction, and the runtime still reads it as a defence rating, which inflates player damage by 6 to 20 percent.
Lighting a church so it reads at combat range
Exposure is fixed rather than automatic, so moonlit stone sits below mid-grey and the Pyre's fixed 220 candela light reads as a warm pool instead of clipping.
The first renders showed milky interiors. Height fog went from 0.035 to 0.012 and volumetric extinction from 1.6 to 0.7, with a 45 metre volumetric distance and thinner local fog volumes. That fixed the milk and exposed the opposite problem: the nave read too dark and the west-window moonlight never reached the floor, because buttresses and deep reveals shade the glass from a north-west moon at 24 degrees. The answer was exposure at -0.3 EV100 with local exposure lifting the shadows, and unshadowed moon spotlights running along the line of the light from each window. The nave now has readable piers, pews and cold diagonal shafts toward the altar.
Sound: the loudest variant, the double hit, the stretched sting
The sound library is 426 effects. Each was scanned for peak, level and segments, and the bells, braams and drones were pitch-analysed so the music beds could share one key, D-sharp minor.
The first pass produced 125 cues from 306 variants. Three things were wrong with it:
- Level. Variants of a single cue sat up to 11.4 dB apart. Loudness is now measured K-weighted, so bass-heavy and bright takes compare as they are heard, and transient-heavy variants go through a look-ahead limiter instead of stopping at a ceiling. Within every cue the spread is now at most 2.0 dB.
- Double hits. Some parry, block and hit sounds contained a flam, two attacks a few milliseconds apart. A stage check now fails any such cue, and the offending sources were replaced or cut to a single attack. Every swing variant peaks at 0.10 seconds, give or take 0.008, so the sound lands on the swing.
- Duration. Pitch-shifting by resampling stretches a sound. The bell layer, dropped an octave, had turned the death sting into 22 seconds and the boss-felled swell into 15. They are capped at 8 and 11.
The finished stage builds 130 cues from 326 variants in about three minutes with no errors.
Renders that lied
Three separate renders on the same day were wrong for the same reason: the renderer was not ready.
- Item icons came back empty because the content commandlet runs as a non-client, so preview worlds get no scene to draw. The icon stage now marks itself as a client for the life of its preview world and logs how many pixels each capture covered.
- Early gallery shots drew no flames, fog or sky, because shaders for changed materials were still compiling in the run. Re-rendered with warm caches, they were correct.
- The first captures after compiling the icon shaders still drew the default material. A warm-up render that waits for the glow fixed it.
A similar trap in the character review: pitch-black torsos under hat brims turned out to be unlit shadow, not missing geometry. A per-part position probe settled it, and the review map got fill lights.
A test that can say blocked
The headless self-test grew from 52 passing early in the day to 107 by the afternoon. One change was to the meaning of a result rather than the count. A test whose timing depends on an animation clip that does not exist yet is now reported as BLOCKED, counted apart from passes, even when its state logic passes. Passing against a missing clip is not evidence. The afternoon run read 99 passed, 0 failed, 8 blocked.
