UNPKG

scum-quest-library

Version:
603 lines (592 loc) 19.3 kB
import { z } from 'zod'; const BaseConditionSchema = z.object({ CanBeAutoCompleted: z.boolean().default(false), TrackingCaption: z.string(), SequenceIndex: z.number().int().min(0), LocationsShownOnMap: z .array(z.object({ Location: z.union([ z.object({ X: z.number(), Y: z.number(), Z: z.number(), }), z.string(), ]), SizeFactor: z.number().positive().default(1.0), })) .optional(), }); const EliminationConditionsSchema = BaseConditionSchema.extend({ Type: z.literal('Elimination'), TargetCharacters: z.array(z.string()).min(1), Amount: z.number().int().min(1), AllowedWeapons: z.array(z.string()).optional(), }); const CookLevel = z.enum([ 'Raw', 'Undercooked', 'Cooked', 'Overcooked', 'Burned', ]); const CookQuality = z.enum([ 'Ruined', 'Bad', 'Poor', 'Good', 'Excellent', 'Perfect', ]); const ItemRequirementSchema = z.object({ AcceptedItems: z.array(z.string()).min(1), RequiredNum: z.number().int().min(1), RandomAdditionalRequiredNum: z.number().int().min(0).optional(), MinAcceptedItemUses: z.number().int().min(0).optional(), MinAcceptedCookLevel: CookLevel.optional(), MaxAcceptedCookLevel: CookLevel.optional(), MinAcceptedCookQuality: CookQuality.optional(), MinAcceptedItemMass: z.number().int().min(0).optional(), MinAcceptedItemHealth: z.number().int().min(0).max(100).optional(), MinAcceptedItemResourceRatio: z.number().int().min(0).max(100).optional(), MinAcceptedItemResourceAmount: z.number().int().min(0).optional(), }); const FetchConditionSchema = BaseConditionSchema.extend({ Type: z.literal('Fetch'), DisablePurchaseOfRequiredItems: z.boolean().default(false), PlayerKeepsItems: z.boolean().default(false), RequiredItems: z.array(ItemRequirementSchema).min(1), }); const InteractionLocationSchema = z.object({ AnchorMesh: z.string().min(1), Instance: z.number().int().optional(), FallbackTransform: z.string().optional(), VisibleMesh: z.string().optional(), }); const InteractionConditionSchema = BaseConditionSchema.extend({ Type: z.literal('Interaction'), Locations: z.array(InteractionLocationSchema).min(1), MinNeeded: z.number().int().min(1), MaxNeeded: z.number().int().min(1), SpawnOnlyNeeded: z.boolean().default(true), WorldMarkersShowDistance: z.number().int().min(0).default(50), }); const ConditionSchema = z .discriminatedUnion('Type', [ EliminationConditionsSchema, FetchConditionSchema, InteractionConditionSchema, ]) .refine(condition => { if (condition.Type === 'Interaction') { return condition.MinNeeded <= condition.MaxNeeded; } return true; }, { message: 'MinNeeded must be less than or equal to MaxNeeded' }) .refine(condition => { if (condition.Type === 'Interaction') { return condition.MaxNeeded <= condition.Locations.length; } return true; }, { message: 'MaxNeeded must be less than or equal to the number of locations', }); // Condition type constants const CONDITION_TYPES = ['Fetch', 'Elimination', 'Interaction']; // Helper functions for condition conversion const extractConditionItems = (condition) => { if (condition.Type === 'Fetch') { return condition.RequiredItems.flatMap(item => item.AcceptedItems); } if (condition.Type === 'Elimination') { return condition.TargetCharacters; } return []; }; const extractInteractionObjects = (condition) => { if (condition.Type === 'Interaction') { return condition.Locations.map(location => location.AnchorMesh); } return []; }; const getConditionType = (condition) => { return condition.Type; }; const isFetchCondition = (condition) => { return condition.Type === 'Fetch'; }; const isEliminationCondition = (condition) => { return condition.Type === 'Elimination'; }; const isInteractionCondition = (condition) => { return condition.Type === 'Interaction'; }; const NPCSchema = z.enum([ 'Armorer', 'Banker', 'Barber', 'Bartender', 'Doctor', 'Fischerman', 'GeneralGoods', 'Mechanic', ]); const SkillSchema = z.enum([ 'Archery', 'Aviation', 'Awareness', 'Boxing', 'Camouflage', 'Cooking', 'Demolition', 'Diving', 'Endurance', 'Engineering', 'Farming', 'Handgun', 'Medical', 'MeleeWeapons', 'Motorcycle', 'Rifles', 'Running', 'Sniping', 'Stealth', 'Survival', 'Tactics', 'Thievery', ]); const SkillRewardSchema = z.object({ Skill: SkillSchema, Experience: z.number().int().positive(), }); const TradeDealSchema = z.object({ Item: z.string().min(1), Price: z.number().int().positive().optional(), Amount: z.number().int().min(1).optional(), AllowExcluded: z.boolean().optional(), Fame: z.number().int().min(0).optional(), }); const ItemRewardSchema = z.object({ Item: z.string().min(1), Amount: z.number().int().positive(), }); const RewardSchema = z .object({ // Currency Group: 1 Slot CurrencyNormal: z.number().int().min(0).optional(), CurrencyGold: z.number().int().min(0).optional(), Fame: z.number().int().min(0).optional(), // Skills (each skill counts as 1 slot) Skills: z.array(SkillRewardSchema).optional(), // First counts as two slots - additional counts as one TradeDeals: z.array(TradeDealSchema).min(1).optional(), }) .refine(reward => { let slots = 0; if (reward.CurrencyNormal || reward.CurrencyGold || reward.Fame) slots += 1; slots += reward.Skills?.length ?? 0; if (reward.TradeDeals?.length) { slots += 2; if (reward.TradeDeals.length > 1) { slots += reward.TradeDeals.length - 1; } } return slots <= 5; }, { message: 'Total slots for currency, skills, and trade deals must be less than or equal to 5', }); // import { NPCSchema } from '../common/enums'; // No longer needed // Quest tier type for external use const QuestTierSchema = z.union([ z.literal(1), z.literal(2), z.literal(3), ]); const QuestSchema = z .object({ // Accept both casings for compatibility, normalize to AssociatedNpc AssociatedNpc: z.string().min(1).optional(), AssociatedNPC: z.string().min(1).optional(), Tier: QuestTierSchema, Title: z.string().min(1), Description: z.string().min(1), RewardPool: z.array(RewardSchema).min(1), Conditions: z.array(ConditionSchema).min(1), // Required properties TimeLimitHours: z.number().positive(), }) .refine(data => !!(data.AssociatedNpc || data.AssociatedNPC), { message: 'Either AssociatedNpc or AssociatedNPC is required', }) .transform(data => { // Normalize to AssociatedNpc, preferring it if both are present const normalizedNpc = data.AssociatedNpc || data.AssociatedNPC; return { ...data, AssociatedNpc: normalizedNpc, AssociatedNPC: undefined, // Remove the uppercase variant }; }); const BlockedQuestsSchema = z.object({ BlockAllDefaultQuests: z.boolean().default(false), BlockQuestNames: z.array(z.string()).default([]), }); class ConditionBuilder { constructor() { this.condition = {}; this.conditionType = null; } // Set condition type asFetch() { this.conditionType = 'Fetch'; this.condition.Type = 'Fetch'; return this; } asElimination() { this.conditionType = 'Elimination'; this.condition.Type = 'Elimination'; return this; } asInteraction() { this.conditionType = 'Interaction'; this.condition.Type = 'Interaction'; return this; } // Common properties withSequenceIndex(index) { this.condition.SequenceIndex = index; return this; } withCaption(caption) { this.condition.TrackingCaption = caption; return this; } autoComplete(auto = true) { this.condition.CanBeAutoCompleted = auto; return this; } disablePurchase(disable = true) { if (this.conditionType !== 'Fetch') { throw new Error('disablePurchase() can only be used with Fetch conditions'); } this.condition.DisablePurchaseOfRequiredItems = disable; return this; } // Fetch-specific methods requireItems(items, count) { if (this.conditionType !== 'Fetch') { throw new Error('requireItems() can only be used with Fetch conditions'); } const fetchCondition = this.condition; if (!fetchCondition.RequiredItems) fetchCondition.RequiredItems = []; fetchCondition.RequiredItems.push({ AcceptedItems: items, RequiredNum: count, }); return this; } // Add this method to the ConditionBuilder class: requireItemsWithOptions(items, count, options = {}) { if (this.conditionType !== 'Fetch') { throw new Error('requireItemsWithOptions() can only be used with Fetch conditions'); } const fetchCondition = this.condition; if (!fetchCondition.RequiredItems) fetchCondition.RequiredItems = []; const itemReq = { AcceptedItems: items, RequiredNum: count, }; if (options.minHealth !== undefined) itemReq.MinAcceptedItemHealth = options.minHealth; if (options.minMass !== undefined) itemReq.MinAcceptedItemMass = options.minMass; if (options.minUses !== undefined) itemReq.MinAcceptedItemUses = options.minUses; if (options.cookLevel?.min) itemReq.MinAcceptedCookLevel = options.cookLevel.min; if (options.cookLevel?.max) itemReq.MaxAcceptedCookLevel = options.cookLevel.max; if (options.cookQuality) itemReq.MinAcceptedCookQuality = options.cookQuality; if (options.resourceRatio !== undefined) itemReq.MinAcceptedItemResourceRatio = options.resourceRatio; if (options.resourceAmount !== undefined) itemReq.MinAcceptedItemResourceAmount = options.resourceAmount; fetchCondition.RequiredItems.push(itemReq); return this; } requireItemsWithHealth(items, count, minHealth) { if (this.conditionType !== 'Fetch') { throw new Error('requireItemsWithHealth() can only be used with Fetch conditions'); } const fetchCondition = this.condition; if (!fetchCondition.RequiredItems) fetchCondition.RequiredItems = []; fetchCondition.RequiredItems.push({ AcceptedItems: items, RequiredNum: count, MinAcceptedItemHealth: minHealth, }); return this; } addMapLocation(X, Y, Z, sizeFactor = 1.0) { if (!this.condition.LocationsShownOnMap) { this.condition.LocationsShownOnMap = []; } this.condition.LocationsShownOnMap.push({ Location: { X, Y, Z }, SizeFactor: sizeFactor, }); return this; } keepItems(keep = true) { if (this.conditionType !== 'Fetch') { throw new Error('keepItems() can only be used with Fetch conditions'); } this.condition.PlayerKeepsItems = keep; return this; } // Elimination-specific methods eliminateTargets(targets, count) { if (this.conditionType !== 'Elimination') { throw new Error('eliminateTargets() can only be used with Elimination conditions'); } const elimCondition = this.condition; elimCondition.TargetCharacters = targets; elimCondition.Amount = count; return this; } withWeapons(weapons) { if (this.conditionType !== 'Elimination') { throw new Error('withWeapons() can only be used with Elimination conditions'); } this.condition.AllowedWeapons = weapons; return this; } build() { if (!this.conditionType) { throw new Error('Condition type must be set before building'); } return this.condition; } } class RewardBuilder { constructor() { this.reward = {}; } currency(normal, gold, fame) { if (normal !== undefined) this.reward.CurrencyNormal = normal; if (gold !== undefined) this.reward.CurrencyGold = gold; if (fame !== undefined) this.reward.Fame = fame; return this; } addSkill(skill, experience) { if (!this.reward.Skills) this.reward.Skills = []; this.reward.Skills.push({ Skill: skill, Experience: experience }); return this; } addTradeDeal(item, options = {}) { if (!this.reward.TradeDeals) this.reward.TradeDeals = []; this.reward.TradeDeals.push({ Item: item, ...options, }); return this; } build() { return this.reward; } } class QuestBuilder { constructor() { this.quest = { RewardPool: [], Conditions: [], }; } // Basic quest properties withNPC(npc) { this.quest.AssociatedNpc = npc; return this; } withTier(tier) { this.quest.Tier = tier; return this; } withTitle(title) { this.quest.Title = title; return this; } withDescription(description) { this.quest.Description = description; return this; } withTimeLimit(hours) { this.quest.TimeLimitHours = hours; return this; } // Condition builders addCondition(builderFn) { const conditionBuilder = new ConditionBuilder(); const condition = builderFn(conditionBuilder); this.quest.Conditions.push(condition); return this; } addFetchCondition(builderFn) { const conditionBuilder = new ConditionBuilder().asFetch(); const finalBuilder = builderFn(conditionBuilder); this.quest.Conditions.push(finalBuilder.build()); return this; } addEliminationCondition(builderFn) { const conditionBuilder = new ConditionBuilder().asElimination(); const finalBuilder = builderFn(conditionBuilder); this.quest.Conditions.push(finalBuilder.build()); return this; } addInteractionCondition(builderFn) { const conditionBuilder = new ConditionBuilder().asInteraction(); const finalBuilder = builderFn(conditionBuilder); this.quest.Conditions.push(finalBuilder.build()); return this; } // Reward builders addReward(builderFn) { const rewardBuilder = new RewardBuilder(); const finalBuilder = builderFn(rewardBuilder); this.quest.RewardPool.push(finalBuilder.build()); return this; } // Quick reward helpers addCurrencyReward(normal, gold, fame) { return this.addReward(builder => builder.currency(normal, gold, fame)); } // Validation and building validate() { const result = QuestSchema.safeParse(this.quest); if (result.success) { return { success: true }; } return { success: false, errors: result.error.issues.map(issue => `${issue.path.join('.')}: ${issue.message}`), }; } build() { const result = QuestSchema.parse(this.quest); return result; } // Get current state for debugging preview() { return { ...this.quest }; } } // Conversion utilities const questToFormData = (quest) => { return { AssociatedNpc: quest.AssociatedNpc, AssociatedNPC: undefined, // Will be normalized by schema Tier: quest.Tier, Title: quest.Title, Description: quest.Description, TimeLimitHours: quest.TimeLimitHours, RewardPool: quest.RewardPool, Conditions: quest.Conditions, }; }; const formDataToQuest = (formData) => { const result = QuestSchema.safeParse(formData); if (!result.success) { throw new Error(`Invalid quest form data: ${result.error.issues.map(i => i.message).join(', ')}`); } return result.data; }; // Helper functions for form validation (using existing schemas) const validateQuestForm = (data) => { const result = QuestSchema.safeParse(data); if (result.success) { return { success: true }; } return { success: false, errors: result.error.issues.map(issue => `${issue.path.join('.')}: ${issue.message}`), }; }; const validateConditionForm = (data) => { const result = ConditionSchema.safeParse(data); if (result.success) { return { success: true }; } return { success: false, errors: result.error.issues.map(issue => `${issue.path.join('.')}: ${issue.message}`), }; }; const validateRewardForm = (data) => { const result = RewardSchema.safeParse(data); if (result.success) { return { success: true }; } return { success: false, errors: result.error.issues.map(issue => `${issue.path.join('.')}: ${issue.message}`), }; }; // Form-specific helper functions const getConditionTypeOptions = () => { return CONDITION_TYPES.map(type => ({ value: type, label: type })); }; const createEmptyCondition = (type) => { const baseCondition = { CanBeAutoCompleted: false, TrackingCaption: '', SequenceIndex: 0, }; switch (type) { case 'Fetch': return { ...baseCondition, Type: 'Fetch', DisablePurchaseOfRequiredItems: false, PlayerKeepsItems: false, RequiredItems: [], }; case 'Elimination': return { ...baseCondition, Type: 'Elimination', TargetCharacters: [], Amount: 1, }; case 'Interaction': return { ...baseCondition, Type: 'Interaction', Locations: [], MinNeeded: 1, MaxNeeded: 1, SpawnOnlyNeeded: true, WorldMarkersShowDistance: 50, }; default: throw new Error(`Unknown condition type: ${type}`); } }; const createEmptyReward = () => { return { CurrencyNormal: 0, CurrencyGold: 0, Fame: 0, }; }; export { BlockedQuestsSchema, CONDITION_TYPES, ConditionBuilder, ConditionSchema, CookQuality, ItemRewardSchema, NPCSchema, QuestBuilder, QuestSchema, QuestTierSchema, RewardBuilder, RewardSchema, SkillRewardSchema, SkillSchema, TradeDealSchema, createEmptyCondition, createEmptyReward, extractConditionItems, extractInteractionObjects, formDataToQuest, getConditionType, getConditionTypeOptions, isEliminationCondition, isFetchCondition, isInteractionCondition, questToFormData, validateConditionForm, validateQuestForm, validateRewardForm }; //# sourceMappingURL=index.mjs.map