/** * Interactive Mini-Game Button */ class GameButton { constructor(buttonId) { this.button = document.getElementById(buttonId) this.runButton = document.getElementById("runBtn") if (!this.button) { console.error("GameButton: Target button not found") return } // --- Injection Logic --- this.button.classList.add("game-btn") // Add our styling class // Wrap existing text to animate it if (!this.button.querySelector(".btn-text")) { const textSpan = document.createElement("span") textSpan.className = "btn-text" while (this.button.firstChild) { textSpan.appendChild(this.button.firstChild) } this.button.appendChild(textSpan) } // Create Game Container if (!this.button.querySelector(".game-container")) { this.gameContainer = document.createElement("div") this.gameContainer.className = "game-container" this.gameContainer.id = "gameContainer" this.button.appendChild(this.gameContainer) } else { this.gameContainer = this.button.querySelector(".game-container") } // Create Progress Bar if (!this.button.querySelector(".game-button-progress-bar")) { const progBar = document.createElement("div") progBar.className = "game-button-progress-bar" this.progressFill = document.createElement("div") this.progressFill.className = "progress-fill" this.progressFill.id = "progressFill" progBar.appendChild(this.progressFill) this.button.appendChild(progBar) } else { this.progressFill = this.button.querySelector(".progress-fill") } // Create Completion Controls if (!this.button.querySelector(".completion-controls")) { const controls = document.createElement("div") controls.className = "completion-controls" this.compText = document.createElement("span") this.compText.className = "completion-text" this.compText.innerText = "Fertig!" controls.appendChild(this.compText) this.closeBtn = document.createElement("button") this.closeBtn.className = "close-game-btn" this.closeBtn.title = "Close Game" // Use innerHTML for SVG icon this.closeBtn.innerHTML = '' // Prevent close button click from triggering main button this.closeBtn.addEventListener("click", (e) => { e.stopPropagation() this.reset() }) controls.appendChild(this.closeBtn) this.button.appendChild(controls) } else { this.closeBtn = this.button.querySelector(".close-game-btn") this.compText = this.button.querySelector(".completion-text") this.closeBtn.addEventListener("click", (e) => { e.stopPropagation() this.reset() }) } if (!this.button.querySelector(".points-wrapper")) { const pointsWrapper = document.createElement("div") pointsWrapper.className = "points-wrapper" const pointsTextWrapper = document.createElement("span") pointsTextWrapper.className = "points-text-wrapper" const pointsText = document.createElement("span") pointsText.className = "points-text" pointsText.innerText = "p" this.points = document.createElement("span") this.points.className = "points" this.points.innerText = "0" pointsTextWrapper.appendChild(this.points) pointsTextWrapper.appendChild(pointsText) pointsWrapper.appendChild(pointsTextWrapper) this.button.appendChild(pointsWrapper) } else { this.points = this.button.querySelector(".points") } // ----------------------- this.durationInput = document.getElementById("durationInput") this.state = "IDLE" this.processDuration = 5000 this.startTime = 0 this.rafId = null this.gamePoints = 0 this.games = ["snake", "memory", "simon"] this.currentGame = null this.button.addEventListener("click", (e) => { if (this.button.classList.contains("active")) e.preventDefault() if (this.state === "IDLE") this.activate() }) // Pause on blur this.button.addEventListener("blur", () => { // if (this.currentGame && (this.state === "ACTIVE" || this.state === "COMPLETE")) { // this.currentGame.pause() // this.button.classList.add("paused") // } }) // Resume on focus this.button.addEventListener("focus", () => { if (this.currentGame && (this.state === "ACTIVE" || this.state === "COMPLETE")) { this.currentGame.resume() this.button.classList.remove("paused") } }) this.button.addEventListener("focusout", (e) => { if (this.currentGame && (this.state === "ACTIVE" || this.state === "COMPLETE")) { if (e.currentTarget.contains(e.relatedTarget)) { /* Focus will still be within the container */ this.currentGame.resume() this.button.classList.remove("paused") } else { /* Focus will leave the container */ this.currentGame.pause() this.button.classList.add("paused") } } }) } activate() { if (this.state !== "IDLE") return this.runButton.click() // Get duration from input if present if (this.durationInput) { const val = parseInt(this.durationInput.value) if (val && val > 0) this.processDuration = val } this.state = "ACTIVE" this.button.classList.add("active", "process-running") this.button.classList.remove("complete") // Start the fake process this.startTime = Date.now() this.progressFill.style.width = "0%" setTimeout(() => this.updateProgress(), 200) // Pick a random game setTimeout(() => { if (this.state === "ACTIVE") { this.launchRandomGame() } }, 500) } updateProgress() { if (this.state === "IDLE") return const elapsed = Date.now() - this.startTime const firstActiveProgressBar = document.querySelectorAll("div.progress-bar")[0] const progressError = document.querySelectorAll('div[data-testid="status-tracker"] span.error')[0] if (firstActiveProgressBar) { const progress = firstActiveProgressBar.style.width this.progressFill.style.width = `${progress}` } // if (progress >= 100 && this.state !== "COMPLETE") { // this.complete() // } if (progressError) { console.log("complete error") this.completeError() } if (this.button.classList.contains("process-running") === false && this.state !== "COMPLETE") { console.log("complete") this.progressFill.style.width = "100%" this.complete() } if (this.state !== "IDLE") { if (this.button.classList.contains("process-running")) { this.rafId = requestAnimationFrame(() => this.updateProgress()) } } } completeError() { this.state = "COMPLETE" this.compText.innerText = "ERROR!" this.button.classList.add("complete", "error") this.button.classList.remove("process-running") // Game continues running! No cleanup here. } complete() { this.state = "COMPLETE" this.button.classList.add("complete") // Game continues running! No cleanup here. } reset() { this.state = "IDLE" this.button.classList.remove("active") this.button.classList.remove("complete", "error") this.compText.innerText = "Fertig!" this.progressFill.style.width = "0%" if (this.currentGame) { this.currentGame.cleanup() this.currentGame = null } this.gameContainer.innerHTML = "" // Remove paused class if present this.button.classList.remove("paused") } launchRandomGame() { // Simple random const gameType = this.games[Math.floor(Math.random() * this.games.length)] // const gameType = "snake" this.gameContainer.innerHTML = "" const w = this.button.offsetWidth const h = this.button.offsetHeight if (gameType === "snake") { this.currentGame = new SnakeGame(this.gameContainer, w, h) } else if (gameType === "memory") { this.currentGame = new MemoryGame(this.gameContainer) } else if (gameType === "simon") { this.currentGame = new SimonGame(this.gameContainer) } } } /** * Snake Game Implementation */ class SnakeGame { constructor(container, width, height) { this.canvas = document.createElement("canvas") this.canvas.id = "snakeCanvas" this.canvas.width = Math.min(width - 40, 400) this.canvas.height = 70 container.appendChild(this.canvas) this.ctx = this.canvas.getContext("2d") this.gridSize = 10 const startX = Math.floor(this.canvas.width / this.gridSize / 2) const startY = Math.floor(this.canvas.height / this.gridSize / 2) this.snake = [{ x: startX, y: startY }] this.dx = 1 this.dy = 0 this.food = this.spawnFood() this.score = 0 this.gameOver = false this.interval = setInterval(() => this.loop(), 120) this.handleKey = this.handleKey.bind(this) document.addEventListener("keydown", this.handleKey) this.canvas.addEventListener("mousedown", (e) => this.handleClick(e)) } spawnFood() { return { x: Math.floor(Math.random() * (this.canvas.width / this.gridSize)), y: Math.floor(Math.random() * (this.canvas.height / this.gridSize)), } } handleKey(e) { if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) { e.preventDefault() } switch (e.key) { case "ArrowUp": if (this.dy === 0) { this.dx = 0 this.dy = -1 } break case "ArrowDown": if (this.dy === 0) { this.dx = 0 this.dy = 1 } break case "ArrowLeft": if (this.dx === 0) { this.dx = -1 this.dy = 0 } break case "ArrowRight": if (this.dx === 0) { this.dx = 1 this.dy = 0 } break } } handleClick(e) { e.stopPropagation() const rect = this.canvas.getBoundingClientRect() const clickX = e.clientX - rect.left const clickY = e.clientY - rect.top const headScreenX = this.snake[0].x * this.gridSize + this.gridSize / 2 const headScreenY = this.snake[0].y * this.gridSize + this.gridSize / 2 const diffX = clickX - headScreenX const diffY = clickY - headScreenY if (Math.abs(diffX) > Math.abs(diffY)) { if (diffX > 0 && this.dx === 0) { this.dx = 1 this.dy = 0 } else if (diffX < 0 && this.dx === 0) { this.dx = -1 this.dy = 0 } } else { if (diffY > 0 && this.dy === 0) { this.dx = 0 this.dy = 1 } else if (diffY < 0 && this.dy === 0) { this.dx = 0 this.dy = -1 } } } loop() { if (this.gameOver) return const head = { x: this.snake[0].x + this.dx, y: this.snake[0].y + this.dy } const cols = Math.floor(this.canvas.width / this.gridSize) const rows = Math.floor(this.canvas.height / this.gridSize) if (head.x < 0) head.x = cols - 1 if (head.x >= cols) head.x = 0 if (head.y < 0) head.y = rows - 1 if (head.y >= rows) head.y = 0 if (this.snake.some((s) => s.x === head.x && s.y === head.y)) { this.snake = [{ x: Math.floor(cols / 2), y: Math.floor(rows / 2) }] this.gameOver = true this.canvas.classList.add("game-over") this.restart(4000) return } this.snake.unshift(head) if (head.x === this.food.x && head.y === this.food.y) { this.score += 10 document.querySelector(".points").innerText = this.score this.food = this.spawnFood() } else { this.snake.pop() } this.draw() } draw() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) this.ctx.fillStyle = "#10b981" this.snake.forEach((s) => { this.ctx.fillRect(s.x * this.gridSize, s.y * this.gridSize, this.gridSize - 1, this.gridSize - 1) }) this.ctx.fillStyle = "#ef4444" this.ctx.fillRect(this.food.x * this.gridSize, this.food.y * this.gridSize, this.gridSize - 1, this.gridSize - 1) } cleanup() { clearInterval(this.interval) this.restart() document.removeEventListener("keydown", this.handleKey) } pause() { clearInterval(this.interval) } resume() { clearInterval(this.interval) this.canvas.classList.remove("game-over") this.interval = setInterval(() => this.loop(), 120) } restart(time) { setTimeout(() => { this.snake = [{ x: Math.floor(this.canvas.width / this.gridSize / 2), y: Math.floor(this.canvas.height / this.gridSize / 2) }] this.dx = 1 this.dy = 0 this.food = this.spawnFood() this.score = 0 document.querySelector(".points").innerText = this.score this.gameOver = false }, time || 0) } } /** * Memory Game Implementation */ class MemoryGame { constructor(container) { this.grid = document.createElement("div") this.grid.className = "memory-grid" container.appendChild(this.grid) const icons = ["🐌", "🦕", "♥️", "⚡", "🌱", "🤖"] this.cards = [...icons, ...icons] this.cards.sort(() => Math.random() - 0.5) this.flipped = [] this.matched = [] this.render() } render() { this.cards.forEach((icon, index) => { const { r, g, b } = getAverageColor(icon) const card = document.createElement("div") card.style.setProperty("--card-color", `rgb(${r},${g},${b})`) // card.style.setProperty("--card-color", `rgba(${r},${g},${b}, 0.5)`) // card.style.setProperty("--card-color", `color(from rgb(${r},${g},${b}) h calc(from rgb(${r},${g},${b}) h + 180)`) card.className = "memory-card" card.dataset.icon = icon card.addEventListener("mousedown", (e) => e.preventDefault()) card.addEventListener("click", (e) => { e.stopPropagation() this.flip(card, index) }) this.grid.appendChild(card) }) } flip(card, index) { if (this.flipped.length >= 2 || this.flipped.includes(index) || this.matched.includes(index)) return card.innerText = this.cards[index] card.classList.add("flipped") this.flipped.push(index) if (this.flipped.length === 2) { this.checkMatch() } } checkMatch() { const [idx1, idx2] = this.flipped const card1 = this.grid.children[idx1] const card2 = this.grid.children[idx2] if (this.cards[idx1] === this.cards[idx2]) { this.matched.push(idx1, idx2) card1.classList.add("matched") card2.classList.add("matched") this.flipped = [] if (this.matched.length === this.cards.length) { setTimeout(() => { this.matched = [] this.flipped = [] this.cards.sort(() => Math.random() - 0.5) this.grid.innerHTML = "" this.render() }, 2000) } } else { setTimeout(() => { card1.classList.remove("flipped") card1.innerText = "" card2.classList.remove("flipped") card2.innerText = "" this.flipped = [] }, 800) } } cleanup() {} pause() {} resume() {} } /** * Simon Game Implementation */ class SimonGame { constructor(container) { this.board = document.createElement("div") this.board.className = "simon-board" container.appendChild(this.board) const colors = ["simon-green", "simon-red", "simon-yellow", "simon-blue"] this.sequence = [] this.playerSequence = [] this.buttons = [] this.score = 0 colors.forEach((color, i) => { const btn = document.createElement("div") btn.className = `simon-btn ${color}` btn.dataset.id = i btn.addEventListener("mousedown", (e) => e.preventDefault()) btn.addEventListener("click", (e) => { e.stopPropagation() this.handleInput(i) }) this.board.appendChild(btn) this.buttons.push(btn) }) this.isActive = false this.timer = setTimeout(() => this.nextRound(), 600) } nextRound() { this.sequence.push(Math.floor(Math.random() * 4)) this.playerSequence = [] this.playSequence() } playSequence() { this.isActive = false let i = 0 this.interval = setInterval(() => { this.flash(this.sequence[i]) i++ if (i >= this.sequence.length) { clearInterval(this.interval) this.isActive = true } }, 800) } flash(btnIndex) { if (!this.buttons[btnIndex]) return const btn = this.buttons[btnIndex] btn.classList.add("lit") setTimeout(() => btn.classList.remove("lit"), 300) } handleInput(index) { if (!this.isActive) return this.flash(index) this.playerSequence.push(index) const currentStep = this.playerSequence.length - 1 if (this.playerSequence[currentStep] !== this.sequence[currentStep]) { // Fail this.sequence = [] this.isActive = false this.board.classList.add("game-over") setTimeout(() => { this.score = 0 document.querySelector(".points").innerText = this.score this.board.classList.remove("game-over") this.nextRound() }, 4000) return } if (this.playerSequence.length === this.sequence.length) { this.isActive = false this.score += 10 document.querySelector(".points").innerText = this.score setTimeout(() => this.nextRound(), 1000) } } cleanup() { clearTimeout(this.timer) this.score = 0 document.querySelector(".points").innerText = this.score this.board.classList.remove("game-over") if (this.interval) clearInterval(this.interval) } pause() { this.paused = true clearTimeout(this.timer) if (this.interval) clearInterval(this.interval) this.buttons.forEach((b) => b.classList.remove("lit")) } resume() { if (!this.paused) return this.paused = false this.playSequence() } } // Initialize const initInterval = setInterval(() => { if (document.querySelector("#gameBtn")) { console.log("Game Button found!") const btn = new GameButton("gameBtn") clearInterval(initInterval) } }, 250) const getAverageColor = (emoji) => { const canvas = document.createElement("canvas") const ctx = canvas.getContext("2d") const size = 30 canvas.width = size canvas.height = size ctx.textBaseline = "middle" ctx.textAlign = "center" ctx.font = `${size - 4}px sans-serif` ctx.fillText(emoji, size / 2, size / 2) const data = ctx.getImageData(0, 0, size, size).data let r = 0, g = 0, b = 0, count = 0 for (let i = 0; i < data.length; i += 4) { const alpha = data[i + 3] if (alpha > 50) { r += data[i] g += data[i + 1] b += data[i + 2] count++ } } if (count > 0) { r = Math.floor(r / count) g = Math.floor(g / count) b = Math.floor(b / count) } else { r = 255 g = 255 b = 255 } return { r, g, b } }