/** * Player Store * * Manages player state for multiplayer mode, including nickname, connection status, * and opponent information. */ import { defineStore } from "pinia"; import { StorageKey, localStorageManager } from "@/utils/storage"; import { getRandomNickname } from "@/data/defaultNicknames"; /** * Player state interface */ export interface PlayerState { nickname: string; // Local player nickname playerId: string | null; // Socket ID playerColor: "black" | "white" | null; // Player's stone color in current game roomId: string | null; // Current room ID roomName: string | null; // Human-readable room name isAdmin: boolean; // Whether player is room admin opponentNickname: string | null; // Opponent's nickname connectionStatus: "disconnected" | "connected" | "in-room"; // Connection state } /** * Nickname validation result */ interface ValidationResult { valid: boolean; error?: string; } /** * Validate a nickname against the rules: * - Length: 3-20 characters * - Characters: alphanumeric + spaces only * - No leading/trailing whitespace */ function validateNickname(nickname: string): ValidationResult { if (!nickname || typeof nickname !== "string") { return { valid: false, error: "Nickname is required" }; } if (nickname.length < 3) { return { valid: false, error: "Nickname must be at least 3 characters" }; } if (nickname.length > 20) { return { valid: false, error: "Nickname must be 20 characters or less" }; } if (!/^[a-zA-Z0-9 ]+$/.test(nickname)) { return { valid: false, error: "Only letters, numbers, and spaces allowed" }; } if (nickname.trim() !== nickname) { return { valid: false, error: "No leading or trailing spaces allowed" }; } return { valid: true }; } /** * Load nickname from localStorage or generate a random one */ function loadNickname(): string { const stored = localStorageManager.getString(StorageKey.PLAYER_NICKNAME); if (stored) { // Validate stored nickname const validation = validateNickname(stored); if (validation.valid) { return stored; } } // Generate random nickname if none exists or invalid return getRandomNickname(); } /** * Save nickname to localStorage */ function saveNickname(nickname: string): void { localStorageManager.setString(StorageKey.PLAYER_NICKNAME, nickname); } /** * Player store */ export const usePlayerStore = defineStore("player", { state: (): PlayerState => ({ nickname: loadNickname(), playerId: null, playerColor: null, roomId: null, roomName: null, isAdmin: false, opponentNickname: null, connectionStatus: "disconnected" }), getters: { /** * Is player currently in a room? */ isInRoom: (state): boolean => state.connectionStatus === "in-room", /** * Does player have an opponent in the room? */ hasOpponent: (state): boolean => state.opponentNickname !== null, /** * Display name with fallback to "Guest" */ displayName: (state): string => state.nickname || "Guest", /** * Is player connected to socket server? */ isConnected: (state): boolean => state.connectionStatus !== "disconnected" }, actions: { /** * Set the player's nickname * @param nickname - New nickname to set * @returns true if successful, false if validation failed */ setNickname(nickname: string): boolean { const validation = validateNickname(nickname); if (!validation.valid) { console.warn(`[PlayerStore] Invalid nickname: ${validation.error}`); return false; } this.nickname = nickname; saveNickname(nickname); return true; }, /** * Set the player's socket ID * @param id - Socket ID from server */ setPlayerId(id: string): void { this.playerId = id; this.connectionStatus = "connected"; }, /** * Join a room with the given ID and assigned color * @param roomId - Room ID * @param color - Assigned player color * @param roomName - Human-readable room name * @param isAdmin - Whether player is room admin */ joinRoom(roomId: string, color: "black" | "white", roomName?: string, isAdmin?: boolean): void { this.roomId = roomId; this.roomName = roomName || null; this.isAdmin = isAdmin || false; this.playerColor = color; this.connectionStatus = "in-room"; }, /** * Set the room name * @param name - New room name */ setRoomName(name: string): void { this.roomName = name; }, /** * Set the opponent's nickname * @param nickname - Opponent's nickname */ setOpponentNickname(nickname: string): void { this.opponentNickname = nickname; }, /** * Leave the current room */ leaveRoom(): void { this.roomId = null; this.roomName = null; this.isAdmin = false; this.playerColor = null; this.opponentNickname = null; this.connectionStatus = "connected"; }, /** * Disconnect from the server */ disconnect(): void { this.playerId = null; this.roomId = null; this.roomName = null; this.isAdmin = false; this.playerColor = null; this.opponentNickname = null; this.connectionStatus = "disconnected"; }, /** * Reset player state to initial state * Note: Nickname is preserved in localStorage */ reset(): void { this.playerId = null; this.playerColor = null; this.roomId = null; this.roomName = null; this.isAdmin = false; this.opponentNickname = null; this.connectionStatus = "disconnected"; } } }); /** * Export validation function for use in other components */ export { validateNickname };