void BuildEndGameEquipment(EquipmentJobRank[] scoredJobEquipment, JobData jobData, Saint.ClassJob sJob, bool isCombatJob) { // We only want endgame equipment for jobs, not starting classes. // Filter out DoW/M classes that don't have a parent. // Everything before the Heavensward classless jobs. if (sJob.Key < 31 && isCombatJob && sJob.ParentClassJob == sJob) { return; } // EndGame equipment is simply grouped by slot and ordered. var endGameJobEquipmentList = new JObject(); _builder.Db.EndGameEquipmentByJob[sJob.Abbreviation] = endGameJobEquipmentList; var endGameEquipmentBySlot = scoredJobEquipment // Go back 10 levels for non-combat jobs, so Lv. 60 shows prior Lv. 50 gear for comparison. .Where(e => e.EquipmentLevel >= jobData.MaxLevel - (isCombatJob ? 0 : 10)) .GroupBy(e => e.Slot); foreach (var endGameSlot in endGameEquipmentBySlot) { // Sort results in this slot. EquipmentJobRank[] orderedResults; if (isCombatJob) { orderedResults = endGameSlot.OrderByDescending(e => e.Equipment.ItemLevel.Key).ThenByDescending(e => e.Equipment.Key).ToArray(); } else { orderedResults = endGameSlot.OrderByDescending(e => e.Rank.Score).ToArray(); } // Skip slots that don't have a single piece at cap. (WHM wands and shields) if (orderedResults.Max(e => e.EquipmentLevel) < jobData.MaxLevel) { continue; } // Add references to top results. var topResults = orderedResults.Take(9).ToArray(); foreach (var result in topResults) { _builder.Db.AddReference(endGameJobEquipmentList, "item", result.Equipment.Key, false); } // Store them in the list. var results = topResults .Select(e => new JObject(new JProperty("id", e.Equipment.Key))) .ToArray(); endGameJobEquipmentList[endGameSlot.Key.Key.ToString()] = new JArray(results); // Don't predict progression on combat jobs. if (isCombatJob) { continue; } // Record end-game progression on DoH/DoL jobs. dynamic previousItem = null; double previousScore = 0; foreach (var e in orderedResults.Take(9).Reverse()) { if (e.Rank.Score == previousScore) { continue; } var item = _builder.Db.ItemsById[e.Equipment.Key]; _builder.UpgradeItem(previousItem, item); previousItem = item; previousScore = e.Rank.Score; } } }
EquipmentRank Rank(Saint.Items.Equipment equipment, JobData jobData, SW[] weights, bool isCombatJob) { if (isCombatJob && equipment.EquipmentLevel >= jobData.MaxLevel) { return(null); // Optimization: Stat ranks at cap are obsolete for combat jobs. } var rank = new EquipmentRank() { Equipment = equipment }; var melds = 0; var maxMeldsAllowed = equipment.IsAdvancedMeldingPermitted ? 5 : equipment.FreeMateriaSlots; var ranksByWeight = new Dictionary <SW, EquipmentStatRank>(); // First calculate rankings with base weights. foreach (var weight in weights) { var value = equipment.GetParameterValue(weight.BaseParam, equipment.CanBeHq); var statRank = new EquipmentStatRank(); statRank.Param = weight.BaseParam.Name; statRank.Score = value * weight.Value; statRank.BaseScore = (weight.ExcludeFromBaseValue ? 0 : value * weight.Value); statRank.MaxValue = equipment.GetMaximumParamValue(weight.BaseParam); statRank.Value = value; rank.StatRanks.Add(statRank); ranksByWeight[weight] = statRank; rank.Score += statRank.Score; rank.BaseScore += statRank.BaseScore; } // Kick out PVP equipment now. if (equipment.IsPvP) { return(rank); } // Next calculate optimal melds, one at a time. while (melds < maxMeldsAllowed) { EquipmentStatRank currentBestStatRank = null; int currentBestNewValue = 0; double currentBestWeightedIncrease = 0; double currentOvermeldPenalty = 0; // Check each meldable stat. foreach (var weight in weights.Where(w => w.Materia != null)) { var statRank = ranksByWeight[weight]; // Check each meld tier. foreach (var sMateria in weight.Materia) { var newValue = Math.Min(statRank.MaxValue, statRank.Value + sMateria.Value); var weightedIncrease = (newValue - statRank.Value) * weight.Value; // Don't count advanced melds that can't overcome their overmeld penalty. double penalty = 0; if (melds >= equipment.FreeMateriaSlots) { if (!sMateria.Item.IsAdvancedMeldingPermitted) { continue; } var slot = melds - equipment.FreeMateriaSlots; if (sMateria.Tier == 5 && slot > 0) { continue; // Can't overmeld VI past the first slot. } penalty = OvermeldPenalties[slot, sMateria.Tier]; if (weightedIncrease < penalty) { continue; } } // Check for a new best meld choice. if (currentBestWeightedIncrease < weightedIncrease) { currentBestWeightedIncrease = weightedIncrease; currentBestNewValue = newValue; currentBestStatRank = statRank; currentOvermeldPenalty = penalty; } } } // Stop when no good melds are left. if (currentBestNewValue == 0) { break; } // Apply a good meld. currentBestStatRank.Value = currentBestNewValue; currentBestStatRank.Score += currentBestWeightedIncrease; rank.Score += currentBestWeightedIncrease; rank.OvermeldPenalty -= currentOvermeldPenalty; melds++; } // Apply overmeld penalty if applicable. if (melds > equipment.FreeMateriaSlots && rank.OvermeldPenalty < 0) { var overmeldPenalty = new EquipmentStatRank(); overmeldPenalty.Param = "Overmeld Penalty"; overmeldPenalty.Score = rank.OvermeldPenalty; overmeldPenalty.BaseScore = 0; rank.StatRanks.Add(overmeldPenalty); rank.Score += overmeldPenalty.Score; } return(rank); }
void BuildLevelingEquipment(EquipmentJobRank[] scoredJobEquipment, JobData jobData, Saint.ClassJob sJob) { var levelingJobEquipmentArray = new JArray(); _builder.Db.LevelingEquipmentByJob[sJob.Abbreviation] = levelingJobEquipmentArray; Dictionary <Saint.EquipSlotCategory, List <EquipmentJobRank> > previousLevelingItems = null; // Find the best crafted equipment from 1-max that isn't a star recipe. for (var elvl = 1; elvl < jobData.MaxLevel; elvl++) { var currentLevelingItems = new Dictionary <Saint.EquipSlotCategory, List <EquipmentJobRank> >(); var relevantEquipment = scoredJobEquipment .Where(e => e.HasCraftingRecipe || e.HasGcVendor || e.HasGilVendor) .Where(e => e.EquipmentLevel <= elvl) .Where(e => e.Item.achievements == null) // No achievement gear. .Where(e => e.Vendors == null || !e.Vendors.Select(t => (int)t).Contains(1006004)) // No calamity salvager gear. .GroupBy(e => e.Slot); var equipmentLevelSlots = new JObject(); foreach (var scoresBySlot in relevantEquipment) { // Use the base score without melds because max overmelding on leveling equ is unrealistic. var orderedEquipment = scoresBySlot .OrderByDescending(s => s.Rank.BaseScore) .ThenBy(s => s.Equipment.Key) .ToArray(); // Find the highest scores in 3 categories: // 1. With a crafting recipe. // 2. From the GC vendor. // 3. From a regular vendor. var currentList = new List <EquipmentJobRank>(); var highestCrafted = orderedEquipment.FirstOrDefault(s => s.HasCraftingRecipe); if (highestCrafted != null) { currentList.Add(highestCrafted); } var highestGc = orderedEquipment.FirstOrDefault(s => s.HasGcVendor); var highestGil = orderedEquipment.FirstOrDefault(s => s.HasGilVendor); // Ignore GC gear if gil shop gear is better. if (highestGc != null && highestGil != null && highestGil.Rank.BaseScore > highestGc.Rank.BaseScore) { highestGc = null; } if (highestGc != null) { currentList.Add(highestGc); } if (highestGil != null) { currentList.Add(highestGil); } // No equipment - probably nothing available for this slot at this level. if (currentList.Count == 0) { continue; } currentLevelingItems[scoresBySlot.Key] = currentList; var records = new JArray(); foreach (var item in currentList.Distinct().OrderByDescending(i => i.Rank.BaseScore)) { dynamic record = new JObject(); record.id = item.Equipment.Key; if (item.HasCraftingRecipe) { record.craft = 1; } if (item.HasGcVendor) { record.gc = 1; } if (item.HasGilVendor) { record.gil = 1; } records.Add(record); _builder.Db.AddReference(levelingJobEquipmentArray, "item", item.Equipment.Key, false); } equipmentLevelSlots[scoresBySlot.Key.Key.ToString()] = records; } levelingJobEquipmentArray.Add(equipmentLevelSlots); // Link upgrades if (previousLevelingItems != null) { foreach (var pair in currentLevelingItems) { if (!previousLevelingItems.TryGetValue(pair.Key, out var previousEquipment)) { continue; } foreach (var previousItem in previousEquipment) { foreach (var currentItem in pair.Value) { // Skip identical equipment levels. The tool covers horizontal upgrades. if (currentItem.EquipmentLevel == previousItem.EquipmentLevel) { continue; } // List all classes of upgrade, regardless of type. if (currentItem.Rank.BaseScore > previousItem.Rank.BaseScore) { _builder.UpgradeItem(previousItem.Item, currentItem.Item); } } } } } previousLevelingItems = currentLevelingItems; } }
void Initialize(Saint.BaseParam[] sBaseParams, Saint.Materia[] sMateria) { // Monk _jobsByKey[20] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Physical Damage", 15), new SW("Strength", 1), new SW("Determination", .001), new SW("Skill Speed", .001), new SW("Critical Hit", .001), new SW("Direct Hit Rate", .001) }); _jobsByKey[2] = _jobsByKey[20]; // Pugilist // Dragoon _jobsByKey[22] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Physical Damage", 15), new SW("Strength", 1), new SW("Determination", .001), new SW("Skill Speed", .001), new SW("Critical Hit", .001), new SW("Direct Hit Rate", .001) }); _jobsByKey[4] = _jobsByKey[22]; // Lancer // Bard _jobsByKey[23] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Physical Damage", 15), new SW("Dexterity", 1), new SW("Determination", .001), new SW("Skill Speed", .001), new SW("Critical Hit", .001), new SW("Direct Hit Rate", .001) }); _jobsByKey[5] = _jobsByKey[23]; // Archer // Machinist _jobsByKey[31] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Physical Damage", 15), new SW("Dexterity", 1), new SW("Determination", .001), new SW("Skill Speed", .001), new SW("Critical Hit", .001), new SW("Direct Hit Rate", .001) }); // Black Mage _jobsByKey[25] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Magic Damage", 15), new SW("Intelligence", 1), new SW("Determination", .001), new SW("Spell Speed", .001), new SW("Critical Hit", .001), new SW("Direct Hit Rate", .001) }); _jobsByKey[7] = _jobsByKey[25]; // Thaumaturge // Summoner (Garuda) _jobsByKey[27] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Magic Damage", 15), new SW("Intelligence", 1), new SW("Determination", .001), new SW("Spell Speed", .001), new SW("Critical Hit", .001), new SW("Direct Hit Rate", .001) }); _jobsByKey[26] = _jobsByKey[27]; // Arcanist // Ninja _jobsByKey[30] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Physical Damage", 15), new SW("Dexterity", 1), new SW("Determination", .001), new SW("Skill Speed", .001), new SW("Critical Hit", .001), new SW("Direct Hit Rate", .001) }); _jobsByKey[29] = _jobsByKey[30]; // Rogue // Samurai _jobsByKey[34] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Physical Damage", 15), new SW("Strength", 1), new SW("Determination", .001), new SW("Skill Speed", .001), new SW("Critical Hit", .001), new SW("Direct Hit Rate", .001) }); // Red Mage _jobsByKey[35] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Magic Damage", 15), new SW("Intelligence", 1), new SW("Determination", .001), new SW("Spell Speed", .001), new SW("Critical Hit", .001), new SW("Direct Hit Rate", .001) }); // Blue Mage _jobsByKey[36] = new JobData(GarlandDatabase.BlueMageLevelCap, new SW[] { new SW("Magic Damage", 15), new SW("Intelligence", 1), new SW("Determination", .001), new SW("Spell Speed", .001), new SW("Critical Hit", .001), new SW("Direct Hit Rate", .001) }); // Warrior (Defiance) _jobsByKey[21] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Physical Damage", 15), new SW("Vitality", 1.3), new SW("Strength", 1), new SW("Defense", 2), new SW("Magic Defense", 2) }); _jobsByKey[3] = _jobsByKey[21]; // Marauder // Paladin (Shield Oath) _jobsByKey[19] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Physical Damage", 15), new SW("Vitality", 1.3), new SW("Strength", 1), new SW("Defense", 2), new SW("Magic Defense", 2), new SW("Block Strength", 2), new SW("Block Rate", 2) }); _jobsByKey[1] = _jobsByKey[19]; // Gladiator // Dark Knight _jobsByKey[32] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Physical Damage", 15), new SW("Vitality", 1.3), new SW("Strength", 1), new SW("Defense", 2), new SW("Magic Defense", 2) }); // White Mage _jobsByKey[24] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Magic Damage", 15), new SW("Mind", 1), new SW("Vitality", .1), new SW("Defense", .5, true), new SW("Magic Defense", .5, true), new SW("Determination", .001), new SW("Spell Speed", .001), new SW("Critical Hit", .001), new SW("Direct Hit Rate", .001) }); _jobsByKey[6] = _jobsByKey[24]; // Conjurer // Astrologian _jobsByKey[33] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Magic Damage", 15), new SW("Mind", 1), new SW("Vitality", .1), new SW("Defense", .5, true), new SW("Magic Defense", .5, true), new SW("Determination", .001), new SW("Spell Speed", .001), new SW("Critical Hit", .001), new SW("Direct Hit Rate", .001) }); // Scholar _jobsByKey[28] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Magic Damage", 15), new SW("Mind", 1), new SW("Vitality", .1), new SW("Defense", .5, true), new SW("Magic Defense", .5, true), new SW("Determination", .001), new SW("Spell Speed", .001), new SW("Critical Hit", .001), new SW("Direct Hit Rate", .001) }); // Miner, Botanist, Fisher _jobsByKey[16] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Gathering", 1), new SW("Perception", 1), new SW("GP", 1) }); _jobsByKey[17] = _jobsByKey[16]; _jobsByKey[18] = _jobsByKey[16]; // Disciples of the Hand _jobsByKey[8] = new JobData(GarlandDatabase.LevelCap, new SW[] { new SW("Craftsmanship", .85), new SW("Control", 1), new SW("CP", 1.1) }); _jobsByKey[9] = _jobsByKey[8]; _jobsByKey[10] = _jobsByKey[8]; _jobsByKey[11] = _jobsByKey[8]; _jobsByKey[12] = _jobsByKey[8]; _jobsByKey[13] = _jobsByKey[8]; _jobsByKey[14] = _jobsByKey[8]; _jobsByKey[15] = _jobsByKey[8]; // Fill materia info. foreach (var weight in _jobsByKey.Values.SelectMany(j => j.Weights)) { weight.BaseParam = sBaseParams.First(bp => bp.Name == weight.Attribute); var applicableMateria = sMateria.FirstOrDefault(m => m.BaseParam == weight.BaseParam); if (applicableMateria != null) { weight.Materia = applicableMateria.Items .Where(i => !string.IsNullOrEmpty(i.Item.Name)) .OrderBy(i => i.Tier) .ToArray(); } } }