1
0

PlantHolderSystem.cs 37 KB

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