# Mantimon TCG Frontend - AI Agent Guidelines
Guidelines for AI agents working on the frontend codebase.
## Tech Stack
| Technology | Purpose |
|------------|---------|
| Vue 3 | UI framework (Composition API + `
```
### TypeScript
**Use strict typing:**
```typescript
// Good - explicit types
interface CardProps {
cardId: string
size?: 'sm' | 'md' | 'lg'
}
// Good - type imports
import type { Card, GameState } from '@/types'
// Good - const assertions for literals
const SIZES = ['sm', 'md', 'lg'] as const
type Size = typeof SIZES[number]
// Avoid - any
const data: any = response // NO
const data: unknown = response // OK if you'll narrow it
```
### Naming Conventions
| Type | Convention | Example |
|------|------------|---------|
| Components | PascalCase | `CardDisplay.vue`, `DeckBuilder.vue` |
| Composables | camelCase with `use` prefix | `useAuth.ts`, `useDeckValidation.ts` |
| Stores | camelCase with descriptive name | `auth.ts`, `game.ts` |
| Types/Interfaces | PascalCase | `Card`, `GameState`, `ApiResponse` |
| Constants | UPPER_SNAKE_CASE | `MAX_HAND_SIZE`, `API_BASE_URL` |
| CSS classes | kebab-case (BEM optional) | `card-display`, `card-display--active` |
---
## Pinia Stores
**Use setup store syntax:**
```typescript
// stores/game.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { GameState } from '@/types'
export const useGameStore = defineStore('game', () => {
// State
const gameState = ref(null)
const isConnected = ref(false)
// Getters (computed)
const myHand = computed(() => gameState.value?.myHand ?? [])
const isMyTurn = computed(() => gameState.value?.currentPlayer === 'me')
// Actions
function setGameState(state: GameState) {
gameState.value = state
}
function clearGame() {
gameState.value = null
isConnected.value = false
}
return {
// State
gameState,
isConnected,
// Getters
myHand,
isMyTurn,
// Actions
setGameState,
clearGame,
}
})
```
---
## Composables
**Pattern for API composables:**
```typescript
// composables/useDecks.ts
import { ref } from 'vue'
import { apiClient } from '@/api/client'
import type { Deck, DeckCreate } from '@/types'
export function useDecks() {
const decks = ref([])
const isLoading = ref(false)
const error = ref(null)
async function fetchDecks() {
isLoading.value = true
error.value = null
try {
decks.value = await apiClient.get('/api/decks')
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to fetch decks'
} finally {
isLoading.value = false
}
}
async function createDeck(data: DeckCreate) {
isLoading.value = true
error.value = null
try {
const deck = await apiClient.post('/api/decks', data)
decks.value.push(deck)
return deck
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to create deck'
throw e
} finally {
isLoading.value = false
}
}
return {
decks,
isLoading,
error,
fetchDecks,
createDeck,
}
}
```
---
## Vue-Phaser Integration
**Phaser is for rendering only. Game logic lives in backend.**
### Communication Pattern
```typescript
// Vue -> Phaser (intentions)
phaserGame.value?.events.emit('card:play', { cardId, targetZone })
phaserGame.value?.events.emit('attack:select', { attackIndex })
// Phaser -> Vue (completions/UI requests)
phaserGame.value?.events.on('animation:complete', handleAnimationComplete)
phaserGame.value?.events.on('card:clicked', handleCardClicked)
```
### State Sync
```typescript
// Phaser scene reads from Pinia store
import { useGameStore } from '@/stores/game'
class MatchScene extends Phaser.Scene {
private gameStore = useGameStore()
update() {
// React to store changes
if (this.gameStore.gameState) {
this.renderState(this.gameStore.gameState)
}
}
}
```
---
## Tailwind Guidelines
**Mobile-first responsive:**
```html
```
**Use design system colors:**
```html
Fire Type
Water Type
```
---
## Testing
**Component tests with Vitest + Vue Test Utils:**
```typescript
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import CardDisplay from '@/components/cards/CardDisplay.vue'
describe('CardDisplay', () => {
it('renders card name', () => {
/**
* Test that the card component displays the card name.
*
* Card names must be visible for players to identify cards
* in their hand and on the board.
*/
const wrapper = mount(CardDisplay, {
props: { card: { id: '1', name: 'Pikachu', hp: 60 } }
})
expect(wrapper.text()).toContain('Pikachu')
})
it('emits click event with card id', async () => {
/**
* Test that clicking a card emits the correct event.
*
* Card clicks drive all game interactions - playing cards,
* selecting targets, viewing details.
*/
const wrapper = mount(CardDisplay, {
props: { card: { id: '1', name: 'Pikachu', hp: 60 } }
})
await wrapper.trigger('click')
expect(wrapper.emitted('click')?.[0]).toEqual(['1'])
})
})
```
---
## Critical Rules
1. **Mobile-first** - Base styles for mobile, use `md:` and `lg:` for larger screens
2. **TypeScript strict** - No `any`, explicit types for props/emits/returns
3. **Composition API only** - No Options API, use `