Oliver, after the facing fix: still flashing, female worst. Film-strip diagnosis in the pane: with facing pinned the racket STILL alternated — frame 4 of the female cycle tucked it across the body, and half the frames of both player cycles lost or shrank the hoop to cut fragments (overlapping figures sliced by the even-spaced valley cuts). Both cycles re-rolled with three new prompt rules that now live in _runroll.py: the racket stays right of the midline in EVERY figure (named again for the airborne pose, where it kept vanishing), and the figures never overlap (no more sliced hoops — a debris-stripping pass was tried and removed: after the recrop a real racket is indistinguishable from a fragment, prevention beats cleanup). The model returns 2-row grids at times; a coherent band is cropped and cut. Installed: racket-side spread +0.01..+0.05 across all frames of both cycles (was a full side swap). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
437 lines
30 KiB
HTML
437 lines
30 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Splatter Tennis</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<!-- save-to-homescreen: a bloody tennis ball. iOS needs the apple-* tags (it ignores the
|
|
manifest's display/name); Android reads the manifest. -->
|
|
<link rel="icon" type="image/png" href="assets/favicon.png">
|
|
<link rel="apple-touch-icon" href="assets/icon-180.png">
|
|
<link rel="manifest" href="manifest.webmanifest">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-title" content="Splatter">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<meta name="theme-color" content="#2b2735">
|
|
<!-- STAGING WEARS A DIFFERENT FACE. Both builds get added to the same phone's homescreen, and two
|
|
identical bloody tennis balls is a coin toss every time you open one. Driven by HOSTNAME, not by
|
|
the branch: this block is identical on staging and on main, so it never conflicts on a merge and
|
|
never has to be remembered. Prod is the one named host; everything else (staging, localhost, an
|
|
IP) is a test build and says so. Runs in <head>, before the install prompt can read any of it. -->
|
|
<script>
|
|
if (location.hostname !== 'splatter-tennis.wuwi.one') {
|
|
for (const [sel, href] of [['link[rel=icon]', 'assets/favicon-stg.png'],
|
|
['link[rel=apple-touch-icon]', 'assets/icon-180-stg.png'],
|
|
['link[rel=manifest]', 'manifest-stg.webmanifest']])
|
|
document.querySelector(sel).setAttribute('href', href);
|
|
document.querySelector('meta[name=apple-mobile-web-app-title]').setAttribute('content', 'Splatter STG');
|
|
document.querySelector('meta[name=theme-color]').setAttribute('content', '#14334a');
|
|
document.title = 'Splatter Tennis — STAGING';
|
|
}
|
|
</script>
|
|
<style>
|
|
/* pixel HUD font — bundled so it renders offline; used by the canvas scoreboard + rack.
|
|
?v= like the sprites: server.py caches a VERSIONED asset for a year and an unversioned one for
|
|
a day, so these three files are fetched once per release instead of revalidated on every cold
|
|
start. Bump alongside SPRV in render.js if a font file is ever replaced. */
|
|
@font-face { font-family: 'Silkscreen'; font-weight: 400; font-display: block;
|
|
src: url('assets/fonts/Silkscreen-Regular.ttf?v=86') format('truetype'); }
|
|
@font-face { font-family: 'Silkscreen'; font-weight: 700; font-display: block;
|
|
src: url('assets/fonts/Silkscreen-Bold.ttf?v=86') format('truetype'); }
|
|
/* the ANNOUNCEMENT face. Silkscreen is the HUD and the instructions — small, even, quiet. The
|
|
things that shout (SET, CHAPTER, EPIC!, PERFECT) are a second register on purpose, so a message
|
|
tells you which kind it is before you have read a word of it. Bundled, like the other one. */
|
|
@font-face { font-family: 'Bangers'; font-weight: 400; font-display: block;
|
|
src: url('assets/fonts/Bangers-Regular.ttf?v=86') format('truetype'); }
|
|
/* html needs a real HEIGHT: the root element's background paints the viewport, but a GRADIENT is
|
|
laid out in the root's own box — and with the canvas position:fixed the root has no content and
|
|
collapses, so the gradient render.js sets had nowhere to go. */
|
|
html { height: 100%; height: 100dvh; }
|
|
html, body { margin: 0; padding: 0; background: #10141c; overflow: hidden; }
|
|
/* invisible probe: env() is only readable from CSS, so park the insets on an element and let
|
|
render.js measure them. The canvas stays FULL-BLEED (the stands under the notch look good) —
|
|
it is the HUD that has to move, see SAFE in render.js. */
|
|
#safe { position: fixed; top: 0; left: 0; width: 0; height: 0; pointer-events: none; visibility: hidden;
|
|
padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px); }
|
|
/* The canvas STRETCHES to the viewport and render.js derives its logical height from the box it
|
|
actually got. Sizing it the other way round (compute a scale from innerWidth/innerHeight, then set
|
|
explicit px) meant it was laid out against whatever innerHeight reported at load — before iOS
|
|
Safari's toolbars had settled — and it stayed a strip short of the bottom forever after.
|
|
100dvh is the dynamic viewport height, so it tracks those toolbars. */
|
|
/* TOP inset only. The status bar is opaque UI and the scoreboard must clear it, so the canvas starts
|
|
below it — but the home indicator is a slim translucent bar, so the canvas runs all the way to the
|
|
PHYSICAL bottom edge and the court art fills it. Only the weapon RACK lifts clear of the indicator,
|
|
via SAFE.bottom in render.js. Reserving the bottom inset here instead just left a dead band.
|
|
100dvh is the dynamic viewport height, so it tracks iOS Safari's toolbars appearing/hiding. */
|
|
canvas { image-rendering: pixelated; display: block; position: fixed; touch-action: none;
|
|
top: env(safe-area-inset-top, 0px);
|
|
left: env(safe-area-inset-left, 0px);
|
|
width: calc(100vw - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px));
|
|
height: calc(100vh - env(safe-area-inset-top, 0px));
|
|
height: calc(100dvh - env(safe-area-inset-top, 0px)); }
|
|
/* ...but NOT installed. Launched from the homescreen, WebKit does not initialise the DYNAMIC
|
|
viewport until the geometry is "exercised" — 100dvh stays provisional until you rotate the
|
|
phone, which is exactly the bug. Standalone has no toolbars, so 100vh is both correct and
|
|
stable from the first frame; dvh only exists for the browser's disappearing toolbars. */
|
|
@media (display-mode: standalone) {
|
|
html { height: 100vh; }
|
|
canvas { height: calc(100vh - env(safe-area-inset-top, 0px)); }
|
|
}
|
|
/* desktop key hint — up in the empty top-right corner now that the rack owns the bottom edge */
|
|
#hint { position: fixed; top: 6px; right: 10px; text-align: right;
|
|
font: 400 10px Silkscreen, monospace; color: #6b7688; }
|
|
@media (pointer: coarse) { #hint { display: none; } }
|
|
/* ---- THE MENU PALETTE. The canvas HUD keeps its own blue-grey and gold; the menus are
|
|
deliberately a different room — dark court green, one acid accent, and one crimson reserved for
|
|
the thing you actually came here to press. Five names so a repaint is five edits, not fifty. */
|
|
/* COOL, not olive. The first pass sat around hue 80 — yellow-green — and read loud and warm next
|
|
to the art. These are the same idea rotated towards blue-green and desaturated: the field is
|
|
nearly neutral, the borders are quiet, and the acid accent is the only thing allowed to be
|
|
bright, so it still means "this is the number / this is selected". */
|
|
:root { --ink:#0e1714; --panel:rgba(42,62,54,0.42); --line:#2c4238; --acid:#bcd455;
|
|
--cream:#ccd7ca; --dim:#6c7f74; --red:#a4231f; --bad:#cf5a4e;
|
|
/* the same green as --panel over --ink, but OPAQUE: the pinned header and footer have
|
|
content scrolling underneath them and a translucent bar shows it through */
|
|
--solid:#16211d; }
|
|
/* touch-action is pan-y, not none: the BODY COUNT list is taller than a phone and has to scroll.
|
|
align-content is `safe center` rather than place-items — plain centring on a grid whose item
|
|
overflows pushes the top of the item out of the scrollport, where nothing can reach it. */
|
|
#intro { position: fixed; inset: 0; z-index: 10; touch-action: pan-y; background: var(--ink);
|
|
display: grid; grid-template-columns: 1fr; justify-items: center;
|
|
align-content: safe center; overflow-y: auto; overscroll-behavior: contain; }
|
|
/* the SPLASH sits on the court; the other stages are lists and want a flat field behind them.
|
|
The art is dimmed here rather than baked dark, so the same file can be reused at another level. */
|
|
#intro[data-stage="splash"] {
|
|
/* Three stops, not two. The logo needs a dark, quiet band behind it and the blood needs to be
|
|
READABLE, and those pull opposite ways — so the scrim is heaviest at the top, lifts over the
|
|
court where the stain is, and comes back down only far enough to seat the buttons. */
|
|
background: linear-gradient(rgba(12,20,17,0.60) 0%, rgba(12,20,17,0.30) 46%,
|
|
rgba(12,20,17,0.58) 100%),
|
|
url('assets/menu_bg.png?v=86') 50% 42% / cover no-repeat var(--ink);
|
|
image-rendering: pixelated; }
|
|
/* THE BACK-ROOM HOTSPOT: five taps here open it (main.js). Top-left CORNER, not the logo and not
|
|
the screen — a hidden door has to be somewhere you do not put your thumb by accident, and the
|
|
whole splash was reachable by every stray tap on the way to PLAY MATCH. Invisible on purpose,
|
|
and a child of #splash so it exists on no other stage. */
|
|
#hot { position: fixed; top: 0; left: 0; width: 26vw; max-width: 150px; height: 15vh;
|
|
max-height: 130px; z-index: 4; }
|
|
#intro .card { font: 400 12px Silkscreen, monospace; color: var(--cream); text-align: center;
|
|
line-height: 1.9; padding: 16px 14px 20px; width: min(370px, 94vw);
|
|
animation: drop 0.5s ease-out; }
|
|
@keyframes drop { from { transform: translateY(-14px); opacity: 0; } }
|
|
/* EVERY SCREEN FILLS THE VIEWPORT, and every screen is the same three bands: something pinned to
|
|
the top, something pinned to the bottom, and whatever is between them taking the slack. The
|
|
card is a full-height flex column, so `margin-top:auto` on the footer drops it to the bottom
|
|
when the content is short and `position:sticky` keeps it there once the content is long enough
|
|
to scroll. Centring the card instead left the buttons floating in the middle of a tall phone. */
|
|
#intro { align-content: stretch; }
|
|
#intro .card { display: flex; flex-direction: column; min-height: 100dvh; padding: 0 14px; }
|
|
/* the slack band: the title sits in the middle of whatever is left over */
|
|
.mid { flex: 1; display: flex; flex-direction: column; justify-content: center; padding: 12px 0; }
|
|
/* the logo is generated pixel art (assets/logo.png), not stacked CSS text */
|
|
#intro .logo { display: block; width: min(300px, 74vw); height: auto; margin: 0 auto 6px;
|
|
image-rendering: pixelated; filter: drop-shadow(0 3px 0 rgba(0,0,0,0.45)); }
|
|
/* the slogan carries most of the gap between the logo's blood and the first button */
|
|
#intro .slogan { margin: 8px 0 20px; font-size: 11px; color: #a8a09e; letter-spacing: 0.5px; }
|
|
#intro .slogan b { color: #c9564a; }
|
|
/* which build is served — filled in by main.js, empty (and invisible) if the header is missing */
|
|
#build { margin: 14px 0 0; font-size: 9px; color: #4a5a52; letter-spacing: 0.5px; }
|
|
/* ONE overlay, four panels, swapped by #intro[data-stage]. Keeping the backdrop and the card frame
|
|
alive across every screen is the point — a screen that unmounts the whole overlay flashes the
|
|
court through between menus. */
|
|
#splash, #setup, #stats, #ranks, #newbest, #secret, #bosses { display: none; }
|
|
#intro[data-stage="splash"] #splash { display: flex; flex-direction: column; flex: 1; }
|
|
/* flex COLUMN, not block: it is what lets the footer's margin-top:auto reach the bottom of a
|
|
screen whose content does not fill it */
|
|
#intro[data-stage="setup"] #setup,
|
|
#intro[data-stage="stats"] #stats,
|
|
#intro[data-stage="ranks"] #ranks,
|
|
#intro[data-stage="secret"] #secret,
|
|
#intro[data-stage="bosses"] #bosses,
|
|
#intro[data-stage="newbest"] #newbest { display: flex; flex-direction: column; flex: 1;
|
|
animation: drop 0.3s ease-out; }
|
|
/* the overlay is HIDDEN during play, not removed: a restart puts it straight back, and the
|
|
display:grid above would otherwise win over the hidden attribute */
|
|
#intro[hidden] { display: none; }
|
|
/* Screen titles and the primary button are BANGERS — the same announcement face the canvas uses
|
|
for SET and CHAPTER. A screen title is a thing that shouts. Everything that is a NUMBER or a
|
|
label stays Silkscreen, so the two registers keep meaning what they have always meant. */
|
|
#intro h2 { margin: 0; font: 400 26px Bangers, cursive; color: var(--acid); letter-spacing: 1.5px; }
|
|
#intro h3 { display: flex; justify-content: space-between; margin: 20px 0 9px;
|
|
font: 400 10px Silkscreen, monospace; color: var(--dim); letter-spacing: 2px;
|
|
text-align: left; }
|
|
/* header strip: back chevron + title. Every screen but the splash wears one, pinned to the top. */
|
|
.hdr { position: sticky; top: 0; z-index: 3; display: flex; align-items: center; gap: 10px;
|
|
padding: 10px 12px; margin: 0 -14px; border-bottom: 2px solid var(--line);
|
|
background: var(--solid);
|
|
padding-top: max(10px, env(safe-area-inset-top)); }
|
|
.hdr h2 { padding-left: 2px; }
|
|
.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;
|
|
-webkit-tap-highlight-color: transparent; }
|
|
/* the BODY COUNT headline row: three boxed figures sharing one frame */
|
|
.figs { display: flex; border: 2px solid var(--line); background: var(--panel); }
|
|
.fig { flex: 1; min-width: 0; padding: 7px 6px; text-align: center; }
|
|
.fig + .fig { border-left: 2px solid var(--line); }
|
|
.fig b { display: block; font: 400 9px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; }
|
|
.fig i { display: block; font: 700 18px Silkscreen, monospace; color: var(--acid);
|
|
font-style: normal; letter-spacing: 1px; }
|
|
.fig.bad i { color: var(--bad); }
|
|
/* one button look for every menu screen */
|
|
/* OPAQUE, not the translucent panel fill. On the list screens it makes no visible difference —
|
|
they sit on flat ink — but on the splash a see-through button lets the court's white lines run
|
|
straight through the lettering, and the button stops reading as a button. */
|
|
.btn { display: block; width: 100%; margin: 0 auto 10px; padding: 12px 10px;
|
|
background: var(--solid); border: 2px solid var(--line); color: var(--cream);
|
|
font: 400 13px Silkscreen, monospace; letter-spacing: 1px; cursor: pointer;
|
|
-webkit-tap-highlight-color: transparent; }
|
|
/* The generated button plates are gone with the tutorial. The primary is a flat crimson block
|
|
now: it survives being resized, which a fixed-size plate crop never did, and the mockup's
|
|
hierarchy is carried by colour rather than by art. */
|
|
.btn.big { padding: 13px 10px 11px; background: var(--red); border-color: #69201c; color: #f6e7dc;
|
|
font: 400 26px Bangers, cursive; letter-spacing: 2px; line-height: 1.15; }
|
|
.two { display: flex; gap: 8px; }
|
|
.two .btn { margin-bottom: 0; }
|
|
.btn.sec i { display: block; margin-top: 4px; font: 400 9px Silkscreen, monospace;
|
|
font-style: normal; color: var(--dim); letter-spacing: 0.5px; }
|
|
/* the tour: two tiles SIDE BY SIDE, not two more full-width rows. The figures face the camera —
|
|
in play you only ever see your player from behind, so a rear view would show nothing. */
|
|
#tours { display: flex; gap: 8px; }
|
|
#tours button { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px;
|
|
padding: 8px 6px 10px; background: var(--panel); border: 2px solid var(--line);
|
|
color: inherit; font: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
|
#tours button.on { border-color: var(--acid); background: rgba(188,212,85,0.09); }
|
|
#tours img { height: 88px; width: auto; display: block; image-rendering: pixelated; }
|
|
#tours b { font: 400 11px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px; }
|
|
/* THE BOSS PICKER (back room). Two columns of portraits, because you pick a boss by looking at
|
|
him — the tile is mostly art. Same plate as the tour tiles above; the grid is what makes ten
|
|
of them fit on a phone. */
|
|
#bossList { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding-bottom: 14px; }
|
|
#bossList button { display: flex; flex-direction: column; align-items: center; gap: 5px;
|
|
padding: 8px 6px 10px; background: var(--panel); border: 2px solid var(--line);
|
|
color: inherit; font: inherit; cursor: pointer;
|
|
-webkit-tap-highlight-color: transparent; }
|
|
#bossList img { height: 78px; width: auto; display: block; image-rendering: pixelated; }
|
|
#bossList b { font: 400 9px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px;
|
|
text-align: center; line-height: 1.35; }
|
|
#bossList i { font: 400 8px Silkscreen, monospace; color: var(--dim); font-style: normal;
|
|
letter-spacing: 1px; }
|
|
/* a row, not a stacked tile: three 3:2 tiles stacked fill a phone twice over, and the art still
|
|
reads at this size. -webkit-tap-highlight-color kills iOS's blue flash over the pixel art. */
|
|
/* MODE tiles reuse the court row wholesale — same box, same selected state. Only the picture
|
|
differs: a court tile is a photo of a court and crops fine, a mode tile is a SPRITE and must be
|
|
shown whole, so it gets `contain` and a dark plate to stand on. */
|
|
#modeList img { object-fit: contain !important; background: rgba(0,0,0,0.35); }
|
|
#courtList button.court, #modeList button.court { display: flex; align-items: center; gap: 11px; width: 100%;
|
|
margin: 0 0 8px; padding: 7px; background: var(--panel);
|
|
border: 2px solid var(--line); color: inherit; font: inherit; text-align: left;
|
|
cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
|
#courtList button.court.on, #modeList button.court.on { border-color: var(--acid); background: rgba(188,212,85,0.09); }
|
|
#courtList img, #modeList img { width: 92px; height: 66px; display: block; image-rendering: pixelated; flex: none;
|
|
object-fit: cover; }
|
|
#courtList .cb, #modeList .cb { flex: 1; min-width: 0; }
|
|
#courtList b, #modeList b { display: block; font: 400 12px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px; }
|
|
#courtList i, #modeList i { display: block; margin: 3px 0 6px; font: 400 9px Silkscreen, monospace; color: var(--dim); font-style: normal; }
|
|
/* 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. */
|
|
.meter { display: flex; align-items: center; gap: 6px; margin-top: 4px;
|
|
font: 400 8px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; }
|
|
.meter u { flex: none; width: 46px; text-decoration: none; }
|
|
.meter s { flex: 1; height: 5px; background: rgba(0,0,0,0.45); text-decoration: none; display: block; }
|
|
.meter s > i { display: block; height: 100%; background: var(--acid); }
|
|
.meter.b s > i { background: #4e8fa6; }
|
|
/* the commit strip. #intro is the scrollport, so sticky resolves against it and the button stays
|
|
reachable however long the list above grows. */
|
|
/* margin-top:auto pins it to the BOTTOM of a short screen; sticky keeps it there on a long one */
|
|
.foot { position: sticky; bottom: 0; z-index: 2; margin: auto -14px 0; padding: 10px 14px;
|
|
padding-bottom: max(10px, env(safe-area-inset-bottom));
|
|
border-top: 2px solid var(--line); background: var(--solid); }
|
|
.foot .btn { margin-bottom: 0; }
|
|
/* the splash's bottom band: no bar, no rule — the court art has to keep running under it */
|
|
.foot.bare { border: 0; background: none; }
|
|
.foot.bare .btn.big { margin-bottom: 10px; }
|
|
/* the boot bar wears the stats screen's kill bar, doing duty as a loading bar. While #splash is
|
|
.busy it stands in the buttons' place — same band, same height, so nothing jumps when it goes. */
|
|
#boot { display: none; margin-bottom: 10px; }
|
|
#boot s { display: block; height: 6px; background: rgba(0,0,0,0.45); text-decoration: none; }
|
|
#boot s > i { display: block; width: 0; height: 100%; background: var(--acid);
|
|
transition: width 0.15s linear; }
|
|
#boot u { display: block; margin-top: 7px; text-align: center; text-decoration: none;
|
|
font: 400 9px Silkscreen, monospace; color: var(--dim); letter-spacing: 2px; }
|
|
#splash.busy #boot { display: block; }
|
|
#splash.busy .btn, #splash.busy .two, #splash.busy #build { display: none; }
|
|
#sSum { display: flex; justify-content: space-between; gap: 8px; margin: 0 0 8px;
|
|
font: 400 9px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; }
|
|
.tabs { display: flex; gap: 6px; margin: 12px 0 14px; }
|
|
.tabs button { flex: 1; padding: 11px 2px; background: var(--panel); border: 2px solid var(--line);
|
|
color: var(--dim); font: 400 10px Silkscreen, monospace; letter-spacing: 1px;
|
|
cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
|
.tabs button.on { border-color: var(--acid); color: var(--acid); }
|
|
/* one opponent. The sprite is the walk frame the game already draws, sat on its own baseline. */
|
|
.zrow { display: flex; align-items: center; gap: 11px; padding: 9px 10px; margin-bottom: 6px;
|
|
border: 2px solid var(--line); background: var(--panel); text-align: left; }
|
|
.zrow.boss { border-color: #6e2a24; background: rgba(110,42,36,0.22); }
|
|
.zrow img { width: 48px; height: 62px; object-fit: contain; object-position: bottom;
|
|
image-rendering: pixelated; flex: none; }
|
|
.zb { flex: 1; min-width: 0; display: grid; grid-template-columns: 1fr auto; gap: 0 8px;
|
|
line-height: 1.4; }
|
|
.zb b { font: 400 12px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px;
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.zb > span { font: 700 15px Silkscreen, monospace; color: var(--acid); text-align: right; }
|
|
.zrow.boss .zb > span { color: var(--bad); }
|
|
.pb { grid-column: 1 / -1; height: 6px; margin-top: 4px; background: rgba(0,0,0,0.45); }
|
|
.pb s { display: block; height: 100%; background: var(--acid); text-decoration: none; }
|
|
.zrow.boss .pb s { background: var(--bad); }
|
|
.zb i { grid-column: 1 / -1; margin-top: 4px; font: 400 9px Silkscreen, monospace;
|
|
font-style: normal; color: var(--dim); letter-spacing: 1px; }
|
|
#stW { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
.wtile { flex: 1 1 62px; max-width: 84px; padding: 8px 2px; border: 2px solid var(--line);
|
|
background: var(--panel); }
|
|
.wtile img { width: 34px; height: 34px; object-fit: contain; display: block; margin: 0 auto 2px;
|
|
image-rendering: pixelated; }
|
|
.wtile b { display: block; font: 400 11px Silkscreen, monospace; color: var(--acid); }
|
|
.none { margin: 4px 0 0; font: 400 9px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px;
|
|
text-align: left; }
|
|
#stFoot { display: flex; justify-content: space-between; align-items: baseline;
|
|
border-top: 2px solid var(--line); }
|
|
#stFoot b { font: 400 9px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; }
|
|
#stFoot i { font: 700 16px Silkscreen, monospace; color: var(--acid); font-style: normal; }
|
|
.rkrow { display: flex; align-items: center; gap: 10px; padding: 8px 10px; margin-bottom: 5px;
|
|
border: 2px solid var(--line); background: var(--panel); text-align: left; }
|
|
.rkrow.me { border-color: var(--acid); background: rgba(188,212,85,0.11); }
|
|
.rkrow b { flex: none; width: 68px; font: 700 12px Silkscreen, monospace; color: var(--dim); }
|
|
.rkrow.me b { color: var(--acid); }
|
|
.rkrow span { flex: 1; min-width: 0; font: 400 11px Silkscreen, monospace; color: var(--cream);
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.rkrow i { font: 400 11px Silkscreen, monospace; font-style: normal; color: var(--dim); }
|
|
/* ---- the name prompt. It is the only screen in the game that asks for typing, so the field is
|
|
deliberately big, centred and already focused: on a phone this appears with the keyboard
|
|
rising under it and there is no room for a second tap to find the input. */
|
|
.cent { text-align: center; }
|
|
#newbest h2 { font-size: 30px; margin-bottom: 6px; }
|
|
#nbScore { margin: 0 0 22px; font: 400 11px Silkscreen, monospace; color: var(--dim);
|
|
letter-spacing: 1px; }
|
|
#nbScore b { color: var(--acid); font-weight: 400; }
|
|
#nbName { display: block; width: 100%; box-sizing: border-box; margin: 0 auto 10px;
|
|
padding: 14px 10px; background: var(--solid); border: 2px solid var(--line);
|
|
border-radius: 0; color: var(--acid); font: 700 20px Silkscreen, monospace;
|
|
letter-spacing: 3px; text-align: center; text-transform: uppercase;
|
|
-webkit-appearance: none; -webkit-tap-highlight-color: transparent; }
|
|
#nbName:focus { outline: none; border-color: var(--acid); }
|
|
#nbName::placeholder { color: var(--dim); letter-spacing: 2px; font-size: 14px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="c" width="480" height="270"></canvas>
|
|
<div id="safe"></div>
|
|
<div id="hint">move: WASD/arrows · serve/swing: SPACE · aim: hold a direction while swinging · T: tuning panel</div>
|
|
<div id="intro" data-stage="splash">
|
|
<div class="card">
|
|
<!-- The title screen IS the menu. A separate menu stage behind it was the same logo over the
|
|
same backdrop with two buttons, so it read as the title screen failing to advance. -->
|
|
<!-- three bands: the figures at the top, the title floating in the middle, the buttons under
|
|
your thumb. Same anchoring as every other screen. -->
|
|
<div id="splash">
|
|
<div id="hot"></div>
|
|
<div class="mid">
|
|
<img class="logo" src="assets/logo.png" alt="Splatter Tennis">
|
|
<p class="slogan">Fifteen. Thirty. Forty. <b>Dead.</b></p>
|
|
</div>
|
|
<div class="foot bare">
|
|
<!-- THE BOOT BAR. Every sprite is requested the moment render.js is imported; this only
|
|
shows how far that got, and stands where the buttons will be until it is done — a menu
|
|
you can press before the sheets have landed is how you reach the stats screen with half
|
|
the zombies missing. Swapped by #splash.busy, see main.js. -->
|
|
<div id="boot"><s><i></i></s><u>LOADING</u></div>
|
|
<button class="btn big" id="mStart">PLAY MATCH</button>
|
|
<div class="two">
|
|
<button class="btn sec" id="mStats">STATS<i id="mKills">0 KILLS</i></button>
|
|
<button class="btn sec" id="mRanks">RANKINGS<i>WORLD BOARD</i></button>
|
|
</div>
|
|
<p id="build"></p>
|
|
</div>
|
|
</div>
|
|
<!-- tour tiles and court rows are both built from tables in main.js (config.TOURS / config.COURTS):
|
|
adding either is a table row, never markup -->
|
|
<div id="setup">
|
|
<div class="hdr"><button class="chev" id="sBack">‹</button><h2>MATCH SETUP</h2></div>
|
|
<h3>01 · MODE</h3>
|
|
<div id="modeList"></div>
|
|
<h3>02 · PLAYER</h3>
|
|
<div id="tours"></div>
|
|
<h3>03 · COURT</h3>
|
|
<div id="courtList"></div>
|
|
<div class="foot">
|
|
<p id="sSum"></p>
|
|
<button class="btn big" id="cStart">SERVE UP</button>
|
|
</div>
|
|
</div>
|
|
<!-- BODY COUNT. Every row in here is built by stats.js from the run records in localStorage. -->
|
|
<div id="stats">
|
|
<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
|
|
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. -->
|
|
<div class="tabs" id="stModes">
|
|
<button class="on" data-m="g">SPLATTER</button>
|
|
<button data-m="z">TENNIS</button>
|
|
<button data-m="c">CLASSIC</button>
|
|
</div>
|
|
<div class="tabs" id="stTabs">
|
|
<button class="on" data-p="all">ALL TIME</button>
|
|
<button data-p="last">LAST RUN</button>
|
|
<button data-p="7d">7 DAYS</button>
|
|
</div>
|
|
<div class="figs" id="stFigs"></div>
|
|
<h3 id="stZHdr">BY OPPONENT<span id="stPeriod"></span></h3>
|
|
<div id="stZ"></div>
|
|
<h3 id="stWHdr">BY WEAPON</h3>
|
|
<div id="stW"></div>
|
|
<div class="foot" id="stFoot"></div>
|
|
</div>
|
|
<div id="ranks">
|
|
<div class="hdr"><button class="chev" id="rkBack">‹</button><h2>WORLD BOARD</h2></div>
|
|
<h3>BEST SINGLE RUN<span>KILLS · GAMES</span></h3>
|
|
<div id="rkList"></div>
|
|
</div>
|
|
<!-- THE BACK ROOM. Five taps on the logo (main.js) and nothing else — no button anywhere hints
|
|
at it, which is the whole point: it replaced a triple-tap on the in-play scoreboard that
|
|
players kept tripping by accident. -->
|
|
<div id="secret">
|
|
<div class="hdr"><button class="chev" id="scBack">‹</button><h2>BACK ROOM</h2></div>
|
|
<div class="foot bare">
|
|
<button class="btn big" id="scWeapons">WEAPON TEST</button>
|
|
<button class="btn big" id="scBosses">BOSS TEST</button>
|
|
<button class="btn big" id="scDrill">DRILL TEST</button>
|
|
<button class="btn big" id="scMatch">MATCH TEST</button>
|
|
<button class="btn big" id="scBench">SPRITE BENCH</button>
|
|
</div>
|
|
</div>
|
|
<!-- 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 -->
|
|
<div id="bosses">
|
|
<div class="hdr"><button class="chev" id="bsBack">‹</button><h2>BOSS TEST</h2></div>
|
|
<h3>PICK A CHAPTER<span>FULL ARSENAL</span></h3>
|
|
<div id="bossList"></div>
|
|
</div>
|
|
<!-- shown only when a run beats your own best kill count — see main.js askName -->
|
|
<div id="newbest">
|
|
<div class="mid">
|
|
<h2 class="cent">NEW PERSONAL BEST</h2>
|
|
<p id="nbScore"></p>
|
|
<input id="nbName" maxlength="12" placeholder="YOUR NAME" autocomplete="off"
|
|
autocapitalize="characters" spellcheck="false">
|
|
<p class="none cent" id="nbMsg">PUT IT ON THE WORLD BOARD.</p>
|
|
</div>
|
|
<div class="foot bare">
|
|
<button class="btn big" id="nbSend">SIGN THE BOARD</button>
|
|
<button class="btn" id="nbSkip">NOT THIS TIME</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script type="module" src="src/main.js?v=218"></script>
|
|
</body>
|
|
</html>
|