CLAUDE: Fix pitcher/catcher recovery and lineup data format
Backend fixes: - state_manager: Properly recover current_pitcher and current_catcher from fielding team during game state recovery (fixes pitcher badge not showing) - handlers: Add headshot field to lineup data, use lineup_service for proper player data loading on cache miss - lineup_service: Minor adjustments for headshot support Frontend fixes: - player.ts: Update Lineup type to match WebSocket event format - lineup_id (was 'id'), card_id fields - player.headshot for UI circles - Optional fields for event variations - CurrentSituation.vue: Adapt to updated type structure - Substitution selectors: Use updated Lineup type fields This fixes the issue where pitcher badge wouldn't show after game recovery because current_pitcher was being set from batting team instead of fielding team. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9546d2a370
commit
1a562a75d2
@ -272,43 +272,70 @@ class StateManager:
|
|||||||
is_active=lineup.get('is_active', True)
|
is_active=lineup.get('is_active', True)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get placeholder current_batter (required field)
|
# Determine fielding team based on current half
|
||||||
# _prepare_next_play() will set the correct batter after recovery
|
current_half = game.get('current_half', 'top')
|
||||||
|
home_team_id = game['home_team_id']
|
||||||
|
away_team_id = game['away_team_id']
|
||||||
|
|
||||||
|
if current_half == 'top':
|
||||||
|
# Top of inning: away team batting, home team fielding
|
||||||
|
batting_team_id = away_team_id
|
||||||
|
fielding_team_id = home_team_id
|
||||||
|
else:
|
||||||
|
# Bottom of inning: home team batting, away team fielding
|
||||||
|
batting_team_id = home_team_id
|
||||||
|
fielding_team_id = away_team_id
|
||||||
|
|
||||||
|
# Get current batter from batting team (player with batting_order 1 as placeholder)
|
||||||
current_batter_placeholder = None
|
current_batter_placeholder = None
|
||||||
for lineup in lineups:
|
for lineup in lineups:
|
||||||
if lineup.get('batting_order') == 1 and lineup.get('is_active'):
|
if (lineup.get('team_id') == batting_team_id and
|
||||||
|
lineup.get('batting_order') == 1 and
|
||||||
|
lineup.get('is_active')):
|
||||||
current_batter_placeholder = get_lineup_player(lineup['id'])
|
current_batter_placeholder = get_lineup_player(lineup['id'])
|
||||||
break
|
break
|
||||||
|
|
||||||
# If no batter found, use first available lineup
|
# If no batter found, use first available lineup from batting team
|
||||||
if not current_batter_placeholder and lineups:
|
if not current_batter_placeholder:
|
||||||
first_lineup = lineups[0]
|
for lineup in lineups:
|
||||||
current_batter_placeholder = LineupPlayerState(
|
if lineup.get('team_id') == batting_team_id and lineup.get('is_active'):
|
||||||
lineup_id=first_lineup['id'],
|
current_batter_placeholder = get_lineup_player(lineup['id'])
|
||||||
card_id=first_lineup.get('card_id') or 0,
|
break
|
||||||
position=first_lineup['position'],
|
|
||||||
batting_order=first_lineup.get('batting_order'),
|
|
||||||
is_active=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# If still no batter (no lineups at all), raise error - game is in invalid state
|
# If still no batter (no lineups at all), raise error - game is in invalid state
|
||||||
if not current_batter_placeholder:
|
if not current_batter_placeholder:
|
||||||
raise ValueError(f"Cannot recover game {game['id']}: No lineups found")
|
raise ValueError(f"Cannot recover game {game['id']}: No lineups found for batting team")
|
||||||
|
|
||||||
|
# Get current pitcher and catcher from fielding team
|
||||||
|
current_pitcher = None
|
||||||
|
current_catcher = None
|
||||||
|
for lineup in lineups:
|
||||||
|
if lineup.get('team_id') == fielding_team_id and lineup.get('is_active'):
|
||||||
|
if lineup.get('position') == 'P' and not current_pitcher:
|
||||||
|
current_pitcher = get_lineup_player(lineup['id'])
|
||||||
|
elif lineup.get('position') == 'C' and not current_catcher:
|
||||||
|
current_catcher = get_lineup_player(lineup['id'])
|
||||||
|
|
||||||
|
# Stop if we found both
|
||||||
|
if current_pitcher and current_catcher:
|
||||||
|
break
|
||||||
|
|
||||||
state = GameState(
|
state = GameState(
|
||||||
game_id=game['id'],
|
game_id=game['id'],
|
||||||
league_id=game['league_id'],
|
league_id=game['league_id'],
|
||||||
home_team_id=game['home_team_id'],
|
home_team_id=home_team_id,
|
||||||
away_team_id=game['away_team_id'],
|
away_team_id=away_team_id,
|
||||||
home_team_is_ai=game.get('home_team_is_ai', False),
|
home_team_is_ai=game.get('home_team_is_ai', False),
|
||||||
away_team_is_ai=game.get('away_team_is_ai', False),
|
away_team_is_ai=game.get('away_team_is_ai', False),
|
||||||
status=game['status'],
|
status=game['status'],
|
||||||
inning=game.get('current_inning', 1),
|
inning=game.get('current_inning', 1),
|
||||||
half=game.get('current_half', 'top'),
|
half=current_half,
|
||||||
home_score=game.get('home_score', 0),
|
home_score=game.get('home_score', 0),
|
||||||
away_score=game.get('away_score', 0),
|
away_score=game.get('away_score', 0),
|
||||||
play_count=len(game_data.get('plays', [])),
|
play_count=len(game_data.get('plays', [])),
|
||||||
current_batter=current_batter_placeholder # Placeholder - corrected by _prepare_next_play()
|
current_batter=current_batter_placeholder,
|
||||||
|
current_pitcher=current_pitcher,
|
||||||
|
current_catcher=current_catcher
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get last completed play to recover runner state and batter indices
|
# Get last completed play to recover runner state and batter indices
|
||||||
|
|||||||
@ -46,7 +46,8 @@ class LineupPlayerState(BaseModel):
|
|||||||
|
|
||||||
# Player data (loaded at game start from SBA/PD API)
|
# Player data (loaded at game start from SBA/PD API)
|
||||||
player_name: Optional[str] = None
|
player_name: Optional[str] = None
|
||||||
player_image: Optional[str] = None
|
player_image: Optional[str] = None # Card image
|
||||||
|
player_headshot: Optional[str] = None # Headshot for UI circles
|
||||||
|
|
||||||
# Phase 3E-Main: Position rating (loaded at game start for PD league)
|
# Phase 3E-Main: Position rating (loaded at game start for PD league)
|
||||||
position_rating: Optional['PositionRating'] = None
|
position_rating: Optional['PositionRating'] = None
|
||||||
|
|||||||
@ -30,7 +30,8 @@ class LineupEntryWithPlayer:
|
|||||||
is_active: bool
|
is_active: bool
|
||||||
# Player data from API
|
# Player data from API
|
||||||
player_name: str
|
player_name: str
|
||||||
player_image: str
|
player_image: str # Card image
|
||||||
|
player_headshot: str # Headshot for UI circles
|
||||||
|
|
||||||
|
|
||||||
class LineupService:
|
class LineupService:
|
||||||
@ -88,11 +89,13 @@ class LineupService:
|
|||||||
# Step 2: Fetch player data from SBA API
|
# Step 2: Fetch player data from SBA API
|
||||||
player_name = f"Player #{player_id}"
|
player_name = f"Player #{player_id}"
|
||||||
player_image = ""
|
player_image = ""
|
||||||
|
player_headshot = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
player = await sba_api_client.get_player(player_id)
|
player = await sba_api_client.get_player(player_id)
|
||||||
player_name = player.name
|
player_name = player.name
|
||||||
player_image = player.get_image_url()
|
player_image = player.get_image_url()
|
||||||
|
player_headshot = player.headshot or ""
|
||||||
logger.info(f"Loaded player data for {player_id}: {player_name}")
|
logger.info(f"Loaded player data for {player_id}: {player_name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to fetch player data for {player_id}: {e}")
|
logger.warning(f"Failed to fetch player data for {player_id}: {e}")
|
||||||
@ -107,7 +110,8 @@ class LineupService:
|
|||||||
is_starter=is_starter,
|
is_starter=is_starter,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
player_name=player_name,
|
player_name=player_name,
|
||||||
player_image=player_image
|
player_image=player_image,
|
||||||
|
player_headshot=player_headshot
|
||||||
)
|
)
|
||||||
|
|
||||||
async def load_team_lineup_with_player_data(
|
async def load_team_lineup_with_player_data(
|
||||||
@ -152,11 +156,13 @@ class LineupService:
|
|||||||
for p in lineup_entries:
|
for p in lineup_entries:
|
||||||
player_name = None
|
player_name = None
|
||||||
player_image = None
|
player_image = None
|
||||||
|
player_headshot = None
|
||||||
|
|
||||||
if league_id == 'sba' and p.player_id and player_data.get(p.player_id): # type: ignore[arg-type]
|
if league_id == 'sba' and p.player_id and player_data.get(p.player_id): # type: ignore[arg-type]
|
||||||
player = player_data.get(p.player_id) # type: ignore[arg-type]
|
player = player_data.get(p.player_id) # type: ignore[arg-type]
|
||||||
player_name = player.name
|
player_name = player.name
|
||||||
player_image = player.get_image_url()
|
player_image = player.get_image_url()
|
||||||
|
player_headshot = player.headshot
|
||||||
|
|
||||||
players.append(LineupPlayerState(
|
players.append(LineupPlayerState(
|
||||||
lineup_id=p.id, # type: ignore[arg-type]
|
lineup_id=p.id, # type: ignore[arg-type]
|
||||||
@ -166,7 +172,8 @@ class LineupService:
|
|||||||
is_active=p.is_active, # type: ignore[arg-type]
|
is_active=p.is_active, # type: ignore[arg-type]
|
||||||
is_starter=p.is_starter, # type: ignore[arg-type]
|
is_starter=p.is_starter, # type: ignore[arg-type]
|
||||||
player_name=player_name,
|
player_name=player_name,
|
||||||
player_image=player_image
|
player_image=player_image,
|
||||||
|
player_headshot=player_headshot
|
||||||
))
|
))
|
||||||
|
|
||||||
return TeamLineupState(team_id=team_id, players=players)
|
return TeamLineupState(team_id=team_id, players=players)
|
||||||
|
|||||||
@ -15,6 +15,7 @@ from app.core.validators import ValidationError as GameValidationError
|
|||||||
from app.config.result_charts import PlayOutcome
|
from app.config.result_charts import PlayOutcome
|
||||||
from app.database.operations import DatabaseOperations
|
from app.database.operations import DatabaseOperations
|
||||||
from app.services.sba_api_client import sba_api_client
|
from app.services.sba_api_client import sba_api_client
|
||||||
|
from app.services.lineup_service import lineup_service
|
||||||
|
|
||||||
logger = logging.getLogger(f'{__name__}.handlers')
|
logger = logging.getLogger(f'{__name__}.handlers')
|
||||||
|
|
||||||
@ -1022,7 +1023,8 @@ def register_handlers(sio: AsyncServer, manager: ConnectionManager) -> None:
|
|||||||
"player": {
|
"player": {
|
||||||
"id": p.card_id,
|
"id": p.card_id,
|
||||||
"name": p.player_name or f"Player #{p.card_id}",
|
"name": p.player_name or f"Player #{p.card_id}",
|
||||||
"image": p.player_image or ""
|
"image": p.player_image or "",
|
||||||
|
"headshot": p.player_headshot or ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for p in lineup.players if p.is_active
|
for p in lineup.players if p.is_active
|
||||||
@ -1031,13 +1033,22 @@ def register_handlers(sio: AsyncServer, manager: ConnectionManager) -> None:
|
|||||||
)
|
)
|
||||||
logger.info(f"Lineup data sent for game {game_id}, team {team_id}")
|
logger.info(f"Lineup data sent for game {game_id}, team {team_id}")
|
||||||
else:
|
else:
|
||||||
# Lineup not in cache - try to load from database
|
# Lineup not in cache - try to load from database with player data
|
||||||
db_ops = DatabaseOperations()
|
# Get league_id from game state or database
|
||||||
lineup_entries = await db_ops.get_active_lineup(game_id, team_id)
|
state = state_manager.get_state(game_id)
|
||||||
|
league_id = state.league_id if state else "sba"
|
||||||
|
|
||||||
if lineup_entries:
|
lineup_state = await lineup_service.load_team_lineup_with_player_data(
|
||||||
# Note: Player data not available when loading directly from DB
|
game_id=game_id,
|
||||||
# This is a cache miss scenario - normally lineups are cached with player data
|
team_id=team_id,
|
||||||
|
league_id=league_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if lineup_state:
|
||||||
|
# Cache the lineup for future requests
|
||||||
|
state_manager.set_lineup(game_id, team_id, lineup_state)
|
||||||
|
|
||||||
|
# Send lineup data with player info
|
||||||
await manager.emit_to_user(
|
await manager.emit_to_user(
|
||||||
sid,
|
sid,
|
||||||
"lineup_data",
|
"lineup_data",
|
||||||
@ -1046,23 +1057,23 @@ def register_handlers(sio: AsyncServer, manager: ConnectionManager) -> None:
|
|||||||
"team_id": team_id,
|
"team_id": team_id,
|
||||||
"players": [
|
"players": [
|
||||||
{
|
{
|
||||||
"lineup_id": entry.id, # type: ignore[assignment]
|
"lineup_id": p.lineup_id,
|
||||||
"card_id": entry.card_id or entry.player_id, # type: ignore[assignment]
|
"card_id": p.card_id,
|
||||||
"position": entry.position, # type: ignore[assignment]
|
"position": p.position,
|
||||||
"batting_order": entry.batting_order, # type: ignore[assignment]
|
"batting_order": p.batting_order,
|
||||||
"is_active": entry.is_active, # type: ignore[assignment]
|
"is_active": p.is_active,
|
||||||
"is_starter": entry.is_starter, # type: ignore[assignment]
|
"is_starter": p.is_starter,
|
||||||
"player": {
|
"player": {
|
||||||
"id": entry.card_id or entry.player_id, # type: ignore[assignment]
|
"id": p.card_id,
|
||||||
"name": f"Player #{entry.card_id or entry.player_id}", # type: ignore[assignment]
|
"name": p.player_name or f"Player #{p.card_id}",
|
||||||
"image": ""
|
"image": p.player_image or ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for entry in lineup_entries
|
for p in lineup_state.players if p.is_active
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
logger.info(f"Lineup data loaded from DB for game {game_id}, team {team_id} (without player details)")
|
logger.info(f"Lineup data loaded from DB with player data for game {game_id}, team {team_id}")
|
||||||
else:
|
else:
|
||||||
await manager.emit_to_user(
|
await manager.emit_to_user(
|
||||||
sid,
|
sid,
|
||||||
|
|||||||
@ -11,8 +11,8 @@
|
|||||||
<!-- Pitcher Image/Badge -->
|
<!-- Pitcher Image/Badge -->
|
||||||
<div class="w-12 h-12 rounded-full flex items-center justify-center shadow-lg flex-shrink-0 overflow-hidden">
|
<div class="w-12 h-12 rounded-full flex items-center justify-center shadow-lg flex-shrink-0 overflow-hidden">
|
||||||
<img
|
<img
|
||||||
v-if="pitcherPlayer?.image"
|
v-if="pitcherPlayer?.headshot"
|
||||||
:src="pitcherPlayer.image"
|
:src="pitcherPlayer.headshot"
|
||||||
:alt="pitcherName"
|
:alt="pitcherName"
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
@ -52,8 +52,8 @@
|
|||||||
<!-- Batter Image/Badge -->
|
<!-- Batter Image/Badge -->
|
||||||
<div class="w-12 h-12 rounded-full flex items-center justify-center shadow-lg flex-shrink-0 overflow-hidden">
|
<div class="w-12 h-12 rounded-full flex items-center justify-center shadow-lg flex-shrink-0 overflow-hidden">
|
||||||
<img
|
<img
|
||||||
v-if="batterPlayer?.image"
|
v-if="batterPlayer?.headshot"
|
||||||
:src="batterPlayer.image"
|
:src="batterPlayer.headshot"
|
||||||
:alt="batterName"
|
:alt="batterName"
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
@ -90,8 +90,8 @@
|
|||||||
<!-- Pitcher Image/Badge -->
|
<!-- Pitcher Image/Badge -->
|
||||||
<div class="w-16 h-16 rounded-full flex items-center justify-center shadow-xl flex-shrink-0 overflow-hidden">
|
<div class="w-16 h-16 rounded-full flex items-center justify-center shadow-xl flex-shrink-0 overflow-hidden">
|
||||||
<img
|
<img
|
||||||
v-if="pitcherPlayer?.image"
|
v-if="pitcherPlayer?.headshot"
|
||||||
:src="pitcherPlayer.image"
|
:src="pitcherPlayer.headshot"
|
||||||
:alt="pitcherName"
|
:alt="pitcherName"
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
@ -124,8 +124,8 @@
|
|||||||
<!-- Batter Image/Badge -->
|
<!-- Batter Image/Badge -->
|
||||||
<div class="w-16 h-16 rounded-full flex items-center justify-center shadow-xl flex-shrink-0 overflow-hidden">
|
<div class="w-16 h-16 rounded-full flex items-center justify-center shadow-xl flex-shrink-0 overflow-hidden">
|
||||||
<img
|
<img
|
||||||
v-if="batterPlayer?.image"
|
v-if="batterPlayer?.headshot"
|
||||||
:src="batterPlayer.image"
|
:src="batterPlayer.headshot"
|
||||||
:alt="batterName"
|
:alt="batterName"
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -61,7 +61,7 @@
|
|||||||
<div v-else class="bench-grid">
|
<div v-else class="bench-grid">
|
||||||
<button
|
<button
|
||||||
v-for="player in filteredBenchPlayers"
|
v-for="player in filteredBenchPlayers"
|
||||||
:key="player.id"
|
:key="player.lineup_id"
|
||||||
:class="[
|
:class="[
|
||||||
'bench-player-card',
|
'bench-player-card',
|
||||||
selectedPlayerId === player.player.id ? 'bench-player-selected' : 'bench-player-default'
|
selectedPlayerId === player.player.id ? 'bench-player-selected' : 'bench-player-default'
|
||||||
@ -194,7 +194,7 @@ const handleSubmit = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emit('submit', {
|
emit('submit', {
|
||||||
playerOutLineupId: props.playerOut.id,
|
playerOutLineupId: props.playerOut.lineup_id,
|
||||||
playerInCardId: selectedPlayer.value.player.id,
|
playerInCardId: selectedPlayer.value.player.id,
|
||||||
newPosition: selectedPosition.value,
|
newPosition: selectedPosition.value,
|
||||||
teamId: props.teamId,
|
teamId: props.teamId,
|
||||||
|
|||||||
@ -36,7 +36,7 @@
|
|||||||
<div v-else class="bench-grid">
|
<div v-else class="bench-grid">
|
||||||
<button
|
<button
|
||||||
v-for="player in availableBenchPlayers"
|
v-for="player in availableBenchPlayers"
|
||||||
:key="player.id"
|
:key="player.lineup_id"
|
||||||
:class="[
|
:class="[
|
||||||
'bench-player-card',
|
'bench-player-card',
|
||||||
selectedPlayerId === player.player.id ? 'bench-player-selected' : 'bench-player-default'
|
selectedPlayerId === player.player.id ? 'bench-player-selected' : 'bench-player-default'
|
||||||
@ -138,7 +138,7 @@ const handleSubmit = () => {
|
|||||||
if (!canSubmit.value || !props.playerOut || !selectedPlayer.value) return
|
if (!canSubmit.value || !props.playerOut || !selectedPlayer.value) return
|
||||||
|
|
||||||
emit('submit', {
|
emit('submit', {
|
||||||
playerOutLineupId: props.playerOut.id,
|
playerOutLineupId: props.playerOut.lineup_id,
|
||||||
playerInCardId: selectedPlayer.value.player.id,
|
playerInCardId: selectedPlayer.value.player.id,
|
||||||
teamId: props.teamId,
|
teamId: props.teamId,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -171,7 +171,7 @@ const handleSubmit = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emit('submit', {
|
emit('submit', {
|
||||||
playerOutLineupId: props.currentPitcher.id,
|
playerOutLineupId: props.currentPitcher.lineup_id,
|
||||||
playerInCardId: selectedPitcher.value.player.id,
|
playerInCardId: selectedPitcher.value.player.id,
|
||||||
teamId: props.teamId,
|
teamId: props.teamId,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -78,7 +78,7 @@
|
|||||||
<div class="player-grid">
|
<div class="player-grid">
|
||||||
<button
|
<button
|
||||||
v-for="player in activeFielders"
|
v-for="player in activeFielders"
|
||||||
:key="player.id"
|
:key="player.lineup_id"
|
||||||
class="player-button"
|
class="player-button"
|
||||||
@click="selectDefensivePlayer(player)"
|
@click="selectDefensivePlayer(player)"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -335,7 +335,7 @@ export const useGameStore = defineStore('game', () => {
|
|||||||
*/
|
*/
|
||||||
function findPlayerInLineup(lineupId: number): Lineup | undefined {
|
function findPlayerInLineup(lineupId: number): Lineup | undefined {
|
||||||
return [...homeLineup.value, ...awayLineup.value].find(
|
return [...homeLineup.value, ...awayLineup.value].find(
|
||||||
p => p.id === lineupId
|
p => p.lineup_id === lineupId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -48,26 +48,31 @@ export interface SbaPlayer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lineup entry - player assignment in a game
|
* Lineup entry - player assignment in a game
|
||||||
* Backend: Lineup (db model)
|
* Backend: Lineup (db model) via WebSocket lineup_data event
|
||||||
*/
|
*/
|
||||||
export interface Lineup {
|
export interface Lineup {
|
||||||
id: number
|
lineup_id: number // Unique lineup entry ID (was 'id')
|
||||||
game_id: string
|
card_id: number // Player/card ID
|
||||||
team_id: number
|
game_id?: string // Optional - not always sent in events
|
||||||
player_id: number
|
team_id?: number // Optional - not always sent in events
|
||||||
position: string
|
position: string
|
||||||
batting_order: number | null
|
batting_order: number | null
|
||||||
is_starter: boolean
|
is_starter: boolean
|
||||||
is_active: boolean
|
is_active: boolean
|
||||||
entered_inning: number
|
entered_inning?: number // Optional - not always sent in events
|
||||||
|
|
||||||
// Substitution tracking
|
// Substitution tracking (optional - not always sent in events)
|
||||||
replacing_id: number | null
|
replacing_id?: number | null
|
||||||
after_play: number | null
|
after_play?: number | null
|
||||||
is_fatigued: boolean
|
is_fatigued?: boolean
|
||||||
|
|
||||||
// Player data (embedded)
|
// Player data (embedded from SBA API)
|
||||||
player: SbaPlayer
|
player: {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
image: string // Card image
|
||||||
|
headshot: string // Headshot for UI circles
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user