scum-quest-library
Version:
S.C.U.M. Quest Library
633 lines (621 loc) • 20.4 kB
JavaScript
'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