This commit is contained in:
2025-12-10 12:38:55 +01:00
parent 0f7fe67d3c
commit fbbcf414a4
87 changed files with 11972 additions and 390 deletions

View File

@@ -12,6 +12,7 @@ export interface LiveryDecalProps {
y: number;
width: number;
height: number;
rotation: number; // Degrees, 0-360
zIndex: number;
type: DecalType;
}
@@ -23,6 +24,7 @@ export class LiveryDecal {
readonly y: number;
readonly width: number;
readonly height: number;
readonly rotation: number;
readonly zIndex: number;
readonly type: DecalType;
@@ -33,13 +35,18 @@ export class LiveryDecal {
this.y = props.y;
this.width = props.width;
this.height = props.height;
this.rotation = props.rotation;
this.zIndex = props.zIndex;
this.type = props.type;
}
static create(props: LiveryDecalProps): LiveryDecal {
this.validate(props);
return new LiveryDecal(props);
static create(props: Omit<LiveryDecalProps, 'rotation'> & { rotation?: number }): LiveryDecal {
const propsWithRotation = {
...props,
rotation: props.rotation ?? 0,
};
this.validate(propsWithRotation);
return new LiveryDecal(propsWithRotation);
}
private static validate(props: LiveryDecalProps): void {
@@ -71,6 +78,10 @@ export class LiveryDecal {
throw new Error('LiveryDecal zIndex must be a non-negative integer');
}
if (props.rotation < 0 || props.rotation > 360) {
throw new Error('LiveryDecal rotation must be between 0 and 360 degrees');
}
if (!props.type) {
throw new Error('LiveryDecal type is required');
}
@@ -108,6 +119,25 @@ export class LiveryDecal {
});
}
/**
* Rotate decal
*/
rotate(rotation: number): LiveryDecal {
// Normalize rotation to 0-360 range
const normalizedRotation = ((rotation % 360) + 360) % 360;
return LiveryDecal.create({
...this,
rotation: normalizedRotation,
});
}
/**
* Get CSS transform string for rendering
*/
getCssTransform(): string {
return `rotate(${this.rotation}deg)`;
}
/**
* Check if this decal overlaps with another
*/

View File

@@ -0,0 +1,208 @@
/**
* Value Object: SponsorshipPricing
*
* Represents the sponsorship slot configuration and pricing for any sponsorable entity.
* Used by drivers, teams, races, and leagues to define their sponsorship offerings.
*/
import { Money } from './Money';
export interface SponsorshipSlotConfig {
tier: 'main' | 'secondary';
price: Money;
benefits: string[];
available: boolean;
maxSlots: number; // How many sponsors of this tier can exist (1 for main, 2 for secondary typically)
}
export interface SponsorshipPricingProps {
mainSlot?: SponsorshipSlotConfig;
secondarySlots?: SponsorshipSlotConfig;
acceptingApplications: boolean;
customRequirements?: string;
}
export class SponsorshipPricing {
readonly mainSlot?: SponsorshipSlotConfig;
readonly secondarySlots?: SponsorshipSlotConfig;
readonly acceptingApplications: boolean;
readonly customRequirements?: string;
private constructor(props: SponsorshipPricingProps) {
this.mainSlot = props.mainSlot;
this.secondarySlots = props.secondarySlots;
this.acceptingApplications = props.acceptingApplications;
this.customRequirements = props.customRequirements;
}
static create(props: Partial<SponsorshipPricingProps> = {}): SponsorshipPricing {
return new SponsorshipPricing({
mainSlot: props.mainSlot,
secondarySlots: props.secondarySlots,
acceptingApplications: props.acceptingApplications ?? true,
customRequirements: props.customRequirements,
});
}
/**
* Create default pricing for a driver
*/
static defaultDriver(): SponsorshipPricing {
return new SponsorshipPricing({
mainSlot: {
tier: 'main',
price: Money.create(200, 'USD'),
benefits: ['Suit logo', 'Helmet branding', 'Social mentions'],
available: true,
maxSlots: 1,
},
acceptingApplications: true,
});
}
/**
* Create default pricing for a team
*/
static defaultTeam(): SponsorshipPricing {
return new SponsorshipPricing({
mainSlot: {
tier: 'main',
price: Money.create(500, 'USD'),
benefits: ['Team name suffix', 'Car livery', 'All driver suits'],
available: true,
maxSlots: 1,
},
secondarySlots: {
tier: 'secondary',
price: Money.create(250, 'USD'),
benefits: ['Team page logo', 'Minor livery placement'],
available: true,
maxSlots: 2,
},
acceptingApplications: true,
});
}
/**
* Create default pricing for a race
*/
static defaultRace(): SponsorshipPricing {
return new SponsorshipPricing({
mainSlot: {
tier: 'main',
price: Money.create(300, 'USD'),
benefits: ['Race title sponsor', 'Stream overlay', 'Results banner'],
available: true,
maxSlots: 1,
},
acceptingApplications: true,
});
}
/**
* Create default pricing for a league/season
*/
static defaultLeague(): SponsorshipPricing {
return new SponsorshipPricing({
mainSlot: {
tier: 'main',
price: Money.create(800, 'USD'),
benefits: ['Hood placement', 'League banner', 'Prominent logo', 'League page URL'],
available: true,
maxSlots: 1,
},
secondarySlots: {
tier: 'secondary',
price: Money.create(250, 'USD'),
benefits: ['Side logo placement', 'League page listing'],
available: true,
maxSlots: 2,
},
acceptingApplications: true,
});
}
/**
* Check if a specific tier is available
*/
isSlotAvailable(tier: 'main' | 'secondary'): boolean {
if (tier === 'main') {
return !!this.mainSlot?.available;
}
return !!this.secondarySlots?.available;
}
/**
* Get price for a specific tier
*/
getPrice(tier: 'main' | 'secondary'): Money | null {
if (tier === 'main') {
return this.mainSlot?.price ?? null;
}
return this.secondarySlots?.price ?? null;
}
/**
* Get benefits for a specific tier
*/
getBenefits(tier: 'main' | 'secondary'): string[] {
if (tier === 'main') {
return this.mainSlot?.benefits ?? [];
}
return this.secondarySlots?.benefits ?? [];
}
/**
* Update main slot pricing
*/
updateMainSlot(config: Partial<SponsorshipSlotConfig>): SponsorshipPricing {
const currentMain = this.mainSlot ?? {
tier: 'main' as const,
price: Money.create(0, 'USD'),
benefits: [],
available: true,
maxSlots: 1,
};
return new SponsorshipPricing({
...this,
mainSlot: {
...currentMain,
...config,
tier: 'main',
},
});
}
/**
* Update secondary slot pricing
*/
updateSecondarySlot(config: Partial<SponsorshipSlotConfig>): SponsorshipPricing {
const currentSecondary = this.secondarySlots ?? {
tier: 'secondary' as const,
price: Money.create(0, 'USD'),
benefits: [],
available: true,
maxSlots: 2,
};
return new SponsorshipPricing({
...this,
secondarySlots: {
...currentSecondary,
...config,
tier: 'secondary',
},
});
}
/**
* Enable/disable accepting applications
*/
setAcceptingApplications(accepting: boolean): SponsorshipPricing {
return new SponsorshipPricing({
...this,
acceptingApplications: accepting,
});
}
}