import { MediaReference } from '@core/domain/media/MediaReference'; import { Driver } from '@core/racing/domain/entities/Driver'; import { faker } from '@faker-js/faker'; import { seedId } from './SeedIdHelper'; export interface DriverStats { rating: number; safetyRating: number; sportsmanshipRating: number; totalRaces: number; wins: number; podiums: number; dnfs: number; avgFinish: number; bestFinish: number; worstFinish: number; consistency: number; experienceLevel: 'beginner' | 'intermediate' | 'advanced' | 'veteran'; overallRank: number | null; } export class RacingDriverFactory { constructor( private readonly driverCount: number, private readonly baseDate: Date, private readonly persistence: 'postgres' | 'inmemory' = 'inmemory', ) {} /** * Get deterministic MediaReference for a driver's avatar based on their ID * Uses hash % 3 to determine variant: 0 -> male, 1 -> female, 2 -> neutral */ getDriverAvatarRef(driverId: string): MediaReference { // Deterministic selection based on driver ID hash const numericSuffixMatch = driverId.match(/(\d+)$/); let avatarVariant: 'male' | 'female' | 'neutral'; if (numericSuffixMatch && numericSuffixMatch[1]) { const numericSuffix = parseInt(numericSuffixMatch[1], 10); const hashMod = numericSuffix % 3; if (hashMod === 0) { avatarVariant = 'male'; } else if (hashMod === 1) { avatarVariant = 'female'; } else { avatarVariant = 'neutral'; } } else { // Fallback hash let hash = 0; for (let i = 0; i < driverId.length; i++) { hash = (hash * 31 + driverId.charCodeAt(i)) | 0; } const hashMod = Math.abs(hash) % 3; if (hashMod === 0) { avatarVariant = 'male'; } else if (hashMod === 1) { avatarVariant = 'female'; } else { avatarVariant = 'neutral'; } } // Create system-default reference with avatar variant return MediaReference.createSystemDefault('avatar', avatarVariant); } create(): Driver[] { const countries = ['DE', 'NL', 'FR', 'GB', 'US', 'CA', 'SE', 'NO', 'IT', 'ES', 'AU', 'BR', 'JP', 'KR', 'RU', 'PL', 'CZ', 'HU', 'AT', 'CH'] as const; const categories = ['beginner', 'intermediate', 'advanced', 'pro', 'endurance', 'sprint']; return Array.from({ length: this.driverCount }, (_, idx) => { const i = idx + 1; // Vary bio presence: 80% have bio, 20% empty const hasBio = i % 5 !== 0; // Vary joined dates: some recent (new drivers), some old (veterans) let joinedAt: Date; if (i % 10 === 0) { // Very recent drivers (within last week) joinedAt = faker.date.recent({ days: 7, refDate: this.baseDate }); } else if (i % 7 === 0) { // Recent drivers (within last month) joinedAt = faker.date.recent({ days: 30, refDate: this.baseDate }); } else if (i % 5 === 0) { // Medium tenure (6-12 months ago) joinedAt = faker.date.between({ from: new Date(this.baseDate.getTime() - 365 * 24 * 60 * 60 * 1000), to: new Date(this.baseDate.getTime() - 180 * 24 * 60 * 60 * 1000) }); } else { // Veterans (1-2 years ago) joinedAt = faker.date.past({ years: 2, refDate: this.baseDate }); } // Assign category - use all available categories const category = faker.helpers.arrayElement(categories); const driverId = seedId(`driver-${i}`, this.persistence); const driverData: { id: string; iracingId: string; name: string; country: string; bio?: string; joinedAt?: Date; category?: string; avatarRef: MediaReference; } = { id: driverId, iracingId: String(100000 + i), name: faker.person.fullName(), country: faker.helpers.arrayElement(countries), joinedAt, category, avatarRef: this.getDriverAvatarRef(driverId), }; if (hasBio) { driverData.bio = faker.lorem.sentences(2); } return Driver.create(driverData); }); } /** * Generate driver statistics for profile display * This would be stored in a separate stats table/service in production */ generateDriverStats(drivers: Driver[]): Map { const statsMap = new Map(); drivers.forEach((driver, idx) => { const i = idx + 1; // Determine experience level based on join date and index let experienceLevel: 'beginner' | 'intermediate' | 'advanced' | 'veteran'; let totalRaces: number; let rating: number; let safetyRating: number; let sportsmanshipRating: number; if (i % 10 === 0) { // Very recent drivers experienceLevel = 'beginner'; totalRaces = faker.number.int({ min: 1, max: 10 }); rating = faker.number.int({ min: 800, max: 1200 }); safetyRating = faker.number.int({ min: 60, max: 85 }); sportsmanshipRating = Math.round(faker.number.float({ min: 3.0, max: 4.2 }) * 10) / 10; } else if (i % 7 === 0) { // Recent drivers experienceLevel = 'beginner'; totalRaces = faker.number.int({ min: 5, max: 25 }); rating = faker.number.int({ min: 1000, max: 1400 }); safetyRating = faker.number.int({ min: 70, max: 90 }); sportsmanshipRating = Math.round(faker.number.float({ min: 3.5, max: 4.5 }) * 10) / 10; } else if (i % 5 === 0) { // Medium tenure experienceLevel = 'intermediate'; totalRaces = faker.number.int({ min: 20, max: 60 }); rating = faker.number.int({ min: 1300, max: 1700 }); safetyRating = faker.number.int({ min: 75, max: 95 }); sportsmanshipRating = Math.round(faker.number.float({ min: 3.8, max: 4.8 }) * 10) / 10; } else if (i % 3 === 0) { // Advanced experienceLevel = 'advanced'; totalRaces = faker.number.int({ min: 50, max: 120 }); rating = faker.number.int({ min: 1600, max: 1900 }); safetyRating = faker.number.int({ min: 80, max: 98 }); sportsmanshipRating = Math.round(faker.number.float({ min: 4.0, max: 5.0 }) * 10) / 10; } else { // Veterans experienceLevel = 'veteran'; totalRaces = faker.number.int({ min: 100, max: 200 }); rating = faker.number.int({ min: 1700, max: 2000 }); safetyRating = faker.number.int({ min: 85, max: 100 }); sportsmanshipRating = Math.round(faker.number.float({ min: 4.2, max: 5.0 }) * 10) / 10; } // Calculate performance stats based on total races const winRate = experienceLevel === 'beginner' ? faker.number.float({ min: 0, max: 5, fractionDigits: 1 }) : experienceLevel === 'intermediate' ? faker.number.float({ min: 2, max: 10, fractionDigits: 1 }) : experienceLevel === 'advanced' ? faker.number.float({ min: 5, max: 15, fractionDigits: 1 }) : faker.number.float({ min: 8, max: 20, fractionDigits: 1 }); const podiumRate = experienceLevel === 'beginner' ? faker.number.float({ min: 5, max: 15, fractionDigits: 1 }) : experienceLevel === 'intermediate' ? faker.number.float({ min: 10, max: 25, fractionDigits: 1 }) : experienceLevel === 'advanced' ? faker.number.float({ min: 15, max: 35, fractionDigits: 1 }) : faker.number.float({ min: 20, max: 45, fractionDigits: 1 }); const wins = Math.round((winRate / 100) * totalRaces); const podiums = Math.round((podiumRate / 100) * totalRaces); const dnfs = Math.round(faker.number.float({ min: 0.05, max: 0.15, fractionDigits: 2 }) * totalRaces); const avgFinish = experienceLevel === 'beginner' ? faker.number.float({ min: 8, max: 15, fractionDigits: 1 }) : experienceLevel === 'intermediate' ? faker.number.float({ min: 5, max: 10, fractionDigits: 1 }) : experienceLevel === 'advanced' ? faker.number.float({ min: 3, max: 7, fractionDigits: 1 }) : faker.number.float({ min: 2, max: 5, fractionDigits: 1 }); const bestFinish = experienceLevel === 'beginner' ? faker.number.int({ min: 3, max: 8 }) : experienceLevel === 'intermediate' ? faker.number.int({ min: 1, max: 5 }) : faker.number.int({ min: 1, max: 3 }); const worstFinish = experienceLevel === 'beginner' ? faker.number.int({ min: 12, max: 20 }) : experienceLevel === 'intermediate' ? faker.number.int({ min: 8, max: 15 }) : experienceLevel === 'advanced' ? faker.number.int({ min: 5, max: 12 }) : faker.number.int({ min: 4, max: 10 }); const consistency = experienceLevel === 'beginner' ? faker.number.float({ min: 40, max: 65, fractionDigits: 0 }) : experienceLevel === 'intermediate' ? faker.number.float({ min: 60, max: 80, fractionDigits: 0 }) : experienceLevel === 'advanced' ? faker.number.float({ min: 75, max: 90, fractionDigits: 0 }) : faker.number.float({ min: 85, max: 98, fractionDigits: 0 }); statsMap.set(driver.id.toString(), { rating, safetyRating, sportsmanshipRating, totalRaces, wins, podiums, dnfs, avgFinish: parseFloat(avgFinish.toFixed(1)), bestFinish, worstFinish, consistency: parseFloat(consistency.toFixed(1)), experienceLevel, overallRank: null, // Will be calculated after all stats are loaded }); }); return statsMap; } }