Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/game/StarMonsterDatabase.cpp
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/StarMonsterDatabase.cpp
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/game/StarMonsterDatabase.cpp')
-rw-r--r--source/game/StarMonsterDatabase.cpp502
1 files changed, 502 insertions, 0 deletions
diff --git a/source/game/StarMonsterDatabase.cpp b/source/game/StarMonsterDatabase.cpp
new file mode 100644
index 0000000..0fbc796
--- /dev/null
+++ b/source/game/StarMonsterDatabase.cpp
@@ -0,0 +1,502 @@
+#include "StarMonsterDatabase.hpp"
+#include "StarMonster.hpp"
+#include "StarAssets.hpp"
+#include "StarRoot.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarRandom.hpp"
+#include "StarLexicalCast.hpp"
+
+namespace Star {
+
+MonsterDatabase::MonsterDatabase() {
+ auto assets = Root::singleton().assets();
+
+ auto monsterTypes = assets->scanExtension("monstertype");
+ auto monsterParts = assets->scanExtension("monsterpart");
+ auto monsterSkills = assets->scanExtension("monsterskill");
+ auto monsterColors = assets->scanExtension("monstercolors");
+
+ assets->queueJsons(monsterTypes);
+ assets->queueJsons(monsterParts);
+ assets->queueJsons(monsterSkills);
+ assets->queueJsons(monsterColors);
+
+ for (auto const& file : monsterTypes) {
+ try {
+ auto config = assets->json(file);
+ String typeName = config.getString("type");
+
+ if (m_monsterTypes.contains(typeName))
+ throw MonsterException(strf("Repeat monster type name '%s'", typeName));
+
+ MonsterType& monsterType = m_monsterTypes[typeName];
+
+ monsterType.typeName = typeName;
+ monsterType.shortDescription = config.optString("shortdescription");
+ monsterType.description = config.optString("description");
+
+ monsterType.categories = jsonToStringList(config.get("categories"));
+ monsterType.partTypes = jsonToStringList(config.get("parts"));
+
+ monsterType.animationConfigPath = AssetPath::relativeTo(file, config.getString("animation"));
+ monsterType.colors = config.getString("colors", "default");
+
+ monsterType.reversed = config.getBool("reversed", false);
+
+ if (config.contains("dropPools"))
+ monsterType.dropPools = config.getArray("dropPools");
+
+ monsterType.baseParameters = config.get("baseParameters", {});
+
+ // for updated monsters, use the partParameterDescription from the
+ // .partparams file
+ if (config.contains("partParameters")) {
+ Json partParameterSource = assets->json(AssetPath::relativeTo(file, config.getString("partParameters")));
+ monsterType.partParameterDescription = partParameterSource.getObject("partParameterDescription");
+ monsterType.partParameterOverrides = partParameterSource.getObject("partParameters");
+ } else {
+ // outdated monsters still have partParameterDescription defined
+ // directly in the
+ // .monstertype file
+ monsterType.partParameterDescription = config.getObject("partParameterDescription", {});
+ }
+
+ } catch (StarException const& e) {
+ throw MonsterException(strf("Error loading monster type '%s'", file), e);
+ }
+ }
+
+ for (auto file : monsterParts) {
+ try {
+ auto config = assets->json(file);
+ if (config.isNull())
+ continue;
+
+ MonsterPart part;
+ part.name = config.getString("name");
+ part.category = config.getString("category");
+ part.type = config.getString("type");
+ part.path = AssetPath::directory(file);
+ part.frames = config.getObject("frames");
+ part.partParameters = config.get("parameters", JsonObject());
+
+ auto& partMap = m_partDirectory[part.category][part.type];
+
+ if (partMap.contains(part.name))
+ throw MonsterException(strf("Repeat monster part name '%s' for category '%s'", part.name, part.category));
+ else
+ partMap[part.name] = part;
+ } catch (StarException const& e) {
+ throw MonsterException(strf("Error loading monster part '%s'", file), e);
+ }
+ }
+
+ for (auto file : monsterSkills) {
+ try {
+ auto config = assets->json(file);
+ if (config.isNull())
+ continue;
+
+ MonsterSkill skill;
+ skill.name = config.getString("name");
+ skill.label = config.getString("label");
+ skill.image = config.getString("image");
+
+ skill.config = config.get("config", JsonObject());
+ skill.parameters = config.get("parameters", JsonObject());
+ skill.animationParameters = config.get("animationParameters", JsonObject());
+
+ if (m_skills.contains(skill.name))
+ throw MonsterException(strf("Repeat monster skill name '%s'", skill.name));
+ else
+ m_skills[skill.name] = skill;
+ } catch (StarException const& e) {
+ throw MonsterException(strf("Error loading monster skill '%s'", file), e);
+ }
+ }
+
+ for (auto file : monsterColors) {
+ try {
+ auto config = assets->json(file);
+ if (config.isNull())
+ continue;
+
+ auto paletteName = config.getString("name");
+ if (m_colorSwaps.contains(paletteName))
+ throw MonsterException(strf("Duplicate monster colors name '%s'", paletteName));
+
+ ColorReplaceMap colorSwaps;
+ for (auto const& swapSet : config.getArray("swaps")) {
+ ColorReplaceMap colorSwaps;
+ for (auto const& swap : swapSet.iterateObject()) {
+ colorSwaps[Color::fromHex(swap.first).toRgba()] = Color::fromHex(swap.second.toString()).toRgba();
+ }
+ m_colorSwaps[paletteName].append(colorSwaps);
+ }
+ } catch (StarException const& e) {
+ throw MonsterException(strf("Error loading monster colors '%s'", file), e);
+ }
+ }
+}
+
+void MonsterDatabase::cleanup() {
+ MutexLocker locker(m_cacheMutex);
+ m_monsterCache.cleanup();
+}
+
+StringList MonsterDatabase::monsterTypes() const {
+ return m_monsterTypes.keys();
+}
+
+MonsterVariant MonsterDatabase::randomMonster(String const& typeName, Json const& uniqueParameters) const {
+ size_t seed;
+ if (auto seedConfig = uniqueParameters.opt("seed")) {
+ if (seedConfig->type() == Json::Type::String) {
+ seed = lexicalCast<uint64_t>(seedConfig->toString());
+ } else {
+ seed = seedConfig->toUInt();
+ }
+ } else {
+ seed = Random::randu64();
+ }
+
+ return monsterVariant(typeName, seed, uniqueParameters);
+}
+
+MonsterVariant MonsterDatabase::monsterVariant(String const& typeName, uint64_t seed, Json const& uniqueParameters) const {
+ MutexLocker locker(m_cacheMutex);
+ return m_monsterCache.get(make_tuple(typeName, seed, uniqueParameters), [this](tuple<String, uint64_t, Json> const& key) {
+ return produceMonster(get<0>(key), get<1>(key), get<2>(key));
+ });
+}
+
+ByteArray MonsterDatabase::writeMonsterVariant(MonsterVariant const& variant) const {
+ DataStreamBuffer ds;
+
+ ds.write(variant.type);
+ ds.write(variant.seed);
+ ds.write(variant.uniqueParameters);
+
+ return ds.data();
+}
+
+MonsterVariant MonsterDatabase::readMonsterVariant(ByteArray const& data) const {
+ DataStreamBuffer ds(data);
+
+ String type = ds.read<String>();
+ uint64_t seed = ds.read<uint64_t>();
+ Json uniqueParameters = ds.read<Json>();
+
+ return monsterVariant(type, seed, uniqueParameters);
+}
+
+Json MonsterDatabase::writeMonsterVariantToJson(MonsterVariant const& mVar) const {
+ return JsonObject{
+ {"type", mVar.type},
+ {"seed", mVar.seed},
+ {"uniqueParameters", mVar.uniqueParameters},
+ };
+}
+
+MonsterVariant MonsterDatabase::readMonsterVariantFromJson(Json const& variant) const {
+ return monsterVariant(variant.getString("type"), variant.getUInt("seed"), variant.getObject("uniqueParameters"));
+}
+
+MonsterPtr MonsterDatabase::createMonster(
+ MonsterVariant monsterVariant, Maybe<float> level, Json uniqueParameters) const {
+ if (uniqueParameters) {
+ monsterVariant.uniqueParameters = jsonMerge(monsterVariant.uniqueParameters, uniqueParameters);
+ monsterVariant.parameters = jsonMerge(monsterVariant.parameters, monsterVariant.uniqueParameters);
+ readCommonParameters(monsterVariant);
+ }
+ return make_shared<Monster>(monsterVariant, level);
+}
+
+MonsterPtr MonsterDatabase::diskLoadMonster(Json const& diskStore) const {
+ return make_shared<Monster>(diskStore);
+}
+
+MonsterPtr MonsterDatabase::netLoadMonster(ByteArray const& netStore) const {
+ return make_shared<Monster>(readMonsterVariant(netStore));
+}
+
+List<Drawable> MonsterDatabase::monsterPortrait(MonsterVariant const& variant) const {
+ NetworkedAnimator animator(variant.animatorConfig);
+ for (auto const& pair : variant.animatorPartTags)
+ animator.setPartTag(pair.first, "partImage", pair.second);
+ animator.setZoom(variant.animatorZoom);
+ auto colorSwap = variant.colorSwap.value(this->colorSwap(variant.parameters.getString("colors", "default"), variant.seed));
+ if (!colorSwap.empty())
+ animator.setProcessingDirectives(imageOperationToString(ColorReplaceImageOperation{colorSwap}));
+ auto drawables = animator.drawables();
+ Drawable::scaleAll(drawables, TilePixels);
+ return drawables;
+}
+
+std::pair<String, String> MonsterDatabase::skillInfo(String const& skillName) const {
+ if (m_skills.contains(skillName)) {
+ auto& skill = m_skills.get(skillName);
+ return std::make_pair(skill.label, skill.image);
+ } else {
+ return std::make_pair("", "");
+ }
+}
+
+Json MonsterDatabase::skillConfigParameter(String const& skillName, String const& configParameterName) const {
+ if (m_skills.contains(skillName)) {
+ auto& skill = m_skills.get(skillName);
+ return skill.config.get(configParameterName, Json());
+ } else {
+ return Json();
+ }
+}
+
+ColorReplaceMap MonsterDatabase::colorSwap(String const& setName, uint64_t seed) const {
+ if (m_colorSwaps.contains(setName))
+ return staticRandomFrom(m_colorSwaps.get(setName), seed);
+ else {
+ Logger::error("Monster colors '%s' not found!", setName);
+ return staticRandomFrom(m_colorSwaps.get("default"), seed);
+ }
+}
+
+Json MonsterDatabase::mergePartParameters(Json const& partParameterDescription, JsonArray const& parameters) {
+ JsonObject mergedParameters;
+
+ // First assign all the defaults.
+ for (auto const& pair : partParameterDescription.iterateObject())
+ mergedParameters[pair.first] = pair.second.get(1);
+
+ // Then go through parameter list and merge based on the merge rules.
+ for (auto const& applyParams : parameters) {
+ for (auto const& pair : applyParams.iterateObject()) {
+ String mergeMethod = partParameterDescription.get(pair.first).getString(0);
+ Json value = mergedParameters.get(pair.first);
+
+ if (mergeMethod.equalsIgnoreCase("add")) {
+ value = value.toDouble() + pair.second.toDouble();
+ } else if (mergeMethod.equalsIgnoreCase("multiply")) {
+ value = value.toDouble() * pair.second.toDouble();
+ } else if (mergeMethod.equalsIgnoreCase("merge")) {
+ // "merge" means to either merge maps, or *append* lists together
+ if (!pair.second.isNull()) {
+ if (value.isNull()) {
+ value = pair.second;
+ } else if (value.type() != pair.second.type()) {
+ value = pair.second;
+ } else {
+ if (pair.second.type() == Json::Type::Array) {
+ auto array = value.toArray();
+ array.appendAll(pair.second.toArray());
+ value = move(array);
+ } else if (pair.second.type() == Json::Type::Object) {
+ auto obj = value.toObject();
+ obj.merge(pair.second.toObject(), true);
+ value = move(obj);
+ }
+ }
+ }
+ } else if (mergeMethod.equalsIgnoreCase("override") && !pair.second.isNull()) {
+ value = pair.second;
+ }
+
+ mergedParameters[pair.first] = value;
+ }
+ }
+
+ return mergedParameters;
+}
+
+Json MonsterDatabase::mergeFinalParameters(JsonArray const& parameters) {
+ JsonObject mergedParameters;
+
+ for (auto const& applyParams : parameters) {
+ for (auto const& pair : applyParams.iterateObject()) {
+ Json value = mergedParameters.value(pair.first);
+
+ // Hard-coded merge for scripts and skills parameters, otherwise merge.
+ if (pair.first == "scripts" || pair.first == "skills" || pair.first == "specialSkills"
+ || pair.first == "baseSkills") {
+ auto array = value.optArray().value();
+ array.appendAll(pair.second.optArray().value());
+ value = move(array);
+ } else {
+ value = jsonMerge(value, pair.second);
+ }
+
+ mergedParameters[pair.first] = value;
+ }
+ }
+
+ return mergedParameters;
+}
+
+void MonsterDatabase::readCommonParameters(MonsterVariant& variant) {
+ variant.shortDescription = variant.parameters.optString("shortdescription").orMaybe(variant.shortDescription);
+ variant.dropPoolConfig = variant.parameters.get("dropPools", variant.dropPoolConfig);
+ variant.scripts = jsonToStringList(variant.parameters.get("scripts"));
+ variant.animationScripts = jsonToStringList(variant.parameters.getArray("animationScripts", {}));
+ variant.animatorConfig = jsonMerge(variant.animatorConfig, variant.parameters.get("animationCustom", JsonObject()));
+ variant.initialScriptDelta = variant.parameters.getUInt("initialScriptDelta", 5);
+ variant.metaBoundBox = jsonToRectF(variant.parameters.get("metaBoundBox"));
+ variant.renderLayer = variant.parameters.optString("renderLayer").apply(parseRenderLayer).value(RenderLayerMonster);
+ variant.scale = variant.parameters.getFloat("scale");
+ variant.movementSettings = ActorMovementParameters(variant.parameters.get("movementSettings", {}));
+ variant.walkMultiplier = variant.parameters.getFloat("walkMultiplier", 1.0f);
+ variant.runMultiplier = variant.parameters.getFloat("runMultiplier", 1.0f);
+ variant.jumpMultiplier = variant.parameters.getFloat("jumpMultiplier", 1.0f);
+ variant.weightMultiplier = variant.parameters.getFloat("weightMultiplier", 1.0f);
+ variant.healthMultiplier = variant.parameters.getFloat("healthMultiplier", 1.0f);
+ variant.touchDamageMultiplier = variant.parameters.getFloat("touchDamageMultiplier", 1.0f);
+ variant.touchDamageConfig = variant.parameters.get("touchDamage", {});
+ variant.animationDamageParts = variant.parameters.getObject("animationDamageParts", {});
+ variant.statusSettings = variant.parameters.get("statusSettings");
+ variant.mouthOffset = jsonToVec2F(variant.parameters.get("mouthOffset")) / TilePixels;
+ variant.feetOffset = jsonToVec2F(variant.parameters.get("feetOffset")) / TilePixels;
+ variant.powerLevelFunction = variant.parameters.getString("powerLevelFunction", "monsterLevelPowerMultiplier");
+ variant.healthLevelFunction = variant.parameters.getString("healthLevelFunction", "monsterLevelHealthMultiplier");
+ variant.clientEntityMode = ClientEntityModeNames.getLeft(variant.parameters.getString("clientEntityMode", "ClientSlaveOnly"));
+ variant.persistent = variant.parameters.getBool("persistent", false);
+ variant.damageTeamType = TeamTypeNames.getLeft(variant.parameters.getString("damageTeamType", "enemy"));
+ variant.damageTeam = variant.parameters.getUInt("damageTeam", 2);
+
+ if (auto sdp = variant.parameters.get("selfDamagePoly", {}))
+ variant.selfDamagePoly = jsonToPolyF(sdp);
+ else
+ variant.selfDamagePoly = *variant.movementSettings.standingPoly;
+
+ variant.portraitIcon = variant.parameters.optString("portraitIcon");
+ variant.damageReceivedAggressiveDuration = variant.parameters.getFloat("damageReceivedAggressiveDuration", 1.0f);
+ variant.onDamagedOthersAggressiveDuration = variant.parameters.getFloat("onDamagedOthersAggressiveDuration", 5.0f);
+ variant.onFireAggressiveDuration = variant.parameters.getFloat("onFireAggressiveDuration", 5.0f);
+
+ variant.nametagColor = jsonToVec3B(variant.parameters.get("nametagColor", JsonArray{255, 255, 255}));
+
+ variant.colorSwap = variant.parameters.optObject("colorSwap").apply([](JsonObject const& json) -> ColorReplaceMap {
+ ColorReplaceMap swaps;
+ for (auto pair : json) {
+ swaps.insert(Color::fromHex(pair.first).toRgba(), Color::fromHex(pair.second.toString()).toRgba());
+ }
+ return swaps;
+ });
+}
+
+MonsterVariant MonsterDatabase::produceMonster(String const& typeName, uint64_t seed, Json const& uniqueParameters) const {
+ RandomSource rand = RandomSource(seed);
+
+ auto const& monsterType = m_monsterTypes.get(typeName);
+
+ MonsterVariant monsterVariant;
+ monsterVariant.type = typeName;
+ monsterVariant.shortDescription = monsterType.shortDescription;
+ monsterVariant.description = monsterType.description;
+ monsterVariant.seed = seed;
+ monsterVariant.uniqueParameters = uniqueParameters;
+
+ monsterVariant.animatorConfig = Root::singleton().assets()->fetchJson(monsterType.animationConfigPath);
+ monsterVariant.reversed = monsterType.reversed;
+
+ // select a list of monster parts
+ List<MonsterPart> monsterParts;
+ auto categoryName = rand.randFrom(monsterType.categories);
+
+ for (auto const& partTypeName : monsterType.partTypes) {
+ auto randPart = rand.randFrom(m_partDirectory.get(categoryName).get(partTypeName)).second;
+ auto selectedPart = uniqueParameters.get("selectedParts", JsonObject()).optString(partTypeName);
+ if (selectedPart)
+ monsterParts.append(m_partDirectory.get(categoryName).get(partTypeName).get(*selectedPart));
+ else
+ monsterParts.append(randPart);
+ }
+
+ for (auto const& partConfig : monsterParts) {
+ for (auto const& pair : partConfig.frames)
+ monsterVariant.animatorPartTags[pair.first] = AssetPath::relativeTo(partConfig.path, pair.second.toString());
+ }
+ JsonArray partParameterList;
+ for (auto const& partConfig : monsterParts) {
+ partParameterList.append(partConfig.partParameters);
+ // Include part parameter overrides
+ if (!monsterType.partParameterOverrides.isNull() && monsterType.partParameterOverrides.contains(partConfig.name))
+ partParameterList.append(monsterType.partParameterOverrides.getObject(partConfig.name));
+ }
+
+ // merge part parameters and unique parameters into base parameters
+ Json baseParameters = monsterType.baseParameters;
+ Json mergedPartParameters = mergePartParameters(monsterType.partParameterDescription, partParameterList);
+ monsterVariant.parameters = mergeFinalParameters({baseParameters, mergedPartParameters});
+ monsterVariant.parameters = jsonMerge(monsterVariant.parameters, uniqueParameters);
+
+ tie(monsterVariant.parameters, monsterVariant.animatorConfig) = chooseSkills(monsterVariant.parameters, monsterVariant.animatorConfig, rand);
+ monsterVariant.animatorZoom = 1.0f;
+ monsterVariant.dropPoolConfig = monsterType.dropPools;
+
+ readCommonParameters(monsterVariant);
+ monsterVariant.animatorZoom = monsterVariant.scale;
+ if (monsterVariant.dropPoolConfig.isType(Json::Type::Array))
+ monsterVariant.dropPoolConfig = rand.randValueFrom(monsterVariant.dropPoolConfig.toArray());
+
+ return monsterVariant;
+}
+
+pair<Json, Json> MonsterDatabase::chooseSkills(
+ Json const& parameters, Json const& animatorConfig, RandomSource& rand) const {
+ // Pick a subset of skills, then merge in any params from those skills
+ if (parameters.contains("baseSkills") || parameters.contains("specialSkills")) {
+ auto skillCount = parameters.getUInt("skillCount", 2);
+
+ auto baseSkillNames = jsonToStringList(parameters.get("baseSkills"));
+ auto specialSkillNames = jsonToStringList(parameters.get("specialSkills"));
+
+ List<String> skillNames;
+
+ // First, pick from base skills
+ while (!baseSkillNames.empty() && skillNames.size() < skillCount)
+ skillNames.append(baseSkillNames.takeAt(rand.randInt(baseSkillNames.size() - 1)));
+
+ // ...then fill in from special skills as needed
+ while (!specialSkillNames.empty() && skillNames.size() < skillCount)
+ skillNames.append(specialSkillNames.takeAt(rand.randInt(specialSkillNames.size() - 1)));
+
+ Json finalAnimatorConfig = animatorConfig;
+ JsonArray allParameters({parameters});
+ for (auto& skillName : skillNames) {
+ if (m_skills.contains(skillName)) {
+ auto const& skill = m_skills.get(skillName);
+ allParameters.append(skill.parameters);
+ finalAnimatorConfig = jsonMerge(finalAnimatorConfig, skill.animationParameters);
+ }
+ }
+
+ // Need to override the final list of skills, instead of merging the lists
+ Json finalParameters = mergeFinalParameters(allParameters).set("skills", jsonFromStringList(skillNames));
+
+ return {finalParameters, finalAnimatorConfig};
+ } else if (parameters.contains("skills")) {
+ auto availableSkillNames = jsonToStringList(parameters.get("skills"));
+ auto skillCount = min<size_t>(parameters.getUInt("skillCount", 2), availableSkillNames.size());
+
+ List<String> skillNames;
+ for (size_t i = 0; i < skillCount; ++i)
+ skillNames.append(availableSkillNames.takeAt(rand.randInt(availableSkillNames.size() - 1)));
+
+ Json finalAnimatorConfig = animatorConfig;
+ JsonArray allParameters({parameters});
+ for (auto& skillName : skillNames) {
+ if (m_skills.contains(skillName)) {
+ auto const& skill = m_skills.get(skillName);
+ allParameters.append(skill.parameters);
+ finalAnimatorConfig = jsonMerge(finalAnimatorConfig, skill.animationParameters);
+ }
+ }
+
+ // Need to override the final list of skills, instead of merging the lists
+ Json finalParameters = mergeFinalParameters(allParameters).set("skills", jsonFromStringList(skillNames));
+
+ return {finalParameters, finalAnimatorConfig};
+ } else {
+ return {parameters, animatorConfig};
+ }
+}
+
+}