trigo / trigo-web /app /src /stores /playerStore.ts
k-l-lambda's picture
feat: room rename and room switching with confirmation
6f4808d
/**
* 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 };