The leagues, the polish round: the way-up ribbon, upright chevrons, the breathing badges, the card counts up
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87ac3294bc
commit
fa6db7f77b
4 files changed
+113
-42
No files matched your search
+22
@@ -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
|
`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.
|
FACE with the milestone cards is right; sharing their SIZE made every footnote read as a milestone.
|
||||||
|
|
||||||
|
## Session log: THE LEAGUES — the polish round: the ribbon, the glow, the count-up (2026-08-29, same day, STAGING ONLY)
|
||||||
|
|
||||||
|
Oliver's retest ("schon viel besser") with five polish items, all built:
|
||||||
|
|
||||||
|
- **Every chevron is UPRIGHT now.** Bangers is drawn slanted, so `‹`/`›` read as italic on every
|
||||||
|
screen (his "an vielen Stellen"). All `.chev` buttons and the plaque's arrow use Silkscreen's
|
||||||
|
straight ASCII `<`/`>` instead — the announcement face keeps its job, arrows stop leaning.
|
||||||
|
- **The plaque wears rounded corners (8px) and real padding** (12/14 against 8/10).
|
||||||
|
- **THE WAY-UP RIBBON**: the tag became a banderole — red field, thick white Silkscreen with a
|
||||||
|
light shadow, rotated -45° over the poster's top-left corner, ends clipped by the poster's own
|
||||||
|
`overflow: hidden` (that clip is what sells the wrap). And it shows on BOTH picker levels: the
|
||||||
|
top-level ZOMBIE TENNIS poster carries it too (`tag` on the MENU row), so the click path to the
|
||||||
|
promotion is marked from the first screen.
|
||||||
|
- **Poster captions grew**: titles 12 → 15 bold, sub lines 9 → 10 and a brighter grey — "viel
|
||||||
|
Text in sehr kleiner Schrift" was true of every banner.
|
||||||
|
- **The card performs its entrance**: earned badges and the current crest BREATHE a subtle gold
|
||||||
|
halo (`badgeGlow` keyframes, CSS-only — the parked menu loop is irrelevant to CSS animations;
|
||||||
|
the dark slots stay dead, which is their point), both bars sweep from empty (0.7s eased
|
||||||
|
transition, forced-reflow reset), and every number counts up fast — the XP line and all six
|
||||||
|
ledger figures through one `countUp` easer (setTimeout, the bracket's own no-rAF rule; timers
|
||||||
|
cancelled on rebuild so re-entry never races itself).
|
||||||
|
|
||||||
## Session log: THE LEAGUES — the plaque round: the meta speaks (2026-08-29, same day, STAGING ONLY)
|
## Session log: THE LEAGUES — the plaque round: the meta speaks (2026-08-29, same day, STAGING ONLY)
|
||||||
|
|
||||||
Oliver's first screenshot review, four items, all built. The theme: the meta layer existed but
|
Oliver's first screenshot review, four items, all built. The theme: the meta layer existed but
|
||||||
|
|||||||
+43
-25
@@ -209,8 +209,10 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
background: var(--solid);
|
background: var(--solid);
|
||||||
padding-top: max(10px, env(safe-area-inset-top)); }
|
padding-top: max(10px, env(safe-area-inset-top)); }
|
||||||
.hdr h2 { padding-left: 2px; }
|
.hdr h2 { padding-left: 2px; }
|
||||||
|
/* UPRIGHT, not Bangers: the announcement face is drawn slanted, and a slanted arrow read as
|
||||||
|
italic on every screen. Silkscreen's ASCII < > are straight pixel arrows. */
|
||||||
.chev { width: 34px; height: 34px; flex: none; background: none; border: 2px solid var(--line);
|
.chev { width: 34px; height: 34px; flex: none; background: none; border: 2px solid var(--line);
|
||||||
color: var(--acid); font: 400 20px Bangers, cursive; line-height: 1; cursor: pointer;
|
color: var(--acid); font: 700 14px Silkscreen, monospace; line-height: 1; cursor: pointer;
|
||||||
-webkit-tap-highlight-color: transparent; }
|
-webkit-tap-highlight-color: transparent; }
|
||||||
/* the BODY COUNT headline row: three boxed figures sharing one frame */
|
/* the BODY COUNT headline row: three boxed figures sharing one frame */
|
||||||
.figs { display: flex; border: 2px solid var(--line); background: var(--panel); }
|
.figs { display: flex; border: 2px solid var(--line); background: var(--panel); }
|
||||||
@@ -291,6 +293,13 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
#cdBadges { display: flex; gap: 10px; align-items: flex-end; }
|
#cdBadges { display: flex; gap: 10px; align-items: flex-end; }
|
||||||
#cdBadges img { height: 46px; image-rendering: pixelated; }
|
#cdBadges img { height: 46px; image-rendering: pixelated; }
|
||||||
#cdBadges img.dark { filter: grayscale(1) brightness(0.3); }
|
#cdBadges img.dark { filter: grayscale(1) brightness(0.3); }
|
||||||
|
/* earned badges BREATHE — a subtle gold halo, CSS-only (the menu parks the game loop, CSS
|
||||||
|
animations do not care). The dark slots stay dead, which is the point of them. */
|
||||||
|
#cdBadges img:not(.dark), #cdBadge {
|
||||||
|
animation: badgeGlow 2.6s ease-in-out infinite; }
|
||||||
|
@keyframes badgeGlow {
|
||||||
|
0%, 100% { filter: drop-shadow(0 0 2px rgba(232, 178, 58, 0.25)); }
|
||||||
|
50% { filter: drop-shadow(0 0 7px rgba(232, 178, 58, 0.75)); } }
|
||||||
.rkbadge { height: 15px; margin-right: 6px; image-rendering: pixelated; flex: none; }
|
.rkbadge { height: 15px; margin-right: 6px; image-rendering: pixelated; flex: none; }
|
||||||
/* THE LEVEL STRIP: the one meta number over a thin bar, riding the splash foot. The fill colour
|
/* THE LEVEL STRIP: the one meta number over a thin bar, riding the splash foot. The fill colour
|
||||||
is the acid accent for now; the leagues will hand it the skill colour in phase 3. */
|
is the acid accent for now; the leagues will hand it the skill colour in phase 3. */
|
||||||
@@ -301,8 +310,9 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
level+league over the bar, and the standing GOAL LINE ("win the X cup to advance"), with a
|
level+league over the bar, and the standing GOAL LINE ("win the X cup to advance"), with a
|
||||||
chevron that says it opens. The splash stays two buttons + one plaque; the badge COLLECTION
|
chevron that says it opens. The splash stays two buttons + one plaque; the badge COLLECTION
|
||||||
stays on the card, one (now discoverable) tap away. */
|
stays on the card, one (now discoverable) tap away. */
|
||||||
#plaque { display: flex; align-items: center; gap: 10px; width: 100%; margin: 0 0 10px;
|
#plaque { display: flex; align-items: center; gap: 12px; width: 100%; margin: 0 0 10px;
|
||||||
padding: 8px 10px; background: var(--solid); border: 2px solid var(--line);
|
padding: 12px 14px; background: var(--solid); border: 2px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
cursor: pointer; text-align: left; -webkit-tap-highlight-color: transparent; }
|
cursor: pointer; text-align: left; -webkit-tap-highlight-color: transparent; }
|
||||||
#plBadge { height: 34px; flex: none; image-rendering: pixelated; }
|
#plBadge { height: 34px; flex: none; image-rendering: pixelated; }
|
||||||
#plBody { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
#plBody { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
||||||
@@ -313,20 +323,27 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
letter-spacing: 1px; white-space: nowrap; }
|
letter-spacing: 1px; white-space: nowrap; }
|
||||||
#plGoal { font: 400 8px Silkscreen, monospace; font-style: normal; color: var(--acid);
|
#plGoal { font: 400 8px Silkscreen, monospace; font-style: normal; color: var(--acid);
|
||||||
letter-spacing: 1px; }
|
letter-spacing: 1px; }
|
||||||
#plaque em { flex: none; font: 400 22px Bangers, cursive; font-style: normal; color: var(--acid); }
|
#plaque em { flex: none; font: 700 16px Silkscreen, monospace; font-style: normal; color: var(--acid); }
|
||||||
#plRow s, #cdBar s, #cdCareer s { flex: 1; height: 6px; background: rgba(255,255,255,0.10);
|
#plRow s, #cdBar s, #cdCareer s { flex: 1; height: 6px; background: rgba(255,255,255,0.10);
|
||||||
border: 1px solid var(--line); text-decoration: none; display: block; }
|
border: 1px solid var(--line); text-decoration: none; display: block; }
|
||||||
#cdBar s { height: 8px; }
|
#cdBar s { height: 8px; }
|
||||||
#cdCareer { display: flex; align-items: center; gap: 8px; }
|
#cdCareer { display: flex; align-items: center; gap: 8px; }
|
||||||
#cdCareer u { font: 700 8px Silkscreen, monospace; color: var(--dim); text-decoration: none;
|
#cdCareer u { font: 700 8px Silkscreen, monospace; color: var(--dim); text-decoration: none;
|
||||||
letter-spacing: 1px; }
|
letter-spacing: 1px; }
|
||||||
/* the mode picker's WAY-UP tag: the standing answer to "how do I advance", worn by the cup */
|
/* the WAY-UP RIBBON: the standing answer to "how do I advance", worn as a banderole wrapped
|
||||||
.way { position: absolute; top: 8px; left: 8px; padding: 3px 6px; font: 700 8px Silkscreen, monospace;
|
over the poster's corner (Oliver: bigger, red, thick white type, light shadow). The poster
|
||||||
font-style: normal; color: var(--ink); background: var(--acid); letter-spacing: 1px;
|
clips its ends diagonally (overflow hidden + the -45deg rotation), which is what sells the
|
||||||
text-decoration: none; }
|
wrap. */
|
||||||
|
.way { position: absolute; top: 20px; left: -42px; width: 150px; transform: rotate(-45deg);
|
||||||
|
padding: 6px 0; font: 700 11px Silkscreen, monospace; font-style: normal;
|
||||||
|
text-align: center; color: #fff; background: var(--red);
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.35); border-bottom: 1px solid #69201c;
|
||||||
|
text-shadow: 1px 1px 0 rgba(0,0,0,0.55); box-shadow: 0 2px 6px rgba(0,0,0,0.4);
|
||||||
|
letter-spacing: 1px; text-decoration: none; }
|
||||||
#plRow s i, #cdBar s i, #cdCareer s i { display: block; height: 100%; width: 0%;
|
#plRow s i, #cdBar s i, #cdCareer s i { display: block; height: 100%; width: 0%;
|
||||||
background: linear-gradient(90deg, #3f9560, var(--acid) 55%, #e8b23a);
|
background: linear-gradient(90deg, #3f9560, var(--acid) 55%, #e8b23a);
|
||||||
transition: width 0.4s ease; }
|
transition: width 0.4s ease; }
|
||||||
|
#cdBar s i, #cdCareer s i { transition: width 0.7s cubic-bezier(0.2, 0.7, 0.3, 1); }
|
||||||
/* KIT and SKIN: a row of colour squares, under a label that names whatever is selected. That is
|
/* KIT and SKIN: a row of colour squares, under a label that names whatever is selected. That is
|
||||||
the whole UI — the pick has no meters and no art to show, and a labelled tile per colour would
|
the whole UI — the pick has no meters and no art to show, and a labelled tile per colour would
|
||||||
be five times the screen for a choice you make by looking at the colour. */
|
be five times the screen for a choice you make by looking at the colour. */
|
||||||
@@ -391,7 +408,8 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
a live answer to the header's question; tapping answers and advances. The only memory shown is
|
a live answer to the header's question; tapping answers and advances. The only memory shown is
|
||||||
the LAST tag — an indicator, not a selection. A border that means "SERVE UP will use this"
|
the LAST tag — an indicator, not a selection. A border that means "SERVE UP will use this"
|
||||||
exists solely on the final screen, where a confirm button actually follows. */
|
exists solely on the final screen, where a confirm button actually follows. */
|
||||||
#modeList button.mode, #modeSub button.mode { display: flex; align-items: flex-end; width: 100%; min-height: 96px;
|
#modeList button.mode, #modeSub button.mode { overflow: hidden; /* clips the WAY-UP ribbon's ends */
|
||||||
|
display: flex; align-items: flex-end; width: 100%; min-height: 96px;
|
||||||
margin: 0 0 8px; padding: 7px 9px; background-size: cover; background-position: center bottom;
|
margin: 0 0 8px; padding: 7px 9px; background-size: cover; background-position: center bottom;
|
||||||
image-rendering: pixelated; border: 2px solid var(--line); color: inherit; font: inherit;
|
image-rendering: pixelated; border: 2px solid var(--line); color: inherit; font: inherit;
|
||||||
text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent;
|
text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent;
|
||||||
@@ -425,8 +443,8 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
#courtList img { width: 92px; height: 66px; display: block; image-rendering: pixelated; flex: none;
|
#courtList img { width: 92px; height: 66px; display: block; image-rendering: pixelated; flex: none;
|
||||||
object-fit: cover; }
|
object-fit: cover; }
|
||||||
#courtList .cb, #modeList .cb, #modeSub .cb { flex: 1; min-width: 0; }
|
#courtList .cb, #modeList .cb, #modeSub .cb { flex: 1; min-width: 0; }
|
||||||
#courtList b, #modeList b, #modeSub b { display: block; font: 400 12px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px; }
|
#courtList b, #modeList b, #modeSub b { display: block; font: 700 15px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px; }
|
||||||
#courtList i, #modeList i, #modeSub i { display: block; margin: 3px 0 6px; font: 400 9px Silkscreen, monospace; color: var(--dim); font-style: normal; }
|
#courtList i, #modeList i, #modeSub i { display: block; margin: 4px 0 6px; font: 400 10px Silkscreen, monospace; color: #c9c4b8; font-style: normal; }
|
||||||
/* The two meters read straight off the court's own bounce numbers (see main.js), so they cannot
|
/* The two meters read straight off the court's own bounce numbers (see main.js), so they cannot
|
||||||
drift away from what the ball actually does on it. */
|
drift away from what the ball actually does on it. */
|
||||||
.meter { display: flex; align-items: center; gap: 6px; margin-top: 4px;
|
.meter { display: flex; align-items: center; gap: 6px; margin-top: 4px;
|
||||||
@@ -587,7 +605,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
<span id="plRow"><u id="plLv">LEVEL 1</u><s><i id="plFill"></i></s></span>
|
<span id="plRow"><u id="plLv">LEVEL 1</u><s><i id="plFill"></i></s></span>
|
||||||
<i id="plGoal"></i>
|
<i id="plGoal"></i>
|
||||||
</span>
|
</span>
|
||||||
<em>›</em>
|
<em>></em>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn big" id="mStart">PLAY MATCH</button>
|
<button class="btn big" id="mStart">PLAY MATCH</button>
|
||||||
<div class="two">
|
<div class="two">
|
||||||
@@ -605,15 +623,15 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
who-and-where with SERVE UP. Every step comes up pre-selected from last time, so a
|
who-and-where with SERVE UP. Every step comes up pre-selected from last time, so a
|
||||||
returning player is three taps from serving. -->
|
returning player is three taps from serving. -->
|
||||||
<div id="stmode">
|
<div id="stmode">
|
||||||
<div class="hdr"><button class="chev" id="mBack">‹</button><h2>PICK A GAME</h2></div>
|
<div class="hdr"><button class="chev" id="mBack"><</button><h2>PICK A GAME</h2></div>
|
||||||
<div id="modeList"></div>
|
<div id="modeList"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="stsub">
|
<div id="stsub">
|
||||||
<div class="hdr"><button class="chev" id="subBack">‹</button><h2 id="subTitle">DIFFICULTY</h2></div>
|
<div class="hdr"><button class="chev" id="subBack"><</button><h2 id="subTitle">DIFFICULTY</h2></div>
|
||||||
<div id="modeSub"></div>
|
<div id="modeSub"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="setup">
|
<div id="setup">
|
||||||
<div class="hdr"><button class="chev" id="sBack">‹</button><h2>MATCH SETUP</h2></div>
|
<div class="hdr"><button class="chev" id="sBack"><</button><h2>MATCH SETUP</h2></div>
|
||||||
<!-- ONE PLAYER BLOCK. It was four stacked sections — two big portrait tiles, then the live
|
<!-- ONE PLAYER BLOCK. It was four stacked sections — two big portrait tiles, then the live
|
||||||
player, then a kit row, then a skin row — which said the same thing twice (the tiles were
|
player, then a kit row, then a skin row — which said the same thing twice (the tiles were
|
||||||
a second, FRONT-FACING drawing of a man who does not react to any of the colours) and ran
|
a second, FRONT-FACING drawing of a man who does not react to any of the colours) and ran
|
||||||
@@ -644,7 +662,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
keys), so the two screens cannot disagree about who you are. Rebuilt on every visit by
|
keys), so the two screens cannot disagree about who you are. Rebuilt on every visit by
|
||||||
main.buildPlayer, so the .on states track the setup screen's. -->
|
main.buildPlayer, so the .on states track the setup screen's. -->
|
||||||
<div id="player">
|
<div id="player">
|
||||||
<div class="hdr"><button class="chev" id="pBack">‹</button><h2>YOUR PLAYER</h2></div>
|
<div class="hdr"><button class="chev" id="pBack"><</button><h2>YOUR PLAYER</h2></div>
|
||||||
<h3>NAME<span>YOURS ON EVERY BOARD</span></h3>
|
<h3>NAME<span>YOURS ON EVERY BOARD</span></h3>
|
||||||
<div id="nameRow">
|
<div id="nameRow">
|
||||||
<input id="pName" class="tin" maxlength="12" autocomplete="off" spellcheck="false">
|
<input id="pName" class="tin" maxlength="12" autocomplete="off" spellcheck="false">
|
||||||
@@ -684,7 +702,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
main.buildCard from the profile + stats; the honours row shows every badge slot, earned
|
main.buildCard from the profile + stats; the honours row shows every badge slot, earned
|
||||||
bright and unearned dark. -->
|
bright and unearned dark. -->
|
||||||
<div id="card">
|
<div id="card">
|
||||||
<div class="hdr"><button class="chev" id="cdBack">‹</button><h2 id="cdName">YOUR PLAYER</h2></div>
|
<div class="hdr"><button class="chev" id="cdBack"><</button><h2 id="cdName">YOUR PLAYER</h2></div>
|
||||||
<div id="cdTop">
|
<div id="cdTop">
|
||||||
<canvas id="cdLook" width="96" height="224"></canvas>
|
<canvas id="cdLook" width="96" height="224"></canvas>
|
||||||
<div id="cdMeta">
|
<div id="cdMeta">
|
||||||
@@ -710,7 +728,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
(main.js hands the settled rungs to startMatch). The rows are built in main.js from
|
(main.js hands the settled rungs to startMatch). The rows are built in main.js from
|
||||||
config.CUP_ROUNDS, so a round added there needs no markup. -->
|
config.CUP_ROUNDS, so a round added there needs no markup. -->
|
||||||
<div id="bracket">
|
<div id="bracket">
|
||||||
<div class="hdr"><button class="chev" id="brBack">‹</button><h2>THE DRAW</h2></div>
|
<div class="hdr"><button class="chev" id="brBack"><</button><h2>THE DRAW</h2></div>
|
||||||
<h3>FOUR ROUNDS<span id="brState">DRAWING…</span></h3>
|
<h3>FOUR ROUNDS<span id="brState">DRAWING…</span></h3>
|
||||||
<div id="cupRows"></div>
|
<div id="cupRows"></div>
|
||||||
<div class="foot">
|
<div class="foot">
|
||||||
@@ -721,7 +739,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
<!-- THE TRAINING COURT's opponent picker: the whole ladder face-up, tap a body to drill it.
|
<!-- THE TRAINING COURT's opponent picker: the whole ladder face-up, tap a body to drill it.
|
||||||
Built in main.js from config's roster (matchFoe/LADDER), so a new rung needs no markup. -->
|
Built in main.js from config's roster (matchFoe/LADDER), so a new rung needs no markup. -->
|
||||||
<div id="train">
|
<div id="train">
|
||||||
<div class="hdr"><button class="chev" id="trBack">‹</button><h2>TRAINING</h2></div>
|
<div class="hdr"><button class="chev" id="trBack"><</button><h2>TRAINING</h2></div>
|
||||||
<h3>PICK YOUR OPPONENT<span>ONE GAME · GO AGAIN</span></h3>
|
<h3>PICK YOUR OPPONENT<span>ONE GAME · GO AGAIN</span></h3>
|
||||||
<div id="trainList"></div>
|
<div id="trainList"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -730,7 +748,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
a socket is open, so the room code and its SHARE button stand where the buttons were and
|
a socket is open, so the room code and its SHARE button stand where the buttons were and
|
||||||
the game stays parked behind this overlay until the room fills (main.js, THE LOBBY). -->
|
the game stays parked behind this overlay until the room fills (main.js, THE LOBBY). -->
|
||||||
<div id="vs">
|
<div id="vs">
|
||||||
<div class="hdr"><button class="chev" id="vsBack">‹</button><h2>VS A FRIEND</h2></div>
|
<div class="hdr"><button class="chev" id="vsBack"><</button><h2>VS A FRIEND</h2></div>
|
||||||
<div id="vsPick">
|
<div id="vsPick">
|
||||||
<h3>01 · YOUR NAME<span>ON THEIR SCOREBOARD</span></h3>
|
<h3>01 · YOUR NAME<span>ON THEIR SCOREBOARD</span></h3>
|
||||||
<input id="vsName" class="tin" maxlength="12" placeholder="YOUR NAME" autocomplete="off"
|
<input id="vsName" class="tin" maxlength="12" placeholder="YOUR NAME" autocomplete="off"
|
||||||
@@ -758,7 +776,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
</div>
|
</div>
|
||||||
<!-- BODY COUNT. Every row in here is built by stats.js from the run records in localStorage. -->
|
<!-- BODY COUNT. Every row in here is built by stats.js from the run records in localStorage. -->
|
||||||
<div id="stats">
|
<div id="stats">
|
||||||
<div class="hdr"><button class="chev" id="stBack">‹</button><h2 id="stTitle">BODY COUNT</h2></div>
|
<div class="hdr"><button class="chev" id="stBack"><</button><h2 id="stTitle">BODY COUNT</h2></div>
|
||||||
<!-- WHICH MODE, then which period. The three modes do not measure the same thing (see the
|
<!-- WHICH MODE, then which period. The three modes do not measure the same thing (see the
|
||||||
record-shape note in stats.endRun), so this strip changes the whole screen under it —
|
record-shape note in stats.endRun), so this strip changes the whole screen under it —
|
||||||
the title, the three figures, the roster's heading and the footer. -->
|
the title, the three figures, the roster's heading and the footer. -->
|
||||||
@@ -783,7 +801,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
a ranking. Same tab strip as the stats screen's, and stats.drawRanks rewrites the column
|
a ranking. Same tab strip as the stats screen's, and stats.drawRanks rewrites the column
|
||||||
heading with the tab, because the number means something different on each one. -->
|
heading with the tab, because the number means something different on each one. -->
|
||||||
<div id="ranks">
|
<div id="ranks">
|
||||||
<div class="hdr"><button class="chev" id="rkBack">‹</button><h2>WORLD BOARD</h2></div>
|
<div class="hdr"><button class="chev" id="rkBack"><</button><h2>WORLD BOARD</h2></div>
|
||||||
<div class="tabs" id="rkModes">
|
<div class="tabs" id="rkModes">
|
||||||
<button class="on" data-m="g">SPLATTER</button>
|
<button class="on" data-m="g">SPLATTER</button>
|
||||||
<button data-m="z">TENNIS</button>
|
<button data-m="z">TENNIS</button>
|
||||||
@@ -796,7 +814,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
at it, which is the whole point: it replaced a triple-tap on the in-play scoreboard that
|
at it, which is the whole point: it replaced a triple-tap on the in-play scoreboard that
|
||||||
players kept tripping by accident. -->
|
players kept tripping by accident. -->
|
||||||
<div id="secret">
|
<div id="secret">
|
||||||
<div class="hdr"><button class="chev" id="scBack">‹</button><h2>BACK ROOM</h2></div>
|
<div class="hdr"><button class="chev" id="scBack"><</button><h2>BACK ROOM</h2></div>
|
||||||
<div class="foot bare">
|
<div class="foot bare">
|
||||||
<button class="btn big" id="scWeapons">WEAPON TEST</button>
|
<button class="btn big" id="scWeapons">WEAPON TEST</button>
|
||||||
<button class="btn big" id="scBosses">BOSS TEST</button>
|
<button class="btn big" id="scBosses">BOSS TEST</button>
|
||||||
@@ -808,7 +826,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
<!-- one tile per chapter boss, built from config.bossArt in main.js: adding a design to the
|
<!-- one tile per chapter boss, built from config.bossArt in main.js: adding a design to the
|
||||||
roster puts it on this screen and never touches this markup -->
|
roster puts it on this screen and never touches this markup -->
|
||||||
<div id="bosses">
|
<div id="bosses">
|
||||||
<div class="hdr"><button class="chev" id="bsBack">‹</button><h2>BOSS TEST</h2></div>
|
<div class="hdr"><button class="chev" id="bsBack"><</button><h2>BOSS TEST</h2></div>
|
||||||
<h3>PICK A CHAPTER<span>FULL ARSENAL</span></h3>
|
<h3>PICK A CHAPTER<span>FULL ARSENAL</span></h3>
|
||||||
<div id="bossList"></div>
|
<div id="bossList"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -828,6 +846,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="src/main.js?v=316"></script>
|
<script type="module" src="src/main.js?v=317"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+4
-1
@@ -844,7 +844,10 @@ export const MODES = [
|
|||||||
// (`mode`) or carries its own card and the MODES ids it fans out to (`kids`).
|
// (`mode`) or carries its own card and the MODES ids it fans out to (`kids`).
|
||||||
export const MENU = [
|
export const MENU = [
|
||||||
{ mode: 'classic', diffs: true },
|
{ mode: 'classic', diffs: true },
|
||||||
{ name: 'ZOMBIE TENNIS', sub: 'THE DEAD CLUB PLAYS YOU', art: 'mode_zombie', kids: ['zombie', 'cup', 'train'] },
|
// the WAY-UP ribbon shows on THIS level too, so the click path to the promotion is marked
|
||||||
|
// from the first screen of the picker (main.banner reads `tag`)
|
||||||
|
{ name: 'ZOMBIE TENNIS', sub: 'THE DEAD CLUB PLAYS YOU', art: 'mode_zombie', kids: ['zombie', 'cup', 'train'],
|
||||||
|
tag: 'THE WAY UP' },
|
||||||
{ mode: 'gore' },
|
{ mode: 'gore' },
|
||||||
// NETPLAY (prototype): a poster with no run mode of its own — its second step is HOST / JOIN,
|
// NETPLAY (prototype): a poster with no run mode of its own — its second step is HOST / JOIN,
|
||||||
// and either chip starts classic tennis against the other phone (beats.startNetVs + src/net.js)
|
// and either chip starts classic tennis against the other phone (beats.startNetVs + src/net.js)
|
||||||
|
|||||||
+44
-16
@@ -506,7 +506,21 @@ document.getElementById('trBack').addEventListener('click', () => go('setup'));
|
|||||||
// honours row is a COLLECTION: every slot always visible, earned badges bright, the rest dark —
|
// honours row is a COLLECTION: every slot always visible, earned badges bright, the rest dark —
|
||||||
// the empty slots are the incentive. The ledger reads the same stats module the BODY COUNT does.
|
// the empty slots are the incentive. The ledger reads the same stats module the BODY COUNT does.
|
||||||
const fig = (label, val) => `<div class="fig"><b>${label}</b><i>${val}</i></div>`;
|
const fig = (label, val) => `<div class="fig"><b>${label}</b><i>${val}</i></div>`;
|
||||||
|
// THE CARD COUNTS UP (Oliver): bars sweep in and every number climbs fast on entry. setTimeout,
|
||||||
|
// not rAF — the menu parks the game loop (the bracket's own rule). Timers are cancelled on the
|
||||||
|
// next build so a re-entry never races itself.
|
||||||
|
let cardTimers = [];
|
||||||
|
const countUp = (write, target, dur = 700) => {
|
||||||
|
const t0 = performance.now();
|
||||||
|
(function tick() {
|
||||||
|
const p = Math.min(1, (performance.now() - t0) / dur);
|
||||||
|
write(Math.round(target * (1 - Math.pow(1 - p, 3)))); // easeOutCubic: fast, then settles
|
||||||
|
if (p < 1) cardTimers.push(setTimeout(tick, 30));
|
||||||
|
})();
|
||||||
|
};
|
||||||
function buildCard() {
|
function buildCard() {
|
||||||
|
for (const t of cardTimers) clearTimeout(t);
|
||||||
|
cardTimers = [];
|
||||||
const lg = leagueOf(profile.prof.lg), pr = profile.progress();
|
const lg = leagueOf(profile.prof.lg), pr = profile.progress();
|
||||||
document.getElementById('cdName').textContent = stats.savedName() || 'THE WALK-ON';
|
document.getElementById('cdName').textContent = stats.savedName() || 'THE WALK-ON';
|
||||||
document.getElementById('cdBadge').src = `assets/badge_l${lg.id}.png?v=${SPRV}`;
|
document.getElementById('cdBadge').src = `assets/badge_l${lg.id}.png?v=${SPRV}`;
|
||||||
@@ -515,14 +529,20 @@ function buildCard() {
|
|||||||
el.style.color = lg.color;
|
el.style.color = lg.color;
|
||||||
document.getElementById('cdLevel').textContent =
|
document.getElementById('cdLevel').textContent =
|
||||||
'LEVEL ' + pr.level + ' OF ' + profile.LEVEL_CAP;
|
'LEVEL ' + pr.level + ' OF ' + profile.LEVEL_CAP;
|
||||||
document.getElementById('cdFill').style.width = Math.round(pr.frac * 100) + '%';
|
// both bars sweep from empty to their mark (the 0.7s eased transition in the CSS)
|
||||||
document.getElementById('cdXp').textContent = profile.prof.xp.toLocaleString('en-GB') + ' XP'
|
for (const [id, w] of [['cdFill', Math.round(pr.frac * 100)],
|
||||||
+ (pr.need ? ' · ' + (pr.need - pr.into).toLocaleString('en-GB') + ' TO LEVEL ' + (pr.level + 1)
|
['cdCareerFill', Math.min(100, Math.round(profile.prof.xp / profile.xpFor(profile.LEVEL_CAP) * 100))]]) {
|
||||||
: ' · THE CAP');
|
const bar = document.getElementById(id);
|
||||||
// the CAREER bar is the long form the splash deliberately does not carry: whole-curve progress,
|
bar.style.transition = 'none'; bar.style.width = '0%';
|
||||||
// honest about how far level 50 is
|
void bar.offsetWidth; // commit the empty bar before the sweep
|
||||||
document.getElementById('cdCareerFill').style.width =
|
bar.style.transition = '';
|
||||||
Math.min(100, Math.round(profile.prof.xp / profile.xpFor(profile.LEVEL_CAP) * 100)) + '%';
|
cardTimers.push(setTimeout(() => { bar.style.width = w + '%'; }, 40));
|
||||||
|
}
|
||||||
|
const xpTail = pr.need
|
||||||
|
? ' · ' + (pr.need - pr.into).toLocaleString('en-GB') + ' TO LEVEL ' + (pr.level + 1)
|
||||||
|
: ' · THE CAP';
|
||||||
|
const cdXp = document.getElementById('cdXp');
|
||||||
|
countUp(v => cdXp.textContent = v.toLocaleString('en-GB') + ' XP' + xpTail, profile.prof.xp);
|
||||||
const row = document.getElementById('cdBadges');
|
const row = document.getElementById('cdBadges');
|
||||||
row.innerHTML = '';
|
row.innerHTML = '';
|
||||||
for (const l of LEAGUES) {
|
for (const l of LEAGUES) {
|
||||||
@@ -542,14 +562,22 @@ function buildCard() {
|
|||||||
const g = stats.read('all', undefined, stats.MODE_GORE),
|
const g = stats.read('all', undefined, stats.MODE_GORE),
|
||||||
z = stats.read('all', undefined, stats.MODE_MATCH),
|
z = stats.read('all', undefined, stats.MODE_MATCH),
|
||||||
c = stats.read('all', undefined, stats.MODE_CLASSIC);
|
c = stats.read('all', undefined, stats.MODE_CLASSIC);
|
||||||
document.getElementById('cdFigs1').innerHTML =
|
// the ledger climbs: rows are [box id, [label, target, format]...]; every value starts at 0 and
|
||||||
fig('KILLS', (g.kills + z.kills + c.kills).toLocaleString('en-GB'))
|
// counts up through the same easing the bars sweep with
|
||||||
+ fig('MULTI-KILLS', (g.multi + z.multi + c.multi).toLocaleString('en-GB'))
|
for (const [boxId, rows] of [
|
||||||
+ fig('DEATHS', (g.deaths + z.deaths + c.deaths).toLocaleString('en-GB'));
|
['cdFigs1', [['KILLS', g.kills + z.kills + c.kills],
|
||||||
document.getElementById('cdFigs2').innerHTML =
|
['MULTI-KILLS', g.multi + z.multi + c.multi],
|
||||||
fig('BEST RUN', g.longest + ' G')
|
['DEATHS', g.deaths + z.deaths + c.deaths]]],
|
||||||
+ fig('MOST FELLED', z.longest)
|
['cdFigs2', [['BEST RUN', g.longest, v => v + ' G'],
|
||||||
+ fig('RUNS', (g.matches + z.matches + c.matches).toLocaleString('en-GB'));
|
['MOST FELLED', z.longest],
|
||||||
|
['RUNS', g.matches + z.matches + c.matches]]]]) {
|
||||||
|
const box = document.getElementById(boxId);
|
||||||
|
box.innerHTML = rows.map(([label]) => fig(label, '0')).join('');
|
||||||
|
[...box.querySelectorAll('.fig i')].forEach((cell, i) => {
|
||||||
|
const fmt = rows[i][2] || (v => v.toLocaleString('en-GB'));
|
||||||
|
countUp(v => cell.textContent = fmt(v), rows[i][1]);
|
||||||
|
});
|
||||||
|
}
|
||||||
refreshLook();
|
refreshLook();
|
||||||
}
|
}
|
||||||
document.getElementById('plaque').addEventListener('click', () => { buildCard(); go('card'); });
|
document.getElementById('plaque').addEventListener('click', () => { buildCard(); go('card'); });
|
||||||
|
|||||||
Reference in new issue
Block a user