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

summaryrefslogtreecommitdiff
path: root/source/game/StarNpcDatabase.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'source/game/StarNpcDatabase.cpp')
-rw-r--r--source/game/StarNpcDatabase.cpp394
1 files changed, 394 insertions, 0 deletions
diff --git a/source/game/StarNpcDatabase.cpp b/source/game/StarNpcDatabase.cpp
new file mode 100644
index 0000000..2af5511
--- /dev/null
+++ b/source/game/StarNpcDatabase.cpp
@@ -0,0 +1,394 @@
+#include "StarNpcDatabase.hpp"
+#include "StarEncode.hpp"
+#include "StarRandom.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarNpc.hpp"
+#include "StarRoot.hpp"
+#include "StarItemDatabase.hpp"
+#include "StarSpeciesDatabase.hpp"
+#include "StarNameGenerator.hpp"
+#include "StarStoredFunctions.hpp"
+#include "StarAssets.hpp"
+#include "StarEncode.hpp"
+#include "StarArmors.hpp"
+
+namespace Star {
+
+NpcDatabase::NpcDatabase() {
+ auto assets = Root::singleton().assets();
+
+ auto files = assets->scanExtension("npctype");
+ assets->queueJsons(files);
+ for (auto file : files) {
+ try {
+ auto config = assets->json(file);
+ String typeName = config.getString("type");
+
+ if (m_npcTypes.contains(typeName))
+ throw NpcException(strf("Repeat npc type name '%s'", typeName));
+
+ m_npcTypes[typeName] = config;
+
+ } catch (StarException const& e) {
+ throw NpcException(strf("Error loading npc type '%s'", file), e);
+ }
+ }
+}
+
+NpcVariant NpcDatabase::generateNpcVariant(String const& species, String const& typeName, float level) const {
+ return generateNpcVariant(species, typeName, level, Random::randu64(), {});
+}
+
+NpcVariant NpcDatabase::generateNpcVariant(
+ String const& species, String const& typeName, float level, uint64_t seed, Json const& overrides) const {
+ NpcVariant variant;
+ variant.species = species;
+ variant.typeName = typeName;
+ variant.seed = seed;
+ variant.overrides = overrides;
+
+ RandomSource randSource(seed);
+
+ auto config = buildConfig(typeName, overrides);
+
+ auto levelVariance = jsonToVec2F(config.getArray("levelVariance", {0, 0}));
+ variant.level = max(randSource.randf(level + levelVariance[0], level + levelVariance[1]), 0.0f);
+
+ variant.scripts = jsonToStringList(config.get("scripts"));
+ variant.initialScriptDelta = config.getUInt("initialScriptDelta", 5);
+ variant.scriptConfig = config.get("scriptConfig");
+
+ HumanoidIdentity identity;
+ auto speciesDefinition = Root::singleton().speciesDatabase()->species(species);
+
+ if (config.contains("humanoidConfig"))
+ variant.humanoidConfig = Root::singleton().assets()->json(config.getString("humanoidConfig"));
+ else
+ variant.humanoidConfig = speciesDefinition->humanoidConfig();
+
+ speciesDefinition->generateHumanoid(identity, seed);
+
+ if (config.contains("npcname"))
+ identity.name = config.getString("npcname");
+ else if (config.contains("nameGen")) {
+ identity.name = Root::singleton().nameGenerator()->generateName(
+ jsonToStringList(config.get("nameGen"))[(int)identity.gender], randSource);
+ }
+
+ identity.personality = parsePersonality(randSource.randFrom(variant.humanoidConfig.getArray("personalities")));
+
+ if (config.contains("identity"))
+ identity = HumanoidIdentity(jsonMerge(identity.toJson(), config.get("identity")));
+
+ variant.humanoidIdentity = identity;
+
+ variant.movementParameters =
+ jsonMerge(variant.humanoidConfig.get("movementParameters"), config.get("movementParameters", {}));
+ variant.statusControllerSettings = config.get("statusControllerSettings");
+
+ auto functionDatabase = Root::singleton().functionDatabase();
+ float powerMultiplierModifier = functionDatabase->function("npcLevelPowerMultiplierModifier")->evaluate(variant.level);
+ float protectionMultiplier = functionDatabase->function("npcLevelProtectionMultiplier")->evaluate(variant.level);
+ float maxHealthMultiplier = functionDatabase->function("npcLevelHealthMultiplier")->evaluate(variant.level);
+ float maxEnergyMultiplier = functionDatabase->function("npcLevelEnergyMultiplier")->evaluate(variant.level);
+
+ variant.innateStatusEffects = config.get("innateStatusEffects", JsonArray()).toArray().transformed(jsonToPersistentStatusEffect);
+ variant.innateStatusEffects.append(StatModifier(StatValueModifier{"powerMultiplier", powerMultiplierModifier}));
+ variant.innateStatusEffects.append(StatModifier(StatBaseMultiplier{"protection", protectionMultiplier}));
+ variant.innateStatusEffects.append(StatModifier(StatBaseMultiplier{"maxHealth", maxHealthMultiplier}));
+ variant.innateStatusEffects.append(StatModifier(StatBaseMultiplier{"maxEnergy", maxEnergyMultiplier}));
+
+ variant.touchDamageConfig = config.get("touchDamage", {});
+
+ auto itemsConfig = config.get("items", {});
+ if (!itemsConfig.isNull()) {
+ auto speciesItemsConfig = itemsConfig.get("override", {});
+ if (speciesItemsConfig.isNull())
+ speciesItemsConfig = itemsConfig.get(species, {});
+ if (speciesItemsConfig.isNull())
+ speciesItemsConfig = itemsConfig.get("default", {});
+
+ if (!speciesItemsConfig.isNull()) {
+ Json highestLevelItemsConfig;
+ for (auto levelItemsConfig : speciesItemsConfig.toArray()) {
+ if (variant.level >= levelItemsConfig.getFloat(0)) {
+ highestLevelItemsConfig = levelItemsConfig.get(1);
+ }
+ }
+
+ if (!highestLevelItemsConfig.isNull()) {
+ int randomColorIndex = -1;
+ bool matchColorIndices = config.getBool("matchColorIndices", false);
+
+ for (auto itemSlotConfig : randSource.randFrom(highestLevelItemsConfig.toArray()).toObject()) {
+ ItemDescriptor item = ItemDescriptor(randSource.randFrom(itemSlotConfig.second.toArray()));
+
+ // Randomize color index if colorIndex is an array
+ if (item.parameters().contains("colorIndex")) {
+ auto colorIndex = item.parameters().get("colorIndex");
+ if (colorIndex.isType(Json::Type::Array)) {
+ if (!matchColorIndices || randomColorIndex == -1)
+ randomColorIndex = randSource.randFrom(colorIndex.toArray()).toInt();
+
+ item = item.applyParameters({{"colorIndex", randomColorIndex}});
+ }
+ }
+
+ variant.items[itemSlotConfig.first] = move(item);
+ }
+ }
+ }
+ }
+
+ variant.disableWornArmor = config.getBool("disableWornArmor", true);
+
+ variant.dropPools = jsonToStringList(config.get("dropPools", JsonArray{}));
+
+ variant.persistent = config.getBool("persistent", false);
+ variant.keepAlive = config.getBool("keepAlive", false);
+
+ variant.damageTeam = config.getUInt("damageTeam", 0);
+ variant.damageTeamType = TeamTypeNames.getLeft(config.getString("damageTeamType", "enemy"));
+
+ variant.nametagColor = jsonToVec3B(config.get("nametagColor", JsonArray{255, 255, 255}));
+
+ variant.splashConfig = EntitySplashConfig(config.get("splashConfig"));
+
+ return variant;
+}
+
+ByteArray NpcDatabase::writeNpcVariant(NpcVariant const& variant) const {
+ DataStreamBuffer ds;
+
+ ds.write(variant.species);
+ ds.write(variant.typeName);
+ ds.write(variant.level);
+ ds.write(variant.seed);
+ ds.write(variant.overrides);
+
+ ds.write(variant.initialScriptDelta);
+ ds.write(variant.humanoidIdentity);
+
+ ds.writeMapContainer(variant.items);
+
+ ds.write(variant.persistent);
+ ds.write(variant.keepAlive);
+ ds.write(variant.damageTeam);
+ ds.write(variant.damageTeamType);
+
+ return ds.data();
+}
+
+NpcVariant NpcDatabase::readNpcVariant(ByteArray const& data) const {
+ DataStreamBuffer ds(data);
+
+ NpcVariant variant;
+
+ ds.read(variant.species);
+ ds.read(variant.typeName);
+ ds.read(variant.level);
+ ds.read(variant.seed);
+ ds.read(variant.overrides);
+
+ auto config = buildConfig(variant.typeName, variant.overrides);
+
+ variant.scripts = jsonToStringList(config.get("scripts"));
+ variant.scriptConfig = config.get("scriptConfig");
+
+ ds.read(variant.initialScriptDelta);
+ ds.read(variant.humanoidIdentity);
+
+ auto speciesDefinition = Root::singleton().speciesDatabase()->species(variant.species);
+ if (config.contains("humanoidConfig"))
+ variant.humanoidConfig = Root::singleton().assets()->json(config.getString("humanoidConfig"));
+ else
+ variant.humanoidConfig = speciesDefinition->humanoidConfig();
+
+ variant.movementParameters =
+ jsonMerge(variant.humanoidConfig.get("movementParameters"), config.get("movementParameters", {}));
+ variant.statusControllerSettings = config.get("statusControllerSettings");
+
+ auto functionDatabase = Root::singleton().functionDatabase();
+ float powerMultiplierModifier =
+ functionDatabase->function("npcLevelPowerMultiplierModifier")->evaluate(variant.level);
+ float protectionMultiplier = functionDatabase->function("npcLevelProtectionMultiplier")->evaluate(variant.level);
+ float maxHealthMultiplier = functionDatabase->function("npcLevelHealthMultiplier")->evaluate(variant.level);
+ float maxEnergyMultiplier = functionDatabase->function("npcLevelEnergyMultiplier")->evaluate(variant.level);
+
+ variant.innateStatusEffects =
+ config.get("innateStatusEffects", JsonArray()).toArray().transformed(jsonToPersistentStatusEffect);
+ variant.innateStatusEffects.append(StatModifier(StatValueModifier{"powerMultiplier", powerMultiplierModifier}));
+ variant.innateStatusEffects.append(StatModifier(StatBaseMultiplier{"protection", protectionMultiplier}));
+ variant.innateStatusEffects.append(StatModifier(StatBaseMultiplier{"maxHealth", maxHealthMultiplier}));
+ variant.innateStatusEffects.append(StatModifier(StatBaseMultiplier{"maxEnergy", maxEnergyMultiplier}));
+
+ variant.touchDamageConfig = config.get("touchDamage", {});
+
+ ds.readMapContainer(variant.items);
+
+ variant.disableWornArmor = config.getBool("disableWornArmor", true);
+ variant.dropPools = jsonToStringList(config.get("dropPools", JsonArray{}));
+
+ ds.read(variant.persistent);
+ ds.read(variant.keepAlive);
+ ds.read(variant.damageTeam);
+ ds.read(variant.damageTeamType);
+
+ variant.nametagColor = jsonToVec3B(config.get("nametagColor", JsonArray{255, 255, 255}));
+
+ variant.splashConfig = EntitySplashConfig(config.get("splashConfig"));
+
+ return variant;
+}
+
+Json NpcDatabase::writeNpcVariantToJson(NpcVariant const& variant) const {
+ return JsonObject{{"species", variant.species},
+ {"typeName", variant.typeName},
+ {"level", variant.level},
+ {"seed", variant.seed},
+ {"overrides", variant.overrides},
+ {"initialScriptDelta", variant.initialScriptDelta},
+ {"humanoidIdentity", variant.humanoidIdentity.toJson()},
+ {"items", jsonFromMapV<StringMap<ItemDescriptor>>(variant.items, mem_fn(&ItemDescriptor::diskStore))},
+ {"persistent", variant.persistent},
+ {"keepAlive", variant.keepAlive},
+ {"damageTeam", variant.damageTeam},
+ {"damageTeamType", TeamTypeNames.getRight(variant.damageTeamType)}};
+}
+
+NpcVariant NpcDatabase::readNpcVariantFromJson(Json const& data) const {
+ NpcVariant variant;
+
+ variant.species = data.getString("species");
+ variant.typeName = data.getString("typeName");
+ variant.level = data.getFloat("level");
+ variant.seed = data.getUInt("seed");
+ variant.overrides = data.get("overrides");
+
+ auto config = buildConfig(variant.typeName, variant.overrides);
+
+ variant.scripts = jsonToStringList(config.get("scripts"));
+ variant.scriptConfig = config.get("scriptConfig");
+
+ variant.initialScriptDelta = data.getInt("initialScriptDelta");
+ variant.humanoidIdentity = HumanoidIdentity(data.get("humanoidIdentity"));
+
+ auto speciesDefinition = Root::singleton().speciesDatabase()->species(variant.species);
+ if (config.contains("humanoidConfig"))
+ variant.humanoidConfig = Root::singleton().assets()->json(config.getString("humanoidConfig"));
+ else
+ variant.humanoidConfig = speciesDefinition->humanoidConfig();
+
+ variant.movementParameters =
+ jsonMerge(variant.humanoidConfig.get("movementParameters"), config.get("movementParameters", {}));
+ variant.statusControllerSettings = config.get("statusControllerSettings", {});
+
+ auto functionDatabase = Root::singleton().functionDatabase();
+ float powerMultiplierModifier =
+ functionDatabase->function("npcLevelPowerMultiplierModifier")->evaluate(variant.level);
+ float protectionMultiplier = functionDatabase->function("npcLevelProtectionMultiplier")->evaluate(variant.level);
+ float maxHealthMultiplier = functionDatabase->function("npcLevelHealthMultiplier")->evaluate(variant.level);
+ float maxEnergyMultiplier = functionDatabase->function("npcLevelEnergyMultiplier")->evaluate(variant.level);
+
+ variant.innateStatusEffects =
+ config.get("innateStatusEffects", JsonArray()).toArray().transformed(jsonToPersistentStatusEffect);
+ variant.innateStatusEffects.append(StatModifier(StatValueModifier{"powerMultiplier", powerMultiplierModifier}));
+ variant.innateStatusEffects.append(StatModifier(StatBaseMultiplier{"protection", protectionMultiplier}));
+ variant.innateStatusEffects.append(StatModifier(StatBaseMultiplier{"maxHealth", maxHealthMultiplier}));
+ variant.innateStatusEffects.append(StatModifier(StatBaseMultiplier{"maxEnergy", maxEnergyMultiplier}));
+
+ variant.touchDamageConfig = config.get("touchDamage", {});
+
+ variant.items = jsonToMapV<StringMap<ItemDescriptor>>(data.get("items"), ItemDescriptor::loadStore);
+
+ variant.disableWornArmor = config.getBool("disableWornArmor", true);
+ variant.dropPools = jsonToStringList(config.get("dropPools", JsonArray{}));
+
+ variant.persistent = data.getBool("persistent");
+ variant.keepAlive = data.getBool("keepAlive");
+ variant.damageTeam = data.getUInt("damageTeam");
+ variant.damageTeamType = TeamTypeNames.getLeft(data.getString("damageTeamType"));
+
+ variant.nametagColor = jsonToVec3B(config.get("nametagColor", JsonArray{255, 255, 255}));
+
+ variant.splashConfig = EntitySplashConfig(config.get("splashConfig"));
+
+ return variant;
+}
+
+NpcPtr NpcDatabase::createNpc(NpcVariant const& npcVariant) const {
+ return make_shared<Npc>(npcVariant);
+}
+
+NpcPtr NpcDatabase::diskLoadNpc(Json const& diskStore) const {
+ auto npcVariant = readNpcVariantFromJson(diskStore.get("npcVariant"));
+ return make_shared<Npc>(npcVariant, diskStore);
+}
+
+NpcPtr NpcDatabase::netLoadNpc(ByteArray const& netStore) const {
+ return make_shared<Npc>(readNpcVariant(netStore));
+}
+
+List<Drawable> NpcDatabase::npcPortrait(NpcVariant const& npcVariant, PortraitMode mode) const {
+ Humanoid humanoid(npcVariant.humanoidConfig);
+ humanoid.setIdentity(npcVariant.humanoidIdentity);
+
+ auto itemDatabase = Root::singleton().itemDatabase();
+ auto items = StringMap<ItemDescriptor, CaseInsensitiveStringHash, CaseInsensitiveStringCompare>::from(npcVariant.items);
+
+ auto makeItem = [&npcVariant, &itemDatabase](ItemDescriptor itemDescriptor) -> ItemPtr {
+ return itemDatabase->item(itemDescriptor, npcVariant.level, npcVariant.seed);
+ };
+
+ ArmorWearer armor;
+ if (items.contains("head"))
+ armor.setHeadItem(as<HeadArmor>(makeItem(items["head"])));
+ if (items.contains("headCosmetic"))
+ armor.setHeadItem(as<HeadArmor>(makeItem(items["headCosmetic"])));
+ if (items.contains("chest"))
+ armor.setChestItem(as<ChestArmor>(makeItem(items["chest"])));
+ if (items.contains("chestCosmetic"))
+ armor.setChestCosmeticItem(as<ChestArmor>(makeItem(items["chestCosmetic"])));
+ if (items.contains("legs"))
+ armor.setLegsItem(as<LegsArmor>(makeItem(items["legs"])));
+ if (items.contains("legsCosmetic"))
+ armor.setLegsCosmeticItem(as<LegsArmor>(makeItem(items["legsCosmetic"])));
+ if (items.contains("back"))
+ armor.setBackItem(as<BackArmor>(makeItem(items["back"])));
+ if (items.contains("backCosmetic"))
+ armor.setBackCosmeticItem(as<BackArmor>(makeItem(items["backCosmetic"])));
+
+ armor.setupHumanoidClothingDrawables(humanoid, false);
+
+ return humanoid.renderPortrait(mode);
+}
+
+Json NpcDatabase::buildConfig(String const& typeName, Json const& overrides) const {
+ auto const& baseConfig = m_npcTypes.get(typeName);
+ auto config = mergeConfigValues(baseConfig, overrides);
+
+ String baseTypeName = baseConfig.getString("baseType", "");
+ if (baseTypeName.empty()) {
+ return config;
+ } else {
+ return buildConfig(baseTypeName, config);
+ }
+}
+
+Json NpcDatabase::mergeConfigValues(Json const& base, Json const& merger) const {
+ if (base.type() == Json::Type::Object && merger.type() == Json::Type::Object) {
+ auto map = base.toObject();
+ for (auto const& entry : merger.iterateObject()) {
+ if (!map.insert(entry.first, entry.second).second) {
+ map[entry.first] = mergeConfigValues(map[entry.first], entry.second);
+ }
+ }
+ return std::move(map);
+ } else if (merger.isNull()) {
+ return base;
+ } else {
+ return merger;
+ }
+}
+
+}