The portrait canvas is adaptive now. A fixed 270x480 is 9:16 while a modern iPhone is taller, so width bound the fit and ~150 CSS px of screen went unused at the bottom. Height is derived from the device aspect, and TUNE.horizon rides with it — proj() places the court in absolute pixels, so leaving horizon fixed just grew a dead green band between the court and the rack instead of moving the court down. RES 3 to 4 for the extra pixels: an iPhone gives ~1125 device px across and we were rendering 810. That is the first knob to turn back down if the phone stutters. The swish cooldown scales with how swarmed you are — 1.00s at one body, 0.47 at four, 0.22 from fourteen. Oliver died on every game-20 horde because the wave grows with hordeGrow while the panic button stayed a flat second; he chose scaling the melee over shrinking the horde. The first version measured the crowd after meleeSwipe had already killed it, so it counted survivors and barely moved. Winning a duel while wounded lost the weapon popup entirely: showModal never clobbers an open modal, so the heal modal opened first and the tin's was dropped. One popup carries both now. And the mop boy stays off the court unless a stain actually passes the test mopRound uses to pick a target. He was walking on, finding nothing reachable, and walking straight off again. Tennis untouched: 50 hits / 0 whiffs. 45-game soak clean, all four chapters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66 lines
3.7 KiB
HTML
66 lines
3.7 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">
|
|
<style>
|
|
/* pixel HUD font — bundled so it renders offline; used by the canvas scoreboard + rack */
|
|
@font-face { font-family: 'Silkscreen'; font-weight: 400; font-display: block;
|
|
src: url('assets/fonts/Silkscreen-Regular.ttf') format('truetype'); }
|
|
@font-face { font-family: 'Silkscreen'; font-weight: 700; font-display: block;
|
|
src: url('assets/fonts/Silkscreen-Bold.ttf') format('truetype'); }
|
|
html, body { margin: 0; height: 100%; background: #10141c; display: grid; place-items: center; }
|
|
canvas { image-rendering: pixelated; width: min(96vw, calc(96vh * 16 / 9)); aspect-ratio: 16/9; touch-action: none; }
|
|
#hint { position: fixed; bottom: 8px; width: 100%; text-align: center;
|
|
font: 400 10px Silkscreen, monospace; color: #6b7688; }
|
|
@media (pointer: coarse) { #hint, #intro .kb { display: none; } }
|
|
#intro { position: fixed; inset: 0; z-index: 10; touch-action: none; display: grid; place-items: center;
|
|
background: radial-gradient(circle at 50% 34%, rgba(34,16,20,0.92), rgba(8,10,16,0.96)); }
|
|
#intro .card { font: 400 11px Silkscreen, monospace; color: #dfe6f2; text-align: center; line-height: 1.9;
|
|
padding: 18px 26px; max-width: 92vw; animation: drop 0.5s ease-out; }
|
|
@keyframes drop { from { transform: translateY(-14px); opacity: 0; } }
|
|
/* the logo is generated pixel art now (assets/logo.png), not stacked CSS text */
|
|
#intro .logo { display: block; width: min(320px, 78vw); height: auto; margin: 0 auto 8px;
|
|
image-rendering: pixelated; filter: drop-shadow(0 3px 0 rgba(0,0,0,0.45)); }
|
|
#intro .slogan { margin: 8px 0 16px; font-size: 12px; color: #b6a0a4; letter-spacing: 0.5px; }
|
|
#intro .slogan b { color: #c9564a; }
|
|
#intro .how { display: inline-block; text-align: left; color: #cbd4e2; line-height: 2; }
|
|
#intro .how p { margin: 0; }
|
|
#intro .how span { display: inline-block; width: 30px; }
|
|
#intro em { color: #8a96ab; font-style: normal; font-size: 12px; }
|
|
#intro .start { margin-top: 18px; color: #e0cc72; font-weight: bold; letter-spacing: 1px; animation: blink 1.1s step-end infinite; }
|
|
@keyframes blink { 50% { opacity: 0.25; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="c" width="480" height="270"></canvas>
|
|
<div id="hint">move: WASD/arrows · serve/swing: SPACE · aim: hold a direction while swinging · T: tuning panel</div>
|
|
<div id="intro">
|
|
<div class="card">
|
|
<img class="logo" src="assets/logo.png" alt="Splatter Tennis">
|
|
|
|
<p class="slogan">Fifteen. Thirty. Forty. <b>Dead.</b></p>
|
|
<div class="how">
|
|
<p><span>👆</span> tap — serve + swing</p>
|
|
<p><span>👉</span> flick — aimed shot</p>
|
|
<p><span>✋</span> drag — run <em>(auto-run is on)</em></p>
|
|
<p class="kb"><span>⌨️</span> WASD / arrows + SPACE</p>
|
|
</div>
|
|
<p class="start">TAP TO START</p>
|
|
</div>
|
|
</div>
|
|
<script type="module" src="main.js?v=44"></script>
|
|
</body>
|
|
</html>
|