Repeatable quest generation

This commit is contained in:
Chomp
2025-01-17 22:37:40 +00:00
parent 7a4bc0a3a6
commit a8a0ce1880
4 changed files with 154 additions and 87 deletions
+21 -16
View File
@@ -14,6 +14,7 @@ using Core.Servers;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
using Core.Utils.Extensions;
namespace Core.Controllers;
@@ -315,28 +316,32 @@ public class RepeatableQuestController
questPool.Pool.Pickup.Locations[ELocationName.any] = ["any"];
var eliminationConfig = _repeatableQuestHelper.GetEliminationConfigByPmcLevel(pmcLevel.Value, repeatableConfig);
var targetsConfig = _repeatableQuestHelper.ProbabilityObjectArray<string, BossInfo>(eliminationConfig.Targets);
var targetsConfig = _repeatableQuestHelper.ProbabilityObjectArray<Target, string, BossInfo>(eliminationConfig.Targets);
// Populate Elimination quest targets and their locations
foreach (var target in targetsConfig) {
// Target is boss
//if (target.isBoss)
//{
// questPool.Pool.Elimination.Targets[targetKey] = new { locations: ["any"] };
//}
//else
//{
// // Non-boss targets
// var possibleLocations = locations;
if (target.Data.IsBoss)
{
var targets = questPool.Pool.Elimination.Targets.Get<TargetLocation>(target.Key);
targets.Locations.Clear();
targets.Locations.Add("any");
}
else
{
// Non-boss targets
var possibleLocations = locations;
// var allowedLocations =
// targetKey == "Savage"
// ? possibleLocations.filter((location) => location != "laboratory") // Exclude labs for Savage targets.
// : possibleLocations;
var targets = questPool.Pool.Elimination.Targets.Get<TargetLocation>(target.Key);
var allowedLocations =
target.Key == "Savage"
? targets.Locations.Where((location) => location != "laboratory") // Exclude labs for Savage targets.
: possibleLocations;
// questPool.Pool.Elimination.Targets[targetKey] = new { Locations = allowedLocations };
//}
_logger.Error("NOT IMPLEMENTED - GenerateQuestPool");
targets.Locations.Clear();
targets.Locations.AddRange(allowedLocations);
}
}
return questPool;
+119 -70
View File
@@ -1,16 +1,13 @@
using Core.Annotations;
using Core.Annotations;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Health;
using Core.Models.Eft.Hideout;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Models.Spt.Repeatable;
using Core.Models.Utils;
using Core.Utils;
using static System.Runtime.InteropServices.JavaScript.JSType;
using System.Collections.Generic;
using Core.Helpers;
using Core.Servers;
using Core.Services;
using Core.Utils.Collections;
using Core.Utils.Extensions;
@@ -23,12 +20,14 @@ public class RepeatableQuestGenerator
{
protected ISptLogger<RepeatableQuestGenerator> _logger;
protected RandomUtil _randomUtil;
private readonly HashUtil _hashUtil;
private readonly MathUtil _mathUtil;
private readonly RepeatableQuestHelper _repeatableQuestHelper;
private readonly ItemHelper _itemHelper;
private readonly RepeatableQuestRewardGenerator _repeatableQuestRewardGenerator;
private readonly DatabaseService _databaseService;
protected HashUtil _hashUtil;
protected MathUtil _mathUtil;
protected RepeatableQuestHelper _repeatableQuestHelper;
protected ItemHelper _itemHelper;
protected RepeatableQuestRewardGenerator _repeatableQuestRewardGenerator;
protected DatabaseService _databaseService;
protected ConfigServer _configServer;
protected QuestConfig _questConfig;
public RepeatableQuestGenerator(
ISptLogger<RepeatableQuestGenerator> logger,
@@ -38,7 +37,8 @@ public class RepeatableQuestGenerator
RepeatableQuestHelper repeatableQuestHelper,
ItemHelper itemHelper,
RepeatableQuestRewardGenerator repeatableQuestRewardGenerator,
DatabaseService databaseService
DatabaseService databaseService,
ConfigServer configServer
)
{
_logger = logger;
@@ -49,6 +49,9 @@ public class RepeatableQuestGenerator
_itemHelper = itemHelper;
_repeatableQuestRewardGenerator = repeatableQuestRewardGenerator;
_databaseService = databaseService;
_configServer = configServer;
_questConfig = _configServer.GetConfig<QuestConfig>();
}
/// <summary>
@@ -57,7 +60,7 @@ public class RepeatableQuestGenerator
/// </summary>
/// <param name="sessionId">Session id</param>
/// <param name="pmcLevel">Player's level for requested items and reward generation</param>
/// <param name="pmcTraderInfo">Players traper standing/rep levels</param>
/// <param name="pmcTraderInfo">Players trader standing/rep levels</param>
/// <param name="questTypePool">Possible quest types pool</param>
/// <param name="repeatableConfig">Repeatable quest config</param>
/// <returns>RepeatableQuest</returns>
@@ -73,10 +76,10 @@ public class RepeatableQuestGenerator
// Get traders from whitelist and filter by quest type availability
var traders = repeatableConfig.TraderWhitelist
.Where((x) => x.QuestTypes.Contains(questType))
.Select((x) => x.TraderId).ToList();
.Where(x => x.QuestTypes.Contains(questType))
.Select(x => x.TraderId).ToList();
// filter out locked traders
traders = traders.Where((x) => pmcTraderInfo[x].Unlocked.GetValueOrDefault(false)).ToList();
traders = traders.Where(x => pmcTraderInfo[x].Unlocked.GetValueOrDefault(false)).ToList();
var traderId = _randomUtil.DrawRandomFromList(traders).FirstOrDefault();
return questType switch
@@ -92,13 +95,14 @@ public class RepeatableQuestGenerator
/// <summary>
/// Generate a randomised Elimination quest
/// </summary>
/// <param name="sessionId">Session id</param>
/// <param name="pmcLevel">Player's level for requested items and reward generation</param>
/// <param name="traderId">Trader from which the quest will be provided</param>
/// <param name="questTypePool">Pools for quests (used to avoid redundant quests)</param>
/// <param name="repeatableConfig">The configuration for the repeatably kind (daily, weekly) as configured in QuestConfig for the requestd quest</param>
/// <returns>Object of quest type format for "Elimination" (see assets/database/templates/repeatableQuests.json)</returns>
protected RepeatableQuest GenerateEliminationQuest(
string sessionid,
string sessionId,
int pmcLevel,
string traderId,
QuestTypePool questTypePool,
@@ -110,7 +114,7 @@ public class RepeatableQuestGenerator
var eliminationConfig = _repeatableQuestHelper.GetEliminationConfigByPmcLevel(pmcLevel, repeatableConfig);
var locationsConfig = repeatableConfig.Locations;
var targetsConfig = _repeatableQuestHelper.ProbabilityObjectArray<Target, string, BossInfo>(eliminationConfig.Targets);
var bodypartsConfig = _repeatableQuestHelper.ProbabilityObjectArray<BodyPart, string, List<string>>(eliminationConfig.BodyParts);
var bodyPartsConfig = _repeatableQuestHelper.ProbabilityObjectArray<BodyPart, string, List<string>>(eliminationConfig.BodyParts);
var weaponCategoryRequirementConfig = _repeatableQuestHelper.ProbabilityObjectArray<WeaponRequirement, string, List<string>>(eliminationConfig.WeaponCategoryRequirements);
var weaponRequirementConfig = _repeatableQuestHelper.ProbabilityObjectArray<WeaponRequirement, string, List<string>>(eliminationConfig.WeaponRequirements);
@@ -139,7 +143,7 @@ public class RepeatableQuestGenerator
// Target on bodyPart max. difficulty is that of the least probable element
var maxTargetDifficulty = 1 / targetsConfig.MinProbability();
var maxBodyPartsDifficulty = eliminationConfig.MinKills / bodypartsConfig.MinProbability();
var maxBodyPartsDifficulty = eliminationConfig.MinKills / bodyPartsConfig.MinProbability();
// maxDistDifficulty is defined by 2, this could be a tuning parameter if we don't like the reward generation
var maxDistDifficulty = 2;
@@ -159,13 +163,13 @@ public class RepeatableQuestGenerator
var targetKey = targetsConfig.Draw()[0];
var targetDifficulty = 1 / targetsConfig.Probability(targetKey);
var locations = questTypePool.Pool.Elimination.Targets[targetKey].locations;
var locations = questTypePool.Pool.Elimination.Targets.Get<TargetLocation>(targetKey).Locations;
// we use any as location if "any" is in the pool and we do not hit the specific location random
// we use any also if the random condition is not met in case only "any" was in the pool
var locationKey = "any";
if (locations.includes("any") &&
(eliminationConfig.SpecificLocationProbability < rand.Next() || locations.length <= 1)
if (locations.Contains("any") &&
(eliminationConfig.SpecificLocationProbability < rand.Next() || locations.Count <= 1)
)
{
locationKey = "any";
@@ -173,13 +177,13 @@ public class RepeatableQuestGenerator
}
else
{
locations = locations.Where((l) => l != "any");
if (locations.length > 0)
locations = locations.Where(l => l != "any").ToList();
if (locations.Count > 0)
{
locationKey = _randomUtil.DrawRandomFromList(locations)[0];
questTypePool.Pool.Elimination.Targets[targetKey].locations = locations.Where(
(l) => l != locationKey);
if (questTypePool.Pool.Elimination.Targets[targetKey].locations.length == 0)
locationKey = _randomUtil.DrawRandomFromList(locations).FirstOrDefault();
questTypePool.Pool.Elimination.Targets.Get<TargetLocation>(targetKey).Locations = locations.Where(
(l) => l != locationKey).ToList();
if (questTypePool.Pool.Elimination.Targets.Get<TargetLocation>(targetKey).Locations.Count == 0)
{
questTypePool.Pool.Elimination.Targets.Remove(targetKey);
}
@@ -193,20 +197,20 @@ public class RepeatableQuestGenerator
// draw the target body part and calculate the difficulty factor
var bodyPartsToClient = new List<string>();
var bodyPartDifficulty = 0;
var bodyPartDifficulty = 0d;
if (eliminationConfig.BodyPartProbability > rand.Next())
{
// if we add a bodyPart condition, we draw randomly one or two parts
// each bodyPart of the BODYPARTS ProbabilityObjectArray includes the string(s) which need to be presented to the client in ProbabilityObjectArray.data
// e.g. we draw "Arms" from the probability array but must present ["LeftArm", "RightArm"] to the client
bodyPartsToClient = [];
var bodyParts = bodypartsConfig.Draw(_randomUtil.RandInt(1, 3), false);
var bodyParts = bodyPartsConfig.Draw(_randomUtil.RandInt(1, 3), false);
double probability = 0;
foreach (var bi in bodyParts)
{
// more than one part lead to an "OR" condition hence more parts reduce the difficulty
probability += bodypartsConfig.Probability(bi);
foreach (var biClient in bodypartsConfig.Data(bi))
probability += bodyPartsConfig.Probability(bi).Value;
foreach (var biClient in bodyPartsConfig.Data(bi))
{
bodyPartsToClient.Add(biClient);
}
@@ -215,11 +219,11 @@ public class RepeatableQuestGenerator
}
// Draw a distance condition
int distance;
int? distance = -1;
var distanceDifficulty = 0;
var isDistanceRequirementAllowed = !eliminationConfig.DistLocationBlacklist.Contains(locationKey);
if (targetsConfig.Data(targetKey).IsBoss)
if (targetsConfig.Data(targetKey).IsBoss.GetValueOrDefault(false))
{
// Get all boss spawn information
var bossSpawns = _databaseService.GetLocations().GetDictionary()
@@ -243,27 +247,31 @@ public class RepeatableQuestGenerator
if (eliminationConfig.DistanceProbability > rand.Next() && isDistanceRequirementAllowed)
{
// Random distance with lower values more likely; simple distribution for starters...
distance = Math.Floor(
Math.Abs(Math.Random() - rand.Next()) * (1 + eliminationConfig.MaxDistance - eliminationConfig.MinDistance) +
eliminationConfig.MinDistance);
distance = Math.Ceiling(distance / 5) * 5;
distanceDifficulty = (maxDistDifficulty * distance) / eliminationConfig.MaxDistance;
distance = (int)Math.Floor(
(decimal)(Math.Abs(rand.Next(0, 1) - rand.Next(0, 1)) * (1 + eliminationConfig.MaxDistance - eliminationConfig.MinDistance) +
eliminationConfig.MinDistance));
distance = (int)Math.Ceiling((decimal)(distance / 5)) * 5;
distanceDifficulty = (int)(maxDistDifficulty * distance / eliminationConfig.MaxDistance);
}
string allowedWeaponsCategory;
string? allowedWeaponsCategory = null;
if (eliminationConfig.WeaponCategoryRequirementProbability > rand.Next())
{
// Filter out close range weapons from far distance requirement
if (distance > 50)
{
weaponCategoryRequirementConfig = weaponCategoryRequirementConfig.Where((category) =>
["Shotgun", "Pistol"].Contains(category.Key));
List<string> weaponTypes = ["Shotgun", "Pistol"];
weaponCategoryRequirementConfig = (ProbabilityObjectArray<WeaponRequirement, string, List<string>>)weaponCategoryRequirementConfig
.Where((category) => weaponTypes
.Contains(category.Key));
}
else if (distance < 20)
{
List<string> weaponTypes = ["MarksmanRifle", "DMR"];
// Filter out far range weapons from close distance requirement
weaponCategoryRequirementConfig = weaponCategoryRequirementConfig.Where((category) =>
["MarksmanRifle", "DMR"].Contains(category.Key));
weaponCategoryRequirementConfig = (ProbabilityObjectArray<WeaponRequirement, string, List<string>>)weaponCategoryRequirementConfig
.Where((category) => weaponTypes
.Contains(category.Key));
}
// Pick a weighted weapon category
@@ -274,12 +282,12 @@ public class RepeatableQuestGenerator
}
// Only allow a specific weapon requirement if a weapon category was not chosen
string allowedWeapon;
if (!allowedWeaponsCategory && eliminationConfig.WeaponRequirementProb > rand.Next())
string? allowedWeapon = null;
if (allowedWeaponsCategory is not null && eliminationConfig.WeaponRequirementProbability > rand.Next())
{
var weaponRequirement = weaponRequirementConfig.Draw(1, false);
var allowedWeaponsCategory = weaponRequirementConfig.Data(weaponRequirement[0])[0];
var allowedWeapons = _itemHelper.GetItemTplsOfBaseType(allowedWeaponsCategory);
var specificAllowedWeaponCategory = weaponRequirementConfig.Data(weaponRequirement[0])[0];
var allowedWeapons = _itemHelper.GetItemTplsOfBaseType(specificAllowedWeaponCategory);
allowedWeapon = _randomUtil.GetArrayValue(allowedWeapons);
}
@@ -291,18 +299,18 @@ public class RepeatableQuestGenerator
// e.g. killing reshala 5 times from a distance of 200m with a headshot.
var maxDifficulty = DifficultyWeighing(1, 1, 1, 1, 1);
var curDifficulty = DifficultyWeighing(
targetDifficulty / maxTargetDifficulty,
bodyPartDifficulty / maxBodyPartsDifficulty,
targetDifficulty.Value / maxTargetDifficulty,
bodyPartDifficulty / maxBodyPartsDifficulty.Value,
distanceDifficulty / maxDistDifficulty,
killDifficulty / maxKillDifficulty,
allowedWeaponsCategory || allowedWeapon ? 1 : 0);
killDifficulty / maxKillDifficulty.Value,
allowedWeaponsCategory is not null || allowedWeapon is not null ? 1 : 0);
// Aforementioned issue makes it a bit crazy since now all easier quests give significantly lower rewards than Completion / Exploration
// I therefore moved the mapping a bit up (from 0.2...1 to 0.5...2) so that normal difficulty still gives good reward and having the
// crazy maximum difficulty will lead to a higher difficulty reward gain factor than 1
var difficulty = _mathUtil.MapToRange(curDifficulty, minDifficulty, maxDifficulty, 0.5, 2);
var quest = GenerateRepeatableTemplate("Elimination", traderId, repeatableConfig.Side, sessionid);
var quest = GenerateRepeatableTemplate("Elimination", traderId, repeatableConfig.Side, sessionId);
// ASSUMPTION: All fence quests are for scavs
if (traderId == Traders.FENCE)
@@ -317,13 +325,14 @@ public class RepeatableQuestGenerator
// Only add specific location condition if specific map selected
if (locationKey != "any")
{
availableForFinishCondition.Counter.Conditions.Add(GenerateEliminationLocation(locationsConfig[locationKey]));
Enum.TryParse(typeof (ELocationName), locationKey, true, out var locationId);
availableForFinishCondition.Counter.Conditions.Add(GenerateEliminationLocation(locationsConfig[(ELocationName)locationId]));
}
availableForFinishCondition.Counter.Conditions.Add(
GenerateEliminationCondition(
targetKey,
bodyPartsToClient,
distance,
distance.Value,
allowedWeapon,
allowedWeaponsCategory)
);
@@ -350,32 +359,31 @@ public class RepeatableQuestGenerator
*/
protected int GetEliminationKillCount(
string targetKey,
object targetsConfig, //ProbabilityObjectArray<string, IBossInfo>
ProbabilityObjectArray<Target, string, BossInfo> targetsConfig,
EliminationConfig eliminationConfig)
{
if (targetsConfig.Data(targetKey).isBoss)
if (targetsConfig.Data(targetKey).IsBoss.GetValueOrDefault(false))
{
return _randomUtil.RandInt(eliminationConfig.MinBossKills, eliminationConfig.MaxBossKills + 1);
return _randomUtil.RandInt(eliminationConfig.MinBossKills.Value, eliminationConfig.MaxBossKills + 1);
}
if (targetsConfig.Data(targetKey).isPmc)
if (targetsConfig.Data(targetKey).IsPmc.GetValueOrDefault(false))
{
return _randomUtil.RandInt(eliminationConfig.MinPmcKills, eliminationConfig.MaxPmcKills + 1);
return _randomUtil.RandInt(eliminationConfig.MinPmcKills.Value, eliminationConfig.MaxPmcKills + 1);
}
return _randomUtil.RandInt(eliminationConfig.MinKills, eliminationConfig.MaxKills + 1);
return _randomUtil.RandInt(eliminationConfig.MinKills.Value, eliminationConfig.MaxKills + 1);
}
protected double DifficultyWeighing(
int target,
int bodyPart,
double target,
double bodyPart,
int dist,
int kill,
int weaponRequirement)
{
return Math.Sqrt(Math.Sqrt(target) + bodyPart + dist + weaponRequirement) * kill;
}
}
/// <summary>
/// Get a number of kills needed to complete elimination quest
@@ -415,13 +423,54 @@ public class RepeatableQuestGenerator
/// <returns>EliminationCondition object</returns>
protected QuestConditionCounterCondition GenerateEliminationCondition(
string target,
List<string> targetedBodyParts,
double distance,
string allowedWeapon,
string allowedWeaponCategory
List<string>? targetedBodyParts,
double? distance,
string? allowedWeapon,
string? allowedWeaponCategory
)
{
throw new NotImplementedException();
var killConditionProps = new QuestConditionCounterCondition{
Id = _hashUtil.Generate(),
DynamicLocale = true,
Target = target, // e,g, "AnyPmc"
Value = 1,
ResetOnSessionEnd = false,
EnemyHealthEffects = [],
Daytime = new DaytimeCounter(){ From = 0, To = 0 },
ConditionType = "Kills"};
if (target.StartsWith("boss"))
{
killConditionProps.Target = "Savage";
killConditionProps.SavageRole = [target];
}
// Has specific body part hit condition
if (targetedBodyParts is not null)
{
killConditionProps.BodyPart = targetedBodyParts;
}
// Don't allow distance + melee requirement
if (distance is not null && allowedWeaponCategory != "5b5f7a0886f77409407a7f96")
{
killConditionProps.Distance = new CounterConditionDistance{ CompareMethod = ">=", Value = distance.Value };
}
// Has specific weapon requirement
if (allowedWeapon is not null)
{
killConditionProps.Weapon = [allowedWeapon];
}
// Has specific weapon category requirement
if (allowedWeaponCategory?.Length > 0)
{
// TODO - fix - does weaponCategories exist?
// killConditionProps.weaponCategories = [allowedWeaponCategory];
}
return killConditionProps;
}
/// <summary>
@@ -500,7 +549,7 @@ public class RepeatableQuestGenerator
/// <returns>guid</returns>
protected string GetQuestLocationByMapId(string locationKey)
{
throw new NotImplementedException();
return _questConfig.LocationIdMap[locationKey];
}
/// <summary>
+1 -1
View File
@@ -435,7 +435,7 @@ public record ValueCompare
public record CounterConditionDistance
{
[JsonPropertyName("value")]
public int? Value { get; set; }
public double? Value { get; set; }
[JsonPropertyName("compareMethod")]
public string? CompareMethod { get; set; }
+13
View File
@@ -1,3 +1,5 @@
using Core.Models.Spt.Repeatable;
namespace Core.Utils.Extensions
{
public static class ObjectExtensions
@@ -6,5 +8,16 @@ namespace Core.Utils.Extensions
{
return obj.GetType().GetProperties().Any(x => x.Name == key.ToString());
}
public static T? Get<T>(this object obj, string toLower)
{
return (T?)obj.GetType().GetProperties().SingleOrDefault(p => p.GetJsonName() == toLower)?.GetValue(obj);
}
public static void Remove<T>(this EliminationTargetPool pool, T key)
{
}
}
}