diff options
Diffstat (limited to 'source/game/StarNpcDatabase.cpp')
-rw-r--r-- | source/game/StarNpcDatabase.cpp | 394 |
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; + } +} + +} |