Upstream 28.02-03.03 (#268)

* More Tajaran Markings (#1834)

<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Description

<!--
Explain this PR in as much detail as applicable

Some example prompts to consider:
How might this affect the game? The codebase?
What might be some alternatives to this?
How/Who does this benefit/hurt [the game/codebase]?
-->

Description.
Adds separate eye colors to Tajaran and makes most of the markings from
"Fashion Update: Earrings & Makeup" available to Tajarans
---

<!--
A list of everything you have to do before this PR is "complete"
You probably won't have to complete everything before merging but it's
good to leave future references
-->

---

<!--
This is default collapsed, readers click to expand it and see all your
media
The PR media section can get very large at times, so this is a good way
to keep it clean
The title is written using HTML tags
The title must be within the <summary> tags or you won't see it
-->

<details><summary><h1>Media</h1></summary>
<p>

![image1](https://github.com/user-attachments/assets/4ceace8e-c1bd-4ee8-833a-19cf2cf9626d)

![image2](https://github.com/user-attachments/assets/7b2d6d25-4335-4f5e-96eb-8f0ae187e459)

</p>
</details>

---

# Changelog

<!--
You can add an author after the `🆑` to change the name that appears
in the changelog (ex: `🆑 Death`)
Leaving it blank will default to your GitHub display name
This includes all available types for the changelog
-->

🆑 Tonk
- add: Tajarans now have separate eye, wrist, tattoo, and makeup
markings

---------

Co-authored-by: VMSolidus <evilexecutive@gmail.com>
(cherry picked from commit e45008ddf8a529c2126907ecac8ffff2a74058de)

* Automatic Changelog Update (#1834)

(cherry picked from commit 0091c1ebdc4bc768c0906049fa9d417d962d1839)

* Cybernetics Trait Changes (#1828)

# Description

Changes/buffs to Cybernetic Traits.
Some lesser used traits get some love, while some other stuff gets some
logical re-balancing.

Feel free to point out if some shitcode is broken or need explaining.

---

# TODO

- [ ] I got ideas cooking that I don't know how to code

---

# Changelog

🆑

tweak: Striking Calluses no longer require you to be one of 3 jobs and
Human. Also increased the +1 damage to +2.
tweak: Bionic Spinarette SHOULD no longer have a hunger penalty and
costs less.
tweak: Platelet Factories heal rate buffed from 0.07 to 0.35, airloss
from 0.7 to 0.25 and healing cap increased from 200 to 400.
tweak: Decreased the cost of Thermal Vision to be in line with Night
Vision.
    tweak: IPC Platelet Factories healing cap increased from 200 to 250
    tweak: Cyber-Eyes Omnihud now pickable by Command too.
fix: Fixed name and description of Cyber-Eyes Modules for Night Vision
and Thermal Vision.
    remove: Mind over Machine from Cyber-Eyes Modules.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- "Striking Calluses" now delivers increased unarmed strike damage,
enhancing combat performance.

- **Documentation**
- Trait names and descriptions have been updated for greater clarity and
consistency, including changes to "Cyber-Eyes" terminology.

- **Chores**
- Redundant trait options were removed from the selection pool to
streamline gameplay.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Raikyr0 <Kurohana@hotmail.com.au>
Signed-off-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>

(cherry picked from commit a480c6605ebdfdd32d87a9001f2aef8303433a8d)

* Automatic Changelog Update (#1828)

(cherry picked from commit 365dd4353a06854120e0a38ff05f193bad48bbc7)

* Shadowkin Age Fixes & Plus Plushies (#1684)

# Description

Shadowkin middle-aged increased to 80, old age lowered to 175, max age
lowered to 250. Shadowkin can now collect their goofy little plushie
from a variety of sources

---

# TODO

- [x] Adjust Shadowkin age brackets
- [x] Add Shadowkin plushie to crates and stuff

---

# Changelog

🆑 ShirouAjisai
- add: Added Shadowkin plushie to crates and stuff
- tweak: Tweaked Shadowkin age brackets

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a new "shadowkin plushie" loadout item, enhancing
customization options.
- Expanded the collectible pool by adding a new plushie available in
multiple game areas, including reward systems and random spawners.
- Enhanced the variety of items available for the `PresentRandom` entity
with the addition of the "shadowkin plushie."

- **Adjustments**
- Refined life-stage parameters for the Shadowkin species, adjusting age
thresholds to better define maturity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: SixplyDev <einlichen@gmail.com>
Signed-off-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: ShirouAjisai <zaneromeave319@gmail.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>
(cherry picked from commit fb3d00036f6a21d7fa3c4b41341cd61b1e41e0d0)

* Automatic Changelog Update (#1684)

(cherry picked from commit caf8572352d38f51b15d21f0e1f92434f869dd14)

* Trait Add Tag (#1846)

# Description

Added TraitAddTag Function, which for example can be used to add
Spidercraft to the Spinerette trait.

# Changelog

🆑
- add: TraitAddTag Function

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Entities now receive automatic tag assignments at spawn, enhancing the
system's trait interaction and overall categorization capabilities.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Raikyr0 <Kurohana@hotmail.com.au>
Signed-off-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
(cherry picked from commit b1acdc4017dc1181b7f557351e82ef1df93635c2)

* Automatic Changelog Update (#1846)

(cherry picked from commit 9622d443d5308eda14231c3b3bb3130884465272)

* Arachne SpiderCrafting (#1847)

# Description

Added SpiderCraft Tag to Arachne

# Changelog

🆑
- add: Added SpiderCraft to Arachne

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced a new "SpiderCraft" classification for the Urist McArachne
entity, expanding its behavior and interactions related to
spider-specific mechanisms.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Raikyr0 <Kurohana@hotmail.com.au>
Signed-off-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
(cherry picked from commit ff4146f879d397993eee22a2a4807e986e404641)

* Automatic Changelog Update (#1847)

(cherry picked from commit 91d40483c2c49f86d7b2609a5ac9cd7b30d16c00)

* Add Centcom Disabler (#1845)

<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Description

<!--
Explain this PR in as much detail as applicable

Some example prompts to consider:
How might this affect the game? The codebase?
What might be some alternatives to this?
How/Who does this benefit/hurt [the game/codebase]?
-->

it self recharges a bit. It's also green. It's also a steal target,
because it's green.

I noticed the Nanotrasen Representative has a disabler in his locker by
default, but does not get to pick one in a loadout. I figured I'd remedy
this, by giving him a shiny green Disabler that slightly recharges on
its own.

It deals the same stamina damage as the normal Disabler. The only
differences are:
- Green
- Steal Target
- Slightly higher rate of fire
- Slightly recharges itself (half as slow as the antique pistol)
- Admits Centcom doesn't trust you with lethals in its description

---

<!--
This is default collapsed, readers click to expand it and see all your
media
The PR media section can get very large at times, so this is a good way
to keep it clean
The title is written using HTML tags
The title must be within the <summary> tags or you won't see it
-->

<details><summary><h1>Media</h1></summary>
<p>

![image](https://github.com/user-attachments/assets/e1e36ae6-8888-4d60-b946-50c90af16f9f)

![image](https://github.com/user-attachments/assets/887aa64f-53cd-4e91-bea5-23f83243bfbc)

https://github.com/user-attachments/assets/f7eaff3d-b8b9-4954-9688-fb9ef0d04588

![image](https://github.com/user-attachments/assets/15dcb85c-7675-4477-bda3-c790e26aebd6)

</p>
</details>

---

# Changelog

<!--
You can add an author after the `🆑` to change the name that appears
in the changelog (ex: `🆑 Death`)
Leaving it blank will default to your GitHub display name
This includes all available types for the changelog
-->

🆑
- add: Added CentCom disabler as loadout option for the Nanotrasen
Representative.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Expanded loadout options for the Nanotrasen Representative role with a
dedicated weapon configuration.
- Introduced the "CentCom disabler," a new self-defense weapon option
with advanced features.
- Updated localization entries to reflect the new representative weapon
grouping.
- Added a new steal target group for the Nanotrasen representative's
weapon.
- Enhanced visual assets with updated animations and states for the new
weapon.
- Introduced new objectives related to the Nanotrasen Representative
role, enhancing gameplay experiences.
	- Added the "Nanotrasen Representative" job title to localization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

(cherry picked from commit 143d24951a200ab94f3e4e88d3a3a90eeb8856ca)

* Automatic Changelog Update (#1845)

(cherry picked from commit 7ca0757334ee9a1d87c9cbf1f9cc02a860ecc136)

* Plant Analyzer Port (#1849)

<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Description

<!--
Explain this PR in as much detail as applicable

Some example prompts to consider:
How might this affect the game? The codebase?
What might be some alternatives to this?
How/Who does this benefit/hurt [the game/codebase]?
-->

Ported directly from
https://github.com/Goob-Station/Goob-Station/pull/1685
I tweaked the sprite, and changed its usage of a Papersystem. I can't
speak for the code quality, since I didn't write it, but I'm willing to
fix things so long as I have the capability to do so.

---

<!--
This is default collapsed, readers click to expand it and see all your
media
The PR media section can get very large at times, so this is a good way
to keep it clean
The title is written using HTML tags
The title must be within the <summary> tags or you won't see it
-->

<details><summary><h1>Media</h1></summary>
<p>

![image](https://github.com/user-attachments/assets/18e93d53-9537-49fd-9dfb-b4983d2630f0)

![image](https://github.com/user-attachments/assets/91ceaaca-7441-4afc-be2e-489b00c320d4)

![image](https://github.com/user-attachments/assets/e03cc8b6-6b07-449b-918f-2eb7783dcfac)

https://github.com/user-attachments/assets/0189567a-57ca-4e9d-ba0d-74e622e1d30d

https://github.com/user-attachments/assets/25ea6100-1458-4804-98e4-5f70b6bfcd45

</p>
</details>

---

# Changelog

<!--
You can add an author after the `🆑` to change the name that appears
in the changelog (ex: `🆑 Death`)
Leaving it blank will default to your GitHub display name
This includes all available types for the changelog
-->

🆑
- add: Port Plant Analyzer from botanySupremacist, who took it from
ian321

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a comprehensive plant analyzer interface that displays
detailed plant health, tray data, and environmental conditions.
- Added an in-game report printing feature for easy access to analysis
results.

- **Enhancements**
  - Refined yield calculations and plant metabolism behaviors.
- Integrated the analyzer item into crafting recipes, vending machines,
and locker inventories.
- Expanded localization for clearer, user-friendly plant analysis
information.
  - Added new localization strings for printer status feedback.
- Introduced new classes and messages for improved data handling and
user interaction within the plant analyzer system.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Timfa <timfalken@hotmail.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
(cherry picked from commit 47a55408ad92af463159dea3325edd0c9c9611ce)

* Automatic Changelog Update (#1849)

(cherry picked from commit 4da1efdfd293d5df1c8bd889c621eea94ed5fed8)

* Mind Role Entities (#31318)

* Mind Role Entities wip

* headrev count fix

* silicon stuff, cleanup

* exclusive antag config, cleanup

* jobroleadd overwerite

* logging stuff

* MindHasRole cleanup, admin log stuff

* last second cleanup

* ocd

* minor cleanup

* remove createdTime datafield

* now actually using the event replacement I made for role time tracking

* weh

(cherry picked from commit 24fae223e698b09cf9928c4a0f2f1dc774f266ab)

* Fix error

(cherry picked from commit d33bf89a62ae2f5d51f3af01b4ae2ef54341b5c5)

* Update SharedContentIoC.cs

(cherry picked from commit a50fed2fee56b57d0507a58ebf7bc13de82ad9d2)

* dragon antag refactor (#28217)

* remove dragon system usage of GenericAntag

* add AntagRandomSpawn for making antags spawn at a random tile

* add AntagSpawner to make an antag spawner just spawn an entity

* add antag prototype for dragon since it never had one

* make dragon spawner a GhostRoleAntagSpawner, remove GenericAntag

* make dragon rule use AntagSelection and stuff

* remove dragon GenericAntag rule

* add back to spawn menu

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
(cherry picked from commit c0a56377bc5b9563de973d04f92d7d6923ca9145)

* Cultist Mind Roles

(cherry picked from commit 585e26103a67cc2bd185faaa468ddc5840a8e9c3)

* Update midround.yml

(cherry picked from commit b78d24ce6bb7f8cb4a85a89f6f974fbce1d83055)

* Update ghost_roles.yml

(cherry picked from commit 22df7509b5c5113afc8f1ba168223b0756de5d47)

* Solarian Alliance Content (#1851)

# Description

This PR acts as a proper introduction to players for the Sol Aliance
faction, a major antagonist group from my old home server's lore. To do
so, I've ported a large number of assets from Aurora.3 to this repo, as
well as created a new Midround Antagonist called "Deserters", which
shows off this group to players.

<details><summary><h1>Media</h1></summary>
<p>

![image](https://github.com/user-attachments/assets/c57f48d7-ecf9-4099-998f-4ea3e3e95008)

![image](https://github.com/user-attachments/assets/b0fcd092-4072-4c2f-a61d-9118bc1ab140)

![image](https://github.com/user-attachments/assets/9fc2049f-1197-4eb8-93ea-7c2be2531085)

</p>
</details>

# Changelog

🆑
- add: A new Midround Antagonist has been added to the game. The
"Solarian Deserters" are a group of highly trained soldiers who haven't
been paid for far too long, whom have come to the station to loot it for
everything valuable.
- add: Lore guidebook entry for the Solarian Alliance, a majorly
antagonistic faction.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced the "Solarian Navy Deserter" role with detailed
localization, objectives, and traits.
- Launched a dynamic shuttle event featuring interactive elements like
secure doors, turret defenses, and specialized equipment.
- Expanded gameplay with new storage options, tactical helmets,
uniforms, identification cards, and door access tailored for the Sol
Alliance.
- Added a new NPC faction and game events enhancing combat and role-play
dynamics.
- Introduced various clothing items and uniforms associated with the Sol
Alliance, including tactical gear and dress uniforms.
- Added new metadata and structured entries for various clothing and
equipment assets.

- **Documentation**
- Enriched lore and guidebook entries with expanded nation details,
emphasizing the Solarian Alliance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
(cherry picked from commit 6d919038f3845bb4008a17e1d068196779162f4a)

* Automatic Changelog Update (#1851)

(cherry picked from commit ffaf99ca4b01e63f6bb98731e630f066fad25909)

* Supermatter Atmos Mapping Assets (#1859)

# Description

This adds "High Flow" variants of all existing atmos devices, which are
useful for supermatter engines. I also added the ability for
FixAtmosMarkers to optionally accept a gas mixture directly, as opposed
to the stupid hardcoded gas mixes that they were limited to using
previously.

# Changelog

🆑
- add: Added high pressure variants of atmos devices intended for
supermatter engines.
- add: Added engineering locked high security doors, also for use in
supermatter engines.
- add: Fix Atmos markers can now accept a gas mixture directly for
modifying their tile.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Enhanced atmospheric commands now dynamically use specific gas
mixtures for more flexible performance.
- Introduced a new supermatter coolant entity, offering an alternative
liquid nitrogen-like option.
- Added several high-pressure and high-flow gas components, including
pumps, filters, mixers, vents, and injectors.
- Updated map elements by refining door access prototypes and labels for
improved in-game clarity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

(cherry picked from commit b9c3c8b366c15b5f09cfd641c90b09254f06de94)

* Automatic Changelog Update (#1859)

(cherry picked from commit 468a263863f17772e6233032e5099d6c83764616)

* Rerotate Arena (#1853)

<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Description

<!--
Explain this PR in as much detail as applicable

Some example prompts to consider:
How might this affect the game? The codebase?
What might be some alternatives to this?
How/Who does this benefit/hurt [the game/codebase]?
-->

Rerotates Arena. Adds an AI satellite, and maps a few station maps,
cameras, and psionic registry computers.
Adds myself as the maintainer for it. Do note that I am still learning
how to map. Please state any changes that is wished to be seen before it
is ready to merge.
It is 3AM, I need sleep.

---

# TODO

<!--
A list of everything you have to do before this PR is "complete"
You probably won't have to complete everything before merging but it's
good to leave future references
-->

- [x] Space cleanup
- [x] Psionic Registry Computers
- [x] AI Satellite & Related Stuff

---

<!--
This is default collapsed, readers click to expand it and see all your
media
The PR media section can get very large at times, so this is a good way
to keep it clean
The title is written using HTML tags
The title must be within the <summary> tags or you won't see it
-->

<details><summary><h1>Media</h1></summary>
<p>

![Arena](https://github.com/user-attachments/assets/883ce04f-70c4-4628-8b2c-2ad75439421a)

</p>
</details>

---

# Changelog

<!--
You can add an author after the `🆑` to change the name that appears
in the changelog (ex: `🆑 Death`)
Leaving it blank will default to your GitHub display name
This includes all available types for the changelog
-->

🆑
- add: Arena is back

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- The Arena map is now reactivated with updated configurations and
active maintenance.
- Enhanced gameplay details and role assignments have been enabled for a
more engaging experience.
- The configuration for the Arena map has been fully activated,
including various roles and attributes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

(cherry picked from commit 534a058eb489ceb4abaadac7e4943ed2baaa8c67)

* Automatic Changelog Update (#1853)

(cherry picked from commit 3b30c0a1fe8dc5e10c3cb0536e26d101893663a2)

* Port Grab Intent From Goob (#1856)

# Description
After months, Grab intent is finally ported to EE, as a result of a 4
hour Adderall induced code binge.

##  This PR is more shit than code.
Required for CQC, an attempt to port that will come later.
@Erisfiregamer1 requires this for
[Changelings](https://github.com/Simple-Station/Einstein-Engines/pull/1855).

Thanks to Gus for the Goobstation pr, and to Spatison for the original
port on WWDP
Tests on my local repo worked.
# TODO
* [ ]  Await review
* [ ]  pain

# Media

![dqt2naw4ox651](https://github.com/user-attachments/assets/9a97cea7-d2c8-47df-85e1-de243409bbe6)
# Changelog
🆑 Eagle

* add: Ported Grab Intent from Goobstation

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Enhanced pulling and grabbing interactions now feature multiple stages
that impact how actions and collisions feel.
- Virtual item handling during throws and drops has been refined for
more dynamic in-game outcomes.
- Alert visuals have been updated to provide nuanced feedback depending
on the intensity of pulls and grabs.
- Player movement and breathing mechanics have been fine-tuned for more
realistic behavior.
- New localization strings deliver clearer, context-sensitive
notifications for grab-related actions.
- Introduced a new component and system for managing entities thrown
while grabbed, including damage handling and visual effects.
- New event classes enhance interaction handling for virtual items
during grabbing actions.

- **Bug Fixes**
- Improved logic for stopping pull actions to ignore grab states when
necessary.

- **Chores**
- Added metadata for new textures related to alerts in the user
interface.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: VMSolidus <evilexecutive@gmail.com>
(cherry picked from commit 18722e86f3190632026127af111dcc0d10d4af49)

* Automatic Changelog Update (#1856)

(cherry picked from commit 309ab74013fed2be64d9fb0457631210d860644b)

* Port Role Types (#1860)

Ports https://github.com/space-wizards/space-station-14/pull/33420

This is the last requirement before we can start mass-porting new
antags.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Enhanced role displays in player and character interfaces with a new
"Role Type" column.
  - Updated admin overlay options, including a classic antagonist label.
- Expanded ghost role behaviors for various entities, offering more
dynamic gameplay.
- New localization entries for role types and UI settings for sounds and
layout customization.
- Added new mind roles and role types, improving role management and
gameplay interactions.
- Introduced new events for player spawning processes to enhance
gameplay scenarios.

- **Refactor**
- Streamlined role management and update processes for improved
reliability and performance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: DrSmugleaf <drsmugleaf@gmail.com>
(cherry picked from commit e10c51cdb39845ed1f2bb9b08f0b226cefbd402e)

* Rock And Stone

<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Description

<!--
Explain this PR in as much detail as applicable

Some example prompts to consider:
How might this affect the game? The codebase?
What might be some alternatives to this?
How/Who does this benefit/hurt [the game/codebase]?
-->

Ports Lavaland and required systems from Goobstation.

---

# TODO

<!--
A list of everything you have to do before this PR is "complete"
You probably won't have to complete everything before merging but it's
good to leave future references
-->

- [X] Port over _Lavaland
- [x] Port over required codepatches
- [-] Test locally (Should be fine)
- [X] Pass tests

---

<!--
This is default collapsed, readers click to expand it and see all your
media
The PR media section can get very large at times, so this is a good way
to keep it clean
The title is written using HTML tags
The title must be within the <summary> tags or you won't see it
-->

<details><summary><h1>Media</h1></summary>
<p>

![No](https://github.com/user-attachments/assets/cfede61a-80c9-4ecd-9473-5170d080ac34)

</p>
</details>

---

# Changelog

<!--
You can add an author after the `🆑` to change the name that appears
in the changelog (ex: `🆑 Death`)
Leaving it blank will default to your GitHub display name
This includes all available types for the changelog
-->

🆑
- add: Lavaland has been ported!

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a dynamic mining voucher interface allowing players to
redeem various mining kits.
- Enhanced shuttle docking systems with an updated console interface for
smoother FTL transitions.
- Added immersive boss music management for enhanced in-game boss
encounters.
- Expanded Lavaland gameplay with new procedural map generation, weather
events, and storm scheduling.
- Integrated new interactive commands and UI improvements for advanced
weapon upgrades, Hierophant boss actions, and research features.
- Added new components and systems for managing various gameplay
elements, including damage squares, tendrils, and block charges.
- Implemented new localization entries for improved player experience
across various game features.
- Introduced new components for managing mining vendors and vouchers,
enhancing the interaction with mining kits.
- Added a new system for managing the deployment of shelter capsules in
the Lavaland environment.

- **Tests**
- Added integration tests to validate Lavaland planet generation and map
initialization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: VMSolidus <evilexecutive@gmail.com>

(cherry picked from commit f2f5d4610db795a124b37780230eec5d5ca0264a)

* Automatic Changelog Update (#1844)

(cherry picked from commit 990878b9ed60b4e22388038b63714ec2dc693bbf)

* fixs

* fix

* fuck

---------

Co-authored-by: Tonk-GCR <190437025+Tonk-GCR@users.noreply.github.com>
Co-authored-by: SimpleStation Changelogs <SimpleStation14@users.noreply.github.com>
Co-authored-by: Raikyr0 <kurohana@hotmail.com.au>
Co-authored-by: SixplyDev <einlichen@gmail.com>
Co-authored-by: Timfa <timfalken@hotmail.com>
Co-authored-by: Errant <35878406+errant-4@users.noreply.github.com>
Co-authored-by: sleepyyapril <123355664+sleepyyapril@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: astriloqua <129308840+astriloqua@users.noreply.github.com>
Co-authored-by: Eagle-0 <114363363+Eagle-0@users.noreply.github.com>
Co-authored-by: Eris <eris@erisws.com>
This commit is contained in:
Spatison
2025-03-03 19:02:48 +03:00
committed by GitHub
parent feccf10a0a
commit 8a95ee85bf
883 changed files with 107083 additions and 6465 deletions

View File

@@ -1,73 +1,100 @@
using System.Linq;
using System.Numerics;
using Content.Client.Administration.Systems;
using Content.Shared.CCVar;
using Content.Shared.Mind;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Content.Client.Administration
namespace Content.Client.Administration;
internal sealed class AdminNameOverlay : Overlay
{
internal sealed class AdminNameOverlay : Overlay
[Dependency] private readonly IConfigurationManager _config = default!;
private readonly AdminSystem _system;
private readonly IEntityManager _entityManager;
private readonly IEyeManager _eyeManager;
private readonly EntityLookupSystem _entityLookup;
private readonly IUserInterfaceManager _userInterfaceManager;
private readonly Font _font;
//TODO make this adjustable via GUI
private readonly ProtoId<RoleTypePrototype>[] _filter =
["SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"];
private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic");
private readonly Color _antagColorClassic = Color.OrangeRed;
public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup, IUserInterfaceManager userInterfaceManager)
{
private readonly AdminSystem _system;
private readonly IEntityManager _entityManager;
private readonly IEyeManager _eyeManager;
private readonly EntityLookupSystem _entityLookup;
private readonly Font _font;
IoCManager.InjectDependencies(this);
public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup)
_system = system;
_entityManager = entityManager;
_eyeManager = eyeManager;
_entityLookup = entityLookup;
_userInterfaceManager = userInterfaceManager;
ZIndex = 200;
_font = new VectorFont(resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
}
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
protected override void Draw(in OverlayDrawArgs args)
{
var viewport = args.WorldAABB;
//TODO make this adjustable via GUI
var classic = _config.GetCVar(CCVars.AdminOverlayClassic);
foreach (var playerInfo in _system.PlayerList)
{
_system = system;
_entityManager = entityManager;
_eyeManager = eyeManager;
_entityLookup = entityLookup;
ZIndex = 200;
_font = new VectorFont(resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
}
var entity = _entityManager.GetEntity(playerInfo.NetEntity);
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
protected override void Draw(in OverlayDrawArgs args)
{
var viewport = args.WorldAABB;
foreach (var playerInfo in _system.PlayerList)
// Otherwise the entity can not exist yet
if (entity == null || !_entityManager.EntityExists(entity))
{
var entity = _entityManager.GetEntity(playerInfo.NetEntity);
// Otherwise the entity can not exist yet
if (entity == null || !_entityManager.EntityExists(entity))
{
continue;
}
// if not on the same map, continue
if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != _eyeManager.CurrentMap)
{
continue;
}
var aabb = _entityLookup.GetWorldAABB(entity.Value);
// if not on screen, continue
if (!aabb.Intersects(in viewport))
{
continue;
}
var lineoffset = new Vector2(0f, 11f);
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
if (playerInfo.Antag)
{
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), "ANTAG", Color.OrangeRed);
}
args.ScreenHandle.DrawString(_font, screenCoordinates+lineoffset, playerInfo.Username, playerInfo.Connected ? Color.Yellow : Color.White);
args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, playerInfo.Connected ? Color.Aquamarine : Color.White);
continue;
}
// if not on the same map, continue
if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
{
continue;
}
var aabb = _entityLookup.GetWorldAABB(entity.Value);
// if not on screen, continue
if (!aabb.Intersects(in viewport))
{
continue;
}
var uiScale = _userInterfaceManager.RootControl.UIScale;
var lineoffset = new Vector2(0f, 11f) * uiScale;
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
if (classic && playerInfo.Antag)
{
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), _antagLabelClassic, uiScale, _antagColorClassic);
}
else if (!classic && _filter.Contains(playerInfo.RoleProto.ID))
{
var label = Loc.GetString(playerInfo.RoleProto.Name).ToUpper();
var color = playerInfo.RoleProto.Color;
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), label, uiScale, color);
}
args.ScreenHandle.DrawString(_font, screenCoordinates + lineoffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
}
}
}

View File

@@ -1,6 +1,7 @@
using Content.Client.Administration.Managers;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
namespace Content.Client.Administration.Systems
{
@@ -11,6 +12,7 @@ namespace Content.Client.Administration.Systems
[Dependency] private readonly IClientAdminManager _adminManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
private AdminNameOverlay _adminNameOverlay = default!;
@@ -19,7 +21,7 @@ namespace Content.Client.Administration.Systems
private void InitializeOverlay()
{
_adminNameOverlay = new AdminNameOverlay(this, EntityManager, _eyeManager, _resourceCache, _entityLookup);
_adminNameOverlay = new AdminNameOverlay(this, EntityManager, _eyeManager, _resourceCache, _entityLookup, _userInterfaceManager);
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
}

View File

@@ -197,6 +197,7 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
Header.Character => Compare(x.CharacterName, y.CharacterName),
Header.Job => Compare(x.StartingJob, y.StartingJob),
Header.Antagonist => x.Antag.CompareTo(y.Antag),
Header.RoleType => Compare(x.RoleProto.Name , y.RoleProto.Name),
Header.Playtime => TimeSpan.Compare(x.OverallPlaytime ?? default, y.OverallPlaytime ?? default),
_ => 1
};

View File

@@ -23,6 +23,12 @@
SizeFlagsStretchRatio="2"
HorizontalExpand="True"
ClipText="True"/>
<customControls:VSeparator/>
<Label Name="RoleTypeLabel"
SizeFlagsStretchRatio="2"
HorizontalExpand="True"
ClipText="True"/>
<customControls:VSeparator/>
<Label Name="OverallPlaytimeLabel"
SizeFlagsStretchRatio="2"
HorizontalExpand="True"

View File

@@ -23,6 +23,8 @@ public sealed partial class PlayerTabEntry : PanelContainer
if (player.IdentityName != player.CharacterName)
CharacterLabel.Text += $" [{player.IdentityName}]";
AntagonistLabel.Text = Loc.GetString(player.Antag ? "player-tab-is-antag-yes" : "player-tab-is-antag-no");
RoleTypeLabel.Text = Loc.GetString(player.RoleProto.Name);
RoleTypeLabel.FontColorOverride = player.RoleProto.Color;
BackgroundColorPanel.PanelOverride = styleBoxFlat;
OverallPlaytimeLabel.Text = player.PlaytimeString;
PlayerEntity = player.NetEntity;

View File

@@ -31,6 +31,14 @@
ClipText="True"
Text="{Loc player-tab-antagonist}"
MouseFilter="Pass"/>
<cc:VSeparator/>
<Label Name="RoleTypeLabel"
SizeFlagsStretchRatio="2"
HorizontalExpand="True"
ClipText="True"
Text="{Loc player-tab-roletype}"
MouseFilter="Pass"/>
<cc:VSeparator/>
<Label Name="PlaytimeLabel"
SizeFlagsStretchRatio="2"
HorizontalExpand="True"

View File

@@ -19,6 +19,7 @@ public sealed partial class PlayerTabHeader : Control
CharacterLabel.OnKeyBindDown += CharacterClicked;
JobLabel.OnKeyBindDown += JobClicked;
AntagonistLabel.OnKeyBindDown += AntagonistClicked;
RoleTypeLabel.OnKeyBindDown += RoleTypeClicked;
PlaytimeLabel.OnKeyBindDown += PlaytimeClicked;
}
@@ -30,6 +31,7 @@ public sealed partial class PlayerTabHeader : Control
Header.Character => CharacterLabel,
Header.Job => JobLabel,
Header.Antagonist => AntagonistLabel,
Header.RoleType => RoleTypeLabel,
Header.Playtime => PlaytimeLabel,
_ => throw new ArgumentOutOfRangeException(nameof(header), header, null)
};
@@ -41,6 +43,7 @@ public sealed partial class PlayerTabHeader : Control
CharacterLabel.Text = Loc.GetString("player-tab-character");
JobLabel.Text = Loc.GetString("player-tab-job");
AntagonistLabel.Text = Loc.GetString("player-tab-antagonist");
RoleTypeLabel.Text = Loc.GetString("player-tab-roletype");
PlaytimeLabel.Text = Loc.GetString("player-tab-playtime");
}
@@ -75,6 +78,11 @@ public sealed partial class PlayerTabHeader : Control
HeaderClicked(args, Header.Antagonist);
}
private void RoleTypeClicked(GUIBoundKeyEventArgs args)
{
HeaderClicked(args, Header.RoleType);
}
private void PlaytimeClicked(GUIBoundKeyEventArgs args)
{
HeaderClicked(args, Header.Playtime);
@@ -90,6 +98,7 @@ public sealed partial class PlayerTabHeader : Control
CharacterLabel.OnKeyBindDown -= CharacterClicked;
JobLabel.OnKeyBindDown -= JobClicked;
AntagonistLabel.OnKeyBindDown -= AntagonistClicked;
RoleTypeLabel.OnKeyBindDown -= RoleTypeClicked;
PlaytimeLabel.OnKeyBindDown -= PlaytimeClicked;
}
}
@@ -100,6 +109,7 @@ public sealed partial class PlayerTabHeader : Control
Character,
Job,
Antagonist,
RoleType,
Playtime
}
}

View File

@@ -0,0 +1,42 @@
using Content.Shared.Botany.PlantAnalyzer;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client.Botany.PlantAnalyzer;
[UsedImplicitly]
public sealed class PlantAnalyzerBoundUserInterface : BoundUserInterface
{
[ViewVariables]
private PlantAnalyzerWindow? _window;
public PlantAnalyzerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<PlantAnalyzerWindow>();
_window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
_window.Print.OnPressed += _ => Print();
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
if (_window is null
|| message is not PlantAnalyzerScannedUserMessage cast)
return;
_window.Populate(cast);
}
private void Print()
{
SendMessage(new PlantAnalyzerPrintMessage());
if (_window is null)
return;
_window.Print.Disabled = true;
}
}

View File

@@ -0,0 +1,121 @@
<controls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinWidth="320"
MaxWidth="320">
<ScrollContainer
Margin="5 5 5 5"
ReturnMeasure="True"
VerticalExpand="True"
HorizontalExpand="False">
<BoxContainer
Name="RootContainer"
VerticalExpand="True"
Orientation="Vertical">
<BoxContainer
Name="DataContainer"
Margin="0 0 0 5"
Orientation="Vertical">
<BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64" />
<TextureRect Name="NoDataIcon" Access="Public" SetSize="64 64" Visible="false" Stretch="KeepAspectCentered" TexturePath="/Textures/Interface/Misc/health_analyzer_out_of_range.png"/>
<BoxContainer Margin="5 0 0 0" Orientation="Vertical" VerticalAlignment="Top">
<RichTextLabel Name="SeedLabel" SetWidth="150"
Text="{Loc 'generic-unknown'}"/>
<Label Name="ContainerLabel" VerticalAlignment="Top" StyleClasses="LabelSubText"
Text="{Loc 'generic-unknown'}"/>
</BoxContainer>
<Label Margin="0 0 5 0" HorizontalExpand="True" HorizontalAlignment="Right" VerticalExpand="True"
VerticalAlignment="Top" Name="ScanModeLabel"
Text="{Loc 'health-analyzer-window-entity-unknown-text'}" />
</BoxContainer>
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer Name="PlantDataTags" Margin="5 5 0 0" Orientation="Horizontal" Visible="false">
<RichTextLabel Margin="0 0 8 0" Name="Alive" Visible="false" Text="{Loc 'plant-analyzer-component-alive'}" />
<RichTextLabel Margin="0 0 8 0" Name="Dead" Visible="false" Text="{Loc 'plant-analyzer-component-dead'}" />
<RichTextLabel Margin="0 0 8 0" Name="Unviable" Visible="false" Text="{Loc 'plant-analyzer-component-unviable'}" />
<RichTextLabel Margin="0 0 8 0" Name="Kudzu" Visible="false" Text="{Loc 'plant-analyzer-component-kudzu'}" />
<RichTextLabel Margin="0 0 8 0" Name="Mutating" Visible="false" Text="{Loc 'plant-analyzer-component-mutating'}" />
</BoxContainer>
<GridContainer Name="PlantDataGrid" Margin="0 0 0 5" Columns="6" Visible="false">
<Label Text=" · " />
<Label Text="{Loc 'plant-analyzer-component-health'}" />
<Label SetWidth="8" />
<Label HorizontalAlignment="Right" Name="Health" />
<Label Text=" / " />
<Label HorizontalAlignment="Right" Name="Endurance" />
<Label Text=" · " />
<Label Text="{Loc 'plant-analyzer-component-age'}" />
<Label SetWidth="8" />
<Label HorizontalAlignment="Right" Name="Age" />
<Label Text=" / " />
<Label HorizontalAlignment="Right" Name="Lifespan" />
</GridContainer>
<PanelContainer Name="PlantDataDivider" Visible="false" StyleClasses="LowDivider" />
<GridContainer Name="ContainerGrid" Margin="0 5" Columns="8" Visible="false">
<!-- Max values from `PlantHolderSystem.CheckLevelSanity` -->
<Label Text=" · " />
<Label Text="{Loc 'plant-analyzer-component-water'}" />
<Label SetWidth="8" />
<Label FontColorOverride="cyan" HorizontalAlignment="Right" Name="WaterLevelLabel" />
<Label Text=" / " />
<Label HorizontalAlignment="Right" Text="100.00" />
<Label Margin="12 0 0 0" Name="GtFieldIfTolerances1" />
<Label HorizontalAlignment="Right" Name="WaterConsumptionLabel" />
<Label Text=" · " />
<Label Text="{Loc 'plant-analyzer-component-nutrition'}" />
<Label SetWidth="8" />
<Label FontColorOverride="orange" HorizontalAlignment="Right" Name="NutritionLevelLabel" />
<Label Text=" / " />
<Label HorizontalAlignment="Right" Text="100.00" />
<Label Margin="12 0 0 0" Name="GtFieldIfTolerances2" />
<Label HorizontalAlignment="Right" Name="NutritionConsumptionLabel" />
<Label Text=" · " />
<Label Text="{Loc 'plant-analyzer-component-toxins'}" />
<Label SetWidth="8" />
<Label FontColorOverride="yellowgreen" HorizontalAlignment="Right" Name="ToxinsLabel" />
<Label Text=" / " />
<Label HorizontalAlignment="Right" Text="100.00" />
<Label Margin="12 0 0 0" Name="LtFieldIfTolerances1" />
<Label HorizontalAlignment="Right" Name="ToxinsResistanceLabel" />
<Label Text=" · " />
<Label Text="{Loc 'plant-analyzer-component-pests'}" />
<Label SetWidth="8" />
<Label FontColorOverride="magenta" HorizontalAlignment="Right" Name="PestLevelLabel" />
<Label Text=" / " />
<Label HorizontalAlignment="Right" Text="10.00" />
<Label Margin="12 0 0 0" Name="LtFieldIfTolerances2" />
<Label HorizontalAlignment="Right" Name="PestResistanceLabel" />
<Label Text=" · " />
<Label Text="{Loc 'plant-analyzer-component-weeds'}" />
<Label SetWidth="8" />
<Label FontColorOverride="red" HorizontalAlignment="Right" Name="WeedLevelLabel" />
<Label Text=" / " />
<Label HorizontalAlignment="Right" Text="10.00" />
<Label Margin="12 0 0 0" Name="LtFieldIfTolerances3" />
<Label HorizontalAlignment="Right" Name="WeedResistanceLabel" />
</GridContainer>
<PanelContainer Name="ContainerDivider" Visible="false" StyleClasses="LowDivider" />
<BoxContainer Name="ChemicalsInWaterBox" Visible="false" Orientation="Horizontal" Margin="5">
<RichTextLabel Name="ChemicalsInWaterLabel" SetWidth="290" />
</BoxContainer>
<PanelContainer Name="ChemicalsInWaterDivider" Visible="false" StyleClasses="LowDivider" />
<BoxContainer Name="EnvironmentBox" Visible="false" Orientation="Horizontal" Margin="5">
<RichTextLabel Name="EnvironmentLabel" SetWidth="290" />
</BoxContainer>
<PanelContainer Name="EnvironmentDivider" Visible="false" StyleClasses="LowDivider" />
<BoxContainer Name="ProduceBox" Visible="false" Orientation="Horizontal" Margin="5">
<RichTextLabel Name="ProduceLabel" SetWidth="290" />
</BoxContainer>
<PanelContainer Name="ProduceDivider" Visible="false" StyleClasses="LowDivider" />
<Button Name="Print"
TextAlign="Center"
HorizontalExpand="True"
Access="Public"
Disabled="True"
Margin="0 5 0 0"
Text="{Loc 'plant-analyzer-print'}" />
</BoxContainer>
</BoxContainer>
</ScrollContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,207 @@
using System.Linq;
using Content.Client.UserInterface.Controls;
using Content.Shared.Botany.PlantAnalyzer;
using Content.Shared.IdentityManagement;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Botany.PlantAnalyzer;
[GenerateTypedNameReferences]
public sealed partial class PlantAnalyzerWindow : FancyWindow
{
private readonly IEntityManager _entityManager;
private readonly IPrototypeManager _prototypeManager;
private readonly IGameTiming _gameTiming;
public PlantAnalyzerWindow()
{
RobustXamlLoader.Load(this);
var dependencies = IoCManager.Instance!;
_entityManager = dependencies.Resolve<IEntityManager>();
_prototypeManager = dependencies.Resolve<IPrototypeManager>();
_gameTiming = dependencies.Resolve<IGameTiming>();
}
public void Populate(PlantAnalyzerScannedUserMessage msg)
{
Print.Disabled = !msg.ScanMode.GetValueOrDefault(false)
|| msg.PrintReadyAt.GetValueOrDefault(TimeSpan.MaxValue) > _gameTiming.CurTime
|| msg.PlantData is null;
if (!_entityManager.TryGetEntity(msg.TargetEntity, out var target) || target is null)
return;
// Section 1: Icon and basic information.
SpriteView.SetEntity(target.Value);
SpriteView.Visible = msg.ScanMode.HasValue && msg.ScanMode.Value;
NoDataIcon.Visible = !SpriteView.Visible;
ScanModeLabel.Text = msg.ScanMode.HasValue
? msg.ScanMode.Value
? Loc.GetString("plant-analyzer-window-scan-mode-active")
: Loc.GetString("plant-analyzer-window-scan-mode-inactive")
: Loc.GetString("plant-analyzer-window-entity-unknown-text");
ScanModeLabel.FontColorOverride = msg.ScanMode.HasValue && msg.ScanMode.Value ? Color.Green : Color.Red;
SeedLabel.Text = msg.PlantData == null
? Loc.GetString("plant-analyzer-component-no-seed")
: Loc.GetString(msg.PlantData.SeedDisplayName);
ContainerLabel.Text = _entityManager.HasComponent<MetaDataComponent>(target.Value)
? Identity.Name(target.Value, _entityManager)
: Loc.GetString("generic-unknown");
// Section 2: Information regarding the plant.
if (msg.PlantData is not null)
{
Health.Text = msg.PlantData.Health.ToString("0.00");
Endurance.Text = msg.PlantData.Endurance.ToString("0.00");
Age.Text = msg.PlantData.Age.ToString("0.00");
Lifespan.Text = msg.PlantData.Lifespan.ToString("0.00");
// These mostly exists to prevent shifting of the text.
Dead.Visible = msg.PlantData.Dead;
Alive.Visible = !Dead.Visible;
Unviable.Visible = !msg.PlantData.Viable;
Mutating.Visible = msg.PlantData.Mutating;
Kudzu.Visible = msg.PlantData.Kudzu;
PlantDataGrid.Visible = true;
}
else
{
PlantDataGrid.Visible = false;
}
PlantDataTags.Visible = PlantDataGrid.Visible;
PlantDataDivider.Visible = PlantDataGrid.Visible;
// Section 3: Input
if (msg.TrayData is not null)
{
WaterLevelLabel.Text = msg.TrayData.WaterLevel.ToString("0.00");
NutritionLevelLabel.Text = msg.TrayData.NutritionLevel.ToString("0.00");
ToxinsLabel.Text = msg.TrayData.Toxins.ToString("0.00");
PestLevelLabel.Text = msg.TrayData.PestLevel.ToString("0.00");
WeedLevelLabel.Text = msg.TrayData.WeedLevel.ToString("0.00");
// Section 3.1: Tolerances part 1.
if (msg.TolerancesData is not null)
{
GtFieldIfTolerances1.Text = ">";
LtFieldIfTolerances1.Text = "<";
WaterConsumptionLabel.Text = msg.TolerancesData.WaterConsumption.ToString("0.00");
NutritionConsumptionLabel.Text = msg.TolerancesData.NutrientConsumption.ToString("0.00");
// Technically would be "x + epsilon" for toxin and pest.
// But it makes no difference here since I only display two digits.
ToxinsResistanceLabel.Text = msg.TolerancesData.ToxinsTolerance.ToString("0.00");
PestResistanceLabel.Text = msg.TolerancesData.PestTolerance.ToString("0.00");
WeedResistanceLabel.Text = msg.TolerancesData.WeedTolerance.ToString("0.00");
}
else
{
GtFieldIfTolerances1.Text = "";
LtFieldIfTolerances1.Text = "";
WaterConsumptionLabel.Text = "";
NutritionConsumptionLabel.Text = "";
ToxinsResistanceLabel.Text = "";
PestResistanceLabel.Text = "";
WeedResistanceLabel.Text = "";
}
GtFieldIfTolerances2.Text = GtFieldIfTolerances1.Text;
LtFieldIfTolerances2.Text = LtFieldIfTolerances1.Text;
LtFieldIfTolerances3.Text = LtFieldIfTolerances1.Text;
ContainerGrid.Visible = true;
}
else
{
ContainerGrid.Visible = false;
}
ContainerDivider.Visible = ContainerGrid.Visible;
// Section 3.5: They are putting chemicals in the water!
if (msg.TrayData?.Chemicals != null)
{
var count = msg.TrayData.Chemicals.Count;
var holder = ContainerLabel.Text;
var chemicals = PlantAnalyzerLocalizationHelper.ChemicalsToLocalizedStrings(msg.TrayData.Chemicals, _prototypeManager);
if (count == 0)
ChemicalsInWaterLabel.Text = Loc.GetString("plant-analyzer-soil-empty", ("holder", holder));
else
ChemicalsInWaterLabel.Text = Loc.GetString("plant-analyzer-soil", ("count", count), ("holder", holder), ("chemicals", chemicals));
ChemicalsInWaterBox.Visible = true;
}
else
{
ChemicalsInWaterBox.Visible = false;
}
ChemicalsInWaterDivider.Visible = ChemicalsInWaterBox.Visible;
// Section 4: Tolerances part 2.
if (msg.TolerancesData is not null)
{
(string, string)[] parameters = [
("seedName", SeedLabel.Text),
("gases", PlantAnalyzerLocalizationHelper.GasesToLocalizedStrings(msg.TolerancesData.ConsumeGasses, _prototypeManager)),
("kpa", msg.TolerancesData.IdealPressure.ToString("0.00")),
("kpaTolerance", msg.TolerancesData.PressureTolerance.ToString("0.00")),
("temp", msg.TolerancesData.IdealHeat.ToString("0.00")),
("tempTolerance", msg.TolerancesData.HeatTolerance.ToString("0.00")),
("lightLevel", msg.TolerancesData.IdealLight.ToString("0.00")),
("lightTolerance", msg.TolerancesData.LightTolerance.ToString("0.00"))
];
EnvironmentLabel.Text = msg.TolerancesData.ConsumeGasses.Count == 0
? msg.TolerancesData.IdealHeat - msg.TolerancesData.HeatTolerance <= 0f && msg.TolerancesData.IdealPressure - msg.TolerancesData.PressureTolerance <= 0f
? Loc.GetString("plant-analyzer-component-environment-void", [.. parameters])
: Loc.GetString("plant-analyzer-component-environment", [.. parameters])
: Loc.GetString("plant-analyzer-component-environment-gas", [.. parameters]);
EnvironmentBox.Visible = true;
}
else
{
EnvironmentBox.Visible = false;
}
EnvironmentDivider.Visible = EnvironmentBox.Visible;
// Section 5: Output
if (msg.ProduceData is not null)
{
var gases = PlantAnalyzerLocalizationHelper.GasesToLocalizedStrings(msg.ProduceData.ExudeGasses, _prototypeManager);
var (produce, producePlural) = PlantAnalyzerLocalizationHelper.ProduceToLocalizedStrings(msg.ProduceData.Produce, _prototypeManager);
var chemicals = PlantAnalyzerLocalizationHelper.ChemicalsToLocalizedStrings(msg.ProduceData.Chemicals, _prototypeManager);
(string, object)[] parameters = [
("yield", msg.ProduceData.Yield),
("gasCount", msg.ProduceData.ExudeGasses.Count),
("gases", gases),
("potency", Loc.GetString(msg.ProduceData.Potency)),
("seedless", msg.ProduceData.Seedless),
("firstProduce", msg.ProduceData.Produce.FirstOrDefault() ?? ""),
("produce", produce),
("producePlural", producePlural),
("chemCount", msg.ProduceData.Chemicals.Count),
("chemicals", chemicals),
("nothing", "")
];
ProduceLabel.Text = Loc.GetString("plant-analyzer-output", [.. parameters]);
ProduceBox.Visible = true;
}
else
{
ProduceBox.Visible = false;
}
ProduceDivider.Visible = ProduceBox.Visible;
}
}

View File

@@ -22,13 +22,10 @@ using Content.Client.Voting;
using Content.Shared.Administration.Logs;
using Content.Client.Lobby;
using Content.Client.Players.RateLimiting;
using Content.Client.Replay;
using Content.Shared.Administration.Managers;
using Content.Shared.Chat;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Players.RateLimiting;
using Robust.Client.GameObjects;
namespace Content.Client.IoC
{
internal static class ClientContentIoC

View File

@@ -1,11 +1,15 @@
using System.Linq;
using Content.Client.CharacterInfo;
using Content.Client.Gameplay;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Character.Controls;
using Content.Client.UserInterface.Systems.Character.Windows;
using Content.Client.UserInterface.Systems.Objectives.Controls;
using Content.Shared.Input;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Player;
@@ -13,6 +17,7 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input.Binding;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using static Content.Client.CharacterInfo.CharacterInfoSystem;
using static Robust.Client.UserInterface.Controls.BaseButton;
@@ -22,10 +27,25 @@ namespace Content.Client.UserInterface.Systems.Character;
[UsedImplicitly]
public sealed class CharacterUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>, IOnSystemChanged<CharacterInfoSystem>
{
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly ILogManager _logMan = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[UISystemDependency] private readonly CharacterInfoSystem _characterInfo = default!;
[UISystemDependency] private readonly SpriteSystem _sprite = default!;
private ISawmill _sawmill = default!;
public override void Initialize()
{
base.Initialize();
_sawmill = _logMan.GetSawmill("character");
SubscribeNetworkEvent<MindRoleTypeChangedEvent>(OnRoleTypeChanged);
}
private CharacterWindow? _window;
private MenuButton? CharacterButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.CharacterButton;
@@ -108,6 +128,9 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
var (entity, job, objectives, briefing, entityName) = data;
_window.SpriteView.SetEntity(entity);
UpdateRoleType();
_window.NameLabel.Text = entityName;
_window.SubText.Text = job;
_window.Objectives.RemoveAllChildren();
@@ -165,6 +188,37 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
_window.RolePlaceholder.Visible = briefing == null && !controls.Any() && !objectives.Any();
}
private void OnRoleTypeChanged(MindRoleTypeChangedEvent ev, EntitySessionEventArgs _)
{
UpdateRoleType();
}
private void UpdateRoleType()
{
if (_window == null || !_window.IsOpen)
return;
if (!_ent.TryGetComponent<MindContainerComponent>(_player.LocalEntity, out var container)
|| container.Mind is null)
return;
if (!_ent.TryGetComponent<MindComponent>(container.Mind.Value, out var mind))
return;
var roleText = Loc.GetString("role-type-crew-aligned-name");
var color = Color.White;
if (_prototypeManager.TryIndex(mind.RoleType, out var proto))
{
roleText = Loc.GetString(proto.Name);
color = proto.Color;
}
else
_sawmill.Error($"{_player.LocalEntity} has invalid Role Type '{mind.RoleType}'. Displaying '{roleText}' instead");
_window.RoleType.Text = roleText;
_window.RoleType.FontColorOverride = color;
}
private void CharacterDetached(EntityUid uid)
{
CloseWindow();

View File

@@ -7,6 +7,7 @@
MinHeight="545">
<ScrollContainer>
<BoxContainer Orientation="Vertical">
<Label Name="RoleType" VerticalAlignment="Top" Margin="0 6 0 10" HorizontalAlignment="Center" StyleClasses="LabelHeading" Access="Public"/>
<BoxContainer Orientation="Horizontal">
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64"/>
<BoxContainer Orientation="Vertical" VerticalAlignment="Top">

View File

@@ -0,0 +1,27 @@
using Content.Shared._DV.Salvage;
using Robust.Client.UserInterface;
namespace Content.Client._DV.Salvage.UI;
public sealed class MiningVoucherBoundUserInterface : BoundUserInterface
{
[ViewVariables]
private MiningVoucherMenu? _menu;
public MiningVoucherBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<MiningVoucherMenu>();
_menu.SetEntity(Owner);
_menu.OnSelected += i =>
{
SendMessage(new MiningVoucherSelectMessage(i));
Close();
};
}
}

View File

@@ -0,0 +1,6 @@
<ui:RadialMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
BackButtonStyleClass="RadialMenuBackButton" CloseButtonStyleClass="RadialMenuCloseButton"
VerticalExpand="True" HorizontalExpand="True" MinSize="450 450">
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False" />
</ui:RadialMenu>

View File

@@ -0,0 +1,59 @@
using Content.Client.UserInterface.Controls;
using Content.Shared._DV.Salvage.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using System.Numerics;
namespace Content.Client._DV.Salvage.UI;
[GenerateTypedNameReferences]
public sealed partial class MiningVoucherMenu : RadialMenu
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
private readonly SpriteSystem _sprite;
public event Action<int>? OnSelected;
public MiningVoucherMenu()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
_sprite = _entMan.System<SpriteSystem>();
}
public void SetEntity(EntityUid owner)
{
if (!_entMan.TryGetComponent<MiningVendorComponent>(owner, out var comp))
return;
for (int i = 0; i < comp.Kits.Count; i++)
{
var index = i; // copy so the closure doesn't borrow it
var kit = _proto.Index(comp.Kits[i]);
var button = new RadialMenuTextureButton()
{
StyleClasses = { "RadialMenuTextureButton" },
SetSize = new Vector2(64f, 64f),
ToolTip = Loc.GetString(kit.Description)
};
button.AddChild(new TextureRect()
{
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Texture = _sprite.Frame0(kit.Sprite),
TextureScale = new Vector2(2f, 2f)
});
button.OnPressed += _ => OnSelected?.Invoke(index);
Main.AddChild(button);
}
}
}

View File

@@ -0,0 +1,5 @@
using Content.Shared._Lavaland.Aggression;
namespace Content.Client._Lavaland.Aggression;
public sealed class AggressorsSystem : SharedAggressorsSystem;

View File

@@ -0,0 +1,240 @@
using Content.Client.Audio;
using Content.Shared._Lavaland.Audio;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Mobs;
using Robust.Client.Audio;
using Robust.Client.Player;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Components;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client._Lavaland.Audio;
public sealed class BossMusicSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly ContentAudioSystem _audioContent = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private static float _volumeSlider;
private Entity<AudioComponent?>? _bossMusicStream;
private BossMusicPrototype? _musicProto;
// Need how much volume to change per tick and just remove it when it drops below "0"
private readonly Dictionary<EntityUid, float> _fadingOut = new();
// Need volume change per tick + target volume.
private readonly Dictionary<EntityUid, (float VolumeChange, float TargetVolume)> _fadingIn = new();
private readonly List<EntityUid> _fadeToRemove = new();
private const float MinVolume = -32f;
private const float DefaultDuration = 2f;
public override void Initialize()
{
base.Initialize();
Subs.CVar(_configManager, CCVars.LobbyMusicVolume, BossVolumeCVarChanged, true);
SubscribeNetworkEvent<BossMusicStartupEvent>(OnBossInit);
SubscribeNetworkEvent<BossMusicStopEvent>(OnBossDefeated);
SubscribeLocalEvent<LocalPlayerDetachedEvent>(OnMindRemoved);
SubscribeLocalEvent<ActorComponent, MobStateChangedEvent>(OnPlayerDeath);
SubscribeLocalEvent<ActorComponent, EntParentChangedMessage>(OnPlayerParentChange);
SubscribeLocalEvent<RoundEndMessageEvent>(OnRoundEnd);
}
public override void Shutdown()
{
base.Shutdown();
_bossMusicStream = _audio.Stop(_bossMusicStream);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!_timing.IsFirstTimePredicted)
return;
UpdateFade(frameTime);
}
private void BossVolumeCVarChanged(float obj)
{
_volumeSlider = SharedAudioSystem.GainToVolume(obj);
if (_bossMusicStream != null && _musicProto != null)
{
_audio.SetVolume(_bossMusicStream, _musicProto.Sound.Params.Volume + _volumeSlider);
}
}
private void OnBossInit(BossMusicStartupEvent args)
{
if (_musicProto != null || _bossMusicStream != null)
return;
_audioContent.DisableAmbientMusic();
var sound = _proto.Index(args.MusicId);
_musicProto = sound;
var strim = _audio.PlayGlobal(
sound.Sound,
Filter.Local(),
false,
AudioParams.Default.WithVolume(sound.Sound.Params.Volume + _volumeSlider).WithLoop(true));
if (_musicProto.FadeIn && strim != null)
{
_bossMusicStream = (strim.Value.Entity, strim.Value.Component);
FadeIn(_bossMusicStream, strim.Value.Component, sound.FadeInTime);
}
}
private void OnBossDefeated(BossMusicStopEvent args)
{
EndAllMusic();
}
private void OnMindRemoved(LocalPlayerDetachedEvent args)
{
EndAllMusic();
}
private void OnPlayerDeath(Entity<ActorComponent> ent, ref MobStateChangedEvent args)
{
if (ent.Comp.PlayerSession == _player.LocalSession &&
args.NewMobState == MobState.Dead)
EndAllMusic();
}
/// <summary>
/// Raised when salvager escapes from lavaland (ohio reference)
/// </summary>
private void OnPlayerParentChange(Entity<ActorComponent> ent, ref EntParentChangedMessage args)
{
if (ent.Comp.PlayerSession == _player.LocalSession &&
args.OldMapId != null)
EndAllMusic();
}
private void OnRoundEnd(RoundEndMessageEvent args)
{
_bossMusicStream = _audio.Stop(_bossMusicStream);
}
private void EndAllMusic()
{
if (_musicProto == null || _bossMusicStream == null)
return;
if (_musicProto.FadeIn)
{
FadeOut(_bossMusicStream, duration: _musicProto.FadeOutTime);
}
else
{
_audio.Stop(_bossMusicStream);
}
_musicProto = null;
_bossMusicStream = null;
}
#region Fades
private void FadeOut(EntityUid? stream, AudioComponent? component = null, float duration = DefaultDuration)
{
if (stream == null || duration <= 0f || !Resolve(stream.Value, ref component))
return;
// Just in case
// TODO: Maybe handle the removals by making it seamless?
_fadingIn.Remove(stream.Value);
var diff = component.Volume - MinVolume;
_fadingOut.Add(stream.Value, diff / duration);
}
private void FadeIn(EntityUid? stream, AudioComponent? component = null, float duration = DefaultDuration)
{
if (stream == null || duration <= 0f || !Resolve(stream.Value, ref component) || component.Volume < MinVolume)
return;
_fadingOut.Remove(stream.Value);
var curVolume = component.Volume;
var change = (MinVolume - curVolume) / duration;
_fadingIn.Add(stream.Value, (change, component.Volume));
component.Volume = MinVolume;
}
private void UpdateFade(float frameTime)
{
_fadeToRemove.Clear();
foreach (var (stream, change) in _fadingOut)
{
if (!TryComp(stream, out AudioComponent? component))
{
_fadeToRemove.Add(stream);
continue;
}
var volume = component.Volume - change * frameTime;
volume = MathF.Max(MinVolume, volume);
_audio.SetVolume(stream, volume, component);
if (component.Volume.Equals(MinVolume))
{
_audio.Stop(stream);
_fadeToRemove.Add(stream);
}
}
foreach (var stream in _fadeToRemove)
{
_fadingOut.Remove(stream);
}
_fadeToRemove.Clear();
foreach (var (stream, (change, target)) in _fadingIn)
{
// Cancelled elsewhere
if (!TryComp(stream, out AudioComponent? component))
{
_fadeToRemove.Add(stream);
continue;
}
var volume = component.Volume - change * frameTime;
volume = MathF.Min(target, volume);
_audio.SetVolume(stream, volume, component);
if (component.Volume.Equals(target))
{
_fadeToRemove.Add(stream);
}
}
foreach (var stream in _fadeToRemove)
{
_fadingIn.Remove(stream);
}
}
#endregion
}

View File

@@ -0,0 +1,5 @@
using Content.Shared._Lavaland.Damage;
namespace Content.Client._Lavaland.Mobs;
public sealed class DamageSquareSystem : SharedDamageSquareSystem;

View File

@@ -0,0 +1,5 @@
using Content.Shared._Lavaland.Shuttles.Systems;
namespace Content.Client._Lavaland.Shuttles.Systems;
public sealed class DockingConsoleSystem : SharedDockingConsoleSystem;

View File

@@ -0,0 +1,39 @@
using Content.Shared._Lavaland.Shuttles;
namespace Content.Client._Lavaland.Shuttles.UI;
public sealed class DockingConsoleBoundUserInterface : BoundUserInterface
{
[ViewVariables]
private DockingConsoleWindow? _window;
public DockingConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = new DockingConsoleWindow(Owner);
_window.OnFTL += index => SendMessage(new DockingConsoleFTLMessage(index));
_window.OnShuttleCall += args => SendMessage(new DockingConsoleShuttleCheckMessage());
_window.OnClose += Close;
_window.OpenCentered();
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is DockingConsoleState cast)
_window?.UpdateState(cast);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
_window?.Orphan();
}
}

View File

@@ -0,0 +1,19 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="500 500">
<BoxContainer Orientation="Vertical">
<ScrollContainer SetHeight="256" HorizontalExpand="True">
<ItemList Name="Destinations"/> <!-- Populated from comp.Destinations -->
</ScrollContainer>
<controls:StripeBack MinSize="48 48">
<Label Text="{Loc 'shuttle-console-ftl-label'}" VerticalExpand="True" HorizontalAlignment="Center"/>
</controls:StripeBack>
<Label Name="MapFTLState" Text="{Loc 'shuttle-console-ftl-state-Available'}" VerticalAlignment="Stretch" HorizontalAlignment="Center"/>
<ProgressBar Name="FTLBar" HorizontalExpand="True" Margin="5" MinValue="0.0" MaxValue="1.0" Value="1.0" SetHeight="32"/>
<controls:StripeBack HorizontalExpand="True">
<Button Name="FTLButton" Text="{Loc 'docking-console-ftl'}" Disabled="False" SetSize="128 48" Margin="5" HorizontalAlignment="Left"/>
<Button Name="ShuttleCallButton" Text="{Loc 'docking-console-call'}" Disabled="False" SetSize="128 48" Margin="5" HorizontalAlignment="Right"/>
</controls:StripeBack>
<Label Name="MapFTLMessage" Text="{Loc 'docking-console-ftl-message-Unknown'}" VerticalAlignment="Bottom" HorizontalAlignment="Center"/>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,158 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Access.Systems;
using Content.Shared._Lavaland.Shuttles;
using Content.Shared._Lavaland.Shuttles.Components;
using Content.Shared.Shuttles.Systems;
using Content.Shared.Timing;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client._Lavaland.Shuttles.UI;
[GenerateTypedNameReferences]
public sealed partial class DockingConsoleWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _player = default!;
private readonly AccessReaderSystem _access;
public event Action<int>? OnFTL;
public event Action<bool>? OnShuttleCall;
private readonly EntityUid _owner;
private readonly StyleBoxFlat _ftlStyle;
private FTLState _state;
private int? _selected;
private StartEndTime _ftlTime;
public DockingConsoleWindow(EntityUid owner)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_access = _entMan.System<AccessReaderSystem>();
_owner = owner;
FTLButton.Disabled = false;
ShuttleCallButton.Disabled = true;
_ftlStyle = new StyleBoxFlat(Color.LimeGreen);
FTLBar.ForegroundStyleBoxOverride = _ftlStyle;
if (!_entMan.TryGetComponent<DockingConsoleComponent>(owner, out var comp))
return;
Title = Loc.GetString(comp.WindowTitle);
if (!comp.HasShuttle)
{
MapFTLState.Text = Loc.GetString("docking-console-no-shuttle");
_ftlStyle.BackgroundColor = Color.FromHex("#B02E26");
FTLButton.Disabled = true;
ShuttleCallButton.Disabled = false;
ShuttleCallButton.OnPressed += _ =>
{
OnShuttleCall?.Invoke(true);
};
return;
}
Destinations.OnItemSelected += args =>
{
_selected = args.ItemIndex;
UpdateButton();
};
Destinations.OnItemDeselected += _ =>
{
_selected = null;
UpdateButton();
};
FTLButton.OnPressed += _ =>
{
if (_selected is {} index)
OnFTL?.Invoke(index);
};
}
public void UpdateState(DockingConsoleState state)
{
_state = state.FTLState;
_ftlTime = state.FTLTime;
MapFTLState.Text = Loc.GetString($"shuttle-console-ftl-state-{_state.ToString()}");
_ftlStyle.BackgroundColor = Color.FromHex(_state switch
{
FTLState.Available => "#80C71F",
FTLState.Starting => "#169C9C",
FTLState.Travelling => "#8932B8",
FTLState.Arriving => "#F9801D",
_ => "#B02E26" // cooldown and fallback
});
UpdateButton();
if (Destinations.Count == state.Destinations.Count)
return;
Destinations.Clear();
foreach (var dest in state.Destinations)
{
Destinations.AddItem(dest.Name);
}
}
private void UpdateButton()
{
MiningFtlState state = MiningFtlState.Unknown;
if (!HasAccess())
state = MiningFtlState.NoAccess;
else if (_selected == null)
state = MiningFtlState.NoSelection;
else
{
state = _state switch
{
FTLState.Available => MiningFtlState.Ready,
FTLState.Cooldown => MiningFtlState.RechargingFtl,
FTLState.Starting or FTLState.Travelling or FTLState.Arriving => MiningFtlState.InFtl,
_ => state,
};
}
FTLButton.Disabled = state != MiningFtlState.Ready;
MapFTLMessage.Text = Loc.GetString($"docking-console-ftl-message-{state.ToString()}");
}
private bool HasAccess()
{
return _player.LocalSession?.AttachedEntity is {} player && _access.IsAllowed(player, _owner);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
var progress = _ftlTime.ProgressAt(_timing.CurTime);
FTLBar.Value = float.IsFinite(progress) ? progress : 1;
}
private enum MiningFtlState
{
Unknown,
Ready,
NoSelection,
NoAccess,
InFtl,
RechargingFtl,
}
}

View File

@@ -0,0 +1,9 @@
using Content.Shared._Lavaland.Weapons.Block;
namespace Content.Client._Lavaland.Weapons.Block;
public sealed class BlockChargeSystem : SharedBlockChargeSystem
{
}

View File

@@ -0,0 +1,8 @@
using Content.Shared._Lavaland.Weapons.Ranged.Upgrades;
namespace Content.Client._Lavaland.Weapons.Ranged.Upgrades;
public sealed class GunUpgradeSystem : SharedGunUpgradeSystem
{
}

View File

@@ -0,0 +1,26 @@
using Content.Shared._Lavaland.Weapons;
using Robust.Client.GameObjects;
namespace Content.Client._Lavaland.Weapons;
public sealed class WeaponAttachmentSystem : SharedWeaponAttachmentSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<WeaponAttachmentComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleStateEvent);
}
protected override void AddSharp(EntityUid uid) { }
protected override void RemSharp(EntityUid uid) { }
private void OnAfterAutoHandleStateEvent(EntityUid uid, WeaponAttachmentComponent component, ref AfterAutoHandleStateEvent args)
{
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
sprite.LayerSetVisible(WeaponVisualLayers.Bayonet, component.BayonetAttached);
sprite.LayerSetVisible(WeaponVisualLayers.FlightOff, component.LightAttached && !component.LightOn);
sprite.LayerSetVisible(WeaponVisualLayers.FlightOn, component.LightAttached && component.LightOn);
}
}

View File

@@ -265,7 +265,7 @@ namespace Content.IntegrationTests.Tests
{
// TODO fix ninja
// Currently ninja fails to equip their own loadout.
if (protoId == "MobHumanSpaceNinja")
if (protoId == "MobHumanSpaceNinja" || protoId == "LavalandHierophantTelepad") // TODO Lavaland Change: fix telepad
continue;
// TODO fix tests properly upstream

View File

@@ -1,6 +1,7 @@
/* WD edit
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.Body.Components;
using Content.Server.GameTicking;
@@ -111,11 +112,37 @@ public sealed class NukeOpsTest
Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True);
Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False);
var roles = roleSys.MindGetAllRoles(mind);
var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander" && x.Component is NukeopsRoleComponent);
var roles = roleSys.MindGetAllRoleInfo(mind);
var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander");
Assert.That(cmdRoles.Count(), Is.EqualTo(1));
// The second dummy player should be a medic
var dummyMind = mindSys.GetMind(dummyEnts[1])!.Value;
Assert.That(entMan.HasComponent<NukeOperativeComponent>(dummyEnts[1]));
Assert.That(roleSys.MindIsAntagonist(dummyMind));
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(dummyMind));
Assert.That(factionSys.IsMember(dummyEnts[1], "Syndicate"), Is.True);
Assert.That(factionSys.IsMember(dummyEnts[1], "NanoTrasen"), Is.False);
roles = roleSys.MindGetAllRoleInfo(dummyMind);
cmdRoles = roles.Where(x => x.Prototype == "NukeopsMedic");
Assert.That(cmdRoles.Count(), Is.EqualTo(1));
// The other two players should have just spawned in as normal.
CheckDummy(0);
CheckDummy(2);
void CheckDummy(int i)
{
var ent = dummyEnts[i];
var mindCrew = mindSys.GetMind(ent)!.Value;
Assert.That(entMan.HasComponent<NukeOperativeComponent>(ent), Is.False);
Assert.That(roleSys.MindIsAntagonist(mindCrew), Is.False);
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mindCrew), Is.False);
Assert.That(factionSys.IsMember(ent, "Syndicate"), Is.False);
Assert.That(factionSys.IsMember(ent, "NanoTrasen"), Is.True);
var nukeroles = new List<string>() { "Nukeops", "NukeopsMedic", "NukeopsCommander" };
Assert.That(roleSys.MindGetAllRoleInfo(mindCrew).Any(x => nukeroles.Contains(x.Prototype)), Is.False);
}
// The game rule exists, and all the stations/shuttles/maps are properly initialized
var rule = entMan.AllComponents<NukeopsRuleComponent>().Single().Component;
var gridsRule = entMan.AllComponents<RuleGridsComponent>().Single().Component;
@@ -209,13 +236,29 @@ public sealed class NukeOpsTest
for (var tick = 0; tick < totalTicks; tick += increment)
{
await pair.RunTicksSync(increment);
Assert.That(resp.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold));
Assert.That(resp.SuffocationCycWles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold));
Assert.That(damage.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
}
//ticker.SetGamePreset((GamePresetPrototype?)null); WD edit
server.CfgMan.SetCVar(CCVars.GridFill, false);
await pair.SetAntagPref("NukeopsCommander", false);
// Check that the round does not end prematurely when agents are deleted in the outpost
var nukies = dummyEnts.Where(entMan.HasComponent<NukeOperativeComponent>).Append(player).ToArray();
await server.WaitAssertion(() =>
{
for (var i = 0; i < nukies.Length - 1; i++)
{
entMan.DeleteEntity(nukies[i]);
Assert.That(roundEndSys.IsRoundEndRequested,
Is.False,
$"The round ended, but {nukies.Length - i - 1} nukies are still alive!");
}
// Delete the last nukie and make sure the round ends.
entMan.DeleteEntity(nukies[^1]);
Assert.That(roundEndSys.IsRoundEndRequested,
"All nukies were deleted, but the round didn't end!");
});
ticker.SetGamePreset((GamePresetPrototype?) null);
await pair.CleanReturnAsync();
}
}

View File

@@ -1,10 +1,8 @@
#nullable enable
using System.Linq;
using Content.Server.Ghost;
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind.Commands;
using Content.Server.Players;
using Content.Server.Roles;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
@@ -18,7 +16,6 @@ using Robust.Server.Console;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
@@ -287,27 +284,27 @@ public sealed partial class MindTests
Assert.Multiple(() =>
{
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId), Is.False);
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId), Is.False);
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId), Is.False);
});
var traitorRole = new TraitorRoleComponent();
var traitorRole = "MindRoleTraitor";
roleSystem.MindAddRole(mindId, traitorRole);
Assert.Multiple(() =>
{
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId));
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId), Is.False);
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId), Is.False);
});
var jobRole = new JobComponent();
var jobRole = "";
roleSystem.MindAddRole(mindId, jobRole);
roleSystem.MindAddJobRole(mindId, jobPrototype:jobRole);
Assert.Multiple(() =>
{
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId));
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId));
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId));
});
roleSystem.MindRemoveRole<TraitorRoleComponent>(mindId);
@@ -315,15 +312,15 @@ public sealed partial class MindTests
Assert.Multiple(() =>
{
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId), Is.False);
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId));
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId));
});
roleSystem.MindRemoveRole<JobComponent>(mindId);
roleSystem.MindRemoveRole<JobRoleComponent>(mindId);
Assert.Multiple(() =>
{
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId), Is.False);
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId), Is.False);
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId), Is.False);
});
});

View File

@@ -58,7 +58,7 @@ namespace Content.IntegrationTests.Tests
"Saltern", // Maintained by the Sin Mapping Team, ODJ, and TCJ.
"Shoukou", // Maintained by Violet
// "Tortuga", // De-rotated, no current maintainer.
// "Arena", // De-rotated, no current maintainer.
"Arena", // Maintained by astriloqua.
// "Asterisk", // De-rotated, no current maintainer.
"Glacier", // Maintained by Violet
// "TheHive", // De-rotated, no current maintainer.
@@ -66,6 +66,7 @@ namespace Content.IntegrationTests.Tests
"Lighthouse", // Maintained by Violet
// "Submarine", // De-rotated, no current maintainer.
"Gax", // Maintained by Estacao Pirata
"Lavatest", // Lavaland Change
"Rad", // Maintained by Estacao Pirata
// "Europa", // De-rotated, has significant issues.
"Meta", // Maintained by Estacao Pirata

View File

@@ -50,7 +50,7 @@ public sealed class EvacShuttleTest
var data = entMan.GetComponent<StationDataComponent>(station);
var shuttleData = entMan.GetComponent<StationEmergencyShuttleComponent>(station);
var saltern = data.Grids.Single();
var saltern = data.Grids.First(x => !entMan.HasComponent<Content.Server._Lavaland.Procedural.Components.LavalandStationComponent>(x)); // Lavaland change - ignore lavaland outpost
Assert.That(entMan.HasComponent<MapGridComponent>(saltern));
var shuttle = shuttleData.EmergencyShuttle!.Value;

View File

@@ -0,0 +1,83 @@
using System.Linq;
using Content.Server._Lavaland.Procedural.Components;
using Content.Server._Lavaland.Procedural.Systems;
using Content.Server.GameTicking;
using Content.Shared._Lavaland.Procedural.Prototypes;
using Content.Shared.CCVar;
using Content.Shared.Parallax.Biomes;
using Robust.Shared.GameObjects;
namespace Content.IntegrationTests.Tests._Lavaland;
[TestFixture]
[TestOf(typeof(LavalandPlanetSystem))]
public sealed class LavalandGenerationTest
{
[Test]
public async Task LavalandPlanetGenerationTest()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { DummyTicker = false, Dirty = true, Fresh = true});
var server = pair.Server;
var entMan = server.EntMan;
var protoMan = server.ProtoMan;
var mapMan = server.MapMan;
var ticker = server.System<GameTicker>();
var lavaSystem = entMan.System<LavalandPlanetSystem>();
var mapSystem = entMan.System<SharedMapSystem>();
// Setup
pair.Server.CfgMan.SetCVar(CCVars.LavalandEnabled, false);
pair.Server.CfgMan.SetCVar(CCVars.GameDummyTicker, false);
var gameMap = pair.Server.CfgMan.GetCVar(CCVars.GameMap);
pair.Server.CfgMan.SetCVar(CCVars.GameMap, "Saltern");
await server.WaitPost(() => ticker.RestartRound());
await pair.RunTicksSync(25);
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
// Get all possible types of Lavaland and test them.
var planets = protoMan.EnumeratePrototypes<LavalandMapPrototype>().ToList();
foreach (var planet in planets)
{
const int seed = 1;
var attempt = false;
Entity<LavalandMapComponent>? lavaland = null;
// Seed is always the same to reduce randomness
await server.WaitPost(() => attempt = lavaSystem.SetupLavalandPlanet(out lavaland, planet, seed));
await pair.RunTicksSync(30);
Assert.That(lavaland, Is.Not.Null);
var mapId = entMan.GetComponent<TransformComponent>(lavaland.Value).MapID;
// Now check the basics
Assert.That(attempt, Is.True);
Assert.That(mapMan.MapExists(mapId));
Assert.That(entMan.EntityExists(lavaland.Value.Owner));
Assert.That(entMan.EntityExists(lavaland.Value.Comp.Outpost));
Assert.That(mapMan.GetAllGrids(mapId).ToList(), Is.Not.Empty);
Assert.That(mapSystem.IsInitialized(mapId));
Assert.That(mapSystem.IsPaused(mapId), Is.False);
// Test that the biome setup is right
var biome = entMan.GetComponent<BiomeComponent>(lavaland.Value);
Assert.That(biome.Enabled, Is.True);
Assert.That(biome.Seed, Is.EqualTo(seed));
Assert.That(biome.Template, Is.Not.Null);
Assert.That(biome.Layers, Is.Not.Empty);
}
await pair.RunTicksSync(10);
var lavalands = lavaSystem.GetLavalands();
Assert.That(planets, Has.Count.EqualTo(lavalands.Count));
pair.Server.CfgMan.SetCVar(CCVars.GameMap, gameMap);
pair.ClearModifiedCvars();
await pair.CleanReturnAsync();
}
}

View File

@@ -0,0 +1,68 @@
using Robust.Shared.Audio;
namespace Content.Server.AbstractAnalyzer;
/// <summary>
/// After scanning, retrieves the target Uid to use with its related UI.
/// </summary>
/// <remarks>
/// Requires <c>ItemToggleComponent</c>.
/// </remarks>
public abstract partial class AbstractAnalyzerComponent : Component
{
/// <summary>
/// When should the next update be sent for the patient
/// </summary>
/// <remarks>
/// Override this in your implementation with:
///
/// ```cs
/// [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
/// [AutoPausedField]
/// public override TimeSpan NextUpdate { get; set; } = TimeSpan.Zero;
/// ```
/// </remarks>
public abstract TimeSpan NextUpdate { get; set; }
/// <summary>
/// The delay between patient health updates
/// </summary>
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
/// <summary>
/// How long it takes to scan someone.
/// </summary>
[DataField]
public TimeSpan ScanDelay = TimeSpan.FromSeconds(0.8);
/// <summary>
/// Which entity has been scanned, for continuous updates
/// </summary>
[DataField]
public EntityUid? ScannedEntity;
/// <summary>
/// The maximum range in tiles at which the analyzer can receive continuous updates
/// </summary>
[DataField]
public float MaxScanRange = 2.5f;
/// <summary>
/// Sound played on scanning begin
/// </summary>
[DataField]
public SoundSpecifier? ScanningBeginSound;
/// <summary>
/// Sound played on scanning end
/// </summary>
[DataField]
public SoundSpecifier ScanningEndSound = new SoundPathSpecifier("/Audio/Items/Medical/healthscanner.ogg");
/// <summary>
/// Whether to show up the popup
/// </summary>
[DataField]
public bool Silent;
}

View File

@@ -0,0 +1,194 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.PowerCell;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Item.ItemToggle;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Popups;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Server.AbstractAnalyzer;
public abstract class AbstractAnalyzerSystem<TAnalyzerComponent, TAnalyzerDoAfterEvent> : EntitySystem
where TAnalyzerComponent : AbstractAnalyzerComponent
where TAnalyzerDoAfterEvent : SimpleDoAfterEvent, new()
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PowerCellSystem _cell = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly ItemToggleSystem _toggle = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
public override void Initialize()
{
SubscribeLocalEvent<TAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<TAnalyzerComponent, TAnalyzerDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<TAnalyzerComponent, EntGotInsertedIntoContainerMessage>(OnInsertedIntoContainer);
SubscribeLocalEvent<TAnalyzerComponent, ItemToggledEvent>(OnToggled);
SubscribeLocalEvent<TAnalyzerComponent, DroppedEvent>(OnDropped);
}
public override void Update(float frameTime)
{
var analyzerQuery = EntityQueryEnumerator<TAnalyzerComponent, TransformComponent>();
while (analyzerQuery.MoveNext(out var uid, out var component, out var transform))
{
//Update rate limited to 1 second
if (component.NextUpdate > _timing.CurTime
|| component.ScannedEntity is not { } target)
continue;
if (Deleted(target))
{
StopAnalyzingEntity((uid, component), target);
continue;
}
component.NextUpdate = _timing.CurTime + component.UpdateInterval;
//Get distance between analyzer and the scanned entity
var targetCoordinates = Transform(target).Coordinates;
if (!_transformSystem.InRange(targetCoordinates, transform.Coordinates, component.MaxScanRange))
{
//Range too far, disable updates
StopAnalyzingEntity((uid, component), target);
continue;
}
UpdateScannedUser(uid, target, true);
}
}
/// <summary>
/// Trigger the doafter for scanning
/// </summary>
private void OnAfterInteract(Entity<TAnalyzerComponent> uid, ref AfterInteractEvent args)
{
if (args.Target == null || !args.CanReach || !ValidScanTarget(args.Target) || !_cell.HasDrawCharge(uid, user: args.User))
return;
_audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
var doAfterCancelled = !_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager,
args.User, uid.Comp.ScanDelay, new TAnalyzerDoAfterEvent(), uid, target: args.Target, used: uid)
{
NeedHand = true,
BreakOnMove = true,
});
if (args.Target == args.User || doAfterCancelled || uid.Comp.Silent || args.Target is null)
return;
if (ScanTargetPopupMessage(uid, args, out var msg))
_popupSystem.PopupEntity(msg, args.Target.Value, args.Target.Value, PopupType.Medium);
}
private void OnDoAfter(Entity<TAnalyzerComponent> uid, ref TAnalyzerDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User))
return;
if (!uid.Comp.Silent)
_audio.PlayPvs(uid.Comp.ScanningEndSound, uid);
OpenUserInterface(args.User, uid);
BeginAnalyzingEntity(uid, args.Target.Value);
args.Handled = true;
}
/// <summary>
/// Turn off when placed into a storage item or moved between slots/hands
/// </summary>
private void OnInsertedIntoContainer(Entity<TAnalyzerComponent> uid, ref EntGotInsertedIntoContainerMessage args)
{
if (uid.Comp.ScannedEntity is { })
_toggle.TryDeactivate(uid.Owner);
}
/// <summary>
/// Disable continuous updates once turned off
/// </summary>
private void OnToggled(Entity<TAnalyzerComponent> ent, ref ItemToggledEvent args)
{
if (!args.Activated && ent.Comp.ScannedEntity is { } target)
StopAnalyzingEntity(ent, target);
}
/// <summary>
/// Turn off the analyser when dropped
/// </summary>
private void OnDropped(Entity<TAnalyzerComponent> uid, ref DroppedEvent args)
{
if (uid.Comp.ScannedEntity is { })
_toggle.TryDeactivate(uid.Owner);
}
private void OpenUserInterface(EntityUid user, EntityUid analyzer)
{
if (!_uiSystem.HasUi(analyzer, GetUiKey()))
return;
_uiSystem.OpenUi(analyzer, GetUiKey(), user);
}
/// <summary>
/// Mark the entity as being analyzed, and link the analyzer to it
/// </summary>
/// <param name="analyzer">The analyzer that should receive the updates</param>
/// <param name="target">The entity to start analyzing</param>
private void BeginAnalyzingEntity(Entity<TAnalyzerComponent> analyzer, EntityUid target)
{
//Link the analyzer to the scanned entity
analyzer.Comp.ScannedEntity = target;
_toggle.TryActivate(analyzer.Owner);
_cell.SetDrawEnabled(analyzer.Owner, true);
UpdateScannedUser(analyzer, target, true);
}
/// <summary>
/// Remove the analyzer from the active list, and remove the component if it has no active analyzers
/// </summary>
/// <param name="analyzer">The analyzer that's receiving the updates</param>
/// <param name="target">The entity to analyze</param>
private void StopAnalyzingEntity(Entity<TAnalyzerComponent> analyzer, EntityUid target)
{
//Unlink the analyzer
analyzer.Comp.ScannedEntity = null;
_toggle.TryDeactivate(analyzer.Owner);
_cell.SetDrawEnabled(analyzer.Owner, false);
UpdateScannedUser(analyzer, target, false);
}
/// <summary>
/// Send an update for the target to the analyzer
/// </summary>
/// <param name="analyzer">The analyzer</param>
/// <param name="target">The entity being scanned</param>
/// <param name="scanMode">True makes the UI show ACTIVE, False makes the UI show INACTIVE</param>
public abstract void UpdateScannedUser(EntityUid analyzer, EntityUid target, bool scanMode);
/// <returns>A <see cref="Robust.Shared.Serialization.NetSerializableAttribute"/> byte enum key.</returns>
protected abstract Enum GetUiKey();
/// <summary>
/// The message the scan target receives on scan.
/// </summary>
/// <returns>true if the message should be shown</returns>
protected abstract bool ScanTargetPopupMessage(Entity<TAnalyzerComponent> uid, AfterInteractEvent args, [NotNullWhen(true)] out string? message);
/// <summary>
/// Used to validate if a specific entity is a valid target for a specific analyzer.
/// </summary>
protected abstract bool ValidScanTarget(EntityUid? target);
}

View File

@@ -4,7 +4,6 @@ using Content.Server.Chat.Managers;
using Content.Server.Forensics;
using Content.Server.GameTicking;
using Content.Server.Hands.Systems;
using Content.Server.IdentityManagement;
using Content.Server.Mind;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Popups;
@@ -16,6 +15,7 @@ using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Inventory;
using Content.Shared.Mind;
using Content.Shared.PDA;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Popups;
@@ -31,6 +31,7 @@ using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Systems
{
@@ -52,6 +53,7 @@ namespace Content.Server.Administration.Systems
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
private readonly Dictionary<NetUserId, PlayerInfo> _playerList = new();
@@ -156,12 +158,14 @@ namespace Content.Server.Administration.Systems
private void OnRoleEvent(RoleEvent ev)
{
var session = _minds.GetSession(ev.Mind);
if (!ev.Antagonist || session == null)
if (!ev.RoleTypeUpdate || session == null)
return;
UpdatePlayerList(session);
}
private void OnAdminPermsChanged(AdminPermsChangedEventArgs obj)
{
UpdatePanicBunker();
@@ -216,7 +220,7 @@ namespace Content.Server.Administration.Systems
RaiseNetworkEvent(ev, playerSession.Channel);
}
private PlayerInfo GetPlayerInfo(SessionData data, ICommonSession session)
private PlayerInfo GetPlayerInfo(SessionData data, ICommonSession? session)
{
var name = data.UserName;
var entityName = string.Empty;
@@ -229,9 +233,16 @@ namespace Content.Server.Administration.Systems
}
var antag = false;
RoleTypePrototype roleType = new();
var startingRole = string.Empty;
if (_minds.TryGetMind(session, out var mindId, out _))
if (_minds.TryGetMind(session, out var mindId, out var mindComp))
{
if (_protoMan.TryIndex(mindComp.RoleType, out var role))
roleType = role;
else
Log.Error($"{ToPrettyString(mindId)} has invalid Role Type '{mindComp.RoleType}'. Displaying '{Loc.GetString(roleType.Name)}' instead");
antag = _role.MindIsAntagonist(mindId);
startingRole = _jobs.MindTryGetJobName(mindId);
}
@@ -245,7 +256,7 @@ namespace Content.Server.Administration.Systems
overallPlaytime = playTime;
}
return new PlayerInfo(name, entityName, identityName, startingRole, antag, GetNetEntity(session?.AttachedEntity), data.UserId,
return new PlayerInfo(name, entityName, identityName, startingRole, antag, roleType, GetNetEntity(session?.AttachedEntity), data.UserId,
connected, _roundActivePlayers.Contains(data.UserId), overallPlaytime);
}

View File

@@ -20,7 +20,7 @@ namespace Content.Server.Alert.Click
if (entManager.TryGetComponent(player, out PullerComponent? puller) &&
entManager.TryGetComponent(puller.Pulling, out PullableComponent? pullableComp))
{
ps.TryStopPull(puller.Pulling.Value, pullableComp, user: player);
ps.TryLowerGrabStage(puller.Pulling.Value, player, true); // Spacious Edit, kill me now
}
}
}

View File

@@ -0,0 +1,22 @@
using Content.Server.Antag.Components;
using Content.Server.GameTicking.Rules;
namespace Content.Server.Antag;
public sealed class AntagRandomSpawnSystem : GameRuleSystem<AntagRandomSpawnComponent>
{
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AntagRandomSpawnComponent, AntagSelectLocationEvent>(OnSelectLocation);
}
private void OnSelectLocation(Entity<AntagRandomSpawnComponent> ent, ref AntagSelectLocationEvent args)
{
if (TryFindRandomTile(out _, out _, out _, out var coords))
args.Coordinates.Add(_transform.ToMapCoordinates(coords));
}
}

View File

@@ -10,14 +10,16 @@ using Content.Server.Objectives;
using Content.Server.Preferences.Managers;
using Content.Server.Roles;
using Content.Server.Roles.Jobs;
using Content.Server.Shuttles.Components;
using Content.Server.Station.Systems;
using Content.Server.Shuttles.Components;
using Content.Shared.Antag;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Content.Shared.Ghost;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Players;
using Content.Shared.Roles;
using Content.Shared.Whitelist;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
@@ -32,13 +34,13 @@ namespace Content.Server.Antag;
public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelectionComponent>
{
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerPreferencesManager _pref = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly GhostRoleSystem _ghostRole = default!;
[Dependency] private readonly JobSystem _jobs = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerPreferencesManager _pref = default!;
[Dependency] private readonly RoleSystem _role = default!;
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
[Dependency] private readonly TransformSystem _transform = default!;
@@ -188,7 +190,10 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <summary>
/// Chooses antagonists from the given selection of players
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool)
/// <param name="ent">The antagonist rule entity</param>
/// <param name="pool">The players to choose from</param>
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
{
if (ent.Comp.SelectionsComplete)
return;
@@ -204,7 +209,14 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <summary>
/// Chooses antagonists from the given selection of players for the given antag definition.
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, AntagSelectionDefinition def)
/// <param name="ent">The antagonist rule entity</param>
/// <param name="pool">The players to choose from</param>
/// <param name="def">The antagonist selection parameters and criteria</param>
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
public void ChooseAntags(Entity<AntagSelectionComponent> ent,
IList<ICommonSession> pool,
AntagSelectionDefinition def,
bool midround = false)
{
var playerPool = GetPlayerPool(ent, pool, def);
var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def);
@@ -315,14 +327,17 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (session != null)
{
var curMind = session.GetMind();
if (curMind == null)
if (curMind == null ||
!TryComp<MindComponent>(curMind.Value, out var mindComp) ||
mindComp.OwnedEntity != antagEnt)
{
curMind = _mind.CreateMind(session.UserId, Name(antagEnt.Value));
_mind.SetUserId(curMind.Value, session.UserId);
}
_mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
_role.MindAddRoles(curMind.Value, def.MindComponents);
_role.MindAddRoles(curMind.Value, def.MindRoles, null, true);
ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
SendBriefing(session, def.Briefing);
}

View File

@@ -0,0 +1,21 @@
using Content.Server.Antag.Components;
namespace Content.Server.Antag;
/// <summary>
/// Spawns an entity when creating an antag for <see cref="AntagSpawnerComponent"/>.
/// </summary>
public sealed class AntagSpawnerSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AntagSpawnerComponent, AntagSelectEntityEvent>(OnSelectEntity);
}
private void OnSelectEntity(Entity<AntagSpawnerComponent> ent, ref AntagSelectEntityEvent args)
{
args.Entity = Spawn(ent.Comp.Prototype);
}
}

View File

@@ -0,0 +1,8 @@
namespace Content.Server.Antag.Components;
/// <summary>
/// Spawns this rule's antags at random tiles on a station using <c>TryGetRandomTile</c>.
/// Requires <see cref="AntagSelectionComponent"/>.
/// </summary>
[RegisterComponent]
public sealed partial class AntagRandomSpawnComponent : Component;

View File

@@ -2,7 +2,6 @@ using Content.Server.Administration.Systems;
using Content.Shared.Antag;
using Content.Shared.Destructible.Thresholds;
using Content.Shared.Roles;
using Content.Shared.Storage;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Player;
@@ -144,10 +143,17 @@ public partial struct AntagSelectionDefinition()
/// <summary>
/// Components added to the player's mind.
/// Do NOT use this to add role-type components. Add those as MindRoles instead
/// </summary>
[DataField]
public ComponentRegistry MindComponents = new();
/// <summary>
/// List of Mind Role Prototypes to be added to the player's mind.
/// </summary>
[DataField]
public List<EntProtoId>? MindRoles;
/// <summary>
/// A set of starting gear that's equipped to the player.
/// </summary>

View File

@@ -0,0 +1,17 @@
using Content.Server.Antag;
using Robust.Shared.Prototypes;
namespace Content.Server.Antag.Components;
/// <summary>
/// Spawns a prototype for antags created with a spawner.
/// </summary>
[RegisterComponent, Access(typeof(AntagSpawnerSystem))]
public sealed partial class AntagSpawnerComponent : Component
{
/// <summary>
/// The entity to spawn.
/// </summary>
[DataField(required: true)]
public EntProtoId Prototype = string.Empty;
}

View File

@@ -1,3 +1,5 @@
using Content.Shared.Atmos;
namespace Content.Server.Atmos.Components
{
/// <summary>
@@ -7,7 +9,10 @@ namespace Content.Server.Atmos.Components
public sealed partial class AtmosFixMarkerComponent : Component
{
// See FixGridAtmos for more details
[DataField("mode")]
[DataField]
public int Mode { get; set; } = 0;
[DataField]
public GasMixture? GasMix = default!;
}
}

View File

@@ -30,90 +30,94 @@ public sealed partial class AtmosphereSystem
[AdminCommand(AdminFlags.Debug)]
private void FixGridAtmosCommand(IConsoleShell shell, string argstr, string[] args)
{
if (args.Length == 0)
{
shell.WriteError("Not enough arguments.");
return;
}
if (args.Length == 0)
{
shell.WriteError("Not enough arguments.");
return;
}
var mixtures = new GasMixture[8];
for (var i = 0; i < mixtures.Length; i++)
mixtures[i] = new GasMixture(Atmospherics.CellVolume) { Temperature = Atmospherics.T20C };
var mixtures = new GasMixture[8];
for (var i = 0; i < mixtures.Length; i++)
mixtures[i] = new GasMixture(Atmospherics.CellVolume) { Temperature = Atmospherics.T20C };
// 0: Air
mixtures[0].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesStandard);
mixtures[0].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesStandard);
// 0: Air
mixtures[0].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesStandard);
mixtures[0].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesStandard);
// 1: Vaccum
// 1: Vaccum
// 2: Oxygen (GM)
mixtures[2].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner);
// 2: Oxygen (GM)
mixtures[2].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner);
// 3: Nitrogen (GM)
mixtures[3].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellGasMiner);
// 3: Nitrogen (GM)
mixtures[3].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellGasMiner);
// 4: Plasma (GM)
mixtures[4].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner);
// 4: Plasma (GM)
mixtures[4].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner);
// 5: Instant Plasmafire (r)
mixtures[5].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner);
mixtures[5].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner);
mixtures[5].Temperature = 5000f;
// 5: Instant Plasmafire (r)
mixtures[5].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner);
mixtures[5].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner);
mixtures[5].Temperature = 5000f;
// 6: (Walk-In) Freezer
mixtures[6].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesStandard);
mixtures[6].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesStandard);
mixtures[6].Temperature = 235f; // Little colder than an actual freezer but gives a grace period to get e.g. themomachines set up, should keep warm for a few door openings
// 6: (Walk-In) Freezer
mixtures[6].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesStandard);
mixtures[6].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesStandard);
mixtures[6].Temperature = 235f; // Little colder than an actual freezer but gives a grace period to get e.g. themomachines set up, should keep warm for a few door openings
// 7: Nitrogen (101kpa) for vox rooms
mixtures[7].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellStandard);
// 7: Nitrogen (101kpa) for vox rooms
mixtures[7].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellStandard);
foreach (var arg in args)
{
if (!NetEntity.TryParse(arg, out var netEntity) || !TryGetEntity(netEntity, out var euid))
{
shell.WriteError($"Failed to parse euid '{arg}'.");
return;
}
foreach (var arg in args)
{
if (!NetEntity.TryParse(arg, out var netEntity) || !TryGetEntity(netEntity, out var euid))
{
shell.WriteError($"Failed to parse euid '{arg}'.");
return;
}
if (!TryComp(euid, out MapGridComponent? gridComp))
{
shell.WriteError($"Euid '{euid}' does not exist or is not a grid.");
return;
}
if (!TryComp(euid, out MapGridComponent? gridComp))
{
shell.WriteError($"Euid '{euid}' does not exist or is not a grid.");
return;
}
if (!TryComp(euid, out GridAtmosphereComponent? gridAtmosphere))
{
shell.WriteError($"Grid \"{euid}\" has no atmosphere component, try addatmos.");
continue;
}
if (!TryComp(euid, out GridAtmosphereComponent? gridAtmosphere))
{
shell.WriteError($"Grid \"{euid}\" has no atmosphere component, try addatmos.");
continue;
}
// Force Invalidate & update air on all tiles
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> grid =
new(euid.Value, gridAtmosphere, Comp<GasTileOverlayComponent>(euid.Value), gridComp, Transform(euid.Value));
// Force Invalidate & update air on all tiles
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> grid =
new(euid.Value, gridAtmosphere, Comp<GasTileOverlayComponent>(euid.Value), gridComp, Transform(euid.Value));
RebuildGridTiles(grid);
RebuildGridTiles(grid);
var query = GetEntityQuery<AtmosFixMarkerComponent>();
foreach (var (indices, tile) in gridAtmosphere.Tiles.ToArray())
{
if (tile.Air is not {Immutable: false} air)
continue;
var query = GetEntityQuery<AtmosFixMarkerComponent>();
foreach (var (indices, tile) in gridAtmosphere.Tiles.ToArray())
{
if (tile.Air is not {Immutable: false} air)
continue;
air.Clear();
var mixtureId = 0;
var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(grid, grid, indices);
while (enumerator.MoveNext(out var entUid))
{
if (query.TryComp(entUid, out var marker))
mixtureId = marker.Mode;
}
air.Clear();
var mixtureId = 0;
GasMixture? gasMix = null;
var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(grid, grid, indices);
while (enumerator.MoveNext(out var entUid))
{
if (!query.TryComp(entUid, out var marker))
continue;
var mixture = mixtures[mixtureId];
Merge(air, mixture);
air.Temperature = mixture.Temperature;
}
}
mixtureId = marker.Mode;
gasMix = marker.GasMix;
}
var mixture = mixtures[mixtureId];
Merge(air, gasMix is not null ? gasMix : mixture);
air.Temperature = mixture.Temperature;
}
}
}
/// <summary>

View File

@@ -320,7 +320,7 @@ namespace Content.Server.Atmos.EntitySystems
public void Ignite(EntityUid uid, EntityUid ignitionSource, FlammableComponent? flammable = null,
EntityUid? ignitionSourceUser = null, bool ignoreFireProtection = false)
{
if (!Resolve(uid, ref flammable))
if (!Resolve(uid, ref flammable, false)) // Lavaland Change: SHUT THE FUCK UP FLAMMABLE
return;
if (flammable.AlwaysCombustible)

View File

@@ -15,7 +15,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// <summary>
/// Target volume to transfer. If <see cref="WideNet"/> is enabled, actual transfer rate will be much higher.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float TransferRate
{
get => _transferRate;

View File

@@ -17,6 +17,7 @@ using Content.Shared.Bed.Cryostorage;
using Content.Shared.Chat;
using Content.Shared.Climbing.Systems;
using Content.Shared.Database;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.Mind.Components;
using Robust.Server.Audio;

View File

@@ -21,6 +21,8 @@ using Content.Shared.Mood;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Content.Shared.Movement.Pulling.Components; // Goobstation
using Content.Shared.Movement.Pulling.Systems; // Goobstation
namespace Content.Server.Body.Systems;
@@ -53,6 +55,15 @@ public sealed class RespiratorSystem : EntitySystem
SubscribeLocalEvent<RespiratorComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
}
// Goobstation start
// Can breathe check for grab
public bool CanBreathe(EntityUid uid, RespiratorComponent respirator)
{
if(respirator.Saturation < respirator.SuffocationThreshold)
return false;
return !TryComp<PullableComponent>(uid, out var pullable) || pullable.GrabStage != GrabStage.Suffocate;
}
// Goobstation end
private void OnMapInit(Entity<RespiratorComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
@@ -98,7 +109,7 @@ public sealed class RespiratorSystem : EntitySystem
}
}
if (respirator.Saturation < respirator.SuffocationThreshold)
if (!CanBreathe(uid, respirator)) // Goobstation edit
{
if (_gameTiming.CurTime >= respirator.LastGaspPopupTime + respirator.GaspPopupCooldown)
{

View File

@@ -0,0 +1,42 @@
using Content.Server.AbstractAnalyzer;
using Content.Server.Botany.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Botany.Components;
/// <inheritdoc/>
[RegisterComponent, AutoGenerateComponentPause]
[Access(typeof(PlantAnalyzerSystem))]
public sealed partial class PlantAnalyzerComponent : AbstractAnalyzerComponent
{
/// <inheritdoc/>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public override TimeSpan NextUpdate { get; set; } = TimeSpan.Zero;
/// <summary>
/// When will the analyzer be ready to print again?
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public TimeSpan PrintReadyAt = TimeSpan.Zero;
/// <summary>
/// How often can the analyzer print?
/// </summary>
[DataField]
public TimeSpan PrintCooldown = TimeSpan.FromSeconds(5);
/// <summary>
/// The sound that's played when the analyzer prints off a report.
/// </summary>
[DataField]
public SoundSpecifier SoundPrint = new SoundPathSpecifier("/Audio/Machines/short_print_and_rip.ogg");
/// <summary>
/// What the machine will print.
/// </summary>
[DataField]
public EntProtoId MachineOutput = "PlantAnalyzerReportPaper";
}

View File

@@ -145,17 +145,7 @@ public sealed partial class BotanySystem : EntitySystem
public IEnumerable<EntityUid> GenerateProduct(SeedData proto, EntityCoordinates position, int yieldMod = 1)
{
var totalYield = 0;
if (proto.Yield > -1)
{
if (yieldMod < 0)
totalYield = proto.Yield;
else
totalYield = proto.Yield * yieldMod;
totalYield = Math.Max(1, totalYield);
}
var totalYield = CalculateTotalYield(proto.Yield, yieldMod);
var products = new List<EntityUid>();
if (totalYield > 1 || proto.HarvestRepeat != HarvestType.NoRepeat)
@@ -222,5 +212,8 @@ public sealed partial class BotanySystem : EntitySystem
return !proto.Ligneous || proto.Ligneous && held != null && HasComp<SharpComponent>(held);
}
public static int CalculateTotalYield(int yield, int yieldMod) =>
yield > -1 ? Math.Max(1, yieldMod < 0 ? yield : yield * yieldMod) : 0;
#endregion
}

View File

@@ -0,0 +1,193 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.AbstractAnalyzer;
using Content.Server.Botany.Components;
using Content.Server.Paper;
using Content.Server.Popups;
using Content.Shared.Botany.PlantAnalyzer;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Labels.EntitySystems;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.Botany.Systems;
public sealed class PlantAnalyzerSystem : AbstractAnalyzerSystem<PlantAnalyzerComponent, PlantAnalyzerDoAfterEvent>
{
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly PaperSystem _paperSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedLabelSystem _labelSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PlantAnalyzerComponent, PlantAnalyzerPrintMessage>(OnPrint);
}
/// <inheritdoc/>
public override void UpdateScannedUser(EntityUid analyzer, EntityUid target, bool scanMode)
{
if (!_uiSystem.HasUi(analyzer, PlantAnalyzerUiKey.Key)
|| !ValidScanTarget(target)
|| !_entityManager.TryGetComponent<PlantAnalyzerComponent>(analyzer, out var analyzerComponent))
return;
_uiSystem.ServerSendUiMessage(analyzer, PlantAnalyzerUiKey.Key, GatherData(analyzerComponent, scanMode, target: target));
}
private PlantAnalyzerScannedUserMessage GatherData(PlantAnalyzerComponent analyzer, bool? scanMode = null, EntityUid? target = null)
{
target ??= analyzer.ScannedEntity;
PlantAnalyzerPlantData? plantData = null;
PlantAnalyzerTrayData? trayData = null;
PlantAnalyzerTolerancesData? tolerancesData = null;
PlantAnalyzerProduceData? produceData = null;
if (_entityManager.TryGetComponent<PlantHolderComponent>(target, out var plantHolder))
{
if (plantHolder.Seed is not null)
{
plantData = new PlantAnalyzerPlantData(
seedDisplayName: plantHolder.Seed.DisplayName,
health: plantHolder.Health,
endurance: plantHolder.Seed.Endurance,
age: plantHolder.Age,
lifespan: plantHolder.Seed.Lifespan,
dead: plantHolder.Dead,
viable: plantHolder.Seed.Viable,
mutating: plantHolder.MutationLevel > 0f,
kudzu: plantHolder.Seed.TurnIntoKudzu
);
tolerancesData = new PlantAnalyzerTolerancesData(
waterConsumption: plantHolder.Seed.WaterConsumption,
nutrientConsumption: plantHolder.Seed.NutrientConsumption,
toxinsTolerance: plantHolder.Seed.ToxinsTolerance,
pestTolerance: plantHolder.Seed.PestTolerance,
weedTolerance: plantHolder.Seed.WeedTolerance,
lowPressureTolerance: plantHolder.Seed.LowPressureTolerance,
highPressureTolerance: plantHolder.Seed.HighPressureTolerance,
idealHeat: plantHolder.Seed.IdealHeat,
heatTolerance: plantHolder.Seed.HeatTolerance,
idealLight: plantHolder.Seed.IdealLight,
lightTolerance: plantHolder.Seed.LightTolerance,
consumeGasses: [.. plantHolder.Seed.ConsumeGasses.Keys]
);
produceData = new PlantAnalyzerProduceData(
yield: plantHolder.Seed.ProductPrototypes.Count == 0 ? 0 : BotanySystem.CalculateTotalYield(plantHolder.Seed.Yield, plantHolder.YieldMod),
potency: plantHolder.Seed.Potency,
chemicals: [.. plantHolder.Seed.Chemicals.Keys],
produce: plantHolder.Seed.ProductPrototypes,
exudeGasses: [.. plantHolder.Seed.ExudeGasses.Keys],
seedless: plantHolder.Seed.Seedless
);
}
trayData = new PlantAnalyzerTrayData(
waterLevel: plantHolder.WaterLevel,
nutritionLevel: plantHolder.NutritionLevel,
toxins: plantHolder.Toxins,
pestLevel: plantHolder.PestLevel,
weedLevel: plantHolder.WeedLevel,
chemicals: plantHolder.SoilSolution?.Comp.Solution.Contents.Select(r => r.Reagent.Prototype).ToList()
);
}
return new PlantAnalyzerScannedUserMessage(
GetNetEntity(target),
scanMode,
plantData,
trayData,
tolerancesData,
produceData,
analyzer.PrintReadyAt
);
}
private void OnPrint(EntityUid uid, PlantAnalyzerComponent component, PlantAnalyzerPrintMessage args)
{
var user = args.Actor;
if (_gameTiming.CurTime < component.PrintReadyAt)
{
// This shouldn't occur due to the UI guarding against it, but
// if it does, tell the user why nothing happened.
_popupSystem.PopupEntity(Loc.GetString("plant-analyzer-printer-not-ready"), uid, user);
return;
}
// Spawn a piece of paper.
var printed = EntityManager.SpawnEntity(component.MachineOutput, Transform(uid).Coordinates);
_handsSystem.PickupOrDrop(args.Actor, printed, checkActionBlocker: false);
if (!TryComp<PaperComponent>(printed, out var paperComp))
{
Log.Error("Printed paper did not have PaperComponent.");
return;
}
var data = GatherData(component);
var missingData = Loc.GetString("plant-analyzer-printout-missing");
var seedName = data.PlantData is not null ? Loc.GetString(data.PlantData.SeedDisplayName) : null;
(string, object)[] parameters = [
("seedName", seedName ?? missingData),
("produce", data.ProduceData is not null ? PlantAnalyzerLocalizationHelper.ProduceToLocalizedStrings(data.ProduceData.Produce, _prototypeManager).Plural : missingData),
("water", data.TolerancesData?.WaterConsumption.ToString("0.00") ?? missingData),
("nutrients", data.TolerancesData?.NutrientConsumption.ToString("0.00") ?? missingData),
("toxins", data.TolerancesData?.ToxinsTolerance.ToString("0.00") ?? missingData),
("pests", data.TolerancesData?.PestTolerance.ToString("0.00") ?? missingData),
("weeds", data.TolerancesData?.WeedTolerance.ToString("0.00") ?? missingData),
("gasesIn", data.TolerancesData is not null ? PlantAnalyzerLocalizationHelper.GasesToLocalizedStrings(data.TolerancesData.ConsumeGasses, _prototypeManager) : missingData),
("kpa", data.TolerancesData?.IdealPressure.ToString("0.00") ?? missingData),
("kpaTolerance", data.TolerancesData?.PressureTolerance.ToString("0.00") ?? missingData),
("temp", data.TolerancesData?.IdealHeat.ToString("0.00") ?? missingData),
("tempTolerance", data.TolerancesData?.HeatTolerance.ToString("0.00") ?? missingData),
("lightLevel", data.TolerancesData?.IdealLight.ToString("0.00") ?? missingData),
("lightTolerance", data.TolerancesData?.LightTolerance.ToString("0.00") ?? missingData),
("yield", data.ProduceData?.Yield ?? -1),
("potency", data.ProduceData is not null ? Loc.GetString(data.ProduceData.Potency) : missingData),
("chemicals", data.ProduceData is not null ? PlantAnalyzerLocalizationHelper.ChemicalsToLocalizedStrings(data.ProduceData.Chemicals, _prototypeManager) : missingData),
("gasesOut", data.ProduceData is not null ? PlantAnalyzerLocalizationHelper.GasesToLocalizedStrings(data.ProduceData.ExudeGasses, _prototypeManager) : missingData),
("endurance", data.PlantData?.Endurance.ToString("0.00") ?? missingData),
("lifespan", data.PlantData?.Lifespan.ToString("0.00") ?? missingData),
("seeds", data.ProduceData is not null ? (data.ProduceData.Seedless ? "no" : "yes") : "other"),
("viable", data.PlantData is not null ? (data.PlantData.Viable ? "yes" : "no") : "other"),
("kudzu", data.PlantData is not null ? (data.PlantData.Kudzu ? "yes" : "no") : "other"),
("indent", " "),
("nl", "\n")
];
_paperSystem.SetContent(printed, Loc.GetString($"plant-analyzer-printout", [.. parameters]));
_labelSystem.Label(printed, seedName);
_audioSystem.PlayPvs(component.SoundPrint, uid,
AudioParams.Default
.WithVariation(0.25f)
.WithVolume(3f)
.WithRolloffFactor(2.8f)
.WithMaxDistance(4.5f));
component.PrintReadyAt = _gameTiming.CurTime + component.PrintCooldown;
}
/// <inheritdoc/>
protected override Enum GetUiKey() => PlantAnalyzerUiKey.Key;
/// <inheritdoc/>
protected override bool ScanTargetPopupMessage(Entity<PlantAnalyzerComponent> uid, AfterInteractEvent args, [NotNullWhen(true)] out string? message)
{
message = null;
return false;
}
/// <inheritdoc/>
protected override bool ValidScanTarget(EntityUid? target) => HasComp<PlantHolderComponent>(target);
}

View File

@@ -36,7 +36,7 @@ public sealed class CharacterInfoSystem : EntitySystem
if (_minds.TryGetMind(entity, out var mindId, out var mind))
{
// Get objectives
foreach (var objective in mind.AllObjectives)
foreach (var objective in mind.Objectives)
{
var info = _objectives.GetInfo(objective, mindId, mind);
if (info == null)

View File

@@ -242,7 +242,7 @@ public sealed partial class CloningSystem : EntitySystem
clonePod.ActivelyCloning = true;
if (_jobs.MindTryGetJob(mindEnt, out _, out var prototype))
if (_jobs.MindTryGetJob(mindEnt, out var prototype))
foreach (var special in prototype.Special)
if (special is AddComponentSpecial)
special.AfterEquip(mob);

View File

@@ -1,11 +1,10 @@
using System.Linq;
using Content.Server.GameTicking;
using Content.Server.Paint;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Station.Systems;
using Content.Shared.CCVar;
using Content.Shared.Clothing.Loadouts.Prototypes;
using Content.Shared.Clothing.Loadouts.Systems;
using Content.Shared.GameTicking;
using Content.Shared.Inventory;
using Content.Shared.Item;
using Content.Shared.Players;

View File

@@ -22,7 +22,7 @@ public sealed class NotJobsRequirementSystem : EntitySystem
return;
// if player has no job then don't care
if (!TryComp<JobComponent>(args.MindId, out var job))
if (!TryComp<JobRoleComponent>(args.MindId, out var job))
return;
foreach (string forbidJob in comp.Jobs)
if (job.Prototype == forbidJob)

View File

@@ -77,7 +77,7 @@ public sealed class ParadoxAnomalySystem : EntitySystem
if (!_proto.TryIndex<SpeciesPrototype>(humanoid.Species, out var species))
continue;
if (_mind.GetMind(uid, mindContainer) is not {} mindId || !HasComp<JobComponent>(mindId))
if (_mind.GetMind(uid, mindContainer) is not {} mindId || !HasComp<JobRoleComponent>(mindId))
continue;
if (_role.MindIsAntagonist(mindId))
@@ -99,7 +99,7 @@ public sealed class ParadoxAnomalySystem : EntitySystem
return null;
var (uid, mindId, species, profile) = _random.Pick(candidates);
var jobId = Comp<JobComponent>(mindId).Prototype;
var jobId = Comp<JobRoleComponent>(mindId).Prototype;
var job = _proto.Index<JobPrototype>(jobId!);
// Find a suitable spawn point.

View File

@@ -1,4 +1,3 @@
using Content.Server.GenericAntag;
using Content.Server.Objectives.Components;
using Content.Server.Objectives.Systems;
using Content.Server.Popups;
@@ -9,10 +8,10 @@ using Content.Shared.Maps;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Systems;
using Content.Shared.NPC.Systems;
using Content.Shared.Zombies;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
@@ -26,10 +25,11 @@ public sealed partial class DragonSystem : EntitySystem
[Dependency] private readonly MovementSpeedModifierSystem _movement = default!;
[Dependency] private readonly NpcFactionSystem _faction = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly RoleSystem _role = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
private EntityQuery<CarpRiftsConditionComponent> _objQuery;
@@ -56,7 +56,6 @@ public sealed partial class DragonSystem : EntitySystem
SubscribeLocalEvent<DragonComponent, DragonSpawnRiftActionEvent>(OnSpawnRift);
SubscribeLocalEvent<DragonComponent, RefreshMovementSpeedModifiersEvent>(OnDragonMove);
SubscribeLocalEvent<DragonComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<DragonComponent, GenericAntagCreatedEvent>(OnCreated);
SubscribeLocalEvent<DragonComponent, EntityZombifiedEvent>(OnZombified);
}
@@ -95,7 +94,8 @@ public sealed partial class DragonSystem : EntitySystem
}
}
comp.RiftAccumulator += frameTime;
if (!_mobState.IsDead(uid))
comp.RiftAccumulator += frameTime;
// Delete it, naughty dragon!
if (comp.RiftAccumulator >= comp.RiftMaxAccumulator)
@@ -149,7 +149,7 @@ public sealed partial class DragonSystem : EntitySystem
// cant stack rifts near eachother
foreach (var (_, riftXform) in EntityQuery<DragonRiftComponent, TransformComponent>(true))
{
if (riftXform.Coordinates.InRange(EntityManager, _transform, xform.Coordinates, RiftRange))
if (_transform.InRange(riftXform.Coordinates, xform.Coordinates, RiftRange))
{
_popup.PopupEntity(Loc.GetString("carp-rift-proximity", ("proximity", RiftRange)), uid, uid);
return;
@@ -157,7 +157,7 @@ public sealed partial class DragonSystem : EntitySystem
}
// cant put a rift on solars
foreach (var tile in grid.GetTilesIntersecting(new Circle(xform.WorldPosition, RiftTileRadius), false))
foreach (var tile in _map.GetTilesIntersecting(xform.GridUid.Value, grid, new Circle(_transform.GetWorldPosition(xform), RiftTileRadius), false))
{
if (!tile.IsSpace(_tileDef))
continue;
@@ -193,18 +193,6 @@ public sealed partial class DragonSystem : EntitySystem
DeleteRifts(uid, false, component);
}
private void OnCreated(EntityUid uid, DragonComponent comp, ref GenericAntagCreatedEvent args)
{
var mindId = args.MindId;
var mind = args.Mind;
_role.MindAddRole(mindId, new DragonRoleComponent(), mind);
_role.MindAddRole(mindId, new RoleBriefingComponent()
{
Briefing = Loc.GetString("dragon-role-briefing")
}, mind);
}
private void OnZombified(Entity<DragonComponent> ent, ref EntityZombifiedEvent args)
{
// prevent carp attacking zombie dragon
@@ -240,7 +228,7 @@ public sealed partial class DragonSystem : EntitySystem
return;
var mind = Comp<MindComponent>(mindContainer.Mind.Value);
foreach (var objId in mind.AllObjectives)
foreach (var objId in mind.Objectives)
{
if (_objQuery.TryGetComponent(objId, out var obj))
{
@@ -262,7 +250,7 @@ public sealed partial class DragonSystem : EntitySystem
return;
var mind = Comp<MindComponent>(mindContainer.Mind.Value);
foreach (var objId in mind.AllObjectives)
foreach (var objId in mind.Objectives)
{
if (_objQuery.TryGetComponent(objId, out var obj))
{

View File

@@ -5,6 +5,7 @@ using Robust.Shared.Prototypes;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Mind;
namespace Content.Server.EntityEffects.EffectConditions;
@@ -15,15 +16,22 @@ public sealed partial class JobCondition : EntityEffectCondition
public override bool Condition(EntityEffectBaseArgs args)
{
args.EntityManager.TryGetComponent<MindContainerComponent>(args.TargetEntity, out var mindContainer);
if (mindContainer != null && mindContainer.Mind != null)
if ( mindContainer is null
|| !args.EntityManager.TryGetComponent<MindComponent>(mindContainer.Mind, out var mind))
return false;
foreach (var roleId in mind.MindRoles)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
if (args.EntityManager.TryGetComponent<JobComponent>(mindContainer?.Mind, out var comp) && prototypeManager.TryIndex(comp.Prototype, out var prototype))
{
foreach (var jobId in Job)
if (prototype.ID == jobId)
return true;
}
if(!args.EntityManager.HasComponent<JobRoleComponent>(roleId))
continue;
if(!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole)
|| mindRole.JobPrototype is null)
continue;
if (Job.Contains(mindRole.JobPrototype.Value))
return true;
}
return false;
@@ -35,5 +43,3 @@ public sealed partial class JobCondition : EntityEffectCondition
return Loc.GetString("reagent-effect-condition-guidebook-job-condition", ("job", ContentLocalizationManager.FormatListToOr(localizedNames)));
}
}

View File

@@ -11,7 +11,7 @@ public sealed partial class PlantAdjustPests : PlantAdjustAttribute
public override void Effect(EntityEffectBaseArgs args)
{
if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager))
if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager, mustHaveAlivePlant: false))
return;
plantHolderComp.PestLevel += Amount;

View File

@@ -11,7 +11,7 @@ public sealed partial class PlantAdjustToxins : PlantAdjustAttribute
public override void Effect(EntityEffectBaseArgs args)
{
if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager))
if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager, mustHaveAlivePlant: false))
return;
plantHolderComp.Toxins += Amount;

View File

@@ -11,7 +11,7 @@ public sealed partial class PlantAdjustWeeds : PlantAdjustAttribute
public override void Effect(EntityEffectBaseArgs args)
{
if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager))
if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager, mustHaveAlivePlant: false))
return;
plantHolderComp.WeedLevel += Amount;

View File

@@ -444,7 +444,7 @@ public sealed partial class ExplosionSystem
foreach (var (entity, damage) in _toDamage)
{
// TODO EXPLOSIONS turn explosions into entities, and pass the the entity in as the damage origin.
_damageableSystem.TryChangeDamage(entity, damage, ignoreResistances: true);
_damageableSystem.TryChangeDamage(entity, damage, ignoreResistances: true, partMultiplier: 0.3f); // Shitmed: Temp change, nerf explosion delimbing
}
}

View File

@@ -72,6 +72,55 @@ public sealed class SpraySystem : EntitySystem
if (diffPos == Vector2.Zero || diffPos == Vector2Helpers.NaN)
return;
// Lavaland Shitcode Start - You should spray yourself NOW.
// Too lazy to learn this system, so you get a copypaste job!
if ((clickMapPos.Position - userMapPos.Position).Length() < 0.5f)
{
// Split a portion of the solution for the self-spray
var adjustedSolutionAmount = entity.Comp.TransferAmount;
var newSolution = _solutionContainer.SplitSolution(soln.Value, adjustedSolutionAmount);
if (newSolution.Volume > 0)
{
// Spawn vapor with a slight offset to create movement
var offset = new Vector2(0.1f, 0); // Small offset to ensure collision
var vapor = Spawn(entity.Comp.SprayedPrototype, userMapPos.Offset(offset));
var vaporXform = xformQuery.GetComponent(vapor);
if (TryComp(vapor, out AppearanceComponent? appearance))
{
_appearance.SetData(vapor, VaporVisuals.Color, solution.GetColor(_proto).WithAlpha(1f), appearance);
_appearance.SetData(vapor, VaporVisuals.State, true, appearance);
}
var vaporComponent = Comp<VaporComponent>(vapor);
var ent = (vapor, vaporComponent);
_vapor.TryAddSolution(ent, newSolution);
// Create a slight movement effect
var rotation = Angle.FromDegrees(45);
var impulseDirection = -offset.Normalized();
var time = 0.5f; // Shorter duration for self-spray
var target = userMapPos.Offset(impulseDirection * 0.5f); // Small movement distance
_vapor.Start(ent, vaporXform, impulseDirection * 0.5f, entity.Comp.SprayVelocity, target, time, args.User);
if (TryComp<PhysicsComponent>(args.User, out var body))
{
if (_gravity.IsWeightless(args.User, body))
_physics.ApplyLinearImpulse(args.User, -impulseDirection.Normalized() * entity.Comp.PushbackAmount, body: body);
}
_audio.PlayPvs(entity.Comp.SpraySound, entity, entity.Comp.SpraySound.Params.WithVariation(0.125f));
if (useDelay != null)
_useDelay.TryResetDelay((entity, useDelay));
return;
}
}
// Lavaland Shitcode End
var diffNorm = diffPos.Normalized();
var diffLength = diffPos.Length();

View File

@@ -19,7 +19,7 @@ namespace Content.Server.GameTicking
/// How long before RoundStartTime do we load maps.
/// </summary>
[ViewVariables]
public TimeSpan RoundPreloadTime { get; } = TimeSpan.FromSeconds(15);
public TimeSpan RoundPreloadTime { get; } = TimeSpan.FromSeconds(20);
[ViewVariables]
private TimeSpan _pauseTime;

View File

@@ -4,6 +4,7 @@ using Content.Server.Discord;
using Content.Server.GameTicking.Events;
using Content.Server.Ghost;
using Content.Server.Maps;
using Content.Server.Roles;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.GameTicking;
@@ -27,6 +28,7 @@ namespace Content.Server.GameTicking
public sealed partial class GameTicker
{
[Dependency] private readonly DiscordWebhook _discord = default!;
[Dependency] private readonly RoleSystem _role = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly AnnouncerSystem _announcer = default!;
@@ -381,7 +383,7 @@ namespace Content.Server.GameTicking
var userId = mind.UserId ?? mind.OriginalOwnerUserId;
var connected = false;
var observer = HasComp<ObserverRoleComponent>(mindId);
var observer = _role.MindHasRole<ObserverRoleComponent>(mindId);
// Continuing
if (userId != null && _playerManager.ValidSessionId(userId.Value))
{
@@ -408,7 +410,7 @@ namespace Content.Server.GameTicking
_pvsOverride.AddGlobalOverride(GetNetEntity(entity.Value), recursive: true);
}
var roles = _roles.MindGetAllRoles(mindId);
var roles = _roles.MindGetAllRoleInfo(mindId);
var playerEndRoundInfo = new RoundEndMessageEvent.RoundEndPlayerInfo()
{

View File

@@ -2,8 +2,8 @@ using System.Globalization;
using System.Linq;
using System.Numerics;
using Content.Server.Administration.Managers;
using Content.Server.Administration.Systems;
using Content.Server.GameTicking.Events;
using Content.Server.Ghost;
using Content.Server.RandomMetadata;
using Content.Server.Spawners.Components;
using Content.Server.Speech.Components;
@@ -12,13 +12,12 @@ using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Dataset;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Players;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Silicon.Components;
using JetBrains.Annotations;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
@@ -33,6 +32,7 @@ namespace Content.Server.GameTicking
{
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly AdminSystem _admin = default!;
[ValidatePrototypeId<EntityPrototype>]
public const string ObserverPrototypeName = "MobObserver";
@@ -248,21 +248,10 @@ namespace Content.Server.GameTicking
_mind.SetUserId(newMind, data.UserId);
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
var job = new JobComponent { Prototype = jobId };
_roles.MindAddRole(newMind, job, silent: silent);
var jobName = _jobs.MindTryGetJobName(newMind);
_playTimeTrackings.PlayerRolesChanged(player);
// Delta-V: Add AlwaysUseSpawner.
var spawnPointType = SpawnPointType.Unset;
if (jobPrototype.AlwaysUseSpawner)
{
lateJoin = false;
spawnPointType = SpawnPointType.Job;
}
var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, job, character, spawnPointType: spawnPointType);
var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character);
DebugTools.AssertNotNull(mobMaybe);
var mob = mobMaybe!.Value;
@@ -283,6 +272,10 @@ namespace Content.Server.GameTicking
_mind.TransferTo(newMind, mob);
_roles.MindAddJobRole(newMind, silent: silent, jobPrototype:jobId);
var jobName = _jobs.MindTryGetJobName(newMind);
_admin.UpdatePlayerList(player);
if (lateJoin && !silent)
{
_chatSystem.DispatchStationAnnouncement(station,
@@ -303,13 +296,17 @@ namespace Content.Server.GameTicking
_stationJobs.TryAssignJob(station, jobPrototype, player.UserId);
if (lateJoin)
{
_adminLogger.Add(LogType.LateJoin,
LogImpact.Medium,
$"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
}
else
{
_adminLogger.Add(LogType.RoundStartJoin,
LogImpact.Medium,
$"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
}
// Make sure they're aware of extended access.
if (Comp<StationJobsComponent>(station).ExtendedAccess
@@ -324,28 +321,13 @@ namespace Content.Server.GameTicking
Loc.GetString("job-greet-station-name", ("stationName", metaData.EntityName)));
}
// Arrivals is unable to do this during spawning as no actor is attached yet.
// We also want this message last.
if (!silent && lateJoin && _arrivals.Enabled)
{
var arrival = _arrivals.NextShuttleArrival();
if (arrival == null)
{
_chatManager.DispatchServerMessage(player, Loc.GetString("latejoin-arrivals-direction"));
}
else
{
_chatManager.DispatchServerMessage(player,
Loc.GetString("latejoin-arrivals-direction-time", ("time", $"{arrival:mm\\:ss}")));
}
}
// We raise this event directed to the mob, but also broadcast it so game rules can do something now.
PlayersJoinedRoundNormally++;
var aev = new PlayerSpawnCompleteEvent(mob,
player,
jobId,
lateJoin,
silent,
PlayersJoinedRoundNormally,
station,
character);
@@ -410,7 +392,7 @@ namespace Content.Server.GameTicking
var (mindId, mindComp) = _mind.CreateMind(player.UserId, name);
mind = (mindId, mindComp);
_mind.SetUserId(mind.Value, player.UserId);
_roles.MindAddRole(mind.Value, new ObserverRoleComponent());
_roles.MindAddRole(mind.Value, "MindRoleObserver");
}
var ghost = _ghost.SpawnGhost(mind.Value);
@@ -565,68 +547,4 @@ namespace Content.Server.GameTicking
#endregion
}
/// <summary>
/// Event raised broadcast before a player is spawned by the GameTicker.
/// You can use this event to spawn a player off-station on late-join but also at round start.
/// When this event is handled, the GameTicker will not perform its own player-spawning logic.
/// </summary>
[PublicAPI]
public sealed class PlayerBeforeSpawnEvent : HandledEntityEventArgs
{
public ICommonSession Player { get; }
public HumanoidCharacterProfile Profile { get; }
public string? JobId { get; }
public bool LateJoin { get; }
public EntityUid Station { get; }
public PlayerBeforeSpawnEvent(ICommonSession player,
HumanoidCharacterProfile profile,
string? jobId,
bool lateJoin,
EntityUid station)
{
Player = player;
Profile = profile;
JobId = jobId;
LateJoin = lateJoin;
Station = station;
}
}
/// <summary>
/// Event raised both directed and broadcast when a player has been spawned by the GameTicker.
/// You can use this to handle people late-joining, or to handle people being spawned at round start.
/// Can be used to give random players a role, modify their equipment, etc.
/// </summary>
[PublicAPI]
public sealed class PlayerSpawnCompleteEvent : EntityEventArgs
{
public EntityUid Mob { get; }
public ICommonSession Player { get; }
public string? JobId { get; }
public bool LateJoin { get; }
public EntityUid Station { get; }
public HumanoidCharacterProfile Profile { get; }
// Ex. If this is the 27th person to join, this will be 27.
public int JoinOrder { get; }
public PlayerSpawnCompleteEvent(EntityUid mob,
ICommonSession player,
string? jobId,
bool lateJoin,
int joinOrder,
EntityUid station,
HumanoidCharacterProfile profile)
{
Mob = mob;
Player = player;
JobId = jobId;
LateJoin = lateJoin;
Station = station;
Profile = profile;
JoinOrder = joinOrder;
}
}
}

View File

@@ -87,5 +87,5 @@ public sealed partial class TraitorRuleComponent : Component
/// The amount of TC traitors start with.
/// </summary>
[DataField]
public FixedPoint2 StartingBalance = 20;
public int StartingBalance = 20;
}

View File

@@ -6,6 +6,7 @@ using Content.Server.Mind;
using Content.Server.Points;
using Content.Server.RoundEnd;
using Content.Server.Station.Systems;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Content.Shared.Points;
using Content.Shared.Storage;

View File

@@ -13,7 +13,6 @@ using Content.Server.RoundEnd;
using Content.Server.Shuttles.Events;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;
using Content.Server.Store.Components;
using Content.Server.Store.Systems;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
@@ -37,9 +36,9 @@ namespace Content.Server.GameTicking.Rules;
public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
{
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergency = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
[Dependency] private readonly StoreSystem _store = default!;
@@ -64,6 +63,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
SubscribeLocalEvent<NukeOperativeComponent, EntityZombifiedEvent>(OnOperativeZombified);
SubscribeLocalEvent<NukeOpsShuttleComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<NukeopsRoleComponent, GetBriefingEvent>(OnGetBriefing);
SubscribeLocalEvent<ConsoleFTLAttemptEvent>(OnShuttleFTLAttempt);
SubscribeLocalEvent<WarDeclaredEvent>(OnWarDeclared);
@@ -72,7 +72,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
SubscribeLocalEvent<NukeopsRuleComponent, AfterAntagEntitySelectedEvent>(OnAfterAntagEntSelected);
}
protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
protected override void Started(EntityUid uid,
NukeopsRuleComponent component,
GameRuleComponent gameRule,
GameRuleStartedEvent args)
{
var eligible = new List<Entity<StationEventEligibleComponent, NpcFactionMemberComponent>>();
@@ -92,7 +94,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
}
#region Event Handlers
protected override void AppendRoundEndText(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
protected override void AppendRoundEndText(EntityUid uid,
NukeopsRuleComponent component,
GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
var winText = Loc.GetString($"nukeops-{component.WinType.ToString().ToLower()}");
@@ -233,7 +237,8 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
// If the disk is currently at Central Command, the crew wins - just slightly.
// This also implies that some nuclear operatives have died.
SetWinType(ent, diskAtCentCom
SetWinType(ent,
diskAtCentCom
? WinType.CrewMinor
: WinType.OpsMinor);
ent.Comp.WinConditions.Add(diskAtCentCom
@@ -461,8 +466,11 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
: WinCondition.AllNukiesDead);
SetWinType(ent, WinType.CrewMajor, false);
_roundEndSystem.DoRoundEndBehavior(
nukeops.RoundEndBehavior, nukeops.EvacShuttleTime, nukeops.RoundEndTextSender, nukeops.RoundEndTextShuttleCall, nukeops.RoundEndTextAnnouncement);
_roundEndSystem.DoRoundEndBehavior(nukeops.RoundEndBehavior,
nukeops.EvacShuttleTime,
nukeops.RoundEndTextSender,
nukeops.RoundEndTextShuttleCall,
nukeops.RoundEndTextAnnouncement);
// prevent it called multiple times
nukeops.RoundEndBehavior = RoundEndBehavior.Nothing;
@@ -470,16 +478,22 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
private void OnAfterAntagEntSelected(Entity<NukeopsRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
if (ent.Comp.TargetStation is not { } station)
return;
var target = (ent.Comp.TargetStation is not null) ? Name(ent.Comp.TargetStation.Value) : "the target";
_antag.SendBriefing(args.Session, Loc.GetString("nukeops-welcome",
("station", station),
_antag.SendBriefing(args.Session,
Loc.GetString("nukeops-welcome",
("station", target),
("name", Name(ent))),
Color.Red,
ent.Comp.GreetSoundNotification);
}
private void OnGetBriefing(Entity<NukeopsRoleComponent> role, ref GetBriefingEvent args)
{
// TODO Different character screen briefing for the 3 nukie types
args.Append(Loc.GetString("nukeops-briefing"));
}
/// <remarks>
/// Is this method the shitty glue holding together the last of my sanity? yes.
/// Do i have a better solution? not presently.

View File

@@ -15,7 +15,6 @@ using Content.Shared.Database;
using Content.Shared.GameTicking.Components;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mindshield.Components;
using Content.Shared.Mobs;
@@ -38,8 +37,8 @@ namespace Content.Server.GameTicking.Rules;
public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleComponent>
{
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly EuiManager _euiMan = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
@@ -49,7 +48,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
[Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly IGameTiming _timing = default!;
//Used in OnPostFlash, no reference to the rule component is available
public readonly ProtoId<NpcFactionPrototype> RevolutionaryNpcFaction = "Revolutionary";
@@ -59,9 +58,12 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
{
base.Initialize();
SubscribeLocalEvent<CommandStaffComponent, MobStateChangedEvent>(OnCommandMobStateChanged);
SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
SubscribeLocalEvent<RevolutionaryRoleComponent, GetBriefingEvent>(OnGetBriefing);
SubscribeLocalEvent<HeadRevolutionaryComponent, AfterFlashedEvent>(OnPostFlash);
SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
SubscribeLocalEvent<RevolutionaryRoleComponent, GetBriefingEvent>(OnGetBriefing);
}
protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
@@ -85,7 +87,9 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
}
}
protected override void AppendRoundEndText(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule,
protected override void AppendRoundEndText(EntityUid uid,
RevolutionaryRuleComponent component,
GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
base.AppendRoundEndText(uid, component, gameRule, ref args);
@@ -101,7 +105,9 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
args.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", sessionData.Count)));
foreach (var (mind, data, name) in sessionData)
{
var count = CompOrNull<RevolutionaryRoleComponent>(mind)?.ConvertedCount ?? 0;
_role.MindHasRole<RevolutionaryRoleComponent>(mind, out var role);
var count = CompOrNull<RevolutionaryRoleComponent>(role)?.ConvertedCount ?? 0;
args.AddLine(Loc.GetString("rev-headrev-name-user",
("name", name),
("username", data.UserName),
@@ -113,10 +119,8 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
private void OnGetBriefing(EntityUid uid, RevolutionaryRoleComponent comp, ref GetBriefingEvent args)
{
if (!TryComp<MindComponent>(uid, out var mind) || mind.OwnedEntity == null)
return;
var head = HasComp<HeadRevolutionaryComponent>(mind.OwnedEntity);
var ent = args.Mind.Comp.OwnedEntity;
var head = HasComp<HeadRevolutionaryComponent>(ent);
args.Append(Loc.GetString(head ? "head-rev-briefing" : "rev-briefing"));
}
@@ -145,15 +149,20 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
if (ev.User != null)
{
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary");
_adminLogManager.Add(LogType.Mind,
LogImpact.Medium,
$"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary");
if (_mind.TryGetRole<RevolutionaryRoleComponent>(ev.User.Value, out var headrev))
headrev.ConvertedCount++;
if (_mind.TryGetMind(ev.User.Value, out var revMindId, out _))
{
if (_role.MindHasRole<RevolutionaryRoleComponent>(revMindId, out var role))
role.Value.Comp2.ConvertedCount++;
}
}
if (mindId == default || !_role.MindHasRole<RevolutionaryRoleComponent>(mindId))
{
_role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = RevPrototypeId });
_role.MindAddRole(mindId, "MindRoleRevolutionary");
}
if (mind?.Session != null)

View File

@@ -1,21 +1,13 @@
using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Objectives;
using Content.Server.Roles;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;
public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly ObjectivesSystem _objectives = default!;
public override void Initialize()
{
@@ -26,32 +18,33 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
SubscribeLocalEvent<ThiefRoleComponent, GetBriefingEvent>(OnGetBriefing);
}
private void AfterAntagSelected(Entity<ThiefRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
// Greeting upon thief activation
private void AfterAntagSelected(Entity<ThiefRuleComponent> mindId, ref AfterAntagEntitySelectedEvent args)
{
if (!_mindSystem.TryGetMind(args.EntityUid, out var mindId, out var mind))
return;
//Generate objectives
_antag.SendBriefing(args.EntityUid, MakeBriefing(args.EntityUid), null, null);
var ent = args.EntityUid;
_antag.SendBriefing(ent, MakeBriefing(ent), null, null);
}
//Add mind briefing
private void OnGetBriefing(Entity<ThiefRoleComponent> thief, ref GetBriefingEvent args)
// Character screen briefing
private void OnGetBriefing(Entity<ThiefRoleComponent> role, ref GetBriefingEvent args)
{
if (!TryComp<MindComponent>(thief.Owner, out var mind) || mind.OwnedEntity == null)
return;
var ent = args.Mind.Comp.OwnedEntity;
args.Append(MakeBriefing(mind.OwnedEntity.Value));
if (ent is null)
return;
args.Append(MakeBriefing(ent.Value));
}
private string MakeBriefing(EntityUid thief)
private string MakeBriefing(EntityUid ent)
{
var isHuman = HasComp<HumanoidAppearanceComponent>(thief);
var isHuman = HasComp<HumanoidAppearanceComponent>(ent);
var briefing = isHuman
? Loc.GetString("thief-role-greeting-human")
: Loc.GetString("thief-role-greeting-animal");
briefing += "\n \n" + Loc.GetString("thief-role-greeting-equipment") + "\n";
if (isHuman)
briefing += "\n \n" + Loc.GetString("thief-role-greeting-equipment") + "\n";
return briefing;
}
}

View File

@@ -10,9 +10,7 @@ using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.NPC.Systems;
using Content.Shared.Objectives.Components;
using Content.Shared.PDA;
using Content.Shared.Radio;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Roles.RoleCodeword;
@@ -28,17 +26,15 @@ namespace Content.Server.GameTicking.Rules;
public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
{
private static readonly Color TraitorCodewordColor = Color.FromHex("#cc3b3b");
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly ObjectivesSystem _objectives = default!;
[Dependency] private readonly SharedRoleCodewordSystem _roleCodewordSystem = default!;
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!; // WD EDIT
public override void Initialize()
@@ -46,7 +42,6 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
base.Initialize();
SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
}
@@ -76,7 +71,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component)
{
//Grab the mind if it wasnt provided
//Grab the mind if it wasn't provided
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
return false;
@@ -95,13 +90,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
{
// Calculate the amount of currency on the uplink.
var startingBalance = component.StartingBalance;
if (_jobs.MindTryGetJob(mindId, out _, out var job))
{
if (startingBalance < job.AntagAdvantage) // Can't use Math functions on FixedPoint2
startingBalance = 0;
else
startingBalance = startingBalance - job.AntagAdvantage;
}
if (_jobs.MindTryGetJob(mindId, out var prototype))
startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
// creadth: we need to create uplink for the antag.
// PDA should be in place already
@@ -119,14 +109,18 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
_antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code, issuer), null, component.GreetSoundNotification);
component.TraitorMinds.Add(mindId);
// Assign briefing
_roleSystem.MindAddRole(mindId, new RoleBriefingComponent
//Since this provides neither an antag/job prototype, nor antag status/roletype,
//and is intrinsically related to the traitor role
//it does not need to be a separate Mind Role Entity
_roleSystem.MindHasRole<TraitorRoleComponent>(mindId, out var traitorRole);
if (traitorRole is not null)
{
Briefing = briefing
}, mind, true);
AddComp<RoleBriefingComponent>(traitorRole.Value.Owner);
Comp<RoleBriefingComponent>(traitorRole.Value.Owner).Briefing = briefing;
}
// Send codewords to only the traitor client
var color = TraitorCodewordColor; // Fall back to a dark red Syndicate color if a prototype is not found

View File

@@ -3,6 +3,7 @@ using Content.Server.Announcements.Systems;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Popups;
using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
@@ -13,6 +14,7 @@ using Content.Shared.Mind;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Roles;
using Content.Shared.Zombies;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -22,26 +24,42 @@ namespace Content.Server.GameTicking.Rules;
public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
{
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly ZombieSystem _zombie = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly AnnouncerSystem _announcer = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly ZombieSystem _zombie = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InitialInfectedRoleComponent, GetBriefingEvent>(OnGetBriefing);
SubscribeLocalEvent<ZombieRoleComponent, GetBriefingEvent>(OnGetBriefing);
SubscribeLocalEvent<IncurableZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
}
protected override void AppendRoundEndText(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule,
private void OnGetBriefing(Entity<InitialInfectedRoleComponent> role, ref GetBriefingEvent args)
{
if (!_roles.MindHasRole<ZombieRoleComponent>(args.Mind.Owner))
args.Append(Loc.GetString("zombie-patientzero-role-greeting"));
}
private void OnGetBriefing(Entity<ZombieRoleComponent> role, ref GetBriefingEvent args)
{
args.Append(Loc.GetString("zombie-infection-greeting"));
}
protected override void AppendRoundEndText(EntityUid uid,
ZombieRuleComponent component,
GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
base.AppendRoundEndText(uid, component, gameRule, ref args);

View File

@@ -1,11 +1,12 @@
namespace Content.Server.Ghost
using Content.Shared.Roles;
namespace Content.Server.Ghost;
/// <summary>
/// This is used to mark Observers properly, as they get Minds
/// </summary>
[RegisterComponent]
public sealed partial class ObserverRoleComponent : BaseMindRoleComponent
{
/// <summary>
/// This is used to mark Observers properly, as they get Minds
/// </summary>
[RegisterComponent]
public sealed partial class ObserverRoleComponent : Component
{
public string Name => Loc.GetString("observer-role-name");
}
public string Name => Loc.GetString("observer-role-name");
}

View File

@@ -2,99 +2,113 @@
using Content.Server.Mind.Commands;
using Content.Shared.Customization.Systems;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Server.Ghost.Roles.Components
namespace Content.Server.Ghost.Roles.Components;
[RegisterComponent]
[Access(typeof(GhostRoleSystem))]
public sealed partial class GhostRoleComponent : Component
{
[RegisterComponent]
[Access(typeof(GhostRoleSystem))]
public sealed partial class GhostRoleComponent : Component
[DataField("name")] private string _roleName = "Unknown";
[DataField("description")] private string _roleDescription = "Unknown";
[DataField("rules")] private string _roleRules = "ghost-role-component-default-rules";
// Actually make use of / enforce this requirement?
// Why is this even here.
// Move to ghost role prototype & respect CCvars.GameRoleTimerOverride
[DataField("requirements")]
public List<CharacterRequirement>? Requirements;
/// <summary>
/// Whether the <see cref="MakeSentientCommand"/> should run on the mob.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField("makeSentient")]
public bool MakeSentient = true;
/// <summary>
/// The probability that this ghost role will be available after init.
/// Used mostly for takeover roles that want some probability of being takeover, but not 100%.
/// </summary>
[DataField("prob")]
public float Probability = 1f;
// We do this so updating RoleName and RoleDescription in VV updates the open EUIs.
[ViewVariables(VVAccess.ReadWrite)]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public string RoleName
{
[DataField("name")] private string _roleName = "Unknown";
[DataField("description")] private string _roleDescription = "Unknown";
[DataField("rules")] private string _roleRules = "ghost-role-component-default-rules";
[DataField("requirements")]
public List<CharacterRequirement>? Requirements;
/// <summary>
/// Whether the <see cref="MakeSentientCommand"/> should run on the mob.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField("makeSentient")]
public bool MakeSentient = true;
/// <summary>
/// The probability that this ghost role will be available after init.
/// Used mostly for takeover roles that want some probability of being takeover, but not 100%.
/// </summary>
[DataField("prob")]
public float Probability = 1f;
// We do this so updating RoleName and RoleDescription in VV updates the open EUIs.
[ViewVariables(VVAccess.ReadWrite)]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public string RoleName
get => Loc.GetString(_roleName);
set
{
get => Loc.GetString(_roleName);
set
{
_roleName = value;
EntitySystem.Get<GhostRoleSystem>().UpdateAllEui();
}
_roleName = value;
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
}
[ViewVariables(VVAccess.ReadWrite)]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public string RoleDescription
{
get => Loc.GetString(_roleDescription);
set
{
_roleDescription = value;
EntitySystem.Get<GhostRoleSystem>().UpdateAllEui();
}
}
[ViewVariables(VVAccess.ReadWrite)]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public string RoleRules
{
get => Loc.GetString(_roleRules);
set
{
_roleRules = value;
EntitySystem.Get<GhostRoleSystem>().UpdateAllEui();
}
}
[DataField("allowSpeech")]
[ViewVariables(VVAccess.ReadWrite)]
public bool AllowSpeech { get; set; } = true;
[DataField("allowMovement")]
[ViewVariables(VVAccess.ReadWrite)]
public bool AllowMovement { get; set; }
[ViewVariables(VVAccess.ReadOnly)]
public bool Taken { get; set; }
[ViewVariables]
public uint Identifier { get; set; }
/// <summary>
/// Reregisters the ghost role when the current player ghosts.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("reregister")]
public bool ReregisterOnGhost { get; set; } = true;
/// <summary>
/// If set, ghost role is raffled, otherwise it is first-come-first-serve.
/// </summary>
[DataField("raffle")]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public GhostRoleRaffleConfig? RaffleConfig { get; set; }
}
[ViewVariables(VVAccess.ReadWrite)]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public string RoleDescription
{
get => Loc.GetString(_roleDescription);
set
{
_roleDescription = value;
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
}
}
[ViewVariables(VVAccess.ReadWrite)]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public string RoleRules
{
get => Loc.GetString(_roleRules);
set
{
_roleRules = value;
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
}
}
/// <summary>
/// The mind roles that will be added to the mob's mind entity
/// </summary>
[DataField, Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // Don't make eye contact
public List<EntProtoId> MindRoles = new() { "MindRoleGhostRoleNeutral" };
[DataField]
public bool AllowSpeech { get; set; } = true;
[DataField]
public bool AllowMovement { get; set; }
[ViewVariables(VVAccess.ReadOnly)]
public bool Taken { get; set; }
[ViewVariables]
public uint Identifier { get; set; }
/// <summary>
/// Reregisters the ghost role when the current player ghosts.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("reregister")]
public bool ReregisterOnGhost { get; set; } = true;
/// <summary>
/// If set, ghost role is raffled, otherwise it is first-come-first-serve.
/// </summary>
[DataField("raffle")]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public GhostRoleRaffleConfig? RaffleConfig { get; set; }
/// <summary>
/// Job the entity will receive after adding the mind.
/// </summary>
[DataField("job")]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // also FIXME Friends
public ProtoId<JobPrototype>? JobProto = null;
}

View File

@@ -1,4 +1,7 @@
namespace Content.Server.Ghost.Roles.Components;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Server.Ghost.Roles.Components;
/// <summary>
/// This is used for a ghost role which can be toggled on and off at will, like a PAI.
@@ -6,33 +9,81 @@
[RegisterComponent, Access(typeof(ToggleableGhostRoleSystem))]
public sealed partial class ToggleableGhostRoleComponent : Component
{
[DataField("examineTextMindPresent")]
/// <summary>
/// The text shown on the entity's Examine when it is controlled by a player
/// </summary>
[DataField]
public string ExamineTextMindPresent = string.Empty;
[DataField("examineTextMindSearching")]
/// <summary>
/// The text shown on the entity's Examine when it is waiting for a controlling player
/// </summary>
[DataField]
public string ExamineTextMindSearching = string.Empty;
[DataField("examineTextNoMind")]
/// <summary>
/// The text shown on the entity's Examine when it has no controlling player
/// </summary>
[DataField]
public string ExamineTextNoMind = string.Empty;
[DataField("beginSearchingText")]
/// <summary>
/// The popup text when the entity (PAI/positronic brain) it is activated to seek a controlling player
/// </summary>
[DataField]
public string BeginSearchingText = string.Empty;
[DataField("roleName")]
/// <summary>
/// The name shown on the Ghost Role list
/// </summary>
[DataField]
public string RoleName = string.Empty;
[DataField("roleDescription")]
/// <summary>
/// The description shown on the Ghost Role list
/// </summary>
[DataField]
public string RoleDescription = string.Empty;
[DataField("wipeVerbText")]
/// <summary>
/// The introductory message shown when trying to take the ghost role/join the raffle
/// </summary>
[DataField]
public string RoleRules = string.Empty;
/// <summary>
/// A list of mind roles that will be added to the entity's mind
/// </summary>
[DataField]
public List<EntProtoId> MindRoles;
/// <summary>
/// The displayed name of the verb to wipe the controlling player
/// </summary>
[DataField]
public string WipeVerbText = string.Empty;
[DataField("wipeVerbPopup")]
/// /// <summary>
/// The popup message when wiping the controlling player
/// </summary>
[DataField]
public string WipeVerbPopup = string.Empty;
[DataField("stopSearchVerbText")]
/// <summary>
/// The displayed name of the verb to stop searching for a controlling player
/// </summary>
[DataField]
public string StopSearchVerbText = string.Empty;
[DataField("stopSearchVerbPopup")]
/// /// <summary>
/// The popup message when stopping to search for a controlling player
/// </summary>
[DataField]
public string StopSearchVerbPopup = string.Empty;
/// /// <summary>
/// The prototype ID of the job that will be given to the controlling mind
/// </summary>
[DataField("job")]
public ProtoId<JobPrototype>? JobProto;
}

View File

@@ -1,11 +1,15 @@
namespace Content.Server.Ghost.Roles;
using Content.Shared.Roles;
namespace Content.Server.Ghost.Roles;
/// <summary>
/// This is used for round end display of ghost roles.
/// It may also be used to ensure some ghost roles count as antagonists in future.
/// Added to mind role entities to tag that they are a ghostrole.
/// It also holds the name for the round end display
/// </summary>
[RegisterComponent]
public sealed partial class GhostRoleMarkerRoleComponent : Component
public sealed partial class GhostRoleMarkerRoleComponent : BaseMindRoleComponent
{
[DataField("name")] public string? Name;
//TODO does anything still use this? It gets populated by GhostRolesystem but I don't see anything ever reading it
[DataField] public string? Name;
}

View File

@@ -3,7 +3,6 @@ using Content.Server.Administration.Logs;
using Content.Server.EUI;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Ghost.Roles.Events;
using Content.Server.Ghost.Roles.Raffles;
using Content.Shared.Ghost.Roles.Raffles;
using Content.Server.Ghost.Roles.UI;
using Content.Server.Mind.Commands;
@@ -398,8 +397,8 @@ namespace Content.Server.Ghost.Roles
if (raffle.AllMembers.Add(player) && raffle.AllMembers.Count > 1
&& raffle.CumulativeTime.Add(raffle.JoinExtendsDurationBy) <= raffle.MaxDuration)
{
raffle.Countdown += raffle.JoinExtendsDurationBy;
raffle.CumulativeTime += raffle.JoinExtendsDurationBy;
raffle.Countdown += raffle.JoinExtendsDurationBy;
raffle.CumulativeTime += raffle.JoinExtendsDurationBy;
}
UpdateAllEui();
@@ -504,10 +503,14 @@ namespace Content.Server.Ghost.Roles
var newMind = _mindSystem.CreateMind(player.UserId,
EntityManager.GetComponent<MetaDataComponent>(mob).EntityName);
_roleSystem.MindAddRole(newMind, new GhostRoleMarkerRoleComponent { Name = role.RoleName });
_mindSystem.SetUserId(newMind, player.UserId);
_mindSystem.TransferTo(newMind, mob);
_roleSystem.MindAddRoles(newMind.Owner, role.MindRoles, newMind.Comp);
if (_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind!, out var markerRole))
markerRole.Value.Comp2.Name = role.RoleName;
}
/// <summary>

View File

@@ -51,15 +51,20 @@ public sealed class ToggleableGhostRoleSystem : EntitySystem
var ghostRole = EnsureComp<GhostRoleComponent>(uid);
EnsureComp<GhostTakeoverAvailableComponent>(uid);
//GhostRoleComponent inherits custom settings from the ToggleableGhostRoleComponent
ghostRole.RoleName = Loc.GetString(component.RoleName);
ghostRole.RoleDescription = Loc.GetString(component.RoleDescription);
ghostRole.RoleRules = Loc.GetString(component.RoleRules);
ghostRole.JobProto = component.JobProto;
ghostRole.MindRoles = component.MindRoles;
}
private void OnExamined(EntityUid uid, ToggleableGhostRoleComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (TryComp<MindContainerComponent>(uid, out var mind) && mind.HasMind)
{
args.PushMarkup(Loc.GetString(component.ExamineTextMindPresent));

View File

@@ -10,6 +10,7 @@ using Content.Shared._Shitmed.Body.Events; // Shitmed Change
using Content.Shared.CombatMode;
using Content.Shared.Damage.Systems;
using Content.Shared.Explosion;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Input;
@@ -95,7 +96,7 @@ namespace Content.Server.Hands.Systems
// Break any pulls
if (TryComp(uid, out PullerComponent? puller) && TryComp(puller.Pulling, out PullableComponent? pullable))
_pullingSystem.TryStopPull(puller.Pulling.Value, pullable);
_pullingSystem.TryStopPull(puller.Pulling.Value, pullable, ignoreGrab: true); // Goobstation edit added check for grab
var offsetRandomCoordinates = _transformSystem.GetMoverCoordinates(args.Target).Offset(_random.NextVector2(1f, 1.5f));
if (!ThrowHeldItem(args.Target, offsetRandomCoordinates))
@@ -202,6 +203,20 @@ namespace Content.Server.Hands.Systems
if (playerSession?.AttachedEntity is not { Valid: true } player || !Exists(player))
return false;
// Goobstation start
if (TryGetActiveItem(player, out var item) && TryComp<VirtualItemComponent>(item, out var virtComp))
{
var userEv = new VirtualItemDropAttemptEvent(virtComp.BlockingEntity, player, item.Value, true);
RaiseLocalEvent(player, userEv);
var targEv = new VirtualItemDropAttemptEvent(virtComp.BlockingEntity, player, item.Value, true);
RaiseLocalEvent(virtComp.BlockingEntity, targEv);
if (userEv.Cancelled || targEv.Cancelled)
return false;
}
// Goobstation end
return ThrowHeldItem(player, coordinates);
}
@@ -215,6 +230,18 @@ namespace Content.Server.Hands.Systems
hands.ActiveHandEntity is not { } throwEnt ||
!_actionBlockerSystem.CanThrow(player, throwEnt))
return false;
// Goobstation start added throwing for grabbed mobs, mnoved direction.
var direction = _transformSystem.ToMapCoordinates(coordinates).Position - _transformSystem.GetWorldPosition(player);
if (TryComp<VirtualItemComponent>(throwEnt, out var virt))
{
var userEv = new VirtualItemThrownEvent(virt.BlockingEntity, player, throwEnt, direction);
RaiseLocalEvent(player, userEv);
var targEv = new VirtualItemThrownEvent(virt.BlockingEntity, player, throwEnt, direction);
RaiseLocalEvent(virt.BlockingEntity, targEv);
}
// Goobstation end
if (_timing.CurTime < hands.NextThrowTime)
return false;
@@ -230,7 +257,6 @@ namespace Content.Server.Hands.Systems
throwEnt = splitStack.Value;
}
var direction = coordinates.ToMapPos(EntityManager, _transformSystem) - Transform(player).WorldPosition;
if (direction == Vector2.Zero)
return true;

View File

@@ -18,8 +18,6 @@ using Content.Server.Maps;
using Content.Server.Players.JobWhitelist;
using Content.Server.MoMMI;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Players;
using Content.Server.Players.JobWhitelist;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Players.RateLimiting;
using Content.Server.Preferences.Managers;

View File

@@ -4,7 +4,7 @@ using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Emp;
using Content.Server.GameTicking;
using Content.Shared.GameTicking;
using Content.Server.Medical.CrewMonitoring;
using Content.Server.Popups;
using Content.Server.Station.Systems;

View File

@@ -43,7 +43,7 @@ namespace Content.Server.Mind.Commands
builder.AppendFormat("player: {0}, mob: {1}\nroles: ", mind.UserId, mind.OwnedEntity);
var roles = _entities.System<SharedRoleSystem>();
foreach (var role in roles.MindGetAllRoles(mindId))
foreach (var role in roles.MindGetAllRoleInfo(mindId))
{
builder.AppendFormat("{0} ", role.Name);
}

View File

@@ -17,6 +17,15 @@ public sealed partial class NPCMeleeCombatComponent : Component
[ViewVariables]
public CombatStatus Status = CombatStatus.Normal;
/// <summary>
/// Lava edit - how much seconds does it take for a mob to begin attacking once in range.
/// This is to prevent instant attacks and give more time to dodge.
/// </summary>
// Lavaland Change Start
[ViewVariables] public float ChargeupDelay = 1f;
[ViewVariables] public float ChargeupTimer = 0f;
// Lavaland Change end
}
public enum CombatStatus : byte

View File

@@ -54,11 +54,12 @@ public sealed partial class NPCCombatSystem
continue;
}
Attack(uid, comp, curTime, physicsQuery, xformQuery);
Attack(uid, comp, curTime, frameTime, physicsQuery, xformQuery); // Lavaland Change - added frameTime
}
}
private void Attack(EntityUid uid, NPCMeleeCombatComponent component, TimeSpan curTime, EntityQuery<PhysicsComponent> physicsQuery, EntityQuery<TransformComponent> xformQuery)
// Lavaland Change - added frameTime
private void Attack(EntityUid uid, NPCMeleeCombatComponent component, TimeSpan curTime, float frameTime, EntityQuery<PhysicsComponent> physicsQuery, EntityQuery<TransformComponent> xformQuery)
{
component.Status = CombatStatus.Normal;
@@ -106,6 +107,16 @@ public sealed partial class NPCCombatSystem
if (weapon.NextAttack > curTime || !Enabled)
return;
// Lavaland Change Start
if (component.ChargeupTimer < component.ChargeupDelay)
{
component.ChargeupTimer += frameTime;
return;
}
component.ChargeupTimer = 0f;
// Lavaland Change End
if (_random.Prob(component.MissChance) &&
physicsQuery.TryGetComponent(component.Target, out var targetPhysics) &&
targetPhysics.LinearVelocity.LengthSquared() != 0f)

View File

@@ -33,7 +33,7 @@ namespace Content.Server.Objectives.Commands
}
shell.WriteLine($"Objectives for player {player.UserId}:");
var objectives = mind.AllObjectives.ToList();
var objectives = mind.Objectives.ToList();
if (objectives.Count == 0)
{
shell.WriteLine("None.");

View File

@@ -59,7 +59,7 @@ public sealed class HelpProgressConditionSystem : EntitySystem
if (!TryComp<MindComponent>(traitor, out var mind))
continue;
foreach (var objective in mind.AllObjectives)
foreach (var objective in mind.Objectives)
{
if (HasComp<HelpProgressConditionComponent>(objective))
removeList.Add(traitor);
@@ -88,7 +88,7 @@ public sealed class HelpProgressConditionSystem : EntitySystem
if (TryComp<MindComponent>(target, out var mind))
{
foreach (var objective in mind.AllObjectives)
foreach (var objective in mind.Objectives)
{
// this has the potential to loop forever, anything setting target has to check that there is no HelpProgressCondition.
var info = _objectives.GetInfo(objective, target, mind);

View File

@@ -90,7 +90,7 @@ public sealed class KillPersonConditionSystem : EntitySystem
foreach (var mind in allHumans)
{
// RequireAdminNotify used as a cheap way to check for command department
if (_job.MindTryGetJob(mind, out _, out var prototype) && prototype.RequireAdminNotify)
if (_job.MindTryGetJob(mind, out var prototype) && prototype.RequireAdminNotify)
allHeads.Add(mind);
}

View File

@@ -1,9 +1,10 @@
using Content.Server.Objectives.Components;
using Content.Server.Roles;
using Content.Server.Warps;
using Content.Shared.Objectives.Components;
using Content.Shared.Ninja.Components;
using Content.Shared.Roles;
using Robust.Shared.Random;
using Content.Server.Roles;
namespace Content.Server.Objectives.Systems;
@@ -16,6 +17,7 @@ public sealed class NinjaConditionsSystem : EntitySystem
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly NumberObjectiveSystem _number = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
public override void Initialize()
{
@@ -46,10 +48,8 @@ public sealed class NinjaConditionsSystem : EntitySystem
// spider charge
private void OnSpiderChargeRequirementCheck(EntityUid uid, SpiderChargeConditionComponent comp, ref RequirementCheckEvent args)
{
if (args.Cancelled || !HasComp<NinjaRoleComponent>(args.MindId))
{
if (args.Cancelled || !_roles.MindHasRole<NinjaRoleComponent>(args.MindId))
return;
}
// choose spider charge detonation point
var warps = new List<EntityUid>();

View File

@@ -21,7 +21,7 @@ public sealed class NotCommandRequirementSystem : EntitySystem
return;
// cheap equivalent to checking that job department is command, since all command members require admin notification when leaving
if (_job.MindTryGetJob(args.MindId, out _, out var prototype) && prototype.RequireAdminNotify)
if (_job.MindTryGetJob(args.MindId, out var prototype) && prototype.RequireAdminNotify)
args.Cancelled = true;
}
}

View File

@@ -8,6 +8,8 @@ namespace Content.Server.Objectives.Systems;
/// </summary>
public sealed class NotJobRequirementSystem : EntitySystem
{
[Dependency] private readonly SharedJobSystem _jobs = default!;
public override void Initialize()
{
base.Initialize();
@@ -20,11 +22,10 @@ public sealed class NotJobRequirementSystem : EntitySystem
if (args.Cancelled)
return;
// if player has no job then don't care
if (!TryComp<JobComponent>(args.MindId, out var job))
return;
_jobs.MindTryGetJob(args.MindId, out var proto);
if (job.Prototype == comp.Job)
// if player has no job then don't care
if (proto is not null && proto.ID == comp.Job)
args.Cancelled = true;
}
}

View File

@@ -23,7 +23,7 @@ public sealed class ObjectiveBlacklistRequirementSystem : EntitySystem
if (args.Cancelled)
return;
foreach (var objective in args.Mind.AllObjectives)
foreach (var objective in args.Mind.Objectives)
{
if (_whitelistSystem.IsBlacklistPass(comp.Blacklist, objective))
{

View File

@@ -22,8 +22,6 @@ public sealed class RoleRequirementSystem : EntitySystem
if (args.Cancelled)
return;
// this whitelist trick only works because roles are components on the mind and not entities
// if that gets reworked then this will need changing
if (_whitelistSystem.IsWhitelistFail(comp.Roles, args.MindId))
args.Cancelled = true;
}

View File

@@ -31,17 +31,16 @@ namespace Content.Server.Players.PlayTimeTracking;
/// </summary>
public sealed class PlayTimeTrackingSystem : EntitySystem
{
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IAfkManager _afk = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly MindSystem _minds = default!;
[Dependency] private readonly PlayTimeTrackingManager _tracking = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly CharacterRequirementsSystem _characterRequirements = default!;
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
public override void Initialize()
{
@@ -52,8 +51,8 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundEnd);
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<RoleAddedEvent>(OnRoleAdd);
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleRemove);
SubscribeLocalEvent<RoleAddedEvent>(OnRoleEvent);
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleEvent);
SubscribeLocalEvent<AFKEvent>(OnAFK);
SubscribeLocalEvent<UnAFKEvent>(OnUnAFK);
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
@@ -105,10 +104,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
public IEnumerable<string> GetTimedRoles(EntityUid mindId)
{
var ev = new MindGetAllRolesEvent(new List<RoleInfo>());
RaiseLocalEvent(mindId, ref ev);
foreach (var role in ev.Roles)
foreach (var role in _roles.MindGetAllRoleInfo(mindId))
{
if (string.IsNullOrWhiteSpace(role.PlayTimeTrackerId))
continue;
@@ -127,13 +123,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
return GetTimedRoles(contentData.Mind.Value);
}
private void OnRoleRemove(RoleRemovedEvent ev)
{
if (_minds.TryGetSession(ev.Mind, out var session))
_tracking.QueueRefreshTrackers(session);
}
private void OnRoleAdd(RoleAddedEvent ev)
private void OnRoleEvent(RoleEvent ev)
{
if (_minds.TryGetSession(ev.Mind, out var session))
_tracking.QueueRefreshTrackers(session);
@@ -226,7 +216,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
job,
EntityManager,
_prototypes,
_config,
_cfg,
out _);
}
@@ -257,7 +247,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
job,
EntityManager,
_prototypes,
_config,
_cfg,
out _))
continue;
@@ -304,7 +294,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
jobber,
EntityManager,
_prototypes,
_config,
_cfg,
out _))
{
jobs.RemoveSwap(i);

View File

@@ -11,3 +11,5 @@ public sealed partial class CommandStaffComponent : Component
public float PsionicBonusModifier = 1;
public float PsionicBonusOffset = 0.25f;
}
//TODO this should probably be on a mind role, not the mob

View File

@@ -4,9 +4,9 @@ using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Role used to keep track of space dragons for antag purposes.
/// Added to mind role entities to tag that they are a space dragon.
/// </summary>
[RegisterComponent, Access(typeof(DragonSystem)), ExclusiveAntagonist]
public sealed partial class DragonRoleComponent : AntagonistRoleComponent
[RegisterComponent, Access(typeof(DragonSystem))]
public sealed partial class DragonRoleComponent : BaseMindRoleComponent
{
}

View File

@@ -2,8 +2,11 @@ using Content.Shared.Roles;
namespace Content.Server.Roles;
[RegisterComponent, ExclusiveAntagonist]
public sealed partial class InitialInfectedRoleComponent : AntagonistRoleComponent
/// <summary>
/// Added to mind role entities to tag that they are an initial infected.
/// </summary>
[RegisterComponent]
public sealed partial class InitialInfectedRoleComponent : BaseMindRoleComponent
{
}

View File

@@ -19,10 +19,25 @@ public sealed class JobSystem : SharedJobSystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MindComponent, MindRoleAddedEvent>(MindOnDoGreeting);
SubscribeLocalEvent<RoleAddedEvent>(OnRoleAddedEvent);
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleRemovedEvent);
}
private void MindOnDoGreeting(EntityUid mindId, MindComponent component, ref MindRoleAddedEvent args)
private void OnRoleAddedEvent(RoleAddedEvent args)
{
MindOnDoGreeting(args.MindId, args.Mind, args);
if (args.RoleTypeUpdate)
_roles.RoleUpdateMessage(args.Mind);
}
private void OnRoleRemovedEvent(RoleRemovedEvent args)
{
if (args.RoleTypeUpdate)
_roles.RoleUpdateMessage(args.Mind);
}
private void MindOnDoGreeting(EntityUid mindId, MindComponent component, RoleAddedEvent args)
{
if (args.Silent)
return;
@@ -30,7 +45,7 @@ public sealed class JobSystem : SharedJobSystem
if (!_mind.TryGetSession(mindId, out var session))
return;
if (!MindTryGetJob(mindId, out _, out var prototype))
if (!MindTryGetJob(mindId, out var prototype))
return;
_chat.DispatchServerMessage(session, Loc.GetString("job-greet-introduce-job-name",
@@ -47,6 +62,6 @@ public sealed class JobSystem : SharedJobSystem
if (MindHasJobWithId(mindId, jobPrototypeId))
return;
_roles.MindAddRole(mindId, new JobComponent { Prototype = jobPrototypeId });
_roles.MindAddJobRole(mindId, null, false, jobPrototypeId);
}
}

View File

@@ -2,7 +2,10 @@ using Content.Shared.Roles;
namespace Content.Server.Roles;
[RegisterComponent, ExclusiveAntagonist]
public sealed partial class NinjaRoleComponent : AntagonistRoleComponent
/// <summary>
/// Added to mind role entities to tag that they are a space ninja.
/// </summary>
[RegisterComponent]
public sealed partial class NinjaRoleComponent : BaseMindRoleComponent
{
}

View File

@@ -3,9 +3,9 @@ using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind entities to tag that they are a nuke operative.
/// Added to mind role entities to tag that they are a nuke operative.
/// </summary>
[RegisterComponent, ExclusiveAntagonist]
public sealed partial class NukeopsRoleComponent : AntagonistRoleComponent
[RegisterComponent]
public sealed partial class NukeopsRoleComponent : BaseMindRoleComponent
{
}

Some files were not shown because too many files have changed in this diff Show More