diff --git a/Resources/CHANGELOG.txt b/Resources/CHANGELOG.txt index e97b7e0..bc4d5e1 100644 --- a/Resources/CHANGELOG.txt +++ b/Resources/CHANGELOG.txt @@ -17,6 +17,8 @@ - The age generation curve in a race's thing definition now determines the minimum and maximum age allowed for a starting character. This provides better compatibility with the Baby and Children mod. + - Shift-click to skip the confirmation dialog when deleting a pawn. + - Added support for more modded items in the equipment selection view. _____________________________________________________________________________ diff --git a/Resources/Languages/English/Keyed/EdBPrepareCarefully.xml b/Resources/Languages/English/Keyed/EdBPrepareCarefully.xml index 8298ef3..d29a1fd 100644 --- a/Resources/Languages/English/Keyed/EdBPrepareCarefully.xml +++ b/Resources/Languages/English/Keyed/EdBPrepareCarefully.xml @@ -6,6 +6,7 @@ + @@ -278,7 +279,6 @@ This is not the case for faction leaders. If you make this character a faction No injuries, conditions or implants Select a location Select a severity level - {0}: <color={2}>{1}</color> None None of your pawns are capable of the following work types that are considered to be required by the vanilla game. diff --git a/Source/CostCalculator.cs b/Source/CostCalculator.cs index d9692ee..d260f16 100644 --- a/Source/CostCalculator.cs +++ b/Source/CostCalculator.cs @@ -1,334 +1,334 @@ -using RimWorld; -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; -using Verse; - -namespace EdB.PrepareCarefully { - public class ColonistCostDetails { - public string name; - public double total = 0; - public double passionCount = 0; - public double passions = 0; - public double traits = 0; - public double apparel = 0; - public double bionics = 0; - public double animals = 0; - public double marketValue = 0; - public void Clear() { - total = 0; - passions = 0; - traits = 0; - apparel = 0; - bionics = 0; - animals = 0; - marketValue = 0; - } - public void ComputeTotal() { - total = Math.Ceiling(passions + traits + apparel + bionics + marketValue + animals); - } - public void Multiply(double amount) { - passions = Math.Ceiling(passions * amount); - traits = Math.Ceiling(traits * amount); - marketValue = Math.Ceiling(marketValue * amount); - ComputeTotal(); - } - } - - public class CostDetails { - public double total = 0; - public List colonistDetails = new List(); - public double colonists = 0; - public double colonistApparel = 0; - public double colonistBionics = 0; - public double equipment = 0; - public double animals = 0; - public Pawn pawn = null; - public void Clear(int colonistCount) { - total = 0; - equipment = 0; - animals = 0; - colonists = 0; - colonistApparel = 0; - colonistBionics = 0; - int listSize = colonistDetails.Count; - if (colonistCount != listSize) { - if (colonistCount < listSize) { - int diff = listSize - colonistCount; - colonistDetails.RemoveRange(colonistDetails.Count - diff, diff); - } - else { - int diff = colonistCount - listSize; - for (int i = 0; i < diff; i++) { - colonistDetails.Add(new ColonistCostDetails()); - } - } - } - } - public void ComputeTotal() { - equipment = Math.Ceiling(equipment); - animals = Math.Ceiling(animals); - total = equipment + animals; - foreach (var cost in colonistDetails) { - total += cost.total; - colonists += cost.total; - colonistApparel += cost.apparel; - colonistBionics += cost.bionics; - } - total = Math.Ceiling(total); - colonists = Math.Ceiling(colonists); - colonistApparel = Math.Ceiling(colonistApparel); - colonistBionics = Math.Ceiling(colonistBionics); - } - } - - public class CostCalculator { - protected HashSet freeApparel = new HashSet(); - protected HashSet cheapApparel = new HashSet(); - - public CostCalculator() { - cheapApparel.Add("Apparel_Pants"); - cheapApparel.Add("Apparel_BasicShirt"); - cheapApparel.Add("Apparel_Jacket"); - } - - public void Calculate(CostDetails cost, List pawns, List equipment, List animals) { - cost.Clear(pawns.Count); - - int i = 0; - foreach (var pawn in pawns) { - if (pawn.Type == CustomPawnType.Colonist) { - CalculatePawnCost(cost.colonistDetails[i++], pawn); - } - } - foreach (var e in equipment) { - cost.equipment += CalculateEquipmentCost(e); - } - cost.ComputeTotal(); - } - - public void CalculatePawnCost(ColonistCostDetails cost, CustomPawn pawn) { - cost.Clear(); - cost.name = pawn.NickName; - - // Start with the market value plus a bit of a mark-up. - cost.marketValue = pawn.Pawn.MarketValue; - cost.marketValue += 300; - - // Calculate passion cost. Each passion above 8 makes all passions - // cost more. Minor passion counts as one passion. Major passion - // counts as 3. - double skillCount = pawn.currentPassions.Keys.Count(); - double passionLevelCount = 0; - double passionLevelCost = 20; - double passionateSkillCount = 0; - foreach (SkillDef def in pawn.currentPassions.Keys) { - Passion passion = pawn.currentPassions[def]; - int level = pawn.GetSkillLevel(def); - - if (passion == Passion.Major) { - passionLevelCount += 3.0; - passionateSkillCount += 1.0; - } - else if (passion == Passion.Minor) { - passionLevelCount += 1.0; - passionateSkillCount += 1.0; - } - } - double levelCost = passionLevelCost; - if (passionLevelCount > 8) { - double penalty = passionLevelCount - 8; - levelCost += penalty * 0.4; - } - cost.marketValue += levelCost * passionLevelCount; - - // Calculate trait cost. - if (pawn.TraitCount > Constraints.MaxVanillaTraits) { - int extraTraitCount = pawn.TraitCount - Constraints.MaxVanillaTraits; - double extraTraitCost = 100; - for (int i=0; i< extraTraitCount; i++) { - cost.marketValue += extraTraitCost; - extraTraitCost = Math.Ceiling(extraTraitCost * 2.5); - } - } - - // Calculate cost of worn apparel. - foreach (var layer in PrepareCarefully.Instance.Providers.PawnLayers.GetLayersForPawn(pawn)) { - if (layer.Apparel) { - var def = pawn.GetAcceptedApparel(layer); - if (def == null) { - continue; - } - EquipmentKey key = new EquipmentKey(); - key.ThingDef = def; - key.StuffDef = pawn.GetSelectedStuff(layer); - EquipmentRecord record = PrepareCarefully.Instance.EquipmentDatabase.Find(key); - if (record == null) { - continue; - } - EquipmentSelection selection = new EquipmentSelection(record, 1); - double c = CalculateEquipmentCost(selection); - if (def != null) { - // TODO: Discounted materials should be based on the faction, not hard-coded. - // TODO: Should we continue with the discounting? - if (key.StuffDef != null) { - if (key.StuffDef.defName == "Synthread") { - if (freeApparel.Contains(key.ThingDef.defName)) { - c = 0; - } - else if (cheapApparel.Contains(key.ThingDef.defName)) { - c = c * 0.15d; - } - } - } - } - cost.apparel += c; - } - } - - // Calculate cost for any materials needed for implants. - OptionsHealth healthOptions = PrepareCarefully.Instance.Providers.Health.GetOptions(pawn); - foreach (Implant option in pawn.Implants) { - - // Check if there are any ancestor parts that override the selection. - UniqueBodyPart uniquePart = healthOptions.FindBodyPartsForRecord(option.BodyPartRecord); - if (uniquePart == null) { - Log.Warning("Prepare Carefully could not find body part record when computing the cost of an implant: " + option.BodyPartRecord.def.defName); - continue; - } - if (pawn.AtLeastOneImplantedPart(uniquePart.Ancestors.Select((UniqueBodyPart p) => { return p.Record; }))) { - continue; - } - - // Figure out the cost of the part replacement based on its recipe's ingredients. - if (option.recipe != null) { - RecipeDef def = option.recipe; - foreach (IngredientCount amount in def.ingredients) { - int count = 0; - double totalCost = 0; - bool skip = false; - foreach (ThingDef ingredientDef in amount.filter.AllowedThingDefs) { - if (ingredientDef.IsMedicine) { - skip = true; - break; - } - count++; - EquipmentRecord entry = PrepareCarefully.Instance.EquipmentDatabase.LookupEquipmentRecord(new EquipmentKey(ingredientDef, null)); - if (entry != null) { - totalCost += entry.cost * (double)amount.GetBaseCount(); - } - } - if (skip || count == 0) { - continue; - } - cost.bionics += (int)(totalCost / (double)count); - } - } - } - - cost.apparel = Math.Ceiling(cost.apparel); - cost.bionics = Math.Ceiling(cost.bionics); - - // Use a multiplier to balance pawn cost vs. equipment cost. - // Disabled for now. - cost.Multiply(1.0); - - cost.ComputeTotal(); - } - - public double CalculateEquipmentCost(EquipmentSelection equipment) { - EquipmentRecord entry = PrepareCarefully.Instance.EquipmentDatabase.LookupEquipmentRecord(equipment.Key); - if (entry != null) { - return (double)equipment.Count * entry.cost; - } - else { - return 0; - } - } - - /* - public double CalculateAnimalCost(SelectedAnimal animal) { - AnimalRecord record = PrepareCarefully.Instance.AnimalDatabase.FindAnimal(animal.Key); - if (record != null) { - return (double)animal.Count * record.Cost; - } - else { - return 0; - } - } - */ - - public double GetBaseThingCost(ThingDef def, ThingDef stuffDef) { - if (def == null) { - Log.Warning("Prepare Carefully is trying to calculate the cost of a null ThingDef"); - return 0; - } - if (def.BaseMarketValue > 0) { - if (stuffDef == null) { - return def.BaseMarketValue; - } - else { - // TODO: - // Should look at ThingMaker.MakeThing() to decide which validations we need to do - // before calling that method. That method doesn't do null checks everywhere, so we - // may need to do those validations ourselves to avoid null pointer exceptions. - // Should re-evaluate for each new release and then update the todo comment with the next - // alpha version. - if (def.thingClass == null) { - Log.Warning("Prepare Carefully trying to calculate the cost of a ThingDef with null thingClass: " + def.defName); - return 0; - } - if (def.MadeFromStuff && stuffDef == null) { - Log.Warning("Prepare Carefully trying to calculate the cost of a \"made-from-stuff\" ThingDef without specifying any stuff: " + def.defName); - return 0; - } - - try { - // TODO: Creating an instance of a thing may not be the best way to calculate - // its market value. It may be considered a relatively expensive operation, - // especially when a lot of mods are enabled. There may be a lower-level set of - // methods in the vanilla codebase that could be called. Should investigate. - Thing thing = ThingMaker.MakeThing(def, stuffDef); - if (thing == null) { - Log.Warning("Prepare Carefully failed when calling MakeThing(" + def.defName + ", ...) to calculate a ThingDef's market value"); - return 0; - } - return thing.MarketValue; - } - catch (Exception e) { - Log.Warning("Prepare Carefully failed to calculate the cost of a ThingDef (" + def.defName + "): "); - Log.Warning(e.ToString()); - return 0; - } - } - } - else { - return 0; - } - } - - public double CalculateStackCost(ThingDef def, ThingDef stuffDef, double baseCost) { - double cost = baseCost; - - if (def.MadeFromStuff) { - if (def.IsApparel) { - cost = cost * 1; - } - else { - cost = cost * 0.5; - } - } - - if (def.IsRangedWeapon) { - cost = cost * 2; - } - - //cost = cost * 1.25; - cost = Math.Round(cost, 1); - - return cost; - } - } -} - +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using Verse; + +namespace EdB.PrepareCarefully { + public class ColonistCostDetails { + public string name; + public double total = 0; + public double passionCount = 0; + public double passions = 0; + public double traits = 0; + public double apparel = 0; + public double bionics = 0; + public double animals = 0; + public double marketValue = 0; + public void Clear() { + total = 0; + passions = 0; + traits = 0; + apparel = 0; + bionics = 0; + animals = 0; + marketValue = 0; + } + public void ComputeTotal() { + total = Math.Ceiling(passions + traits + apparel + bionics + marketValue + animals); + } + public void Multiply(double amount) { + passions = Math.Ceiling(passions * amount); + traits = Math.Ceiling(traits * amount); + marketValue = Math.Ceiling(marketValue * amount); + ComputeTotal(); + } + } + + public class CostDetails { + public double total = 0; + public List colonistDetails = new List(); + public double colonists = 0; + public double colonistApparel = 0; + public double colonistBionics = 0; + public double equipment = 0; + public double animals = 0; + public Pawn pawn = null; + public void Clear(int colonistCount) { + total = 0; + equipment = 0; + animals = 0; + colonists = 0; + colonistApparel = 0; + colonistBionics = 0; + int listSize = colonistDetails.Count; + if (colonistCount != listSize) { + if (colonistCount < listSize) { + int diff = listSize - colonistCount; + colonistDetails.RemoveRange(colonistDetails.Count - diff, diff); + } + else { + int diff = colonistCount - listSize; + for (int i = 0; i < diff; i++) { + colonistDetails.Add(new ColonistCostDetails()); + } + } + } + } + public void ComputeTotal() { + equipment = Math.Ceiling(equipment); + animals = Math.Ceiling(animals); + total = equipment + animals; + foreach (var cost in colonistDetails) { + total += cost.total; + colonists += cost.total; + colonistApparel += cost.apparel; + colonistBionics += cost.bionics; + } + total = Math.Ceiling(total); + colonists = Math.Ceiling(colonists); + colonistApparel = Math.Ceiling(colonistApparel); + colonistBionics = Math.Ceiling(colonistBionics); + } + } + + public class CostCalculator { + protected HashSet freeApparel = new HashSet(); + protected HashSet cheapApparel = new HashSet(); + + public CostCalculator() { + cheapApparel.Add("Apparel_Pants"); + cheapApparel.Add("Apparel_BasicShirt"); + cheapApparel.Add("Apparel_Jacket"); + } + + public void Calculate(CostDetails cost, List pawns, List equipment, List animals) { + cost.Clear(pawns.Where(pawn => pawn.Type == CustomPawnType.Colonist).Count()); + + int i = 0; + foreach (var pawn in pawns) { + if (pawn.Type == CustomPawnType.Colonist) { + CalculatePawnCost(cost.colonistDetails[i++], pawn); + } + } + foreach (var e in equipment) { + cost.equipment += CalculateEquipmentCost(e); + } + cost.ComputeTotal(); + } + + public void CalculatePawnCost(ColonistCostDetails cost, CustomPawn pawn) { + cost.Clear(); + cost.name = pawn.NickName; + + // Start with the market value plus a bit of a mark-up. + cost.marketValue = pawn.Pawn.MarketValue; + cost.marketValue += 300; + + // Calculate passion cost. Each passion above 8 makes all passions + // cost more. Minor passion counts as one passion. Major passion + // counts as 3. + double skillCount = pawn.currentPassions.Keys.Count(); + double passionLevelCount = 0; + double passionLevelCost = 20; + double passionateSkillCount = 0; + foreach (SkillDef def in pawn.currentPassions.Keys) { + Passion passion = pawn.currentPassions[def]; + int level = pawn.GetSkillLevel(def); + + if (passion == Passion.Major) { + passionLevelCount += 3.0; + passionateSkillCount += 1.0; + } + else if (passion == Passion.Minor) { + passionLevelCount += 1.0; + passionateSkillCount += 1.0; + } + } + double levelCost = passionLevelCost; + if (passionLevelCount > 8) { + double penalty = passionLevelCount - 8; + levelCost += penalty * 0.4; + } + cost.marketValue += levelCost * passionLevelCount; + + // Calculate trait cost. + if (pawn.TraitCount > Constraints.MaxVanillaTraits) { + int extraTraitCount = pawn.TraitCount - Constraints.MaxVanillaTraits; + double extraTraitCost = 100; + for (int i=0; i< extraTraitCount; i++) { + cost.marketValue += extraTraitCost; + extraTraitCost = Math.Ceiling(extraTraitCost * 2.5); + } + } + + // Calculate cost of worn apparel. + foreach (var layer in PrepareCarefully.Instance.Providers.PawnLayers.GetLayersForPawn(pawn)) { + if (layer.Apparel) { + var def = pawn.GetAcceptedApparel(layer); + if (def == null) { + continue; + } + EquipmentKey key = new EquipmentKey(); + key.ThingDef = def; + key.StuffDef = pawn.GetSelectedStuff(layer); + EquipmentRecord record = PrepareCarefully.Instance.EquipmentDatabase.Find(key); + if (record == null) { + continue; + } + EquipmentSelection selection = new EquipmentSelection(record, 1); + double c = CalculateEquipmentCost(selection); + if (def != null) { + // TODO: Discounted materials should be based on the faction, not hard-coded. + // TODO: Should we continue with the discounting? + if (key.StuffDef != null) { + if (key.StuffDef.defName == "Synthread") { + if (freeApparel.Contains(key.ThingDef.defName)) { + c = 0; + } + else if (cheapApparel.Contains(key.ThingDef.defName)) { + c = c * 0.15d; + } + } + } + } + cost.apparel += c; + } + } + + // Calculate cost for any materials needed for implants. + OptionsHealth healthOptions = PrepareCarefully.Instance.Providers.Health.GetOptions(pawn); + foreach (Implant option in pawn.Implants) { + + // Check if there are any ancestor parts that override the selection. + UniqueBodyPart uniquePart = healthOptions.FindBodyPartsForRecord(option.BodyPartRecord); + if (uniquePart == null) { + Log.Warning("Prepare Carefully could not find body part record when computing the cost of an implant: " + option.BodyPartRecord.def.defName); + continue; + } + if (pawn.AtLeastOneImplantedPart(uniquePart.Ancestors.Select((UniqueBodyPart p) => { return p.Record; }))) { + continue; + } + + // Figure out the cost of the part replacement based on its recipe's ingredients. + if (option.recipe != null) { + RecipeDef def = option.recipe; + foreach (IngredientCount amount in def.ingredients) { + int count = 0; + double totalCost = 0; + bool skip = false; + foreach (ThingDef ingredientDef in amount.filter.AllowedThingDefs) { + if (ingredientDef.IsMedicine) { + skip = true; + break; + } + count++; + EquipmentRecord entry = PrepareCarefully.Instance.EquipmentDatabase.LookupEquipmentRecord(new EquipmentKey(ingredientDef, null)); + if (entry != null) { + totalCost += entry.cost * (double)amount.GetBaseCount(); + } + } + if (skip || count == 0) { + continue; + } + cost.bionics += (int)(totalCost / (double)count); + } + } + } + + cost.apparel = Math.Ceiling(cost.apparel); + cost.bionics = Math.Ceiling(cost.bionics); + + // Use a multiplier to balance pawn cost vs. equipment cost. + // Disabled for now. + cost.Multiply(1.0); + + cost.ComputeTotal(); + } + + public double CalculateEquipmentCost(EquipmentSelection equipment) { + EquipmentRecord entry = PrepareCarefully.Instance.EquipmentDatabase.LookupEquipmentRecord(equipment.Key); + if (entry != null) { + return (double)equipment.Count * entry.cost; + } + else { + return 0; + } + } + + /* + public double CalculateAnimalCost(SelectedAnimal animal) { + AnimalRecord record = PrepareCarefully.Instance.AnimalDatabase.FindAnimal(animal.Key); + if (record != null) { + return (double)animal.Count * record.Cost; + } + else { + return 0; + } + } + */ + + public double GetBaseThingCost(ThingDef def, ThingDef stuffDef) { + if (def == null) { + Log.Warning("Prepare Carefully is trying to calculate the cost of a null ThingDef"); + return 0; + } + if (def.BaseMarketValue > 0) { + if (stuffDef == null) { + return def.BaseMarketValue; + } + else { + // TODO: + // Should look at ThingMaker.MakeThing() to decide which validations we need to do + // before calling that method. That method doesn't do null checks everywhere, so we + // may need to do those validations ourselves to avoid null pointer exceptions. + // Should re-evaluate for each new release and then update the todo comment with the next + // alpha version. + if (def.thingClass == null) { + Log.Warning("Prepare Carefully trying to calculate the cost of a ThingDef with null thingClass: " + def.defName); + return 0; + } + if (def.MadeFromStuff && stuffDef == null) { + Log.Warning("Prepare Carefully trying to calculate the cost of a \"made-from-stuff\" ThingDef without specifying any stuff: " + def.defName); + return 0; + } + + try { + // TODO: Creating an instance of a thing may not be the best way to calculate + // its market value. It may be considered a relatively expensive operation, + // especially when a lot of mods are enabled. There may be a lower-level set of + // methods in the vanilla codebase that could be called. Should investigate. + Thing thing = ThingMaker.MakeThing(def, stuffDef); + if (thing == null) { + Log.Warning("Prepare Carefully failed when calling MakeThing(" + def.defName + ", ...) to calculate a ThingDef's market value"); + return 0; + } + return thing.MarketValue; + } + catch (Exception e) { + Log.Warning("Prepare Carefully failed to calculate the cost of a ThingDef (" + def.defName + "): "); + Log.Warning(e.ToString()); + return 0; + } + } + } + else { + return 0; + } + } + + public double CalculateStackCost(ThingDef def, ThingDef stuffDef, double baseCost) { + double cost = baseCost; + + if (def.MadeFromStuff) { + if (def.IsApparel) { + cost = cost * 1; + } + else { + cost = cost * 0.5; + } + } + + if (def.IsRangedWeapon) { + cost = cost * 2; + } + + //cost = cost * 1.25; + cost = Math.Round(cost, 1); + + return cost; + } + } +} + diff --git a/Source/EquipmentDatabase.cs b/Source/EquipmentDatabase.cs index 13d045f..4381569 100644 --- a/Source/EquipmentDatabase.cs +++ b/Source/EquipmentDatabase.cs @@ -29,7 +29,6 @@ public class EquipmentDatabase { protected ThingCategoryDef thingCategorySweetMeals = null; protected ThingCategoryDef thingCategoryMeatRaw = null; - protected ThingCategoryDef thingCategoryBodyPartsArtificial = null; public EquipmentDatabase() { types.Add(TypeResources); @@ -42,7 +41,6 @@ public EquipmentDatabase() { thingCategorySweetMeals = DefDatabase.GetNamedSilentFail("SweetMeals"); thingCategoryMeatRaw = DefDatabase.GetNamedSilentFail("MeatRaw"); - thingCategoryBodyPartsArtificial = DefDatabase.GetNamedSilentFail("BodyPartsArtificial"); } public LoadingState LoadingProgress { get; protected set; } = new LoadingState(); @@ -115,7 +113,7 @@ protected void NextPhase() { protected void CountDefs() { for (int i = 0; i < LoadingProgress.defsToCountPerFrame; i++) { if (!LoadingProgress.enumerator.MoveNext()) { - Log.Message("Prepare Carefully finished counting " + LoadingProgress.defCount + " thing definition(s)"); + //Log.Message("Prepare Carefully finished counting " + LoadingProgress.defCount + " thing definition(s)"); NextPhase(); return; } @@ -223,6 +221,20 @@ public void BuildEquipmentLists() { } */ + private bool FoodTypeIsClassifiedAsFood(ThingDef def) { + int foodTypes = (int)def.ingestible.foodType; + if ((foodTypes & (int)FoodTypeFlags.Liquor) > 0) { + return true; + } + if ((foodTypes & (int)FoodTypeFlags.Meal) > 0) { + return true; + } + if ((foodTypes & (int)FoodTypeFlags.VegetableOrFruit) > 0) { + return true; + } + return false; + } + public EquipmentType ClassifyThingDef(ThingDef def) { if (def.mote != null) { return TypeDiscard; @@ -245,54 +257,51 @@ public EquipmentType ClassifyThingDef(ThingDef def) { if (def.weaponTags != null && def.weaponTags.Count > 0 && def.IsWeapon) { return TypeWeapons; } + if (BelongsToCategoryContaining(def, "Weapon")) { + return TypeWeapons; + } if (def.IsApparel && !def.destroyOnDrop) { return TypeApparel; } - if (def.defName.StartsWith("MechSerum")) { - return TypeMedical; + if (BelongsToCategory(def, "Foods")) { + return TypeFood; } - if (def.CountAsResource) { - if (def.IsShell) { - return TypeWeapons; - } - if (def.IsDrug || (def.statBases != null && def.IsMedicine)) { - if (def.ingestible != null) { - if (def.thingCategories != null) { - if (thingCategorySweetMeals != null && def.thingCategories.Contains(thingCategorySweetMeals)) { - return TypeFood; - } - } - int foodTypes = (int) def.ingestible.foodType; - bool isFood = ((foodTypes & (int)FoodTypeFlags.Liquor) > 0) | ((foodTypes & (int)FoodTypeFlags.Meal) > 0); - if (isFood) { - return TypeFood; - } - } - return TypeMedical; - } + // Ingestibles + if (def.IsDrug || (def.statBases != null && def.IsMedicine)) { if (def.ingestible != null) { - if (thingCategoryMeatRaw != null && def.thingCategories != null && def.thingCategories.Contains(thingCategoryMeatRaw)) { + if (BelongsToCategory(def, thingCategorySweetMeals)) { return TypeFood; } - if (def.ingestible.drugCategory == DrugCategory.Medical) { - return TypeMedical; - } - if (def.ingestible.preferability == FoodPreferability.DesperateOnly || def.ingestible.preferability == FoodPreferability.NeverForNutrition) { - return TypeResources; + if (FoodTypeIsClassifiedAsFood(def)) { + return TypeFood; } + } + return TypeMedical; + } + if (def.ingestible != null) { + if (BelongsToCategory(def, thingCategoryMeatRaw)) { return TypeFood; } + if (def.ingestible.drugCategory == DrugCategory.Medical) { + return TypeMedical; + } + if (def.ingestible.preferability == FoodPreferability.DesperateOnly) { + return TypeResources; + } + return TypeFood; + } + + if (def.CountAsResource) { + if (def.IsShell) { + return TypeWeapons; + } return TypeResources; } - if (thingCategoryBodyPartsArtificial != null && def.thingCategories != null && def.thingCategories.Contains(thingCategoryBodyPartsArtificial)) { - return TypeMedical; - } - if (def.building != null && def.Minifiable) { return TypeBuildings; } @@ -301,9 +310,75 @@ public EquipmentType ClassifyThingDef(ThingDef def) { return TypeAnimals; } + if (def.category == ThingCategory.Item) { + if (def.defName.StartsWith("MechSerum")) { + return TypeMedical; + } + // Body parts should be medical + if (BelongsToCategoryStartingWith(def, "BodyParts")) { + return TypeMedical; + } + // EPOE parts should be medical + if (BelongsToCategoryContaining(def, "Prostheses")) { + return TypeMedical; + } + if (BelongsToCategory(def, "GlitterworldParts")) { + return TypeMedical; + } + if (BelongsToCategoryEndingWith(def, "Organs")) { + return TypeMedical; + } + return TypeResources; + } + return null; } + public bool BelongsToCategory(ThingDef def, ThingCategoryDef categoryDef) { + if (categoryDef == null || def.thingCategories == null) { + return false; + } + return def.thingCategories.FirstOrDefault(d => { + return categoryDef == d; + }) != null; + } + + public bool BelongsToCategoryStartingWith(ThingDef def, string categoryNamePrefix) { + if (categoryNamePrefix.NullOrEmpty() || def.thingCategories == null) { + return false; + } + return def.thingCategories.FirstOrDefault(d => { + return d.defName.StartsWith(categoryNamePrefix); + }) != null; + } + + public bool BelongsToCategoryEndingWith(ThingDef def, string categoryNameSuffix) { + if (categoryNameSuffix.NullOrEmpty() || def.thingCategories == null) { + return false; + } + return def.thingCategories.FirstOrDefault(d => { + return d.defName.EndsWith(categoryNameSuffix); + }) != null; + } + + public bool BelongsToCategoryContaining(ThingDef def, string categoryNameSubstring) { + if (categoryNameSubstring.NullOrEmpty() || def.thingCategories == null) { + return false; + } + return def.thingCategories.FirstOrDefault(d => { + return d.defName.Contains(categoryNameSubstring); + }) != null; + } + + public bool BelongsToCategory(ThingDef def, string categoryName) { + if (categoryName.NullOrEmpty() || def.thingCategories == null) { + return false; + } + return def.thingCategories.FirstOrDefault(d => { + return categoryName == d.defName; + }) != null; + } + public IEnumerable AllEquipmentOfType(EquipmentType type) { return entries.Values.Where((EquipmentRecord e) => { return e.type == type; diff --git a/Source/LabelTrimmer.cs b/Source/LabelTrimmer.cs index 5862b02..fbc835a 100644 --- a/Source/LabelTrimmer.cs +++ b/Source/LabelTrimmer.cs @@ -5,6 +5,46 @@ using Verse; namespace EdB.PrepareCarefully { public class LabelTrimmer { + + public interface LabelProvider { + string Trim(); + string Current { + get; + } + string CurrentWithSuffix(string suffix); + } + + public struct DefaultLabelProvider : LabelProvider { + private string label; + private bool trimmed; + public DefaultLabelProvider(string label) { + this.label = label; + this.trimmed = false; + } + public string Trim() { + int length = label.Length; + if (length == 0) { + return ""; + } + label = label.Substring(0, length - 1).TrimEnd(); + trimmed = true; + return label; + } + public string Current { + get { + return label; + } + } + public string CurrentWithSuffix(string suffix) { + if (trimmed) { + return label + suffix; + } + else { + return label; + } + } + } + private Dictionary cache = new Dictionary(); private string suffix = "..."; private float width; @@ -21,30 +61,43 @@ public float Width { return width; } set { + if (width != value) { + cache.Clear(); + } width = value; } } public string TrimLabelIfNeeded(string name) { - if (Text.CalcSize(name).x <= width) { - return name; + return TrimLabelIfNeeded(new DefaultLabelProvider(name)); + } + public string TrimLabelIfNeeded(LabelProvider provider) { + string label = provider.Current; + if (Text.CalcSize(label).x <= width) { + return label; } - string shorter; - if (cache.TryGetValue(name, out shorter)) { - return shorter + suffix; + if (cache.TryGetValue(label, out string shorter)) { + return shorter; } - shorter = name; + return TrimLabel(provider); + } + public string TrimLabel(LabelProvider provider) { + string original = provider.Current; + string shorter = original; while (!shorter.NullOrEmpty()) { - shorter = shorter.Substring(0, shorter.Length - 1); - if (shorter.EndsWith(" ")) { - continue; + int length = shorter.Length; + shorter = provider.Trim(); + // The trimmer should always return a shorter length. If it doesn't we bail--it's a bad implementation. + if (shorter.Length >= length) { + break; } - Vector2 size = Text.CalcSize(shorter + suffix); + string withSuffix = provider.CurrentWithSuffix(suffix); + Vector2 size = Text.CalcSize(withSuffix); if (size.x <= width) { - cache.Add(name, shorter); - return shorter + suffix; + cache.Add(original, withSuffix); + return shorter; } } - return name; + return original; } } } diff --git a/Source/PanelHealth.cs b/Source/PanelHealth.cs index faec852..88a6dcb 100644 --- a/Source/PanelHealth.cs +++ b/Source/PanelHealth.cs @@ -1,4 +1,4 @@ -using RimWorld; +using RimWorld; using System; using System.Collections.Generic; using System.Linq; @@ -77,7 +77,7 @@ public override void Resize(Rect rect) { RectButtonDelete = new Rect(RectField.xMax - buttonPadding - buttonSize.x, RectField.y + RectField.height * 0.5f - buttonSize.y * 0.5f, buttonSize.x, buttonSize.y); - labelTrimmer.Width = RectField.width - (RectField.xMax - RectButtonDelete.xMin) * 2; + labelTrimmer.Width = RectField.width - (RectField.xMax - RectButtonDelete.xMin) * 2 - 10; RectScrollFrame = new Rect(panelPadding, BodyRect.y, contentSize.x, contentSize.y); RectScrollView = new Rect(0, 0, RectScrollFrame.width, RectScrollFrame.height); @@ -135,6 +135,8 @@ protected override void DrawPanelContent(State state) { base.DrawPanelContent(state); CustomPawn customPawn = state.CurrentPawn; + bool wasScrolling = scrollView.ScrollbarsVisible; + float cursor = 0; GUI.BeginGroup(RectScrollFrame); @@ -159,6 +161,15 @@ protected override void DrawPanelContent(State state) { } partRemovalList.Clear(); } + + // If the addition or removal of an item changed whether or not the scrollbars are visible, then we + // need to resize the label trimmer. + if (wasScrolling && !scrollView.ScrollbarsVisible) { + labelTrimmer.Width += 16; + } + else if (!wasScrolling && scrollView.ScrollbarsVisible) { + labelTrimmer.Width -= 16; + } } public void DrawAddButton() { @@ -515,6 +526,68 @@ public float DrawCustomBodyParts(float cursor) { return cursor; } + // Custom label provider for health diffs that properly maintains the rich text/html tags while trimming. + public struct HealthPanelLabelProvider : LabelTrimmer.LabelProvider { + private static readonly int PART_NAME = 0; + private static readonly int CHANGE_NAME = 1; + private int elementToTrim; + private string partName; + private string changeName; + private readonly string color; + public HealthPanelLabelProvider(string partName, string changeName, Color color) { + this.partName = partName; + this.changeName = changeName; + this.color = ColorUtility.ToHtmlStringRGBA(color); + this.elementToTrim = CHANGE_NAME; + } + public string Current { + get { + if (elementToTrim == CHANGE_NAME) { + return partName + ": " + changeName + ""; + } + else { + return partName; + } + } + } + public string CurrentWithSuffix(string suffix) { + if (elementToTrim == CHANGE_NAME) { + return partName + ": " + changeName + suffix + ""; + } + else { + return partName + suffix; + } + } + public string Trim() { + if (elementToTrim == CHANGE_NAME) { + if (!TrimChangeName()) { + elementToTrim = PART_NAME; + } + } + else { + TrimPartName(); + } + return Current; + } + private bool TrimString(ref string value) { + int length = value.Length; + if (length == 0) { + return false; + } + value = value.Substring(0, length - 1).TrimEnd(); + if (length == 0) { + return false; + } + return true; + } + private bool TrimChangeName() { + return TrimString(ref changeName); + } + private bool TrimPartName() { + return TrimString(ref partName); + } + } + public float DrawCustomBodyPart(float cursor, CustomBodyPart customPart, Field field) { bool willScroll = scrollView.ScrollbarsVisible; Rect entryRect = RectItem; @@ -530,16 +603,14 @@ public float DrawCustomBodyPart(float cursor, CustomBodyPart customPart, Field f Rect fieldRect = RectField; fieldRect.width = fieldRect.width - (willScroll ? 16 : 0); field.Rect = fieldRect; - string label; if (customPart.BodyPartRecord != null) { - label = "EdB.PC.Panel.Health.PartWithInjury".Translate(customPart.PartName, customPart.ChangeName, "#" + ColorUtility.ToHtmlStringRGBA(customPart.LabelColor)); + field.Label = labelTrimmer.TrimLabelIfNeeded(new HealthPanelLabelProvider(customPart.PartName, customPart.ChangeName, customPart.LabelColor)); field.Color = Color.white; } else { - label = customPart.ChangeName; + field.Label = labelTrimmer.TrimLabelIfNeeded(customPart.ChangeName); field.Color = customPart.LabelColor; } - field.Label = labelTrimmer.TrimLabelIfNeeded(label); if (customPart.HasTooltip) { field.Tip = customPart.Tooltip; diff --git a/Source/PanelPawnList.cs b/Source/PanelPawnList.cs index 34693cd..6609044 100644 --- a/Source/PanelPawnList.cs +++ b/Source/PanelPawnList.cs @@ -141,8 +141,9 @@ protected override void DrawPanelContent(State state) { base.DrawPanelContent(state); CustomPawn currentPawn = state.CurrentPawn; - CustomPawn newPawnSelection = null; - CustomPawn swapSelection = null; + CustomPawn pawnToSelect = null; + CustomPawn pawnToSwap = null; + CustomPawn pawnToDelete = null; List pawns = GetPawnListFromState(state); int colonistCount = pawns.Count(); @@ -181,8 +182,8 @@ protected override void DrawPanelContent(State state) { foreach (var pawn in pawns) { bool selected = pawn == currentPawn; Rect rect = RectEntry; - rect.y = rect.y + cursor; - rect.width = rect.width - (scrollView.ScrollbarsVisible ? 16 : 0); + rect.y += cursor; + rect.width -= (scrollView.ScrollbarsVisible ? 16 : 0); GUI.color = Style.ColorPanelBackground; GUI.DrawTexture(rect, BaseContent.WhiteTex); @@ -204,25 +205,32 @@ protected override void DrawPanelContent(State state) { // Replacing it with a mousedown event check fixes it for some reason. //if (GUI.Button(deleteRect, string.Empty, Widgets.EmptyStyle)) { if (Event.current.type == EventType.MouseDown && deleteRect.Contains(Event.current.mousePosition)) { - CustomPawn localPawn = pawn; - Find.WindowStack.Add( - new Dialog_Confirm("EdB.PC.Panel.PawnList.Delete.Confirm".Translate(), - delegate { - PawnDeleted(localPawn); - }, - true, null, true) - ); + // Shift-click skips the confirmation dialog + if (Event.current.shift) { + // Delete after we've iterated and drawn everything + pawnToDelete = pawn; + } + else { + CustomPawn localPawn = pawn; + Find.WindowStack.Add( + new Dialog_Confirm("EdB.PC.Panel.PawnList.Delete.Confirm".Translate(), + delegate { + PawnDeleted(localPawn); + }, + true, null, true) + ); + } } GUI.color = Color.white; } if (rect.Contains(Event.current.mousePosition)) { Rect swapRect = RectButtonSwap.OffsetBy(rect.position); - swapRect.x = swapRect.x - (scrollView.ScrollbarsVisible ? 16 : 0); + swapRect.x -= (scrollView.ScrollbarsVisible ? 16 : 0); if (CanDeleteLastPawn || colonistCount > 1) { Style.SetGUIColorForButton(swapRect); GUI.DrawTexture(swapRect, pawn.Type == CustomPawnType.Colonist ? Textures.TextureButtonWorldPawn : Textures.TextureButtonColonyPawn); if (Event.current.type == EventType.MouseDown && swapRect.Contains(Event.current.mousePosition)) { - swapSelection = pawn; + pawnToSwap = pawn; } GUI.color = Color.white; } @@ -267,8 +275,8 @@ protected override void DrawPanelContent(State state) { Widgets.Label(professionRect, descriptionTrimmer.TrimLabelIfNeeded(description)); } - if (pawn != state.CurrentPawn && Event.current.type == EventType.MouseDown && rect.Contains(Event.current.mousePosition) && swapSelection == null) { - newPawnSelection = pawn; + if (pawn != state.CurrentPawn && Event.current.type == EventType.MouseDown && rect.Contains(Event.current.mousePosition) && pawnToSwap == null) { + pawnToSelect = pawn; } cursor += rect.height + SizeEntrySpacing; @@ -292,12 +300,16 @@ protected override void DrawPanelContent(State state) { Text.Font = GameFont.Small; Text.Anchor = TextAnchor.UpperLeft; - if (swapSelection != null) { - PawnSwapped(swapSelection); + if (pawnToDelete != null) { + PawnDeleted(pawnToDelete); } - else if (newPawnSelection != null) { - PawnSelected(newPawnSelection); + else if (pawnToSwap != null) { + PawnSwapped(pawnToSwap); } + else if (pawnToSelect != null) { + PawnSelected(pawnToSelect); + } + } protected abstract bool IsMaximized(State state);