claude-code-best/packages/pokemon/src/ui/BattleField.tsx
2026-04-22 13:33:02 +08:00

99 lines
3.1 KiB
TypeScript

import React, { useEffect, useState, useMemo } from 'react'
import { Box, Text } from '@anthropic/ink'
import { parseSprite, renderSprite, flipSpriteLines, EMPTY_PIXEL, EMPTY_ROW } from '../sprites/renderer'
import type { Pixel } from '../sprites/renderer'
/**
* Combined battle field — composites both sprites into one canvas.
* Opponent (top-right) and player (bottom-left) share overlapping rows,
* like the classic GBA Pokemon battle layout.
*
* Bounce: fast 0-1-2-1px vertical, staggered between the two.
*/
const BOUNCE = [0, 1, 2, 1]
/** How many rows the player sprite overlaps into opponent's area */
const OVERLAP = 3
interface BattleFieldProps {
opponentLines: string[]
playerLines: string[]
animEnabled?: boolean
}
export function BattleField({ opponentLines, playerLines, animEnabled = true }: BattleFieldProps) {
const [tick, setTick] = useState(0)
useEffect(() => {
if (!animEnabled) return
const timer = setInterval(() => setTick(t => t + 1), 120)
return () => clearInterval(timer)
}, [animEnabled])
// Parse & flip (cached)
const oppGrid = useMemo(() => parseSprite(opponentLines), [opponentLines])
const playerGrid = useMemo(() => parseSprite(flipSpriteLines(playerLines)), [playerLines])
// Composited canvas
const canvas = useMemo(() => {
const oppH = oppGrid.length
const playerH = playerGrid.length
const totalH = oppH + playerH - OVERLAP
const canvasW = Math.max(
widthOf(oppGrid),
widthOf(playerGrid),
)
// Build empty canvas
const rows: Pixel[][] = Array.from({ length: totalH }, () =>
Array.from({ length: canvasW }, () => EMPTY_PIXEL),
)
// Bounce offsets
const oppOffset = animEnabled ? BOUNCE[tick % BOUNCE.length]! : 0
const playerOffset = animEnabled ? BOUNCE[(tick + 2) % BOUNCE.length]! : 0
// Blit opponent (top-right, shifted up by bounce)
const oppY = -oppOffset // negative = shift up
blit(rows, oppGrid, oppY, canvasW - widthOf(oppGrid))
// Blit player (bottom-left, shifted up by bounce)
const playerStartRow = oppH - OVERLAP
const playerY = playerStartRow - playerOffset
blit(rows, playerGrid, playerY, 0)
return rows
}, [oppGrid, playerGrid, animEnabled, tick])
const rendered = renderSprite(canvas)
return (
<Box flexDirection="column">
{rendered.map((line, i) => (
<Text key={i}>{line || ' '}</Text>
))}
</Box>
)
}
/** Get width of a pixel grid */
function widthOf(grid: Pixel[][]): number {
return Math.max(0, ...grid.map(row => row.length))
}
/** Blit source grid onto target at (startRow, startCol). Non-empty pixels overwrite. */
function blit(target: Pixel[][], source: Pixel[][], startRow: number, startCol: number) {
for (let sy = 0; sy < source.length; sy++) {
const ty = startRow + sy
if (ty < 0 || ty >= target.length) continue
for (let sx = 0; sx < source[sy].length; sx++) {
const tx = startCol + sx
if (tx < 0 || tx >= target[ty].length) continue
const pixel = source[sy][sx]
if (pixel.char !== ' ') {
target[ty][tx] = pixel
}
}
}
}