PlantHolderSystem.cs 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975
  1. using Content.Server.Atmos.EntitySystems;
  2. using Content.Server.Botany.Components;
  3. using Content.Server.Kitchen.Components;
  4. using Content.Server.Popups;
  5. using Content.Shared.Chemistry.EntitySystems;
  6. using Content.Shared.Atmos;
  7. using Content.Shared.Botany;
  8. using Content.Shared.Burial.Components;
  9. using Content.Shared.Chemistry.Reagent;
  10. using Content.Shared.Coordinates.Helpers;
  11. using Content.Shared.Examine;
  12. using Content.Shared.FixedPoint;
  13. using Content.Shared.Hands.Components;
  14. using Content.Shared.IdentityManagement;
  15. using Content.Shared.Interaction;
  16. using Content.Shared.Popups;
  17. using Content.Shared.Random;
  18. using Content.Shared.Tag;
  19. using Robust.Server.GameObjects;
  20. using Robust.Shared.Audio.Systems;
  21. using Robust.Shared.Player;
  22. using Robust.Shared.Prototypes;
  23. using Robust.Shared.Random;
  24. using Robust.Shared.Timing;
  25. using Content.Server.Labels.Components;
  26. using Content.Shared.Containers.ItemSlots;
  27. using Content.Shared.Weather;
  28. namespace Content.Server.Botany.Systems;
  29. public sealed class PlantHolderSystem : EntitySystem
  30. {
  31. [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
  32. [Dependency] private readonly BotanySystem _botany = default!;
  33. [Dependency] private readonly IPrototypeManager _prototype = default!;
  34. [Dependency] private readonly MutationSystem _mutation = default!;
  35. [Dependency] private readonly AppearanceSystem _appearance = default!;
  36. [Dependency] private readonly SharedAudioSystem _audio = default!;
  37. [Dependency] private readonly PopupSystem _popup = default!;
  38. [Dependency] private readonly IGameTiming _gameTiming = default!;
  39. [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
  40. [Dependency] private readonly TagSystem _tagSystem = default!;
  41. [Dependency] private readonly RandomHelperSystem _randomHelper = default!;
  42. [Dependency] private readonly IRobustRandom _random = default!;
  43. [Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
  44. public const float HydroponicsSpeedMultiplier = 1f;
  45. public const float HydroponicsConsumptionMultiplier = 2f;
  46. public override void Initialize()
  47. {
  48. base.Initialize();
  49. SubscribeLocalEvent<PlantHolderComponent, ExaminedEvent>(OnExamine);
  50. SubscribeLocalEvent<PlantHolderComponent, InteractUsingEvent>(OnInteractUsing);
  51. SubscribeLocalEvent<PlantHolderComponent, InteractHandEvent>(OnInteractHand);
  52. SubscribeLocalEvent<PlantHolderComponent, SolutionTransferredEvent>(OnSolutionTransferred);
  53. }
  54. public override void Update(float frameTime)
  55. {
  56. base.Update(frameTime);
  57. var query = EntityQueryEnumerator<PlantHolderComponent>();
  58. while (query.MoveNext(out var uid, out var plantHolder))
  59. {
  60. if (plantHolder.NextUpdate > _gameTiming.CurTime)
  61. continue;
  62. plantHolder.NextUpdate = _gameTiming.CurTime + plantHolder.UpdateDelay;
  63. Update(uid, plantHolder);
  64. }
  65. }
  66. private int GetCurrentGrowthStage(Entity<PlantHolderComponent> entity)
  67. {
  68. var (uid, component) = entity;
  69. if (component.Seed == null)
  70. return 0;
  71. var result = Math.Max(1, (int)(component.Age * component.Seed.GrowthStages / component.Seed.Maturation));
  72. return result;
  73. }
  74. private void OnExamine(Entity<PlantHolderComponent> entity, ref ExaminedEvent args)
  75. {
  76. if (!args.IsInDetailsRange)
  77. return;
  78. var (_, component) = entity;
  79. using (args.PushGroup(nameof(PlantHolderComponent)))
  80. {
  81. if (component.Seed == null)
  82. {
  83. args.PushMarkup(Loc.GetString("plant-holder-component-nothing-planted-message"));
  84. }
  85. else if (!component.Dead)
  86. {
  87. var displayName = Loc.GetString(component.Seed.DisplayName);
  88. args.PushMarkup(Loc.GetString("plant-holder-component-something-already-growing-message",
  89. ("seedName", displayName),
  90. ("toBeForm", displayName.EndsWith('s') ? "are" : "is")));
  91. if (component.Health <= component.Seed.Endurance / 2)
  92. {
  93. args.PushMarkup(Loc.GetString(
  94. "plant-holder-component-something-already-growing-low-health-message",
  95. ("healthState",
  96. Loc.GetString(component.Age > component.Seed.Lifespan
  97. ? "plant-holder-component-plant-old-adjective"
  98. : "plant-holder-component-plant-unhealthy-adjective"))));
  99. }
  100. }
  101. else
  102. {
  103. args.PushMarkup(Loc.GetString("plant-holder-component-dead-plant-matter-message"));
  104. }
  105. if (component.WeedLevel >= 5)
  106. args.PushMarkup(Loc.GetString("plant-holder-component-weed-high-level-message"));
  107. if (component.PestLevel >= 5)
  108. args.PushMarkup(Loc.GetString("plant-holder-component-pest-high-level-message"));
  109. args.PushMarkup(Loc.GetString($"plant-holder-component-water-level-message",
  110. ("waterLevel", (int)component.WaterLevel)));
  111. args.PushMarkup(Loc.GetString($"plant-holder-component-nutrient-level-message",
  112. ("nutritionLevel", (int)component.NutritionLevel)));
  113. if (component.DrawWarnings)
  114. {
  115. if (component.Toxins > 40f)
  116. args.PushMarkup(Loc.GetString("plant-holder-component-toxins-high-warning"));
  117. if (component.ImproperLight)
  118. args.PushMarkup(Loc.GetString("plant-holder-component-light-improper-warning"));
  119. if (component.ImproperHeat)
  120. args.PushMarkup(Loc.GetString("plant-holder-component-heat-improper-warning"));
  121. if (component.ImproperPressure)
  122. args.PushMarkup(Loc.GetString("plant-holder-component-pressure-improper-warning"));
  123. if (component.MissingGas > 0)
  124. args.PushMarkup(Loc.GetString("plant-holder-component-gas-missing-warning"));
  125. }
  126. }
  127. }
  128. private void OnInteractUsing(Entity<PlantHolderComponent> entity, ref InteractUsingEvent args)
  129. {
  130. var (uid, component) = entity;
  131. if (TryComp(args.Used, out SeedComponent? seeds))
  132. {
  133. if (component.Seed == null)
  134. {
  135. if (!_botany.TryGetSeed(seeds, out var seed))
  136. return;
  137. args.Handled = true;
  138. var name = Loc.GetString(seed.Name);
  139. var noun = Loc.GetString(seed.Noun);
  140. _popup.PopupCursor(Loc.GetString("plant-holder-component-plant-success-message",
  141. ("seedName", name),
  142. ("seedNoun", noun)), args.User, PopupType.Medium);
  143. component.Seed = seed;
  144. component.Dead = false;
  145. component.Age = 1;
  146. if (seeds.HealthOverride != null)
  147. {
  148. component.Health = seeds.HealthOverride.Value;
  149. }
  150. else
  151. {
  152. component.Health = component.Seed.Endurance;
  153. }
  154. component.LastCycle = _gameTiming.CurTime;
  155. if (TryComp<PaperLabelComponent>(args.Used, out var paperLabel))
  156. {
  157. _itemSlots.TryEjectToHands(args.Used, paperLabel.LabelSlot, args.User);
  158. }
  159. QueueDel(args.Used);
  160. CheckLevelSanity(uid, component);
  161. UpdateSprite(uid, component);
  162. return;
  163. }
  164. args.Handled = true;
  165. _popup.PopupCursor(Loc.GetString("plant-holder-component-already-seeded-message",
  166. ("name", Comp<MetaDataComponent>(uid).EntityName)), args.User, PopupType.Medium);
  167. return;
  168. }
  169. if (_tagSystem.HasTag(args.Used, "Hoe"))
  170. {
  171. args.Handled = true;
  172. if (component.WeedLevel > 0)
  173. {
  174. _popup.PopupCursor(Loc.GetString("plant-holder-component-remove-weeds-message",
  175. ("name", Comp<MetaDataComponent>(uid).EntityName)), args.User, PopupType.Medium);
  176. _popup.PopupEntity(Loc.GetString("plant-holder-component-remove-weeds-others-message",
  177. ("otherName", Comp<MetaDataComponent>(args.User).EntityName)), uid, Filter.PvsExcept(args.User), true);
  178. component.WeedLevel = 0;
  179. UpdateSprite(uid, component);
  180. }
  181. else
  182. {
  183. _popup.PopupCursor(Loc.GetString("plant-holder-component-no-weeds-message"), args.User);
  184. }
  185. return;
  186. }
  187. if (HasComp<ShovelComponent>(args.Used))
  188. {
  189. args.Handled = true;
  190. if (component.Seed != null)
  191. {
  192. _popup.PopupCursor(Loc.GetString("plant-holder-component-remove-plant-message",
  193. ("name", Comp<MetaDataComponent>(uid).EntityName)), args.User, PopupType.Medium);
  194. _popup.PopupEntity(Loc.GetString("plant-holder-component-remove-plant-others-message",
  195. ("name", Comp<MetaDataComponent>(args.User).EntityName)), uid, Filter.PvsExcept(args.User), true);
  196. RemovePlant(uid, component);
  197. }
  198. else
  199. {
  200. _popup.PopupCursor(Loc.GetString("plant-holder-component-no-plant-message",
  201. ("name", Comp<MetaDataComponent>(uid).EntityName)), args.User);
  202. }
  203. return;
  204. }
  205. if (_tagSystem.HasTag(args.Used, "PlantSampleTaker"))
  206. {
  207. args.Handled = true;
  208. if (component.Seed == null)
  209. {
  210. _popup.PopupCursor(Loc.GetString("plant-holder-component-nothing-to-sample-message"), args.User);
  211. return;
  212. }
  213. if (component.Sampled)
  214. {
  215. _popup.PopupCursor(Loc.GetString("plant-holder-component-already-sampled-message"), args.User);
  216. return;
  217. }
  218. if (component.Dead)
  219. {
  220. _popup.PopupCursor(Loc.GetString("plant-holder-component-dead-plant-message"), args.User);
  221. return;
  222. }
  223. if (GetCurrentGrowthStage(entity) <= 1)
  224. {
  225. _popup.PopupCursor(Loc.GetString("plant-holder-component-early-sample-message"), args.User);
  226. return;
  227. }
  228. component.Health -= (_random.Next(3, 5) * 10);
  229. float? healthOverride;
  230. if (component.Harvest)
  231. {
  232. healthOverride = null;
  233. }
  234. else
  235. {
  236. healthOverride = component.Health;
  237. }
  238. var packetSeed = component.Seed;
  239. var seed = _botany.SpawnSeedPacket(packetSeed, Transform(args.User).Coordinates, args.User, healthOverride);
  240. _randomHelper.RandomOffset(seed, 0.25f);
  241. var displayName = Loc.GetString(component.Seed.DisplayName);
  242. _popup.PopupCursor(Loc.GetString("plant-holder-component-take-sample-message",
  243. ("seedName", displayName)), args.User);
  244. DoScream(entity.Owner, component.Seed);
  245. if (_random.Prob(0.3f))
  246. component.Sampled = true;
  247. // Just in case.
  248. CheckLevelSanity(uid, component);
  249. ForceUpdateByExternalCause(uid, component);
  250. return;
  251. }
  252. if (HasComp<SharpComponent>(args.Used))
  253. {
  254. args.Handled = true;
  255. DoHarvest(uid, args.User, component);
  256. return;
  257. }
  258. if (TryComp<ProduceComponent>(args.Used, out var produce) && false) // Deactivated untill we ballance composting directly into the field
  259. {
  260. args.Handled = true;
  261. _popup.PopupCursor(Loc.GetString("plant-holder-component-compost-message",
  262. ("owner", uid),
  263. ("usingItem", args.Used)), args.User, PopupType.Medium);
  264. _popup.PopupEntity(Loc.GetString("plant-holder-component-compost-others-message",
  265. ("user", Identity.Entity(args.User, EntityManager)),
  266. ("usingItem", args.Used),
  267. ("owner", uid)), uid, Filter.PvsExcept(args.User), true);
  268. if (_solutionContainerSystem.TryGetSolution(args.Used, produce.SolutionName, out var soln2, out var solution2))
  269. {
  270. if (_solutionContainerSystem.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution, out var solution1))
  271. {
  272. // We try to fit as much of the composted plant's contained solution into the hydroponics tray as we can,
  273. // since the plant will be consumed anyway.
  274. var fillAmount = FixedPoint2.Min(solution2.Volume, solution1.AvailableVolume);
  275. _solutionContainerSystem.TryAddSolution(component.SoilSolution.Value, _solutionContainerSystem.SplitSolution(soln2.Value, fillAmount));
  276. ForceUpdateByExternalCause(uid, component);
  277. }
  278. }
  279. var seed = produce.Seed;
  280. if (seed != null)
  281. {
  282. var nutrientBonus = seed.Potency / 2.5f;
  283. AdjustNutrient(uid, nutrientBonus, component);
  284. }
  285. QueueDel(args.Used);
  286. }
  287. }
  288. private void OnSolutionTransferred(Entity<PlantHolderComponent> ent, ref SolutionTransferredEvent args)
  289. {
  290. _audio.PlayPvs(ent.Comp.WateringSound, ent.Owner);
  291. }
  292. private void OnInteractHand(Entity<PlantHolderComponent> entity, ref InteractHandEvent args)
  293. {
  294. DoHarvest(entity, args.User, entity.Comp);
  295. }
  296. public void WeedInvasion()
  297. {
  298. // TODO
  299. }
  300. public void Update(EntityUid uid, PlantHolderComponent? component = null)
  301. {
  302. if (!Resolve(uid, ref component))
  303. return;
  304. UpdateReagents(uid, component);
  305. var curTime = _gameTiming.CurTime;
  306. if (component.ForceUpdate)
  307. component.ForceUpdate = false;
  308. else if (curTime < (component.LastCycle + component.CycleDelay))
  309. {
  310. if (component.UpdateSpriteAfterUpdate)
  311. UpdateSprite(uid, component);
  312. return;
  313. }
  314. component.LastCycle = curTime;
  315. // Process mutations
  316. if (component.MutationLevel > 0)
  317. {
  318. Mutate(uid, Math.Min(component.MutationLevel, 25), component);
  319. component.UpdateSpriteAfterUpdate = true;
  320. component.MutationLevel = 0;
  321. }
  322. // Weeds like water and nutrients! They may appear even if there's not a seed planted.
  323. if (component.WaterLevel > 10 && component.NutritionLevel > 5)
  324. {
  325. var chance = 0f;
  326. if (component.Seed == null)
  327. chance = 0.05f;
  328. else if (component.Seed.TurnIntoKudzu)
  329. chance = 1f;
  330. else
  331. chance = 0.01f;
  332. if (_random.Prob(chance))
  333. component.WeedLevel += 1 + HydroponicsSpeedMultiplier * component.WeedCoefficient;
  334. if (component.DrawWarnings)
  335. component.UpdateSpriteAfterUpdate = true;
  336. }
  337. if (component.Seed != null && component.Seed.TurnIntoKudzu
  338. && component.WeedLevel >= component.Seed.WeedHighLevelThreshold)
  339. {
  340. Spawn(component.Seed.KudzuPrototype, Transform(uid).Coordinates.SnapToGrid(EntityManager));
  341. component.Seed.TurnIntoKudzu = false;
  342. component.Health = 0;
  343. }
  344. // There's a chance for a weed explosion to happen if weeds take over.
  345. // Plants that are themselves weeds (WeedTolerance > 8) are unaffected.
  346. if (component.WeedLevel >= 10 && _random.Prob(0.1f))
  347. {
  348. if (component.Seed == null || component.WeedLevel >= component.Seed.WeedTolerance + 2)
  349. WeedInvasion();
  350. }
  351. // If we have no seed planted, or the plant is dead, stop processing here.
  352. if (component.Seed == null || component.Dead)
  353. {
  354. if (component.UpdateSpriteAfterUpdate)
  355. UpdateSprite(uid, component);
  356. return;
  357. }
  358. // There's a small chance the pest population increases.
  359. // Can only happen when there's a live seed planted.
  360. if (_random.Prob(0.01f))
  361. {
  362. component.PestLevel += 0.5f * HydroponicsSpeedMultiplier;
  363. if (component.DrawWarnings)
  364. component.UpdateSpriteAfterUpdate = true;
  365. }
  366. // Advance plant age here.
  367. if (component.SkipAging > 0)
  368. component.SkipAging--;
  369. else
  370. {
  371. if (_random.Prob(0.8f))
  372. component.Age += (int)(1 * HydroponicsSpeedMultiplier);
  373. component.UpdateSpriteAfterUpdate = true;
  374. }
  375. // Nutrient consumption.
  376. if (component.Seed.NutrientConsumption > 0 && component.NutritionLevel > 0 && _random.Prob(0.75f))
  377. {
  378. component.NutritionLevel -= MathF.Max(0f, component.Seed.NutrientConsumption * HydroponicsSpeedMultiplier);
  379. if (component.DrawWarnings)
  380. component.UpdateSpriteAfterUpdate = true;
  381. }
  382. // Water consumption.
  383. if (component.Seed.WaterConsumption > 0 && component.WaterLevel > 0 && _random.Prob(0.75f))
  384. {
  385. var weather = EntityQueryEnumerator<WeatherNomadsComponent>();
  386. while (weather.MoveNext(out var uuid, out var weatherComponent))
  387. {
  388. if (weatherComponent.CurrentWeather == "Rain" || weatherComponent.CurrentWeather == "Storm")
  389. {
  390. component.WaterLevel += 2f;
  391. }
  392. }
  393. component.WaterLevel -= MathF.Max(0f,
  394. component.Seed.WaterConsumption * HydroponicsConsumptionMultiplier * HydroponicsSpeedMultiplier);
  395. if (component.DrawWarnings)
  396. component.UpdateSpriteAfterUpdate = true;
  397. }
  398. var healthMod = _random.Next(1, 3) * HydroponicsSpeedMultiplier;
  399. // Make sure genetics are viable.
  400. if (!component.Seed.Viable)
  401. {
  402. AffectGrowth(uid, -1, component);
  403. component.Health -= 6 * healthMod;
  404. }
  405. // Prevents the plant from aging when lacking resources.
  406. // Limits the effect on aging so that when resources are added, the plant starts growing in a reasonable amount of time.
  407. if (component.SkipAging < 10)
  408. {
  409. // Make sure the plant is not starving.
  410. if (component.NutritionLevel > 5)
  411. {
  412. component.Health += Convert.ToInt32(_random.Prob(0.35f)) * healthMod;
  413. }
  414. else
  415. {
  416. AffectGrowth(uid, -1, component);
  417. component.Health -= healthMod;
  418. }
  419. // Make sure the plant is not thirsty.
  420. if (component.WaterLevel > 10)
  421. {
  422. component.Health += Convert.ToInt32(_random.Prob(0.35f)) * healthMod;
  423. }
  424. else
  425. {
  426. AffectGrowth(uid, -1, component);
  427. component.Health -= healthMod;
  428. }
  429. if (component.DrawWarnings)
  430. component.UpdateSpriteAfterUpdate = true;
  431. }
  432. var environment = _atmosphere.GetContainingMixture(uid, true, true) ?? GasMixture.SpaceGas;
  433. component.MissingGas = 0;
  434. if (component.Seed.ConsumeGasses.Count > 0)
  435. {
  436. foreach (var (gas, amount) in component.Seed.ConsumeGasses)
  437. {
  438. if (environment.GetMoles(gas) < amount)
  439. {
  440. component.MissingGas++;
  441. continue;
  442. }
  443. environment.AdjustMoles(gas, -amount);
  444. }
  445. if (component.MissingGas > 0)
  446. {
  447. component.Health -= component.MissingGas * HydroponicsSpeedMultiplier;
  448. if (component.DrawWarnings)
  449. component.UpdateSpriteAfterUpdate = true;
  450. }
  451. }
  452. // SeedPrototype pressure resistance.
  453. var pressure = environment.Pressure;
  454. if (pressure < component.Seed.LowPressureTolerance || pressure > component.Seed.HighPressureTolerance)
  455. {
  456. component.Health -= healthMod;
  457. component.ImproperPressure = true;
  458. if (component.DrawWarnings)
  459. component.UpdateSpriteAfterUpdate = true;
  460. }
  461. else
  462. {
  463. component.ImproperPressure = false;
  464. }
  465. // SeedPrototype ideal temperature.
  466. if (MathF.Abs(environment.Temperature - component.Seed.IdealHeat) > component.Seed.HeatTolerance)
  467. {
  468. component.Health -= healthMod;
  469. component.ImproperHeat = true;
  470. if (component.DrawWarnings)
  471. component.UpdateSpriteAfterUpdate = true;
  472. }
  473. else
  474. {
  475. component.ImproperHeat = false;
  476. }
  477. // Gas production.
  478. var exudeCount = component.Seed.ExudeGasses.Count;
  479. if (exudeCount > 0)
  480. {
  481. foreach (var (gas, amount) in component.Seed.ExudeGasses)
  482. {
  483. environment.AdjustMoles(gas,
  484. MathF.Max(1f, MathF.Round(amount * MathF.Round(component.Seed.Potency) / exudeCount)));
  485. }
  486. }
  487. // Toxin levels beyond the plant's tolerance cause damage.
  488. // They are, however, slowly reduced over time.
  489. if (component.Toxins > 0)
  490. {
  491. var toxinUptake = MathF.Max(1, MathF.Round(component.Toxins / 10f));
  492. if (component.Toxins > component.Seed.ToxinsTolerance)
  493. {
  494. component.Health -= toxinUptake;
  495. }
  496. component.Toxins -= toxinUptake;
  497. if (component.DrawWarnings)
  498. component.UpdateSpriteAfterUpdate = true;
  499. }
  500. // Weed levels.
  501. if (component.PestLevel > 0)
  502. {
  503. // TODO: Carnivorous plants?
  504. if (component.PestLevel > component.Seed.PestTolerance)
  505. {
  506. component.Health -= HydroponicsSpeedMultiplier;
  507. }
  508. if (component.DrawWarnings)
  509. component.UpdateSpriteAfterUpdate = true;
  510. }
  511. // Weed levels.
  512. if (component.WeedLevel > 0)
  513. {
  514. // TODO: Parasitic plants.
  515. if (component.WeedLevel >= component.Seed.WeedTolerance)
  516. {
  517. component.Health -= HydroponicsSpeedMultiplier;
  518. }
  519. if (component.DrawWarnings)
  520. component.UpdateSpriteAfterUpdate = true;
  521. }
  522. if (component.Age > component.Seed.Lifespan)
  523. {
  524. component.Health -= _random.Next(3, 5) * HydroponicsSpeedMultiplier;
  525. if (component.DrawWarnings)
  526. component.UpdateSpriteAfterUpdate = true;
  527. }
  528. else if (component.Age < 0) // Revert back to seed packet!
  529. {
  530. var packetSeed = component.Seed;
  531. // will put it in the trays hands if it has any, please do not try doing this
  532. _botany.SpawnSeedPacket(packetSeed, Transform(uid).Coordinates, uid);
  533. RemovePlant(uid, component);
  534. component.ForceUpdate = true;
  535. Update(uid, component);
  536. return;
  537. }
  538. CheckHealth(uid, component);
  539. if (component.Harvest && component.Seed.HarvestRepeat == HarvestType.SelfHarvest)
  540. AutoHarvest(uid, component);
  541. // If enough time has passed since the plant was harvested, we're ready to harvest again!
  542. if (!component.Dead && component.Seed.ProductPrototypes.Count > 0)
  543. {
  544. if (component.Age > component.Seed.Production)
  545. {
  546. if (component.Age - component.LastProduce > component.Seed.Production && !component.Harvest)
  547. {
  548. component.Harvest = true;
  549. component.LastProduce = component.Age;
  550. }
  551. }
  552. else
  553. {
  554. if (component.Harvest)
  555. {
  556. component.Harvest = false;
  557. component.LastProduce = component.Age;
  558. }
  559. }
  560. }
  561. CheckLevelSanity(uid, component);
  562. if (component.UpdateSpriteAfterUpdate)
  563. UpdateSprite(uid, component);
  564. }
  565. //TODO: kill this bullshit
  566. public void CheckLevelSanity(EntityUid uid, PlantHolderComponent? component = null)
  567. {
  568. if (!Resolve(uid, ref component))
  569. return;
  570. if (component.Seed != null)
  571. component.Health = MathHelper.Clamp(component.Health, 0, component.Seed.Endurance);
  572. else
  573. {
  574. component.Health = 0f;
  575. component.Dead = false;
  576. }
  577. component.MutationLevel = MathHelper.Clamp(component.MutationLevel, 0f, 100f);
  578. component.NutritionLevel = MathHelper.Clamp(component.NutritionLevel, 0f, 100f);
  579. component.WaterLevel = MathHelper.Clamp(component.WaterLevel, 0f, 100f);
  580. component.PestLevel = MathHelper.Clamp(component.PestLevel, 0f, 10f);
  581. component.WeedLevel = MathHelper.Clamp(component.WeedLevel, 0f, 10f);
  582. component.Toxins = MathHelper.Clamp(component.Toxins, 0f, 100f);
  583. component.YieldMod = MathHelper.Clamp(component.YieldMod, 0, 2);
  584. component.MutationMod = MathHelper.Clamp(component.MutationMod, 0f, 3f);
  585. }
  586. public bool DoHarvest(EntityUid plantholder, EntityUid user, PlantHolderComponent? component = null)
  587. {
  588. if (!Resolve(plantholder, ref component))
  589. return false;
  590. if (component.Seed == null || Deleted(user))
  591. return false;
  592. if (component.Harvest && !component.Dead)
  593. {
  594. if (TryComp<HandsComponent>(user, out var hands))
  595. {
  596. if (!_botany.CanHarvest(component.Seed, hands.ActiveHandEntity))
  597. {
  598. _popup.PopupCursor(Loc.GetString("plant-holder-component-ligneous-cant-harvest-message"), user);
  599. return false;
  600. }
  601. }
  602. else if (!_botany.CanHarvest(component.Seed))
  603. {
  604. return false;
  605. }
  606. _botany.Harvest(component.Seed, user, component.YieldMod);
  607. AfterHarvest(plantholder, component);
  608. return true;
  609. }
  610. if (!component.Dead)
  611. return false;
  612. RemovePlant(plantholder, component);
  613. AfterHarvest(plantholder, component);
  614. return true;
  615. }
  616. /// <summary>
  617. /// Force do scream on PlantHolder (like plant is screaming) using seed's ScreamSound specifier (collection or soundPath)
  618. /// </summary>
  619. /// <returns></returns>
  620. public bool DoScream(EntityUid plantholder, SeedData? seed = null)
  621. {
  622. if (seed == null || seed.CanScream == false)
  623. return false;
  624. _audio.PlayPvs(seed.ScreamSound, plantholder);
  625. return true;
  626. }
  627. public void AutoHarvest(EntityUid uid, PlantHolderComponent? component = null)
  628. {
  629. if (!Resolve(uid, ref component))
  630. return;
  631. if (component.Seed == null || !component.Harvest)
  632. return;
  633. _botany.AutoHarvest(component.Seed, Transform(uid).Coordinates);
  634. AfterHarvest(uid, component);
  635. }
  636. private void AfterHarvest(EntityUid uid, PlantHolderComponent? component = null)
  637. {
  638. if (!Resolve(uid, ref component))
  639. return;
  640. component.Harvest = false;
  641. component.LastProduce = component.Age;
  642. DoScream(uid, component.Seed);
  643. if (component.Seed?.HarvestRepeat == HarvestType.NoRepeat)
  644. RemovePlant(uid, component);
  645. CheckLevelSanity(uid, component);
  646. UpdateSprite(uid, component);
  647. }
  648. public void CheckHealth(EntityUid uid, PlantHolderComponent? component = null)
  649. {
  650. if (!Resolve(uid, ref component))
  651. return;
  652. if (component.Health <= 0)
  653. {
  654. Die(uid, component);
  655. }
  656. }
  657. public void Die(EntityUid uid, PlantHolderComponent? component = null)
  658. {
  659. if (!Resolve(uid, ref component))
  660. return;
  661. component.Dead = true;
  662. component.Harvest = false;
  663. component.MutationLevel = 0;
  664. component.YieldMod = 1;
  665. component.MutationMod = 1;
  666. component.ImproperLight = false;
  667. component.ImproperHeat = false;
  668. component.ImproperPressure = false;
  669. component.WeedLevel += 1 * HydroponicsSpeedMultiplier;
  670. component.PestLevel = 0;
  671. UpdateSprite(uid, component);
  672. }
  673. public void RemovePlant(EntityUid uid, PlantHolderComponent? component = null)
  674. {
  675. if (!Resolve(uid, ref component))
  676. return;
  677. component.YieldMod = 1;
  678. component.MutationMod = 1;
  679. component.PestLevel = 0;
  680. component.Seed = null;
  681. component.Dead = false;
  682. component.Age = 0;
  683. component.LastProduce = 0;
  684. component.Sampled = false;
  685. component.Harvest = false;
  686. component.ImproperLight = false;
  687. component.ImproperPressure = false;
  688. component.ImproperHeat = false;
  689. UpdateSprite(uid, component);
  690. }
  691. public void AffectGrowth(EntityUid uid, int amount, PlantHolderComponent? component = null)
  692. {
  693. if (!Resolve(uid, ref component))
  694. return;
  695. if (component.Seed == null)
  696. return;
  697. if (amount > 0)
  698. {
  699. if (component.Age < component.Seed.Maturation)
  700. component.Age += amount;
  701. else if (!component.Harvest && component.Seed.Yield <= 0f)
  702. component.LastProduce -= amount;
  703. }
  704. else
  705. {
  706. if (component.Age < component.Seed.Maturation)
  707. component.SkipAging++;
  708. else if (!component.Harvest && component.Seed.Yield <= 0f)
  709. component.LastProduce += amount;
  710. }
  711. }
  712. public void AdjustNutrient(EntityUid uid, float amount, PlantHolderComponent? component = null)
  713. {
  714. if (!Resolve(uid, ref component))
  715. return;
  716. component.NutritionLevel += amount;
  717. }
  718. public void AdjustWater(EntityUid uid, float amount, PlantHolderComponent? component = null)
  719. {
  720. if (!Resolve(uid, ref component))
  721. return;
  722. component.WaterLevel += amount;
  723. // Water dilutes toxins.
  724. if (amount > 0)
  725. {
  726. component.Toxins -= amount * 4f;
  727. }
  728. }
  729. public void UpdateReagents(EntityUid uid, PlantHolderComponent? component = null)
  730. {
  731. if (!Resolve(uid, ref component))
  732. return;
  733. if (!_solutionContainerSystem.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution, out var solution))
  734. return;
  735. if (solution.Volume > 0 && component.MutationLevel < 25)
  736. {
  737. var amt = FixedPoint2.New(1);
  738. foreach (var entry in _solutionContainerSystem.RemoveEachReagent(component.SoilSolution.Value, amt))
  739. {
  740. var reagentProto = _prototype.Index<ReagentPrototype>(entry.Reagent.Prototype);
  741. reagentProto.ReactionPlant(uid, entry, solution);
  742. }
  743. }
  744. CheckLevelSanity(uid, component);
  745. }
  746. private void Mutate(EntityUid uid, float severity, PlantHolderComponent? component = null)
  747. {
  748. if (!Resolve(uid, ref component))
  749. return;
  750. if (component.Seed != null)
  751. {
  752. EnsureUniqueSeed(uid, component);
  753. _mutation.MutateSeed(uid, ref component.Seed, severity);
  754. }
  755. }
  756. public void UpdateSprite(EntityUid uid, PlantHolderComponent? component = null)
  757. {
  758. if (!Resolve(uid, ref component))
  759. return;
  760. component.UpdateSpriteAfterUpdate = false;
  761. if (!TryComp<AppearanceComponent>(uid, out var app))
  762. return;
  763. if (component.Seed != null)
  764. {
  765. if (component.DrawWarnings)
  766. {
  767. _appearance.SetData(uid, PlantHolderVisuals.HealthLight, component.Health <= component.Seed.Endurance / 2f);
  768. }
  769. if (component.Dead)
  770. {
  771. _appearance.SetData(uid, PlantHolderVisuals.PlantRsi, component.Seed.PlantRsi.ToString(), app);
  772. _appearance.SetData(uid, PlantHolderVisuals.PlantState, "dead", app);
  773. }
  774. else if (component.Harvest)
  775. {
  776. _appearance.SetData(uid, PlantHolderVisuals.PlantRsi, component.Seed.PlantRsi.ToString(), app);
  777. _appearance.SetData(uid, PlantHolderVisuals.PlantState, "harvest", app);
  778. }
  779. else if (component.Age < component.Seed.Maturation)
  780. {
  781. var growthStage = GetCurrentGrowthStage((uid, component));
  782. _appearance.SetData(uid, PlantHolderVisuals.PlantRsi, component.Seed.PlantRsi.ToString(), app);
  783. _appearance.SetData(uid, PlantHolderVisuals.PlantState, $"stage-{growthStage}", app);
  784. component.LastProduce = component.Age;
  785. }
  786. else
  787. {
  788. _appearance.SetData(uid, PlantHolderVisuals.PlantRsi, component.Seed.PlantRsi.ToString(), app);
  789. _appearance.SetData(uid, PlantHolderVisuals.PlantState, $"stage-{component.Seed.GrowthStages}", app);
  790. }
  791. }
  792. else
  793. {
  794. _appearance.SetData(uid, PlantHolderVisuals.PlantState, "", app);
  795. _appearance.SetData(uid, PlantHolderVisuals.HealthLight, false, app);
  796. }
  797. if (!component.DrawWarnings)
  798. return;
  799. _appearance.SetData(uid, PlantHolderVisuals.WaterLight, component.WaterLevel <= 15, app);
  800. _appearance.SetData(uid, PlantHolderVisuals.NutritionLight, component.NutritionLevel <= 8, app);
  801. _appearance.SetData(uid, PlantHolderVisuals.AlertLight,
  802. component.WeedLevel >= 5 || component.PestLevel >= 5 || component.Toxins >= 40 || component.ImproperHeat ||
  803. component.ImproperLight || component.ImproperPressure || component.MissingGas > 0, app);
  804. _appearance.SetData(uid, PlantHolderVisuals.HarvestLight, component.Harvest, app);
  805. }
  806. /// <summary>
  807. /// Check if the currently contained seed is unique. If it is not, clone it so that we have a unique seed.
  808. /// Necessary to avoid modifying global seeds.
  809. /// </summary>
  810. public void EnsureUniqueSeed(EntityUid uid, PlantHolderComponent? component = null)
  811. {
  812. if (!Resolve(uid, ref component))
  813. return;
  814. if (component.Seed is { Unique: false })
  815. component.Seed = component.Seed.Clone();
  816. }
  817. public void ForceUpdateByExternalCause(EntityUid uid, PlantHolderComponent? component = null)
  818. {
  819. if (!Resolve(uid, ref component))
  820. return;
  821. component.SkipAging++; // We're forcing an update cycle, so one age hasn't passed.
  822. component.ForceUpdate = true;
  823. Update(uid, component);
  824. }
  825. }