# 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 `