The weapon licence, the peer line, and the steward gets his vest back
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No files matched your search
@@ -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?")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 23 KiB |
@@ -864,6 +864,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="src/main.js?v=333"></script>
|
||||
<script type="module" src="src/main.js?v=334"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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_<fam>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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <img> 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".
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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; };
|
||||
|
||||