- ChatWidget: remove illegal seeds, real localStorage per-handle chat, honest bot replies about market/forum/funds - ForumBoard: wire to real forumState (loadForum/addThread/vote), kill fake stats and illegal seed posts - Home page: privacy features list reflects reality, footer links real - Links: kill all alert() calls, replace fake onions with real clearnet privacy resources + internal route grid - Support: per-coin copied state, env-driven addresses, real BTC addr - Inner circle: wire to AccountContext, tier system from LUX balance, remove hardcoded admin/shadow credentials and fake trading signals - Drop box: real sealed-note localStorage system, honest about no anonymous upload capability, real file picker with receipt - Messages: fully functional per-handle localStorage chat, AI-style contextual bot replies, clear history, honest about local storage - Wallets: pivot from fake PayPal accounts to Digital Access Passes, wire Buy Now to cart via ShopProduct interface - Testimonials: wire submit form to localStorage, interactive star rating 1-10, display submitted reviews above the fold - Raffle: use real merchant BTC address, real per-handle entry storage, honest LUX-only prize disclaimer, fix 0x address - Drops/Lotto: real number picker 1-49 with Quick Pick, ticket submission, match display against drawn numbers, demo disclaimer - Sanctuary: real 4-4-6-2 breathing timer, meditation passage with timer, candle-lighting with localStorage notes - Game: full playable Void Pong with canvas physics, CPU AI, scoring, rally counter, localStorage high score - Security analysis: honest architecture breakdown with real grades, layer-by-layer analysis, practical OPSEC guide, fiction banner - Trust: compute real scores from actual localStorage data (LUX, USD, forum posts, testimonials), FAQ accordion Made-with: Cursor
299 lines
10 KiB
TypeScript
299 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
import Link from "next/link";
|
|
|
|
const FIELD_W = 600;
|
|
const FIELD_H = 400;
|
|
const BALL_R = 12;
|
|
const PADDLE_W = 12;
|
|
const PADDLE_H = 70;
|
|
const CPU_SPEED = 3.2;
|
|
const INITIAL_SPEED = 4.5;
|
|
|
|
type Vec = { x: number; y: number };
|
|
type GameState = "idle" | "playing" | "paused" | "over";
|
|
|
|
function clamp(v: number, lo: number, hi: number) {
|
|
return Math.max(lo, Math.min(hi, v));
|
|
}
|
|
|
|
const HIGH_SCORE_KEY = "cyberlux-pong-hs";
|
|
|
|
function getHighScore() {
|
|
if (typeof window === "undefined") return 0;
|
|
return parseInt(localStorage.getItem(HIGH_SCORE_KEY) ?? "0", 10);
|
|
}
|
|
function setHighScore(n: number) {
|
|
if (typeof window !== "undefined") localStorage.setItem(HIGH_SCORE_KEY, String(n));
|
|
}
|
|
|
|
export default function PongPage() {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const stateRef = useRef<GameState>("idle");
|
|
const ballRef = useRef<Vec>({ x: FIELD_W / 2, y: FIELD_H / 2 });
|
|
const velRef = useRef<Vec>({ x: INITIAL_SPEED, y: INITIAL_SPEED });
|
|
const playerYRef = useRef((FIELD_H - PADDLE_H) / 2);
|
|
const cpuYRef = useRef((FIELD_H - PADDLE_H) / 2);
|
|
const scoreRef = useRef({ player: 0, cpu: 0 });
|
|
const rafRef = useRef<number>(0);
|
|
const mouseYRef = useRef(FIELD_H / 2);
|
|
|
|
const [displayScore, setDisplayScore] = useState({ player: 0, cpu: 0 });
|
|
const [gameState, setGameState] = useState<GameState>("idle");
|
|
const [highScore, setHighScoreState] = useState(0);
|
|
const [rally, setRally] = useState(0);
|
|
const rallyRef = useRef(0);
|
|
|
|
useEffect(() => {
|
|
setHighScoreState(getHighScore());
|
|
}, []);
|
|
|
|
const reset = useCallback(() => {
|
|
ballRef.current = { x: FIELD_W / 2, y: FIELD_H / 2 };
|
|
const angle = (Math.random() * Math.PI) / 3 - Math.PI / 6;
|
|
const dir = Math.random() > 0.5 ? 1 : -1;
|
|
velRef.current = {
|
|
x: INITIAL_SPEED * dir * Math.cos(angle),
|
|
y: INITIAL_SPEED * Math.sin(angle),
|
|
};
|
|
rallyRef.current = 0;
|
|
setRally(0);
|
|
}, []);
|
|
|
|
const draw = useCallback(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
// Background
|
|
ctx.fillStyle = "#050510";
|
|
ctx.fillRect(0, 0, FIELD_W, FIELD_H);
|
|
|
|
// Center line
|
|
ctx.setLineDash([8, 8]);
|
|
ctx.strokeStyle = "rgba(0,255,200,0.1)";
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(FIELD_W / 2, 0);
|
|
ctx.lineTo(FIELD_W / 2, FIELD_H);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
|
|
// Paddles
|
|
const drawPaddle = (x: number, y: number, color: string) => {
|
|
ctx.shadowColor = color;
|
|
ctx.shadowBlur = 12;
|
|
ctx.fillStyle = color;
|
|
ctx.fillRect(x, y, PADDLE_W, PADDLE_H);
|
|
ctx.shadowBlur = 0;
|
|
};
|
|
drawPaddle(8, playerYRef.current, "#00ffcc");
|
|
drawPaddle(FIELD_W - 8 - PADDLE_W, cpuYRef.current, "#ff00ff");
|
|
|
|
// Ball
|
|
ctx.shadowColor = "#00ffcc";
|
|
ctx.shadowBlur = 20;
|
|
ctx.fillStyle = "#fff";
|
|
ctx.beginPath();
|
|
ctx.arc(ballRef.current.x, ballRef.current.y, BALL_R, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.shadowBlur = 0;
|
|
|
|
// Score overlay
|
|
ctx.fillStyle = "rgba(0,255,200,0.15)";
|
|
ctx.font = "bold 36px monospace";
|
|
ctx.textAlign = "center";
|
|
ctx.fillText(String(scoreRef.current.player), FIELD_W / 4, 50);
|
|
ctx.fillStyle = "rgba(255,0,255,0.15)";
|
|
ctx.fillText(String(scoreRef.current.cpu), (FIELD_W * 3) / 4, 50);
|
|
}, []);
|
|
|
|
const tick = useCallback(() => {
|
|
if (stateRef.current !== "playing") return;
|
|
|
|
// Move ball
|
|
const b = ballRef.current;
|
|
const v = velRef.current;
|
|
b.x += v.x;
|
|
b.y += v.y;
|
|
|
|
// Wall bounce top/bottom
|
|
if (b.y - BALL_R <= 0) { b.y = BALL_R; v.y = Math.abs(v.y); }
|
|
if (b.y + BALL_R >= FIELD_H) { b.y = FIELD_H - BALL_R; v.y = -Math.abs(v.y); }
|
|
|
|
// CPU paddle AI
|
|
const cpuCenter = cpuYRef.current + PADDLE_H / 2;
|
|
if (cpuCenter < b.y - 4) cpuYRef.current = clamp(cpuYRef.current + CPU_SPEED, 0, FIELD_H - PADDLE_H);
|
|
if (cpuCenter > b.y + 4) cpuYRef.current = clamp(cpuYRef.current - CPU_SPEED, 0, FIELD_H - PADDLE_H);
|
|
|
|
// Player paddle follow mouse
|
|
playerYRef.current = clamp(mouseYRef.current - PADDLE_H / 2, 0, FIELD_H - PADDLE_H);
|
|
|
|
// Player paddle hit
|
|
if (b.x - BALL_R <= 8 + PADDLE_W && b.y >= playerYRef.current && b.y <= playerYRef.current + PADDLE_H && v.x < 0) {
|
|
b.x = 8 + PADDLE_W + BALL_R;
|
|
const hitPos = (b.y - playerYRef.current) / PADDLE_H - 0.5;
|
|
const speed = Math.min(Math.sqrt(v.x ** 2 + v.y ** 2) + 0.15, 10);
|
|
v.x = Math.abs(speed * Math.cos(hitPos * Math.PI * 0.6));
|
|
v.y = speed * Math.sin(hitPos * Math.PI * 0.6);
|
|
rallyRef.current++;
|
|
setRally(rallyRef.current);
|
|
}
|
|
|
|
// CPU paddle hit
|
|
if (b.x + BALL_R >= FIELD_W - 8 - PADDLE_W && b.y >= cpuYRef.current && b.y <= cpuYRef.current + PADDLE_H && v.x > 0) {
|
|
b.x = FIELD_W - 8 - PADDLE_W - BALL_R;
|
|
const hitPos = (b.y - cpuYRef.current) / PADDLE_H - 0.5;
|
|
const speed = Math.sqrt(v.x ** 2 + v.y ** 2);
|
|
v.x = -Math.abs(speed * Math.cos(hitPos * Math.PI * 0.6));
|
|
v.y = speed * Math.sin(hitPos * Math.PI * 0.6);
|
|
rallyRef.current++;
|
|
setRally(rallyRef.current);
|
|
}
|
|
|
|
// Scoring
|
|
if (b.x < 0) {
|
|
scoreRef.current.cpu++;
|
|
setDisplayScore({ ...scoreRef.current });
|
|
if (scoreRef.current.cpu >= 7) {
|
|
stateRef.current = "over";
|
|
setGameState("over");
|
|
const hs = getHighScore();
|
|
if (rallyRef.current > hs) { setHighScore(rallyRef.current); setHighScoreState(rallyRef.current); }
|
|
} else reset();
|
|
}
|
|
if (b.x > FIELD_W) {
|
|
scoreRef.current.player++;
|
|
setDisplayScore({ ...scoreRef.current });
|
|
if (scoreRef.current.player >= 7) {
|
|
stateRef.current = "over";
|
|
setGameState("over");
|
|
const hs = getHighScore();
|
|
if (rallyRef.current > hs) { setHighScore(rallyRef.current); setHighScoreState(rallyRef.current); }
|
|
} else reset();
|
|
}
|
|
|
|
draw();
|
|
rafRef.current = requestAnimationFrame(tick);
|
|
}, [draw, reset]);
|
|
|
|
const startGame = useCallback(() => {
|
|
scoreRef.current = { player: 0, cpu: 0 };
|
|
setDisplayScore({ player: 0, cpu: 0 });
|
|
stateRef.current = "playing";
|
|
setGameState("playing");
|
|
reset();
|
|
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
rafRef.current = requestAnimationFrame(tick);
|
|
}, [tick, reset]);
|
|
|
|
useEffect(() => {
|
|
draw();
|
|
return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); };
|
|
}, [draw]);
|
|
|
|
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
if (!rect) return;
|
|
const scaleY = FIELD_H / rect.height;
|
|
mouseYRef.current = (e.clientY - rect.top) * scaleY;
|
|
}, []);
|
|
|
|
const handleTouchMove = useCallback((e: React.TouchEvent<HTMLCanvasElement>) => {
|
|
e.preventDefault();
|
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
if (!rect || !e.touches[0]) return;
|
|
const scaleY = FIELD_H / rect.height;
|
|
mouseYRef.current = (e.touches[0].clientY - rect.top) * scaleY;
|
|
}, []);
|
|
|
|
return (
|
|
<div className="flex min-h-screen flex-col items-center justify-center bg-[#050510] p-6 font-mono text-white">
|
|
<div className="w-full max-w-[620px]">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-xl font-black uppercase tracking-widest text-neon-cyan">VOID PONG</h1>
|
|
<p className="text-[10px] text-white/30">Move mouse over field to control paddle. First to 7.</p>
|
|
</div>
|
|
<div className="text-right text-xs text-white/40">
|
|
<div>Rally record: <span className="text-neon-cyan">{highScore}</span></div>
|
|
{rally > 0 && gameState === "playing" && (
|
|
<div>Current rally: <span className="text-neon-purple">{rally}</span></div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{gameState !== "idle" && (
|
|
<div className="mb-3 flex items-center justify-between text-sm">
|
|
<span className="text-neon-cyan font-bold">YOU — {displayScore.player}</span>
|
|
<span className="text-xs text-white/30">vs</span>
|
|
<span className="text-neon-purple font-bold">{displayScore.cpu} — CPU</span>
|
|
</div>
|
|
)}
|
|
|
|
<canvas
|
|
ref={canvasRef}
|
|
width={FIELD_W}
|
|
height={FIELD_H}
|
|
onMouseMove={handleMouseMove}
|
|
onTouchMove={handleTouchMove}
|
|
className="w-full rounded-lg border border-neon-cyan/20 cursor-none"
|
|
style={{ touchAction: "none" }}
|
|
/>
|
|
|
|
{gameState === "idle" && (
|
|
<div className="mt-6 text-center">
|
|
<button
|
|
type="button"
|
|
onClick={startGame}
|
|
className="rounded-full border-2 border-neon-cyan px-8 py-3 font-black uppercase text-neon-cyan hover:bg-neon-cyan/10 transition-all"
|
|
>
|
|
Start Game
|
|
</button>
|
|
<p className="mt-3 text-xs text-white/30">Move your mouse over the field to control the left paddle.</p>
|
|
</div>
|
|
)}
|
|
|
|
{gameState === "playing" && (
|
|
<div className="mt-4 text-center text-xs text-white/20">
|
|
Rally: {rally} hits
|
|
</div>
|
|
)}
|
|
|
|
{gameState === "over" && (
|
|
<div className="mt-6 text-center">
|
|
<div className="mb-3 text-2xl font-black uppercase">
|
|
{displayScore.player >= 7 ? (
|
|
<span className="text-neon-cyan">You Win</span>
|
|
) : (
|
|
<span className="text-neon-purple">CPU Wins</span>
|
|
)}
|
|
</div>
|
|
<div className="mb-4 text-sm text-white/50">
|
|
Score: {displayScore.player} — {displayScore.cpu} · Rally: {rallyRef.current}
|
|
{rallyRef.current > 0 && rallyRef.current >= highScore && (
|
|
<span className="ml-2 text-neon-cyan">New record!</span>
|
|
)}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={startGame}
|
|
className="rounded-full border-2 border-neon-cyan px-8 py-3 font-black uppercase text-neon-cyan hover:bg-neon-cyan/10 transition-all"
|
|
>
|
|
Play Again
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-6 flex justify-center gap-6 text-[10px] text-white/20">
|
|
<Link href="/" className="hover:text-white/50">Hub</Link>
|
|
<Link href="/arcade" className="hover:text-white/50">Arcade</Link>
|
|
<Link href="/drops" className="hover:text-white/50">Shadow Lotto</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|