osb/source/game/StarMonsterDatabase.cpp

503 lines
20 KiB
C++
Raw Normal View History

2023-06-20 04:33:09 +00:00
#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))
2023-06-27 10:23:44 +00:00
throw MonsterException(strf("Repeat monster type name '{}'", typeName));
2023-06-20 04:33:09 +00:00
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) {
2023-06-27 10:23:44 +00:00
throw MonsterException(strf("Error loading monster type '{}'", file), e);
2023-06-20 04:33:09 +00:00
}
}
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))
2023-06-27 10:23:44 +00:00
throw MonsterException(strf("Repeat monster part name '{}' for category '{}'", part.name, part.category));
2023-06-20 04:33:09 +00:00
else
partMap[part.name] = part;
} catch (StarException const& e) {
2023-06-27 10:23:44 +00:00
throw MonsterException(strf("Error loading monster part '{}'", file), e);
2023-06-20 04:33:09 +00:00
}
}
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))
2023-06-27 10:23:44 +00:00
throw MonsterException(strf("Repeat monster skill name '{}'", skill.name));
2023-06-20 04:33:09 +00:00
else
m_skills[skill.name] = skill;
} catch (StarException const& e) {
2023-06-27 10:23:44 +00:00
throw MonsterException(strf("Error loading monster skill '{}'", file), e);
2023-06-20 04:33:09 +00:00
}
}
for (auto file : monsterColors) {
try {
auto config = assets->json(file);
if (config.isNull())
continue;
auto paletteName = config.getString("name");
if (m_colorSwaps.contains(paletteName))
2023-06-27 10:23:44 +00:00
throw MonsterException(strf("Duplicate monster colors name '{}'", paletteName));
2023-06-20 04:33:09 +00:00
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) {
2023-06-27 10:23:44 +00:00
throw MonsterException(strf("Error loading monster colors '{}'", file), e);
2023-06-20 04:33:09 +00:00
}
}
}
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 {
2023-06-27 10:23:44 +00:00
Logger::error("Monster colors '{}' not found!", setName);
2023-06-20 04:33:09 +00:00
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};
}
}
}