197 lines
6.8 KiB
HTML
197 lines
6.8 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Tapdown vs CPU — Cyber Sale Edition</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
</head>
|
|
<body class="bg-slate-950 text-white min-h-screen flex flex-col">
|
|
<!-- Top bar -->
|
|
<header class="max-w-5xl mx-auto w-full px-4 py-4 flex items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm text-slate-400">Player:</span>
|
|
<span class="font-semibold">{{ name }}</span>
|
|
<span class="mx-2 opacity-50">·</span>
|
|
<span class="text-sm text-slate-400">Persona:</span>
|
|
<span class="font-semibold">{{ persona }}</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<label class="text-sm opacity-80">Difficulty</label>
|
|
<select id="difficulty" class="text-black p-1 rounded">
|
|
<option value="easy">Easy</option>
|
|
<option value="normal" selected>Normal</option>
|
|
<option value="hard">Hard</option>
|
|
<option value="insane">Insane</option>
|
|
</select>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Arena -->
|
|
<main class="max-w-5xl mx-auto w-full px-4 flex-1">
|
|
<div class="mb-3 text-center text-slate-300 text-sm">
|
|
Beat the <b>Super Saver</b> bot — then browse Cyber Sale deals!
|
|
</div>
|
|
|
|
<div class="relative h-44 rounded-2xl bg-slate-900/60 border border-slate-800 overflow-hidden">
|
|
<div class="absolute inset-y-0 left-1/2 w-0.5 bg-slate-700"></div>
|
|
<div class="absolute inset-y-0 left-0 w-1 bg-emerald-500"></div>
|
|
<div class="absolute inset-y-0 right-0 w-1 bg-cyan-500"></div>
|
|
<div id="puck" class="absolute top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-white shadow"></div>
|
|
</div>
|
|
|
|
<p id="status" class="mt-3 text-center text-lg"></p>
|
|
|
|
<div class="grid grid-cols-2 gap-4 mt-6">
|
|
<button id="tapLeft" class="text-2xl py-12 rounded-xl bg-emerald-600 active:scale-95 font-semibold">YOU TAP</button>
|
|
<button id="tapRight" class="text-2xl py-12 rounded-xl bg-cyan-700 opacity-60 cursor-not-allowed font-semibold" disabled>CPU</button>
|
|
</div>
|
|
|
|
<div class="mt-6 flex items-center justify-center gap-3">
|
|
<button id="startBtn" class="bg-emerald-500 hover:bg-emerald-600 px-5 py-2 rounded-lg font-semibold">Start</button>
|
|
<button id="resetBtn" class="bg-slate-800 hover:bg-slate-700 px-5 py-2 rounded-lg font-semibold">Reset</button>
|
|
<a id="shopBtn" href="/"
|
|
class="bg-slate-900 border border-slate-700 px-5 py-2 rounded-lg font-semibold">Back</a>
|
|
</div>
|
|
|
|
<!-- Cyber Sale CTA under the arena -->
|
|
<div class="mt-8 text-center">
|
|
<a href="{{ url_for('index') }}" class="text-slate-300 underline decoration-dotted">Cyber Sale details & QR on the promo page</a>
|
|
</div>
|
|
</main>
|
|
|
|
<footer class="max-w-5xl mx-auto w-full px-4 py-6 text-center text-slate-400 text-xs">
|
|
Tip: Holding the button auto-taps. Bursts & fatigue make the bot feel human.
|
|
</footer>
|
|
|
|
<script>
|
|
/* --- Pull persona from template to lightly tweak the feel --- */
|
|
const PERSONA = "{{ persona }}";
|
|
|
|
/* --- Physics constants --- */
|
|
const EDGE = 100.0;
|
|
const BASE_IMPULSE = 7.0;
|
|
let FRICTION = 0.92;
|
|
const TICK_HZ = 60;
|
|
|
|
/* Persona perk (very light-touch) */
|
|
let leftImpulseMult = 1.0;
|
|
if (PERSONA === "Speed Demon") leftImpulseMult = 1.02;
|
|
if (PERSONA === "Tank") FRICTION = 0.91;
|
|
|
|
/* --- Game state --- */
|
|
let puck = 0.0, vel = 0.0, running = false;
|
|
let lastTapLeft = 0, lastTapRight = 0;
|
|
let MIN_TAP_MS = 85;
|
|
|
|
/* --- CPU model --- */
|
|
const DIFF = {
|
|
easy: { baseHz: 4.8, burstHz: 7.2, burstProb: 0.20, whiff: 0.08, friction: 0.92 },
|
|
normal: { baseHz: 6.6, burstHz: 9.2, burstProb: 0.28, whiff: 0.05, friction: 0.92 },
|
|
hard: { baseHz: 8.2, burstHz: 11.5, burstProb: 0.35, whiff: 0.035, friction: 0.91 },
|
|
insane: { baseHz: 9.8, burstHz: 13.5, burstProb: 0.45, whiff: 0.02, friction: 0.905 },
|
|
};
|
|
let cpuCfg = DIFF.normal;
|
|
let cpuBurstUntil = 0;
|
|
let cpuNextTapAt = 0;
|
|
|
|
/* --- UI elements --- */
|
|
const statusEl = document.getElementById("status");
|
|
const puckEl = document.getElementById("puck");
|
|
const leftBtn = document.getElementById("tapLeft");
|
|
const startBtn = document.getElementById("startBtn");
|
|
const resetBtn = document.getElementById("resetBtn");
|
|
const diffSel = document.getElementById("difficulty");
|
|
|
|
/* --- Helpers --- */
|
|
function flash(msg){ statusEl.textContent = msg; setTimeout(()=>statusEl.textContent="", 1200); }
|
|
function setDifficulty(key){
|
|
cpuCfg = DIFF[key] || DIFF.normal;
|
|
FRICTION = cpuCfg.friction;
|
|
}
|
|
diffSel.onchange = () => { setDifficulty(diffSel.value); flash(`Difficulty: ${diffSel.value.toUpperCase()}`); };
|
|
|
|
/* --- Controls --- */
|
|
leftBtn.addEventListener("pointerdown", (e) => {
|
|
e.preventDefault(); tap("left");
|
|
const t = setInterval(()=> tap("left"), 100);
|
|
const stop = ()=>{ clearInterval(t); window.removeEventListener("pointerup", stop); leftBtn.removeEventListener("pointerleave", stop); };
|
|
window.addEventListener("pointerup", stop, { once: true });
|
|
leftBtn.addEventListener("pointerleave", stop, { once: true });
|
|
});
|
|
startBtn.onclick = startMatch;
|
|
resetBtn.onclick = resetMatch;
|
|
|
|
/* --- Game functions --- */
|
|
function startMatch(){
|
|
resetMatch();
|
|
running = true;
|
|
scheduleCpuTap(performance.now());
|
|
flash("Fight!");
|
|
}
|
|
function resetMatch(){
|
|
running = false; vel = 0; puck = 0; updatePuck(puck);
|
|
statusEl.textContent = "";
|
|
}
|
|
function tap(side){
|
|
if(!running) return;
|
|
const now = performance.now();
|
|
if(side === "left"){
|
|
if(now - lastTapLeft < MIN_TAP_MS) return;
|
|
lastTapLeft = now;
|
|
vel -= BASE_IMPULSE * leftImpulseMult;
|
|
} else {
|
|
if(now - lastTapRight < MIN_TAP_MS) return;
|
|
lastTapRight = now;
|
|
vel += BASE_IMPULSE;
|
|
}
|
|
}
|
|
|
|
/* --- CPU scheduling --- */
|
|
function scheduleCpuTap(now){
|
|
if(now > cpuBurstUntil && Math.random() < cpuCfg.burstProb){
|
|
cpuBurstUntil = now + (300 + Math.random()*400);
|
|
}
|
|
const inBurst = now < cpuBurstUntil;
|
|
const hz = inBurst ? cpuCfg.burstHz : cpuCfg.baseHz;
|
|
const intervalMs = 1000 * ( -Math.log(1 - Math.random()) / hz );
|
|
cpuNextTapAt = now + intervalMs;
|
|
}
|
|
function maybeCpuTap(now){
|
|
if(now < cpuNextTapAt) return;
|
|
if(Math.random() > cpuCfg.whiff) tap("right");
|
|
scheduleCpuTap(now);
|
|
}
|
|
|
|
/* --- Loop --- */
|
|
function tick(){
|
|
const dt = 1.0 / TICK_HZ;
|
|
if(running){
|
|
vel *= FRICTION;
|
|
puck += vel * dt * 6.0;
|
|
const now = performance.now();
|
|
maybeCpuTap(now);
|
|
if(puck <= -EDGE || puck >= EDGE){
|
|
running = false;
|
|
statusEl.textContent = puck <= -EDGE ? "YOU WIN! 🥳" : "CPU WINS! 🤖";
|
|
}
|
|
}
|
|
updatePuck(puck);
|
|
requestAnimationFrame(tick);
|
|
}
|
|
requestAnimationFrame(tick);
|
|
|
|
function updatePuck(p){
|
|
const track = document.querySelector(".relative.h-44");
|
|
const w = track.clientWidth;
|
|
const x = (p / 100) * (w/2 - 10);
|
|
puckEl.style.left = `calc(50% + ${x}px)`;
|
|
}
|
|
|
|
/* init */
|
|
setDifficulty("normal");
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|