#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 '{}'", 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 '{}'", file), e); } } for (auto const& 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 '{}' for category '{}'", part.name, part.category)); else partMap[part.name] = part; } catch (StarException const& e) { throw MonsterException(strf("Error loading monster part '{}'", file), e); } } for (auto const& 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 '{}'", skill.name)); else m_skills[skill.name] = skill; } catch (StarException const& e) { throw MonsterException(strf("Error loading monster skill '{}'", file), e); } } for (auto const& 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 '{}'", 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 '{}'", 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(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 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(); uint64_t seed = ds.read(); Json uniqueParameters = ds.read(); 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 level, Json uniqueParameters) const { if (uniqueParameters) { monsterVariant.uniqueParameters = jsonMerge(monsterVariant.uniqueParameters, uniqueParameters); monsterVariant.parameters = jsonMerge(monsterVariant.parameters, monsterVariant.uniqueParameters); readCommonParameters(monsterVariant); } return make_shared(monsterVariant, level); } MonsterPtr MonsterDatabase::diskLoadMonster(Json const& diskStore) const { return make_shared(diskStore); } MonsterPtr MonsterDatabase::netLoadMonster(ByteArray const& netStore) const { return make_shared(readMonsterVariant(netStore)); } List 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 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 '{}' 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 = std::move(array); } else if (pair.second.type() == Json::Type::Object) { auto obj = value.toObject(); obj.merge(pair.second.toObject(), true); value = std::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 = std::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 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 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 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(parameters.getUInt("skillCount", 2), availableSkillNames.size()); List 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}; } } }