mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
feat: 解决显示问题
This commit is contained in:
@@ -3,7 +3,7 @@ import { Protocol } from '@pkmn/protocol'
|
||||
import type { Creature, SpeciesId } from '../types'
|
||||
import { TO_DEX_STAT, FROM_DEX_STAT } from '../dex/pkmn'
|
||||
import { STAT_NAMES } from '../types'
|
||||
import type { BattleState, BattlePokemon, BattleEvent, PlayerAction, StatusCondition } from './types'
|
||||
import type { BattleState, BattlePokemon, BattleEvent, PlayerAction, StatusCondition, WeatherKind, FieldCondition } from './types'
|
||||
import { chooseAIMove } from './ai'
|
||||
|
||||
// ─── Types ───
|
||||
@@ -145,9 +145,13 @@ function projectBoosts(boosts: Record<string, number> | undefined): Record<strin
|
||||
return result
|
||||
}
|
||||
|
||||
function projectState(battle: any, bagItems?: { id: string; count: number }[]): BattleState {
|
||||
function projectState(battle: any, bagItems?: { id: string; count: number }[], prevConditions?: { player: FieldCondition[]; opponent: FieldCondition[] }): BattleState {
|
||||
const p1 = battle.p1
|
||||
const p2 = battle.p2
|
||||
// Extract weather from battle field
|
||||
const weatherRaw = battle.field?.weather ?? ''
|
||||
const weather = mapWeather(weatherRaw)
|
||||
|
||||
return {
|
||||
playerPokemon: projectPokemon(p1.active[0]),
|
||||
opponentPokemon: projectPokemon(p2.active[0]),
|
||||
@@ -157,9 +161,39 @@ function projectState(battle: any, bagItems?: { id: string; count: number }[]):
|
||||
events: [],
|
||||
finished: battle.ended,
|
||||
usableItems: bagItems?.filter(i => i.count > 0).map(i => ({ id: i.id, name: i.id, count: i.count })) ?? [],
|
||||
weather,
|
||||
playerConditions: prevConditions?.player ?? projectSideConditions(p1),
|
||||
opponentConditions: prevConditions?.opponent ?? projectSideConditions(p2),
|
||||
}
|
||||
}
|
||||
|
||||
function mapWeather(raw: string): WeatherKind | undefined {
|
||||
if (!raw) return undefined
|
||||
const w = raw.toLowerCase()
|
||||
if (w.includes('sun') || w.includes('desolateland')) return 'sun'
|
||||
if (w.includes('rain') || w.includes('primordialsea')) return 'rain'
|
||||
if (w.includes('sandstorm')) return 'sandstorm'
|
||||
if (w.includes('hail')) return 'hail'
|
||||
if (w.includes('snow')) return 'snow'
|
||||
if (w.includes('deltastream')) return 'deltastream'
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Extract field conditions from a side object */
|
||||
function projectSideConditions(side: any): FieldCondition[] {
|
||||
const conditions: FieldCondition[] = []
|
||||
if (!side) return conditions
|
||||
const sr = side.sideConditions?.stealthrock
|
||||
if (sr) conditions.push({ id: 'Stealth Rock', side: side === side.battle?.p1 ? 'player' as const : 'opponent' as const, level: 1 })
|
||||
const spikes = side.sideConditions?.spikes
|
||||
if (spikes) conditions.push({ id: 'Spikes', side: side === side.battle?.p1 ? 'player' as const : 'opponent' as const, level: spikes.levels ?? 1 })
|
||||
const tspikes = side.sideConditions?.toxicspikes
|
||||
if (tspikes) conditions.push({ id: 'Toxic Spikes', side: side === side.battle?.p1 ? 'player' as const : 'opponent' as const, level: tspikes.levels ?? 1 })
|
||||
const webs = side.sideConditions?.stickyweb
|
||||
if (webs) conditions.push({ id: 'Sticky Web', side: side === side.battle?.p1 ? 'player' as const : 'opponent' as const, level: 1 })
|
||||
return conditions
|
||||
}
|
||||
|
||||
// ─── Protocol Event Parsing (from spectator chunks) ───
|
||||
|
||||
function parseChunkToEvents(chunk: string, prevHp?: { player: { hp: number; maxHp: number }; opponent: { hp: number; maxHp: number } }): BattleEvent[] {
|
||||
@@ -169,11 +203,11 @@ function parseChunkToEvents(chunk: string, prevHp?: { player: { hp: number; maxH
|
||||
|
||||
for (const line of chunk.split('\n')) {
|
||||
if (!line.startsWith('|')) continue
|
||||
// Skip non-battle lines
|
||||
// Skip non-battle lines (but NOT |upkeep| anymore!)
|
||||
if (line.startsWith('|t:|') || line === '|' || line.startsWith('|gametype|') || line.startsWith('|player|') ||
|
||||
line.startsWith('|gen|') || line.startsWith('|tier|') || line.startsWith('|clearpoke|') ||
|
||||
line.startsWith('|poke|') || line.startsWith('|teampreview|') || line.startsWith('|teamsize|') ||
|
||||
line.startsWith('|start|') || line.startsWith('|done|') || line.startsWith('|upkeep|')) continue
|
||||
line.startsWith('|start|') || line.startsWith('|done|')) continue
|
||||
|
||||
const parts = line.split('|')
|
||||
const cmd = parts[1]
|
||||
@@ -244,6 +278,10 @@ function parseChunkToEvents(chunk: string, prevHp?: { player: { hp: number; maxH
|
||||
case '-status':
|
||||
events.push({ type: 'status', side, status: mapStatus(parts[3]) })
|
||||
break
|
||||
case '-curestatus':
|
||||
// Pokémon cured of status — represent as status 'none'
|
||||
events.push({ type: 'status', side, status: 'none' })
|
||||
break
|
||||
case '-boost':
|
||||
case '-unboost': {
|
||||
const stages = cmd === '-boost' ? Number(parts[4]) : -Number(parts[4])
|
||||
@@ -253,6 +291,58 @@ function parseChunkToEvents(chunk: string, prevHp?: { player: { hp: number; maxH
|
||||
case '-ability':
|
||||
events.push({ type: 'ability', side, ability: parts[3] ?? '' })
|
||||
break
|
||||
case '-item':
|
||||
events.push({ type: 'item', side, item: parts[3] ?? '' })
|
||||
break
|
||||
case 'fail':
|
||||
events.push({ type: 'fail', side, reason: parts[3] ?? '' })
|
||||
break
|
||||
case '-fail':
|
||||
events.push({ type: 'fail', side, reason: parts[3] ?? '' })
|
||||
break
|
||||
case '-weather': {
|
||||
const weatherRaw = parts[2] ?? ''
|
||||
if (weatherRaw === 'none' || weatherRaw === '') {
|
||||
events.push({ type: 'weather', weather: 'none' })
|
||||
} else {
|
||||
const weather = mapWeather(weatherRaw)
|
||||
events.push({ type: 'weather', weather: weather ?? 'none', source: parts[3] ?? undefined })
|
||||
}
|
||||
break
|
||||
}
|
||||
case '-fieldstart':
|
||||
case '-fieldend': {
|
||||
const fieldId = parts[2] ?? ''
|
||||
const action = cmd === '-fieldstart' ? 'add' as const : 'remove' as const
|
||||
// Terrains etc. — map to fieldCondition
|
||||
events.push({ type: 'fieldCondition', side: 'player', id: fieldId, level: 1, action })
|
||||
break
|
||||
}
|
||||
case '-sidestart': {
|
||||
const conditionId = parts[3] ?? ''
|
||||
const condSide = parts[2]?.startsWith('p1') ? 'player' as const : 'opponent' as const
|
||||
const level = conditionId.match(/\d/) ? parseInt(conditionId.match(/\d/)![0], 10) : 1
|
||||
const cleanId = conditionId.replace(/\d+$/, '').trim()
|
||||
events.push({ type: 'fieldCondition', side: condSide, id: cleanId, level, action: 'add' })
|
||||
break
|
||||
}
|
||||
case '-sideend': {
|
||||
const conditionId = parts[3] ?? ''
|
||||
const condSide = parts[2]?.startsWith('p1') ? 'player' as const : 'opponent' as const
|
||||
events.push({ type: 'fieldCondition', side: condSide, id: conditionId, level: 0, action: 'remove' })
|
||||
break
|
||||
}
|
||||
case '-activate': {
|
||||
const effect = parts[3] ?? parts[2] ?? ''
|
||||
events.push({ type: 'activate', side, effect })
|
||||
break
|
||||
}
|
||||
case '-immune':
|
||||
events.push({ type: 'immune', side })
|
||||
break
|
||||
case 'upkeep':
|
||||
events.push({ type: 'upkeep' })
|
||||
break
|
||||
case 'turn':
|
||||
events.push({ type: 'turn', number: Number(parts[2]) })
|
||||
break
|
||||
@@ -317,7 +407,7 @@ export async function createBattle(
|
||||
|
||||
// Use Battle object for rich state projection
|
||||
const battle = stream.battle!
|
||||
const state = projectState(battle, _bagItems)
|
||||
const state = projectState(battle, _bagItems, { player: [], opponent: [] })
|
||||
state.events = initialEvents
|
||||
|
||||
return { streams, stream, state }
|
||||
@@ -365,8 +455,11 @@ export async function executeTurn(
|
||||
opponent: { hp: prevState.opponentPokemon.hp, maxHp: prevState.opponentPokemon.maxHp },
|
||||
})
|
||||
|
||||
// Project rich state from Battle object
|
||||
const state = projectState(battle, prevState.usableItems)
|
||||
// Project rich state from Battle object, preserving field conditions
|
||||
const state = projectState(battle, prevState.usableItems, {
|
||||
player: prevState.playerConditions,
|
||||
opponent: prevState.opponentConditions,
|
||||
})
|
||||
state.events = [...prevState.events, ...newEvents]
|
||||
|
||||
// Forced switch detection via Battle object
|
||||
@@ -429,7 +522,10 @@ export async function executeSwitch(
|
||||
})
|
||||
|
||||
// Project state
|
||||
const state = projectState(battle, prevState.usableItems)
|
||||
const state = projectState(battle, prevState.usableItems, {
|
||||
player: prevState.playerConditions,
|
||||
opponent: prevState.opponentConditions,
|
||||
})
|
||||
state.events = [...prevState.events, ...newEvents]
|
||||
|
||||
// Forced switch detection via Battle object
|
||||
|
||||
@@ -31,6 +31,15 @@ export type PlayerAction =
|
||||
| { type: 'switch'; partyIndex: number }
|
||||
| { type: 'item'; itemId: string }
|
||||
|
||||
export type WeatherKind = 'sun' | 'rain' | 'sandstorm' | 'hail' | 'snow' | 'desolateland' | 'primordialsea' | 'deltastream'
|
||||
|
||||
export type FieldCondition = {
|
||||
/** e.g. 'Stealth Rock', 'Spikes', 'Toxic Spikes', 'Sticky Web' */
|
||||
id: string
|
||||
side: 'player' | 'opponent'
|
||||
level: number // 1-3 for Spikes/Toxic Spikes, 1 for others
|
||||
}
|
||||
|
||||
export type BattleEvent =
|
||||
| { type: 'move'; side: 'player' | 'opponent'; move: string; user: string }
|
||||
| { type: 'damage'; side: 'player' | 'opponent'; amount: number; percentage: number }
|
||||
@@ -44,7 +53,12 @@ export type BattleEvent =
|
||||
| { type: 'statChange'; side: 'player' | 'opponent'; stat: string; stages: number }
|
||||
| { type: 'ability'; side: 'player' | 'opponent'; ability: string }
|
||||
| { type: 'item'; side: 'player' | 'opponent'; item: string }
|
||||
| { type: 'fail'; reason: string }
|
||||
| { type: 'fail'; side: 'player' | 'opponent'; reason: string }
|
||||
| { type: 'weather'; weather: WeatherKind | 'none'; source?: string }
|
||||
| { type: 'upkeep' }
|
||||
| { type: 'fieldCondition'; side: 'player' | 'opponent'; id: string; level: number; action: 'add' | 'remove' }
|
||||
| { type: 'activate'; side: 'player' | 'opponent'; effect: string }
|
||||
| { type: 'immune'; side: 'player' | 'opponent' }
|
||||
| { type: 'turn'; number: number }
|
||||
|
||||
export type BattleResult = {
|
||||
@@ -66,4 +80,7 @@ export type BattleState = {
|
||||
result?: BattleResult
|
||||
usableItems: { id: string; name: string; count: number }[]
|
||||
needsSwitch?: boolean // player's active Pokémon fainted, must switch
|
||||
weather?: WeatherKind // current weather
|
||||
playerConditions: FieldCondition[] // hazards on player's side
|
||||
opponentConditions: FieldCondition[] // hazards on opponent's side
|
||||
}
|
||||
|
||||
@@ -442,7 +442,7 @@ export function BattleFlow({ buddyData: initialData, onClose, isActive = true, i
|
||||
if (key.return) handleEvolutionConfirm()
|
||||
return
|
||||
}
|
||||
}, [isActive, phase, menuPhase, cursorIndex, speciesIndex, opponentSpeciesId, buddyData, battleState, battleInit, pendingMoves, pendingEvos, onClose, handleRandomBattle, handleStartBattle, handleAction, handleResultContinue, handleForcedSwitch, handleMoveLearn, handleMoveSkip, handleEvolutionConfirm, moveMainCursor])
|
||||
}, [isActive, phase, menuPhase, cursorIndex, configCursor, speciesIndex, opponentSpeciesId, buddyData, battleState, battleInit, pendingMoves, pendingEvos, onClose, handleRandomBattle, handleStartBattle, handleAction, handleResultContinue, handleForcedSwitch, handleMoveLearn, handleMoveSkip, handleEvolutionConfirm, moveMainCursor])
|
||||
|
||||
// Expose handleInput via ref
|
||||
useEffect(() => {
|
||||
|
||||
@@ -17,10 +17,23 @@ function eventColor(event: BattleEvent): string {
|
||||
case 'status': return 'warning'
|
||||
case 'switch': return 'claude'
|
||||
case 'turn': return 'inactive'
|
||||
case 'weather': return 'claude'
|
||||
case 'fieldCondition': return 'warning'
|
||||
case 'activate': return 'claude'
|
||||
case 'immune': return 'inactive'
|
||||
case 'upkeep': return 'inactive'
|
||||
case 'ability': return 'claude'
|
||||
case 'item': return 'warning'
|
||||
case 'fail': return 'inactive'
|
||||
default: return 'inactive'
|
||||
}
|
||||
}
|
||||
|
||||
const WEATHER_NAMES: Record<string, string> = {
|
||||
sun: '大晴天', rain: '雨天', sandstorm: '沙暴', hail: '冰雹',
|
||||
snow: '下雪', desolateland: '大日照', primordialsea: '大雨', deltastream: '强气流',
|
||||
}
|
||||
|
||||
function formatEvent(event: BattleEvent): string {
|
||||
switch (event.type) {
|
||||
case 'move': return `${event.side === 'player' ? '我方' : '对手'}使用了 ${event.move}!`
|
||||
@@ -30,7 +43,7 @@ function formatEvent(event: BattleEvent): string {
|
||||
case 'crit': return '击中要害!'
|
||||
case 'miss': return '攻击没有命中!'
|
||||
case 'effectiveness': return event.multiplier > 1 ? '效果拔群!' : '效果不佳...'
|
||||
case 'status': return `${event.side === 'player' ? '我方' : '对手'}陷入了${event.status}状态!`
|
||||
case 'status': return `${event.side === 'player' ? '我方' : '对手'}${event.status === 'none' ? '恢复了异常状态!' : `陷入了${event.status}状态!`}`
|
||||
case 'switch': return `${event.side === 'player' ? '我方' : '对手'}换上了 ${event.name}!`
|
||||
case 'turn': return `── 回合 ${event.number} ──`
|
||||
case 'statChange': {
|
||||
@@ -38,8 +51,17 @@ function formatEvent(event: BattleEvent): string {
|
||||
return `${event.side === 'player' ? '我方' : '对手'}的 ${event.stat} ${sign}${Math.abs(event.stages)}`
|
||||
}
|
||||
case 'ability': return `${event.side === 'player' ? '我方' : '对手'}的特性 ${event.ability} 发动了!`
|
||||
case 'item': return `${event.side === 'player' ? '我方' : '对手'}使用了 ${event.item}!`
|
||||
case 'fail': return `失败了: ${event.reason}`
|
||||
case 'item': return `${event.side === 'player' ? '我方' : '对手'}的 ${event.item} 发动了!`
|
||||
case 'fail': return `${event.side === 'player' ? '我方' : '对手'}的攻击失败了!`
|
||||
case 'weather':
|
||||
if (event.weather === 'none') return '天气恢复了正常'
|
||||
return `${WEATHER_NAMES[event.weather] ?? event.weather} 开始了!`
|
||||
case 'upkeep': return '── 回合结束处理 ──'
|
||||
case 'fieldCondition':
|
||||
if (event.action === 'add') return `${event.side === 'player' ? '我方' : '对手'}场地: ${event.id}!`
|
||||
return `${event.side === 'player' ? '我方' : '对手'}场地的 ${event.id} 消失了`
|
||||
case 'activate': return `${event.side === 'player' ? '我方' : '对手'}触发了 ${event.effect}`
|
||||
case 'immune': return `${event.side === 'player' ? '我方' : '对手'}不受影响!`
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import type { BattleState } from '../battle/types'
|
||||
import type { BattleState, WeatherKind } from '../battle/types'
|
||||
import type { SpeciesId } from '../types'
|
||||
import { loadSprite } from '../core/spriteCache'
|
||||
import { getFallbackSprite } from '../sprites/fallback'
|
||||
@@ -32,6 +32,11 @@ interface BattleSceneProps {
|
||||
onToggleAnim: () => void
|
||||
}
|
||||
|
||||
const WEATHER_LABELS: Record<WeatherKind, string> = {
|
||||
sun: '☀ 大晴天', rain: '🌧 雨天', sandstorm: '🌪 沙暴', hail: '❄ 冰雹',
|
||||
snow: '🌨 下雪', desolateland: '☀ 大日照', primordialsea: '🌧 大雨', deltastream: '🌀 强气流',
|
||||
}
|
||||
|
||||
export function BattleScene({
|
||||
state,
|
||||
menuPhase,
|
||||
@@ -64,7 +69,7 @@ export function BattleScene({
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor="success"
|
||||
borderText={{ content: ` 回合 ${state.turn} `, position: 'top', align: 'center' }}
|
||||
borderText={{ content: state.weather ? ` ${WEATHER_LABELS[state.weather]} · 回合 ${state.turn} ` : ` 回合 ${state.turn} `, position: 'top', align: 'center' }}
|
||||
paddingX={1}
|
||||
paddingY={0}
|
||||
width="60%"
|
||||
@@ -90,8 +95,8 @@ export function BattleScene({
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Player: sprite left, HP card right — no spacer, visually close */}
|
||||
<Box flexDirection="row" justifyContent="space-between" alignItems="flex-end">
|
||||
{/* Player: overlaps opponent area by pulling up */}
|
||||
<Box flexDirection="row" justifyContent="space-between" alignItems="flex-end" marginTop={-10}>
|
||||
<BattleSprite
|
||||
lines={playerSpriteLines}
|
||||
flip
|
||||
|
||||
Reference in New Issue
Block a user