UNPKG

scum-quest-library

Version:
633 lines (621 loc) 20.4 kB
'use strict'; var zod = require('zod'); const BaseConditionSchema = zod.z.object({ CanBeAutoCompleted: zod.z.boolean().default(false), TrackingCaption: zod.z.string(), SequenceIndex: zod.z.number().int().min(0), LocationsShownOnMap: zod.z .array(zod.z.object({ Location: zod.z.union([ zod.z.object({ X: zod.z.number(), Y: zod.z.number(), Z: zod.z.number(), }), zod.z.string(), ]), SizeFactor: zod.z.number().positive().default(1.0), })) .optional(), }); const EliminationConditionsSchema = BaseConditionSchema.extend({ Type: zod.z.literal('Elimination'), TargetCharacters: zod.z.array(zod.z.string()).min(1), Amount: zod.z.number().int().min(1), AllowedWeapons: zod.z.array(zod.z.string()).optional(), }); const CookLevel = zod.z.enum([ 'Raw', 'Undercooked', 'Cooked', 'Overcooked', 'Burned', ]); const CookQuality = zod.z.enum([ 'Ruined', 'Bad', 'Poor', 'Good', 'Excellent', 'Perfect', ]); const ItemRequirementSchema = zod.z.object({ AcceptedItems: zod.z.array(zod.z.string()).min(1), RequiredNum: zod.z.number().int().min(1), RandomAdditionalRequiredNum: zod.z.number().int().min(0).optional(), MinAcceptedItemUses: zod.z.number().int().min(0).optional(), MinAcceptedCookLevel: CookLevel.optional(), MaxAcceptedCookLevel: CookLevel.optional(), MinAcceptedCookQuality: CookQuality.optional(), MinAcceptedItemMass: zod.z.number().int().min(0).optional(), MinAcceptedItemHealth: zod.z.number().int().min(0).max(100).optional(), MinAcceptedItemResourceRatio: zod.z.number().int().min(0).max(100).optional(), MinAcceptedItemResourceAmount: zod.z.number().int().min(0).optional(), }); const FetchConditionSchema = BaseConditionSchema.extend({ Type: zod.z.literal('Fetch'), DisablePurchaseOfRequiredItems: zod.z.boolean().default(false), PlayerKeepsItems: zod.z.boolean().default(false), RequiredItems: zod.z.array(ItemRequirementSchema).min(1), }); const InteractionLocationSchema = zod.z.object({ AnchorMesh: zod.z.string().min(1), Instance: zod.z.number().int().optional(), FallbackTransform: zod.z.string().optional(), VisibleMesh: zod.z.string().optional(), }); const InteractionConditionSchema = BaseConditionSchema.extend({ Type: zod.z.literal('Interaction'), Locations: zod.z.array(InteractionLocationSchema).min(1), MinNeeded: zod.z.number().int().min(1), MaxNeeded: zod.z.number().int().min(1), SpawnOnlyNeeded: zod.z.boolean().default(true), WorldMarkersShowDistance: zod.z.number().int().min(0).default(50), }); const ConditionSchema = zod.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 = zod.z.enum([ 'Armorer', 'Banker', 'Barber', 'Bartender', 'Doctor', 'Fischerman', 'GeneralGoods', 'Mechanic', ]); const SkillSchema = zod.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 = zod.z.object({ Skill: SkillSchema, Experience: zod.z.number().int().positive(), }); const TradeDealSchema = zod.z.object({ Item: zod.z.string().min(1), Price: zod.z.number().int().positive().optional(), Amount: zod.z.number().int().min(1).optional(), AllowExcluded: zod.z.boolean().optional(), Fame: zod.z.number().int().min(0).optional(), }); const ItemRewardSchema = zod.z.object({ Item: zod.z.string().min(1), Amount: zod.z.number().int().positive(), }); const RewardSchema = zod.z .object({ // Currency Group: 1 Slot CurrencyNormal: zod.z.number().int().min(0).optional(), CurrencyGold: zod.z.number().int().min(0).optional(), Fame: zod.z.number().int().min(0).optional(), // Skills (each skill counts as 1 slot) Skills: zod.z.array(SkillRewardSchema).optional(), // First counts as two slots - additional counts as one TradeDeals: zod.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 = zod.z.union([ zod.z.literal(1), zod.z.literal(2), zod.z.literal(3), ]); const QuestSchema = zod.z .object({ // Accept both casings for compatibility, normalize to AssociatedNpc AssociatedNpc: zod.z.string().min(1).optional(), AssociatedNPC: zod.z.string().min(1).optional(), Tier: QuestTierSchema, Title: zod.z.string().min(1), Description: zod.z.string().min(1), RewardPool: zod.z.array(RewardSchema).min(1), Conditions: zod.z.array(ConditionSchema).min(1), // Required properties TimeLimitHours: zod.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 = zod.z.object({ BlockAllDefaultQuests: zod.z.boolean().default(false), BlockQuestNames: zod.z.array(zod.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, }; }; exports.BlockedQuestsSchema = BlockedQuestsSchema; exports.CONDITION_TYPES = CONDITION_TYPES; exports.ConditionBuilder = ConditionBuilder; exports.ConditionSchema = ConditionSchema; exports.CookQuality = CookQuality; exports.ItemRewardSchema = ItemRewardSchema; exports.NPCSchema = NPCSchema; exports.QuestBuilder = QuestBuilder; exports.QuestSchema = QuestSchema; exports.QuestTierSchema = QuestTierSchema; exports.RewardBuilder = RewardBuilder; exports.RewardSchema = RewardSchema; exports.SkillRewardSchema = SkillRewardSchema; exports.SkillSchema = SkillSchema; exports.TradeDealSchema = TradeDealSchema; exports.createEmptyCondition = createEmptyCondition; exports.createEmptyReward = createEmptyReward; exports.extractConditionItems = extractConditionItems; exports.extractInteractionObjects = extractInteractionObjects; exports.formDataToQuest = formDataToQuest; exports.getConditionType = getConditionType; exports.getConditionTypeOptions = getConditionTypeOptions; exports.isEliminationCondition = isEliminationCondition; exports.isFetchCondition = isFetchCondition; exports.isInteractionCondition = isInteractionCondition; exports.questToFormData = questToFormData; exports.validateConditionForm = validateConditionForm; exports.validateQuestForm = validateQuestForm; exports.validateRewardForm = validateRewardForm; //# sourceMappingURL=index.js.map