64 std::string parseError;
66 if (!document.valid())
return failure(parseError.empty() ?
"invalid JSON" : parseError,
"$");
67 const auto root = document.root();
68 if (!
root.isArray() ||
root.size() == 0)
69 return failure(
"encounter catalogue must be a non-empty array",
"$");
71 std::unordered_map<std::string, EncounterDefinition> proposed;
74 const std::string
path =
"$[" + std::to_string(
index) +
"]";
75 if (!
object.isObject())
return failure(
"encounter definition must be an object",
path);
76 for (
const auto &
key :
object.keys())
77 if (knownFields().count(
key) == 0)
78 return failure(
"encounter definition contains an unknown field",
path +
"." +
key);
79 const auto id =
object.get(
"id");
80 const auto displayName =
object.get(
"displayName");
81 if (!
id.isString() || !validId(
id.asString()))
82 return failure(
"encounter id must be a stable non-empty id",
path +
".id");
83 if (proposed.count(
id.asString()) != 0)
return failure(
"duplicate encounter id",
path +
".id");
84 if (!displayName.isString() || displayName.asString().empty() || displayName.asString().size() > 512)
85 return failure(
"displayName must be a non-empty string of at most 512 bytes",
86 path +
".displayName");
87 const auto members =
object.get(
"members");
89 return failure(
"members must be a non-empty array with at most 32 entries",
91 const auto xpReward =
object.get(
"xpReward");
92 const auto xpGrowth =
object.get(
"xpGrowth");
93 const auto goldReward =
object.get(
"goldReward");
94 if (!finiteNumber(xpReward, 0.0,
true) || !finiteNumber(xpGrowth, 1.0) ||
95 !finiteNumber(goldReward, 0.0))
96 return failure(
"rewards must be finite and within their valid ranges",
path);
97 const auto requiredQuestId =
object.get(
"requiredQuestId");
98 const auto notifyTopic =
object.get(
"notifyTopic");
99 const auto notifyTarget =
object.get(
"notifyTarget");
100 const auto defeatCounterId =
object.get(
"defeatCounterId");
101 const auto levelPointAttributeId =
object.get(
"levelPointAttributeId");
102 if (!requiredQuestId.isString() || !validId(requiredQuestId.asString()) ||
104 return failure(
"requiredQuestId must reference a registered quest",
path +
".requiredQuestId");
105 if (!notifyTopic.isString() || !validId(notifyTopic.asString()) ||
106 !notifyTarget.isString() || !validId(notifyTarget.asString()) ||
107 !defeatCounterId.isString() || !validId(defeatCounterId.asString()) ||
108 !levelPointAttributeId.isString() || !validId(levelPointAttributeId.asString()))
109 return failure(
"notification and counter ids must be stable non-empty ids",
path);
110 const auto notifyAmount =
object.get(
"notifyAmount");
111 const auto defeatCounterAmount =
object.get(
"defeatCounterAmount");
112 const auto pointsPerLevel =
object.get(
"pointsPerLevel");
113 if (!validPositiveInt(notifyAmount) || !validPositiveInt(defeatCounterAmount) ||
114 !validPositiveInt(pointsPerLevel,
true))
115 return failure(
"notification/counter amounts must be positive and pointsPerLevel non-negative",
path);
118 definition.id =
id.asString();
119 definition.displayName = displayName.asString();
120 std::unordered_set<std::string> memberIds;
121 for (std::size_t memberIndex = 0; memberIndex <
members.size(); ++memberIndex) {
123 const std::string memberPath =
path +
".members[" + std::to_string(memberIndex) +
"]";
124 if (!
member.isObject())
return failure(
"encounter member must be an object", memberPath);
126 if (knownMemberFields().count(
key) == 0)
127 return failure(
"encounter member contains an unknown field", memberPath +
"." +
key);
128 const auto memberId =
member.get(
"id");
129 const auto memberName =
member.get(
"displayName");
130 const auto memberSkill =
member.get(
"skillId");
131 const auto attack =
member.get(
"attack");
132 const auto defense =
member.get(
"defense");
133 const auto maxHp =
member.get(
"maxHp");
134 const auto speed =
member.get(
"speed");
135 if (!memberId.isString() || !validId(memberId.asString()) ||
136 !memberIds.insert(memberId.asString()).second)
137 return failure(
"member id must be stable and unique within the encounter",
139 if (!memberName.isString() || memberName.asString().empty() ||
140 memberName.asString().size() > 512)
141 return failure(
"member displayName must be a non-empty string of at most 512 bytes",
142 memberPath +
".displayName");
143 if (!memberSkill.isString() || !validId(memberSkill.asString()) ||
145 return failure(
"member skillId must reference a registered skill",
146 memberPath +
".skillId");
147 if (!finiteNumber(attack, 0.0) || !finiteNumber(defense, 0.0) ||
148 !finiteNumber(maxHp, 0.0,
true) || !finiteNumber(speed, 0.0))
149 return failure(
"member combat stats must be finite and within their valid ranges",
152 parsedMember.
id = memberId.asString();
154 parsedMember.
skillId = memberSkill.asString();
155 parsedMember.
attack = attack.asDouble();
156 parsedMember.
defense = defense.asDouble();
157 parsedMember.
maxHp = maxHp.asDouble();
158 parsedMember.
speed = speed.asDouble();
159 definition.members.push_back(std::move(parsedMember));
161 definition.xpReward = xpReward.asDouble();
162 definition.xpGrowth = xpGrowth.asDouble();
163 definition.goldReward = goldReward.asDouble();
164 definition.requiredQuestId = requiredQuestId.asString();
165 definition.notifyTopic = notifyTopic.asString();
166 definition.notifyTarget = notifyTarget.asString();
167 definition.notifyAmount =
static_cast<int>(notifyAmount.asInt64());
168 definition.defeatCounterId = defeatCounterId.asString();
169 definition.defeatCounterAmount =
static_cast<int>(defeatCounterAmount.asInt64());
170 definition.levelPointAttributeId = levelPointAttributeId.asString();
171 definition.pointsPerLevel =
static_cast<int>(pointsPerLevel.asInt64());
172 proposed.emplace(definition.id, std::move(definition));
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.