diff --git a/PROJECT.md b/PROJECT.md index dddaf49..878305c 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -491,6 +491,28 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu `RETURNED`, `DOUBLE KILL`) is a footnote to something you already watched happen. Sharing the loud FACE with the milestone cards is right; sharing their SIZE made every footnote read as a milestone. +## Session log: the licence, the peer line, the steward's vest (2026-08-29, Oliver's small-finds batch) + +Three of five items from his post-merge test built (training xp and the level/xp display are +under discussion): + +- **THE WEAPON LICENCE hardened.** His catch: the duel's tin was league-gated but the BARRELS + still paid every weapon. Root fix, not a barrel patch: the league is a LICENCE and the run's + `from` gates are the PACING — both must hold, in `tinLadder()` AND `pickBarrelWeapon()`. The + tins' old OR (league unlocks early) just moved his complaint one screen over, so it went: + below league 3 there are no lasers anywhere, ever; rockets need league 4; the gore director's + tuned income curve is untouched for licensed players. Tin test rewritten to licence semantics. +- **THE PEER LINE**: every walk-on modal now carries "LEVEL 34 — YOURS: 20" (config.foeLevel — + the ladder position spread over the profile's 50 levels, display only, nothing scales off it), + so you can feel whether you are that body's equal yet. His ask, verbatim. +- **THE STEWARD GOT HIS VEST BACK.** The tennis family was rolled off z_d1 (which wears the + orange hi-vis) but the FAMS words said "dark uniform jacket" — and the words won. The words + name the vest now ("on him in EVERY single pose"); family re-rolled through + _famroll -> _zsheet, review-cut checked, installed (SPRV 149 -> 150). z_sthead was already cut + from the gore frame and stayed byte-identical. NOTE: a full `_heads.py --install` rebuilds SIX + OTHER head icons differently from what is installed (script drift, br/ch/dr/gk/ma/r) — those + were reverted, not shipped as a side effect; whoever touches heads next should reconcile first. + ## Session log: THE LEVEL IS YOU, IN THE LEAGUE — legs and window (2026-08-29, after the merge) Oliver's deep-test question ("wie wirkt sich mein level aus? werde ich schneller, genauer?") diff --git a/assets/_famroll.py b/assets/_famroll.py index ae29df2..17de3e2 100644 --- a/assets/_famroll.py +++ b/assets/_famroll.py @@ -33,7 +33,11 @@ FAMS = { 'rounded shoulders. Far bulkier, heavier and wider than the figure in image 1'), 'sh': ('z_a1', 'a lean SHAMBLING ZOMBIE in a filthy dark red tennis shirt and grey shorts, gaunt and stooped'), 'bb': ('z_e1', 'a small young ZOMBIE BALL BOY in a pale tournament polo and shorts, slight and short'), - 'st': ('z_d1', 'a ZOMBIE STEWARD in a dark uniform jacket with a high collar, stiff and official'), + # 2026-08-29 (Oliver): the roll dropped his HI-VIS VEST — the words said "dark uniform jacket" + # while the anchor frame z_d1 wears bright orange over it, and the words won. Name the vest. + 'st': ('z_d1', 'a ZOMBIE STEWARD wearing a BRIGHT ORANGE HI-VIS SAFETY VEST with grey reflective ' + 'stripes OVER a dark uniform jacket, stiff and official. The orange vest is on him in ' + 'EVERY single pose — it is his uniform'), 'ru': ('z_f1', 'a wiry FAST ZOMBIE RUNNER in a sleeveless tennis top, lean and forward-leaning'), # THE GROUNDSKEEPER ANCHORS ON THE HAND-FINISHED FRAME, not on z_boss1 (2026-08-16). His boss art # is the last of the coarse legacy sheets, and the old description here ("work overalls and a diff --git a/assets/z_st1.png b/assets/z_st1.png index efa8751..619460b 100644 Binary files a/assets/z_st1.png and b/assets/z_st1.png differ diff --git a/assets/z_st2.png b/assets/z_st2.png index 2b3db42..158480b 100644 Binary files a/assets/z_st2.png and b/assets/z_st2.png differ diff --git a/assets/z_st3.png b/assets/z_st3.png index 99dc6b3..58b0fa0 100644 Binary files a/assets/z_st3.png and b/assets/z_st3.png differ diff --git a/assets/z_st4.png b/assets/z_st4.png index 6022b63..291b1f0 100644 Binary files a/assets/z_st4.png and b/assets/z_st4.png differ diff --git a/assets/z_stbounce.png b/assets/z_stbounce.png index ff1e905..4925286 100644 Binary files a/assets/z_stbounce.png and b/assets/z_stbounce.png differ diff --git a/assets/z_stfall1.png b/assets/z_stfall1.png index 5c38baf..072ca4b 100644 Binary files a/assets/z_stfall1.png and b/assets/z_stfall1.png differ diff --git a/assets/z_stfall2.png b/assets/z_stfall2.png index 798c398..eae1a98 100644 Binary files a/assets/z_stfall2.png and b/assets/z_stfall2.png differ diff --git a/assets/z_stfall3.png b/assets/z_stfall3.png index 501c028..2cc117f 100644 Binary files a/assets/z_stfall3.png and b/assets/z_stfall3.png differ diff --git a/assets/z_strise.png b/assets/z_strise.png index cb96f92..367dbe1 100644 Binary files a/assets/z_strise.png and b/assets/z_strise.png differ diff --git a/assets/z_stserve.png b/assets/z_stserve.png index 1ea7858..4856707 100644 Binary files a/assets/z_stserve.png and b/assets/z_stserve.png differ diff --git a/assets/z_sttoss.png b/assets/z_sttoss.png index 02bf921..c99baa5 100644 Binary files a/assets/z_sttoss.png and b/assets/z_sttoss.png differ diff --git a/assets/z_stw1.png b/assets/z_stw1.png index d42f2d7..379b943 100644 Binary files a/assets/z_stw1.png and b/assets/z_stw1.png differ diff --git a/assets/z_stw1b.png b/assets/z_stw1b.png index dc41fc4..3ea484d 100644 Binary files a/assets/z_stw1b.png and b/assets/z_stw1b.png differ diff --git a/assets/z_stw1bounce.png b/assets/z_stw1bounce.png index 2975096..012e690 100644 Binary files a/assets/z_stw1bounce.png and b/assets/z_stw1bounce.png differ diff --git a/assets/z_stw1fall1.png b/assets/z_stw1fall1.png index 7a1886d..f2cb7df 100644 Binary files a/assets/z_stw1fall1.png and b/assets/z_stw1fall1.png differ diff --git a/assets/z_stw1fall2.png b/assets/z_stw1fall2.png index ea7bdfa..6a6b5b4 100644 Binary files a/assets/z_stw1fall2.png and b/assets/z_stw1fall2.png differ diff --git a/assets/z_stw1fall3.png b/assets/z_stw1fall3.png index 6685393..7779290 100644 Binary files a/assets/z_stw1fall3.png and b/assets/z_stw1fall3.png differ diff --git a/assets/z_stw1rise.png b/assets/z_stw1rise.png index d840440..d7f5a9f 100644 Binary files a/assets/z_stw1rise.png and b/assets/z_stw1rise.png differ diff --git a/assets/z_stw1s.png b/assets/z_stw1s.png index d159448..4f08eb2 100644 Binary files a/assets/z_stw1s.png and b/assets/z_stw1s.png differ diff --git a/assets/z_stw1serve.png b/assets/z_stw1serve.png index d04da9e..0ac8a8e 100644 Binary files a/assets/z_stw1serve.png and b/assets/z_stw1serve.png differ diff --git a/assets/z_stw1toss.png b/assets/z_stw1toss.png index 9e46057..0e09401 100644 Binary files a/assets/z_stw1toss.png and b/assets/z_stw1toss.png differ diff --git a/assets/z_stw2.png b/assets/z_stw2.png index b84f745..0526bdc 100644 Binary files a/assets/z_stw2.png and b/assets/z_stw2.png differ diff --git a/assets/z_stw2b.png b/assets/z_stw2b.png index 710ad2a..9c8b76d 100644 Binary files a/assets/z_stw2b.png and b/assets/z_stw2b.png differ diff --git a/assets/z_stw2bounce.png b/assets/z_stw2bounce.png index e58e156..a045c69 100644 Binary files a/assets/z_stw2bounce.png and b/assets/z_stw2bounce.png differ diff --git a/assets/z_stw2fall1.png b/assets/z_stw2fall1.png index 6f089ac..ceab2c9 100644 Binary files a/assets/z_stw2fall1.png and b/assets/z_stw2fall1.png differ diff --git a/assets/z_stw2fall2.png b/assets/z_stw2fall2.png index 972f11c..01c6fdc 100644 Binary files a/assets/z_stw2fall2.png and b/assets/z_stw2fall2.png differ diff --git a/assets/z_stw2fall3.png b/assets/z_stw2fall3.png index 5cc0251..9e6840a 100644 Binary files a/assets/z_stw2fall3.png and b/assets/z_stw2fall3.png differ diff --git a/assets/z_stw2rise.png b/assets/z_stw2rise.png index 21ddcf0..dc9071f 100644 Binary files a/assets/z_stw2rise.png and b/assets/z_stw2rise.png differ diff --git a/assets/z_stw2s.png b/assets/z_stw2s.png index 1d19637..04053fb 100644 Binary files a/assets/z_stw2s.png and b/assets/z_stw2s.png differ diff --git a/assets/z_stw2serve.png b/assets/z_stw2serve.png index 2bd1765..4015eff 100644 Binary files a/assets/z_stw2serve.png and b/assets/z_stw2serve.png differ diff --git a/assets/z_stw2toss.png b/assets/z_stw2toss.png index 800e85f..402a776 100644 Binary files a/assets/z_stw2toss.png and b/assets/z_stw2toss.png differ diff --git a/index.html b/index.html index 4a198bb..04e3451 100644 --- a/index.html +++ b/index.html @@ -864,6 +864,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat - + diff --git a/src/beats.js b/src/beats.js index ccfd48b..f766986 100644 --- a/src/beats.js +++ b/src/beats.js @@ -7,9 +7,9 @@ // (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, matchFoe, BENCH, ZFAMS, COURT, HOME, racketStage, leagueOf, cupRoundsFor } from './config.js'; +import { TUNE, UI, bossArt, matchFoe, BENCH, ZFAMS, COURT, HOME, racketStage, leagueOf, cupRoundsFor, foeLevel } from './config.js'; import { endRun, MODE_MATCH } from './stats.js'; -import { award, prof, save as profSave, push as profPush, setPromo } from './profile.js'; +import { award, prof, save as profSave, push as profPush, setPromo, levelAt } from './profile.js'; import { G, P, O, ball, score, setupPoint, sendBoy, other } from './state.js'; import { voice, zombieDuck, aww } from './audio.js'; import { zombies, pellets, extras, shots, spawnRacketZombie, spawnChapterBoss, spawnZombie, explode, killPlayer } from './zombies.js'; @@ -649,8 +649,11 @@ function matchOpponent() { // 1.6 s intro beat until the tap, and the head wears the same z_head crop the card wore. // In the cup the round names itself on the second line. const cup = G.match.cup; + // the "am I its peer yet?" line (Oliver, 2026-08-29): the body's ladder position spread over + // the profile's own level scale, beside yours — display only, nothing scales off it + const vs = 'LEVEL ' + foeLevel(G.match.opp) + ' — YOURS: ' + levelAt(prof.xp); showModal({ title: zb.foe.name, color: UI.hot, icon: 'z_' + zb.fam + 'head', iconH: 64, - body: [zb.foe.line].concat(cup ? [cup.rounds[cup.round].name + (cup.rounds[cup.round].need > 1 + body: [zb.foe.line, vs].concat(cup ? [cup.rounds[cup.round].name + (cup.rounds[cup.round].need > 1 ? ' · FIRST TO ' + cup.rounds[cup.round].need + ' GAMES' : '')] // the top of the band walking BACK on is the arcade's natural "what now?" moment — the // intro answers it in place (THE LEAGUES: the cup is the only promotion) diff --git a/src/config.js b/src/config.js index a362988..6580cac 100644 --- a/src/config.js +++ b/src/config.js @@ -729,11 +729,13 @@ export const UI = { hot: '#d9705f', warm: '#e0cc72', pick: '#cbd06a', cream: '#e // palette is near it, so "pink means a weapon is in there" needs no explaining pink: '#ff5cf0' }; -// THE TIN TIERS (the leagues, phase 4; settled: tin-tier, NO starting loadout). The league -// unlocks what a won duel's tin can PAY, independent of the run's own barrel gates — "weapons are -// only ever won" stays literal, the league just deepens the pot: lasers join the duel ladder at -// league 3, rockets at 4, and league 5 duels pay DOUBLE tins. The world board is not devalued by -// starting inventory, and every weapon still arrives through resolveInterlude('P'). +// THE WEAPON LICENCE (the leagues; settled: tin-tier, NO starting loadout; hardened 2026-08-29, +// Oliver: "in den Truhen sind nach wie vor alle Waffen verfügbar"). The league is a LICENCE and +// the run's `from` gates stay the PACING: a weapon exists for you only when the league says it is +// yours AND the run has reached it — barrels and duel tins agree (an OR on the tins just moved +// his complaint one screen over). Below league 3 there are no lasers anywhere, ever; the gore +// director's tuned income curve is untouched for licensed players. League 5 duels pay DOUBLE +// tins. "Weapons are only ever won" stays literal. export const TIN_LEAGUE = { laser: 3, rocket: 4 }; export const TIN_DOUBLE_LEAGUE = 5; @@ -915,6 +917,10 @@ export const leagueOf = lg => LEAGUES[Math.min(LEAGUES.length, Math.max(1, lg | // which league a LADDER POSITION belongs to — THE CLIMB's tempo reads this (the world speeds up // as the run crosses each band border), and it is what makes the club read as five territories export const leagueOfRung = n => LEAGUES.find(l => n <= l.band[1]) || LEAGUES[LEAGUES.length - 1]; +// the DISPLAYED level of a ladder position (the walk-on card's "am I its peer yet?" line, +// 2026-08-29): the ladder spread over the profile's 50 levels. Display only — nothing scales +// off it. 49 = profile.LEVEL_CAP - 1 (config cannot import profile; the cycle runs the other way). +export const foeLevel = n => Math.round(1 + (n - 1) / (LADDER - 1) * 49); // The tournament, PER LEAGUE. Same shape CUP_ROUNDS always had (name/band/need) — round one is a // single game so a run opens fast, and the body's pool still scales with `need` (matchOpponent). diff --git a/src/gameplay.js b/src/gameplay.js index 540e2bb..a96e522 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -913,11 +913,11 @@ function spendSlot(i) { // (capped per type at TUNE.wCap) — old stock is kept, so you can bank a bomb for the horde // instead of spending it now. G.weapon picks which tin the next serve throws. const TINS = ['bomb', 'triple', 'fire', 'gun']; // the duel ladder, in the order it hands them out -// THE TIN TIERS (the leagues): the league deepens the pot the duel pays from — a barrel weapon -// joins the ladder when the RUN unlocks it (as ever) OR when the LEAGUE says it is yours -// (config.TIN_LEAGUE). Pure and exported so the suite can pin the pot. +// THE WEAPON LICENCE (the leagues): the league licenses a weapon, the run's `from` gate paces +// it — BOTH must hold (config.TIN_LEAGUE; barrels apply the same licence in pickBarrelWeapon). +// Pure and exported so the suite can pin the pot. export const tinLadder = () => [...TINS, ...Object.keys(BARREL_WEAPONS).filter(w => - G.runGames >= BARREL_WEAPONS[w].from || prof.lg >= (TIN_LEAGUE[w] || 99))]; + G.runGames >= BARREL_WEAPONS[w].from && prof.lg >= (TIN_LEAGUE[w] || 0))]; export function deliverTin() { // WALK the ladder to the next rung you have room for, rather than cycling blindly onto one that // is already at wCap. Blind cycling meant `grantWeapon` clamped the top-up to a no-op and a won diff --git a/src/render.js b/src/render.js index 93fa37b..5a0cd0f 100644 --- a/src/render.js +++ b/src/render.js @@ -236,7 +236,7 @@ function farB(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 = 149; +export const SPRV = 150; // 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/zombies.js b/src/zombies.js index c37be76..03a59a0 100644 --- a/src/zombies.js +++ b/src/zombies.js @@ -3,7 +3,7 @@ // your baseline — one crossing it is a point for THEM. // (zombies <-> gameplay is a runtime-only import cycle, same pattern as input.) -import { TUNE, UI, COURT, CONTACT, BOSS_ART, bossArt, WEAPONS, BARREL_WEAPONS, woundStage, zombieH, EATEN_BURST, EATEN_SUMMARY, EATEN_FEED_R } from './config.js'; +import { TUNE, UI, COURT, CONTACT, BOSS_ART, bossArt, WEAPONS, BARREL_WEAPONS, TIN_LEAGUE, woundStage, zombieH, EATEN_BURST, EATEN_SUMMARY, EATEN_FEED_R } from './config.js'; import { G, P, O, ball, score, sendBoy, setupPoint } from './state.js'; import { splat, moan, pock, thud, zap, voice, boom, swish, shriek, aww, sfx , whoosh, flame } from './audio.js'; import { haptic } from './haptics.js'; @@ -11,7 +11,7 @@ import { drillKill, drillCaught } from './drill.js'; import { splatPoint, cheer, spentBall, aside, grantWeapon, endBurst, sliceCarom } from './gameplay.js'; import { resolveInterlude, endBossFight } from './beats.js'; // runtime-only cycle, like the gameplay one import { tally, multiKill, endRun, MODE_MATCH } from './stats.js'; -import { award, bonus } from './profile.js'; +import { award, bonus, prof } from './profile.js'; export const zombies = []; // {x, z, v, walkT, variant: 'a'|'b'|'c'} export const gore = []; // lawn decals {x, z, r, a} — the flat ground layer @@ -86,6 +86,9 @@ function pickBarrelWeapon() { const pool = []; for (const [id, b] of Object.entries(BARREL_WEAPONS)) { if (G.runGames < b.from) continue; + // THE WEAPON LICENCE (the leagues): the league licenses, `from` paces — a barrel must not + // hand a league-1 player the laser his duels rightly refuse (config.TIN_LEAGUE) + if (prof.lg < (TIN_LEAGUE[id] || 0)) continue; if (id === lastBarrel) continue; // never twice running... for (let k = 0; k < b.weight; k++) pool.push(id); } diff --git a/test.js b/test.js index 188c8c1..b7ef8a2 100644 --- a/test.js +++ b/test.js @@ -2296,19 +2296,19 @@ test('THE DUEL LEARNS: its skill ramps with the run, and game zero is the shippe assert.ok(TUNE.racketQMax + TUNE.duelQBonus < 1, 'its returns never reach perfect quality'); }); -test('THE TIN TIERS: the league deepens the duel pot, and league 5 pays double', () => { +test('THE WEAPON LICENCE: the league licenses, the run paces — tins and barrels agree, league 5 pays double', () => { G.runGames = 0; + prof.prof.lg = 5; + assert.ok(!tinLadder().includes('laser'), 'even the top league waits for the run to reach lasers'); + G.runGames = 12; prof.prof.lg = 1; assert.ok(!tinLadder().includes('laser') && !tinLadder().includes('rocket'), - 'league one, game zero: the shipped pot'); + 'league one NEVER sees a laser, however deep the run — that was the barrel complaint'); prof.prof.lg = 3; assert.ok(tinLadder().includes('laser') && !tinLadder().includes('rocket'), - 'league three adds the laser, not the rocket'); + 'league three licenses the laser, not the rocket'); prof.prof.lg = 4; - assert.ok(tinLadder().includes('rocket'), 'league four adds the rocket'); - prof.prof.lg = 1; - G.runGames = 12; - assert.ok(tinLadder().includes('laser'), 'the run\'s own barrel gates still work without a league'); + assert.ok(tinLadder().includes('rocket'), 'league four licenses the rocket'); G.runGames = 0; // the double tin: an empty rack, one delivery, twice the stock at league five const stockOf = id => { const i = G.rack.findIndex(r => r.id === id); return i < 0 ? 0 : G.rack[i].n; };