395 lines
16 KiB
C++
395 lines
16 KiB
C++
#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 '{}'", typeName));
|
|
|
|
m_npcTypes[typeName] = config;
|
|
|
|
} catch (StarException const& e) {
|
|
throw NpcException(strf("Error loading npc type '{}'", 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 map;
|
|
} else if (merger.isNull()) {
|
|
return base;
|
|
} else {
|
|
return merger;
|
|
}
|
|
}
|
|
|
|
}
|