import { Type } from 'class-transformer'; import { IsBoolean, IsIn, IsNotEmpty, IsNumber, IsOptional, IsString, Length, Matches, Max, Min, ValidateIf } from 'class-validator'; import { MALL_SKIN_ITEMS } from '../mall/mall_catalog'; export const TEST_LAB_MAP_IDS = ['whale_port', 'work_zone', 'whale_cafe'] as const; export type TestLabMapId = (typeof TEST_LAB_MAP_IDS)[number]; export const TEST_LAB_SKIN_IDS = MALL_SKIN_ITEMS.map((item) => item.skinId).filter((skinId): skinId is string => Boolean(skinId)); const TEST_LAB_NICKNAME_PATTERN = /^[\u4E00-\u9FFF A-Za-z0-9_-]+$/; export class CreateTestLabActorDto { @IsString() @IsNotEmpty() @Length(1, 32) @Matches(TEST_LAB_NICKNAME_PATTERN, { message: '昵称仅支持中文、字母、数字、空格、下划线和连字符' }) nickname: string; @IsIn(TEST_LAB_MAP_IDS) mapId: TestLabMapId; @Type(() => Number) @IsNumber() @Min(-8192) @Max(8192) x: number; @Type(() => Number) @IsNumber() @Min(-8192) @Max(8192) y: number; @IsOptional() @IsIn(TEST_LAB_SKIN_IDS) skinId?: string; } export class UpdateTestLabActorDto { @IsOptional() @IsBoolean() online?: boolean; @ValidateIf((value) => value.mapId !== undefined) @IsIn(TEST_LAB_MAP_IDS) mapId?: TestLabMapId; @IsOptional() @Type(() => Number) @IsNumber() @Min(-8192) @Max(8192) x?: number; @IsOptional() @Type(() => Number) @IsNumber() @Min(-8192) @Max(8192) y?: number; @IsOptional() @IsIn(TEST_LAB_SKIN_IDS) skinId?: string; } export class TestLabMessageDto { @IsIn(['local', 'global', 'private']) scope: 'local' | 'global' | 'private'; @IsString() @IsNotEmpty() @Length(1, 1000) content: string; @ValidateIf((value) => value.scope === 'private') @IsString() @IsNotEmpty() targetUserId?: string; } export class TestLabSocialActionDto { @IsIn(['friend_request', 'friend_accept', 'friend_reject', 'block']) action: 'friend_request' | 'friend_accept' | 'friend_reject' | 'block'; @IsString() @IsNotEmpty() targetUserId: string; } export class TestLabRoomPolicyDto { @IsIn(['friends', 'public', 'closed']) roomVisitPolicy: 'friends' | 'public' | 'closed'; }