silly slop

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2025-11-20 01:16:36 +01:00
parent e153cbdb9a
commit ed4d4ea132
2 changed files with 883 additions and 9 deletions

View File

@@ -2,6 +2,7 @@
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { fade, fly } from 'svelte/transition'; import { fade, fly } from 'svelte/transition';
import SnakeGame from './SnakeGame.svelte'; import SnakeGame from './SnakeGame.svelte';
import PhishingGame from './PhishingGame.svelte';
// konami code sequence // konami code sequence
const konamiCode = [ const konamiCode = [
@@ -16,18 +17,52 @@
]; ];
let konamiIndex = 0; let konamiIndex = 0;
// track ctrl+shift+p shortcut
let ctrlPressed = false;
let shiftPressed = false;
let gameVisible = false; let gameVisible = false;
const currentGame = { let currentGame = null;
name: 'snake',
title: 'slop cred snake', const games = [
component: SnakeGame, {
emoji: '' name: 'snake',
}; title: 'slop cred snake',
component: SnakeGame
},
{
name: 'phishing',
title: 'slopedition',
component: PhishingGame
}
];
function getRandomGame() {
return games[Math.floor(Math.random() * games.length)];
}
function handleKeyDown(e) { function handleKeyDown(e) {
// track modifier keys
if (e.key === 'Control') {
ctrlPressed = true;
}
if (e.key === 'Shift') {
shiftPressed = true;
}
// check for ctrl+shift+p to launch random game
if (ctrlPressed && shiftPressed && (e.key === 'p' || e.key === 'P')) {
e.preventDefault();
currentGame = getRandomGame();
gameVisible = true;
return;
}
// check for konami code to launch random game
if (e.key === konamiCode[konamiIndex]) { if (e.key === konamiCode[konamiIndex]) {
konamiIndex++; konamiIndex++;
if (konamiIndex === konamiCode.length) { if (konamiIndex === konamiCode.length) {
currentGame = getRandomGame();
gameVisible = true; gameVisible = true;
konamiIndex = 0; konamiIndex = 0;
} }
@@ -36,6 +71,15 @@
} }
} }
function handleKeyUp(e) {
if (e.key === 'Control') {
ctrlPressed = false;
}
if (e.key === 'Shift') {
shiftPressed = false;
}
}
function closeGame() { function closeGame() {
gameVisible = false; gameVisible = false;
} }
@@ -46,14 +90,16 @@
onMount(() => { onMount(() => {
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
}); });
onDestroy(() => { onDestroy(() => {
window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
}); });
</script> </script>
{#if gameVisible} {#if gameVisible && currentGame}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
@@ -89,9 +135,7 @@
<h1 <h1
class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-pc-pink via-pc-purple to-cta-blue font-phudu text-center" class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-pc-pink via-pc-purple to-cta-blue font-phudu text-center"
> >
{currentGame.emoji}
{currentGame.title} {currentGame.title}
{currentGame.emoji}
</h1> </h1>
<p class="text-pc-lightblue text-xs text-center mt-1 font-titilium">press esc to close</p> <p class="text-pc-lightblue text-xs text-center mt-1 font-titilium">press esc to close</p>
</div> </div>

View File

@@ -0,0 +1,830 @@
<script>
import { onMount } from 'svelte';
export let onGameOver = () => {};
export let onClose = () => {};
let canvas;
let ctx;
let gameLoop;
let score = 0;
let gameRunning = false;
let gameOver = false;
let gameStarted = false;
let showHighScoreEntry = false;
let playerName = '';
let highScores = [];
const canvasWidth = 400;
const canvasHeight = 600;
const lureSize = 20;
const maxDepthValue = 10000;
// game state
let lure = { x: canvasWidth / 2, y: 50, speed: 3 };
let descending = true;
let reachedBottom = false;
let bottomPauseTimer = 0;
let catchPauseTimer = 0;
let employees = [];
let traps = [];
let caughtEmployees = []; // each has: employee data, offsetX, offsetY, angle
let depth = 0;
let combo = 0;
// employee types with different point values
const employeeTypes = [
{
type: 'intern',
color: '#4ade80',
points: 10,
emoji: '🧑‍💻',
title: 'intern',
minDepth: 0,
size: 30
},
{
type: 'employee',
color: '#60a5fa',
points: 25,
emoji: '👔',
title: 'employee',
minDepth: 0,
size: 35
},
{
type: 'analyst',
color: '#22d3ee',
points: 40,
emoji: '📊',
title: 'analyst',
minDepth: 1000,
size: 38
},
{
type: 'manager',
color: '#a78bfa',
points: 75,
emoji: '👨‍💼',
title: 'manager',
minDepth: 2000,
size: 42
},
{
type: 'senior',
color: '#c084fc',
points: 120,
emoji: '🎓',
title: 'senior',
minDepth: 3500,
size: 45
},
{
type: 'director',
color: '#f472b6',
points: 200,
emoji: '🎯',
title: 'director',
minDepth: 5000,
size: 48
},
{
type: 'vp',
color: '#fb923c',
points: 350,
emoji: '💼',
title: 'vp',
minDepth: 6500,
size: 52
},
{
type: 'cto',
color: '#fbbf24',
points: 500,
emoji: '⚡',
title: 'cto',
minDepth: 7500,
size: 55
},
{
type: 'cfo',
color: '#facc15',
points: 750,
emoji: '💰',
title: 'cfo',
minDepth: 8500,
size: 58
},
{
type: 'ceo',
color: '#fde047',
points: 1000,
emoji: '👑',
title: 'ceo',
minDepth: 9500,
size: 62,
hasRing: true
}
];
// trap/obstacle types
const trapTypes = [
{ type: 'firewall', color: '#ef4444', emoji: '🔥', title: 'firewall', size: 55 },
{ type: 'antivirus', color: '#f59e0b', emoji: '🛡️', title: 'antivirus', size: 48 },
{ type: 'training', color: '#ec4899', emoji: '📚', title: 'security training', size: 52 },
{ type: 'passkey', color: '#8b5cf6', emoji: '🔐', title: 'passkey', size: 45 }
];
function init() {
if (canvas) {
ctx = canvas.getContext('2d');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}
loadHighScores();
}
function loadHighScores() {
try {
const saved = localStorage.getItem('phishingGameHighScores');
if (saved) {
highScores = JSON.parse(saved);
}
} catch (e) {
console.error('failed to load high scores:', e);
}
}
function saveHighScore(name) {
highScores.push({ name, score: Math.floor(score) });
highScores.sort((a, b) => b.score - a.score);
highScores = highScores.slice(0, 3);
localStorage.setItem('phishingGameHighScores', JSON.stringify(highScores));
}
function startGame() {
score = 0;
gameStarted = true;
gameRunning = true;
gameOver = false;
descending = true;
reachedBottom = false;
bottomPauseTimer = 0;
catchPauseTimer = 0;
depth = 0;
combo = 0;
lure = { x: canvasWidth / 2, y: 50, speed: 3 };
employees = [];
traps = [];
caughtEmployees = [];
// spawn initial obstacles
spawnInitialObstacles();
gameLoop = setInterval(() => {
update();
draw();
}, 1000 / 60);
}
function spawnInitialObstacles() {
// spawn employees at various depths
for (let i = 0; i < 40; i++) {
spawnEmployee(Math.random() * maxDepthValue + 200);
}
// spawn traps at various depths
for (let i = 0; i < 25; i++) {
spawnTrap(Math.random() * maxDepthValue + 300);
}
}
function spawnEmployee(atDepth) {
// filter employee types available at this depth
const availableTypes = employeeTypes.filter((type) => atDepth >= type.minDepth);
if (availableTypes.length === 0) return;
// weight higher value employees less
const weights = availableTypes.map((_, i) => Math.pow(0.6, i));
const totalWeight = weights.reduce((a, b) => a + b, 0);
const rand = Math.random() * totalWeight;
let cumulative = 0;
let selectedType = availableTypes[0];
for (let i = 0; i < availableTypes.length; i++) {
cumulative += weights[i];
if (rand <= cumulative) {
selectedType = availableTypes[i];
break;
}
}
employees.push({
x: Math.random() * (canvasWidth - selectedType.size) + selectedType.size / 2,
y: atDepth,
...selectedType,
vx: (Math.random() - 0.5) * 2,
direction: Math.random() > 0.5 ? 1 : -1
});
}
function spawnTrap(atDepth) {
const trapType = trapTypes[Math.floor(Math.random() * trapTypes.length)];
traps.push({
x: Math.random() * (canvasWidth - trapType.size) + trapType.size / 2,
y: atDepth,
...trapType
});
}
function update() {
if (!gameRunning) return;
if (descending) {
// move lure down
lure.y += lure.speed;
depth = lure.y;
// check if reached bottom (10000m)
if (lure.y >= maxDepthValue) {
lure.y = maxDepthValue;
reachedBottom = true;
bottomPauseTimer = 60; // pause for 1 second (60 frames)
descending = false;
}
// check collision with employees (catch and start ascending)
for (let i = employees.length - 1; i >= 0; i--) {
const emp = employees[i];
if (checkCollision(lure, emp, emp.size)) {
// catch employee and start ascending
score += emp.points;
combo++;
caughtEmployees.push({
...emp,
offsetX: (Math.random() - 0.5) * 30,
offsetY: (Math.random() - 0.5) * 30 + caughtEmployees.length * 15,
angle: (Math.random() - 0.5) * 60
});
employees.splice(i, 1);
descending = false;
catchPauseTimer = 60; // pause for 1 second before ascending
break;
}
}
// check collision with traps (instant death)
for (const trap of traps) {
if (checkCollision(lure, trap, trap.size)) {
endGame();
return;
}
}
// move employees
for (const emp of employees) {
emp.x += emp.vx * emp.direction;
if (emp.x < emp.size / 2 || emp.x > canvasWidth - emp.size / 2) {
emp.direction *= -1;
}
}
// spawn more obstacles as we go deeper
if (Math.random() < 0.008) {
spawnEmployee(lure.y + canvasHeight);
}
if (Math.random() < 0.005) {
spawnTrap(lure.y + canvasHeight);
}
} else {
// handle bottom pause
if (bottomPauseTimer > 0) {
bottomPauseTimer--;
if (bottomPauseTimer === 0) {
lure.speed = 4;
}
// don't move during pause
} else if (catchPauseTimer > 0) {
// handle catch pause
catchPauseTimer--;
if (catchPauseTimer === 0) {
lure.speed = 4;
}
// don't move during pause
} else {
// ascending - move lure up
lure.y -= lure.speed;
}
// check if reached surface
if (lure.y < 0) {
endGame();
return;
}
// check collision with employees (catch them while ascending)
for (let i = employees.length - 1; i >= 0; i--) {
const emp = employees[i];
if (checkCollision(lure, emp, emp.size)) {
// catch employee
score += emp.points * (1 + combo * 0.1);
combo++;
caughtEmployees.push({
...emp,
offsetX: (Math.random() - 0.5) * 30,
offsetY: (Math.random() - 0.5) * 30 + caughtEmployees.length * 15,
angle: (Math.random() - 0.5) * 60
});
employees.splice(i, 1);
}
}
// check collision with traps (instant death - lose all points)
for (const trap of traps) {
if (checkCollision(lure, trap, trap.size)) {
score = 0;
endGame();
return;
}
}
// move employees
for (const emp of employees) {
emp.x += emp.vx * emp.direction;
if (emp.x < emp.size / 2 || emp.x > canvasWidth - emp.size / 2) {
emp.direction *= -1;
}
}
}
// handle keyboard input for horizontal movement
if (keys.left && lure.x > lureSize) {
lure.x -= 3;
}
if (keys.right && lure.x < canvasWidth - lureSize) {
lure.x += 3;
}
}
function checkCollision(obj1, obj2, size) {
const dx = obj1.x - obj2.x;
const dy = obj1.y - obj2.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance < size / 2 + lureSize / 2;
}
function draw() {
if (!ctx) return;
// clear canvas with depth-based darkness (much darker)
const depthRatio = Math.min(depth / maxDepthValue, 1);
const darkness = Math.floor(10 * (1 - depthRatio * 0.95)); // 10 to almost 0
ctx.fillStyle = `rgb(${darkness}, ${darkness}, ${darkness})`;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
// draw depth gradient that gets darker and more intense deeper
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight);
const alpha1 = 0.05 * (1 - depthRatio * 0.9);
const alpha2 = 0.15 * (1 - depthRatio * 0.85);
const alpha3 = 0.25 * (1 - depthRatio * 0.8);
gradient.addColorStop(0, `rgba(14, 165, 233, ${alpha1})`);
gradient.addColorStop(0.5, `rgba(14, 165, 233, ${alpha2})`);
gradient.addColorStop(1, `rgba(8, 47, 73, ${alpha3})`);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
// calculate camera offset (follow lure)
let cameraY = 0;
if (lure.y > canvasHeight / 2 && descending) {
cameraY = lure.y - canvasHeight / 2;
} else if (!descending && lure.y < depth - canvasHeight / 2) {
cameraY = Math.max(0, lure.y - canvasHeight / 2);
} else if (!descending) {
cameraY = Math.max(0, depth - canvasHeight);
}
ctx.save();
ctx.translate(0, -cameraY);
// draw the bottom floor
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, maxDepthValue, canvasWidth, 100);
// draw bottom glow
const bottomGradient = ctx.createLinearGradient(0, maxDepthValue - 50, 0, maxDepthValue);
bottomGradient.addColorStop(0, 'rgba(139, 92, 246, 0)');
bottomGradient.addColorStop(1, 'rgba(139, 92, 246, 0.5)');
ctx.fillStyle = bottomGradient;
ctx.fillRect(0, maxDepthValue - 50, canvasWidth, 50);
// draw bottom text
ctx.fillStyle = '#8b5cf6';
ctx.font = 'bold 24px monospace';
ctx.textAlign = 'center';
ctx.fillText('BOTTOM', canvasWidth / 2, maxDepthValue + 30);
ctx.font = '14px monospace';
ctx.fillText('10,000m', canvasWidth / 2, maxDepthValue + 55);
// draw depth markers
ctx.strokeStyle = 'rgba(139, 92, 246, 0.2)';
ctx.lineWidth = 1;
for (let i = 0; i < maxDepthValue + canvasHeight; i += 100) {
ctx.beginPath();
ctx.moveTo(0, i);
ctx.lineTo(canvasWidth, i);
ctx.stroke();
ctx.fillStyle = 'rgba(139, 92, 246, 0.3)';
ctx.font = '10px monospace';
ctx.fillText(`${i}m`, 5, i - 5);
}
// draw traps
for (const trap of traps) {
if (trap.y > cameraY - trap.size && trap.y < cameraY + canvasHeight + trap.size) {
// glow (stronger deeper)
const trapDepthRatio = Math.min(trap.y / maxDepthValue, 1);
ctx.shadowBlur = 20 + trapDepthRatio * 30;
ctx.shadowColor = trap.color;
// draw trap
ctx.fillStyle = trap.color;
ctx.fillRect(trap.x - trap.size / 2, trap.y - trap.size / 2, trap.size, trap.size);
// emoji
ctx.shadowBlur = 0;
ctx.font = `${trap.size * 0.6}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(trap.emoji, trap.x, trap.y);
}
}
// draw employees
for (const emp of employees) {
if (emp.y > cameraY - emp.size && emp.y < cameraY + canvasHeight + emp.size) {
// glow (stronger deeper)
const empDepthRatio = Math.min(emp.y / maxDepthValue, 1);
ctx.shadowBlur = 15 + empDepthRatio * 25;
ctx.shadowColor = emp.color;
// draw glowing ring for CEO
if (emp.hasRing) {
ctx.strokeStyle = emp.color;
ctx.lineWidth = 3;
ctx.shadowBlur = 30 + empDepthRatio * 40;
ctx.beginPath();
ctx.arc(emp.x, emp.y, emp.size / 2 + 8, 0, Math.PI * 2);
ctx.stroke();
}
// draw employee circle
ctx.fillStyle = emp.color;
ctx.beginPath();
ctx.arc(emp.x, emp.y, emp.size / 2, 0, Math.PI * 2);
ctx.fill();
// emoji
ctx.shadowBlur = 0;
ctx.font = `${emp.size * 0.6}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(emp.emoji, emp.x, emp.y);
}
}
// draw fishing line
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(lure.x, 0);
ctx.lineTo(lure.x, lure.y);
ctx.stroke();
// draw caught employees attached to hook
for (const caught of caughtEmployees) {
const catchX = lure.x + caught.offsetX;
const catchY = lure.y + caught.offsetY;
ctx.save();
ctx.translate(catchX, catchY);
ctx.rotate((caught.angle * Math.PI) / 180);
// glow (based on depth)
const caughtDepthRatio = Math.min(lure.y / maxDepthValue, 1);
ctx.shadowBlur = 15 + caughtDepthRatio * 25;
ctx.shadowColor = caught.color;
// draw glowing ring for CEO
if (caught.hasRing) {
ctx.strokeStyle = caught.color;
ctx.lineWidth = 3;
ctx.shadowBlur = 30 + caughtDepthRatio * 40;
ctx.beginPath();
ctx.arc(0, 0, caught.size / 2 + 8, 0, Math.PI * 2);
ctx.stroke();
ctx.shadowBlur = 15 + caughtDepthRatio * 25;
}
// draw employee circle
ctx.fillStyle = caught.color;
ctx.beginPath();
ctx.arc(0, 0, caught.size / 2, 0, Math.PI * 2);
ctx.fill();
// emoji
ctx.shadowBlur = 0;
ctx.font = `${caught.size * 0.6}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(caught.emoji, 0, 0);
ctx.restore();
}
// draw lure (phishing email)
const lureDepthRatio = Math.min(lure.y / maxDepthValue, 1);
ctx.shadowBlur = 20 + lureDepthRatio * 30;
ctx.shadowColor = descending ? '#22d3ee' : '#f59e0b';
ctx.fillStyle = descending ? '#22d3ee' : '#f59e0b';
ctx.beginPath();
ctx.arc(lure.x, lure.y, lureSize / 2, 0, Math.PI * 2);
ctx.fill();
// draw hook
ctx.shadowBlur = 0;
ctx.font = `${lureSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('📧', lure.x, lure.y);
ctx.restore();
// draw caught employees indicator
if (caughtEmployees.length > 0) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(10, canvasHeight - 60, 200, 50);
ctx.fillStyle = '#4ade80';
ctx.font = '14px monospace';
ctx.fillText(`caught: ${caughtEmployees.length}`, 20, canvasHeight - 40);
ctx.fillText(`combo: x${combo.toFixed(1)}`, 20, canvasHeight - 20);
}
// draw status
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(canvasWidth - 150, 10, 140, 60);
ctx.fillStyle = '#22d3ee';
ctx.font = '12px monospace';
ctx.fillText(`depth: ${Math.floor(depth)}m`, canvasWidth - 140, 30);
ctx.fillStyle = descending
? '#4ade80'
: bottomPauseTimer > 0 || catchPauseTimer > 0
? '#fbbf24'
: '#f59e0b';
const status = descending
? '⬇ descending'
: bottomPauseTimer > 0
? '⏸ bottom!'
: catchPauseTimer > 0
? '⏸ caught!'
: '⬆ ascending';
ctx.fillText(status, canvasWidth - 140, 50);
}
function endGame() {
gameRunning = false;
gameOver = true;
clearInterval(gameLoop);
// check if high score (top 3)
if (highScores.length < 3 || score > highScores[highScores.length - 1].score) {
showHighScoreEntry = true;
}
onGameOver();
}
function submitHighScore() {
if (playerName.trim().length > 0) {
saveHighScore(playerName.trim());
showHighScoreEntry = false;
playerName = '';
}
}
function restart() {
startGame();
}
// keyboard controls
let keys = { left: false, right: false };
function handleKeyDown(e) {
if (e.key === 'Escape') {
onClose();
return;
}
if (!gameStarted) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
startGame();
}
return;
}
if (gameOver && showHighScoreEntry && e.key === 'Enter') {
e.preventDefault();
submitHighScore();
return;
}
if (gameOver && !showHighScoreEntry && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
restart();
return;
}
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
keys.left = true;
}
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
keys.right = true;
}
}
function handleKeyUp(e) {
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
keys.left = false;
}
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
keys.right = false;
}
}
onMount(() => {
init();
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
if (gameLoop) clearInterval(gameLoop);
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
});
</script>
<div class="flex flex-col items-center gap-4 p-6">
{#if gameStarted}
<div class="flex justify-between w-full max-w-[400px] mb-2">
<div class="text-pc-green">
<div class="text-xs uppercase tracking-wider">score</div>
<div class="text-2xl font-bold">{Math.floor(score)}</div>
</div>
<div class="text-pc-pink">
<div class="text-xs uppercase tracking-wider">depth</div>
<div class="text-2xl font-bold">{Math.floor(depth)}m</div>
</div>
</div>
{/if}
<div class="relative" style="width: {canvasWidth}px; height: {canvasHeight}px;">
<canvas
bind:this={canvas}
class="border-2 border-pc-pink rounded shadow-lg shadow-pc-pink/30"
style="image-rendering: pixelated;"
/>
{#if !gameStarted}
<div
class="absolute inset-0 flex flex-col items-center justify-center bg-black/80 backdrop-blur-sm"
>
<div class="text-pc-lightblue text-sm w-full max-w-[380px] mb-6 space-y-4 px-4">
<div>
<div class="text-pc-green font-bold uppercase text-xs mb-2">🎣 how to play</div>
<ul class="text-xs space-y-1 list-disc list-inside">
<li>use ← → (or a/d) to move horizontally</li>
<li>
<span class="text-pc-purple">descending:</span> catch an employee to start ascending
</li>
<li>hitting traps = instant death!</li>
<li>the deeper you go, the better targets appear</li>
<li>catch multiple employees on the way up for combos</li>
<li>reach max depth (10,000m) for the best catches!</li>
</ul>
</div>
<div>
<div class="text-pc-pink font-bold uppercase text-xs mb-2">
🎯 targets (deeper = better)
</div>
<div class="grid grid-cols-5 gap-1 text-xs">
{#each employeeTypes as emp}
<div class="flex flex-col items-center gap-0.5 text-center">
<span class="text-xl">{emp.emoji}</span>
<span class="text-white text-[10px]">{emp.title}</span>
<span style="color: {emp.color}" class="font-bold text-[10px]"
>{emp.points}pts</span
>
</div>
{/each}
</div>
</div>
<div>
<div class="text-pc-red font-bold uppercase text-xs mb-2">
⚠️ obstacles (instant death!)
</div>
<div class="grid grid-cols-4 gap-2 text-xs">
{#each trapTypes as trap}
<div class="flex flex-col items-center gap-1 text-center">
<span class="text-2xl">{trap.emoji}</span>
<span class="text-white text-xs">{trap.title}</span>
</div>
{/each}
</div>
</div>
</div>
<button
on:click={startGame}
class="bg-gradient-to-r from-pc-pink to-pc-purple text-white px-8 py-3 rounded font-bold uppercase text-sm hover:shadow-lg hover:shadow-pc-pink/50 transition-all"
>
start phishing
</button>
</div>
{:else if showHighScoreEntry}
<div
class="absolute inset-0 flex flex-col items-center justify-center bg-black/90 backdrop-blur-sm p-6"
>
<div class="text-3xl font-bold text-pc-green mb-4">new high score! 🎉</div>
<div class="text-2xl text-pc-pink mb-6">{Math.floor(score)} points</div>
<div class="mb-6">
<label for="player-name" class="text-pc-lightblue text-sm mb-2 block"
>enter your handle:</label
>
<input
id="player-name"
type="text"
bind:value={playerName}
on:keydown={(e) => e.key === 'Enter' && submitHighScore()}
maxlength="20"
class="bg-black/60 border-2 border-pc-purple text-white px-4 py-2 rounded focus:outline-none focus:border-pc-pink w-64 text-center"
placeholder="elite_phisher"
/>
</div>
<button
on:click={submitHighScore}
class="bg-gradient-to-r from-pc-pink to-pc-purple text-white px-8 py-3 rounded font-bold uppercase text-sm hover:shadow-lg hover:shadow-pc-pink/50 transition-all"
>
submit score
</button>
</div>
{:else if gameOver}
<div
class="absolute inset-0 flex flex-col items-center justify-center bg-black/90 backdrop-blur-sm"
>
<div class="text-4xl font-bold text-pc-red mb-4">phishing complete! 🎣</div>
<div class="text-2xl text-pc-pink mb-6">{Math.floor(score)} points</div>
{#if highScores.length > 0}
<div class="mb-6 w-full max-w-[300px]">
<div class="text-pc-purple font-bold uppercase text-sm mb-2 text-center">
🏆 high scores
</div>
<div class="bg-black/60 rounded border border-pc-purple/30 p-3">
{#each highScores as hs, i}
<div class="flex justify-between text-xs text-pc-lightblue mb-1">
<span class="text-pc-pink">{i + 1}. {hs.name}</span>
<span class="text-white font-bold">{hs.score}</span>
</div>
{/each}
</div>
</div>
{/if}
<button
on:click={restart}
class="bg-gradient-to-r from-pc-pink to-pc-purple text-white px-8 py-3 rounded font-bold uppercase text-sm hover:shadow-lg hover:shadow-pc-pink/50 transition-all"
>
phish again
</button>
</div>
{/if}
</div>
{#if gameStarted && !gameOver}
<div class="text-pc-lightblue text-xs text-center">
use ← → or a/d to steer • {descending
? 'catch an employee to start ascending! avoid traps!'
: 'catch more targets!'}
</div>
{/if}
</div>