Skip to content

Commit f662386

Browse files
johnlindquistclaude
andcommitted
chore(site): mobile layout fixes, strike-damped audio, alien heart-defense game
Mobile: - clip horizontal overflow on html/body — decorative overflow was widening the iOS layout viewport, shoving the nav's audio toggle offscreen and desyncing the shader halo from the workshop button - workshop CTA sizes down and caps at its card, so the pulsing halo and the marching ticket train hug the real button on phones - the stretchy name sheet scales to narrow screens instead of forcing a 340px-wide layout Audio: - strike damping: rapidly re-struck notes (letter bumpers, spark landings, wall twangs, glissandi, mash-clicks) ring quieter with each hit and recover with rest — a dot storm on "John Lindquist" now plays a decrescendo instead of a wall of equal-volume strikes Alien defense game: - pixel monsters now raid a five-heart HUD (bottom-right); only slingshot volley darts wound them (two hits to bring one down, brief invulnerability so a single spread can't one-shot), click sprays pass through - an alien on the page adds the four-note invasion march to the groove, doubling to urgent eighths while one charges the hearts - pink pixel hearts drift down to restore health — catch them by tap or by banking sparks into them; downed raiders drop them too - if the aliens win they gather center-stage for a beat-synced taunt dance (schoolyard "nyah-nyah" + saw raspberry), then swagger off and mercy refills the hearts - hidden mdflow:invasion event summons the wave for playtesting Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 51386a1 commit f662386

7 files changed

Lines changed: 587 additions & 50 deletions

File tree

site/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ShaderGuide } from './components/ShaderGuide';
1111
import { ShaderHints } from './components/ShaderHints';
1212
import { CraftedBy } from './components/CraftedBy';
1313
import { EasterEggs } from './components/EasterEggs';
14+
import { AlienDefense } from './components/AlienDefense';
1415
import { shaderAudio } from './components/shaderAudio';
1516
import { Terminal as TerminalIcon, Zap, Volume2, VolumeX } from 'lucide-react';
1617

@@ -237,6 +238,9 @@ export default function App() {
237238
{/* 22 hidden easter eggs + the five-star secret puzzle */}
238239
<EasterEggs />
239240

241+
{/* heart HUD for the alien defense game (ShaderGuide owns the rules) */}
242+
<AlienDefense />
243+
240244
{/* Navigation */}
241245
<nav className="fixed top-0 w-full z-50 border-b border-white/5 bg-[#050505]/80 backdrop-blur-xl">
242246
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">

site/components/AlienDefense.tsx

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React, { useEffect, useRef, useState } from 'react';
2+
3+
/**
4+
* The heart HUD for the pixel-monster defense game (bottom-right).
5+
*
6+
* ShaderGuide owns every game rule (raids, dart hits, pickups, the taunt
7+
* dance) and broadcasts 'mdflow:hearts' — this component only renders the
8+
* consequences. The row carries [data-hearts-anchor] so the raiders in the
9+
* shader know exactly where to charge, and it stays pointer-events-none so
10+
* slingshots fired from this corner still work.
11+
*
12+
* Hidden until the first alien materializes: readers who never meet a
13+
* monster never see a HUD.
14+
*/
15+
16+
interface HeartsDetail {
17+
hearts: number;
18+
max: number;
19+
reason: 'show' | 'steal' | 'gain' | 'defeat' | 'reset';
20+
}
21+
22+
export const AlienDefense: React.FC = () => {
23+
const [state, setState] = useState<HeartsDetail | null>(null);
24+
const [fx, setFx] = useState<'steal' | 'gain' | null>(null);
25+
const [msg, setMsg] = useState<string | null>(null);
26+
const msgTimer = useRef(0);
27+
const fxTimer = useRef(0);
28+
const briefed = useRef(false);
29+
30+
useEffect(() => {
31+
const say = (text: string, ms: number) => {
32+
window.clearTimeout(msgTimer.current);
33+
setMsg(text);
34+
msgTimer.current = window.setTimeout(() => setMsg(null), ms);
35+
};
36+
const onHearts = (ev: Event) => {
37+
const d = (ev as CustomEvent<HeartsDetail>).detail;
38+
setState(d);
39+
if (d.reason === 'steal' || d.reason === 'gain') {
40+
window.clearTimeout(fxTimer.current);
41+
setFx(d.reason);
42+
fxTimer.current = window.setTimeout(() => setFx(null), 700);
43+
}
44+
if (d.reason === 'show' && !briefed.current) {
45+
briefed.current = true;
46+
say('👾 raiders inbound — sling darts at them before they reach your hearts', 9000);
47+
} else if (d.reason === 'steal') {
48+
say(d.hearts <= 1 ? '💔 last heart — stop them!' : '💔 heart stolen!', 4000);
49+
} else if (d.reason === 'gain') {
50+
say('❤️ heart restored', 3000);
51+
} else if (d.reason === 'defeat') {
52+
say('👾👾👾 the aliens win this round — watch them gloat', 9000);
53+
} else if (d.reason === 'reset') {
54+
say('❤️ hearts restored. round two.', 4000);
55+
}
56+
};
57+
window.addEventListener('mdflow:hearts', onHearts);
58+
return () => {
59+
window.removeEventListener('mdflow:hearts', onHearts);
60+
window.clearTimeout(msgTimer.current);
61+
window.clearTimeout(fxTimer.current);
62+
};
63+
}, []);
64+
65+
if (!state) return null;
66+
return (
67+
<>
68+
<style>{`
69+
@keyframes hud-heart-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
70+
@keyframes hud-steal-kf {
71+
0%, 100% { transform: translateX(0); }
72+
20% { transform: translateX(-5px); }
73+
40% { transform: translateX(5px); }
74+
60% { transform: translateX(-4px); }
75+
80% { transform: translateX(3px); }
76+
}
77+
@keyframes hud-gain-kf {
78+
0% { transform: scale(1); }
79+
40% { transform: scale(1.25); }
80+
100% { transform: scale(1); }
81+
}
82+
`}</style>
83+
<div
84+
className="fixed bottom-3 right-3 z-50 flex flex-col items-end gap-1.5 select-none pointer-events-none"
85+
style={{ animation: 'hud-heart-in 0.4s ease-out' }}
86+
>
87+
{msg && (
88+
<div className="max-w-[240px] rounded-lg border border-zinc-700/80 bg-zinc-950/85 px-3 py-2 text-right font-mono text-[11px] leading-snug text-zinc-300 backdrop-blur-md">
89+
{msg}
90+
</div>
91+
)}
92+
<div
93+
data-hearts-anchor
94+
aria-label={`${state.hearts} of ${state.max} hearts left`}
95+
className="flex items-center gap-1 rounded-full border border-white/10 bg-black/60 px-2.5 py-1 backdrop-blur-md"
96+
style={fx === 'steal' ? { animation: 'hud-steal-kf 0.5s ease-in-out' } : undefined}
97+
>
98+
{Array.from({ length: state.max }, (_, i) => {
99+
const filled = i < state.hearts;
100+
return (
101+
<span
102+
key={i}
103+
className={filled
104+
? 'text-sm leading-none text-rose-500 drop-shadow-[0_0_5px_rgba(244,63,94,0.9)]'
105+
: 'text-sm leading-none text-zinc-700'}
106+
style={fx === 'gain' && i === state.hearts - 1
107+
? { animation: 'hud-gain-kf 0.6s ease-out' }
108+
: undefined}
109+
>
110+
111+
</span>
112+
);
113+
})}
114+
</div>
115+
</div>
116+
</>
117+
);
118+
};

site/components/CraftedBy.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export const CraftedBy: React.FC = () => {
170170
viewport={{ once: true, margin: '-80px' }}
171171
transition={{ duration: 0.6 }}
172172
data-shader-credit
173-
className="select-none max-w-3xl mx-auto flex flex-col sm:flex-row items-center gap-10 rounded-2xl border border-orange-500/15 bg-[#0a0a0c]/70 backdrop-blur-sm p-10 sm:p-12 shadow-[0_0_50px_-12px_rgba(249,115,22,0.3),0_0_120px_-30px_rgba(249,115,22,0.2)]"
173+
className="select-none max-w-3xl mx-auto flex flex-col sm:flex-row items-center gap-10 rounded-2xl border border-orange-500/15 bg-[#0a0a0c]/70 backdrop-blur-sm p-6 sm:p-12 shadow-[0_0_50px_-12px_rgba(249,115,22,0.3),0_0_120px_-30px_rgba(249,115,22,0.2)]"
174174
>
175175
{/* grabbable rubber-sheet eggo — stretch it, it snaps back;
176176
dots treat it as a round bumper */}
@@ -234,7 +234,7 @@ export const CraftedBy: React.FC = () => {
234234
data-shader-target="workshop"
235235
data-shader-priority="1"
236236
data-shader-gravity="1.3"
237-
className="group relative z-10 inline-flex items-center gap-3 px-6 py-3.5 rounded-lg bg-white text-black font-mono font-bold text-sm hover:scale-105 active:scale-95 transition-all duration-200 shadow-[0_0_20px_rgba(255,255,255,0.25)] hover:shadow-[0_0_25px_rgba(255,255,255,0.5),0_0_50px_rgba(249,115,22,0.7),0_0_100px_rgba(249,115,22,0.35)]"
237+
className="group relative z-10 inline-flex max-w-full items-center gap-2.5 sm:gap-3 px-4 py-3 sm:px-6 sm:py-3.5 rounded-lg bg-white text-black font-mono font-bold text-xs sm:text-sm hover:scale-105 active:scale-95 transition-all duration-200 shadow-[0_0_20px_rgba(255,255,255,0.25)] hover:shadow-[0_0_25px_rgba(255,255,255,0.5),0_0_50px_rgba(249,115,22,0.7),0_0_100px_rgba(249,115,22,0.35)]"
238238
>
239239
{/* the bolt gets its own stage: an orange chip
240240
with a slow radar ping behind a filled,

site/components/EggoInteractive.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -592,9 +592,16 @@ export const StretchName: React.FC<{
592592
document.fonts?.ready.then(() => { if (alive) setReady(r => r + 1); }).catch(() => {});
593593
return () => { alive = false; };
594594
}, []);
595-
const FONT = '700 46px "Space Grotesk", sans-serif';
596-
const artW = 340;
597-
const artH = 58;
595+
// shrink the whole sheet (art + font) on narrow screens: a fixed 340px
596+
// sheet is the widest thing on a phone and forces horizontal overflow
597+
const [art] = useState(() => {
598+
const w = typeof window === 'undefined' ? 340 : Math.min(340, window.innerWidth - 96);
599+
const s = w / 340;
600+
return { w, h: Math.round(58 * s), font: Math.round(46 * s) };
601+
});
602+
const FONT = `700 ${art.font}px "Space Grotesk", sans-serif`;
603+
const artW = art.w;
604+
const artH = art.h;
598605
// per-letter ink boxes, measured with the same font/pen the sheet draws
599606
// with (art-unit coordinates match the wrapper's css px)
600607
useEffect(() => {

0 commit comments

Comments
 (0)