Files
ben.de-roo.org/haans/index.html.oud

2147 lines
77 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#020a04">
<title>LAUPAN LUCHCD PEILTJUS</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Chakra+Petch:wght@400;600;700&display=swap');
:root {
--crt-green: #39ff14;
--crt-amber: #ffb300;
--crt-red: #ff2244;
--crt-blue: #00d4ff;
--crt-pink: #ff00cc;
--bg: #020a04;
--panel: rgba(0,20,5,0.92);
}
* { margin:0; padding:0; box-sizing:border-box; }
html, body {
width:100%; height:100%;
background: var(--bg);
font-family: 'Press Start 2P', monospace;
overflow: hidden;
color: var(--crt-green);
}
/* CRT effects */
body::before {
content:'';
position:fixed; inset:0; z-index:9999; pointer-events:none;
background: repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(0,0,0,0.18) 3px, rgba(0,0,0,0.18) 4px);
}
body::after {
content:'';
position:fixed; inset:0; z-index:9998; pointer-events:none;
background: radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.75) 100%);
}
.crt-flicker {
animation: flicker 8s infinite;
}
@keyframes flicker {
0%,95%,100% { opacity:1; }
96% { opacity:0.97; }
97% { opacity:1; }
98% { opacity:0.95; }
99% { opacity:1; }
}
/* ===== SCREENS ===== */
.screen {
position:fixed; inset:0;
display:flex; flex-direction:column;
align-items:center; justify-content:center;
z-index:10;
transition: opacity 0.4s;
}
.screen.hidden { display:none; }
/* ===== ATTRACT / TITLE SCREEN ===== */
#screen-attract {
background: var(--bg);
gap: 28px;
}
.title-logo {
text-align:center;
line-height:1.6;
}
.title-top {
font-size: clamp(1rem, 3vw, 1.5rem);
color: var(--crt-amber);
text-shadow: 0 0 10px var(--crt-amber), 0 0 30px rgba(255,179,0,0.5);
letter-spacing: 0.15em;
animation: glow-amber 1.5s ease-in-out infinite alternate;
}
.title-main {
display:block;
font-size: clamp(1.8rem, 5vw, 3.2rem);
color: var(--crt-green);
text-shadow: 0 0 15px var(--crt-green), 0 0 40px rgba(57,255,20,0.4);
letter-spacing: 0.08em;
animation: glow-green 1.5s ease-in-out infinite alternate;
margin: 8px 0;
}
.title-sub {
font-size: clamp(0.5rem, 1.5vw, 0.75rem);
color: var(--crt-blue);
letter-spacing: 0.3em;
text-shadow: 0 0 8px var(--crt-blue);
}
@keyframes glow-green {
from { text-shadow: 0 0 10px var(--crt-green), 0 0 20px rgba(57,255,20,0.3); }
to { text-shadow: 0 0 20px var(--crt-green), 0 0 50px rgba(57,255,20,0.6), 0 0 80px rgba(57,255,20,0.2); }
}
@keyframes glow-amber {
from { text-shadow: 0 0 10px var(--crt-amber); }
to { text-shadow: 0 0 25px var(--crt-amber), 0 0 50px rgba(255,179,0,0.4); }
}
.dart-icon {
font-size: clamp(3rem, 8vw, 5rem);
animation: spin-dart 3s ease-in-out infinite;
filter: drop-shadow(0 0 12px var(--crt-green));
}
@keyframes spin-dart {
0%,100% { transform: rotate(-15deg) scale(1); }
50% { transform: rotate(15deg) scale(1.1); }
}
.insert-coin {
font-size: clamp(0.5rem, 1.5vw, 0.8rem);
color: var(--crt-amber);
letter-spacing: 0.2em;
animation: blink 0.9s step-end infinite;
text-shadow: 0 0 10px var(--crt-amber);
}
@keyframes blink { 0%,100%{opacity:1;} 50%{opacity:0;} }
.highscore-box {
background: rgba(57,255,20,0.05);
border: 2px solid rgba(57,255,20,0.3);
padding: 12px 30px;
text-align:center;
}
.hs-label { font-size:0.45rem; color:rgba(57,255,20,0.6); letter-spacing:0.2em; margin-bottom:6px; }
.hs-value { font-size:1.2rem; color:var(--crt-green); text-shadow:0 0 10px var(--crt-green); }
/* ===== MENU SCREEN ===== */
#screen-menu {
background: var(--bg);
gap: 30px;
}
.menu-title {
font-size: clamp(0.7rem, 2vw, 1rem);
color: var(--crt-amber);
text-shadow: 0 0 15px var(--crt-amber);
letter-spacing: 0.2em;
text-align:center;
}
.menu-question {
font-size: clamp(0.55rem, 1.5vw, 0.75rem);
color: var(--crt-blue);
letter-spacing: 0.15em;
text-align:center;
line-height:2;
}
.menu-choices {
display:flex;
gap: 30px;
flex-wrap:wrap;
justify-content:center;
}
.choice-card {
width: clamp(160px, 25vw, 220px);
background: rgba(0,30,10,0.9);
border: 3px solid rgba(57,255,20,0.3);
padding: 24px 16px;
text-align:center;
cursor:pointer;
transition: all 0.15s;
position:relative;
overflow:hidden;
}
.choice-card::before {
content:'';
position:absolute; inset:0;
background: linear-gradient(135deg, transparent 50%, rgba(57,255,20,0.03) 100%);
}
.choice-card:hover, .choice-card.selected {
border-color: var(--crt-green);
background: rgba(57,255,20,0.1);
box-shadow: 0 0 20px rgba(57,255,20,0.3), inset 0 0 20px rgba(57,255,20,0.05);
transform: scale(1.04);
}
.choice-icon { font-size: clamp(2rem, 5vw, 3rem); margin-bottom:14px; display:block; }
.choice-name {
font-size: clamp(0.5rem, 1.2vw, 0.7rem);
color: var(--crt-green);
letter-spacing:0.1em;
margin-bottom:8px;
}
.choice-desc {
font-family: 'Chakra Petch', sans-serif;
font-size: clamp(0.6rem, 1.2vw, 0.75rem);
color: rgba(57,255,20,0.6);
line-height:1.6;
}
.choice-key {
display:inline-block;
margin-top:10px;
font-size:0.45rem;
background: rgba(57,255,20,0.15);
border: 1px solid rgba(57,255,20,0.4);
padding: 4px 10px;
color: var(--crt-amber);
letter-spacing:0.1em;
}
/* ===== DIFFICULTY SCREEN ===== */
#screen-difficulty {
background: var(--bg);
gap:24px;
}
.diff-cards {
display:flex; gap:20px; flex-wrap:wrap; justify-content:center;
}
.diff-card {
width: clamp(140px, 20vw, 180px);
border: 3px solid rgba(57,255,20,0.3);
background: rgba(0,20,5,0.9);
padding: 20px 12px;
text-align:center;
cursor:pointer;
transition:all 0.15s;
}
.diff-card:hover {
border-color:var(--crt-amber);
box-shadow:0 0 20px rgba(255,179,0,0.3);
transform:scale(1.05);
}
.diff-name { font-size: clamp(0.5rem,1.2vw,0.7rem); margin-bottom:10px; }
.diff-easy { color:#00ff88; }
.diff-medium { color:var(--crt-amber); }
.diff-hard { color:var(--crt-red); }
.diff-stars { font-size:1rem; margin-bottom:8px; }
.diff-info {
font-family:'Chakra Petch',sans-serif;
font-size:clamp(0.55rem,1vw,0.68rem);
color:rgba(57,255,20,0.55);
line-height:1.6;
}
/* ===== GAME SCREEN ===== */
#screen-game {
background: var(--bg);
flex-direction:row;
align-items:stretch;
justify-content:center;
gap:0;
padding:0;
overflow:hidden;
}
.side-panel {
width: clamp(100px, 15vw, 180px);
background: rgba(0,15,5,0.95);
border-right: 2px solid rgba(57,255,20,0.2);
display:flex; flex-direction:column;
align-items:center;
padding:14px 8px;
gap:14px;
flex-shrink:0;
overflow:hidden;
}
.side-panel.right {
border-right:none;
border-left: 2px solid rgba(57,255,20,0.2);
}
.panel-label {
font-size:0.38rem;
letter-spacing:0.18em;
color:rgba(57,255,20,0.5);
text-transform:uppercase;
margin-bottom:3px;
}
.panel-value {
font-size:clamp(1rem,2.5vw,1.8rem);
color:var(--crt-green);
text-shadow:0 0 12px var(--crt-green);
}
.panel-box {
text-align:center;
width:100%;
background:rgba(57,255,20,0.04);
border:1px solid rgba(57,255,20,0.15);
padding:8px 4px;
}
.darts-display {
display:flex; flex-wrap:wrap; gap:6px; justify-content:center; align-items:center;
}
.dart-pip {
width:28px; height:46px;
opacity:1;
transition: opacity 0.3s, filter 0.3s;
filter: drop-shadow(0 0 5px var(--crt-amber));
}
.dart-pip.used {
opacity:0.15;
filter: none;
}
.streak-fire {
font-size:1.3rem;
text-align:center;
animation:streakPulse 0.5s ease-in-out infinite alternate;
}
@keyframes streakPulse {
from { transform:scale(1); }
to { transform:scale(1.2); }
}
.mult-badge {
font-size:clamp(0.42rem,1vw,0.6rem);
padding:3px 8px;
background:rgba(255,0,204,0.2);
border:1px solid var(--crt-pink);
color:var(--crt-pink);
text-shadow:0 0 8px var(--crt-pink);
text-align:center;
}
/* Center arena — fills remaining space */
.arena-wrap {
flex:1;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap:8px;
padding:10px 6px;
min-width:0;
min-height:0;
overflow:hidden;
}
/* Phase indicator */
.phase-bar {
display:flex;
gap:8px;
align-items:center;
flex-shrink:0;
}
.phase-step {
font-size:0.4rem;
letter-spacing:0.08em;
padding:5px 10px;
border:2px solid rgba(57,255,20,0.2);
color:rgba(57,255,20,0.35);
transition:all 0.3s;
}
.phase-step.active {
border-color:var(--crt-amber);
color:var(--crt-amber);
text-shadow:0 0 10px var(--crt-amber);
background:rgba(255,179,0,0.1);
animation:phaseGlow 0.7s ease-in-out infinite alternate;
}
.phase-step.done {
border-color:var(--crt-green);
color:var(--crt-green);
text-shadow:0 0 8px var(--crt-green);
background:rgba(57,255,20,0.08);
}
@keyframes phaseGlow {
from { box-shadow:0 0 5px rgba(255,179,0,0.3); }
to { box-shadow:0 0 20px rgba(255,179,0,0.7); }
}
.phase-arrow { color:rgba(57,255,20,0.4); font-size:0.5rem; }
/* Arena — use aspect-ratio trick to always fit */
.arena {
position:relative;
/* Take up as much of the available space as possible, square */
width: min(
420px,
calc(100vw - clamp(200px,30vw,360px) - 32px), /* vw minus side panels */
calc(100vh - 120px) /* vh minus phase bar + hint */
);
height: min(
420px,
calc(100vw - clamp(200px,30vw,360px) - 32px),
calc(100vh - 120px)
);
aspect-ratio: 1;
border:3px solid rgba(57,255,20,0.25);
background: radial-gradient(ellipse at center, #001a06 0%, #000a02 100%);
box-shadow: 0 0 40px rgba(57,255,20,0.1), inset 0 0 60px rgba(0,0,0,0.6);
overflow:hidden;
flex-shrink:1;
}
/* Horizontal line (X-axis targeting) */
.h-line {
position:absolute;
left:0; right:0;
height:2px;
background: linear-gradient(90deg, transparent, var(--crt-blue), transparent);
box-shadow:0 0 8px var(--crt-blue), 0 0 20px rgba(0,212,255,0.4);
pointer-events:none;
z-index:6;
transition: top 0s;
}
/* Vertical line (Y-axis targeting) */
.v-line {
position:absolute;
top:0; bottom:0;
width:2px;
background: linear-gradient(180deg, transparent, var(--crt-amber), transparent);
box-shadow:0 0 8px var(--crt-amber), 0 0 20px rgba(255,179,0,0.4);
pointer-events:none;
z-index:6;
}
.crosshair-dot {
position:absolute;
width:16px; height:16px;
border-radius:50%;
border:2px solid rgba(255,255,255,0.8);
transform:translate(-50%,-50%);
pointer-events:none;
z-index:7;
display:none;
box-shadow:0 0 10px rgba(255,255,255,0.6);
}
#dartboard {
position:absolute;
image-rendering:pixelated;
}
/* Flying dart overlay canvas */
#dart-canvas {
position:absolute;
inset:0;
pointer-events:none;
z-index:15;
}
/* Stuck dart SVG marker — stays in board, no fade */
.dart-marker {
position:absolute;
pointer-events:none;
z-index:10;
/* tip of dart is at top of SVG (y=0), so translate(-50%, 0%) puts tip on the target point */
transform-origin: 50% 0%;
opacity:0;
}
@keyframes dartWobble {
0% { opacity:0; transform: translate(-50%, -5%) rotate(var(--tilt)) scaleY(1.2) scaleX(0.85); }
30% { opacity:1; transform: translate(-50%, 2%) rotate(var(--tilt)) scaleY(0.9) scaleX(1.06); }
50% { transform: translate(-50%, -1%) rotate(var(--tilt)) scaleY(1.04) scaleX(0.97); }
68% { transform: translate(-50%, 0.5%) rotate(var(--tilt)) scaleY(0.985) scaleX(1.01); }
82% { transform: translate(-50%, 0%) rotate(var(--tilt)) scaleY(1.005) scaleX(0.998); }
100% { opacity:1; transform: translate(-50%, 0%) rotate(var(--tilt)) scale(1); }
}
/* Impact burst ring */
.impact-ring {
position:absolute;
width:32px; height:32px;
border-radius:50%;
border:2.5px solid rgba(255,220,80,0.9);
transform:translate(-50%,-50%) scale(0.2);
pointer-events:none;
z-index:11;
animation:ringExpand 0.5s cubic-bezier(0.2,0.8,0.3,1) forwards;
}
.impact-ring.miss { border-color:rgba(255,60,60,0.9); }
@keyframes ringExpand {
0% { transform:translate(-50%,-50%) scale(0.2); opacity:1; }
100% { transform:translate(-50%,-50%) scale(3); opacity:0; }
}
.score-popup {
position:absolute;
pointer-events:none;
font-family:'Press Start 2P',monospace;
animation:floatScore 1.4s ease-out forwards;
z-index:20;
text-shadow:0 2px 6px rgba(0,0,0,0.9);
white-space:nowrap;
transform-origin:center;
}
@keyframes floatScore {
0% { opacity:1; transform:translate(-50%,-50%) scale(0.4); }
25% { opacity:1; transform:translate(-50%,-70%) scale(1.1); }
80% { opacity:1; transform:translate(-50%,-150%) scale(0.9); }
100% { opacity:0; transform:translate(-50%,-200%) scale(0.7); }
}
.screen-flash {
position:absolute; inset:0;
pointer-events:none; z-index:30;
animation:flashOut 0.3s ease-out forwards;
}
@keyframes flashOut { 0%{opacity:1;} 100%{opacity:0;} }
/* Action hint */
.action-hint {
font-size:clamp(0.38rem,1vw,0.58rem);
letter-spacing:0.12em;
text-align:center;
padding:7px 16px;
border:2px solid rgba(57,255,20,0.3);
color:var(--crt-green);
background:rgba(0,30,5,0.8);
animation:hintPulse 1s ease-in-out infinite alternate;
min-height:32px;
display:flex; align-items:center; justify-content:center;
flex-shrink:0;
white-space:nowrap;
overflow:hidden;
}
@keyframes hintPulse {
from { border-color:rgba(57,255,20,0.3); }
to { border-color:rgba(57,255,20,0.8); box-shadow:0 0 15px rgba(57,255,20,0.3); }
}
/* ===== GAME OVER SCREEN ===== */
#screen-gameover {
background:rgba(0,5,0,0.97);
gap:20px;
}
.go-title {
font-size:clamp(1.5rem,4vw,2.8rem);
color:var(--crt-red);
text-shadow:0 0 20px var(--crt-red), 0 0 50px rgba(255,34,68,0.4);
animation:glow-red 1s ease-in-out infinite alternate;
letter-spacing:0.15em;
}
@keyframes glow-red {
from { text-shadow:0 0 10px var(--crt-red); }
to { text-shadow:0 0 30px var(--crt-red), 0 0 60px rgba(255,34,68,0.5); }
}
.go-score-wrap { text-align:center; }
.go-label { font-size:0.45rem; color:rgba(57,255,20,0.6); letter-spacing:0.2em; margin-bottom:6px; }
.go-score { font-size:clamp(1.8rem,4vw,2.5rem); color:var(--crt-amber); text-shadow:0 0 15px var(--crt-amber); }
.go-best { font-size:0.6rem; color:var(--crt-green); letter-spacing:0.1em; margin-top:6px; }
.go-grade { font-size:clamp(1rem,3vw,1.8rem); letter-spacing:0.2em; }
.btn-row { display:flex; gap:16px; flex-wrap:wrap; justify-content:center; }
.arcade-btn {
font-family:'Press Start 2P',monospace;
font-size:clamp(0.45rem,1.2vw,0.6rem);
letter-spacing:0.12em;
padding:12px 24px;
border:3px solid;
background:transparent;
cursor:pointer;
transition:all 0.15s;
text-transform:uppercase;
}
.arcade-btn-green {
border-color:var(--crt-green);
color:var(--crt-green);
text-shadow:0 0 8px var(--crt-green);
}
.arcade-btn-green:hover {
background:rgba(57,255,20,0.15);
box-shadow:0 0 20px rgba(57,255,20,0.4);
transform:scale(1.05);
}
.arcade-btn-amber {
border-color:var(--crt-amber);
color:var(--crt-amber);
text-shadow:0 0 8px var(--crt-amber);
}
.arcade-btn-amber:hover {
background:rgba(255,179,0,0.15);
box-shadow:0 0 20px rgba(255,179,0,0.4);
transform:scale(1.05);
}
/* Controls reminder */
.controls-reminder {
font-family:'Chakra Petch',sans-serif;
font-size:clamp(0.6rem,1.2vw,0.75rem);
color:rgba(57,255,20,0.5);
letter-spacing:0.1em;
text-align:center;
line-height:2;
}
.key-badge {
display:inline-block;
background:rgba(57,255,20,0.1);
border:1px solid rgba(57,255,20,0.4);
padding:2px 8px;
color:var(--crt-green);
font-family:'Press Start 2P',monospace;
font-size:0.5rem;
}
/* Score history */
.score-history {
display:flex; flex-direction:column; gap:4px; width:100%; padding:0 4px;
}
.score-entry {
font-family:'Chakra Petch',sans-serif;
font-size:0.65rem;
display:flex; justify-content:space-between;
padding:4px 8px;
background:rgba(57,255,20,0.04);
border-left:3px solid rgba(57,255,20,0.3);
color:rgba(57,255,20,0.7);
letter-spacing:0.05em;
animation:slideIn 0.3s ease-out;
}
@keyframes slideIn {
from { transform:translateX(-20px); opacity:0; }
to { transform:translateX(0); opacity:1; }
}
.score-entry .pts { color:var(--crt-amber); font-weight:700; }
/* ===== SPLIT SCREEN ===== */
#screen-game.splitscreen {
flex-direction: column;
padding: 0;
gap: 0;
}
.split-top-bar {
display:flex;
align-items:center;
justify-content:center;
gap: 20px;
padding: 8px 16px;
background: rgba(0,10,2,0.95);
border-bottom: 2px solid rgba(57,255,20,0.2);
flex-shrink:0;
z-index:2;
}
.split-player-hud {
display:flex;
gap:14px;
align-items:center;
}
.split-hud-box {
text-align:center;
padding:4px 12px;
border:1px solid rgba(57,255,20,0.2);
background:rgba(0,20,5,0.8);
}
.split-hud-label { font-size:0.35rem; letter-spacing:0.2em; margin-bottom:2px; }
.split-hud-value { font-size:0.9rem; font-weight:700; }
.split-vs {
font-size:0.9rem;
color:rgba(57,255,20,0.4);
text-shadow:0 0 8px rgba(57,255,20,0.3);
padding:0 10px;
}
.split-arenas {
display:flex;
flex:1;
min-height:0;
overflow:hidden;
width:100%;
}
.split-half {
flex:1;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap:6px;
padding:6px 4px;
position:relative;
min-width:0;
min-height:0;
overflow:hidden;
}
.split-half.p1 { border-right:3px solid rgba(57,255,20,0.25); }
.split-half-label {
font-size:clamp(0.35rem, 0.8vw, 0.5rem);
letter-spacing:0.15em;
padding:3px 10px;
border:1px solid;
flex-shrink:0;
}
.split-half.p1 .split-half-label { color:#00d4ff; border-color:#00d4ff; text-shadow:0 0 8px #00d4ff; }
.split-half.p2 .split-half-label { color:#ff00cc; border-color:#ff00cc; text-shadow:0 0 8px #ff00cc; }
.split-half.p1.active-turn { background:rgba(0,212,255,0.03); }
.split-half.p2.active-turn { background:rgba(255,0,204,0.03); }
/* Split arena: fill half the screen width, minus divider, minus padding; cap at available height */
.split-half .arena {
width: min(
calc(50vw - 24px),
calc(100vh - 120px)
);
height: min(
calc(50vw - 24px),
calc(100vh - 120px)
);
flex-shrink:1;
}
.split-half .action-hint {
font-size:clamp(0.3rem, 0.7vw, 0.48rem);
padding:4px 10px;
min-height:24px;
flex-shrink:0;
}
.turn-arrow {
position:absolute;
top:50%;
transform:translateY(-50%);
font-size:1.5rem;
animation:arrowBounce 0.6s ease-in-out infinite alternate;
pointer-events:none;
z-index:5;
}
.split-half.p1 .turn-arrow { right:-18px; display:none; }
.split-half.p2 .turn-arrow { left:-18px; display:none; }
.split-half.active-turn .turn-arrow { display:block; }
@keyframes arrowBounce {
from { transform:translateY(-50%) scale(1); opacity:0.7; }
to { transform:translateY(-50%) scale(1.2); opacity:1; }
}
.split-phase-bar {
display:flex; gap:4px; align-items:center; flex-shrink:0;
}
.split-phase-bar .phase-step {
font-size:clamp(0.28rem, 0.6vw, 0.38rem);
padding:3px 6px;
}
/* ===== SCOREBOARD ===== */
.sb-table {
display:flex; flex-direction:column; gap:3px;
width:100%;
}
.sb-row {
display:flex; align-items:center; gap:8px;
padding:5px 10px;
background:rgba(57,255,20,0.04);
border-left:3px solid rgba(57,255,20,0.2);
font-family:'Chakra Petch',sans-serif;
font-size:clamp(0.6rem,1.1vw,0.75rem);
animation:slideIn 0.3s ease-out both;
}
.sb-row:nth-child(1) { border-left-color:gold; background:rgba(255,215,0,0.06); }
.sb-row:nth-child(2) { border-left-color:silver; background:rgba(192,192,192,0.05); }
.sb-row:nth-child(3) { border-left-color:#cd7f32; background:rgba(205,127,50,0.05); }
.sb-rank { color:rgba(57,255,20,0.4); min-width:22px; font-size:0.65em; }
.sb-rank.gold { color:gold; }
.sb-rank.silver { color:silver; }
.sb-rank.bronze { color:#cd7f32; }
.sb-name { flex:1; color:var(--crt-green); letter-spacing:0.05em; text-transform:uppercase; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.sb-pts { color:var(--crt-amber); font-weight:700; letter-spacing:0.05em; }
.sb-diff { font-size:0.55em; color:rgba(57,255,20,0.35); }
.sb-empty { color:rgba(57,255,20,0.25); font-family:'Chakra Petch',sans-serif; font-size:0.7rem; text-align:center; padding:12px 0; letter-spacing:0.1em; }
/* ===== CHANGELOG ===== */
.changelog-scroll {
max-height: min(65vh, 420px);
overflow-y: auto;
display:flex; flex-direction:column; gap:14px;
width:100%; max-width:680px;
padding:4px 4px 4px 0;
scrollbar-width:thin;
scrollbar-color: rgba(57,255,20,0.3) transparent;
}
.changelog-scroll::-webkit-scrollbar { width:4px; }
.changelog-scroll::-webkit-scrollbar-thumb { background:rgba(57,255,20,0.3); border-radius:2px; }
.cl-version {
background:rgba(57,255,20,0.03);
border:1px solid rgba(57,255,20,0.15);
border-left:3px solid rgba(57,255,20,0.3);
padding:12px 16px;
position:relative;
}
.cl-version.cl-current {
border-left-color:var(--crt-pink);
border-color:rgba(255,0,204,0.25);
background:rgba(255,0,204,0.04);
}
.cl-ver-badge {
font-size:clamp(0.7rem,1.5vw,0.9rem);
color:var(--crt-amber);
text-shadow:0 0 10px var(--crt-amber);
display:inline-block;
margin-right:10px;
}
.cl-version.cl-current .cl-ver-badge { color:var(--crt-pink); text-shadow:0 0 10px var(--crt-pink); }
.cl-ver-date { display:inline; font-family:'Chakra Petch',sans-serif; font-size:0.65rem; color:rgba(57,255,20,0.35); letter-spacing:0.1em; }
.cl-ver-title {
font-size:clamp(0.42rem,1vw,0.58rem);
color:rgba(57,255,20,0.6);
letter-spacing:0.15em;
margin:5px 0 8px;
}
.cl-list {
list-style:none;
display:flex; flex-direction:column; gap:4px;
padding:0;
}
.cl-list li {
font-family:'Chakra Petch',sans-serif;
font-size:clamp(0.6rem,1.1vw,0.73rem);
padding-left:18px;
position:relative;
line-height:1.5;
color:rgba(255,255,255,0.7);
}
.cl-list li::before { position:absolute; left:0; }
.cl-new::before { content:'✦'; color:var(--crt-green); }
.cl-fix::before { content:'⚙'; color:var(--crt-amber); }
.cl-remove::before { content:'✗'; color:var(--crt-red); }
</style>
</head>
<body class="crt-flicker">
<!-- ===== ATTRACT SCREEN ===== -->
<div class="screen" id="screen-attract">
<div class="dart-icon">🎯</div>
<div class="title-logo">
<div class="title-top">— ARCADE PRESENTS —</div>
<span class="title-main">LAUPAN LUCHCD</span>
<span class="title-main" style="font-size:clamp(1.2rem,3.5vw,2.2rem);color:var(--crt-amber);text-shadow:0 0 15px var(--crt-amber);">PEILTJUS</span>
<div class="title-sub" style="margin-top:10px;">THE ULTIMATE DART MACHINE &nbsp;·&nbsp; <span style="color:var(--crt-pink);text-shadow:0 0 8px var(--crt-pink);">v6.0</span></div>
</div>
<div class="highscore-box">
<div class="hs-label">— HIGH SCORE —</div>
<div class="hs-value" id="hs-display">00000</div>
</div>
<div class="insert-coin">[ DRUK OP SPATIE / KNOP OM TE STARTEN ]</div>
<div style="display:flex;gap:14px;margin-top:6px;">
<button class="arcade-btn arcade-btn-green" style="font-size:0.42rem;padding:7px 16px;" onclick="showScoreboard()">🏆 SCOREBORD</button>
<button class="arcade-btn" style="font-size:0.42rem;padding:7px 16px;border-color:var(--crt-pink);color:var(--crt-pink);" onclick="showChangelog()">📋 CHANGELOG</button>
</div>
</div>
<!-- ===== MENU SCREEN (control choice) ===== -->
<div class="screen hidden" id="screen-menu">
<div class="menu-title">⚙ BESTURING KIEZEN</div>
<div class="menu-question">HOE WIL JE SPELEN?</div>
<div class="menu-choices">
<div class="choice-card" onclick="selectControl('buttons')" id="card-buttons">
<span class="choice-icon">⌨️</span>
<div class="choice-name">KNOPPEN</div>
<div class="choice-desc">Druk <strong>SPATIE</strong> of <strong>KNOP 1</strong><br>om de richtingen te stoppen</div>
<div class="choice-key">SPATIE / BTN1</div>
</div>
<div class="choice-card" onclick="selectControl('joystick')" id="card-joystick">
<span class="choice-icon">🕹️</span>
<div class="choice-name">JOYSTICK</div>
<div class="choice-desc">Druk de <strong>joystick in</strong><br>om de richtingen te stoppen</div>
<div class="choice-key">JS BUTTON</div>
</div>
<div class="choice-card" onclick="selectControl('splitscreen')" id="card-splitscreen">
<span class="choice-icon">👥</span>
<div class="choice-name">SPLIT SCREEN</div>
<div class="choice-desc"><strong>2 spelers</strong> naast elkaar<br>P1: SPATIE &nbsp;|&nbsp; P2: ENTER</div>
<div class="choice-key">2 PLAYERS</div>
</div>
</div>
<div class="controls-reminder">
OF KLIK OP DE KAART MET DE MUIS
</div>
</div>
<!-- ===== SCOREBORD ===== -->
<div class="screen hidden" id="screen-scoreboard">
<div class="menu-title" style="color:var(--crt-amber);text-shadow:0 0 15px var(--crt-amber);margin-bottom:6px;">🏆 SCOREBORD</div>
<div style="display:flex;gap:24px;align-items:flex-start;justify-content:center;flex-wrap:wrap;width:100%;max-width:800px;">
<div style="flex:1;min-width:220px;">
<div style="font-size:0.45rem;letter-spacing:0.2em;color:var(--crt-green);margin-bottom:8px;text-align:center;">— MAKKELIJK —</div>
<div id="sb-easy" class="sb-table"></div>
</div>
<div style="flex:1;min-width:220px;">
<div style="font-size:0.45rem;letter-spacing:0.2em;color:var(--crt-amber);margin-bottom:8px;text-align:center;">— NORMAAL —</div>
<div id="sb-medium" class="sb-table"></div>
</div>
<div style="flex:1;min-width:220px;">
<div style="font-size:0.45rem;letter-spacing:0.2em;color:var(--crt-red);margin-bottom:8px;text-align:center;">— MOEILIJK —</div>
<div id="sb-hard" class="sb-table"></div>
</div>
</div>
<div style="display:flex;gap:12px;margin-top:16px;">
<button class="arcade-btn arcade-btn-green" onclick="goToMenu()">← TERUG</button>
<button class="arcade-btn" style="font-size:0.42rem;padding:7px 14px;border-color:var(--crt-red);color:var(--crt-red);" onclick="clearScoreboard()">🗑 WISSEN</button>
</div>
</div>
<!-- ===== CHANGELOG ===== -->
<div class="screen hidden" id="screen-changelog">
<div class="menu-title" style="color:var(--crt-pink);text-shadow:0 0 15px var(--crt-pink);margin-bottom:4px;">📋 UPDATE LOG</div>
<div class="changelog-scroll">
<div class="cl-version cl-current">
<div class="cl-ver-badge">v6.0</div>
<div class="cl-ver-date">2026</div>
<div class="cl-ver-title">THE SCOREBOARD UPDATE</div>
<ul class="cl-list">
<li class="cl-new">Persistent scorebord per moeilijkheid (top 10)</li>
<li class="cl-new">Naaminvoer na elke game</li>
<li class="cl-new">Update log / changelog scherm</li>
<li class="cl-new">Versie badge op attract screen</li>
<li class="cl-fix">Scorebord opgeslagen in localStorage</li>
</ul>
</div>
<div class="cl-version">
<div class="cl-ver-badge">v5.0</div>
<div class="cl-ver-date">2026</div>
<div class="cl-ver-title">THE KIOSK UPDATE</div>
<ul class="cl-list">
<li class="cl-new">Volledig scherm / kiosk modus</li>
<li class="cl-new">Opstartscherm met tap-to-fullscreen</li>
<li class="cl-new">Automatisch landscape lock op mobiel</li>
<li class="cl-fix">Rechtsklik uitgeschakeld</li>
<li class="cl-fix">Bord past nu altijd op het scherm</li>
</ul>
</div>
<div class="cl-version">
<div class="cl-ver-badge">v4.0</div>
<div class="cl-ver-date">2026</div>
<div class="cl-ver-title">THE SPLIT SCREEN UPDATE</div>
<ul class="cl-list">
<li class="cl-new">2-speler split screen modus</li>
<li class="cl-new">P1: SPATIE &nbsp;·&nbsp; P2: ENTER</li>
<li class="cl-new">Per-speler scorebalk bovenaan</li>
<li class="cl-new">Beurt-wisseling systeem</li>
<li class="cl-fix">Popup toont nu alleen +20 ipv 20 +20</li>
</ul>
</div>
<div class="cl-version">
<div class="cl-ver-badge">v3.0</div>
<div class="cl-ver-date">2026</div>
<div class="cl-ver-title">THE ANIMATION UPDATE</div>
<ul class="cl-list">
<li class="cl-new">POV dart animatie vanuit de hand</li>
<li class="cl-new">Darts blijven in het bord zitten</li>
<li class="cl-new">Wobble animatie bij inslag</li>
<li class="cl-new">SVG pijltjes in de HUD</li>
<li class="cl-new">Impact ringen bij treffer</li>
<li class="cl-fix">Dart tip landt nu exact op raakpunt</li>
</ul>
</div>
<div class="cl-version">
<div class="cl-ver-badge">v2.0</div>
<div class="cl-ver-date">2026</div>
<div class="cl-ver-title">THE TIMING UPDATE</div>
<ul class="cl-list">
<li class="cl-new">Timing mechaniek: eerst X-as, dan Y-as</li>
<li class="cl-new">Menu: knoppen vs joystick vs split screen</li>
<li class="cl-new">3 moeilijkheidsgraden</li>
<li class="cl-new">Joystick / gamepad API support</li>
<li class="cl-new">Fase-indicator balk</li>
<li class="cl-new">Arcade naam: Laupan Luchcd Peiltjus</li>
</ul>
</div>
<div class="cl-version">
<div class="cl-ver-badge">v1.0</div>
<div class="cl-ver-date">2026</div>
<div class="cl-ver-title">INITIAL RELEASE</div>
<ul class="cl-list">
<li class="cl-new">Bewegend dartbord</li>
<li class="cl-new">Klik om te gooien</li>
<li class="cl-new">Streak multiplier systeem</li>
<li class="cl-new">CRT scanlines effect</li>
<li class="cl-new">Arcade stijl UI</li>
</ul>
</div>
</div>
<button class="arcade-btn arcade-btn-green" style="margin-top:14px;" onclick="goToMenu()">← TERUG</button>
</div>
<!-- ===== NAME ENTRY (shown after game over) ===== -->
<div id="name-entry-overlay" style="display:none;position:fixed;inset:0;z-index:200;background:rgba(0,0,0,0.92);align-items:center;justify-content:center;flex-direction:column;gap:18px;">
<div style="font-size:clamp(0.6rem,1.8vw,1rem);color:var(--crt-amber);text-shadow:0 0 15px var(--crt-amber);letter-spacing:0.2em;">🏆 NIEUWE SCORE!</div>
<div style="font-size:clamp(1.5rem,4vw,2.5rem);color:var(--crt-green);text-shadow:0 0 15px var(--crt-green);" id="ne-score-disp">0</div>
<div style="font-size:clamp(0.45rem,1.2vw,0.65rem);color:rgba(57,255,20,0.7);letter-spacing:0.15em;">VOER JE NAAM IN (max 10 tekens)</div>
<input id="name-input" maxlength="10" autocomplete="off" spellcheck="false"
style="background:transparent;border:none;border-bottom:2px solid var(--crt-green);color:var(--crt-green);font-family:'Press Start 2P',monospace;font-size:clamp(1rem,2.5vw,1.6rem);text-align:center;outline:none;letter-spacing:0.2em;width:280px;padding:6px 0;text-transform:uppercase;text-shadow:0 0 10px var(--crt-green);">
<div style="font-size:0.42rem;color:rgba(57,255,20,0.4);letter-spacing:0.15em;">DRUK ENTER OM OP TE SLAAN</div>
<button class="arcade-btn arcade-btn-amber" onclick="submitName()" style="margin-top:4px;">✓ OPSLAAN</button>
</div>
<div class="screen hidden" id="screen-difficulty">
<div class="menu-title" style="color:var(--crt-amber);text-shadow:0 0 15px var(--crt-amber);">MOEILIJKHEID</div>
<div class="diff-cards">
<div class="diff-card" onclick="selectDiff('easy')">
<div class="diff-stars">⭐</div>
<div class="diff-name diff-easy">MAKKELIJK</div>
<div class="diff-info">Langzame lijn<br>3 pijlen<br>Goede timing</div>
</div>
<div class="diff-card" onclick="selectDiff('medium')">
<div class="diff-stars">⭐⭐</div>
<div class="diff-name diff-medium">NORMAAL</div>
<div class="diff-info">Gemiddelde snelheid<br>3 pijlen<br>Let op!</div>
</div>
<div class="diff-card" onclick="selectDiff('hard')">
<div class="diff-stars">⭐⭐⭐</div>
<div class="diff-name diff-hard">MOEILIJK</div>
<div class="diff-info">Hoge snelheid<br>3 pijlen<br>Enkel voor pros</div>
</div>
</div>
</div>
<!-- ===== GAME SCREEN ===== -->
<div class="screen hidden" id="screen-game">
<!-- Left panel (single player only) -->
<div class="side-panel left" id="left-panel">
<div class="panel-box">
<div class="panel-label">SCORE</div>
<div class="panel-value" id="score-disp">0</div>
</div>
<div class="panel-box">
<div class="panel-label">PIJLEN</div>
<div class="darts-display" id="darts-pips"></div>
</div>
<div class="panel-box">
<div class="panel-label">STREAK</div>
<div class="panel-value" id="streak-disp" style="color:var(--crt-amber);text-shadow:0 0 10px var(--crt-amber);">0</div>
<div class="streak-fire" id="streak-fire" style="display:none;">🔥</div>
</div>
<div class="mult-badge" id="mult-badge" style="display:none;">×1</div>
</div>
<!-- Center arena (single player) -->
<div class="arena-wrap" id="single-arena-wrap">
<div class="phase-bar">
<div class="phase-step active" id="phase-x">① X-AS</div>
<div class="phase-arrow">▶</div>
<div class="phase-step" id="phase-y">② Y-AS</div>
<div class="phase-arrow">▶</div>
<div class="phase-step" id="phase-throw">③ GOOIEN</div>
</div>
<div class="arena" id="arena">
<canvas id="dartboard" width="420" height="420"></canvas>
<div class="h-line" id="h-line" style="top:50%"></div>
<div class="v-line" id="v-line" style="left:50%;display:none"></div>
<div class="crosshair-dot" id="crosshair-dot"></div>
<canvas id="dart-canvas"></canvas>
</div>
<div class="action-hint" id="action-hint">WACHT...</div>
</div>
<!-- Right panel: score history (single player) -->
<div class="side-panel right" id="right-panel">
<div class="panel-label" style="color:var(--crt-green);">LAATSTE WORPEN</div>
<div class="score-history" id="score-history"></div>
<div style="margin-top:auto;width:100%;">
<div class="panel-box">
<div class="panel-label">BEST</div>
<div class="panel-value" id="best-disp" style="font-size:1rem;color:var(--crt-pink);text-shadow:0 0 10px var(--crt-pink);">0</div>
</div>
</div>
</div>
<!-- ===== SPLIT SCREEN LAYOUT ===== -->
<!-- Top HUD bar (split only) -->
<div class="split-top-bar" id="split-top-bar" style="display:none;width:100%;">
<div class="split-player-hud">
<div class="split-hud-box">
<div class="split-hud-label" style="color:#00d4ff;">P1 SCORE</div>
<div class="split-hud-value" id="p1-score-disp" style="color:#00d4ff;text-shadow:0 0 8px #00d4ff;">0</div>
</div>
<div class="split-hud-box">
<div class="split-hud-label" style="color:#00d4ff;">PIJLEN</div>
<div class="darts-display" id="p1-darts-pips" style="gap:4px;"></div>
</div>
</div>
<div class="split-vs">⚡ VS ⚡</div>
<div class="split-player-hud">
<div class="split-hud-box">
<div class="split-hud-label" style="color:#ff00cc;">P2 SCORE</div>
<div class="split-hud-value" id="p2-score-disp" style="color:#ff00cc;text-shadow:0 0 8px #ff00cc;">0</div>
</div>
<div class="split-hud-box">
<div class="split-hud-label" style="color:#ff00cc;">PIJLEN</div>
<div class="darts-display" id="p2-darts-pips" style="gap:4px;"></div>
</div>
</div>
</div>
<!-- Split arenas (split only) -->
<div class="split-arenas" id="split-arenas" style="display:none;width:100%;">
<!-- Player 1 half -->
<div class="split-half p1 active-turn" id="split-p1">
<div class="turn-arrow">◀</div>
<div class="split-half-label">⌨ SPELER 1 [SPATIE]</div>
<div class="split-phase-bar">
<div class="phase-step active" id="p1-phase-x">① X-AS</div>
<div class="phase-arrow" style="font-size:0.4rem;">▶</div>
<div class="phase-step" id="p1-phase-y">② Y-AS</div>
<div class="phase-arrow" style="font-size:0.4rem;">▶</div>
<div class="phase-step" id="p1-phase-throw">③ GOOIEN</div>
</div>
<div class="arena" id="arena-p1">
<canvas id="dartboard-p1" width="420" height="420"></canvas>
<div class="h-line" id="h-line-p1" style="top:50%;background:linear-gradient(90deg,transparent,#00d4ff,transparent);box-shadow:0 0 8px #00d4ff;"></div>
<div class="v-line" id="v-line-p1" style="left:50%;display:none;background:linear-gradient(180deg,transparent,#00d4ff,transparent);box-shadow:0 0 8px #00d4ff;"></div>
<div class="crosshair-dot" id="crosshair-dot-p1" style="border-color:#00d4ff;box-shadow:0 0 10px #00d4ff;"></div>
<canvas id="dart-canvas-p1"></canvas>
</div>
<div class="action-hint" id="action-hint-p1" style="border-color:rgba(0,212,255,0.4);color:#00d4ff;">WACHT...</div>
</div>
<!-- Player 2 half -->
<div class="split-half p2" id="split-p2">
<div class="turn-arrow">▶</div>
<div class="split-half-label">⌨ SPELER 2 [ENTER]</div>
<div class="split-phase-bar">
<div class="phase-step active" id="p2-phase-x">① X-AS</div>
<div class="phase-arrow" style="font-size:0.4rem;">▶</div>
<div class="phase-step" id="p2-phase-y">② Y-AS</div>
<div class="phase-arrow" style="font-size:0.4rem;">▶</div>
<div class="phase-step" id="p2-phase-throw">③ GOOIEN</div>
</div>
<div class="arena" id="arena-p2">
<canvas id="dartboard-p2" width="420" height="420"></canvas>
<div class="h-line" id="h-line-p2" style="top:50%;background:linear-gradient(90deg,transparent,#ff00cc,transparent);box-shadow:0 0 8px #ff00cc;"></div>
<div class="v-line" id="v-line-p2" style="left:50%;display:none;background:linear-gradient(180deg,transparent,#ff00cc,transparent);box-shadow:0 0 8px #ff00cc;"></div>
<div class="crosshair-dot" id="crosshair-dot-p2" style="border-color:#ff00cc;box-shadow:0 0 10px #ff00cc;"></div>
<canvas id="dart-canvas-p2"></canvas>
</div>
<div class="action-hint" id="action-hint-p2" style="border-color:rgba(255,0,204,0.4);color:#ff00cc;">WACHT...</div>
</div>
</div>
</div>
<!-- ===== GAME OVER SCREEN ===== -->
<div class="screen hidden" id="screen-gameover">
<div class="go-title" id="go-title">GAME OVER</div>
<div id="go-single-wrap">
<div class="go-score-wrap">
<div class="go-label">— JOUW SCORE —</div>
<div class="go-score" id="go-score-val">0</div>
<div class="go-best" id="go-best-val"></div>
</div>
<div class="go-grade" id="go-grade"></div>
</div>
<div id="go-split-wrap" style="display:none;gap:40px;flex-wrap:wrap;justify-content:center;">
<div class="go-score-wrap" style="text-align:center;">
<div class="go-label" style="color:#00d4ff;">— SPELER 1 —</div>
<div class="go-score" id="go-p1-score" style="color:#00d4ff;text-shadow:0 0 15px #00d4ff;">0</div>
<div class="go-grade" id="go-p1-grade"></div>
</div>
<div style="font-size:2rem;color:rgba(57,255,20,0.4);align-self:center;">VS</div>
<div class="go-score-wrap" style="text-align:center;">
<div class="go-label" style="color:#ff00cc;">— SPELER 2 —</div>
<div class="go-score" id="go-p2-score" style="color:#ff00cc;text-shadow:0 0 15px #ff00cc;">0</div>
<div class="go-grade" id="go-p2-grade"></div>
</div>
</div>
<div class="go-best" id="go-winner" style="font-size:0.8rem;letter-spacing:0.15em;margin-top:4px;"></div>
<div class="btn-row">
<button class="arcade-btn arcade-btn-green" onclick="goToMenu()">🕹 MENU</button>
<button class="arcade-btn arcade-btn-amber" onclick="restartGame()">↺ OPNIEUW</button>
<button class="arcade-btn" style="font-size:0.42rem;padding:7px 14px;border-color:var(--crt-pink);color:var(--crt-pink);" onclick="showScoreboard()">🏆 SCOREBORD</button>
</div>
<div class="controls-reminder">
SPATIE = OPNIEUW &nbsp;|&nbsp; ESC = MENU
</div>
</div>
<script>
// ============================================================
// VERSION
// ============================================================
const VERSION = '6.0';
// ============================================================
// SCOREBOARD (persistent via localStorage)
// ============================================================
const SB_KEY = 'peiltjus_scoreboard_v1';
function loadScoreboard() {
try { return JSON.parse(localStorage.getItem(SB_KEY)) || {easy:[],medium:[],hard:[]}; }
catch(e) { return {easy:[],medium:[],hard:[]}; }
}
function saveScoreboard(sb) {
localStorage.setItem(SB_KEY, JSON.stringify(sb));
}
function addScoreToBoard(name, pts, diff) {
const sb = loadScoreboard();
const list = sb[diff] || [];
list.push({ name: name.toUpperCase().slice(0,10)||'AAA', pts, date: new Date().toLocaleDateString('nl') });
list.sort((a,b) => b.pts - a.pts);
sb[diff] = list.slice(0, 10);
saveScoreboard(sb);
}
function renderScoreboard() {
const sb = loadScoreboard();
const diffs = ['easy','medium','hard'];
const medals = ['🥇','🥈','🥉'];
diffs.forEach(d => {
const el = document.getElementById('sb-'+d);
const list = sb[d] || [];
if (!list.length) {
el.innerHTML = '<div class="sb-empty">GEEN SCORES NOG</div>';
return;
}
el.innerHTML = list.map((e,i) => {
const rankClass = i<3 ? ['gold','silver','bronze'][i] : '';
const medal = i<3 ? medals[i] : `#${i+1}`;
return `<div class="sb-row" style="animation-delay:${i*0.04}s">
<span class="sb-rank ${rankClass}">${medal}</span>
<span class="sb-name">${e.name}</span>
<span class="sb-pts">${e.pts}</span>
<span class="sb-diff">${e.date||''}</span>
</div>`;
}).join('');
});
}
function showScoreboard() {
renderScoreboard();
showScreen('scoreboard');
}
function showChangelog() { showScreen('changelog'); }
function clearScoreboard() {
if (confirm('Scorebord wissen?')) {
saveScoreboard({easy:[],medium:[],hard:[]});
renderScoreboard();
}
}
// ── Name entry overlay ────────────────────────────────────────
let _pendingScore = 0, _pendingDiff = 'medium', _pendingCallback = null;
function promptName(pts, diff, callback) {
_pendingScore = pts;
_pendingDiff = diff;
_pendingCallback = callback;
document.getElementById('ne-score-disp').textContent = pts;
document.getElementById('name-input').value = '';
const overlay = document.getElementById('name-entry-overlay');
overlay.style.display = 'flex';
setTimeout(() => document.getElementById('name-input').focus(), 50);
}
function submitName() {
const name = (document.getElementById('name-input').value || 'AAA').toUpperCase();
document.getElementById('name-entry-overlay').style.display = 'none';
addScoreToBoard(name, _pendingScore, _pendingDiff);
if (_pendingCallback) _pendingCallback();
}
document.addEventListener('keydown', e => {
if (e.code === 'Enter' && document.getElementById('name-entry-overlay').style.display === 'flex') {
e.preventDefault(); submitName();
}
});
let currentScreen = 'attract';
let controlMode = 'buttons';
let difficulty = 'medium';
let highScore = 0;
let isSplit = false;
// Single-player state
let score=0, dartsLeft=3, streak=0, mult=1;
let phase='x', lineXPos=50, lineYPos=50;
let lineXDir=1, lineYDir=1, lineXSpeed=1, lineYSpeed=0.8;
let animFrame=null, lastTime=0;
let gamepads={}, jsButtonWasDown=false;
// Split-screen state — per player
const P = [
{ id:0, score:0, darts:3, streak:0, mult:1,
phase:'x', lxp:50, lyp:50, lxd:1, lyd:1, lxs:1, lys:0.8,
stuckDarts:[], color:'#00d4ff', key:'SPATIE', name:'SPELER 1' },
{ id:1, score:0, darts:3, streak:0, mult:1,
phase:'x', lxp:50, lyp:50, lxd:1, lyd:1, lxs:1, lys:0.8,
stuckDarts:[], color:'#ff00cc', key:'ENTER', name:'SPELER 2' }
];
let activeSplitPlayer = 0; // whose turn it is (0 or 1)
let splitAnimFrame=null, splitLastTime=0;
const diffSettings = {
easy: { speed:0.6, darts:3 },
medium: { speed:1.1, darts:3 },
hard: { speed:1.9, darts:3 },
};
const SEG_VALUES=[20,1,18,4,13,6,10,15,2,17,3,19,7,16,8,11,14,9,12,5];
// ============================================================
// SCREEN MANAGEMENT
// ============================================================
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s=>s.classList.add('hidden'));
document.getElementById('screen-'+id).classList.remove('hidden');
currentScreen = id;
}
function initAttract() {
document.getElementById('hs-display').textContent = String(highScore).padStart(5,'0');
showScreen('attract');
}
function openMenu() { showScreen('menu'); }
function selectControl(mode) {
controlMode = mode;
isSplit = mode === 'splitscreen';
['buttons','joystick','splitscreen'].forEach(x=>{
const c=document.getElementById('card-'+x);
if(c) c.classList.toggle('selected', x===mode);
});
setTimeout(()=>showScreen('difficulty'), 220);
}
function selectDiff(d) {
difficulty = d;
if(isSplit) startSplitGame();
else startGame();
}
// ============================================================
// DRAW BOARD (works for any canvas id)
// ============================================================
function drawBoardOn(canvasId, arenaId) {
const canvas=document.getElementById(canvasId);
if(!canvas) return;
const ctx=canvas.getContext('2d');
const SIZE=canvas.width, r=SIZE*0.43, cx=SIZE/2, cy=SIZE/2;
const seg=20, aps=(Math.PI*2)/seg, off=-Math.PI/2-aps/2;
ctx.clearRect(0,0,SIZE,SIZE);
ctx.beginPath(); ctx.arc(cx,cy,r+16,0,Math.PI*2);
ctx.fillStyle='#0d1a0d'; ctx.fill();
ctx.strokeStyle='rgba(57,255,20,0.5)'; ctx.lineWidth=3; ctx.stroke();
for(let i=0;i<seg;i++){
const a1=off+i*aps, a2=a1+aps, dark=i%2===0;
const cDark='#141a14', cLight='#c8b870';
const cHit=dark?'#8b1a1a':'#1a6b1a', cHit2=dark?'#661111':'#115511';
ctx.beginPath(); ctx.moveTo(cx,cy); ctx.arc(cx,cy,r*0.62,a1,a2); ctx.closePath(); ctx.fillStyle=dark?cDark:cLight; ctx.fill();
ctx.beginPath(); ctx.arc(cx,cy,r*0.68,a1,a2); ctx.arc(cx,cy,r*0.62,a2,a1,true); ctx.closePath(); ctx.fillStyle=cHit; ctx.fill();
ctx.beginPath(); ctx.arc(cx,cy,r*0.88,a1,a2); ctx.arc(cx,cy,r*0.68,a2,a1,true); ctx.closePath(); ctx.fillStyle=dark?cDark:cLight; ctx.fill();
ctx.beginPath(); ctx.arc(cx,cy,r*0.95,a1,a2); ctx.arc(cx,cy,r*0.88,a2,a1,true); ctx.closePath(); ctx.fillStyle=cHit2; ctx.fill();
const ma=a1+aps/2, nr=r*1.05;
ctx.save(); ctx.translate(cx+Math.cos(ma)*nr, cy+Math.sin(ma)*nr); ctx.rotate(ma+Math.PI/2);
ctx.font=`bold ${Math.round(r*0.08)}px 'Press Start 2P',monospace`;
ctx.fillStyle='#fff'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.shadowColor='rgba(0,0,0,0.9)'; ctx.shadowBlur=4; ctx.fillText(SEG_VALUES[i],0,0); ctx.restore();
}
for(let i=0;i<seg;i++){
const a=off+i*aps;
ctx.beginPath(); ctx.moveTo(cx+Math.cos(a)*r*0.11, cy+Math.sin(a)*r*0.11);
ctx.lineTo(cx+Math.cos(a)*r*0.95, cy+Math.sin(a)*r*0.95);
ctx.strokeStyle='rgba(0,0,0,0.7)'; ctx.lineWidth=1.5; ctx.stroke();
}
[0.95,0.88,0.68,0.62,0.11].forEach(f=>{
ctx.beginPath(); ctx.arc(cx,cy,r*f,0,Math.PI*2);
ctx.strokeStyle='rgba(100,80,30,0.9)'; ctx.lineWidth=1.5; ctx.stroke();
});
ctx.beginPath(); ctx.arc(cx,cy,r*0.11,0,Math.PI*2); ctx.fillStyle='#1d7a1d'; ctx.fill();
ctx.beginPath(); ctx.arc(cx,cy,r*0.05,0,Math.PI*2);
const bg=ctx.createRadialGradient(cx-2,cy-2,0,cx,cy,r*0.05);
bg.addColorStop(0,'#ff8800'); bg.addColorStop(1,'#cc0000');
ctx.fillStyle=bg; ctx.fill(); ctx.strokeStyle='rgba(255,255,255,0.7)'; ctx.lineWidth=1.5; ctx.stroke();
if(arenaId){
const arena=document.getElementById(arenaId);
const aw=arena.clientWidth, ah=arena.clientHeight;
canvas.style.left=(aw/2-canvas.offsetWidth/2)+'px';
canvas.style.top =(ah/2-canvas.offsetHeight/2)+'px';
}
}
function drawBoard(){ drawBoardOn('dartboard','arena'); }
// ============================================================
// SINGLE PLAYER
// ============================================================
const stuckDarts=[];
function clearStuckDarts(){
stuckDarts.forEach(d=>d.el&&d.el.remove());
stuckDarts.length=0;
}
function startGame() {
isSplit=false;
const s=diffSettings[difficulty];
score=0; streak=0; mult=1; dartsLeft=s.darts;
phase='x'; lineXPos=50; lineYPos=Math.random()*70+15;
lineXDir=Math.random()>0.5?1:-1; lineYDir=Math.random()>0.5?1:-1;
lineXSpeed=s.speed*(0.8+Math.random()*0.4);
lineYSpeed=s.speed*(0.8+Math.random()*0.4);
jsButtonWasDown=false;
// Show single layout
document.getElementById('left-panel').style.display='';
document.getElementById('right-panel').style.display='';
document.getElementById('single-arena-wrap').style.display='';
document.getElementById('split-top-bar').style.display='none';
document.getElementById('split-arenas').style.display='none';
document.getElementById('screen-game').classList.remove('splitscreen');
showScreen('game');
drawBoard();
requestAnimationFrame(()=>{
const a=document.getElementById('arena');
const oc=document.getElementById('dart-canvas');
oc.width=a.clientWidth; oc.height=a.clientHeight;
});
updateHUD(); updatePhaseUI(); updateDartPips();
document.getElementById('score-history').innerHTML='';
clearStuckDarts();
document.getElementById('best-disp').textContent=highScore;
document.getElementById('h-line').style.top=lineYPos+'%';
document.getElementById('h-line').style.display='block';
document.getElementById('v-line').style.display='none';
document.getElementById('crosshair-dot').style.display='none';
cancelAnimationFrame(animFrame);
lastTime=0;
animFrame=requestAnimationFrame(gameLoop);
setHint();
}
function gameLoop(ts) {
if(currentScreen!=='game'||isSplit) return;
const dt=lastTime?Math.min((ts-lastTime)/16.67,3):1;
lastTime=ts;
if(controlMode==='joystick') pollGamepad();
if(phase==='x'){
lineYPos+=lineYDir*lineYSpeed*dt;
if(lineYPos<=3||lineYPos>=97){lineYDir*=-1;lineYPos=Math.max(3,Math.min(97,lineYPos));}
document.getElementById('h-line').style.top=lineYPos+'%';
} else if(phase==='y'){
lineXPos+=lineXDir*lineXSpeed*dt;
if(lineXPos<=3||lineXPos>=97){lineXDir*=-1;lineXPos=Math.max(3,Math.min(97,lineXPos));}
document.getElementById('v-line').style.left=lineXPos+'%';
const ch=document.getElementById('crosshair-dot');
ch.style.left=lineXPos+'%'; ch.style.top=lineYPos+'%';
}
animFrame=requestAnimationFrame(gameLoop);
}
function pollGamepad() {
const gps=navigator.getGamepads?navigator.getGamepads():[];
for(const gp of gps){
if(!gp) continue;
const btnDown=gp.buttons[0]?.pressed||gp.buttons[1]?.pressed||gp.buttons[2]?.pressed||gp.buttons[3]?.pressed;
if(btnDown&&!jsButtonWasDown) fireAction('p1');
jsButtonWasDown=btnDown;
}
}
function fireAction(player='p1') {
if(currentScreen==='attract'){openMenu();return;}
if(currentScreen==='gameover'){restartGame();return;}
if(currentScreen!=='game') return;
if(isSplit){
fireSplitAction(player);
return;
}
if(phase==='x'){
phase='y';
document.getElementById('v-line').style.display='block';
document.getElementById('v-line').style.left=lineXPos+'%';
document.getElementById('crosshair-dot').style.display='block';
document.getElementById('crosshair-dot').style.left=lineXPos+'%';
document.getElementById('crosshair-dot').style.top=lineYPos+'%';
lineYSpeed*=1.15; updatePhaseUI(); setHint();
} else if(phase==='y'){
phase='locked'; updatePhaseUI(); throwDart();
}
}
// ============================================================
// SPLIT SCREEN
// ============================================================
function startSplitGame() {
isSplit=true;
const s=diffSettings[difficulty];
// Reset both players
P.forEach((p,i)=>{
p.score=0; p.darts=s.darts; p.streak=0; p.mult=1;
p.phase='x'; p.lxp=50; p.lyp=Math.random()*70+15;
p.lxd=Math.random()>0.5?1:-1; p.lyd=Math.random()>0.5?1:-1;
p.lxs=s.speed*(0.8+Math.random()*0.4);
p.lys=s.speed*(0.8+Math.random()*0.4);
p.stuckDarts.forEach(d=>d.el&&d.el.remove());
p.stuckDarts.length=0;
});
activeSplitPlayer=0;
// Show split layout
document.getElementById('left-panel').style.display='none';
document.getElementById('right-panel').style.display='none';
document.getElementById('single-arena-wrap').style.display='none';
document.getElementById('split-top-bar').style.display='flex';
document.getElementById('split-arenas').style.display='flex';
document.getElementById('screen-game').classList.add('splitscreen');
showScreen('game');
requestAnimationFrame(()=>{
drawBoardOn('dartboard-p1','arena-p1');
drawBoardOn('dartboard-p2','arena-p2');
['p1','p2'].forEach(pid=>{
const a=document.getElementById('arena-'+pid);
const oc=document.getElementById('dart-canvas-'+pid);
oc.width=a.clientWidth; oc.height=a.clientHeight;
});
resetSplitLines('p1');
resetSplitLines('p2');
updateSplitHUD();
setActiveSplitPlayer(0);
});
cancelAnimationFrame(splitAnimFrame);
splitLastTime=0;
splitAnimFrame=requestAnimationFrame(splitGameLoop);
}
function resetSplitLines(pid) {
const p=pid==='p1'?P[0]:P[1];
document.getElementById('h-line-'+pid).style.top=p.lyp+'%';
document.getElementById('h-line-'+pid).style.display='block';
document.getElementById('v-line-'+pid).style.display='none';
document.getElementById('crosshair-dot-'+pid).style.display='none';
}
function setActiveSplitPlayer(idx) {
activeSplitPlayer=idx;
document.getElementById('split-p1').classList.toggle('active-turn',idx===0);
document.getElementById('split-p2').classList.toggle('active-turn',idx===1);
updateSplitPhaseUI('p1');
updateSplitPhaseUI('p2');
setSplitHint('p1');
setSplitHint('p2');
}
function splitGameLoop(ts) {
if(currentScreen!=='game'||!isSplit) return;
const dt=splitLastTime?Math.min((ts-splitLastTime)/16.67,3):1;
splitLastTime=ts;
const ap=activeSplitPlayer;
const pid=ap===0?'p1':'p2';
const p=P[ap];
if(p.phase==='x'){
p.lyp+=p.lyd*p.lys*dt;
if(p.lyp<=3||p.lyp>=97){p.lyd*=-1;p.lyp=Math.max(3,Math.min(97,p.lyp));}
document.getElementById('h-line-'+pid).style.top=p.lyp+'%';
} else if(p.phase==='y'){
p.lxp+=p.lxd*p.lxs*dt;
if(p.lxp<=3||p.lxp>=97){p.lxd*=-1;p.lxp=Math.max(3,Math.min(97,p.lxp));}
document.getElementById('v-line-'+pid).style.left=p.lxp+'%';
const ch=document.getElementById('crosshair-dot-'+pid);
ch.style.left=p.lxp+'%'; ch.style.top=p.lyp+'%';
}
splitAnimFrame=requestAnimationFrame(splitGameLoop);
}
function fireSplitAction(player) {
const idx=player==='p1'?0:1;
if(idx!==activeSplitPlayer) return; // not your turn
const p=P[idx];
const pid=player;
if(p.phase==='x'){
p.phase='y';
document.getElementById('v-line-'+pid).style.display='block';
document.getElementById('v-line-'+pid).style.left=p.lxp+'%';
document.getElementById('crosshair-dot-'+pid).style.display='block';
document.getElementById('crosshair-dot-'+pid).style.left=p.lxp+'%';
document.getElementById('crosshair-dot-'+pid).style.top=p.lyp+'%';
p.lys*=1.15;
updateSplitPhaseUI(pid); setSplitHint(pid);
} else if(p.phase==='y'){
p.phase='locked';
updateSplitPhaseUI(pid);
throwSplitDart(idx, pid);
}
}
function throwSplitDart(idx, pid) {
const p=P[idx];
const arenaEl=document.getElementById('arena-'+pid);
const aw=arenaEl.clientWidth, ah=arenaEl.clientHeight;
const tx=p.lxp/100*aw, ty=p.lyp/100*ah;
const dbCanvas=document.getElementById('dartboard-'+pid);
const bcx=dbCanvas.offsetLeft+dbCanvas.offsetWidth/2;
const bcy=dbCanvas.offsetTop+dbCanvas.offsetHeight/2;
const scale=dbCanvas.width/dbCanvas.offsetWidth;
const dx=(tx-bcx)*scale, dy=(ty-bcy)*scale;
const zone=getZone(dx,dy);
const hit=zone.pts>0;
// Animate
const oc=document.getElementById('dart-canvas-'+pid);
oc.width=aw; oc.height=ah;
const octx=oc.getContext('2d');
const startX=aw*0.52, startY=ah*1.05;
let t=0; const DUR=38;
function easeOutQuart(x){return 1-Math.pow(1-x,4);}
let flyRAF;
function flyStep(){
if(t>DUR){octx.clearRect(0,0,aw,ah);landSplitDart(idx,pid,tx,ty,hit,zone,arenaEl);return;}
const ep=easeOutQuart(t/DUR);
const midX=lerp(startX,tx,0.4), midY=lerp(startY,ty,0.3);
const bx=lerp(lerp(startX,midX,ep),lerp(midX,tx,ep),ep);
const by=lerp(lerp(startY,midY,ep),lerp(midY,ty,ep),ep);
const sc=lerp(1.0,0.12,ep);
const alpha=t/DUR<0.85?1:lerp(1,0,(t/DUR-0.85)/0.15);
octx.clearRect(0,0,aw,ah);
drawPovDartOn(octx,bx,by,tx,ty,sc,alpha,p.color);
t++; flyRAF=requestAnimationFrame(flyStep);
}
flyRAF=requestAnimationFrame(flyStep);
}
function landSplitDart(idx,pid,ax,ay,hit,zone,arenaEl){
const p=P[idx];
if(hit){p.streak++;} else{p.streak=0;}
p.mult=p.streak>=6?3:p.streak>=3?2:1;
const pts=zone.pts*p.mult;
// Stuck dart
const m=document.createElement('div');
m.className='dart-marker';
m.style.left=ax+'px'; m.style.top=ay+'px';
const tilt=(Math.random()-0.5)*12;
m.style.setProperty('--tilt',tilt+'deg');
m.style.animation='dartWobble 0.45s cubic-bezier(0.2,0.8,0.3,1) forwards';
const tipCol=hit?'#d8e4ec':'#ff7777';
const bodyCol=hit?p.color:'#cc2222';
const glowCol=hit?`${p.color}cc`:'rgba(255,50,50,0.85)';
m.innerHTML=makeDartSVGInline(tipCol,bodyCol,glowCol,ax);
arenaEl.appendChild(m);
p.stuckDarts.push({el:m});
// Impact rings
for(let i=0;i<2;i++){
setTimeout(()=>{
const ring=document.createElement('div');
ring.className='impact-ring'+(hit?'':' miss');
ring.style.left=ax+'px'; ring.style.top=ay+'px';
if(hit) ring.style.borderColor=p.color;
arenaEl.appendChild(ring); setTimeout(()=>ring.remove(),550);
},i*100);
}
// Flash
const fl=document.createElement('div');
fl.className='screen-flash';
fl.style.background=hit?`${p.color}18`:'rgba(255,34,68,0.2)';
arenaEl.appendChild(fl); setTimeout(()=>fl.remove(),300);
// Score popup
const pop=document.createElement('div');
pop.className='score-popup';
pop.style.left=ax+'px'; pop.style.top=(ay-14)+'px';
pop.style.color=zone.color;
let lbl=zone.label;
if(hit&&p.mult>1) lbl+=' ×'+p.mult+'!';
pop.textContent=hit?(`+${pts}${lbl?' '+lbl:''}`):lbl||'MISS';
pop.style.fontSize=pts>=100?'0.75rem':pts>=50?'0.6rem':'0.5rem';
arenaEl.appendChild(pop); setTimeout(()=>pop.remove(),1400);
p.score+=pts; p.darts--;
updateSplitHUD();
// Check if both players done
const bothDone=P[0].darts<=0&&P[1].darts<=0;
const thisDone=p.darts<=0;
if(bothDone){setTimeout(endSplitGame,700);return;}
setTimeout(()=>{
// Reset this player's lines for next throw
p.phase='x';
p.lyp=Math.random()*70+15; p.lxp=50;
const s=diffSettings[difficulty];
const spd=s.speed*(1+(p.score)/400);
p.lys=spd*(0.8+Math.random()*0.4);
p.lxs=spd*(0.8+Math.random()*0.4);
document.getElementById('v-line-'+pid).style.display='none';
document.getElementById('crosshair-dot-'+pid).style.display='none';
document.getElementById('h-line-'+pid).style.top=p.lyp+'%';
// Switch to other player if they still have darts
const nextIdx=(idx+1)%2;
if(P[nextIdx].darts>0){
setActiveSplitPlayer(nextIdx);
} else if(!thisDone){
setActiveSplitPlayer(idx); // other player is done, keep going
}
},400);
}
function updateSplitHUD(){
P.forEach((p,i)=>{
const pid='p'+(i+1);
document.getElementById(pid+'-score-disp').textContent=p.score;
const pips=document.getElementById(pid+'-darts-pips');
pips.innerHTML='';
const total=diffSettings[difficulty].darts;
for(let j=0;j<total;j++){
const pip=document.createElement('div');
pip.className='dart-pip'+(j>=p.darts?' used':'');
pip.style.width='18px'; pip.style.height='30px';
pip.innerHTML=makeDartSVG(p.color);
pips.appendChild(pip);
}
});
}
function updateSplitPhaseUI(pid){
const idx=pid==='p1'?0:1;
const p=P[idx];
const isActive=activeSplitPlayer===idx;
['x','y','throw'].forEach(name=>{
const el=document.getElementById(pid+'-phase-'+name);
if(!el) return;
el.className='phase-step';
if(!isActive){el.style.opacity='0.3';return;}
el.style.opacity='1';
if(p.phase==='x'&&name==='x') el.classList.add('active');
else if(p.phase==='y'&&name==='x') el.classList.add('done');
else if(p.phase==='y'&&name==='y') el.classList.add('active');
else if(p.phase==='locked'){el.classList.add('done');}
});
}
function setSplitHint(pid){
const idx=pid==='p1'?0:1;
const p=P[idx];
const isActive=activeSplitPlayer===idx;
const el=document.getElementById('action-hint-'+pid);
if(!isActive){el.textContent='WACHT OP BEURT...';el.style.opacity='0.4';return;}
el.style.opacity='1';
const key=p.key;
if(p.phase==='x') el.textContent=`[${key}] STOP X-AS`;
else if(p.phase==='y') el.textContent=`[${key}] STOP Y-AS`;
else el.textContent='GOOIEN...';
}
function endSplitGame(){
cancelAnimationFrame(splitAnimFrame);
const p1=P[0].score, p2=P[1].score;
document.getElementById('go-single-wrap').style.display='none';
document.getElementById('go-split-wrap').style.display='flex';
document.getElementById('go-p1-score').textContent=p1;
document.getElementById('go-p2-score').textContent=p2;
document.getElementById('go-p1-grade').textContent='RANG: '+getGrade(p1).g;
document.getElementById('go-p1-grade').style.color=getGrade(p1).c;
document.getElementById('go-p2-grade').textContent='RANG: '+getGrade(p2).g;
document.getElementById('go-p2-grade').style.color=getGrade(p2).c;
const winnerEl=document.getElementById('go-winner');
if(p1>p2){ winnerEl.textContent='🏆 SPELER 1 WINT!'; winnerEl.style.color='#00d4ff'; }
else if(p2>p1){ winnerEl.textContent='🏆 SPELER 2 WINT!'; winnerEl.style.color='#ff00cc'; }
else { winnerEl.textContent='🤝 GELIJKSPEL!'; winnerEl.style.color='var(--crt-amber)'; }
document.getElementById('go-title').textContent='GAME OVER';
if(Math.max(p1,p2)>highScore) highScore=Math.max(p1,p2);
// Prompt P1 name entry, then P2
const saveAndShow = () => showScreen('gameover');
if(p2 > 0){
promptName(p2, difficulty, ()=>{
if(p1 > 0) promptName(p1, difficulty, saveAndShow);
else saveAndShow();
});
} else if(p1 > 0){
promptName(p1, difficulty, saveAndShow);
} else {
saveAndShow();
}
}
function getGrade(s){
const grades=[{min:300,g:'S',c:'#ffdd00'},{min:200,g:'A',c:'#00ffcc'},{min:120,g:'B',c:'#39ff14'},{min:60,g:'C',c:'#ffb300'},{min:0,g:'D',c:'#ff4444'}];
return grades.find(x=>s>=x.min);
}
// ============================================================
// THROW DART (single player)
// ============================================================
function throwDart() {
const arena=document.getElementById('arena');
const aw=arena.clientWidth, ah=arena.clientHeight;
const tx=lineXPos/100*aw, ty=lineYPos/100*ah;
const dartboard=document.getElementById('dartboard');
const bcx=dartboard.offsetLeft+dartboard.offsetWidth/2;
const bcy=dartboard.offsetTop+dartboard.offsetHeight/2;
const scale=dartboard.width/dartboard.offsetWidth;
const dx=(tx-bcx)*scale, dy=(ty-bcy)*scale;
const zone=getZone(dx,dy);
const hit=zone.pts>0;
const oc=document.getElementById('dart-canvas');
oc.width=aw; oc.height=ah;
const octx=oc.getContext('2d');
const startX=aw*0.52, startY=ah*1.05;
let t=0; const DUR=38;
function easeOutQuart(x){return 1-Math.pow(1-x,4);}
let flyRAF;
function flyStep(){
if(t>DUR){octx.clearRect(0,0,aw,ah);landDart(tx,ty,hit,zone);return;}
const ep=easeOutQuart(t/DUR);
const midX=lerp(startX,tx,0.4), midY=lerp(startY,ty,0.3);
const bx=lerp(lerp(startX,midX,ep),lerp(midX,tx,ep),ep);
const by=lerp(lerp(startY,midY,ep),lerp(midY,ty,ep),ep);
const sc=lerp(1.0,0.12,ep);
const alpha=t/DUR<0.85?1:lerp(1,0,(t/DUR-0.85)/0.15);
octx.clearRect(0,0,aw,ah);
drawPovDartOn(octx,bx,by,tx,ty,sc,alpha,'#ffaa00');
t++; flyRAF=requestAnimationFrame(flyStep);
}
flyRAF=requestAnimationFrame(flyStep);
function landDart(ax,ay,hit,zone){
if(hit){streak++;} else{streak=0;}
mult=streak>=6?3:streak>=3?2:1;
const pts=zone.pts*mult;
const m=document.createElement('div');
m.className='dart-marker';
m.style.left=ax+'px'; m.style.top=ay+'px';
const tilt=(Math.random()-0.5)*12;
m.style.setProperty('--tilt',tilt+'deg');
m.style.animation='dartWobble 0.45s cubic-bezier(0.2,0.8,0.3,1) forwards';
const tipCol=hit?'#d8e4ec':'#ff7777';
const bodyCol=hit?'#ffaa00':'#cc2222';
const glowCol=hit?'rgba(255,200,50,0.85)':'rgba(255,50,50,0.85)';
m.innerHTML=makeDartSVGInline(tipCol,bodyCol,glowCol,ax);
arena.appendChild(m);
stuckDarts.push({el:m,hit});
for(let i=0;i<2;i++){
setTimeout(()=>{
const ring=document.createElement('div');
ring.className='impact-ring'+(hit?'':' miss');
ring.style.left=ax+'px'; ring.style.top=ay+'px';
arena.appendChild(ring); setTimeout(()=>ring.remove(),550);
},i*100);
}
const fl=document.createElement('div');
fl.className='screen-flash';
fl.style.background=hit?'rgba(57,255,20,0.09)':'rgba(255,34,68,0.2)';
arena.appendChild(fl); setTimeout(()=>fl.remove(),300);
const pop=document.createElement('div');
pop.className='score-popup';
pop.style.left=ax+'px'; pop.style.top=(ay-14)+'px';
pop.style.color=zone.color;
let lbl=zone.label;
if(hit&&mult>1) lbl+=' ×'+mult+'!';
pop.textContent=hit?(`+${pts}${lbl?' '+lbl:''}`):lbl||'MISS';
pop.style.fontSize=pts>=100?'0.75rem':pts>=50?'0.6rem':'0.5rem';
arena.appendChild(pop); setTimeout(()=>pop.remove(),1400);
addScoreEntry(zone.label,pts,hit);
score+=pts; dartsLeft--;
updateHUD(); updateDartPips();
if(dartsLeft<=0){setTimeout(endGame,900);return;}
setTimeout(()=>{
phase='x'; lineYPos=Math.random()*70+15; lineXPos=50;
const s=diffSettings[difficulty];
const spd=s.speed*(1+score/400);
lineYSpeed=spd*(0.8+Math.random()*0.4);
lineXSpeed=spd*(0.8+Math.random()*0.4);
document.getElementById('v-line').style.display='none';
document.getElementById('crosshair-dot').style.display='none';
document.getElementById('h-line').style.top=lineYPos+'%';
updatePhaseUI(); setHint();
},400);
}
}
// ============================================================
// SHARED DART DRAWING
// ============================================================
function drawPovDartOn(octx,px,py,tx,ty,sc,alpha,color){
octx.save();
octx.globalAlpha=alpha;
octx.translate(px,py);
const ang=Math.atan2(ty-py,tx-px);
octx.rotate(ang-Math.PI/2);
const L=110*sc, W=8*sc;
for(let i=6;i>0;i--){
octx.beginPath(); octx.moveTo(0,L*0.1); octx.lineTo(0,L*0.7);
octx.strokeStyle=`rgba(255,200,60,${0.04*i/6})`;
octx.lineWidth=(W+i*3)*sc*0.6; octx.lineCap='round'; octx.stroke();
}
octx.shadowColor='rgba(0,0,0,0.5)'; octx.shadowBlur=8*sc; octx.shadowOffsetX=2; octx.shadowOffsetY=2;
octx.beginPath(); octx.moveTo(0,-L*0.08); octx.lineTo(-W*0.35,L*0.10); octx.lineTo(W*0.35,L*0.10); octx.closePath();
const tipGrad=octx.createLinearGradient(-W*0.35,0,W*0.35,0);
tipGrad.addColorStop(0,'#aab8c0'); tipGrad.addColorStop(0.45,'#e8f0f5'); tipGrad.addColorStop(1,'#8090a0');
octx.fillStyle=tipGrad; octx.shadowBlur=0; octx.fill();
const barGrad=octx.createLinearGradient(-W*0.5,0,W*0.5,0);
barGrad.addColorStop(0,'#6a4800'); barGrad.addColorStop(0.25,color); barGrad.addColorStop(0.55,'#cc8800'); barGrad.addColorStop(1,'#4a3000');
octx.shadowColor='rgba(0,0,0,0.6)'; octx.shadowBlur=8*sc;
octx.beginPath(); octx.roundRect(-W*0.5,L*0.09,W,L*0.38,W*0.25); octx.fillStyle=barGrad; octx.fill(); octx.shadowBlur=0;
octx.beginPath(); octx.roundRect(-W*0.12,L*0.10,W*0.2,L*0.35,W*0.1); octx.fillStyle='rgba(255,255,255,0.22)'; octx.fill();
for(let g=0;g<4;g++){
const gy=L*(0.14+g*0.08);
octx.beginPath(); octx.roundRect(-W*0.55,gy,W*1.1,L*0.025,W*0.1); octx.fillStyle='rgba(0,0,0,0.45)'; octx.fill();
}
octx.beginPath(); octx.roundRect(-W*0.18,L*0.47,W*0.36,L*0.22,W*0.1); octx.fillStyle='#777'; octx.fill();
octx.beginPath(); octx.moveTo(0,L*0.69); octx.bezierCurveTo(-W*2.2,L*0.58,-W*3,L*0.82,-W*0.15,L*0.92); octx.closePath(); octx.fillStyle=`${color}dd`; octx.fill();
octx.beginPath(); octx.moveTo(0,L*0.69); octx.bezierCurveTo(W*2.2,L*0.58,W*3,L*0.82,W*0.15,L*0.92); octx.closePath(); octx.fillStyle=`${color}aa`; octx.fill();
octx.beginPath(); octx.moveTo(0,L*0.69); octx.lineTo(0,L*0.92); octx.strokeStyle=`${color}99`; octx.lineWidth=1.2*sc; octx.stroke();
octx.restore();
}
function makeDartSVGInline(tipCol,bodyCol,glowCol,ax){
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 58" width="18" height="58"
style="filter:drop-shadow(0 0 5px ${glowCol}) drop-shadow(0 2px 3px rgba(0,0,0,0.7));overflow:visible;">
<polygon points="9,0 6.5,9 11.5,9" fill="${tipCol}"/>
<rect x="7.5" y="8.5" width="3" height="22" rx="1.5" fill="url(#bg${ax|0})"/>
<defs>
<linearGradient id="bg${ax|0}" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#6a4800"/>
<stop offset="30%" stop-color="${bodyCol}"/>
<stop offset="70%" stop-color="#cc8800"/>
<stop offset="100%" stop-color="#4a3000"/>
</linearGradient>
</defs>
<rect x="7" y="13" width="4" height="2.5" rx="1" fill="rgba(0,0,0,0.4)"/>
<rect x="7" y="18" width="4" height="2.5" rx="1" fill="rgba(0,0,0,0.4)"/>
<rect x="7" y="23" width="4" height="2.5" rx="1" fill="rgba(0,0,0,0.4)"/>
<rect x="8.5" y="9" width="1" height="19" rx="0.5" fill="rgba(255,255,255,0.25)"/>
<rect x="8" y="30.5" width="2" height="10" rx="1" fill="#888"/>
<path d="M9,40.5 C3,36 1,50 8.5,50 Z" fill="${bodyCol}" opacity="0.85"/>
<path d="M9,40.5 C15,36 17,50 9.5,50 Z" fill="${bodyCol}" opacity="0.7"/>
<line x1="9" y1="40.5" x2="9" y2="50" stroke="${bodyCol}" stroke-width="1.2" opacity="0.9"/>
</svg>`;
}
function lerp(a,b,t){return a+(b-a)*t;}
// ============================================================
// SCORE ZONE
// ============================================================
function getZone(dx,dy){
const r=420*0.43, dist=Math.sqrt(dx*dx+dy*dy), frac=dist/r;
if(frac>0.95) return{pts:0,label:'MISS',color:'#ff4444'};
const aps=(Math.PI*2)/20, off=-Math.PI/2-aps/2;
let ang=((Math.atan2(dy,dx)-off)%(Math.PI*2)+Math.PI*2)%(Math.PI*2);
const si=Math.floor(ang/aps)%20, v=SEG_VALUES[si];
if(frac<=0.05) return{pts:50,label:'BULLSEYE!',color:'#ff8800'};
if(frac<=0.11) return{pts:25,label:'BULL',color:'#ff4400'};
if(frac>0.62&&frac<=0.68) return{pts:v*3,label:`T${v}`,color:'#00ffcc'};
if(frac>0.88&&frac<=0.95) return{pts:v*2,label:`D${v}`,color:'#ffdd00'};
return{pts:v,label:'',color:'#fff'};
}
// ============================================================
// SINGLE PLAYER HUD
// ============================================================
function addScoreEntry(label,pts,hit){
const hist=document.getElementById('score-history');
const e=document.createElement('div');
e.className='score-entry';
const displayLabel = label || (hit ? pts : 'MISS');
e.innerHTML=`<span>${hit?'✓':'✗'} ${displayLabel}</span><span class="pts">${hit?'+'+pts:'0'}</span>`;
if(!hit) e.style.borderLeftColor='var(--crt-red)';
hist.insertBefore(e,hist.firstChild);
while(hist.children.length>7) hist.removeChild(hist.lastChild);
}
function updateHUD(){
document.getElementById('score-disp').textContent=score;
document.getElementById('streak-disp').textContent=streak;
document.getElementById('streak-fire').style.display=streak>=3?'block':'none';
const mb=document.getElementById('mult-badge');
if(mult>1){mb.style.display='block';mb.textContent='×'+mult+' STREAK!';}
else mb.style.display='none';
}
function makeDartSVG(color='#ffb300',tipColor='#e0e0e0'){
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 56" width="20" height="56">
<polygon points="10,0 7,10 13,10" fill="${tipColor}" opacity="0.95"/>
<rect x="8.5" y="9" width="3" height="24" rx="1.5" fill="${color}"/>
<rect x="8" y="14" width="4" height="3" rx="1" fill="rgba(0,0,0,0.35)"/>
<rect x="8" y="20" width="4" height="3" rx="1" fill="rgba(0,0,0,0.35)"/>
<rect x="9.2" y="10" width="1.2" height="20" rx="0.6" fill="rgba(255,255,255,0.25)"/>
<rect x="8.5" y="33" width="3" height="5" rx="1" fill="${color}" opacity="0.8"/>
<path d="M10,33 L4,52 L10,46 Z" fill="${color}" opacity="0.75"/>
<path d="M10,33 L16,52 L10,46 Z" fill="${color}" opacity="0.6"/>
</svg>`;
}
function updateDartPips(){
const container=document.getElementById('darts-pips');
container.innerHTML='';
const total=diffSettings[difficulty].darts;
for(let i=0;i<total;i++){
const pip=document.createElement('div');
pip.className='dart-pip'+(i>=dartsLeft?' used':'');
pip.innerHTML=makeDartSVG();
container.appendChild(pip);
}
}
function updatePhaseUI(){
const px=document.getElementById('phase-x');
const py=document.getElementById('phase-y');
const pt=document.getElementById('phase-throw');
px.className='phase-step'; py.className='phase-step'; pt.className='phase-step';
if(phase==='x') px.classList.add('active');
else if(phase==='y'){px.classList.add('done');py.classList.add('active');}
else{px.classList.add('done');py.classList.add('done');pt.classList.add('active');}
}
function setHint(){
const el=document.getElementById('action-hint');
const key=controlMode==='joystick'?'[JS KNOP]':'[SPATIE]';
if(phase==='x') el.textContent=`${key} = STOP X-AS`;
else if(phase==='y') el.textContent=`${key} = STOP Y-AS`;
else el.textContent='GOOIEN...';
}
// ============================================================
// SINGLE END GAME
// ============================================================
function endGame(){
cancelAnimationFrame(animFrame);
const isNew=score>highScore;
if(isNew) highScore=score;
document.getElementById('go-single-wrap').style.display='';
document.getElementById('go-split-wrap').style.display='none';
document.getElementById('go-winner').textContent='';
document.getElementById('go-title').textContent='GAME OVER';
document.getElementById('go-score-val').textContent=score;
document.getElementById('go-best-val').textContent=isNew?'★ NIEUW RECORD! ★':`Beste: ${highScore}`;
document.getElementById('go-best-val').style.color=isNew?'var(--crt-amber)':'var(--crt-green)';
const gr=getGrade(score);
const gradeEl=document.getElementById('go-grade');
gradeEl.textContent=`RANG: ${gr.g}`;
gradeEl.style.color=gr.c;
gradeEl.style.textShadow=`0 0 20px ${gr.c}`;
// Always prompt for name if score > 0
if(score > 0){
promptName(score, difficulty, ()=>{ showScreen('gameover'); });
} else {
showScreen('gameover');
}
}
function goToMenu(){cancelAnimationFrame(animFrame);cancelAnimationFrame(splitAnimFrame);initAttract();}
function restartGame(){if(isSplit) startSplitGame(); else startGame();}
// ============================================================
// INPUT
// ============================================================
document.addEventListener('keydown', e=>{
const isSpace=e.code==='Space'||e.code==='KeyZ'||e.code==='KeyX';
const isEnter=e.code==='Enter'||e.code==='NumpadEnter';
const isFire=isSpace||isEnter;
if(isFire) e.preventDefault();
if(currentScreen==='attract'&&isFire){openMenu();return;}
if(currentScreen==='gameover'){
if(isFire) restartGame();
if(e.code==='Escape') goToMenu();
return;
}
if(currentScreen==='game'){
if(isSplit){
if(isSpace) fireSplitAction('p1');
if(isEnter) fireSplitAction('p2');
} else {
if(isFire) fireAction('p1');
}
}
});
window.addEventListener('gamepadconnected',e=>{gamepads[e.gamepad.index]=e.gamepad;});
window.addEventListener('gamepaddisconnected',e=>{delete gamepads[e.gamepad.index];});
initAttract();
</script>
<!-- FULLSCREEN PROMPT OVERLAY -->
<div id="fs-overlay" style="
position:fixed;inset:0;z-index:99999;
background:#020a04;
display:flex;flex-direction:column;
align-items:center;justify-content:center;
gap:24px;
font-family:'Press Start 2P',monospace;
cursor:pointer;
">
<div style="font-size:clamp(3rem,10vw,6rem);filter:drop-shadow(0 0 20px #39ff14);">🎯</div>
<div style="font-size:clamp(1rem,3vw,1.8rem);color:#39ff14;text-shadow:0 0 20px #39ff14;letter-spacing:0.1em;text-align:center;line-height:1.6;">
LAUPAN LUCHCD<br>
<span style="color:#ffb300;text-shadow:0 0 20px #ffb300;">PEILTJUS</span>
</div>
<div style="font-size:clamp(0.5rem,1.5vw,0.8rem);color:#ffb300;letter-spacing:0.2em;animation:blink 0.9s step-end infinite;">
[ TIK OM TE STARTEN ]
</div>
<div style="font-size:clamp(0.35rem,0.9vw,0.5rem);color:rgba(57,255,20,0.4);letter-spacing:0.15em;margin-top:8px;">
VOLLEDIG SCHERM WORDT GEACTIVEERD
</div>
</div>
<script>
// ── FULLSCREEN / KIOSK ───────────────────────────────────────
const fsOverlay = document.getElementById('fs-overlay');
function enterFullscreen() {
const el = document.documentElement;
const req = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullscreen;
if (req) req.call(el).catch(()=>{});
}
function dismissOverlay() {
enterFullscreen();
fsOverlay.style.transition = 'opacity 0.5s';
fsOverlay.style.opacity = '0';
setTimeout(() => { fsOverlay.style.display = 'none'; }, 500);
}
fsOverlay.addEventListener('click', dismissOverlay);
fsOverlay.addEventListener('touchstart', e => { e.preventDefault(); dismissOverlay(); }, { passive: false });
// Re-enter fullscreen if user accidentally exits (e.g. presses Escape)
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement && fsOverlay.style.display === 'none') {
// Small delay so Escape key doesn't feel broken
setTimeout(enterFullscreen, 800);
}
});
// Prevent right-click context menu (kiosk feel)
document.addEventListener('contextmenu', e => e.preventDefault());
// Prevent text selection on double-tap
document.addEventListener('selectstart', e => e.preventDefault());
// Lock screen orientation to landscape on mobile if supported
if (screen.orientation && screen.orientation.lock) {
screen.orientation.lock('landscape').catch(() => {});
}
</script>
</body>
</html>