diff --git a/app/_assets/entrypoints/application.js b/app/_assets/entrypoints/application.js
index b53942a8c6..156708543f 100644
--- a/app/_assets/entrypoints/application.js
+++ b/app/_assets/entrypoints/application.js
@@ -28,6 +28,7 @@ import "@/javascripts/llm_dropdown";
import "@/javascripts/collapsible_code";
import "@/javascripts/tooltip";
import "@/javascripts/konami";
+import "@/javascripts/bullet_hell";
import "@github/clipboard-copy-element";
document.addEventListener("DOMContentLoaded", function () {
diff --git a/app/_assets/javascripts/bullet_hell/game.js b/app/_assets/javascripts/bullet_hell/game.js
new file mode 100644
index 0000000000..63b452c990
--- /dev/null
+++ b/app/_assets/javascripts/bullet_hell/game.js
@@ -0,0 +1,923 @@
+import { patterns, DEFAULT_PATTERN, pickPattern, activePattern } from "./patterns";
+
+const FIXED_DT = 1000 / 60;
+const PLAYER_SPEED = 4.2;
+const PLAYER_FOCUS_SPEED = 1.8;
+const PLAYER_FIRE_INTERVAL = 6;
+const PLAYER_BULLET_SPEED = 9;
+const PLAYER_HITBOX = 3;
+const BOSS_HP_BASE = 28;
+const BOSS_HP_SCALING = 8;
+const STAGE_HP_BONUS = 16;
+const STAGES_TOTAL = 3;
+const STAGE_BOSS_COUNTS = [0, 2, 2, 3];
+const ENEMY_SIZE = 48;
+const MINION_HP = 1;
+const MINION_HITBOX = 11;
+const MINION_SPEED = 1.5;
+const FADE_IN_MS = 900;
+const POWER_MAX = 4;
+const BOMB_START = 3;
+const BOMB_DURATION = 60;
+const LASER_HALF_WIDTH = 18;
+const POWERUP_PICKUP_RADIUS = 22;
+const POWERUP_MAGNET_RADIUS = 110;
+
+// Player shot configuration per power level.
+const POWER_PATTERNS = {
+ 1: [
+ { dx: -7, dy: -12, vx: 0 },
+ { dx: 7, dy: -12, vx: 0 },
+ ],
+ 2: [
+ { dx: -10, dy: -12, vx: 0 },
+ { dx: -4, dy: -14, vx: 0 },
+ { dx: 4, dy: -14, vx: 0 },
+ { dx: 10, dy: -12, vx: 0 },
+ ],
+ 3: [
+ { dx: -12, dy: -12, vx: 0 },
+ { dx: -5, dy: -14, vx: 0 },
+ { dx: 5, dy: -14, vx: 0 },
+ { dx: 12, dy: -12, vx: 0 },
+ { dx: -18, dy: -8, vx: -1.6 },
+ { dx: 18, dy: -8, vx: 1.6 },
+ ],
+ 4: [
+ { dx: -14, dy: -12, vx: -0.3 },
+ { dx: -7, dy: -14, vx: 0 },
+ { dx: -2, dy: -16, vx: 0 },
+ { dx: 2, dy: -16, vx: 0 },
+ { dx: 7, dy: -14, vx: 0 },
+ { dx: 14, dy: -12, vx: 0.3 },
+ { dx: -22, dy: -8, vx: -2.0 },
+ { dx: 22, dy: -8, vx: 2.0 },
+ ],
+};
+
+// Boss roaming styles. Seed picks one so neighbours don't move in lockstep.
+const MOVE_FNS = [
+ (e) => {
+ e.x = e.baseX + Math.sin(e.t / 60) * 110;
+ e.y = e.baseY + Math.sin(e.t / 95) * 24;
+ },
+ (e) => {
+ e.x = e.baseX + Math.sin(e.t / 45) * 100;
+ e.y = e.baseY + Math.sin(e.t / 90) * 36;
+ },
+ (e) => {
+ e.x = e.baseX + Math.cos(e.t / 55) * 80;
+ e.y = e.baseY + Math.sin(e.t / 55) * 46;
+ },
+ (e) => {
+ e.x = e.baseX + Math.sin(e.t / 38 + e.seed) * 130;
+ e.y = e.baseY + Math.cos(e.t / 80) * 28;
+ },
+];
+
+export function bossSpawn(width) {
+ return { x: width / 2, y: 130 };
+}
+
+// All icons converge to the boss spawn point during morph, with a small
+// stagger so they don't sit perfectly on top of each other.
+export function computeFormation(count, width, _height) {
+ if (count === 0) return [];
+ const target = bossSpawn(width);
+ const slots = [];
+ for (let i = 0; i < count; i++) {
+ const offX = (i - (count - 1) / 2) * 9;
+ const offY = (i % 3) * 5;
+ slots.push({ x: target.x + offX, y: target.y + offY });
+ }
+ return slots;
+}
+
+const KONG_MARK_PATHS =
+ '' +
+ '' +
+ '' +
+ '';
+
+const KONG_MARK_SVG =
+ ``;
+
+const GORILLA_ART = ` ██████████
+ ██▒▒▒▒▒▒▒▒▒▒██
+ ██▒▒▒▒ ▒▒▒▒▒▒▒▒▓▓
+ ██▒▒ ░░▒▒▒▒▒▒▒▒██
+ ██▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒██
+ ██▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒██
+ ██▒▒ ▒▒▒▒▒▒▒▒▒▒▓▓▒▒██
+ ██▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒██
+ ░░▓▓▒▒▒▒▒▒▒▒▒▒▓▓▒▒██
+ ██▒▒▒▒▒▒▒▒▒▒▒▒████
+ ██▒▒▓▓▒▒▒▒▓▓██
+ ██▓▓▒▒▓▓▓▓████
+ ██▓▓▓▓████
+ ██████████
+ ██████
+ ██▓▓██
+ ██░░▒▒▓▓██
+ ██████
+ ▒▒██▒▒
+ ██
+ ██
+ ██
+ ██
+ ██
+ ██
+ ██
+ ██
+ ██
+ ██
+ ██
+ ██
+ ██
+ ██
+ ██
+ ██ ████████████████
+ ██ ██▒▒▒▒▒▒▒▒▒▒▒▒▒▒████
+ ██ ▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒██▓▓
+ ██ ██▒▒██████████▓▓▓▓▒▒▒▒▓▓▓▓██
+ ██ ████ ░░ ░░░░████▓▓▒▒▒▒▓▓▓▓██
+ ██ ██░░░░░░░░░░░░▒▒██▓▓▒▒▒▒▒▒▓▓▓▓██
+ ██ ██████░░░░██████████▓▓▒▒████▓▓▓▓██
+ ██ ██░░██░░ ░░██░░▒▒██▓▓██░░██▒▒▓▓██
+ ██ ██ ░░░░░░░░ ░░░░▒▒████▓▓████▒▒▓▓▓▓██
+ ██ ██ ██░░██ ░░ ░░░░████▓▓▒▒▒▒▒▒▓▓████
+ ██ ██ ░░░░░░░░██▒▒▓▓▒▒▒▒████████
+ ██ ████████░░ ░░░░░░░░░░░░░░████▓▓▒▒▒▒████▓▓████
+ ██ ██░░░░░░░░░░░░░░░░░░░░░░░░▒▒████▒▒▒▒██▓▓▒▒▒▒▓▓████
+ ██ ██░░░░░░▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒████▓▓▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓██▓▓
+ ██ ██████████████████████████▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓██
+ ██ ██▒▒██▓▓▓▓██████████████▓▓▓▓▒▒▓▓▒▒▒▒▒▒▓▓▒▒▒▒▒▒▒▒▒▒████
+ ██████ ██▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒██▓▓▓▓▒▒▒▒▒▒▒▒▓▓████
+ ██ ██ ██▒▒▒▒▒▒▒▒▒▒▒▒████████████▒▒▓▓▒▒▒▒▒▒▒▒▒▒██▓▓▓▓▒▒▒▒▒▒▒▒▓▓████
+ ██░░ ░░░░████▒▒▒▒▒▒▒▒▒▒▒▒██░░░░░░░░░░░░████▒▒▒▒▒▒▒▒▒▒▓▓██▓▓▓▓▒▒▒▒▒▒▒▒▓▓████
+ ██ ░░░░██████▒▒▒▒▒▒██▒▒▒▒██░░ ░░░░░░████▒▒▒▒▒▒▒▒▓▓████▓▓▓▓▒▒▒▒▒▒▒▒▓▓██
+ ██░░░░▒▒██▒▒▒▒██▒▒▓▓██▒▒██░░▒▒░░██ ░░▒▒░░░░██▒▒▓▓▒▒▓▓██████████▒▒▒▒▓▓▒▒▓▓██
+ ████████▒▒▒▒▓▓▓▓▓▓██▒▒████████▒▒████████████▒▒▓▓▓▓██████████████▒▒▒▒▒▒▒▒▓▓
+ ██▒▒██▒▒▒▒▒▒▓▓▓▓████▓▓▒▒████ ░░██████▓▓▓▓▓▓██▒▒▒▒▒▒▓▓██████▓▓▒▒████▓▓
+ ██▒▒██▒▒▒▒▓▓████████▓▓▒▒██ ░░ ░░░░██▓▓▓▓▓▓██▒▒▒▒▒▒▒▒▓▓▓▓████████░░░░██
+ ██▒▒▓▓▓▓██████▓▓▓▓██▒▒██ ░░░░░░████▓▓██▒▒▒▒██▒▒▒▒████▓▓████░░░░░░██
+ ██ ████████████████▓▓▓▓██ ██░░░░░░████▓▓██▒▒██░░████▓▓██▓▓██░░██░░▒▒██
+ ██ ██████░░████▓▓▓▓██░░░░░░░░████▓▓██▒▒▒▒████ ██████████████░░▒▒██
+ ██ ██░░████░░██▓▓▓▓██░░░░░░████▓▓▓▓██▒▒▒▒██ ██░░██████ ██░░██
+ ██ ████ ░░░░████▓▓▓▓██████████▓▓████▒▒██░░ ░░░░████ ██░░██
+ ██ ██░░ ░░██████▓▓▓▓██████████████▒▒██ ░░░░████ ██
+ ████ ██ ██░░░░▒▒████████████████████████ ░░▒▒████░░░░░░
+██ ██ ░░░░░░██░░▒▒████████████████████████░░▒▒████░░░░░░░░░░░░░░░░
+ ████████████ ░░░░░░████░░░░░░░░░░░░░░░░░░░░░░░░████░░░░░░░░░░░░░░░░
+
+ NO AI WITHOUT APIs`;
+
+export class Game {
+ constructor(sprites, onExit) {
+ this.sprites = sprites;
+ this.onExit = onExit;
+ this.keys = new Set();
+ this.lastTime = 0;
+ this.acc = 0;
+ this.running = false;
+ this.state = "playing";
+ this.score = 0;
+ this.frame = 0;
+ this.enemyAlpha = 0;
+ this.minionQueue = [];
+ this.bossQueue = [];
+ this.bossesDefeated = 0;
+ this.stage = 1;
+ this.bossInStage = 0;
+ this.power = 1;
+ this.bombs = BOMB_START;
+ this.bombFrames = 0;
+ this.lasers = [];
+ this.powerups = [];
+
+ this.kongMark = new Image();
+ this.kongMark.src = `data:image/svg+xml;base64,${btoa(KONG_MARK_SVG)}`;
+
+ this.gorilla = document.createElement("pre");
+ this.gorilla.textContent = GORILLA_ART;
+ this.gorilla.style.cssText = [
+ "position:fixed",
+ "top:50%",
+ "right:24px",
+ "transform:translateY(-50%)",
+ "margin:0",
+ "padding:0",
+ "z-index:9998",
+ "pointer-events:none",
+ "user-select:none",
+ "white-space:pre",
+ "font-family:'JetBrains Mono',ui-monospace,monospace",
+ "font-size:11px",
+ "line-height:1.4",
+ "color:rgba(204,255,0,0.3)",
+ "text-shadow:0 0 4px rgba(0,0,0,0.95),0 0 2px rgba(0,0,0,0.95)",
+ "letter-spacing:0",
+ "max-height:90vh",
+ "overflow:hidden",
+ ].join(";");
+
+ this.canvas = document.createElement("canvas");
+ this.canvas.style.cssText = [
+ "position:fixed",
+ "inset:0",
+ "z-index:9999",
+ "background-color:rgba(0,0,0,0)",
+ "cursor:none",
+ ].join(";");
+ this.ctx = this.canvas.getContext("2d");
+
+ this.hud = document.createElement("div");
+ this.hud.style.cssText = [
+ "position:fixed",
+ "inset:0",
+ "z-index:10000",
+ "pointer-events:none",
+ "font-family:'JetBrains Mono',ui-monospace,monospace",
+ "color:#ccff00",
+ "padding:16px",
+ "display:flex",
+ "flex-direction:column",
+ "justify-content:space-between",
+ "opacity:0",
+ `transition:opacity ${FADE_IN_MS}ms ease-in`,
+ ].join(";");
+ const chip = "background:rgba(0,0,0,0.78);padding:6px 10px;border-radius:6px;border:1px solid rgba(204,255,0,0.4)";
+ this.hud.innerHTML = `
+
+
Kong Developer
+
Discover Kong's tools, APIs, and tutorials to help you build, secure, and scale your services.
+
+
+
+ SCORE 0
+ POWER ●○○○
+
+
+ SPECIAL ATTACK ●●●
+ arrows · Z fire · X special · shift focus · esc exit
+
+
+
+ `;
+
+ this.resize = () => {
+ this.canvas.width = window.innerWidth;
+ this.canvas.height = window.innerHeight;
+ };
+ this.resize();
+
+ this.player = {
+ x: this.canvas.width / 2,
+ y: this.canvas.height - 90,
+ cooldown: 0,
+ };
+
+ this.enemies = [];
+ this.playerBullets = [];
+ this.enemyBullets = [];
+
+ this.onKeyDown = (e) => {
+ if (e.key === "Escape") {
+ e.preventDefault();
+ this.exit();
+ return;
+ }
+ if (e.key === "r" || e.key === "R") {
+ if (this.state === "lost") this.restart();
+ return;
+ }
+ if (e.key === "n" || e.key === "N") {
+ if (this.state === "stage-clear") this.nextStage();
+ return;
+ }
+ if (e.key === "x" || e.key === "X" || e.key === "b" || e.key === "B") {
+ e.preventDefault();
+ this.fireBomb();
+ return;
+ }
+ this.keys.add(e.key);
+ };
+ this.onKeyUp = (e) => this.keys.delete(e.key);
+ }
+
+ setSprites(sprites) {
+ this.sprites = sprites.length > 0 ? sprites : [makeFallbackSprite()];
+ this.startStage(1);
+ }
+
+ startStage(stage) {
+ this.stage = stage;
+ this.bossInStage = 0;
+ const count = STAGE_BOSS_COUNTS[stage] || 2;
+ const offset = STAGE_BOSS_COUNTS.slice(1, stage).reduce((a, b) => a + b, 0);
+ this.bossQueue = [];
+ for (let i = 0; i < count; i++) {
+ this.bossQueue.push(this.sprites[(offset + i) % this.sprites.length]);
+ }
+ }
+
+ mountChrome() {
+ document.body.appendChild(this.gorilla);
+ document.body.appendChild(this.canvas);
+ document.body.appendChild(this.hud);
+ requestAnimationFrame(() => {
+ this.hud.style.opacity = "1";
+ });
+ }
+
+ spawnNextBoss() {
+ if (this.bossQueue.length === 0) return false;
+ const sprite = this.bossQueue.shift();
+ const seed = Math.floor(Math.random() * 100000);
+ const { x, y } = bossSpawn(this.canvas.width);
+ const hp =
+ BOSS_HP_BASE +
+ (this.stage - 1) * STAGE_HP_BONUS +
+ this.bossInStage * BOSS_HP_SCALING;
+ this.enemies.push({
+ kind: "boss",
+ sprite,
+ x,
+ y,
+ baseX: x,
+ baseY: y,
+ hp,
+ seed,
+ pattern: pickPattern(seed),
+ t: 0,
+ });
+ this.bossInStage++;
+ return true;
+ }
+
+ spawnMinion() {
+ const x = 80 + Math.random() * (this.canvas.width - 160);
+ this.enemies.push({
+ kind: "minion",
+ x,
+ y: -30,
+ vx: (Math.random() - 0.5) * 1.4,
+ vy: MINION_SPEED + Math.random() * 0.5,
+ hp: MINION_HP,
+ t: 0,
+ seed: Math.floor(Math.random() * 100000),
+ });
+ }
+
+ scheduleMinionWave() {
+ const count = 4 + Math.floor(Math.random() * 2);
+ for (let i = 0; i < count; i++) {
+ this.minionQueue.push(20 + i * 14);
+ }
+ }
+
+ dropPowerup(x, y) {
+ this.powerups.push({
+ x,
+ y,
+ vx: (Math.random() - 0.5) * 0.8,
+ vy: 0.8,
+ });
+ }
+
+ killEnemy(e, index) {
+ this.enemies.splice(index, 1);
+ if (e.kind === "minion") {
+ this.score += 25;
+ this.dropPowerup(e.x, e.y);
+ } else {
+ this.score += 500;
+ this.bossesDefeated++;
+ this.scheduleMinionWave();
+ }
+ }
+
+ fireBomb() {
+ if (this.state !== "playing") return;
+ if (this.bombs <= 0 || this.bombFrames > 0) return;
+ this.bombs--;
+ this.bombFrames = BOMB_DURATION;
+ this.enemyBullets = [];
+ const p = this.player;
+ this.lasers = [
+ { x: p.x - 48, life: BOMB_DURATION, max: BOMB_DURATION },
+ { x: p.x, life: BOMB_DURATION, max: BOMB_DURATION },
+ { x: p.x + 48, life: BOMB_DURATION, max: BOMB_DURATION },
+ ];
+ }
+
+ start() {
+ this.spawnNextBoss();
+ this.enemyAlpha = 0;
+ window.addEventListener("keydown", this.onKeyDown);
+ window.addEventListener("keyup", this.onKeyUp);
+ window.addEventListener("resize", this.resize);
+ this.running = true;
+ this.lastTime = performance.now();
+ requestAnimationFrame(this.loop);
+ }
+
+ unmount() {
+ this.running = false;
+ window.removeEventListener("keydown", this.onKeyDown);
+ window.removeEventListener("keyup", this.onKeyUp);
+ window.removeEventListener("resize", this.resize);
+ this.canvas.remove();
+ this.hud.remove();
+ this.gorilla.remove();
+ }
+
+ exit() {
+ this.unmount();
+ if (this.onExit) this.onExit();
+ }
+
+ restart() {
+ this.resetField();
+ this.power = 1;
+ this.bombs = BOMB_START;
+ this.score = 0;
+ this.frame = 0;
+ this.bossesDefeated = 0;
+ this.startStage(1);
+ this.state = "playing";
+ this.spawnNextBoss();
+ this.setOverlay("");
+ }
+
+ nextStage() {
+ if (this.stage >= STAGES_TOTAL) {
+ this.exit();
+ return;
+ }
+ this.resetField();
+ this.startStage(this.stage + 1);
+ this.state = "playing";
+ this.spawnNextBoss();
+ this.setOverlay("");
+ }
+
+ resetField() {
+ this.enemies = [];
+ this.playerBullets = [];
+ this.enemyBullets = [];
+ this.minionQueue = [];
+ this.powerups = [];
+ this.lasers = [];
+ this.bombFrames = 0;
+ this.player.x = this.canvas.width / 2;
+ this.player.y = this.canvas.height - 90;
+ this.player.cooldown = 0;
+ }
+
+ loop = (now) => {
+ if (!this.running) return;
+ this.acc += now - this.lastTime;
+ this.lastTime = now;
+ let steps = 0;
+ while (this.acc >= FIXED_DT && steps < 5) {
+ this.tick();
+ this.acc -= FIXED_DT;
+ steps++;
+ }
+ this.render();
+ requestAnimationFrame(this.loop);
+ };
+
+ tick() {
+ if (this.enemyAlpha < 1) this.enemyAlpha = Math.min(1, this.enemyAlpha + 0.05);
+ if (this.state !== "playing") return;
+ this.frame++;
+
+ this.minionQueue = this.minionQueue
+ .map((d) => d - 1)
+ .filter((d) => {
+ if (d <= 0) {
+ this.spawnMinion();
+ return false;
+ }
+ return true;
+ });
+
+ const focus = this.keys.has("Shift");
+ const speed = focus ? PLAYER_FOCUS_SPEED : PLAYER_SPEED;
+ let dx = 0;
+ let dy = 0;
+ if (this.keys.has("ArrowLeft") || this.keys.has("a")) dx -= 1;
+ if (this.keys.has("ArrowRight") || this.keys.has("d")) dx += 1;
+ if (this.keys.has("ArrowUp") || this.keys.has("w")) dy -= 1;
+ if (this.keys.has("ArrowDown") || this.keys.has("s")) dy += 1;
+ if (dx && dy) {
+ dx *= 0.7071;
+ dy *= 0.7071;
+ }
+ this.player.x = clamp(this.player.x + dx * speed, 16, this.canvas.width - 16);
+ this.player.y = clamp(this.player.y + dy * speed, 16, this.canvas.height - 16);
+
+ if (this.player.cooldown > 0) this.player.cooldown--;
+ if ((this.keys.has("z") || this.keys.has("Z") || this.keys.has(" ")) && this.player.cooldown === 0) {
+ const pattern = POWER_PATTERNS[this.power] || POWER_PATTERNS[1];
+ for (const b of pattern) {
+ this.playerBullets.push({
+ x: this.player.x + b.dx,
+ y: this.player.y + b.dy,
+ vx: b.vx,
+ vy: -PLAYER_BULLET_SPEED,
+ r: 4.5,
+ });
+ }
+ this.player.cooldown = PLAYER_FIRE_INTERVAL;
+ }
+
+ for (const e of this.enemies) {
+ e.t++;
+ if (e.kind === "minion") {
+ e.x += e.vx;
+ e.y += e.vy;
+ if (e.t === 22 || e.t === 52) {
+ const ax = this.player.x - e.x;
+ const ay = this.player.y - e.y;
+ const m = Math.hypot(ax, ay) || 1;
+ this.enemyBullets.push({
+ x: e.x,
+ y: e.y,
+ vx: (ax / m) * 2.4,
+ vy: (ay / m) * 2.4,
+ r: 5,
+ });
+ }
+ } else {
+ MOVE_FNS[Math.abs(e.seed) % MOVE_FNS.length](e);
+ const key = activePattern(e);
+ const fn = patterns[key] || patterns[DEFAULT_PATTERN];
+ const fresh = fn(e, e.t, this.player);
+ for (const b of fresh) this.enemyBullets.push(b);
+ }
+ }
+
+ for (const b of this.playerBullets) {
+ b.x += b.vx;
+ b.y += b.vy;
+ }
+ for (const b of this.enemyBullets) {
+ b.x += b.vx;
+ b.y += b.vy;
+ }
+
+ if (this.bombFrames > 0) {
+ this.bombFrames--;
+ this.enemyBullets = [];
+ for (const laser of this.lasers) {
+ for (let j = this.enemies.length - 1; j >= 0; j--) {
+ const e = this.enemies[j];
+ if (e.y > this.player.y) continue;
+ if (Math.abs(e.x - laser.x) < LASER_HALF_WIDTH + (e.kind === "minion" ? 10 : 22)) {
+ e.hp--;
+ if (e.hp <= 0) this.killEnemy(e, j);
+ }
+ }
+ }
+ }
+ this.lasers = this.lasers.filter((l) => {
+ l.life--;
+ return l.life > 0;
+ });
+
+ for (let i = this.playerBullets.length - 1; i >= 0; i--) {
+ const b = this.playerBullets[i];
+ let hit = false;
+ for (let j = this.enemies.length - 1; j >= 0; j--) {
+ const e = this.enemies[j];
+ const hitR = e.kind === "minion" ? MINION_HITBOX : ENEMY_SIZE / 2;
+ if (Math.abs(b.x - e.x) < hitR && Math.abs(b.y - e.y) < hitR) {
+ e.hp--;
+ hit = true;
+ if (e.hp <= 0) this.killEnemy(e, j);
+ break;
+ }
+ }
+ if (hit) this.playerBullets.splice(i, 1);
+ }
+
+ if (this.bombFrames === 0) {
+ for (const b of this.enemyBullets) {
+ const dxp = b.x - this.player.x;
+ const dyp = b.y - this.player.y;
+ if (dxp * dxp + dyp * dyp < (PLAYER_HITBOX + b.r) * (PLAYER_HITBOX + b.r)) {
+ this.state = "lost";
+ this.showMenu("GAME OVER", [
+ { label: "RETRY", action: "retry" },
+ { label: "BACK TO KONG DEVELOPER", action: "exit" },
+ ]);
+ return;
+ }
+ }
+ }
+
+ for (let i = this.powerups.length - 1; i >= 0; i--) {
+ const pu = this.powerups[i];
+ const dxp = this.player.x - pu.x;
+ const dyp = this.player.y - pu.y;
+ const distSq = dxp * dxp + dyp * dyp;
+ if (distSq < POWERUP_MAGNET_RADIUS * POWERUP_MAGNET_RADIUS) {
+ const m = Math.sqrt(distSq) || 1;
+ pu.vx += (dxp / m) * 0.4;
+ pu.vy += (dyp / m) * 0.4;
+ }
+ pu.vx *= 0.95;
+ pu.vy = Math.min(pu.vy * 0.97 + 0.04, 3);
+ pu.x += pu.vx;
+ pu.y += pu.vy;
+ if (distSq < POWERUP_PICKUP_RADIUS * POWERUP_PICKUP_RADIUS) {
+ this.powerups.splice(i, 1);
+ if (this.power < POWER_MAX) this.power++;
+ this.score += 50;
+ continue;
+ }
+ if (pu.y > this.canvas.height + 20) {
+ this.powerups.splice(i, 1);
+ }
+ }
+
+ this.playerBullets = this.playerBullets.filter((b) => onScreen(b, this.canvas));
+ this.enemyBullets = this.enemyBullets.filter((b) => onScreen(b, this.canvas));
+ this.enemies = this.enemies.filter(
+ (e) => e.kind !== "minion" || e.y < this.canvas.height + 40,
+ );
+
+ if (
+ this.enemies.length === 0 &&
+ this.minionQueue.length === 0
+ ) {
+ if (this.bossQueue.length > 0) {
+ this.spawnNextBoss();
+ } else if (this.stage < STAGES_TOTAL) {
+ this.state = "stage-clear";
+ this.showMenu(`STAGE ${this.stage} CLEAR`, [
+ { label: "NEXT STAGE →", action: "next" },
+ { label: "BACK TO KONG DEVELOPER", action: "exit" },
+ ]);
+ } else {
+ this.state = "won";
+ this.showMenu("ALL STAGES COMPLETE ★", [
+ { label: "BACK TO KONG DEVELOPER", action: "exit" },
+ ]);
+ }
+ }
+
+ this.updateHud();
+ }
+
+ updateHud() {
+ this.hud.querySelector('[data-hud="score"]').textContent = `SCORE ${this.score}`;
+ const powerStr = "●".repeat(this.power) + "○".repeat(POWER_MAX - this.power);
+ this.hud.querySelector('[data-hud="power"]').textContent = `POWER ${powerStr}`;
+ const bombStr = this.bombs > 0 ? "●".repeat(this.bombs) : "○";
+ this.hud.querySelector('[data-hud="bombs"]').textContent = `SPECIAL ATTACK ${bombStr}`;
+ }
+
+ render() {
+ const { ctx, canvas } = this;
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ ctx.globalAlpha = this.enemyAlpha;
+ for (const e of this.enemies) {
+ if (e.kind === "minion") {
+ ctx.shadowColor = "rgba(0,0,0,0.5)";
+ ctx.shadowBlur = 6;
+ ctx.fillStyle = "#ffa84a";
+ ctx.strokeStyle = "rgba(0,0,0,0.9)";
+ ctx.lineWidth = 1.5;
+ ctx.beginPath();
+ ctx.moveTo(e.x, e.y - 10);
+ ctx.lineTo(e.x + 10, e.y);
+ ctx.lineTo(e.x, e.y + 10);
+ ctx.lineTo(e.x - 10, e.y);
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+ } else {
+ ctx.shadowColor = "rgba(0,0,0,0.5)";
+ ctx.shadowBlur = 10;
+ if (e.sprite.complete && e.sprite.naturalWidth > 0) {
+ ctx.drawImage(e.sprite, e.x - ENEMY_SIZE / 2, e.y - ENEMY_SIZE / 2, ENEMY_SIZE, ENEMY_SIZE);
+ } else {
+ ctx.fillStyle = "#ccff00";
+ ctx.fillRect(e.x - 16, e.y - 16, 32, 32);
+ }
+ }
+ }
+ ctx.shadowBlur = 0;
+ ctx.globalAlpha = 1;
+
+ this.renderPowerups();
+ this.renderLasers();
+
+ ctx.strokeStyle = "rgba(0,0,0,0.85)";
+ ctx.lineWidth = 2;
+ ctx.fillStyle = "#ccff00";
+ for (const b of this.playerBullets) {
+ ctx.beginPath();
+ ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.stroke();
+ }
+
+ for (const b of this.enemyBullets) {
+ ctx.fillStyle = "#ff5577";
+ ctx.beginPath();
+ ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.stroke();
+ ctx.fillStyle = "#ffaabb";
+ ctx.beginPath();
+ ctx.arc(b.x, b.y, b.r * 0.5, 0, Math.PI * 2);
+ ctx.fill();
+ }
+
+ this.renderPlayer();
+
+ if (this.keys.has("Shift") && this.state === "playing") {
+ ctx.strokeStyle = "#fff";
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.arc(this.player.x, this.player.y, PLAYER_HITBOX, 0, Math.PI * 2);
+ ctx.stroke();
+ }
+ }
+
+ renderPowerups() {
+ const { ctx } = this;
+ for (const pu of this.powerups) {
+ const pulse = 1 + Math.sin(this.frame * 0.2) * 0.08;
+ ctx.fillStyle = "#33aaff";
+ ctx.strokeStyle = "rgba(0,0,0,0.9)";
+ ctx.lineWidth = 1.5;
+ ctx.beginPath();
+ ctx.arc(pu.x, pu.y, 9 * pulse, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.stroke();
+ ctx.fillStyle = "white";
+ ctx.font = "bold 11px 'JetBrains Mono', monospace";
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillText("P", pu.x, pu.y + 1);
+ }
+ }
+
+ renderLasers() {
+ const { ctx } = this;
+ const top = 0;
+ const bottom = this.player.y - 18;
+ for (const laser of this.lasers) {
+ const ratio = laser.life / laser.max;
+ const fade = ratio < 0.25 ? ratio / 0.25 : 1;
+ const flicker = 0.9 + Math.sin(this.frame * 0.5 + laser.x) * 0.1;
+ const alpha = fade * flicker;
+ ctx.fillStyle = `rgba(204,255,0,${0.22 * alpha})`;
+ ctx.fillRect(laser.x - 32, top, 64, bottom - top);
+ ctx.fillStyle = `rgba(204,255,0,${0.55 * alpha})`;
+ ctx.fillRect(laser.x - LASER_HALF_WIDTH, top, LASER_HALF_WIDTH * 2, bottom - top);
+ ctx.fillStyle = `rgba(255,255,255,${0.95 * alpha})`;
+ ctx.fillRect(laser.x - 5, top, 10, bottom - top);
+ }
+ }
+
+ renderPlayer() {
+ const { ctx } = this;
+ const p = this.player;
+
+ if (this.bombFrames > 0) {
+ const aura = 14 + Math.sin(this.frame * 0.5) * 4;
+ ctx.fillStyle = `rgba(204,255,0,0.35)`;
+ ctx.beginPath();
+ ctx.arc(p.x, p.y, aura + 12, 0, Math.PI * 2);
+ ctx.fill();
+ }
+
+ const glow = ctx.createRadialGradient(p.x, p.y + 18, 0, p.x, p.y + 18, 16);
+ glow.addColorStop(0, "rgba(204,255,0,0.55)");
+ glow.addColorStop(1, "rgba(204,255,0,0)");
+ ctx.fillStyle = glow;
+ ctx.fillRect(p.x - 16, p.y + 4, 32, 32);
+
+ ctx.strokeStyle = "rgba(0,0,0,0.9)";
+ ctx.lineWidth = 1.5;
+ ctx.fillStyle = "rgba(204,255,0,0.78)";
+ ctx.beginPath();
+ ctx.moveTo(p.x - 17, p.y + 8);
+ ctx.lineTo(p.x - 9, p.y - 2);
+ ctx.lineTo(p.x - 9, p.y + 14);
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.moveTo(p.x + 17, p.y + 8);
+ ctx.lineTo(p.x + 9, p.y - 2);
+ ctx.lineTo(p.x + 9, p.y + 14);
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+
+ ctx.lineWidth = 2;
+ ctx.fillStyle = "#ccff00";
+ ctx.beginPath();
+ ctx.moveTo(p.x, p.y - 18);
+ ctx.lineTo(p.x - 11, p.y + 14);
+ ctx.lineTo(p.x + 11, p.y + 14);
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+
+ if (this.kongMark.complete && this.kongMark.naturalWidth > 0) {
+ const w = 18;
+ const h = 14;
+ ctx.drawImage(this.kongMark, p.x - w / 2, p.y - h / 2 + 1, w, h);
+ } else {
+ ctx.fillStyle = "rgba(0,0,0,0.9)";
+ ctx.font = "bold 14px 'JetBrains Mono', ui-monospace, monospace";
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillText("K", p.x, p.y + 3);
+ }
+ }
+
+ setOverlay(html) {
+ const el = this.hud.querySelector('[data-hud="overlay"]');
+ el.style.pointerEvents = "none";
+ el.innerHTML = html
+ ? `${html}`
+ : "";
+ this.canvas.style.cursor = "none";
+ }
+
+ showMenu(title, buttons) {
+ const el = this.hud.querySelector('[data-hud="overlay"]');
+ el.style.pointerEvents = "auto";
+ this.canvas.style.cursor = "auto";
+ const buttonStyle = [
+ "background:rgba(204,255,0,0.16)",
+ "color:#ccff00",
+ "border:1px solid rgba(204,255,0,0.65)",
+ "padding:12px 20px",
+ "border-radius:6px",
+ "font-family:'JetBrains Mono',ui-monospace,monospace",
+ "font-size:14px",
+ "font-weight:700",
+ "letter-spacing:0.04em",
+ "cursor:pointer",
+ "pointer-events:auto",
+ ].join(";");
+ const buttonsHtml = buttons
+ .map(
+ (b) =>
+ ``,
+ )
+ .join("");
+ el.innerHTML = `
+
+
${title}
+
SCORE ${this.score}
+
${buttonsHtml}
+
+ `;
+ el.querySelectorAll("button").forEach((btn) => {
+ btn.addEventListener("mouseenter", () => {
+ btn.style.background = "rgba(204,255,0,0.32)";
+ });
+ btn.addEventListener("mouseleave", () => {
+ btn.style.background = "rgba(204,255,0,0.16)";
+ });
+ btn.addEventListener("click", () => {
+ const action = btn.getAttribute("data-action");
+ if (action === "next") this.nextStage();
+ else if (action === "retry") this.restart();
+ else if (action === "exit") this.exit();
+ });
+ });
+ }
+}
+
+function clamp(v, lo, hi) {
+ return Math.max(lo, Math.min(hi, v));
+}
+
+function onScreen(b, canvas) {
+ return b.x > -20 && b.x < canvas.width + 20 && b.y > -20 && b.y < canvas.height + 20;
+}
+
+function makeFallbackSprite() {
+ const svg =
+ ``;
+ const img = new Image();
+ img.src = `data:image/svg+xml;base64,${btoa(svg)}`;
+ return img;
+}
diff --git a/app/_assets/javascripts/bullet_hell/index.js b/app/_assets/javascripts/bullet_hell/index.js
new file mode 100644
index 0000000000..de03da663c
--- /dev/null
+++ b/app/_assets/javascripts/bullet_hell/index.js
@@ -0,0 +1,128 @@
+import { morph } from "./morph";
+import { Game } from "./game";
+
+const SHAKE_MS = 600;
+const FLASH_DELAY = 350;
+const BOTTOM_THRESHOLD = 4;
+
+const FLASH_GORILLA = ` ██████████
+ ██▒▒▒▒▒▒▒▒▒▒██
+ ██▒▒▒▒ ▒▒▒▒▒▒▒▒▓▓
+ ██▒▒ ░░▒▒▒▒▒▒▒▒██
+ ██▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒██
+ ██▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒██
+ ██▒▒ ▒▒▒▒▒▒▒▒▒▒▓▓▒▒██
+ ██▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒██
+ ░░▓▓▒▒▒▒▒▒▒▒▒▒▓▓▒▒██
+ ██▒▒▒▒▒▒▒▒▒▒▒▒████
+ ██▒▒▓▓▒▒▒▒▓▓██
+ ██▓▓▒▒▓▓▓▓████
+ ██▓▓▓▓████
+ ██████████`;
+
+function isMobile() {
+ return (
+ window.matchMedia("(pointer: coarse)").matches ||
+ window.innerWidth < 768
+ );
+}
+
+function scroller() {
+ return document.scrollingElement || document.documentElement;
+}
+
+function atBottom() {
+ const se = scroller();
+ return se.scrollTop + window.innerHeight >= se.scrollHeight - BOTTOM_THRESHOLD;
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ if (isMobile()) return;
+ if (scroller().scrollHeight <= window.innerHeight + BOTTOM_THRESHOLD) return;
+
+ const heading = Array.from(document.querySelectorAll("footer h5")).find(
+ (h) => h.textContent.trim() === "Powering the API world",
+ );
+ if (!heading || !heading.parentElement) return;
+ const container = heading.parentElement;
+
+ const banner = document.createElement("div");
+ banner.className = "bullet-hell-engage";
+ banner.setAttribute("role", "button");
+ banner.setAttribute("tabindex", "0");
+ banner.setAttribute("aria-label", "Player 1 Start — launch hidden game");
+ banner.innerHTML = `
+
+
${FLASH_GORILLA}
+
+
PLAYER 1 START
+
CLICK TO ENGAGE
+
+
+ `;
+ container.appendChild(banner);
+
+ let state = "idle";
+ let activeGame = null;
+ let prevY = scroller().scrollTop;
+
+ window.addEventListener(
+ "scroll",
+ () => {
+ if (state !== "idle") return;
+ const y = scroller().scrollTop;
+ const goingDown = y > prevY;
+ prevY = y;
+ if (!goingDown || !atBottom()) return;
+
+ state = "revealing";
+ document.body.classList.add("bullet-hell-shake");
+ setTimeout(
+ () => document.body.classList.remove("bullet-hell-shake"),
+ SHAKE_MS,
+ );
+ setTimeout(reveal, FLASH_DELAY);
+ },
+ { passive: true },
+ );
+
+ function reveal() {
+ banner.classList.add("bullet-hell-engage--ready");
+ state = "ready";
+ }
+
+ banner.addEventListener("click", () => {
+ if (state === "ready") start();
+ });
+ banner.addEventListener("keydown", (e) => {
+ if ((e.key === "Enter" || e.key === " ") && state === "ready") {
+ e.preventDefault();
+ start();
+ }
+ });
+
+ async function start() {
+ if (activeGame) return;
+ state = "running";
+ banner.style.visibility = "hidden";
+ document.documentElement.style.overflow = "hidden";
+
+ const game = new Game([], () => {
+ document.documentElement.style.overflow = "";
+ banner.style.visibility = "";
+ activeGame = null;
+ state = "ready";
+ });
+ game.mountChrome();
+ activeGame = game;
+
+ const { sprites, restore } = await morph();
+ game.onExit = ((orig) => () => {
+ restore();
+ orig();
+ })(game.onExit);
+
+ game.setSprites(sprites);
+ game.start();
+ }
+});
diff --git a/app/_assets/javascripts/bullet_hell/morph.js b/app/_assets/javascripts/bullet_hell/morph.js
new file mode 100644
index 0000000000..d07e9c43fd
--- /dev/null
+++ b/app/_assets/javascripts/bullet_hell/morph.js
@@ -0,0 +1,106 @@
+import { computeFormation } from "./game";
+
+const MORPH_DURATION = 900;
+const ENEMY_SIZE = 48;
+const ICON_EXCLUDE_ANCESTORS =
+ "header, nav, footer, [class*='logo'], [class*='top-nav'], [class*='social'], [class*='copy'], button";
+const MIN_ICON_SIZE = 20;
+
+export function morph() {
+ const svgs = Array.from(document.querySelectorAll("svg")).filter((el) => {
+ if (el.closest(ICON_EXCLUDE_ANCESTORS)) return false;
+ const r = el.getBoundingClientRect();
+ return (
+ r.width >= MIN_ICON_SIZE &&
+ r.height >= MIN_ICON_SIZE &&
+ r.bottom > 0 &&
+ r.top < window.innerHeight &&
+ r.right > 0 &&
+ r.left < window.innerWidth
+ );
+ });
+
+ const captured = svgs.map((el) => {
+ const rect = el.getBoundingClientRect();
+ return { el, rect, html: serializeSvg(el) };
+ });
+
+ for (const c of captured) c.el.style.visibility = "hidden";
+
+ const overlay = document.createElement("div");
+ overlay.style.cssText =
+ "position:fixed;inset:0;z-index:9999;pointer-events:none;overflow:hidden";
+ document.body.appendChild(overlay);
+
+ const formation = computeFormation(captured.length, window.innerWidth, window.innerHeight);
+
+ const clones = captured.map(({ rect, html }, i) => {
+ const wrap = document.createElement("div");
+ wrap.style.cssText = [
+ "position:absolute",
+ `left:${rect.left}px`,
+ `top:${rect.top}px`,
+ `width:${rect.width}px`,
+ `height:${rect.height}px`,
+ `transition:transform ${MORPH_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms ease-out ${MORPH_DURATION - 100}ms`,
+ `transition-delay:${i * 15}ms, ${i * 15 + MORPH_DURATION - 100}ms`,
+ "will-change:transform,opacity",
+ ].join(";");
+ wrap.innerHTML = html;
+ const svg = wrap.querySelector("svg");
+ if (svg) {
+ svg.setAttribute("width", String(rect.width));
+ svg.setAttribute("height", String(rect.height));
+ svg.style.width = "100%";
+ svg.style.height = "100%";
+ }
+ overlay.appendChild(wrap);
+ return { wrap, rect };
+ });
+
+ return new Promise((resolve) => {
+ requestAnimationFrame(() => {
+ clones.forEach(({ wrap, rect }, i) => {
+ const slot = formation[i];
+ const tx = slot.x - (rect.left + rect.width / 2);
+ const ty = slot.y - (rect.top + rect.height / 2);
+ const scale = ENEMY_SIZE / Math.max(rect.width, rect.height);
+ wrap.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`;
+ });
+
+ setTimeout(() => {
+ for (const { wrap } of clones) wrap.style.opacity = "0";
+ }, MORPH_DURATION + clones.length * 15 - 100);
+
+ const total = MORPH_DURATION + clones.length * 15 + 200;
+ setTimeout(() => {
+ overlay.remove();
+ const sprites = captured.map(({ html }) => svgToImage(html));
+ resolve({
+ sprites,
+ restore: () => {
+ for (const c of captured) c.el.style.visibility = "";
+ },
+ });
+ }, total);
+ });
+ });
+}
+
+function serializeSvg(el) {
+ const clone = el.cloneNode(true);
+ if (!clone.getAttribute("xmlns")) {
+ clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
+ }
+ return new XMLSerializer().serializeToString(clone);
+}
+
+function svgToImage(html) {
+ const img = new Image();
+ try {
+ img.src = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(html)))}`;
+ } catch (_) {
+ img.src = "data:image/svg+xml;utf8," + encodeURIComponent(html);
+ }
+ return img;
+}
diff --git a/app/_assets/javascripts/bullet_hell/patterns.js b/app/_assets/javascripts/bullet_hell/patterns.js
new file mode 100644
index 0000000000..99f6724f8e
--- /dev/null
+++ b/app/_assets/javascripts/bullet_hell/patterns.js
@@ -0,0 +1,81 @@
+// Bullet patterns. Each is (enemy, t, player) -> bullets[].
+// t is the enemy's frame counter; enemy.seed makes per-enemy variation
+// deterministic, which is how Touhou-style patterns stay solvable:
+// shapes look random but the gaps are always the same width.
+
+const TWO_PI = Math.PI * 2;
+
+function aimAngle(enemy, player) {
+ return Math.atan2(player.y - enemy.y, player.x - enemy.x);
+}
+
+function bullet(enemy, angle, speed, r = 5.5) {
+ return {
+ x: enemy.x,
+ y: enemy.y + 10,
+ vx: Math.cos(angle) * speed,
+ vy: Math.sin(angle) * speed,
+ r,
+ };
+}
+
+function jitter(n, amount) {
+ const v = Math.sin(n * 12.9898) * 43758.5453;
+ return ((v - Math.floor(v)) - 0.5) * 2 * amount;
+}
+
+export const patterns = {
+ aimed(enemy, t, player) {
+ if (t % 90 !== 0) return [];
+ const a = aimAngle(enemy, player) + jitter(t + enemy.seed, 0.05);
+ return [bullet(enemy, a, 2.7)];
+ },
+
+ spread(enemy, t, player) {
+ if (t % 110 !== 0) return [];
+ const center = aimAngle(enemy, player);
+ const arc = 0.9;
+ const n = 5;
+ const out = [];
+ for (let i = 0; i < n; i++) {
+ const a = center - arc / 2 + (arc / (n - 1)) * i + jitter(t + enemy.seed + i, 0.02);
+ out.push(bullet(enemy, a, 2.3));
+ }
+ return out;
+ },
+
+ ring(enemy, t, player) {
+ if (t % 130 !== 0) return [];
+ const n = 10;
+ const offset = jitter(enemy.seed + t, Math.PI / n);
+ const out = [];
+ for (let i = 0; i < n; i++) {
+ const a = offset + (i / n) * TWO_PI;
+ out.push(bullet(enemy, a, 2.0));
+ }
+ return out;
+ },
+
+ stream(enemy, t, player) {
+ if (t % 9 !== 0) return [];
+ const base = aimAngle(enemy, player);
+ const sweep = Math.sin((t + enemy.seed) / 55) * 0.55;
+ const a = base + sweep + jitter(t + enemy.seed, 0.02);
+ return [bullet(enemy, a, 2.5, 5)];
+ },
+};
+
+export const PATTERN_KEYS = Object.keys(patterns);
+export const DEFAULT_PATTERN = PATTERN_KEYS[0];
+const PATTERN_DURATION = 360;
+
+export function pickPattern(seed) {
+ return PATTERN_KEYS[Math.abs(seed) % PATTERN_KEYS.length];
+}
+
+// Each enemy rotates through patterns independently — the seed offsets its
+// phase so the wave doesn't switch in unison.
+export function activePattern(enemy) {
+ const phase = Math.floor((enemy.t + enemy.seed * 17) / PATTERN_DURATION);
+ return PATTERN_KEYS[Math.abs(phase) % PATTERN_KEYS.length];
+}
diff --git a/app/_assets/stylesheets/index.css b/app/_assets/stylesheets/index.css
index 48fbba6ed6..955bead768 100644
--- a/app/_assets/stylesheets/index.css
+++ b/app/_assets/stylesheets/index.css
@@ -1051,3 +1051,93 @@ ol:has(code) {
.ot-floating-button__open > svg {
@apply justify-self-center;
}
+
+@keyframes bullet-hell-shake {
+ 0%, 100% { transform: translate(0, 0); }
+ 10%, 30%, 50%, 70%, 90% { transform: translate(-4px, 2px); }
+ 20%, 40%, 60%, 80% { transform: translate(4px, -2px); }
+}
+
+.bullet-hell-shake {
+ animation: bullet-hell-shake 600ms ease-in-out;
+}
+
+@keyframes bullet-hell-flash-blink {
+ 0%, 60%, 100% { opacity: 1; }
+ 30% { opacity: 0.25; }
+}
+
+@keyframes bullet-hell-engage-flashin {
+ 0% { opacity: 0; transform: scale(0.7); }
+ 25% { opacity: 1; transform: scale(1.06); }
+ 40% { opacity: 0.25; }
+ 55% { opacity: 1; transform: scale(1); }
+ 70% { opacity: 0.35; }
+ 100% { opacity: 1; }
+}
+
+.bullet-hell-engage {
+ display: none;
+ cursor: pointer;
+ user-select: none;
+ outline: none;
+ margin-top: 12px;
+ align-self: flex-start;
+}
+
+.bullet-hell-engage--ready {
+ display: inline-block;
+}
+
+.bullet-hell-engage--ready .bullet-hell-engage-panel {
+ animation: bullet-hell-engage-flashin 850ms cubic-bezier(0.2, 1.0, 0.4, 1);
+}
+
+.bullet-hell-engage:focus-visible .bullet-hell-engage-panel {
+ box-shadow: 0 0 0 3px rgba(204, 255, 0, 0.6), 0 0 60px rgba(204, 255, 0, 0.4);
+}
+
+.bullet-hell-engage-panel {
+ background: rgba(0, 0, 0, 0.85);
+ border: 2px solid rgba(204, 255, 0, 0.55);
+ border-radius: 12px;
+ padding: 18px 28px;
+ display: inline-flex;
+ align-items: center;
+ gap: 24px;
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
+ box-shadow: 0 0 32px rgba(204, 255, 0, 0.15);
+ transition: transform 200ms cubic-bezier(0.2, 0.8, 0.4, 1), box-shadow 200ms;
+}
+
+.bullet-hell-engage:hover .bullet-hell-engage-panel {
+ transform: scale(1.02);
+ box-shadow: 0 0 60px rgba(204, 255, 0, 0.4);
+ border-color: rgba(204, 255, 0, 0.85);
+}
+
+.bullet-hell-engage-gorilla {
+ margin: 0;
+ padding: 0;
+ color: rgba(204, 255, 0, 0.8);
+ font-size: 8px;
+ line-height: 1.15;
+ white-space: pre;
+ text-shadow: 0 0 6px rgba(0, 0, 0, 0.95);
+}
+
+.bullet-hell-engage-title {
+ font-size: 24px;
+ font-weight: 800;
+ color: #ccff00;
+ letter-spacing: 0.06em;
+ text-shadow: 0 0 12px rgba(204, 255, 0, 0.4);
+ animation: bullet-hell-flash-blink 1100ms infinite;
+}
+
+.bullet-hell-engage-subtitle {
+ font-size: 11px;
+ margin-top: 8px;
+ color: rgba(204, 255, 0, 0.6);
+ letter-spacing: 0.1em;
+}