SharedResearchSystem.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. using System.Linq;
  2. using Content.Shared.Lathe;
  3. using Content.Shared.Research.Components;
  4. using Content.Shared.Research.Prototypes;
  5. using JetBrains.Annotations;
  6. using Robust.Shared.Prototypes;
  7. using Robust.Shared.Random;
  8. using Robust.Shared.Utility;
  9. namespace Content.Shared.Research.Systems;
  10. public abstract class SharedResearchSystem : EntitySystem
  11. {
  12. [Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
  13. [Dependency] private readonly IRobustRandom _random = default!;
  14. [Dependency] private readonly SharedLatheSystem _lathe = default!;
  15. public override void Initialize()
  16. {
  17. base.Initialize();
  18. SubscribeLocalEvent<TechnologyDatabaseComponent, MapInitEvent>(OnMapInit);
  19. }
  20. private void OnMapInit(EntityUid uid, TechnologyDatabaseComponent component, MapInitEvent args)
  21. {
  22. UpdateTechnologyCards(uid, component);
  23. }
  24. public void UpdateTechnologyCards(EntityUid uid, TechnologyDatabaseComponent? component = null)
  25. {
  26. if (!Resolve(uid, ref component))
  27. return;
  28. var availableTechnology = GetAvailableTechnologies(uid, component);
  29. _random.Shuffle(availableTechnology);
  30. component.CurrentTechnologyCards.Clear();
  31. foreach (var discipline in component.SupportedDisciplines)
  32. {
  33. var selected = availableTechnology.FirstOrDefault(p => p.Discipline == discipline);
  34. if (selected == null)
  35. continue;
  36. component.CurrentTechnologyCards.Add(selected.ID);
  37. }
  38. Dirty(uid, component);
  39. }
  40. public List<TechnologyPrototype> GetAvailableTechnologies(EntityUid uid, TechnologyDatabaseComponent? component = null)
  41. {
  42. if (!Resolve(uid, ref component, false))
  43. return new List<TechnologyPrototype>();
  44. var availableTechnologies = new List<TechnologyPrototype>();
  45. var disciplineTiers = GetDisciplineTiers(component);
  46. foreach (var tech in PrototypeManager.EnumeratePrototypes<TechnologyPrototype>())
  47. {
  48. if (IsTechnologyAvailable(component, tech, disciplineTiers))
  49. availableTechnologies.Add(tech);
  50. }
  51. return availableTechnologies;
  52. }
  53. public bool IsTechnologyAvailable(TechnologyDatabaseComponent component, TechnologyPrototype tech, Dictionary<string, int>? disciplineTiers = null)
  54. {
  55. disciplineTiers ??= GetDisciplineTiers(component);
  56. if (tech.Hidden)
  57. return false;
  58. if (!component.SupportedDisciplines.Contains(tech.Discipline))
  59. return false;
  60. if (tech.Tier > disciplineTiers[tech.Discipline])
  61. return false;
  62. if (component.UnlockedTechnologies.Contains(tech.ID))
  63. return false;
  64. foreach (var prereq in tech.TechnologyPrerequisites)
  65. {
  66. if (!component.UnlockedTechnologies.Contains(prereq))
  67. return false;
  68. }
  69. return true;
  70. }
  71. public Dictionary<string, int> GetDisciplineTiers(TechnologyDatabaseComponent component)
  72. {
  73. var tiers = new Dictionary<string, int>();
  74. foreach (var discipline in component.SupportedDisciplines)
  75. {
  76. tiers.Add(discipline, GetHighestDisciplineTier(component, discipline));
  77. }
  78. return tiers;
  79. }
  80. public int GetHighestDisciplineTier(TechnologyDatabaseComponent component, string disciplineId)
  81. {
  82. return GetHighestDisciplineTier(component, PrototypeManager.Index<TechDisciplinePrototype>(disciplineId));
  83. }
  84. public int GetHighestDisciplineTier(TechnologyDatabaseComponent component, TechDisciplinePrototype techDiscipline)
  85. {
  86. var allTech = PrototypeManager.EnumeratePrototypes<TechnologyPrototype>()
  87. .Where(p => p.Discipline == techDiscipline.ID && !p.Hidden).ToList();
  88. var allUnlocked = new List<TechnologyPrototype>();
  89. foreach (var recipe in component.UnlockedTechnologies)
  90. {
  91. var proto = PrototypeManager.Index<TechnologyPrototype>(recipe);
  92. if (proto.Discipline != techDiscipline.ID)
  93. continue;
  94. allUnlocked.Add(proto);
  95. }
  96. var highestTier = techDiscipline.TierPrerequisites.Keys.Max();
  97. var tier = 2; //tier 1 is always given
  98. // todo this might break if you have hidden technologies. i'm not sure
  99. while (tier <= highestTier)
  100. {
  101. // we need to get the tech for the tier 1 below because that's
  102. // what the percentage in TierPrerequisites is referring to.
  103. var unlockedTierTech = allUnlocked.Where(p => p.Tier == tier - 1).ToList();
  104. var allTierTech = allTech.Where(p => p.Discipline == techDiscipline.ID && p.Tier == tier - 1).ToList();
  105. if (allTierTech.Count == 0)
  106. break;
  107. var percent = (float) unlockedTierTech.Count / allTierTech.Count;
  108. if (percent < techDiscipline.TierPrerequisites[tier])
  109. break;
  110. if (tier >= techDiscipline.LockoutTier &&
  111. component.MainDiscipline != null &&
  112. techDiscipline.ID != component.MainDiscipline)
  113. break;
  114. tier++;
  115. }
  116. return tier - 1;
  117. }
  118. public FormattedMessage GetTechnologyDescription(
  119. TechnologyPrototype technology,
  120. bool includeCost = true,
  121. bool includeTier = true,
  122. bool includePrereqs = false,
  123. TechDisciplinePrototype? disciplinePrototype = null)
  124. {
  125. var description = new FormattedMessage();
  126. if (includeTier)
  127. {
  128. disciplinePrototype ??= PrototypeManager.Index(technology.Discipline);
  129. description.AddMarkupOrThrow(Loc.GetString("research-console-tier-discipline-info",
  130. ("tier", technology.Tier), ("color", disciplinePrototype.Color), ("discipline", Loc.GetString(disciplinePrototype.Name))));
  131. description.PushNewline();
  132. }
  133. if (includeCost)
  134. {
  135. description.AddMarkupOrThrow(Loc.GetString("research-console-cost", ("amount", technology.Cost)));
  136. description.PushNewline();
  137. }
  138. if (includePrereqs && technology.TechnologyPrerequisites.Any())
  139. {
  140. description.AddMarkupOrThrow(Loc.GetString("research-console-prereqs-list-start"));
  141. foreach (var recipe in technology.TechnologyPrerequisites)
  142. {
  143. var techProto = PrototypeManager.Index(recipe);
  144. description.PushNewline();
  145. description.AddMarkupOrThrow(Loc.GetString("research-console-prereqs-list-entry",
  146. ("text", Loc.GetString(techProto.Name))));
  147. }
  148. description.PushNewline();
  149. }
  150. description.AddMarkupOrThrow(Loc.GetString("research-console-unlocks-list-start"));
  151. foreach (var recipe in technology.RecipeUnlocks)
  152. {
  153. var recipeProto = PrototypeManager.Index(recipe);
  154. description.PushNewline();
  155. description.AddMarkupOrThrow(Loc.GetString("research-console-unlocks-list-entry",
  156. ("name", _lathe.GetRecipeName(recipeProto))));
  157. }
  158. foreach (var generic in technology.GenericUnlocks)
  159. {
  160. description.PushNewline();
  161. description.AddMarkupOrThrow(Loc.GetString("research-console-unlocks-list-entry-generic",
  162. ("text", Loc.GetString(generic.UnlockDescription))));
  163. }
  164. return description;
  165. }
  166. /// <summary>
  167. /// Returns whether a technology is unlocked on this database or not.
  168. /// </summary>
  169. /// <returns>Whether it is unlocked or not</returns>
  170. public bool IsTechnologyUnlocked(EntityUid uid, TechnologyPrototype technology, TechnologyDatabaseComponent? component = null)
  171. {
  172. return Resolve(uid, ref component) && IsTechnologyUnlocked(uid, technology.ID, component);
  173. }
  174. /// <summary>
  175. /// Returns whether a technology is unlocked on this database or not.
  176. /// </summary>
  177. /// <returns>Whether it is unlocked or not</returns>
  178. public bool IsTechnologyUnlocked(EntityUid uid, string technologyId, TechnologyDatabaseComponent? component = null)
  179. {
  180. return Resolve(uid, ref component, false) && component.UnlockedTechnologies.Contains(technologyId);
  181. }
  182. public void TrySetMainDiscipline(TechnologyPrototype prototype, EntityUid uid, TechnologyDatabaseComponent? component = null)
  183. {
  184. if (!Resolve(uid, ref component))
  185. return;
  186. var discipline = PrototypeManager.Index(prototype.Discipline);
  187. if (prototype.Tier < discipline.LockoutTier)
  188. return;
  189. component.MainDiscipline = prototype.Discipline;
  190. Dirty(uid, component);
  191. var ev = new TechnologyDatabaseModifiedEvent();
  192. RaiseLocalEvent(uid, ref ev);
  193. }
  194. /// <summary>
  195. /// Removes a technology and its recipes from a technology database.
  196. /// </summary>
  197. public bool TryRemoveTechnology(Entity<TechnologyDatabaseComponent> entity, ProtoId<TechnologyPrototype> tech)
  198. {
  199. return TryRemoveTechnology(entity, PrototypeManager.Index(tech));
  200. }
  201. /// <summary>
  202. /// Removes a technology and its recipes from a technology database.
  203. /// </summary>
  204. [PublicAPI]
  205. public bool TryRemoveTechnology(Entity<TechnologyDatabaseComponent> entity, TechnologyPrototype tech)
  206. {
  207. if (!entity.Comp.UnlockedTechnologies.Remove(tech.ID))
  208. return false;
  209. // check to make sure we didn't somehow get the recipe from another tech.
  210. // unlikely, but whatever
  211. var recipes = tech.RecipeUnlocks;
  212. foreach (var recipe in recipes)
  213. {
  214. var hasTechElsewhere = false;
  215. foreach (var unlockedTech in entity.Comp.UnlockedTechnologies)
  216. {
  217. var unlockedTechProto = PrototypeManager.Index<TechnologyPrototype>(unlockedTech);
  218. if (!unlockedTechProto.RecipeUnlocks.Contains(recipe))
  219. continue;
  220. hasTechElsewhere = true;
  221. break;
  222. }
  223. if (!hasTechElsewhere)
  224. entity.Comp.UnlockedRecipes.Remove(recipe);
  225. }
  226. Dirty(entity, entity.Comp);
  227. UpdateTechnologyCards(entity, entity);
  228. return true;
  229. }
  230. /// <summary>
  231. /// Clear all unlocked technologies from the database.
  232. /// </summary>
  233. [PublicAPI]
  234. public void ClearTechs(EntityUid uid, TechnologyDatabaseComponent? comp = null)
  235. {
  236. if (!Resolve(uid, ref comp) || comp.UnlockedTechnologies.Count == 0)
  237. return;
  238. comp.UnlockedTechnologies.Clear();
  239. Dirty(uid, comp);
  240. }
  241. /// <summary>
  242. /// Adds a lathe recipe to the specified technology database
  243. /// without checking if it can be unlocked.
  244. /// </summary>
  245. public void AddLatheRecipe(EntityUid uid, string recipe, TechnologyDatabaseComponent? component = null)
  246. {
  247. if (!Resolve(uid, ref component))
  248. return;
  249. if (component.UnlockedRecipes.Contains(recipe))
  250. return;
  251. component.UnlockedRecipes.Add(recipe);
  252. Dirty(uid, component);
  253. var ev = new TechnologyDatabaseModifiedEvent();
  254. RaiseLocalEvent(uid, ref ev);
  255. }
  256. }