diff --git a/src/collections/intelligence/Factions.ts b/src/collections/intelligence/Factions.ts index 49a9525..b35eb14 100644 --- a/src/collections/intelligence/Factions.ts +++ b/src/collections/intelligence/Factions.ts @@ -13,6 +13,30 @@ export const Factions: CollectionConfig = { delete: isDeveloper, read: ({ req }) => !!req.user, }, + hooks: { + beforeValidate: [ + async ({ data, operation, originalDoc, req }) => { + if (data?.type !== "playerFaction") return data; + + const payload = req.payload; + const existing = await payload.find({ + collection: "factions", + where: { type: { equals: "playerFaction" } }, + limit: 1, + depth: 0, + overrideAccess: true, + }); + + const conflict = existing.docs.find((doc) => doc.id !== originalDoc?.id); + if (conflict) { + throw new Error( + `Only one faction can be the Player Faction at a time. "${conflict.name}" is already the Player Faction.`, + ); + } + return data; + }, + ], + }, fields: [ { name: "name", @@ -24,6 +48,10 @@ export const Factions: CollectionConfig = { name: "type", type: "select", options: [ + { + label: "Player Faction", + value: "playerFaction", + }, { label: "Major Faction", value: "majorFaction", @@ -43,11 +71,68 @@ export const Factions: CollectionConfig = { ], required: true, defaultValue: "minorFaction", + admin: { + description: + "Player Faction = the faction our unit is currently playing as. Only one faction can carry this type at a time.", + }, }, { name: "description", type: "richText", }, + { + name: "reputation", + type: "number", + admin: { + description: + "This faction's standing in the world, from -100 (reviled) to 100 (revered). 0 is neutral. Adjusted manually by admins/GMs as campaign events unfold.", + }, + defaultValue: 0, + min: -100, + max: 100, + }, + { + name: "relationships", + type: "array", + labels: { + singular: "Relationship", + plural: "Relationships", + }, + admin: { + description: + "Pairwise standing between this faction and another faction, from -100 (hostile) to 100 (allied). 0 is neutral. One entry per counterpart faction.", + }, + fields: [ + { + name: "targetFaction", + type: "relationship", + relationTo: "factions", + required: true, + admin: { + description: "The other faction in this pairwise relationship.", + }, + }, + { + name: "value", + type: "number", + required: true, + defaultValue: 0, + min: -100, + max: 100, + admin: { + description: + "Standing from -100 (hostile) to 100 (allied). 0 is neutral.", + }, + }, + { + name: "notes", + type: "textarea", + admin: { + description: "Optional context for why this relationship stands where it does.", + }, + }, + ], + }, { name: "playableFaction", type: "checkbox", @@ -70,5 +155,14 @@ export const Factions: CollectionConfig = { relationTo: "campaigns", hasMany: true, }, + { + name: "coverImage", + type: "upload", + relationTo: "media", + admin: { + description: + "Background image for the faction detail page hero. Renders translucent with a top-to-bottom fade to black. Recommended aspect ratio: 16:9 or wider.", + }, + }, ], }; diff --git a/src/payload-types.ts b/src/payload-types.ts index 2fabf60..7e97ecd 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -986,7 +986,10 @@ export interface Vehicle { export interface Faction { id: number; name: string; - type: 'majorFaction' | 'minorFaction' | 'corporation' | 'special'; + /** + * Player Faction = the faction our unit is currently playing as. Only one faction can carry this type at a time. + */ + type: 'playerFaction' | 'majorFaction' | 'minorFaction' | 'corporation' | 'special'; description?: { root: { type: string; @@ -1002,6 +1005,30 @@ export interface Faction { }; [k: string]: unknown; } | null; + /** + * This faction's standing in the world, from -100 (reviled) to 100 (revered). 0 is neutral. Adjusted manually by admins/GMs as campaign events unfold. + */ + reputation?: number | null; + /** + * Pairwise standing between this faction and another faction, from -100 (hostile) to 100 (allied). 0 is neutral. One entry per counterpart faction. + */ + relationships?: + | { + /** + * The other faction in this pairwise relationship. + */ + targetFaction: number | Faction; + /** + * Standing from -100 (hostile) to 100 (allied). 0 is neutral. + */ + value: number; + /** + * Optional context for why this relationship stands where it does. + */ + notes?: string | null; + id?: string | null; + }[] + | null; /** * Whether or not this faction is playable. Some factions may be specifically intended to be unplayable based on narrative requirements. */ @@ -1011,6 +1038,10 @@ export interface Faction { */ startingFaction?: boolean | null; associatedCampaigns?: (number | Campaign)[] | null; + /** + * Background image for the faction detail page hero. Renders translucent with a top-to-bottom fade to black. Recommended aspect ratio: 16:9 or wider. + */ + coverImage?: (number | null) | Media; updatedAt: string; createdAt: string; } @@ -1040,6 +1071,10 @@ export interface Campaign { } | null; startDate?: string | null; endDate?: string | null; + /** + * Background image for the campaign selector card on the campaigns page. Renders translucent with hover effects. Recommended aspect ratio: 16:9 or square. + */ + coverImage?: (number | null) | Media; updatedAt: string; createdAt: string; } @@ -1519,6 +1554,10 @@ export interface Mission { * Hash of the last attendance state rendered to Discord (managed by the bot). */ discordAttendanceHash?: string | null; + /** + * Background image for the mission page hero header. Renders translucent with a top-to-bottom fade to black. Recommended aspect ratio: 16:9 or wider. + */ + coverImage?: (number | null) | Media; classification: { map: number | Map; missionType: 'Zeus' | 'PvE' | 'PvP' | 'PvPvE' | 'Training' | 'Special'; @@ -2731,6 +2770,7 @@ export interface MissionsSelect { operationType?: T; discordMessageId?: T; discordAttendanceHash?: T; + coverImage?: T; classification?: | T | { @@ -2859,6 +2899,7 @@ export interface CampaignsSelect { description?: T; startDate?: T; endDate?: T; + coverImage?: T; updatedAt?: T; createdAt?: T; } @@ -2870,9 +2911,19 @@ export interface FactionsSelect { name?: T; type?: T; description?: T; + reputation?: T; + relationships?: + | T + | { + targetFaction?: T; + value?: T; + notes?: T; + id?: T; + }; playableFaction?: T; startingFaction?: T; associatedCampaigns?: T; + coverImage?: T; updatedAt?: T; createdAt?: T; }