diff --git a/assets/_regen_batches.json b/assets/_regen_batches.json index b6f0538..0480794 100644 --- a/assets/_regen_batches.json +++ b/assets/_regen_batches.json @@ -1187,7 +1187,7 @@ "state": true, "check": "loose", "anchor": 0, - "subject": "Four frames of the FEMALE ZOMBIE TENNIS PLAYER shown in the FIRST reference image — the same dead woman: matted ponytail, red headband, torn red shirt, dark pleated tennis skirt, grey-green corpse skin, sunken eyes, a wooden racket in her hand. The OTHER reference images are a LIVING tennis player and are there ONLY to show the POSES to copy — never the character, never the clothes, never the skin. HER KIT IS FIXED AND MUST BE IDENTICAL IN EVERY FRAME: matted blond hair in a PONYTAIL, a RED CLOTH HEADBAND tied round her forehead, a torn red short-sleeved tennis shirt, a DARK PLEATED TENNIS SKIRT ENDING ABOVE THE KNEE, bare grey-green dead legs below it, white socks and white tennis shoes, and a wooden racket with a pale strung head. Frame 1: the READY STANCE — copy the pose of reference image 2 exactly: knees bent, weight forward on the toes, the racket held UP in front of the body in both hands, chest and face square to the viewer, waiting to return a serve. Frame 2: a SIDE SHUFFLE STEP toward the viewer — copy the legs of reference image 3, left leg driving out, the racket still held up ready in front. Frame 3: the same shuffle with the RIGHT leg driving out instead — identical to frame 2 in every other respect, only the legs swapped. Frame 4: a FOREHAND SWING at the moment of contact — copy the pose of reference image 4 exactly: the body rotated, the arm extended across, the racket head out to the side at waist height, weight on the front foot. Output exactly 4 figures side by side in ONE horizontal row, all the same height. " + "subject": "Four frames of the FEMALE ZOMBIE TENNIS PLAYER shown in the FIRST reference image — the same dead woman: matted ponytail, red headband, torn red shirt, dark pleated tennis skirt, grey-green corpse skin, sunken eyes, a wooden racket in her hand. The OTHER reference images are a LIVING tennis player and are there ONLY to show the POSES to copy — never the character, never the clothes, never the skin. HER KIT IS FIXED AND MUST BE IDENTICAL IN EVERY FRAME: matted blond hair in a PONYTAIL, a RED CLOTH HEADBAND tied round her forehead, a torn red short-sleeved tennis shirt, a DARK PLEATED TENNIS SKIRT ENDING ABOVE THE KNEE, bare grey-green dead legs below it, white socks and white tennis shoes, and a wooden racket with a pale strung head. Frame 1: the READY STANCE — copy the pose of reference image 2 exactly: knees bent, weight forward on the toes, the racket held UP in front of the body in both hands, chest and face square to the viewer, waiting to return a serve. Frame 2: a SIDE SHUFFLE STEP toward the viewer — copy the legs of reference image 3, left leg driving out, the racket still held up ready in front. Frame 3: the same shuffle with the RIGHT leg driving out instead — identical to frame 2 in every other respect, only the legs swapped. Frame 4: a FOREHAND SWING at the moment of contact — copy the pose of reference image 4 exactly: the body rotated, the arm extended across, the racket head out to the side at waist height, weight on the front foot. Output exactly 4 figures side by side in ONE horizontal row, all the same height. The four figures MUST NOT TOUCH OR OVERLAP each other in any way — leave a WIDE band of empty magenta between every pair, wider than a figure, including between their rackets and their outstretched arms. " }, "racket_f_w1": { "sprites": [ diff --git a/assets/z_r1_f.png b/assets/z_r1_f.png index c4ee1ce..2e8f15d 100644 Binary files a/assets/z_r1_f.png and b/assets/z_r1_f.png differ diff --git a/assets/z_r2_f.png b/assets/z_r2_f.png index bd2d0f8..d9a02ea 100644 Binary files a/assets/z_r2_f.png and b/assets/z_r2_f.png differ diff --git a/assets/z_r3_f.png b/assets/z_r3_f.png index 9a5707c..63ca559 100644 Binary files a/assets/z_r3_f.png and b/assets/z_r3_f.png differ diff --git a/assets/z_r4_f.png b/assets/z_r4_f.png index bd8ac8d..9b47862 100644 Binary files a/assets/z_r4_f.png and b/assets/z_r4_f.png differ diff --git a/assets/z_rw2_f.png b/assets/z_rw2_f.png index 5ae15a6..400d3f2 100644 Binary files a/assets/z_rw2_f.png and b/assets/z_rw2_f.png differ diff --git a/assets/z_rw2b_f.png b/assets/z_rw2b_f.png index 97e420f..3e7f2df 100644 Binary files a/assets/z_rw2b_f.png and b/assets/z_rw2b_f.png differ diff --git a/assets/z_rw2s_f.png b/assets/z_rw2s_f.png index cf76e9a..038a106 100644 Binary files a/assets/z_rw2s_f.png and b/assets/z_rw2s_f.png differ diff --git a/index.html b/index.html index 5ced213..5955b09 100644 --- a/index.html +++ b/index.html @@ -369,16 +369,24 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
-

BODY COUNT

+

BODY COUNT

+ +
+ + + +
- +
-

BY OPPONENT

+

BY OPPONENT

-

BY WEAPON

+

BY WEAPON

@@ -422,6 +430,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { - + diff --git a/src/beats.js b/src/beats.js index b1c3b89..1dca9f2 100644 --- a/src/beats.js +++ b/src/beats.js @@ -7,7 +7,7 @@ // (beats <-> gameplay and beats <-> zombies are runtime-only import cycles, same pattern as // input <-> gameplay: the imported bindings are only touched inside functions, never at load.) -import { TUNE, UI, bossArt, COURT, HOME } from './config.js'; +import { TUNE, UI, bossArt, matchRung, COURT, HOME } from './config.js'; import { G, P, O, ball, score, setupPoint, sendBoy } from './state.js'; import { voice, zombieDuck, aww } from './audio.js'; import { zombies, pellets, extras, shots, spawnRacketZombie, spawnChapterBoss, spawnZombie, explode, killPlayer } from './zombies.js'; @@ -258,6 +258,11 @@ function matchOpponent() { spawnRacketZombie(); const zb = zombies.find(z => z.variant === 'r'); zb.dueled = true; // wakes racketAI, same as a fresh duel + // WHO THIS ONE IS. The rung carries a name off the chapter roster and a set of dial scalings — + // pure data, read where the dials are already read (see matchRung in config). It rides on the + // BODY so that killZombie can file the kill under it without asking G what rung we were on. + zb.rung = matchRung(G.match.opp); + G.card = { title: zb.rung.name, sub1: zb.rung.line, color: UI.hot, t: 2.4 }; zb.hp = zb.hpMax = TUNE.matchOppHp; // a FLAT pool: with regicide gone the body is pure // scoreboard (a quarter per point), so a deeper pool would // change nothing — the ladder escalates in racketAI instead @@ -308,7 +313,10 @@ function matchGameWon(zb, banked = false) { G.runGames++; // the ladder metric, same as every mode G.hp = Math.min(G.hpMax, G.hp + TUNE.matchHeal); // a clean game is how you last voice('game_player', 0.3); cheer(2.4, 0.6); - G.card = { title: 'GAME.', sub1: 'NEXT OPPONENT', color: UI.pick, t: 2.2 }; + // GAME is the FLASH and the next opponent's name plate is the CARD. It used to be the other way + // round and matchOpponent's plate landed on the same slot a frame later, so 'GAME.' never drew + // once — and the plate is the better card anyway: it announces the win by replacing the loser. + G.flash = { text: 'GAME.', t: 1.4 }; d.opp++; matchOpponent(); // fresh body, its own intro beat d.ending = false; diff --git a/src/config.js b/src/config.js index 55ba142..ddb9a5c 100644 --- a/src/config.js +++ b/src/config.js @@ -667,6 +667,35 @@ export const BOSS_ART = [ export const bossArt = ch => ch % 10 === 0 ? BOSS_ART[BOSS_ART.length - 1] : BOSS_ART[(ch - 1 - Math.floor(ch / 10)) % (BOSS_ART.length - 1)]; +// ---- THE MATCH LADDER. Zombie tennis walks the SAME dead club staff onto the court as the gore +// mode's chapter bosses — the roster is the joke, and it is worth telling twice: in gore they come +// for you, here they play you at tennis. So rung N takes design N's NAME (bossArt, champion on +// ten) and a PERSONALITY, and the body stays the racket zombie's, which is the one drawn to play. +// +// A personality is DIALS, not new mechanics: every field below multiplies a number racketAI and the +// return shaping already read, so a rung is a data row and never a branch. All optional. +// pace scale on its returns' speed wide scale on how far off centre they may land +// flub scale on its whiff chance speed scale on how fast it covers the ground +// line the personality, said once, on the name plate as it walks on +const LADDER_TRAITS = [ + { line: 'HE MOWED THIS LAWN FOR THIRTY YEARS.', speed: 0.85, pace: 0.9, wide: 0.85 }, + { line: 'EVERY BALL OF YOURS IS OUT.', wide: 1.25, flub: 0.8 }, + { line: 'HE WILL NOT BE OVERRULED.', speed: 1.1, flub: 0.7 }, + { line: 'HE HAS SEEN YOUR BACKHAND.', pace: 1.15, wide: 1.1 }, + { line: 'HE IS HERE TO TREAT YOU.', speed: 1.2, pace: 1.1 }, + { line: 'THE CROWD LOVES HIM.', flub: 1.2, wide: 1.3 }, + { line: 'HE RESTRUNG THIS RACKET HIMSELF.', pace: 1.2, flub: 0.75 }, + { line: 'HE CAN HAVE YOU REMOVED.', speed: 1.15, wide: 1.2 }, + { line: 'WITH CREAM.', pace: 1.1, speed: 1.1, wide: 1.15 }, + { line: 'HE RETURNS EVERYTHING.', pace: 1.25, speed: 1.3, wide: 1.25, flub: 0.5 }, +]; +// Rung N's identity. The chapter roster's own walk is reused wholesale (bossArt), so the champion +// lands on rung 10 exactly as he lands on chapter 10 and the two rosters can never drift apart. +export const matchRung = n => { + const art = bossArt(n), i = BOSS_ART.indexOf(art); + return { name: art.name, key: 'L' + i, ...LADDER_TRAITS[i] }; +}; + // The roster, as the BODY COUNT screen reads it. Until now a variant's NAME lived only in the // comment above its hp value — fine while nothing ever had to print one. The key is the `variant` // char the zombie already carries, except that 'a' and 'b' fold together (they are one shambler in @@ -694,6 +723,12 @@ export const ENEMIES = { for (let i = 0; i < BOSS_ART.length; i++) // the chapter bosses, named where they already are ENEMIES['B' + i] = { name: BOSS_ART[i].name, spr: BOSS_ART[i].walk[0], boss: true, sub: i === BOSS_ART.length - 1 ? 'THE FINAL · EVERY TENTH CHAPTER' : 'CHAPTER BOSS' }; +// ...and the same roster again as MATCH opponents ('L0'..), because that is who walks on in zombie +// tennis. Separate keys, not shared ones: the two are different fights with different bodies, they +// are counted on different screens now (the mode split in stats.js), and one row cannot mean both. +for (let i = 0; i < BOSS_ART.length; i++) + ENEMIES['L' + i] = { name: BOSS_ART[i].name, spr: 'z_r1', ladder: true, + sub: LADDER_TRAITS[i].line }; // Which wound stage a body is in: 0 = unhurt, 1..n = the skins, spread evenly over its full health. // This is the rule the chapter boss already used, and it turns out to reproduce the brute's old diff --git a/src/gameplay.js b/src/gameplay.js index 4a6984f..3466b48 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -152,7 +152,9 @@ function doHit(ch, quality, aim) { // at ~27 m/s (baseShotSpeed + 0.55 quality) — crossing to your baseline in ~0.6 s, which is less // time than auto-run needs from the far corner. Opponent one gives you the flight time; the // rungs take it back. - s *= Math.min(1, TUNE.matchOppPace + (G.match.opp - 1) * TUNE.matchOppPacePer); + // ...and the RUNG's own pace trait rides on top, inside the cap: full pace is still the ceiling, + // a rung just gets there sooner or later than the flat ladder would (see matchRung in config). + s *= Math.min(1, (TUNE.matchOppPace + (G.match.opp - 1) * TUNE.matchOppPacePer) * (ch.rung?.pace || 1)); } if (G.match) { // THE OPPONENT'S WIDTH IS THE LADDER'S THIRD DIAL. Its returns scatter off aimError x (1 - 0.55 @@ -160,7 +162,7 @@ function doHit(ch, quality, aim) { // balls". Opponent one keeps its returns inside matchWideBase of the centre line; each rung // earns matchWidePer more width, so the line-painting is the LATE ladder, not the opening one. if (ch.side !== 'P') { - const wide = TUNE.matchWideBase + (G.match.opp - 1) * TUNE.matchWidePer; + const wide = (TUNE.matchWideBase + (G.match.opp - 1) * TUNE.matchWidePer) * (ch.rung?.wide || 1); tx = Math.max(-wide, Math.min(wide, tx)); } // THE PLACEMENT, MADE VISIBLE: your aimed rally hit stamps its landing point, and render draws a @@ -1047,7 +1049,8 @@ function racketAI(rz, dt) { // so a harder opponent is a FASTER one that flubs less. The BASE is boosted too — the duel's // 2.8 m/s lumber was tuned for one sudden-death point against a fresh player, and across a // whole game it read as "very weak and slow": opponent one already moves at matchSpeedBase. - const spd = TUNE.racketSpeed * (G.match ? TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer : 1); + const spd = TUNE.racketSpeed * (G.match ? (TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer) + * (rz.rung?.speed || 1) : 1); const step = Math.min(spd * dt, d); rz.dist = (rz.dist || 0) + step; // ground covered — his walk cycle steps off this, not the clock rz.x += dx / d * step; rz.z += dz / d * step; rz.runT += dt; @@ -1059,7 +1062,8 @@ function racketAI(rz, dt) { && ball.y < CONTACT.maxY && ball.y > CONTACT.minY) { // ...and the match has its OWN flub base: the duel's 0.2 hands a fifth of all returns away, // which across a game is an opponent that beats itself. - const flub = G.match ? TUNE.matchFlub * Math.pow(TUNE.matchFlubKeep, G.match.opp - 1) : TUNE.racketFlub; + const flub = G.match ? TUNE.matchFlub * Math.pow(TUNE.matchFlubKeep, G.match.opp - 1) * (rz.rung?.flub || 1) + : TUNE.racketFlub; if (Math.random() < flub) rz.swingCool = 0.5; // whiff — it hesitates (rarer per rung of the ladder) else { rz.swingT = TUNE.swingWindup + TUNE.swingWindow / 2; trySwing(rz, true, TUNE.racketQMax); rz.swingT = -1; rz.swingCool = 0.3; } } diff --git a/src/main.js b/src/main.js index 482e805..864341f 100644 --- a/src/main.js +++ b/src/main.js @@ -174,8 +174,14 @@ function start() { G.classic = pickedMode === 'classic'; if (pickedMode === 'zombie') { intoSplatter(); startMatch(); } } -let stPeriod = 'all'; -const openStats = () => { stats.drawStats(stPeriod, SPRV); go('stats'); }; +let stPeriod = 'all', stMode = stats.MODE_GORE; +// the stats screen opens on the mode you last PICKED to play — that is the one whose numbers you +// came to look at, and it saves a tap on the way in every single time +const openStats = () => { + stMode = pickedMode === 'zombie' ? stats.MODE_MATCH : pickedMode === 'classic' ? stats.MODE_CLASSIC : stats.MODE_GORE; + for (const el of document.getElementById('stModes').children) el.classList.toggle('on', el.dataset.m === stMode); + stats.drawStats(stPeriod, SPRV, stMode); go('stats'); +}; document.getElementById('mStart').addEventListener('click', () => go('setup')); document.getElementById('mStats').addEventListener('click', openStats); document.getElementById('mRanks').addEventListener('click', () => { go('ranks'); stats.drawRanks(); }); @@ -282,11 +288,18 @@ for (let ch = 1; ch <= 10; ch++) { bossList.appendChild(b); } // the three period tabs are one listener on the strip — the button carries the period in data-p +// the two tab strips are one listener each — the button carries its value in a data attribute document.getElementById('stTabs').addEventListener('click', e => { const b = e.target.closest('button'); if (!b) return; stPeriod = b.dataset.p; for (const el of e.currentTarget.children) el.classList.toggle('on', el === b); - stats.drawStats(stPeriod, SPRV); + stats.drawStats(stPeriod, SPRV, stMode); +}); +document.getElementById('stModes').addEventListener('click', e => { + const b = e.target.closest('button'); if (!b) return; + stMode = b.dataset.m; + for (const el of e.currentTarget.children) el.classList.toggle('on', el === b); + stats.drawStats(stPeriod, SPRV, stMode); }); refreshSplash(); diff --git a/src/render.js b/src/render.js index 5ecefa3..abac880 100644 --- a/src/render.js +++ b/src/render.js @@ -195,7 +195,7 @@ export function proj(x, y, z) { // Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the // menu screens hang tags on the same files, and two copies of this number is one copy too // many: the one that does not get bumped is the one that serves a cached 404. -export const SPRV = 92; +export const SPRV = 93; // The boot bar in main.js watches this. Nothing here CHANGES when things load — every sprite was // already requested at import — it just makes the wait visible and holds the menu back until the // last sheet has landed. See the onerror note below for the other half of "sometimes missing". diff --git a/src/stats.js b/src/stats.js index 5f56595..465458c 100644 --- a/src/stats.js +++ b/src/stats.js @@ -209,19 +209,14 @@ const wIcon = id => id === 'ball' ? 'w_ball' : WEAPONS[id] ? WEAPONS[id].icon : // ----------------------------------------------------------------- screens ---- // Both screens are built here rather than in main.js: they are the only readers of the shapes // above, and main.js is the boot file, not a view layer. -export function drawStats(period, v) { - const d = read(period); - document.getElementById('stPeriod').textContent = - period === 'last' ? 'LAST MATCH' : period === '7d' ? '7 DAYS' : 'ALL TIME'; - document.getElementById('stFigs').innerHTML = [ - ['KILLS', n(d.kills)], ['MULTI-KILLS', n(d.multi)], ['DEATHS', n(d.deaths)], - ].map(([k, val], i) => `
${k}${val}
`).join(''); - - // sorted by count, and only bodies you have actually met — an empty roster of everything would - // read as a checklist of things to go and do, which this screen is not - const rows = Object.entries(d.z).filter(([, c]) => c > 0).sort((a, b) => b[1] - a[1]); +// ONE SCREEN, THREE MODES. The three do not share a currency — see the record-shape note in +// endRun — so they do not share a layout either: gore counts bodies and weapons, zombie tennis +// counts NAMED opponents felled, classic tennis counts matches won and lost. What they do share is +// the furniture (the period tabs, the figure strip, the roster list, the footer), so this is one +// function with three sets of words rather than three screens to keep in step. +const roster = (el, rows, v, empty) => { const top = rows.length ? rows[0][1] : 1; - document.getElementById('stZ').innerHTML = rows.length ? rows.map(([k, c]) => { + el.innerHTML = rows.length ? rows.map(([k, c]) => { // a key with no row is a save written by an older build: name it off the key, stripped, because // this is the one string on the screen that does not come from our own tables const e = ENEMIES[k] || { name: k.replace(/[^A-Za-z0-9]/g, '').toUpperCase() || '?', spr: 'z_a1', sub: '' }; @@ -230,15 +225,53 @@ export function drawStats(period, v) {
${e.name}${n(c)}
${e.sub}
`; - }).join('') : '

NOTHING YET. GO AND EARN A ROW.

'; + }).join('') : `

${empty}

`; +}; +export function drawStats(period, v, mode = MODE_GORE) { + const d = read(period, undefined, mode); + const $ = id => document.getElementById(id); + $('stPeriod').textContent = period === 'last' ? 'LAST RUN' : period === '7d' ? '7 DAYS' : 'ALL TIME'; + $('stTitle').textContent = mode === MODE_MATCH ? 'THE LADDER' + : mode === MODE_CLASSIC ? 'THE RECORD' : 'BODY COUNT'; + const figs = mode === MODE_MATCH + ? [['FELLED', n(d.kills)], ['DEEPEST', n(d.longest)], ['RUNS', n(d.matches)]] + : mode === MODE_CLASSIC + ? [['PLAYED', n(d.matches)], ['WON', n(d.matches - d.deaths)], ['LOST', n(d.deaths)]] + : [['KILLS', n(d.kills)], ['MULTI-KILLS', n(d.multi)], ['DEATHS', n(d.deaths)]]; + // the third figure is the RED one in gore and in classic (deaths, matches lost); in the match it + // is just how many runs you have played, so nothing on that strip is bad news + const badIdx = mode === MODE_MATCH ? -1 : 2; + $('stFigs').innerHTML = figs.map(([k, val], i) => + `
${k}${val}
`).join(''); + + // sorted by count, and only bodies you have actually met — an empty roster of everything would + // read as a checklist of things to go and do, which this screen is not + const rows = Object.entries(d.z).filter(([, c]) => c > 0).sort((a, b) => b[1] - a[1]); + $('stZHdr').firstChild.textContent = mode === MODE_MATCH ? 'OPPONENTS FELLED' : 'BY OPPONENT'; + roster($('stZ'), rows, v, mode === MODE_MATCH ? 'NOBODY HAS GONE DOWN YET.' : 'NOTHING YET. GO AND EARN A ROW.'); + + // THE WEAPON SHELF IS THE GORE MODE'S ALONE. Both tennis modes are played with plain balls (the + // rack is hidden in both), so the section is hidden rather than printed empty — a shelf that says + // NO WEAPONS FIRED for a mode that has none reads as something you failed to do. const ws = Object.entries(d.w).filter(([id, c]) => c > 0 && wIcon(id)).sort((a, b) => b[1] - a[1]); - document.getElementById('stW').innerHTML = ws.length ? ws.map(([id, c]) => + // style.display, not the `hidden` attribute: #stW carries a display rule of its own and any + // declared display beats [hidden]'s, so the shelf stayed on screen under a mode with no weapons. + const off = mode !== MODE_GORE ? 'none' : ''; + $('stWHdr').style.display = off; $('stW').style.display = off; + // ...and CLASSIC TENNIS has no roster either: nothing dies in it. A section printing that is a + // line about something the mode does not do, which is worse than the section not being there. + const noZ = mode === MODE_CLASSIC ? 'none' : ''; + $('stZHdr').style.display = noZ; $('stZ').style.display = noZ; + $('stW').innerHTML = ws.length ? ws.map(([id, c]) => `
${n(c)}
`).join('') : '

NO WEAPONS FIRED.

'; - document.getElementById('stFoot').innerHTML = - `LONGEST RUN${d.longest} GAME${d.longest === 1 ? '' : 'S'}`; + $('stFoot').innerHTML = mode === MODE_MATCH + ? `DEEPEST RUN${d.longest} FELLED` + : mode === MODE_CLASSIC + ? `LONGEST MATCH${d.longest} GAME${d.longest === 1 ? '' : 'S'}` + : `LONGEST RUN${d.longest} GAME${d.longest === 1 ? '' : 'S'}`; } // The real board. Async, and every failure lands on a line of copy rather than an empty screen — diff --git a/src/zombies.js b/src/zombies.js index c664d00..dd24f6e 100644 --- a/src/zombies.js +++ b/src/zombies.js @@ -491,8 +491,12 @@ function killZombie(i, quiet) { // down, so it earns a row. The barrel is loot and scores nothing anywhere else in the game — // keep that agreement here too. A chapter boss files under its DESIGN, so the screen can name // the one that has been killing you. + // A MATCH OPPONENT FILES UNDER ITS RUNG, not under the generic body it wears: the ladder screen + // is a list of who you have felled, and 'r' is a body, not a name. zb.rung is set by matchOpponent. if (zb.variant !== 'o') - tally(zb.variant === 'B' ? 'B' + Math.max(0, BOSS_ART.indexOf(zb.art)) : zb.variant, CAUSE); + tally(zb.rung ? zb.rung.key + : zb.variant === 'B' ? 'B' + Math.max(0, BOSS_ART.indexOf(zb.art)) + : zb.variant, CAUSE); zombies.splice(i, 1); // THE DRILL RESOLVES HERE — after the splice, and WITHOUT returning. Both halves are load-bearing: // splice first, because drillKill can draw a new combination on the spot (a double fault