{"version":3,"file":"armin.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/armin.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,wBAAwB,CAAC;AAuD7D,qBAAa,cAAe,YAAW,SAAS;IAC/C,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,aAAa,CAAM;IAE3B,YAAY,EAAE,EAAE,GAAG,EAQlB;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAwB9B;IAED,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,UAAU;IAgElB,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,UAAU;IAqBlB,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,QAAQ;IAmDhB,OAAO,CAAC,QAAQ;IAahB,OAAO,CAAC,OAAO;IAoBf,OAAO,CAAC,UAAU;IAgClB,OAAO,CAAC,YAAY;IAapB,OAAO,CAAC,aAAa;IAIrB,OAAO,IAAI,IAAI,CAEd;CACD","sourcesContent":["/**\n * Armin says hi! A fun easter egg with animated XBM art.\n */\n\nimport type { Component, TUI } from \"@earendil-works/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\n\n// XBM image: 31x36 pixels, LSB first, 1=background, 0=foreground\nconst WIDTH = 31;\nconst HEIGHT = 36;\nconst BITS = [\n\t0xff, 0xff, 0xff, 0x7f, 0xff, 0xf0, 0xff, 0x7f, 0xff, 0xed, 0xff, 0x7f, 0xff, 0xdb, 0xff, 0x7f, 0xff, 0xb7, 0xff,\n\t0x7f, 0xff, 0x77, 0xfe, 0x7f, 0x3f, 0xf8, 0xfe, 0x7f, 0xdf, 0xff, 0xfe, 0x7f, 0xdf, 0x3f, 0xfc, 0x7f, 0x9f, 0xc3,\n\t0xfb, 0x7f, 0x6f, 0xfc, 0xf4, 0x7f, 0xf7, 0x0f, 0xf7, 0x7f, 0xf7, 0xff, 0xf7, 0x7f, 0xf7, 0xff, 0xe3, 0x7f, 0xf7,\n\t0x07, 0xe8, 0x7f, 0xef, 0xf8, 0x67, 0x70, 0x0f, 0xff, 0xbb, 0x6f, 0xf1, 0x00, 0xd0, 0x5b, 0xfd, 0x3f, 0xec, 0x53,\n\t0xc1, 0xff, 0xef, 0x57, 0x9f, 0xfd, 0xee, 0x5f, 0x9f, 0xfc, 0xae, 0x5f, 0x1f, 0x78, 0xac, 0x5f, 0x3f, 0x00, 0x50,\n\t0x6c, 0x7f, 0x00, 0xdc, 0x77, 0xff, 0xc0, 0x3f, 0x78, 0xff, 0x01, 0xf8, 0x7f, 0xff, 0x03, 0x9c, 0x78, 0xff, 0x07,\n\t0x8c, 0x7c, 0xff, 0x0f, 0xce, 0x78, 0xff, 0xff, 0xcf, 0x7f, 0xff, 0xff, 0xcf, 0x78, 0xff, 0xff, 0xdf, 0x78, 0xff,\n\t0xff, 0xdf, 0x7d, 0xff, 0xff, 0x3f, 0x7e, 0xff, 0xff, 0xff, 0x7f,\n];\n\nconst BYTES_PER_ROW = Math.ceil(WIDTH / 8);\nconst DISPLAY_HEIGHT = Math.ceil(HEIGHT / 2); // Half-block rendering\n\ntype Effect = \"typewriter\" | \"scanline\" | \"rain\" | \"fade\" | \"crt\" | \"glitch\" | \"dissolve\";\n\nconst EFFECTS: Effect[] = [\"typewriter\", \"scanline\", \"rain\", \"fade\", \"crt\", \"glitch\", \"dissolve\"];\n\n// Get pixel at (x, y): true = foreground, false = background\nfunction getPixel(x: number, y: number): boolean {\n\tif (y >= HEIGHT) return false;\n\tconst byteIndex = y * BYTES_PER_ROW + Math.floor(x / 8);\n\tconst bitIndex = x % 8;\n\treturn ((BITS[byteIndex] >> bitIndex) & 1) === 0;\n}\n\n// Get the character for a cell (2 vertical pixels packed)\nfunction getChar(x: number, row: number): string {\n\tconst upper = getPixel(x, row * 2);\n\tconst lower = getPixel(x, row * 2 + 1);\n\tif (upper && lower) return \"█\";\n\tif (upper) return \"▀\";\n\tif (lower) return \"▄\";\n\treturn \" \";\n}\n\n// Build the final image grid\nfunction buildFinalGrid(): string[][] {\n\tconst grid: string[][] = [];\n\tfor (let row = 0; row < DISPLAY_HEIGHT; row++) {\n\t\tconst line: string[] = [];\n\t\tfor (let x = 0; x < WIDTH; x++) {\n\t\t\tline.push(getChar(x, row));\n\t\t}\n\t\tgrid.push(line);\n\t}\n\treturn grid;\n}\n\nexport class ArminComponent implements Component {\n\tprivate ui: TUI;\n\tprivate interval: ReturnType<typeof setInterval> | null = null;\n\tprivate effect: Effect;\n\tprivate finalGrid: string[][];\n\tprivate currentGrid: string[][];\n\tprivate effectState: Record<string, unknown> = {};\n\tprivate cachedLines: string[] = [];\n\tprivate cachedWidth = 0;\n\tprivate gridVersion = 0;\n\tprivate cachedVersion = -1;\n\n\tconstructor(ui: TUI) {\n\t\tthis.ui = ui;\n\t\tthis.effect = EFFECTS[Math.floor(Math.random() * EFFECTS.length)];\n\t\tthis.finalGrid = buildFinalGrid();\n\t\tthis.currentGrid = this.createEmptyGrid();\n\n\t\tthis.initEffect();\n\t\tthis.startAnimation();\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedWidth = 0;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (width === this.cachedWidth && this.cachedVersion === this.gridVersion) {\n\t\t\treturn this.cachedLines;\n\t\t}\n\n\t\tconst padding = 1;\n\t\tconst availableWidth = width - padding;\n\n\t\tthis.cachedLines = this.currentGrid.map((row) => {\n\t\t\t// Clip row to available width before applying color\n\t\t\tconst clipped = row.slice(0, availableWidth).join(\"\");\n\t\t\tconst padRight = Math.max(0, width - padding - clipped.length);\n\t\t\treturn ` ${theme.fg(\"accent\", clipped)}${\" \".repeat(padRight)}`;\n\t\t});\n\n\t\t// Add \"ARMIN SAYS HI\" at the end\n\t\tconst message = \"ARMIN SAYS HI\";\n\t\tconst msgPadRight = Math.max(0, width - padding - message.length);\n\t\tthis.cachedLines.push(` ${theme.fg(\"accent\", message)}${\" \".repeat(msgPadRight)}`);\n\n\t\tthis.cachedWidth = width;\n\t\tthis.cachedVersion = this.gridVersion;\n\n\t\treturn this.cachedLines;\n\t}\n\n\tprivate createEmptyGrid(): string[][] {\n\t\treturn Array.from({ length: DISPLAY_HEIGHT }, () => Array(WIDTH).fill(\" \"));\n\t}\n\n\tprivate initEffect(): void {\n\t\tswitch (this.effect) {\n\t\t\tcase \"typewriter\":\n\t\t\t\tthis.effectState = { pos: 0 };\n\t\t\t\tbreak;\n\t\t\tcase \"scanline\":\n\t\t\t\tthis.effectState = { row: 0 };\n\t\t\t\tbreak;\n\t\t\tcase \"rain\":\n\t\t\t\t// Track falling position for each column\n\t\t\t\tthis.effectState = {\n\t\t\t\t\tdrops: Array.from({ length: WIDTH }, () => ({\n\t\t\t\t\t\ty: -Math.floor(Math.random() * DISPLAY_HEIGHT * 2),\n\t\t\t\t\t\tsettled: 0,\n\t\t\t\t\t})),\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase \"fade\": {\n\t\t\t\t// Shuffle all pixel positions\n\t\t\t\tconst positions: [number, number][] = [];\n\t\t\t\tfor (let row = 0; row < DISPLAY_HEIGHT; row++) {\n\t\t\t\t\tfor (let x = 0; x < WIDTH; x++) {\n\t\t\t\t\t\tpositions.push([row, x]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Fisher-Yates shuffle\n\t\t\t\tfor (let i = positions.length - 1; i > 0; i--) {\n\t\t\t\t\tconst j = Math.floor(Math.random() * (i + 1));\n\t\t\t\t\t[positions[i], positions[j]] = [positions[j], positions[i]];\n\t\t\t\t}\n\t\t\t\tthis.effectState = { positions, idx: 0 };\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"crt\":\n\t\t\t\tthis.effectState = { expansion: 0 };\n\t\t\t\tbreak;\n\t\t\tcase \"glitch\":\n\t\t\t\tthis.effectState = { phase: 0, glitchFrames: 8 };\n\t\t\t\tbreak;\n\t\t\tcase \"dissolve\": {\n\t\t\t\t// Start with random noise\n\t\t\t\tthis.currentGrid = Array.from({ length: DISPLAY_HEIGHT }, () =>\n\t\t\t\t\tArray.from({ length: WIDTH }, () => {\n\t\t\t\t\t\tconst chars = [\" \", \"░\", \"▒\", \"▓\", \"█\", \"▀\", \"▄\"];\n\t\t\t\t\t\treturn chars[Math.floor(Math.random() * chars.length)];\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\t// Shuffle positions for gradual resolve\n\t\t\t\tconst dissolvePositions: [number, number][] = [];\n\t\t\t\tfor (let row = 0; row < DISPLAY_HEIGHT; row++) {\n\t\t\t\t\tfor (let x = 0; x < WIDTH; x++) {\n\t\t\t\t\t\tdissolvePositions.push([row, x]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (let i = dissolvePositions.length - 1; i > 0; i--) {\n\t\t\t\t\tconst j = Math.floor(Math.random() * (i + 1));\n\t\t\t\t\t[dissolvePositions[i], dissolvePositions[j]] = [dissolvePositions[j], dissolvePositions[i]];\n\t\t\t\t}\n\t\t\t\tthis.effectState = { positions: dissolvePositions, idx: 0 };\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate startAnimation(): void {\n\t\tconst fps = this.effect === \"glitch\" ? 60 : 30;\n\t\tthis.interval = setInterval(() => {\n\t\t\tconst done = this.tickEffect();\n\t\t\tthis.updateDisplay();\n\t\t\tthis.ui.requestRender();\n\t\t\tif (done) {\n\t\t\t\tthis.stopAnimation();\n\t\t\t}\n\t\t}, 1000 / fps);\n\t}\n\n\tprivate stopAnimation(): void {\n\t\tif (this.interval) {\n\t\t\tclearInterval(this.interval);\n\t\t\tthis.interval = null;\n\t\t}\n\t}\n\n\tprivate tickEffect(): boolean {\n\t\tswitch (this.effect) {\n\t\t\tcase \"typewriter\":\n\t\t\t\treturn this.tickTypewriter();\n\t\t\tcase \"scanline\":\n\t\t\t\treturn this.tickScanline();\n\t\t\tcase \"rain\":\n\t\t\t\treturn this.tickRain();\n\t\t\tcase \"fade\":\n\t\t\t\treturn this.tickFade();\n\t\t\tcase \"crt\":\n\t\t\t\treturn this.tickCrt();\n\t\t\tcase \"glitch\":\n\t\t\t\treturn this.tickGlitch();\n\t\t\tcase \"dissolve\":\n\t\t\t\treturn this.tickDissolve();\n\t\t\tdefault:\n\t\t\t\treturn true;\n\t\t}\n\t}\n\n\tprivate tickTypewriter(): boolean {\n\t\tconst state = this.effectState as { pos: number };\n\t\tconst pixelsPerFrame = 3;\n\n\t\tfor (let i = 0; i < pixelsPerFrame; i++) {\n\t\t\tconst row = Math.floor(state.pos / WIDTH);\n\t\t\tconst x = state.pos % WIDTH;\n\t\t\tif (row >= DISPLAY_HEIGHT) return true;\n\t\t\tthis.currentGrid[row][x] = this.finalGrid[row][x];\n\t\t\tstate.pos++;\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate tickScanline(): boolean {\n\t\tconst state = this.effectState as { row: number };\n\t\tif (state.row >= DISPLAY_HEIGHT) return true;\n\n\t\t// Copy row\n\t\tfor (let x = 0; x < WIDTH; x++) {\n\t\t\tthis.currentGrid[state.row][x] = this.finalGrid[state.row][x];\n\t\t}\n\t\tstate.row++;\n\t\treturn false;\n\t}\n\n\tprivate tickRain(): boolean {\n\t\tconst state = this.effectState as {\n\t\t\tdrops: { y: number; settled: number }[];\n\t\t};\n\n\t\tlet allSettled = true;\n\t\tthis.currentGrid = this.createEmptyGrid();\n\n\t\tfor (let x = 0; x < WIDTH; x++) {\n\t\t\tconst drop = state.drops[x];\n\n\t\t\t// Draw settled pixels\n\t\t\tfor (let row = DISPLAY_HEIGHT - 1; row >= DISPLAY_HEIGHT - drop.settled; row--) {\n\t\t\t\tif (row >= 0) {\n\t\t\t\t\tthis.currentGrid[row][x] = this.finalGrid[row][x];\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Check if this column is done\n\t\t\tif (drop.settled >= DISPLAY_HEIGHT) continue;\n\n\t\t\tallSettled = false;\n\n\t\t\t// Find the target row for this column (lowest non-space pixel)\n\t\t\tlet targetRow = -1;\n\t\t\tfor (let row = DISPLAY_HEIGHT - 1 - drop.settled; row >= 0; row--) {\n\t\t\t\tif (this.finalGrid[row][x] !== \" \") {\n\t\t\t\t\ttargetRow = row;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Move drop down\n\t\t\tdrop.y++;\n\n\t\t\t// Draw falling drop\n\t\t\tif (drop.y >= 0 && drop.y < DISPLAY_HEIGHT) {\n\t\t\t\tif (targetRow >= 0 && drop.y >= targetRow) {\n\t\t\t\t\t// Settle\n\t\t\t\t\tdrop.settled = DISPLAY_HEIGHT - targetRow;\n\t\t\t\t\tdrop.y = -Math.floor(Math.random() * 5) - 1;\n\t\t\t\t} else {\n\t\t\t\t\t// Still falling\n\t\t\t\t\tthis.currentGrid[drop.y][x] = \"▓\";\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn allSettled;\n\t}\n\n\tprivate tickFade(): boolean {\n\t\tconst state = this.effectState as { positions: [number, number][]; idx: number };\n\t\tconst pixelsPerFrame = 15;\n\n\t\tfor (let i = 0; i < pixelsPerFrame; i++) {\n\t\t\tif (state.idx >= state.positions.length) return true;\n\t\t\tconst [row, x] = state.positions[state.idx];\n\t\t\tthis.currentGrid[row][x] = this.finalGrid[row][x];\n\t\t\tstate.idx++;\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate tickCrt(): boolean {\n\t\tconst state = this.effectState as { expansion: number };\n\t\tconst midRow = Math.floor(DISPLAY_HEIGHT / 2);\n\n\t\tthis.currentGrid = this.createEmptyGrid();\n\n\t\t// Draw from middle expanding outward\n\t\tconst top = midRow - state.expansion;\n\t\tconst bottom = midRow + state.expansion;\n\n\t\tfor (let row = Math.max(0, top); row <= Math.min(DISPLAY_HEIGHT - 1, bottom); row++) {\n\t\t\tfor (let x = 0; x < WIDTH; x++) {\n\t\t\t\tthis.currentGrid[row][x] = this.finalGrid[row][x];\n\t\t\t}\n\t\t}\n\n\t\tstate.expansion++;\n\t\treturn state.expansion > DISPLAY_HEIGHT;\n\t}\n\n\tprivate tickGlitch(): boolean {\n\t\tconst state = this.effectState as { phase: number; glitchFrames: number };\n\n\t\tif (state.phase < state.glitchFrames) {\n\t\t\t// Glitch phase: show corrupted version\n\t\t\tthis.currentGrid = this.finalGrid.map((row) => {\n\t\t\t\tconst offset = Math.floor(Math.random() * 7) - 3;\n\t\t\t\tconst glitchRow = [...row];\n\n\t\t\t\t// Random horizontal offset\n\t\t\t\tif (Math.random() < 0.3) {\n\t\t\t\t\tconst shifted = glitchRow.slice(offset).concat(glitchRow.slice(0, offset));\n\t\t\t\t\treturn shifted.slice(0, WIDTH);\n\t\t\t\t}\n\n\t\t\t\t// Random vertical swap\n\t\t\t\tif (Math.random() < 0.2) {\n\t\t\t\t\tconst swapRow = Math.floor(Math.random() * DISPLAY_HEIGHT);\n\t\t\t\t\treturn [...this.finalGrid[swapRow]];\n\t\t\t\t}\n\n\t\t\t\treturn glitchRow;\n\t\t\t});\n\t\t\tstate.phase++;\n\t\t\treturn false;\n\t\t}\n\n\t\t// Final frame: show clean image\n\t\tthis.currentGrid = this.finalGrid.map((row) => [...row]);\n\t\treturn true;\n\t}\n\n\tprivate tickDissolve(): boolean {\n\t\tconst state = this.effectState as { positions: [number, number][]; idx: number };\n\t\tconst pixelsPerFrame = 20;\n\n\t\tfor (let i = 0; i < pixelsPerFrame; i++) {\n\t\t\tif (state.idx >= state.positions.length) return true;\n\t\t\tconst [row, x] = state.positions[state.idx];\n\t\t\tthis.currentGrid[row][x] = this.finalGrid[row][x];\n\t\t\tstate.idx++;\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tthis.gridVersion++;\n\t}\n\n\tdispose(): void {\n\t\tthis.stopAnimation();\n\t}\n}\n"]}