Sebastiankay commited on
Commit
5412de8
·
verified ·
1 Parent(s): 4eb5ffd

Update _res/miniGameButton.js

Browse files
Files changed (1) hide show
  1. _res/miniGameButton.js +689 -689
_res/miniGameButton.js CHANGED
@@ -1,689 +1,689 @@
1
- /**
2
- * Interactive Mini-Game Button
3
- */
4
-
5
- class GameButton {
6
- constructor(buttonId) {
7
- this.button = document.getElementById(buttonId)
8
- this.runButton = document.getElementById("runBtn")
9
- if (!this.button) {
10
- console.error("GameButton: Target button not found")
11
- return
12
- }
13
-
14
- // --- Injection Logic ---
15
- this.button.classList.add("game-btn") // Add our styling class
16
-
17
- // Wrap existing text to animate it
18
- if (!this.button.querySelector(".btn-text")) {
19
- const textSpan = document.createElement("span")
20
- textSpan.className = "btn-text"
21
- while (this.button.firstChild) {
22
- textSpan.appendChild(this.button.firstChild)
23
- }
24
- this.button.appendChild(textSpan)
25
- }
26
-
27
- // Create Game Container
28
- if (!this.button.querySelector(".game-container")) {
29
- this.gameContainer = document.createElement("div")
30
- this.gameContainer.className = "game-container"
31
- this.gameContainer.id = "gameContainer"
32
- this.button.appendChild(this.gameContainer)
33
- } else {
34
- this.gameContainer = this.button.querySelector(".game-container")
35
- }
36
-
37
- // Create Progress Bar
38
- if (!this.button.querySelector(".game-button-progress-bar")) {
39
- const progBar = document.createElement("div")
40
- progBar.className = "game-button-progress-bar"
41
- this.progressFill = document.createElement("div")
42
- this.progressFill.className = "progress-fill"
43
- this.progressFill.id = "progressFill"
44
- progBar.appendChild(this.progressFill)
45
- this.button.appendChild(progBar)
46
- } else {
47
- this.progressFill = this.button.querySelector(".progress-fill")
48
- }
49
-
50
- // Create Completion Controls
51
- if (!this.button.querySelector(".completion-controls")) {
52
- const controls = document.createElement("div")
53
- controls.className = "completion-controls"
54
-
55
- this.compText = document.createElement("span")
56
- this.compText.className = "completion-text"
57
- this.compText.innerText = "Fertig!"
58
- controls.appendChild(this.compText)
59
-
60
- this.closeBtn = document.createElement("button")
61
- this.closeBtn.className = "close-game-btn"
62
- this.closeBtn.title = "Close Game"
63
- // Use innerHTML for SVG icon
64
- this.closeBtn.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>'
65
-
66
- // Prevent close button click from triggering main button
67
- this.closeBtn.addEventListener("click", (e) => {
68
- e.stopPropagation()
69
- this.reset()
70
- })
71
-
72
- controls.appendChild(this.closeBtn)
73
- this.button.appendChild(controls)
74
- } else {
75
- this.closeBtn = this.button.querySelector(".close-game-btn")
76
- this.compText = this.button.querySelector(".completion-text")
77
- this.closeBtn.addEventListener("click", (e) => {
78
- e.stopPropagation()
79
- this.reset()
80
- })
81
- }
82
-
83
- if (!this.button.querySelector(".points-wrapper")) {
84
- const pointsWrapper = document.createElement("div")
85
- pointsWrapper.className = "points-wrapper"
86
-
87
- const pointsTextWrapper = document.createElement("span")
88
- pointsTextWrapper.className = "points-text-wrapper"
89
-
90
- const pointsText = document.createElement("span")
91
- pointsText.className = "points-text"
92
- pointsText.innerText = "p"
93
-
94
- this.points = document.createElement("span")
95
- this.points.className = "points"
96
- this.points.innerText = "0"
97
-
98
- pointsTextWrapper.appendChild(this.points)
99
- pointsTextWrapper.appendChild(pointsText)
100
- pointsWrapper.appendChild(pointsTextWrapper)
101
- this.button.appendChild(pointsWrapper)
102
- } else {
103
- this.points = this.button.querySelector(".points")
104
- }
105
- // -----------------------
106
-
107
- this.durationInput = document.getElementById("durationInput")
108
-
109
- this.state = "IDLE"
110
- this.processDuration = 5000
111
- this.startTime = 0
112
- this.rafId = null
113
- this.gamePoints = 0
114
-
115
- this.games = ["snake", "memory", "simon"]
116
- this.currentGame = null
117
-
118
- this.button.addEventListener("click", (e) => {
119
- if (this.button.classList.contains("active")) e.preventDefault()
120
- if (this.state === "IDLE") this.activate()
121
- })
122
-
123
- // Pause on blur
124
- this.button.addEventListener("blur", () => {
125
- // if (this.currentGame && (this.state === "ACTIVE" || this.state === "COMPLETE")) {
126
- // this.currentGame.pause()
127
- // this.button.classList.add("paused")
128
- // }
129
- })
130
-
131
- // Resume on focus
132
- this.button.addEventListener("focus", () => {
133
- if (this.currentGame && (this.state === "ACTIVE" || this.state === "COMPLETE")) {
134
- this.currentGame.resume()
135
- this.button.classList.remove("paused")
136
- }
137
- })
138
-
139
- this.button.addEventListener("focusout", (e) => {
140
- if (this.currentGame && (this.state === "ACTIVE" || this.state === "COMPLETE")) {
141
- if (e.currentTarget.contains(e.relatedTarget)) {
142
- /* Focus will still be within the container */
143
- this.currentGame.resume()
144
- this.button.classList.remove("paused")
145
- } else {
146
- /* Focus will leave the container */
147
- this.currentGame.pause()
148
- this.button.classList.add("paused")
149
- }
150
- }
151
- })
152
- }
153
-
154
- activate() {
155
- if (this.state !== "IDLE") return
156
-
157
- this.runButton.click()
158
-
159
- // Get duration from input if present
160
- if (this.durationInput) {
161
- const val = parseInt(this.durationInput.value)
162
- if (val && val > 0) this.processDuration = val
163
- }
164
-
165
- this.state = "ACTIVE"
166
- this.button.classList.add("active", "process-running")
167
- this.button.classList.remove("complete")
168
-
169
- // Start the fake process
170
- this.startTime = Date.now()
171
- this.progressFill.style.width = "0%"
172
- setTimeout(() => this.updateProgress(), 200)
173
-
174
- // Pick a random game
175
- setTimeout(() => {
176
- if (this.state === "ACTIVE") {
177
- this.launchRandomGame()
178
- }
179
- }, 500)
180
- }
181
-
182
- updateProgress() {
183
- if (this.state === "IDLE") return
184
-
185
- const elapsed = Date.now() - this.startTime
186
- const firstActiveProgressBar = document.querySelectorAll("div.progress-bar")[0]
187
- const progressError = document.querySelectorAll('div[data-testid="status-tracker"] span.error')[0]
188
-
189
- if (firstActiveProgressBar) {
190
- const progress = firstActiveProgressBar.style.width
191
- this.progressFill.style.width = `${progress}`
192
- }
193
-
194
- // if (progress >= 100 && this.state !== "COMPLETE") {
195
- // this.complete()
196
- // }
197
-
198
- if (progressError) {
199
- console.log("complete error")
200
- this.completeError()
201
- }
202
-
203
- if (this.button.classList.contains("process-running") === false && this.state !== "COMPLETE") {
204
- console.log("complete")
205
- this.progressFill.style.width = "100%"
206
- this.complete()
207
- }
208
-
209
- if (this.state !== "IDLE") {
210
- if (this.button.classList.contains("process-running")) {
211
- this.rafId = requestAnimationFrame(() => this.updateProgress())
212
- }
213
- }
214
- }
215
-
216
- completeError() {
217
- this.state = "COMPLETE"
218
- this.compText.innerText = "ERROR!"
219
- this.button.classList.add("complete", "error")
220
- this.button.classList.remove("process-running")
221
- // Game continues running! No cleanup here.
222
- }
223
-
224
- complete() {
225
- this.state = "COMPLETE"
226
- this.button.classList.add("complete")
227
- // Game continues running! No cleanup here.
228
- }
229
-
230
- reset() {
231
- this.state = "IDLE"
232
- this.button.classList.remove("active")
233
- this.button.classList.remove("complete", "error")
234
- this.compText.innerText = "Fertig!"
235
- this.progressFill.style.width = "0%"
236
-
237
- if (this.currentGame) {
238
- this.currentGame.cleanup()
239
- this.currentGame = null
240
- }
241
- this.gameContainer.innerHTML = ""
242
-
243
- // Remove paused class if present
244
- this.button.classList.remove("paused")
245
- }
246
-
247
- launchRandomGame() {
248
- // Simple random
249
- // const gameType = this.games[Math.floor(Math.random() * this.games.length)]
250
- const gameType = "snake"
251
- this.gameContainer.innerHTML = ""
252
-
253
- const w = this.button.offsetWidth
254
- const h = this.button.offsetHeight
255
-
256
- if (gameType === "snake") {
257
- this.currentGame = new SnakeGame(this.gameContainer, w, h)
258
- } else if (gameType === "memory") {
259
- this.currentGame = new MemoryGame(this.gameContainer)
260
- } else if (gameType === "simon") {
261
- this.currentGame = new SimonGame(this.gameContainer)
262
- }
263
- }
264
- }
265
-
266
- /**
267
- * Snake Game Implementation
268
- */
269
- class SnakeGame {
270
- constructor(container, width, height) {
271
- this.canvas = document.createElement("canvas")
272
- this.canvas.id = "snakeCanvas"
273
- this.canvas.width = Math.min(width - 40, 400)
274
- this.canvas.height = 70
275
-
276
- container.appendChild(this.canvas)
277
-
278
- this.ctx = this.canvas.getContext("2d")
279
- this.gridSize = 10
280
-
281
- const startX = Math.floor(this.canvas.width / this.gridSize / 2)
282
- const startY = Math.floor(this.canvas.height / this.gridSize / 2)
283
-
284
- this.snake = [{ x: startX, y: startY }]
285
- this.dx = 1
286
- this.dy = 0
287
- this.food = this.spawnFood()
288
- this.score = 0
289
- this.gameOver = false
290
-
291
- this.interval = setInterval(() => this.loop(), 120)
292
-
293
- this.handleKey = this.handleKey.bind(this)
294
- document.addEventListener("keydown", this.handleKey)
295
- this.canvas.addEventListener("mousedown", (e) => this.handleClick(e))
296
- }
297
-
298
- spawnFood() {
299
- return {
300
- x: Math.floor(Math.random() * (this.canvas.width / this.gridSize)),
301
- y: Math.floor(Math.random() * (this.canvas.height / this.gridSize)),
302
- }
303
- }
304
-
305
- handleKey(e) {
306
- if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
307
- e.preventDefault()
308
- }
309
-
310
- switch (e.key) {
311
- case "ArrowUp":
312
- if (this.dy === 0) {
313
- this.dx = 0
314
- this.dy = -1
315
- }
316
- break
317
- case "ArrowDown":
318
- if (this.dy === 0) {
319
- this.dx = 0
320
- this.dy = 1
321
- }
322
- break
323
- case "ArrowLeft":
324
- if (this.dx === 0) {
325
- this.dx = -1
326
- this.dy = 0
327
- }
328
- break
329
- case "ArrowRight":
330
- if (this.dx === 0) {
331
- this.dx = 1
332
- this.dy = 0
333
- }
334
- break
335
- }
336
- }
337
-
338
- handleClick(e) {
339
- e.stopPropagation()
340
- const rect = this.canvas.getBoundingClientRect()
341
- const clickX = e.clientX - rect.left
342
- const clickY = e.clientY - rect.top
343
-
344
- const headScreenX = this.snake[0].x * this.gridSize + this.gridSize / 2
345
- const headScreenY = this.snake[0].y * this.gridSize + this.gridSize / 2
346
-
347
- const diffX = clickX - headScreenX
348
- const diffY = clickY - headScreenY
349
-
350
- if (Math.abs(diffX) > Math.abs(diffY)) {
351
- if (diffX > 0 && this.dx === 0) {
352
- this.dx = 1
353
- this.dy = 0
354
- } else if (diffX < 0 && this.dx === 0) {
355
- this.dx = -1
356
- this.dy = 0
357
- }
358
- } else {
359
- if (diffY > 0 && this.dy === 0) {
360
- this.dx = 0
361
- this.dy = 1
362
- } else if (diffY < 0 && this.dy === 0) {
363
- this.dx = 0
364
- this.dy = -1
365
- }
366
- }
367
- }
368
-
369
- loop() {
370
- if (this.gameOver) return
371
-
372
- const head = { x: this.snake[0].x + this.dx, y: this.snake[0].y + this.dy }
373
- const cols = Math.floor(this.canvas.width / this.gridSize)
374
- const rows = Math.floor(this.canvas.height / this.gridSize)
375
-
376
- if (head.x < 0) head.x = cols - 1
377
- if (head.x >= cols) head.x = 0
378
- if (head.y < 0) head.y = rows - 1
379
- if (head.y >= rows) head.y = 0
380
-
381
- if (this.snake.some((s) => s.x === head.x && s.y === head.y)) {
382
- this.snake = [{ x: Math.floor(cols / 2), y: Math.floor(rows / 2) }]
383
- this.gameOver = true
384
- this.canvas.classList.add("game-over")
385
- this.restart(4000)
386
- return
387
- }
388
-
389
- this.snake.unshift(head)
390
-
391
- if (head.x === this.food.x && head.y === this.food.y) {
392
- this.score += 10
393
- document.querySelector(".points").innerText = this.score
394
- this.food = this.spawnFood()
395
- } else {
396
- this.snake.pop()
397
- }
398
-
399
- this.draw()
400
- }
401
-
402
- draw() {
403
- this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
404
-
405
- this.ctx.fillStyle = "#10b981"
406
- this.snake.forEach((s) => {
407
- this.ctx.fillRect(s.x * this.gridSize, s.y * this.gridSize, this.gridSize - 1, this.gridSize - 1)
408
- })
409
-
410
- this.ctx.fillStyle = "#ef4444"
411
- this.ctx.fillRect(this.food.x * this.gridSize, this.food.y * this.gridSize, this.gridSize - 1, this.gridSize - 1)
412
- }
413
-
414
- cleanup() {
415
- clearInterval(this.interval)
416
- this.restart()
417
- document.removeEventListener("keydown", this.handleKey)
418
- }
419
-
420
- pause() {
421
- clearInterval(this.interval)
422
- }
423
-
424
- resume() {
425
- clearInterval(this.interval)
426
- this.canvas.classList.remove("game-over")
427
- this.interval = setInterval(() => this.loop(), 120)
428
- }
429
-
430
- restart(time) {
431
- setTimeout(() => {
432
- this.snake = [{ x: Math.floor(this.canvas.width / this.gridSize / 2), y: Math.floor(this.canvas.height / this.gridSize / 2) }]
433
- this.dx = 1
434
- this.dy = 0
435
- this.food = this.spawnFood()
436
- this.score = 0
437
- document.querySelector(".points").innerText = this.score
438
- this.gameOver = false
439
- }, time || 0)
440
- }
441
- }
442
-
443
- /**
444
- * Memory Game Implementation
445
- */
446
- class MemoryGame {
447
- constructor(container) {
448
- this.grid = document.createElement("div")
449
- this.grid.className = "memory-grid"
450
- container.appendChild(this.grid)
451
-
452
- const icons = ["🐌", "🦕", "♥️", "⚡", "🌱", "🤖"]
453
- this.cards = [...icons, ...icons]
454
- this.cards.sort(() => Math.random() - 0.5)
455
-
456
- this.flipped = []
457
- this.matched = []
458
- this.render()
459
- }
460
-
461
- render() {
462
- this.cards.forEach((icon, index) => {
463
- const { r, g, b } = getAverageColor(icon)
464
- const card = document.createElement("div")
465
- card.style.setProperty("--card-color", `rgb(${r},${g},${b})`)
466
- // card.style.setProperty("--card-color", `rgba(${r},${g},${b}, 0.5)`)
467
- // card.style.setProperty("--card-color", `color(from rgb(${r},${g},${b}) h calc(from rgb(${r},${g},${b}) h + 180)`)
468
-
469
- card.className = "memory-card"
470
- card.dataset.icon = icon
471
-
472
- card.addEventListener("mousedown", (e) => e.preventDefault())
473
- card.addEventListener("click", (e) => {
474
- e.stopPropagation()
475
- this.flip(card, index)
476
- })
477
- this.grid.appendChild(card)
478
- })
479
- }
480
-
481
- flip(card, index) {
482
- if (this.flipped.length >= 2 || this.flipped.includes(index) || this.matched.includes(index)) return
483
-
484
- card.innerText = this.cards[index]
485
- card.classList.add("flipped")
486
- this.flipped.push(index)
487
-
488
- if (this.flipped.length === 2) {
489
- this.checkMatch()
490
- }
491
- }
492
-
493
- checkMatch() {
494
- const [idx1, idx2] = this.flipped
495
- const card1 = this.grid.children[idx1]
496
- const card2 = this.grid.children[idx2]
497
-
498
- if (this.cards[idx1] === this.cards[idx2]) {
499
- this.matched.push(idx1, idx2)
500
- card1.classList.add("matched")
501
- card2.classList.add("matched")
502
- this.flipped = []
503
-
504
- if (this.matched.length === this.cards.length) {
505
- setTimeout(() => {
506
- this.matched = []
507
- this.flipped = []
508
- this.cards.sort(() => Math.random() - 0.5)
509
- this.grid.innerHTML = ""
510
- this.render()
511
- }, 2000)
512
- }
513
- } else {
514
- setTimeout(() => {
515
- card1.classList.remove("flipped")
516
- card1.innerText = ""
517
- card2.classList.remove("flipped")
518
- card2.innerText = ""
519
- this.flipped = []
520
- }, 800)
521
- }
522
- }
523
-
524
- cleanup() {}
525
- pause() {}
526
- resume() {}
527
- }
528
-
529
- /**
530
- * Simon Game Implementation
531
- */
532
- class SimonGame {
533
- constructor(container) {
534
- this.board = document.createElement("div")
535
- this.board.className = "simon-board"
536
- container.appendChild(this.board)
537
-
538
- const colors = ["simon-green", "simon-red", "simon-yellow", "simon-blue"]
539
- this.sequence = []
540
- this.playerSequence = []
541
- this.buttons = []
542
- this.score = 0
543
-
544
- colors.forEach((color, i) => {
545
- const btn = document.createElement("div")
546
- btn.className = `simon-btn ${color}`
547
- btn.dataset.id = i
548
- btn.addEventListener("mousedown", (e) => e.preventDefault())
549
- btn.addEventListener("click", (e) => {
550
- e.stopPropagation()
551
- this.handleInput(i)
552
- })
553
- this.board.appendChild(btn)
554
- this.buttons.push(btn)
555
- })
556
-
557
- this.isActive = false
558
- this.timer = setTimeout(() => this.nextRound(), 600)
559
- }
560
-
561
- nextRound() {
562
- this.sequence.push(Math.floor(Math.random() * 4))
563
- this.playerSequence = []
564
- this.playSequence()
565
- }
566
-
567
- playSequence() {
568
- this.isActive = false
569
- let i = 0
570
-
571
- this.interval = setInterval(() => {
572
- this.flash(this.sequence[i])
573
- i++
574
- if (i >= this.sequence.length) {
575
- clearInterval(this.interval)
576
- this.isActive = true
577
- }
578
- }, 800)
579
- }
580
-
581
- flash(btnIndex) {
582
- if (!this.buttons[btnIndex]) return
583
- const btn = this.buttons[btnIndex]
584
- btn.classList.add("lit")
585
- setTimeout(() => btn.classList.remove("lit"), 300)
586
- }
587
-
588
- handleInput(index) {
589
- if (!this.isActive) return
590
-
591
- this.flash(index)
592
- this.playerSequence.push(index)
593
-
594
- const currentStep = this.playerSequence.length - 1
595
- if (this.playerSequence[currentStep] !== this.sequence[currentStep]) {
596
- // Fail
597
- this.sequence = []
598
- this.isActive = false
599
- this.board.classList.add("game-over")
600
-
601
- setTimeout(() => {
602
- this.score = 0
603
- document.querySelector(".points").innerText = this.score
604
- this.board.classList.remove("game-over")
605
- this.nextRound()
606
- }, 4000)
607
- return
608
- }
609
-
610
- if (this.playerSequence.length === this.sequence.length) {
611
- this.isActive = false
612
- this.score += 10
613
- document.querySelector(".points").innerText = this.score
614
- setTimeout(() => this.nextRound(), 1000)
615
- }
616
- }
617
-
618
- cleanup() {
619
- clearTimeout(this.timer)
620
- this.score = 0
621
- document.querySelector(".points").innerText = this.score
622
- this.board.classList.remove("game-over")
623
- if (this.interval) clearInterval(this.interval)
624
- }
625
-
626
- pause() {
627
- this.paused = true
628
- clearTimeout(this.timer)
629
- if (this.interval) clearInterval(this.interval)
630
- this.buttons.forEach((b) => b.classList.remove("lit"))
631
- }
632
-
633
- resume() {
634
- if (!this.paused) return
635
- this.paused = false
636
- this.playSequence()
637
- }
638
- }
639
-
640
- // Initialize
641
- const initInterval = setInterval(() => {
642
- if (document.querySelector("#gameBtn")) {
643
- console.log("Game Button found!")
644
- const btn = new GameButton("gameBtn")
645
- clearInterval(initInterval)
646
- }
647
- }, 250)
648
-
649
- const getAverageColor = (emoji) => {
650
- const canvas = document.createElement("canvas")
651
- const ctx = canvas.getContext("2d")
652
- const size = 30
653
-
654
- canvas.width = size
655
- canvas.height = size
656
-
657
- ctx.textBaseline = "middle"
658
- ctx.textAlign = "center"
659
- ctx.font = `${size - 4}px sans-serif`
660
- ctx.fillText(emoji, size / 2, size / 2)
661
-
662
- const data = ctx.getImageData(0, 0, size, size).data
663
- let r = 0,
664
- g = 0,
665
- b = 0,
666
- count = 0
667
-
668
- for (let i = 0; i < data.length; i += 4) {
669
- const alpha = data[i + 3]
670
- if (alpha > 50) {
671
- r += data[i]
672
- g += data[i + 1]
673
- b += data[i + 2]
674
- count++
675
- }
676
- }
677
-
678
- if (count > 0) {
679
- r = Math.floor(r / count)
680
- g = Math.floor(g / count)
681
- b = Math.floor(b / count)
682
- } else {
683
- r = 255
684
- g = 255
685
- b = 255
686
- }
687
-
688
- return { r, g, b }
689
- }
 
1
+ /**
2
+ * Interactive Mini-Game Button
3
+ */
4
+
5
+ class GameButton {
6
+ constructor(buttonId) {
7
+ this.button = document.getElementById(buttonId)
8
+ this.runButton = document.getElementById("runBtn")
9
+ if (!this.button) {
10
+ console.error("GameButton: Target button not found")
11
+ return
12
+ }
13
+
14
+ // --- Injection Logic ---
15
+ this.button.classList.add("game-btn") // Add our styling class
16
+
17
+ // Wrap existing text to animate it
18
+ if (!this.button.querySelector(".btn-text")) {
19
+ const textSpan = document.createElement("span")
20
+ textSpan.className = "btn-text"
21
+ while (this.button.firstChild) {
22
+ textSpan.appendChild(this.button.firstChild)
23
+ }
24
+ this.button.appendChild(textSpan)
25
+ }
26
+
27
+ // Create Game Container
28
+ if (!this.button.querySelector(".game-container")) {
29
+ this.gameContainer = document.createElement("div")
30
+ this.gameContainer.className = "game-container"
31
+ this.gameContainer.id = "gameContainer"
32
+ this.button.appendChild(this.gameContainer)
33
+ } else {
34
+ this.gameContainer = this.button.querySelector(".game-container")
35
+ }
36
+
37
+ // Create Progress Bar
38
+ if (!this.button.querySelector(".game-button-progress-bar")) {
39
+ const progBar = document.createElement("div")
40
+ progBar.className = "game-button-progress-bar"
41
+ this.progressFill = document.createElement("div")
42
+ this.progressFill.className = "progress-fill"
43
+ this.progressFill.id = "progressFill"
44
+ progBar.appendChild(this.progressFill)
45
+ this.button.appendChild(progBar)
46
+ } else {
47
+ this.progressFill = this.button.querySelector(".progress-fill")
48
+ }
49
+
50
+ // Create Completion Controls
51
+ if (!this.button.querySelector(".completion-controls")) {
52
+ const controls = document.createElement("div")
53
+ controls.className = "completion-controls"
54
+
55
+ this.compText = document.createElement("span")
56
+ this.compText.className = "completion-text"
57
+ this.compText.innerText = "Fertig!"
58
+ controls.appendChild(this.compText)
59
+
60
+ this.closeBtn = document.createElement("button")
61
+ this.closeBtn.className = "close-game-btn"
62
+ this.closeBtn.title = "Close Game"
63
+ // Use innerHTML for SVG icon
64
+ this.closeBtn.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>'
65
+
66
+ // Prevent close button click from triggering main button
67
+ this.closeBtn.addEventListener("click", (e) => {
68
+ e.stopPropagation()
69
+ this.reset()
70
+ })
71
+
72
+ controls.appendChild(this.closeBtn)
73
+ this.button.appendChild(controls)
74
+ } else {
75
+ this.closeBtn = this.button.querySelector(".close-game-btn")
76
+ this.compText = this.button.querySelector(".completion-text")
77
+ this.closeBtn.addEventListener("click", (e) => {
78
+ e.stopPropagation()
79
+ this.reset()
80
+ })
81
+ }
82
+
83
+ if (!this.button.querySelector(".points-wrapper")) {
84
+ const pointsWrapper = document.createElement("div")
85
+ pointsWrapper.className = "points-wrapper"
86
+
87
+ const pointsTextWrapper = document.createElement("span")
88
+ pointsTextWrapper.className = "points-text-wrapper"
89
+
90
+ const pointsText = document.createElement("span")
91
+ pointsText.className = "points-text"
92
+ pointsText.innerText = "p"
93
+
94
+ this.points = document.createElement("span")
95
+ this.points.className = "points"
96
+ this.points.innerText = "0"
97
+
98
+ pointsTextWrapper.appendChild(this.points)
99
+ pointsTextWrapper.appendChild(pointsText)
100
+ pointsWrapper.appendChild(pointsTextWrapper)
101
+ this.button.appendChild(pointsWrapper)
102
+ } else {
103
+ this.points = this.button.querySelector(".points")
104
+ }
105
+ // -----------------------
106
+
107
+ this.durationInput = document.getElementById("durationInput")
108
+
109
+ this.state = "IDLE"
110
+ this.processDuration = 5000
111
+ this.startTime = 0
112
+ this.rafId = null
113
+ this.gamePoints = 0
114
+
115
+ this.games = ["snake", "memory", "simon"]
116
+ this.currentGame = null
117
+
118
+ this.button.addEventListener("click", (e) => {
119
+ if (this.button.classList.contains("active")) e.preventDefault()
120
+ if (this.state === "IDLE") this.activate()
121
+ })
122
+
123
+ // Pause on blur
124
+ this.button.addEventListener("blur", () => {
125
+ // if (this.currentGame && (this.state === "ACTIVE" || this.state === "COMPLETE")) {
126
+ // this.currentGame.pause()
127
+ // this.button.classList.add("paused")
128
+ // }
129
+ })
130
+
131
+ // Resume on focus
132
+ this.button.addEventListener("focus", () => {
133
+ if (this.currentGame && (this.state === "ACTIVE" || this.state === "COMPLETE")) {
134
+ this.currentGame.resume()
135
+ this.button.classList.remove("paused")
136
+ }
137
+ })
138
+
139
+ this.button.addEventListener("focusout", (e) => {
140
+ if (this.currentGame && (this.state === "ACTIVE" || this.state === "COMPLETE")) {
141
+ if (e.currentTarget.contains(e.relatedTarget)) {
142
+ /* Focus will still be within the container */
143
+ this.currentGame.resume()
144
+ this.button.classList.remove("paused")
145
+ } else {
146
+ /* Focus will leave the container */
147
+ this.currentGame.pause()
148
+ this.button.classList.add("paused")
149
+ }
150
+ }
151
+ })
152
+ }
153
+
154
+ activate() {
155
+ if (this.state !== "IDLE") return
156
+
157
+ this.runButton.click()
158
+
159
+ // Get duration from input if present
160
+ if (this.durationInput) {
161
+ const val = parseInt(this.durationInput.value)
162
+ if (val && val > 0) this.processDuration = val
163
+ }
164
+
165
+ this.state = "ACTIVE"
166
+ this.button.classList.add("active", "process-running")
167
+ this.button.classList.remove("complete")
168
+
169
+ // Start the fake process
170
+ this.startTime = Date.now()
171
+ this.progressFill.style.width = "0%"
172
+ setTimeout(() => this.updateProgress(), 200)
173
+
174
+ // Pick a random game
175
+ setTimeout(() => {
176
+ if (this.state === "ACTIVE") {
177
+ this.launchRandomGame()
178
+ }
179
+ }, 500)
180
+ }
181
+
182
+ updateProgress() {
183
+ if (this.state === "IDLE") return
184
+
185
+ const elapsed = Date.now() - this.startTime
186
+ const firstActiveProgressBar = document.querySelectorAll("div.progress-bar")[0]
187
+ const progressError = document.querySelectorAll('div[data-testid="status-tracker"] span.error')[0]
188
+
189
+ if (firstActiveProgressBar) {
190
+ const progress = firstActiveProgressBar.style.width
191
+ this.progressFill.style.width = `${progress}`
192
+ }
193
+
194
+ // if (progress >= 100 && this.state !== "COMPLETE") {
195
+ // this.complete()
196
+ // }
197
+
198
+ if (progressError) {
199
+ console.log("complete error")
200
+ this.completeError()
201
+ }
202
+
203
+ if (this.button.classList.contains("process-running") === false && this.state !== "COMPLETE") {
204
+ console.log("complete")
205
+ this.progressFill.style.width = "100%"
206
+ this.complete()
207
+ }
208
+
209
+ if (this.state !== "IDLE") {
210
+ if (this.button.classList.contains("process-running")) {
211
+ this.rafId = requestAnimationFrame(() => this.updateProgress())
212
+ }
213
+ }
214
+ }
215
+
216
+ completeError() {
217
+ this.state = "COMPLETE"
218
+ this.compText.innerText = "ERROR!"
219
+ this.button.classList.add("complete", "error")
220
+ this.button.classList.remove("process-running")
221
+ // Game continues running! No cleanup here.
222
+ }
223
+
224
+ complete() {
225
+ this.state = "COMPLETE"
226
+ this.button.classList.add("complete")
227
+ // Game continues running! No cleanup here.
228
+ }
229
+
230
+ reset() {
231
+ this.state = "IDLE"
232
+ this.button.classList.remove("active")
233
+ this.button.classList.remove("complete", "error")
234
+ this.compText.innerText = "Fertig!"
235
+ this.progressFill.style.width = "0%"
236
+
237
+ if (this.currentGame) {
238
+ this.currentGame.cleanup()
239
+ this.currentGame = null
240
+ }
241
+ this.gameContainer.innerHTML = ""
242
+
243
+ // Remove paused class if present
244
+ this.button.classList.remove("paused")
245
+ }
246
+
247
+ launchRandomGame() {
248
+ // Simple random
249
+ const gameType = this.games[Math.floor(Math.random() * this.games.length)]
250
+ // const gameType = "snake"
251
+ this.gameContainer.innerHTML = ""
252
+
253
+ const w = this.button.offsetWidth
254
+ const h = this.button.offsetHeight
255
+
256
+ if (gameType === "snake") {
257
+ this.currentGame = new SnakeGame(this.gameContainer, w, h)
258
+ } else if (gameType === "memory") {
259
+ this.currentGame = new MemoryGame(this.gameContainer)
260
+ } else if (gameType === "simon") {
261
+ this.currentGame = new SimonGame(this.gameContainer)
262
+ }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Snake Game Implementation
268
+ */
269
+ class SnakeGame {
270
+ constructor(container, width, height) {
271
+ this.canvas = document.createElement("canvas")
272
+ this.canvas.id = "snakeCanvas"
273
+ this.canvas.width = Math.min(width - 40, 400)
274
+ this.canvas.height = 70
275
+
276
+ container.appendChild(this.canvas)
277
+
278
+ this.ctx = this.canvas.getContext("2d")
279
+ this.gridSize = 10
280
+
281
+ const startX = Math.floor(this.canvas.width / this.gridSize / 2)
282
+ const startY = Math.floor(this.canvas.height / this.gridSize / 2)
283
+
284
+ this.snake = [{ x: startX, y: startY }]
285
+ this.dx = 1
286
+ this.dy = 0
287
+ this.food = this.spawnFood()
288
+ this.score = 0
289
+ this.gameOver = false
290
+
291
+ this.interval = setInterval(() => this.loop(), 120)
292
+
293
+ this.handleKey = this.handleKey.bind(this)
294
+ document.addEventListener("keydown", this.handleKey)
295
+ this.canvas.addEventListener("mousedown", (e) => this.handleClick(e))
296
+ }
297
+
298
+ spawnFood() {
299
+ return {
300
+ x: Math.floor(Math.random() * (this.canvas.width / this.gridSize)),
301
+ y: Math.floor(Math.random() * (this.canvas.height / this.gridSize)),
302
+ }
303
+ }
304
+
305
+ handleKey(e) {
306
+ if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
307
+ e.preventDefault()
308
+ }
309
+
310
+ switch (e.key) {
311
+ case "ArrowUp":
312
+ if (this.dy === 0) {
313
+ this.dx = 0
314
+ this.dy = -1
315
+ }
316
+ break
317
+ case "ArrowDown":
318
+ if (this.dy === 0) {
319
+ this.dx = 0
320
+ this.dy = 1
321
+ }
322
+ break
323
+ case "ArrowLeft":
324
+ if (this.dx === 0) {
325
+ this.dx = -1
326
+ this.dy = 0
327
+ }
328
+ break
329
+ case "ArrowRight":
330
+ if (this.dx === 0) {
331
+ this.dx = 1
332
+ this.dy = 0
333
+ }
334
+ break
335
+ }
336
+ }
337
+
338
+ handleClick(e) {
339
+ e.stopPropagation()
340
+ const rect = this.canvas.getBoundingClientRect()
341
+ const clickX = e.clientX - rect.left
342
+ const clickY = e.clientY - rect.top
343
+
344
+ const headScreenX = this.snake[0].x * this.gridSize + this.gridSize / 2
345
+ const headScreenY = this.snake[0].y * this.gridSize + this.gridSize / 2
346
+
347
+ const diffX = clickX - headScreenX
348
+ const diffY = clickY - headScreenY
349
+
350
+ if (Math.abs(diffX) > Math.abs(diffY)) {
351
+ if (diffX > 0 && this.dx === 0) {
352
+ this.dx = 1
353
+ this.dy = 0
354
+ } else if (diffX < 0 && this.dx === 0) {
355
+ this.dx = -1
356
+ this.dy = 0
357
+ }
358
+ } else {
359
+ if (diffY > 0 && this.dy === 0) {
360
+ this.dx = 0
361
+ this.dy = 1
362
+ } else if (diffY < 0 && this.dy === 0) {
363
+ this.dx = 0
364
+ this.dy = -1
365
+ }
366
+ }
367
+ }
368
+
369
+ loop() {
370
+ if (this.gameOver) return
371
+
372
+ const head = { x: this.snake[0].x + this.dx, y: this.snake[0].y + this.dy }
373
+ const cols = Math.floor(this.canvas.width / this.gridSize)
374
+ const rows = Math.floor(this.canvas.height / this.gridSize)
375
+
376
+ if (head.x < 0) head.x = cols - 1
377
+ if (head.x >= cols) head.x = 0
378
+ if (head.y < 0) head.y = rows - 1
379
+ if (head.y >= rows) head.y = 0
380
+
381
+ if (this.snake.some((s) => s.x === head.x && s.y === head.y)) {
382
+ this.snake = [{ x: Math.floor(cols / 2), y: Math.floor(rows / 2) }]
383
+ this.gameOver = true
384
+ this.canvas.classList.add("game-over")
385
+ this.restart(4000)
386
+ return
387
+ }
388
+
389
+ this.snake.unshift(head)
390
+
391
+ if (head.x === this.food.x && head.y === this.food.y) {
392
+ this.score += 10
393
+ document.querySelector(".points").innerText = this.score
394
+ this.food = this.spawnFood()
395
+ } else {
396
+ this.snake.pop()
397
+ }
398
+
399
+ this.draw()
400
+ }
401
+
402
+ draw() {
403
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
404
+
405
+ this.ctx.fillStyle = "#10b981"
406
+ this.snake.forEach((s) => {
407
+ this.ctx.fillRect(s.x * this.gridSize, s.y * this.gridSize, this.gridSize - 1, this.gridSize - 1)
408
+ })
409
+
410
+ this.ctx.fillStyle = "#ef4444"
411
+ this.ctx.fillRect(this.food.x * this.gridSize, this.food.y * this.gridSize, this.gridSize - 1, this.gridSize - 1)
412
+ }
413
+
414
+ cleanup() {
415
+ clearInterval(this.interval)
416
+ this.restart()
417
+ document.removeEventListener("keydown", this.handleKey)
418
+ }
419
+
420
+ pause() {
421
+ clearInterval(this.interval)
422
+ }
423
+
424
+ resume() {
425
+ clearInterval(this.interval)
426
+ this.canvas.classList.remove("game-over")
427
+ this.interval = setInterval(() => this.loop(), 120)
428
+ }
429
+
430
+ restart(time) {
431
+ setTimeout(() => {
432
+ this.snake = [{ x: Math.floor(this.canvas.width / this.gridSize / 2), y: Math.floor(this.canvas.height / this.gridSize / 2) }]
433
+ this.dx = 1
434
+ this.dy = 0
435
+ this.food = this.spawnFood()
436
+ this.score = 0
437
+ document.querySelector(".points").innerText = this.score
438
+ this.gameOver = false
439
+ }, time || 0)
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Memory Game Implementation
445
+ */
446
+ class MemoryGame {
447
+ constructor(container) {
448
+ this.grid = document.createElement("div")
449
+ this.grid.className = "memory-grid"
450
+ container.appendChild(this.grid)
451
+
452
+ const icons = ["🐌", "🦕", "♥️", "⚡", "🌱", "🤖"]
453
+ this.cards = [...icons, ...icons]
454
+ this.cards.sort(() => Math.random() - 0.5)
455
+
456
+ this.flipped = []
457
+ this.matched = []
458
+ this.render()
459
+ }
460
+
461
+ render() {
462
+ this.cards.forEach((icon, index) => {
463
+ const { r, g, b } = getAverageColor(icon)
464
+ const card = document.createElement("div")
465
+ card.style.setProperty("--card-color", `rgb(${r},${g},${b})`)
466
+ // card.style.setProperty("--card-color", `rgba(${r},${g},${b}, 0.5)`)
467
+ // card.style.setProperty("--card-color", `color(from rgb(${r},${g},${b}) h calc(from rgb(${r},${g},${b}) h + 180)`)
468
+
469
+ card.className = "memory-card"
470
+ card.dataset.icon = icon
471
+
472
+ card.addEventListener("mousedown", (e) => e.preventDefault())
473
+ card.addEventListener("click", (e) => {
474
+ e.stopPropagation()
475
+ this.flip(card, index)
476
+ })
477
+ this.grid.appendChild(card)
478
+ })
479
+ }
480
+
481
+ flip(card, index) {
482
+ if (this.flipped.length >= 2 || this.flipped.includes(index) || this.matched.includes(index)) return
483
+
484
+ card.innerText = this.cards[index]
485
+ card.classList.add("flipped")
486
+ this.flipped.push(index)
487
+
488
+ if (this.flipped.length === 2) {
489
+ this.checkMatch()
490
+ }
491
+ }
492
+
493
+ checkMatch() {
494
+ const [idx1, idx2] = this.flipped
495
+ const card1 = this.grid.children[idx1]
496
+ const card2 = this.grid.children[idx2]
497
+
498
+ if (this.cards[idx1] === this.cards[idx2]) {
499
+ this.matched.push(idx1, idx2)
500
+ card1.classList.add("matched")
501
+ card2.classList.add("matched")
502
+ this.flipped = []
503
+
504
+ if (this.matched.length === this.cards.length) {
505
+ setTimeout(() => {
506
+ this.matched = []
507
+ this.flipped = []
508
+ this.cards.sort(() => Math.random() - 0.5)
509
+ this.grid.innerHTML = ""
510
+ this.render()
511
+ }, 2000)
512
+ }
513
+ } else {
514
+ setTimeout(() => {
515
+ card1.classList.remove("flipped")
516
+ card1.innerText = ""
517
+ card2.classList.remove("flipped")
518
+ card2.innerText = ""
519
+ this.flipped = []
520
+ }, 800)
521
+ }
522
+ }
523
+
524
+ cleanup() {}
525
+ pause() {}
526
+ resume() {}
527
+ }
528
+
529
+ /**
530
+ * Simon Game Implementation
531
+ */
532
+ class SimonGame {
533
+ constructor(container) {
534
+ this.board = document.createElement("div")
535
+ this.board.className = "simon-board"
536
+ container.appendChild(this.board)
537
+
538
+ const colors = ["simon-green", "simon-red", "simon-yellow", "simon-blue"]
539
+ this.sequence = []
540
+ this.playerSequence = []
541
+ this.buttons = []
542
+ this.score = 0
543
+
544
+ colors.forEach((color, i) => {
545
+ const btn = document.createElement("div")
546
+ btn.className = `simon-btn ${color}`
547
+ btn.dataset.id = i
548
+ btn.addEventListener("mousedown", (e) => e.preventDefault())
549
+ btn.addEventListener("click", (e) => {
550
+ e.stopPropagation()
551
+ this.handleInput(i)
552
+ })
553
+ this.board.appendChild(btn)
554
+ this.buttons.push(btn)
555
+ })
556
+
557
+ this.isActive = false
558
+ this.timer = setTimeout(() => this.nextRound(), 600)
559
+ }
560
+
561
+ nextRound() {
562
+ this.sequence.push(Math.floor(Math.random() * 4))
563
+ this.playerSequence = []
564
+ this.playSequence()
565
+ }
566
+
567
+ playSequence() {
568
+ this.isActive = false
569
+ let i = 0
570
+
571
+ this.interval = setInterval(() => {
572
+ this.flash(this.sequence[i])
573
+ i++
574
+ if (i >= this.sequence.length) {
575
+ clearInterval(this.interval)
576
+ this.isActive = true
577
+ }
578
+ }, 800)
579
+ }
580
+
581
+ flash(btnIndex) {
582
+ if (!this.buttons[btnIndex]) return
583
+ const btn = this.buttons[btnIndex]
584
+ btn.classList.add("lit")
585
+ setTimeout(() => btn.classList.remove("lit"), 300)
586
+ }
587
+
588
+ handleInput(index) {
589
+ if (!this.isActive) return
590
+
591
+ this.flash(index)
592
+ this.playerSequence.push(index)
593
+
594
+ const currentStep = this.playerSequence.length - 1
595
+ if (this.playerSequence[currentStep] !== this.sequence[currentStep]) {
596
+ // Fail
597
+ this.sequence = []
598
+ this.isActive = false
599
+ this.board.classList.add("game-over")
600
+
601
+ setTimeout(() => {
602
+ this.score = 0
603
+ document.querySelector(".points").innerText = this.score
604
+ this.board.classList.remove("game-over")
605
+ this.nextRound()
606
+ }, 4000)
607
+ return
608
+ }
609
+
610
+ if (this.playerSequence.length === this.sequence.length) {
611
+ this.isActive = false
612
+ this.score += 10
613
+ document.querySelector(".points").innerText = this.score
614
+ setTimeout(() => this.nextRound(), 1000)
615
+ }
616
+ }
617
+
618
+ cleanup() {
619
+ clearTimeout(this.timer)
620
+ this.score = 0
621
+ document.querySelector(".points").innerText = this.score
622
+ this.board.classList.remove("game-over")
623
+ if (this.interval) clearInterval(this.interval)
624
+ }
625
+
626
+ pause() {
627
+ this.paused = true
628
+ clearTimeout(this.timer)
629
+ if (this.interval) clearInterval(this.interval)
630
+ this.buttons.forEach((b) => b.classList.remove("lit"))
631
+ }
632
+
633
+ resume() {
634
+ if (!this.paused) return
635
+ this.paused = false
636
+ this.playSequence()
637
+ }
638
+ }
639
+
640
+ // Initialize
641
+ const initInterval = setInterval(() => {
642
+ if (document.querySelector("#gameBtn")) {
643
+ console.log("Game Button found!")
644
+ const btn = new GameButton("gameBtn")
645
+ clearInterval(initInterval)
646
+ }
647
+ }, 250)
648
+
649
+ const getAverageColor = (emoji) => {
650
+ const canvas = document.createElement("canvas")
651
+ const ctx = canvas.getContext("2d")
652
+ const size = 30
653
+
654
+ canvas.width = size
655
+ canvas.height = size
656
+
657
+ ctx.textBaseline = "middle"
658
+ ctx.textAlign = "center"
659
+ ctx.font = `${size - 4}px sans-serif`
660
+ ctx.fillText(emoji, size / 2, size / 2)
661
+
662
+ const data = ctx.getImageData(0, 0, size, size).data
663
+ let r = 0,
664
+ g = 0,
665
+ b = 0,
666
+ count = 0
667
+
668
+ for (let i = 0; i < data.length; i += 4) {
669
+ const alpha = data[i + 3]
670
+ if (alpha > 50) {
671
+ r += data[i]
672
+ g += data[i + 1]
673
+ b += data[i + 2]
674
+ count++
675
+ }
676
+ }
677
+
678
+ if (count > 0) {
679
+ r = Math.floor(r / count)
680
+ g = Math.floor(g / count)
681
+ b = Math.floor(b / count)
682
+ } else {
683
+ r = 255
684
+ g = 255
685
+ b = 255
686
+ }
687
+
688
+ return { r, g, b }
689
+ }