Upstream, Yay (#27)
* Make BaseMedicalPDA abstract (#26567) * Fix GasMixers/Filters not working (#26568) * Fix GasMixers/Filters not working * OKAY GAS FILTERS TOO --------- Co-authored-by: Plykiya <plykiya@protonmail.com> * Industrial Reagent Grinder Hotfix (#26571) fixed * Give stores the ability to check for owner only (#26573) adds a check if the store belongs to the user * Fix round start crash (causing instant restart) (#26579) * Fix round start crash * Make `TryCreateObjective` more error tolerant * Update engine to v217.1.0 (#26588) * Fix initial infected icon hiding (#26585) * Fix Meta evac shuttle name (#26587) * Make timer ignore client predict setting (#26554) * Make timer ignore client predict setting * making tests run --------- Co-authored-by: wrexbe <wrexbe@protonmail.com> * Make advertise system survive no map inits (#26553) * Make advertise system survive no map inits * Add comment to try prevent future bugs * Update Credits (#26589) Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com> * Fix fox spawn on reach (#26584) Co-authored-by: wrexbe <wrexbe@protonmail.com> * Removes SCAF armor (#26566) * removes scaf armor * replace maint loot spawner spot with basic helmet * Update Patrons.yml (#26578) * Automatic changelog update * Make aghost command work on other players using optional argument (#26546) * Translations * Make aghost command work on other players using optional argument * Reviews * Automatic changelog update * Add new component to Make sound on interact (#26523) * Adds new Component: EmitSoundOnInteractUsing * Missed an import * File-scoping * Replace ID check with Prototype check * Moved component and system to shared. Set prediction to true. * Removed impoper imports and changed namespace of component to reflect changed folder. * Following function naming theme * All this code is basically deltanedas's, but it was a learning experience for me * Update Content.Shared/Sound/Components/EmitSoundOnInteractUsingComponent.cs * Update Content.Shared/Sound/Components/EmitSoundOnInteractUsingComponent.cs --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> * Increase syndi duffelbag storage (#26565) * Increase syndi duffelbag storage * weh * Automatic changelog update * Adds construction/decon graphs for plastic flaps (#26341) * Adds construction/decon graphs for plastic flaps * Dang arbitrage * undo conflict --------- Co-authored-by: Velcroboy <velcroboy333@hotmail.com> * Automatic changelog update * Makes secglasses roundstart (#26487) * makes secglasses roundstart * fix epic fail * fix tests questionmark? * Update Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> --------- Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> * Automatic changelog update * Toilet Upgrade (needs review) (#22133) * Toilet Draft * fixes * toilets now have secret stash to place items in cistern. * fixes * plungers now unblock toilets. * fix sprite * new sprites and fix * fixes * improve seat sprites. * fix * removed visualisersystem changed to genericvisualizers * flush sound for toilets and copyright for toilet sprites. * fix atrributions * fixes * fix datafield flushtime * sprite improvements * fixes * multiple changes * fix * fix * fixes remove vv * moved stash related functions to secret stash system from toilet. * fix * fix * changes for recent review. * fix * fix * Automatic changelog update * Uplink store interface searchable with a searchbar. (#24287) * Can now search the uplink store interface with a searchbar. * Search text updates no longer send server messages. Persists listings locally. * Formatting fixes and tidying. * Added helper method to get localised name and description (or otherwise, entity name and description) of store listing items. * Update Content.Client/Store/Ui/StoreMenu.xaml * Review change; moved localisation helper functions to their own class. * Prevent thread-unsafe behaviour as-per review. * Remove dummy boxcontainer --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com> * Automatic changelog update * Improved RCDs (#22799) * Initial radial menu prototyping for the RCD * Radial UI buttons can send messages to the server * Beginning to update RCDSystem * RCD building system in progress * Further updates * Added extra effects, RCDSystem now reads RCD prototype data * Replacing tiles is instant, multiple constructions are allowed, deconstruction is broken * Added extra functionality to RadialContainers plus documentation * Fixed localization of RCD UI strings * Menu opens near cursor, added basic RCD * Avoiding merge conflict * Implemented atomized construction / deconstruction rules * Increased RCD ammo base charges * Moved input context definition to content * Removed obsoleted code * Updates to system * Switch machine and computer frames for electrical cabling * Added construction ghosts * Fixed issue with keybind detection code * Fixed RCD construction ghost mispredications * Code clean up * Updated deconstruction effects * RCDs effects don't rotate * Code clean up * Balancing for ammo counts * Code clean up * Added missing localized strings * More clean up * Made directional window handling more robust * Added documentation to radial menus and made them no longer dependent on Content * Made radial containers more robust * Further robustness to the radial menu * The RCD submenu buttons are only shown when the destination layer has at least one children * Expanded upon deconstructing plus construction balance * Fixed line endings * Updated list of RCD deconstructable entities. Now needs a component to deconstruct instead of a tag * Bug fixes * Revert unnecessary change * Updated RCD strings * Fixed bug * More fixes * Deconstructed tiles/subflooring convert to lattice instead * Fixed failed tests (Linux doesn't like invalid spritespecifer paths) * Fixing merge conflict * Updated airlock assembly * Fixing merge conflict * Fixing merge conflict * More fixing... * Removed erroneous project file change * Fixed string handling issue * Trying to fix merge conflict * Still fixing merge conflicts * Balancing * Hidden RCD construction ghosts when in 'build' mode * Fixing merge conflict * Implemented requested changes (Part 1) * Added more requested changes * Fix for failed test. Removed sussy null suppression * Made requested changes - custom construction ghost system was replaced * Fixing merge conflict * Fixed merge conflict * Fixed bug in RCD construction ghost validation * Fixing merge conflict * Merge conflict fixed * Made required update * Removed lingering RCD deconstruct tag * Fixing merge conflict * Merge conflict fixed * Made requested changes * Bug fixes and balancing * Made string names more consistent * Can no longer stack catwalks * Automatic changelog update * Update submodule to 217.2.0 (#26592) * Southern accent (#26543) * created the AccentComponent and the AccentSystem * word replacement schtuhff * made it a trait fr ongg!!1 * Update Content.Server/Speech/EntitySystems/SouthernAccentSystem.cs --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> * Prevent storing liquids in equipped buckets (#24412) * Block access to solutions in equipped spillables. * Stop Drink verb appearing if the solution can't be accessed. * Automatic changelog update * Fix 'Hypopen shouldn't display solution examine text' (#26453) * stealthy hypo * ExaminableSolution hand check when in covert implement. ExaminableSolution now has 'hidden' datafield to enable chemical inspection only in hand. * cleaning code * more cleaning * Hidden datafield renamed to HeldOnly * review --------- Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com> * Automatic changelog update * Revert Paint (#26593) * Revert "Fix build (#26258)" This reverts commit6de5fbfafb. * Revert "Spray Paint (Review Ready) (#23003)" This reverts commite4d5e7f1ae. # Conflicts: # Resources/Prototypes/Entities/Structures/Holographic/projections.yml * Fix: Prevent single-use hyposprays from getting the toggle draw verb (#26595) Prevent single-use hyposprays from getting the toggle draw verb Co-authored-by: Plykiya <plykiya@protonmail.com> * MeleeHitSoundSystem (#25005) * Began work to unscrew melee noises * finished * cleanup * cleanup * Update Content.Server/Weapons/Melee/MeleeWeaponSystem.cs Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com> * _Style * Fix merge --------- Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com> * Remove physics comp from VendingMachineWallmount (#25632) * Remove physics comp from VendingMachineWallmount * Fixtures removal --------- Co-authored-by: Jeff <velcroboy333@hotmail.com> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com> * Remake hairflowers (#25475) * Add more lily usage (orange hairflower and flowercrown) * comit 2 * ee * more fixes * w * im stupid * bring poppy in authodrobe * weh --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> * Automatic changelog update * Injector UI shows TransferAmount change, Spilling liquid changes Injector mode (#26596) * Injector UI shows TransferAmount change, spill changes mode * Update Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs * Update Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs --------- Co-authored-by: Plykiya <plykiya@protonmail.com> Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> * Update submodule to 217.2.1 (#26599) * disallow unanchoring or opening panels on locked emitters/APEs (#26600) * disallow unanchoring or opening panels on locked emitters/APEs * no locking open panels * oops * needback feedback * Update Content.Shared/Lock/LockSystem.cs * Update Content.Shared/Lock/LockSystem.cs * Update Content.Shared/Lock/LockSystem.cs * sanity --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com> * Automatic changelog update * Fix grave digging sound indefinitely playing if dug by aghost. (#26420) Admins bypass doafters. As such, the code that runs on doafter completion is ran before the sound is actually created. This then leads to the sound never being stopped, and as such it would infinitely play. This commit gets around the issue by manually stopping the sound should the doafter fail to start. If we could be sure that the doafter would never fail, then we could just move the call to StartDigging above starting the doafter but this is currently not possible. Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com> * Make the buttons on the map ui not squished (#26604) Make the map ui work Co-authored-by: wrexbe <wrexbe@protonmail.com> * Combine flower crown and wreath (#26605) * Combine flower crown and wreath * huh * huuh :trollface: * Automatic changelog update * Add AP damage to throwing knives (#26380) * add * ap * no more stam dmg * Automatic changelog update * cancelable brig timers (#26557) brig timers now cancelable. also some screensystem yakshave * Fix orientation of roller skate sprites (#26627) Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es> * Automatic changelog update * Fix GastTileOverlay sending redundant data (#26623) Fix GastTileOverlay not updating properly * Auto DeAdmin sooner (#26551) Co-authored-by: wrexbe <wrexbe@protonmail.com> * Add briefcase to curadrobe and lawdrobe, and some briefcases cleanup (#26527) * Add briefcase to curadrobe and some briefcases cleanup * also add to lawdrobe * Automatic changelog update * Fix some text overflow bugs in HUD (#26615) * Don't clip text in item status * Fix overflow in examine tooltip --------- Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es> * Adds two milk cartons to the BoozeOMat (#26635) * Automatic changelog update * made the hover text less vague (sorry) (#26630) * blacklisted throwing knifes from pneumatic cannon (#26628) * Fix radio jammer not blocking suit sensors. (#26632) As it turns out, they are not in fact on their own netid. They are actually just on wireless. The way I had tested my previous pr led to this mistake being made. I originally had the radio jammer block wireless as well, but decided to take out under the flase assumption that it suit sensors were actually on their own netid and did not require the ability to block all wireless packets at the last moment. * Fix dirt decals in reach not being cleanable (#26636) made all dirt decals cleanable Co-authored-by: hamurlik <renoDeath@protonmail.com> * Automatic changelog update * Replace drill_hit.ogg and drill_use.ogg with better sounds (#26622) * Replace drill_hit.ogg and drill_use.ogg with better sounds * Fix attribution source for drill_hit.ogg * Update Resources/Audio/Items/attributions.yml Co-authored-by: Kara <lunarautomaton6@gmail.com> * Update Resources/Audio/Items/attributions.yml Co-authored-by: Kara <lunarautomaton6@gmail.com> --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: Kara <lunarautomaton6@gmail.com> * Gave Blast door access permissions (#26606) Added access reader to all blast doors. Added pre configured blast doors for engineering and science. * Gives all wheeled objects low friction (#26601) * gives all wheeled objects friction * adjustments to sum stuff * Automatic changelog update * Combine solution injection systems; Fix embeddable injectors (#26268) * Combine injection systems * Update Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> * Automatic changelog update * Add ValueList import (#26640) * Change assault borg modules texture (#26502) * Update borg_modules.yml * Fix borg_modules.yml? * Uh --------- Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com> * Automatic changelog update * Add Cyborg Emote Sounds (#26594) * Hal 9000's first emote * Add Chime emote & Change variation to 0.05 * Modify Buzz emote * Add Buzz-two emote * modified Horn * add ping emote * add slowclap emote * Convert slowclap.ogg to mono, reflect change in attribution.yml * fix capitalization for all chatMessages && change all catagory to category * remove all traces of slowclap.ogg * forgor one file smh * collating copywrite * spelling mistakes will be the death of me * more spelling mistakes * change yml string to list * Automatic changelog update * Coordinates Disks & Shuttle FTL Travel (#23240) * Adds the CentComm Disk and configures it to work with direct-use shuttles * Added functionality for drone shuttles (i.e. cargo shuttle) * Adds support for pods, and a disk console object for disks to be inserted into. Also sprites. * Added the disk to HoP's locker * Removed leftover logs & comments * Fix for integration test * Apply suggestions from code review (formatting & proper DataField) Co-authored-by: 0x6273 <0x40@keemail.me> * Fix integration test & changes based on code review * Includes Disk Cases to contain Coordinate Disks, which are now CDs instead of Floppy Disks * Check pods & non-evac shuttles for CentCom travel, even in FTL * Import * Remove CentCom travel restrictions & pod disk consoles * Major changes that changes the coordinates disk system to work with salvage expeditions * Missed CC diskcase removal * Fix build * Review suggestions and changes * Major additional changes after merge * Minor tag miss * Integration test fix * review --------- Co-authored-by: 0x6273 <0x40@keemail.me> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com> * Automatic changelog update * Add door electronics access configuration menu (#17778) * Add door electronics configuration menu * Use file-scoped namespaces Signed-off-by: c4llv07e <kseandi@gmail.com> * Open door electronics configuration menu only with network configurator Signed-off-by: c4llv07e <kseandi@gmail.com> * Doors will now try to move their AccessReaderComponent to their door electronics when the map is initialized Signed-off-by: c4llv07e <kseandi@gmail.com> * Make the access list in the id card computer a separate control Signed-off-by: c4llv07e <kseandi@gmail.com> * Fix merge conflict Signed-off-by: c4llv07e <kseandi@gmail.com> * Remove DoorElectronics tag Signed-off-by: c4llv07e <kseandi@gmail.com> * Integrate doors with #17927 Signed-off-by: c4llv07e <kseandi@gmail.com> * Move door electornics ui stuff to the right place Signed-off-by: c4llv07e <kseandi@gmail.com> * Some review fixes Signed-off-by: c4llv07e <kseandi@gmail.com> * More fixes Signed-off-by: c4llv07e <kseandi@gmail.com> * review fix Signed-off-by: c4llv07e <kseandi@gmail.com> * move all accesses from airlock prototypes to door electronics Signed-off-by: c4llv07e <kseandi@gmail.com> * rework door electronics config access list Signed-off-by: c4llv07e <kseandi@gmail.com> * Remove Linq from the door electronics user interface * [WIP] Add EntityWhitelist to the activatable ui component Signed-off-by: c4llv07e <kseandi@gmail.com> * Better interaction system Signed-off-by: c4llv07e <kseandi@gmail.com> * Refactor Signed-off-by: c4llv07e <kseandi@gmail.com> * Fix some door electronics not working without AccessReaderComponent Signed-off-by: c4llv07e <kseandi@gmail.com> * Move AccessReaderComponent update code to the AccessReaderSystem Signed-off-by: c4llv07e <kseandi@gmail.com> * Remove unnecesary newlines in the door access prototypes Signed-off-by: c4llv07e <kseandi@gmail.com> * Remove unused variables in access level control Signed-off-by: c4llv07e <kseandi@gmail.com> * Remove unnecessary method from the door electronics configuration menu Signed-off-by: c4llv07e <kseandi@gmail.com> * [WIP] change access type from string to ProtoId<AccessLevelPrototype> Signed-off-by: c4llv07e <kseandi@gmail.com> * Remove unused methods Signed-off-by: c4llv07e <kseandi@gmail.com> * Newline fix Signed-off-by: c4llv07e <kseandi@gmail.com> * Restored to a functional state Signed-off-by: c4llv07e <kseandi@gmail.com> * Fix access configurator not working with door electronics AccessReaderComponent Signed-off-by: c4llv07e <kseandi@gmail.com> * Replace all string access fields with ProtoId Signed-off-by: c4llv07e <kseandi@gmail.com> * move access level control initialization into Populate method Signed-off-by: c4llv07e <kseandi@gmail.com> * Review --------- Signed-off-by: c4llv07e <kseandi@gmail.com> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com> * Automatic changelog update * scoopable ash and foam, solution transfer prediction (#25832) * move SolutionTransfer to shared and predict as much as possible * fully move OpenableSystem to shared now that SolutionTransfer is * fix imports for everything * doc for solution transfer system * trolling * add scoopable system * make ash and foam scoopable * untroll * untroll real * make clickable it work * troll * the scooping room --------- Co-authored-by: deltanedas <@deltanedas:kde.org> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com> * Replace the teleportation logic on the SCRAM implant! (#26429) * Replace the teleportation logic on the SCRAM implant! Now instead of just trying to pick a random tile in range 20 times, the scram teleportation logic now: - Gets a list of grids in range - Until a suitable tile is picked it picks a random grid - From that grid it picks a random tile. - If the tile is suitable, then it is set as the target and the user will be teleported there. - Grids and tiles are randomly picked as outlined above until a valid tile is found, or all valid grids and tiles are exhausted. - Should no suitable tile be found then they get teleported to the same position they are at. Effectively not teleporting them. * Actually make the defaults sane which I forgor in the last commit * Extract tile section to its own function. Bias selection for current grid. Use proper coords for box. * Address reviews as much as possible * Address reviews * Refactored AdvertiseComponent (#26598) * Made it better * ok * alright --------- Co-authored-by: wrexbe <wrexbe@protonmail.com> * Bartender "Essentials" (#25367) * drinks round 1 saving my progress before my hard drive explodes * test 2 please work * name fixes whoops * Update drinks.yml * various fixes am dumb * add sol dry to vends more fixes and changes, yippee! * more fixes & ingame testing shrimple tests * last fixes :trollface: should be ready for pr now * Update soda.yml sate thirst * Automatic changelog update * Add ERT Chaplain (#25956) * ERT Chaplain * Make BibleUser * It was not intended * reword my poor words * 1984 a comment that I decided was unnecessary. * Update Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> * Changes in chemicals page in guidebook (#25831) * Added pages to chemical categories The chemical categories have their own page now. Added the "Chemical Tabs" in /ServerInfo/Guidebook. Moved the Chemicals code from shiftsandjobs.yml to its own .yml file which is "chemicals.yml". * Update guides.ftl * Update chemicals.yml Changed the guide entry's ID for the medical tab from Medicine to Medicinal. Hope this works... * Update Resources/ServerInfo/Guidebook/Chemical Tabs/Biological.xml Co-authored-by: exincore <me@exin.xyz> * Update Resources/ServerInfo/Guidebook/Chemical Tabs/Foods.xml Co-authored-by: exincore <me@exin.xyz> * Update Resources/ServerInfo/Guidebook/Chemical Tabs/Elements.xml Co-authored-by: exincore <me@exin.xyz> * Update Resources/ServerInfo/Guidebook/Chemical Tabs/Narcotics.xml Co-authored-by: exincore <me@exin.xyz> * Update Resources/ServerInfo/Guidebook/Chemical Tabs/Toxins.xml Co-authored-by: exincore <me@exin.xyz> * Fixed a few errors and stuff! A few typos have been fixed thanks to exincore. Added dedicated .xml files to be used for the dedicated category pages (Medicinal and Botanical pages). Made it so it doesn't use any duplicated IDs anymore. If there's more problems, please do tell so I can fix it! * Update settings.json * Fix? --------- Co-authored-by: exincore <me@exin.xyz> * Automatic changelog update * Anomalies behaviours (#24683) * Added new anomaly particle * Add basic anomaly behaviour * +2 parametres * add functional to new particle * add components to behaviours * big content * add shuffle, moved thing to server * clean up * fixes * random pick redo * bonjour behavioUr * fix AJCM * fix * add some new behaviours * power modifier behaviour * rmeove timer * new event for update ui fix * refactor! * fixes * enum * Fix mapinit * Minor touches --------- Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com> * Automatic changelog update * Fix clipping/overlap in lathe machine UIs (#26646) * Add scrollbars to lathe material list when necessary * Fix bug where shrinking window would cause elements to overlap --------- Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es> * Added chat window transparency slider to options (#24990) * Adds a new slider to the misc tab in options that lets the player set chat window transparency * Tweaked variable names * Fixed order to match UI * Renamed set chat window transparency function * Changed and refactored to opacity instead of transparency * Remove unnecessary int to float conversions Slider used to be 0-100 while the CCVar was 0.0-1.0f. This is confusing and was only used for rounding to 2 decimal points. * Round the value to two decimal points * Remove rounding for now * Rename * Unhardcode chat color by moving to stylesheet * Fix indent * Make opacity slider only change opacity --------- Co-authored-by: Your Name <you@example.com> Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com> * Automatic changelog update * Infinity books (#25840) * setup text data * roundstart reshuffling keywords with gibberish words * saved data categorized * add book with hints * start redrawing books * +4 book design * +books +random visual upgrade * finish first file * finish lore file * finish with books.rsi now authorbooks.rsi... * aurora! and some fix * nuke author books * speelbuke update * finish respriting work * fix scientist guide visual * setup datasets * setup stupid funny random story * restore author books, upgrade hint generation * add variety to story generator * add learning system * minor textgen edit * file restruct, hint count variation * more restruct * more renaming add basis learning system logic. Spears locked for special book for test. * nuke all systems, for splitting PR gods * typo fix * update migration with deleted books * add random story books to maint * Update construction-system.ftl * Update Resources/Prototypes/Datasets/Names/books.yml Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> * Update Resources/Prototypes/Datasets/Names/books.yml Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> * Update Resources/Prototypes/Datasets/Names/books.yml Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> * Update Resources/Prototypes/Datasets/Names/books.yml Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> * Update Resources/Prototypes/Datasets/Names/books.yml Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> * Update Resources/Prototypes/Datasets/Names/books.yml Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> * Update Resources/Prototypes/Datasets/Names/books.yml Co-authored-by: Hrosts <35345601+Hrosts@users.noreply.github.com> * Update Resources/Prototypes/Datasets/Names/books.yml Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> * Update Resources/Prototypes/Datasets/Names/books.yml Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> * Update Resources/Prototypes/Datasets/Names/books.yml Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> * Update Resources/Prototypes/Datasets/Names/books.yml Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> * Update Resources/Prototypes/Datasets/Names/books.yml Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> * typo fix * interchangeably * final * Update Resources/Prototypes/Datasets/Names/books.yml Co-authored-by: Hrosts <35345601+Hrosts@users.noreply.github.com> * "." * Update Content.Server/Paper/PaperRandomStorySystem.cs Co-authored-by: Hrosts <35345601+Hrosts@users.noreply.github.com> * Ubazer fix * inadequate * localized * Update meta.json * fuck merge conflicts * fix jani book --------- Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> Co-authored-by: Hrosts <35345601+Hrosts@users.noreply.github.com> * Automatic changelog update * Resprite ambuzol plus pills (#26651) * Automatic changelog update * Fixed air injector visuals (#26654) * Make cyborgs hands explosion proof. (#26515) * Make the advanced treatment modules beakers explosion-proof. * undo changes * Epic rename fail * Explosion recursion data field * Logic for data field * Automatic changelog update * Automatic changelog update * Make typing indicator shaded (#26678) * Automatic changelog update * Validate wire layout prototypes and remove invalid WiresComponents (#26682) Validate wire layout prototypes; delete invalid wirescomponents. * Increase time inbetween anomaly pulses (#26677) nerf anomaly pulse delays * Automatic changelog update * Fix for items dropped being rotated to world north (#26662) * Fix rotation of dropped items * combined world position rotation function for dumpable * scuffed implementation? * less scuffed? * even less scuffed... I guess * capital D --------- Co-authored-by: Plykiya <plykiya@protonmail.com> * Automatic changelog update * fix typo --------- Signed-off-by: c4llv07e <kseandi@gmail.com> Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com> Co-authored-by: Plykiya <58439124+Plykiya@users.noreply.github.com> Co-authored-by: Plykiya <plykiya@protonmail.com> Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com> Co-authored-by: keronshb <54602815+keronshb@users.noreply.github.com> Co-authored-by: Wrexbe (Josh) <81056464+wrexbe@users.noreply.github.com> Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Co-authored-by: wrexbe <wrexbe@protonmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com> Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com> Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com> Co-authored-by: Simon <63975668+Simyon264@users.noreply.github.com> Co-authored-by: blueDev2 <89804215+blueDev2@users.noreply.github.com> Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: Velcroboy <107660393+IamVelcroboy@users.noreply.github.com> Co-authored-by: Velcroboy <velcroboy333@hotmail.com> Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Co-authored-by: brainfood1183 <113240905+brainfood1183@users.noreply.github.com> Co-authored-by: J. Brown <DrMelon@users.noreply.github.com> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com> Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> Co-authored-by: UBlueberry <161545003+UBlueberry@users.noreply.github.com> Co-authored-by: Tayrtahn <tayrtahn@gmail.com> Co-authored-by: drteaspoon420 <87363733+drteaspoon420@users.noreply.github.com> Co-authored-by: Bixkitts <72874643+Bixkitts@users.noreply.github.com> Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com> Co-authored-by: Ubaser <134914314+UbaserB@users.noreply.github.com> Co-authored-by: avery <51971268+graevy@users.noreply.github.com> Co-authored-by: eoineoineoin <github@eoinrul.es> Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es> Co-authored-by: RiceMar1244 <138547931+RiceMar1244@users.noreply.github.com> Co-authored-by: Zealith-Gamer <61980908+Zealith-Gamer@users.noreply.github.com> Co-authored-by: hamurlik <75280571+hamurlik@users.noreply.github.com> Co-authored-by: hamurlik <renoDeath@protonmail.com> Co-authored-by: no <165581243+pissdemon@users.noreply.github.com> Co-authored-by: Kara <lunarautomaton6@gmail.com> Co-authored-by: SoulFN <164462467+SoulFN@users.noreply.github.com> Co-authored-by: Keer-Sar <144283718+Keer-Sar@users.noreply.github.com> Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Co-authored-by: 0x6273 <0x40@keemail.me> Co-authored-by: c4llv07e <38111072+c4llv07e@users.noreply.github.com> Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com> Co-authored-by: Firewatch <54725557+musicmanvr@users.noreply.github.com> Co-authored-by: IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com> Co-authored-by: f0x-n3rd <150924715+f0x-n3rd@users.noreply.github.com> Co-authored-by: exincore <me@exin.xyz> Co-authored-by: Sk1tch <ben.peter.smith@gmail.com> Co-authored-by: Your Name <you@example.com> Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com> Co-authored-by: Hrosts <35345601+Hrosts@users.noreply.github.com> Co-authored-by: osjarw <62134478+osjarw@users.noreply.github.com>
This commit is contained in:
4
Content.Client/Access/UI/AccessLevelControl.xaml
Normal file
4
Content.Client/Access/UI/AccessLevelControl.xaml
Normal file
@@ -0,0 +1,4 @@
|
||||
<GridContainer xmlns="https://spacestation14.io"
|
||||
Columns="5"
|
||||
HorizontalAlignment="Center">
|
||||
</GridContainer>
|
||||
52
Content.Client/Access/UI/AccessLevelControl.xaml.cs
Normal file
52
Content.Client/Access/UI/AccessLevelControl.xaml.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Linq;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Access;
|
||||
using Content.Shared.Access.Systems;
|
||||
|
||||
namespace Content.Client.Access.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AccessLevelControl : GridContainer
|
||||
{
|
||||
public readonly Dictionary<ProtoId<AccessLevelPrototype>, Button> ButtonsList = new();
|
||||
|
||||
public AccessLevelControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void Populate(List<ProtoId<AccessLevelPrototype>> accessLevels, IPrototypeManager prototypeManager)
|
||||
{
|
||||
foreach (var access in accessLevels)
|
||||
{
|
||||
if (!prototypeManager.TryIndex(access, out var accessLevel))
|
||||
{
|
||||
Logger.Error($"Unable to find accesslevel for {access}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var newButton = new Button
|
||||
{
|
||||
Text = accessLevel.GetAccessLevelName(),
|
||||
ToggleMode = true,
|
||||
};
|
||||
AddChild(newButton);
|
||||
ButtonsList.Add(accessLevel.ID, newButton);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateState(
|
||||
List<ProtoId<AccessLevelPrototype>> pressedList,
|
||||
List<ProtoId<AccessLevelPrototype>>? enabledList = null)
|
||||
{
|
||||
foreach (var (accessName, button) in ButtonsList)
|
||||
{
|
||||
button.Pressed = pressedList.Contains(accessName);
|
||||
button.Disabled = !(enabledList?.Contains(accessName) ?? true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ namespace Content.Client.Access.UI
|
||||
_window?.UpdateState(castState);
|
||||
}
|
||||
|
||||
public void SubmitData(List<string> newAccessList)
|
||||
public void SubmitData(List<ProtoId<AccessLevelPrototype>> newAccessList)
|
||||
{
|
||||
SendMessage(new WriteToTargetAccessReaderIdMessage(newAccessList));
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ namespace Content.Client.Access.UI
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private readonly ISawmill _logMill = default!;
|
||||
private readonly AccessOverriderBoundUserInterface _owner;
|
||||
private readonly Dictionary<string, Button> _accessButtons = new();
|
||||
|
||||
@@ -25,7 +24,7 @@ namespace Content.Client.Access.UI
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_logMill = _logManager.GetSawmill(SharedAccessOverriderSystem.Sawmill);
|
||||
var logMill = _logManager.GetSawmill(SharedAccessOverriderSystem.Sawmill);
|
||||
|
||||
_owner = owner;
|
||||
|
||||
@@ -33,13 +32,13 @@ namespace Content.Client.Access.UI
|
||||
{
|
||||
if (!prototypeManager.TryIndex(access, out var accessLevel))
|
||||
{
|
||||
_logMill.Error($"Unable to find accesslevel for {access}");
|
||||
logMill.Error($"Unable to find accesslevel for {access}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var newButton = new Button
|
||||
{
|
||||
Text = GetAccessLevelName(accessLevel),
|
||||
Text = accessLevel.GetAccessLevelName(),
|
||||
ToggleMode = true,
|
||||
};
|
||||
|
||||
@@ -49,14 +48,6 @@ namespace Content.Client.Access.UI
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetAccessLevelName(AccessLevelPrototype prototype)
|
||||
{
|
||||
if (prototype.Name is { } name)
|
||||
return Loc.GetString(name);
|
||||
|
||||
return prototype.ID;
|
||||
}
|
||||
|
||||
public void UpdateState(AccessOverriderBoundUserInterfaceState state)
|
||||
{
|
||||
PrivilegedIdLabel.Text = state.PrivilegedIdName;
|
||||
@@ -105,7 +96,7 @@ namespace Content.Client.Access.UI
|
||||
_owner.SubmitData(
|
||||
|
||||
// Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair
|
||||
_accessButtons.Where(x => x.Value.Pressed).Select(x => x.Key).ToList());
|
||||
_accessButtons.Where(x => x.Value.Pressed).Select(x => new ProtoId<AccessLevelPrototype>(x.Key)).ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Access;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.CrewManifest;
|
||||
@@ -28,7 +29,6 @@ namespace Content.Client.Access.UI
|
||||
if (EntMan.TryGetComponent<IdCardConsoleComponent>(Owner, out var idCard))
|
||||
{
|
||||
accessLevels = idCard.AccessLevels;
|
||||
accessLevels.Sort();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -65,7 +65,7 @@ namespace Content.Client.Access.UI
|
||||
_window?.UpdateState(castState);
|
||||
}
|
||||
|
||||
public void SubmitData(string newFullName, string newJobTitle, List<string> newAccessList, string newJobPrototype)
|
||||
public void SubmitData(string newFullName, string newJobTitle, List<ProtoId<AccessLevelPrototype>> newAccessList, string newJobPrototype)
|
||||
{
|
||||
if (newFullName.Length > MaxFullNameLength)
|
||||
newFullName = newFullName[..MaxFullNameLength];
|
||||
|
||||
@@ -30,10 +30,6 @@
|
||||
<Label Text="{Loc 'id-card-console-window-job-selection-label'}" />
|
||||
<OptionButton Name="JobPresetOptionButton" />
|
||||
</GridContainer>
|
||||
<GridContainer Name="AccessLevelGrid" Columns="5" HorizontalAlignment="Center">
|
||||
|
||||
<!-- Access level buttons are added here by the C# code -->
|
||||
|
||||
</GridContainer>
|
||||
<Control Name="AccessLevelControlContainer" />
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Content.Client.Access.UI
|
||||
|
||||
private readonly IdCardConsoleBoundUserInterface _owner;
|
||||
|
||||
private readonly Dictionary<string, Button> _accessButtons = new();
|
||||
private AccessLevelControl _accessButtons = new();
|
||||
private readonly List<string> _jobPrototypeIds = new();
|
||||
|
||||
private string? _lastFullName;
|
||||
@@ -66,36 +66,18 @@ namespace Content.Client.Access.UI
|
||||
|
||||
JobPresetOptionButton.OnItemSelected += SelectJobPreset;
|
||||
|
||||
foreach (var access in accessLevels)
|
||||
_accessButtons.Populate(accessLevels, prototypeManager);
|
||||
AccessLevelControlContainer.AddChild(_accessButtons);
|
||||
|
||||
foreach (var (id, button) in _accessButtons.ButtonsList)
|
||||
{
|
||||
if (!prototypeManager.TryIndex<AccessLevelPrototype>(access, out var accessLevel))
|
||||
{
|
||||
_logMill.Error($"Unable to find accesslevel for {access}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var newButton = new Button
|
||||
{
|
||||
Text = GetAccessLevelName(accessLevel),
|
||||
ToggleMode = true,
|
||||
};
|
||||
AccessLevelGrid.AddChild(newButton);
|
||||
_accessButtons.Add(accessLevel.ID, newButton);
|
||||
newButton.OnPressed += _ => SubmitData();
|
||||
button.OnPressed += _ => SubmitData();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetAccessLevelName(AccessLevelPrototype prototype)
|
||||
{
|
||||
if (prototype.Name is { } name)
|
||||
return Loc.GetString(name);
|
||||
|
||||
return prototype.ID;
|
||||
}
|
||||
|
||||
private void ClearAllAccess()
|
||||
{
|
||||
foreach (var button in _accessButtons.Values)
|
||||
foreach (var button in _accessButtons.ButtonsList.Values)
|
||||
{
|
||||
if (button.Pressed)
|
||||
{
|
||||
@@ -119,7 +101,7 @@ namespace Content.Client.Access.UI
|
||||
// this is a sussy way to do this
|
||||
foreach (var access in job.Access)
|
||||
{
|
||||
if (_accessButtons.TryGetValue(access, out var button) && !button.Disabled)
|
||||
if (_accessButtons.ButtonsList.TryGetValue(access, out var button) && !button.Disabled)
|
||||
{
|
||||
button.Pressed = true;
|
||||
}
|
||||
@@ -134,7 +116,7 @@ namespace Content.Client.Access.UI
|
||||
|
||||
foreach (var access in groupPrototype.Tags)
|
||||
{
|
||||
if (_accessButtons.TryGetValue(access, out var button) && !button.Disabled)
|
||||
if (_accessButtons.ButtonsList.TryGetValue(access, out var button) && !button.Disabled)
|
||||
{
|
||||
button.Pressed = true;
|
||||
}
|
||||
@@ -184,15 +166,10 @@ namespace Content.Client.Access.UI
|
||||
|
||||
JobPresetOptionButton.Disabled = !interfaceEnabled;
|
||||
|
||||
foreach (var (accessName, button) in _accessButtons)
|
||||
{
|
||||
button.Disabled = !interfaceEnabled;
|
||||
if (interfaceEnabled)
|
||||
{
|
||||
button.Pressed = state.TargetIdAccessList?.Contains(accessName) ?? false;
|
||||
button.Disabled = (!state.AllowedModifyAccessList?.Contains(accessName)) ?? true;
|
||||
}
|
||||
}
|
||||
_accessButtons.UpdateState(state.TargetIdAccessList?.ToList() ??
|
||||
new List<ProtoId<AccessLevelPrototype>>(),
|
||||
state.AllowedModifyAccessList?.ToList() ??
|
||||
new List<ProtoId<AccessLevelPrototype>>());
|
||||
|
||||
var jobIndex = _jobPrototypeIds.IndexOf(state.TargetIdJobPrototype);
|
||||
if (jobIndex >= 0)
|
||||
@@ -215,7 +192,7 @@ namespace Content.Client.Access.UI
|
||||
FullNameLineEdit.Text,
|
||||
JobTitleLineEdit.Text,
|
||||
// Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair
|
||||
_accessButtons.Where(x => x.Value.Pressed).Select(x => x.Key).ToList(),
|
||||
_accessButtons.ButtonsList.Where(x => x.Value.Pressed).Select(x => x.Key).ToList(),
|
||||
jobProtoDirty ? _jobPrototypeIds[JobPresetOptionButton.SelectedId] : string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<controls:FancyWindow
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'anomaly-scanner-ui-title'}"
|
||||
MinSize="350 260"
|
||||
SetSize="350 260">
|
||||
MinSize="350 400"
|
||||
SetSize="350 400">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="10 0 10 10">
|
||||
<RichTextLabel Name="TextDisplay"></RichTextLabel>
|
||||
</BoxContainer>
|
||||
|
||||
@@ -17,6 +17,7 @@ public sealed class InjectorStatusControl : Control
|
||||
|
||||
private FixedPoint2 PrevVolume;
|
||||
private FixedPoint2 PrevMaxVolume;
|
||||
private FixedPoint2 PrevTransferAmount;
|
||||
private InjectorToggleMode PrevToggleState;
|
||||
|
||||
public InjectorStatusControl(Entity<InjectorComponent> parent, SharedSolutionContainerSystem solutionContainers)
|
||||
@@ -37,11 +38,13 @@ public sealed class InjectorStatusControl : Control
|
||||
// only updates the UI if any of the details are different than they previously were
|
||||
if (PrevVolume == solution.Volume
|
||||
&& PrevMaxVolume == solution.MaxVolume
|
||||
&& PrevTransferAmount == _parent.Comp.TransferAmount
|
||||
&& PrevToggleState == _parent.Comp.ToggleState)
|
||||
return;
|
||||
|
||||
PrevVolume = solution.Volume;
|
||||
PrevMaxVolume = solution.MaxVolume;
|
||||
PrevTransferAmount = _parent.Comp.TransferAmount;
|
||||
PrevToggleState = _parent.Comp.ToggleState;
|
||||
|
||||
// Update current volume and injector state
|
||||
|
||||
@@ -96,24 +96,22 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||
private void UpdateState(EntityUid uid, SharedDisposalUnitComponent unit, SpriteComponent sprite, AppearanceComponent appearance)
|
||||
{
|
||||
if (!_appearanceSystem.TryGetData<VisualState>(uid, Visuals.VisualState, out var state, appearance))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sprite.LayerSetVisible(DisposalUnitVisualLayers.Unanchored, state == VisualState.UnAnchored);
|
||||
sprite.LayerSetVisible(DisposalUnitVisualLayers.Base, state == VisualState.Anchored);
|
||||
sprite.LayerSetVisible(DisposalUnitVisualLayers.BaseFlush, state is VisualState.Flushing or VisualState.Charging);
|
||||
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFlush, state is VisualState.OverlayFlushing or VisualState.OverlayCharging);
|
||||
|
||||
var chargingState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseCharging, out var chargingLayer)
|
||||
? sprite.LayerGetState(chargingLayer)
|
||||
: new RSI.StateId(DefaultChargeState);
|
||||
|
||||
// This is a transient state so not too worried about replaying in range.
|
||||
if (state == VisualState.Flushing)
|
||||
if (state == VisualState.OverlayFlushing)
|
||||
{
|
||||
if (!_animationSystem.HasRunningAnimation(uid, AnimationKey))
|
||||
{
|
||||
var flushState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseFlush, out var flushLayer)
|
||||
var flushState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.OverlayFlush, out var flushLayer)
|
||||
? sprite.LayerGetState(flushLayer)
|
||||
: new RSI.StateId(DefaultFlushState);
|
||||
|
||||
@@ -125,7 +123,7 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||
{
|
||||
new AnimationTrackSpriteFlick
|
||||
{
|
||||
LayerKey = DisposalUnitVisualLayers.BaseFlush,
|
||||
LayerKey = DisposalUnitVisualLayers.OverlayFlush,
|
||||
KeyFrames =
|
||||
{
|
||||
// Play the flush animation
|
||||
@@ -154,26 +152,18 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||
_animationSystem.Play(uid, anim, AnimationKey);
|
||||
}
|
||||
}
|
||||
else if (state == VisualState.Charging)
|
||||
{
|
||||
sprite.LayerSetState(DisposalUnitVisualLayers.BaseFlush, chargingState);
|
||||
}
|
||||
else if (state == VisualState.OverlayCharging)
|
||||
sprite.LayerSetState(DisposalUnitVisualLayers.OverlayFlush, new RSI.StateId("disposal-charging"));
|
||||
else
|
||||
{
|
||||
_animationSystem.Stop(uid, AnimationKey);
|
||||
}
|
||||
|
||||
if (!_appearanceSystem.TryGetData<HandleState>(uid, Visuals.Handle, out var handleState, appearance))
|
||||
{
|
||||
handleState = HandleState.Normal;
|
||||
}
|
||||
|
||||
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayEngaged, handleState != HandleState.Normal);
|
||||
|
||||
if (!_appearanceSystem.TryGetData<LightStates>(uid, Visuals.Light, out var lightState, appearance))
|
||||
{
|
||||
lightState = LightStates.Off;
|
||||
}
|
||||
|
||||
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayCharging,
|
||||
(lightState & LightStates.Charging) != 0);
|
||||
@@ -189,7 +179,7 @@ public enum DisposalUnitVisualLayers : byte
|
||||
Unanchored,
|
||||
Base,
|
||||
BaseCharging,
|
||||
BaseFlush,
|
||||
OverlayFlush,
|
||||
OverlayCharging,
|
||||
OverlayReady,
|
||||
OverlayFull,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using Content.Shared.Access;
|
||||
using Content.Shared.Doors.Electronics;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Doors.Electronics;
|
||||
|
||||
public sealed class DoorElectronicsBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private DoorElectronicsConfigurationMenu? _window;
|
||||
|
||||
public DoorElectronicsBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
List<ProtoId<AccessLevelPrototype>> accessLevels = new();
|
||||
|
||||
foreach (var accessLevel in _prototypeManager.EnumeratePrototypes<AccessLevelPrototype>())
|
||||
{
|
||||
if (accessLevel.Name != null)
|
||||
{
|
||||
accessLevels.Add(accessLevel.ID);
|
||||
}
|
||||
}
|
||||
|
||||
accessLevels.Sort();
|
||||
|
||||
_window = new DoorElectronicsConfigurationMenu(this, accessLevels, _prototypeManager);
|
||||
_window.OnClose += Close;
|
||||
_window.OpenCentered();
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
var castState = (DoorElectronicsConfigurationState) state;
|
||||
|
||||
_window?.UpdateState(castState);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing) return;
|
||||
|
||||
_window?.Dispose();
|
||||
}
|
||||
|
||||
public void UpdateConfiguration(List<ProtoId<AccessLevelPrototype>> newAccessList)
|
||||
{
|
||||
SendMessage(new DoorElectronicsUpdateConfigurationMessage(newAccessList));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
<Control Name="AccessLevelControlContainer" />
|
||||
|
||||
</controls:FancyWindow>
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Linq;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Client.Access.UI;
|
||||
using Content.Client.Doors.Electronics;
|
||||
using Content.Shared.Access;
|
||||
using Content.Shared.Doors.Electronics;
|
||||
using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
|
||||
|
||||
namespace Content.Client.Doors.Electronics;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class DoorElectronicsConfigurationMenu : FancyWindow
|
||||
{
|
||||
private readonly DoorElectronicsBoundUserInterface _owner;
|
||||
private AccessLevelControl _buttonsList = new();
|
||||
|
||||
public DoorElectronicsConfigurationMenu(DoorElectronicsBoundUserInterface ui, List<ProtoId<AccessLevelPrototype>> accessLevels, IPrototypeManager prototypeManager)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_owner = ui;
|
||||
|
||||
_buttonsList.Populate(accessLevels, prototypeManager);
|
||||
AccessLevelControlContainer.AddChild(_buttonsList);
|
||||
|
||||
foreach (var (id, button) in _buttonsList.ButtonsList)
|
||||
{
|
||||
button.OnPressed += _ => _owner.UpdateConfiguration(
|
||||
_buttonsList.ButtonsList.Where(x => x.Value.Pressed).Select(x => x.Key).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateState(DoorElectronicsConfigurationState state)
|
||||
{
|
||||
_buttonsList.UpdateState(state.AccessList);
|
||||
}
|
||||
}
|
||||
@@ -212,14 +212,16 @@ namespace Content.Client.Examine
|
||||
var vBox = new BoxContainer
|
||||
{
|
||||
Name = "ExaminePopupVbox",
|
||||
Orientation = LayoutOrientation.Vertical
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
MaxWidth = _examineTooltipOpen.MaxWidth
|
||||
};
|
||||
panel.AddChild(vBox);
|
||||
|
||||
var hBox = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 5
|
||||
SeparationOverride = 5,
|
||||
Margin = new Thickness(6, 0, 6, 0)
|
||||
};
|
||||
|
||||
vBox.AddChild(hBox);
|
||||
@@ -229,8 +231,7 @@ namespace Content.Client.Examine
|
||||
var spriteView = new SpriteView
|
||||
{
|
||||
OverrideDirection = Direction.South,
|
||||
SetSize = new Vector2(32, 32),
|
||||
Margin = new Thickness(2, 0, 2, 0),
|
||||
SetSize = new Vector2(32, 32)
|
||||
};
|
||||
spriteView.SetEntity(target);
|
||||
hBox.AddChild(spriteView);
|
||||
@@ -238,19 +239,17 @@ namespace Content.Client.Examine
|
||||
|
||||
if (knowTarget)
|
||||
{
|
||||
hBox.AddChild(new Label
|
||||
{
|
||||
Text = Identity.Name(target, EntityManager, player),
|
||||
HorizontalExpand = true,
|
||||
});
|
||||
var itemName = FormattedMessage.RemoveMarkup(Identity.Name(target, EntityManager, player));
|
||||
var labelMessage = FormattedMessage.FromMarkup($"[bold]{itemName}[/bold]");
|
||||
var label = new RichTextLabel();
|
||||
label.SetMessage(labelMessage);
|
||||
hBox.AddChild(label);
|
||||
}
|
||||
else
|
||||
{
|
||||
hBox.AddChild(new Label
|
||||
{
|
||||
Text = "???",
|
||||
HorizontalExpand = true,
|
||||
});
|
||||
var label = new RichTextLabel();
|
||||
label.SetMessage(FormattedMessage.FromMarkup("[bold]???[/bold]"));
|
||||
hBox.AddChild(label);
|
||||
}
|
||||
|
||||
panel.Measure(Vector2Helpers.Infinity);
|
||||
|
||||
@@ -45,6 +45,9 @@ namespace Content.Client.Input
|
||||
// Not in engine because the engine doesn't understand what a flipped object is
|
||||
common.AddFunction(ContentKeyFunctions.EditorFlipObject);
|
||||
|
||||
// Not in engine so that the RCD can rotate objects
|
||||
common.AddFunction(EngineKeyFunctions.EditorRotateObject);
|
||||
|
||||
var human = contexts.GetContext("human");
|
||||
human.AddFunction(EngineKeyFunctions.MoveUp);
|
||||
human.AddFunction(EngineKeyFunctions.MoveDown);
|
||||
|
||||
@@ -124,9 +124,7 @@
|
||||
<BoxContainer
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Orientation="Vertical"
|
||||
MinHeight="225"
|
||||
>
|
||||
Orientation="Vertical">
|
||||
<Label Text="{Loc 'lathe-menu-materials-title'}" Margin="5 5 5 5" HorizontalAlignment="Center"/>
|
||||
<BoxContainer
|
||||
Orientation="Vertical"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical"
|
||||
<ScrollContainer xmlns="https://spacestation14.io"
|
||||
SizeFlagsStretchRatio="8"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<Label Name="NoMatsLabel" Text="{Loc 'lathe-menu-no-materials-message'}" Align="Center"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Name="MaterialList" Orientation="Vertical">
|
||||
<Label Name="NoMatsLabel" Text="{Loc 'lathe-menu-no-materials-message'}" Align="Center"/>
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Content.Client.Materials.UI;
|
||||
/// This widget is one row in the lathe eject menu.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MaterialStorageControl : BoxContainer
|
||||
public sealed partial class MaterialStorageControl : ScrollContainer
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
@@ -63,7 +63,7 @@ public sealed partial class MaterialStorageControl : BoxContainer
|
||||
}
|
||||
|
||||
var children = new List<MaterialDisplay>();
|
||||
children.AddRange(Children.OfType<MaterialDisplay>());
|
||||
children.AddRange(MaterialList.Children.OfType<MaterialDisplay>());
|
||||
|
||||
foreach (var display in children)
|
||||
{
|
||||
@@ -71,7 +71,7 @@ public sealed partial class MaterialStorageControl : BoxContainer
|
||||
|
||||
if (extra.Contains(mat))
|
||||
{
|
||||
RemoveChild(display);
|
||||
MaterialList.RemoveChild(display);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ public sealed partial class MaterialStorageControl : BoxContainer
|
||||
foreach (var mat in missing)
|
||||
{
|
||||
var volume = mats[mat];
|
||||
AddChild(new MaterialDisplay(_owner.Value, mat, volume, canEject));
|
||||
MaterialList.AddChild(new MaterialDisplay(_owner.Value, mat, volume, canEject));
|
||||
}
|
||||
|
||||
_currentMaterials = mats;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
namespace Content.Client.Nutrition.EntitySystems;
|
||||
|
||||
public sealed class OpenableSystem : SharedOpenableSystem
|
||||
{
|
||||
}
|
||||
@@ -25,6 +25,14 @@
|
||||
<CheckBox Name="ReducedMotionCheckBox" Text="{Loc 'ui-options-reduced-motion'}" />
|
||||
<CheckBox Name="EnableColorNameCheckBox" Text="{Loc 'ui-options-enable-color-name'}" />
|
||||
<CheckBox Name="ColorblindFriendlyCheckBox" Text="{Loc 'ui-options-colorblind-friendly'}" />
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'ui-options-chat-window-opacity'}" Margin="8 0" />
|
||||
<Slider Name="ChatWindowOpacitySlider"
|
||||
MinValue="0"
|
||||
MaxValue="1"
|
||||
MinWidth="200" />
|
||||
<Label Name="ChatWindowOpacityLabel" Margin="8 0" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'ui-options-screen-shake-intensity'}" Margin="8 0" />
|
||||
<Slider Name="ScreenShakeIntensitySlider"
|
||||
@@ -65,6 +73,3 @@
|
||||
</controls:StripeBack>
|
||||
</BoxContainer>
|
||||
</tabs:MiscTab>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
EnableColorNameCheckBox.OnToggled += OnCheckBoxToggled;
|
||||
ColorblindFriendlyCheckBox.OnToggled += OnCheckBoxToggled;
|
||||
ReducedMotionCheckBox.OnToggled += OnCheckBoxToggled;
|
||||
ChatWindowOpacitySlider.OnValueChanged += OnChatWindowOpacitySliderChanged;
|
||||
ScreenShakeIntensitySlider.OnValueChanged += OnScreenShakeIntensitySliderChanged;
|
||||
// ToggleWalk.OnToggled += OnCheckBoxToggled;
|
||||
StaticStorageUI.OnToggled += OnCheckBoxToggled;
|
||||
@@ -81,6 +82,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
EnableColorNameCheckBox.Pressed = _cfg.GetCVar(CCVars.ChatEnableColorName);
|
||||
ColorblindFriendlyCheckBox.Pressed = _cfg.GetCVar(CCVars.AccessibilityColorblindFriendly);
|
||||
ReducedMotionCheckBox.Pressed = _cfg.GetCVar(CCVars.ReducedMotion);
|
||||
ChatWindowOpacitySlider.Value = _cfg.GetCVar(CCVars.ChatWindowOpacity);
|
||||
ScreenShakeIntensitySlider.Value = _cfg.GetCVar(CCVars.ScreenShakeIntensity) * 100f;
|
||||
// ToggleWalk.Pressed = _cfg.GetCVar(CCVars.ToggleWalk);
|
||||
StaticStorageUI.Pressed = _cfg.GetCVar(CCVars.StaticStorageUI);
|
||||
@@ -101,6 +103,13 @@ namespace Content.Client.Options.UI.Tabs
|
||||
UpdateApplyButton();
|
||||
}
|
||||
|
||||
private void OnChatWindowOpacitySliderChanged(Range range)
|
||||
{
|
||||
ChatWindowOpacityLabel.Text = Loc.GetString("ui-options-chat-window-opacity-percent",
|
||||
("opacity", range.Value));
|
||||
UpdateApplyButton();
|
||||
}
|
||||
|
||||
private void OnScreenShakeIntensitySliderChanged(Range obj)
|
||||
{
|
||||
ScreenShakeIntensityLabel.Text = Loc.GetString("ui-options-screen-shake-percent", ("intensity", ScreenShakeIntensitySlider.Value / 100f));
|
||||
@@ -127,6 +136,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
_cfg.SetCVar(CCVars.ChatEnableColorName, EnableColorNameCheckBox.Pressed);
|
||||
_cfg.SetCVar(CCVars.AccessibilityColorblindFriendly, ColorblindFriendlyCheckBox.Pressed);
|
||||
_cfg.SetCVar(CCVars.ReducedMotion, ReducedMotionCheckBox.Pressed);
|
||||
_cfg.SetCVar(CCVars.ChatWindowOpacity, ChatWindowOpacitySlider.Value);
|
||||
_cfg.SetCVar(CCVars.ScreenShakeIntensity, ScreenShakeIntensitySlider.Value / 100f);
|
||||
// _cfg.SetCVar(CCVars.ToggleWalk, ToggleWalk.Pressed);
|
||||
_cfg.SetCVar(CCVars.StaticStorageUI, StaticStorageUI.Pressed);
|
||||
@@ -154,6 +164,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
var isEnableColorNameSame = EnableColorNameCheckBox.Pressed == _cfg.GetCVar(CCVars.ChatEnableColorName);
|
||||
var isColorblindFriendly = ColorblindFriendlyCheckBox.Pressed == _cfg.GetCVar(CCVars.AccessibilityColorblindFriendly);
|
||||
var isReducedMotionSame = ReducedMotionCheckBox.Pressed == _cfg.GetCVar(CCVars.ReducedMotion);
|
||||
var isChatWindowOpacitySame = Math.Abs(ChatWindowOpacitySlider.Value - _cfg.GetCVar(CCVars.ChatWindowOpacity)) < 0.01f;
|
||||
var isScreenShakeIntensitySame = Math.Abs(ScreenShakeIntensitySlider.Value / 100f - _cfg.GetCVar(CCVars.ScreenShakeIntensity)) < 0.01f;
|
||||
// var isToggleWalkSame = ToggleWalk.Pressed == _cfg.GetCVar(CCVars.ToggleWalk);
|
||||
var isStaticStorageUISame = StaticStorageUI.Pressed == _cfg.GetCVar(CCVars.StaticStorageUI);
|
||||
@@ -170,6 +181,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
isEnableColorNameSame &&
|
||||
isColorblindFriendly &&
|
||||
isReducedMotionSame &&
|
||||
isChatWindowOpacitySame &&
|
||||
isScreenShakeIntensitySame &&
|
||||
// isToggleWalkSame &&
|
||||
isStaticStorageUISame;
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
using System.Linq;
|
||||
using Robust.Client.GameObjects;
|
||||
using static Robust.Client.GameObjects.SpriteComponent;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Paint;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Paint;
|
||||
|
||||
public sealed class PaintedVisualizerSystem : VisualizerSystem<PaintedComponent>
|
||||
{
|
||||
/// <summary>
|
||||
/// Visualizer for Paint which applies a shader and colors the entity.
|
||||
/// </summary>
|
||||
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PaintedComponent, HeldVisualsUpdatedEvent>(OnHeldVisualsUpdated);
|
||||
SubscribeLocalEvent<PaintedComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<PaintedComponent, EquipmentVisualsUpdatedEvent>(OnEquipmentVisualsUpdated);
|
||||
}
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, PaintedComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
var shader = _protoMan.Index<ShaderPrototype>(component.ShaderName).Instance();
|
||||
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
|
||||
// What is this even doing? It's not even checking what the value is.
|
||||
if (!_appearance.TryGetData(uid, PaintVisuals.Painted, out bool isPainted))
|
||||
return;
|
||||
|
||||
var sprite = args.Sprite;
|
||||
|
||||
foreach (var spriteLayer in sprite.AllLayers)
|
||||
{
|
||||
if (spriteLayer is not Layer layer)
|
||||
continue;
|
||||
|
||||
if (layer.Shader == null) // If shader isn't null we dont want to replace the original shader.
|
||||
{
|
||||
layer.Shader = shader;
|
||||
layer.Color = component.Color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHeldVisualsUpdated(EntityUid uid, PaintedComponent component, HeldVisualsUpdatedEvent args)
|
||||
{
|
||||
if (args.RevealedLayers.Count == 0)
|
||||
return;
|
||||
|
||||
if (!TryComp(args.User, out SpriteComponent? sprite))
|
||||
return;
|
||||
|
||||
foreach (var revealed in args.RevealedLayers)
|
||||
{
|
||||
if (!sprite.LayerMapTryGet(revealed, out var layer))
|
||||
continue;
|
||||
|
||||
sprite.LayerSetShader(layer, component.ShaderName);
|
||||
sprite.LayerSetColor(layer, component.Color);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEquipmentVisualsUpdated(EntityUid uid, PaintedComponent component, EquipmentVisualsUpdatedEvent args)
|
||||
{
|
||||
if (args.RevealedLayers.Count == 0)
|
||||
return;
|
||||
|
||||
if (!TryComp(args.Equipee, out SpriteComponent? sprite))
|
||||
return;
|
||||
|
||||
foreach (var revealed in args.RevealedLayers)
|
||||
{
|
||||
if (!sprite.LayerMapTryGet(revealed, out var layer))
|
||||
continue;
|
||||
|
||||
sprite.LayerSetShader(layer, component.ShaderName);
|
||||
sprite.LayerSetColor(layer, component.Color);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, PaintedComponent component, ref ComponentShutdown args)
|
||||
{
|
||||
if (!TryComp(uid, out SpriteComponent? sprite))
|
||||
return;
|
||||
|
||||
component.BeforeColor = sprite.Color;
|
||||
var shader = _protoMan.Index<ShaderPrototype>(component.ShaderName).Instance();
|
||||
|
||||
if (!Terminating(uid))
|
||||
{
|
||||
foreach (var spriteLayer in sprite.AllLayers)
|
||||
{
|
||||
if (spriteLayer is not Layer layer)
|
||||
continue;
|
||||
|
||||
if (layer.Shader == shader) // If shader isn't same as one in component we need to ignore it.
|
||||
{
|
||||
layer.Shader = null;
|
||||
if (layer.Color == component.Color) // If color isn't the same as one in component we don't want to change it.
|
||||
layer.Color = component.BeforeColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,9 +114,16 @@ public partial class NavMapControl : MapGridControl
|
||||
VerticalExpand = false,
|
||||
Children =
|
||||
{
|
||||
_zoom,
|
||||
_beacons,
|
||||
_recenter,
|
||||
new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
_zoom,
|
||||
_beacons,
|
||||
_recenter
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -163,10 +163,13 @@ namespace Content.Client.Popups
|
||||
PopupEntity(message, uid, type);
|
||||
}
|
||||
|
||||
public override void PopupClient(string? message, EntityUid uid, EntityUid recipient, PopupType type = PopupType.Small)
|
||||
public override void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (recipient == null)
|
||||
return;
|
||||
|
||||
if (_timing.IsFirstTimePredicted)
|
||||
PopupEntity(message, uid, recipient, type);
|
||||
PopupEntity(message, uid, recipient.Value, type);
|
||||
}
|
||||
|
||||
public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small)
|
||||
|
||||
122
Content.Client/RCD/AlignRCDConstruction.cs
Normal file
122
Content.Client/RCD/AlignRCDConstruction.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.RCD.Components;
|
||||
using Content.Shared.RCD.Systems;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.State;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Content.Client.RCD;
|
||||
|
||||
public sealed class AlignRCDConstruction : PlacementMode
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
|
||||
[Dependency] private readonly RCDSystem _rcdSystem = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
|
||||
private const float SearchBoxSize = 2f;
|
||||
private const float PlaceColorBaseAlpha = 0.5f;
|
||||
|
||||
private EntityCoordinates _unalignedMouseCoords = default;
|
||||
|
||||
/// <summary>
|
||||
/// This placement mode is not on the engine because it is content specific (i.e., for the RCD)
|
||||
/// </summary>
|
||||
public AlignRCDConstruction(PlacementManager pMan) : base(pMan)
|
||||
{
|
||||
var dependencies = IoCManager.Instance!;
|
||||
_entityManager = dependencies.Resolve<IEntityManager>();
|
||||
_mapManager = dependencies.Resolve<IMapManager>();
|
||||
_playerManager = dependencies.Resolve<IPlayerManager>();
|
||||
_stateManager = dependencies.Resolve<IStateManager>();
|
||||
|
||||
_mapSystem = _entityManager.System<SharedMapSystem>();
|
||||
_rcdSystem = _entityManager.System<RCDSystem>();
|
||||
_transformSystem = _entityManager.System<SharedTransformSystem>();
|
||||
|
||||
ValidPlaceColor = ValidPlaceColor.WithAlpha(PlaceColorBaseAlpha);
|
||||
}
|
||||
|
||||
public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
|
||||
{
|
||||
_unalignedMouseCoords = ScreenToCursorGrid(mouseScreen);
|
||||
MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager, _mapManager);
|
||||
|
||||
var gridId = MouseCoords.GetGridUid(_entityManager);
|
||||
|
||||
if (!_entityManager.TryGetComponent<MapGridComponent>(gridId, out var mapGrid))
|
||||
return;
|
||||
|
||||
CurrentTile = _mapSystem.GetTileRef(gridId.Value, mapGrid, MouseCoords);
|
||||
|
||||
float tileSize = mapGrid.TileSize;
|
||||
GridDistancing = tileSize;
|
||||
|
||||
if (pManager.CurrentPermission!.IsTile)
|
||||
{
|
||||
MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2,
|
||||
CurrentTile.Y + tileSize / 2));
|
||||
}
|
||||
else
|
||||
{
|
||||
MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2 + pManager.PlacementOffset.X,
|
||||
CurrentTile.Y + tileSize / 2 + pManager.PlacementOffset.Y));
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsValidPosition(EntityCoordinates position)
|
||||
{
|
||||
var player = _playerManager.LocalSession?.AttachedEntity;
|
||||
|
||||
// If the destination is out of interaction range, set the placer alpha to zero
|
||||
if (!_entityManager.TryGetComponent<TransformComponent>(player, out var xform))
|
||||
return false;
|
||||
|
||||
if (!xform.Coordinates.InRange(_entityManager, _transformSystem, position, SharedInteractionSystem.InteractionRange))
|
||||
{
|
||||
InvalidPlaceColor = InvalidPlaceColor.WithAlpha(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise restore the alpha value
|
||||
else
|
||||
{
|
||||
InvalidPlaceColor = InvalidPlaceColor.WithAlpha(PlaceColorBaseAlpha);
|
||||
}
|
||||
|
||||
// Determine if player is carrying an RCD in their active hand
|
||||
if (!_entityManager.TryGetComponent<HandsComponent>(player, out var hands))
|
||||
return false;
|
||||
|
||||
var heldEntity = hands.ActiveHand?.HeldEntity;
|
||||
|
||||
if (!_entityManager.TryGetComponent<RCDComponent>(heldEntity, out var rcd))
|
||||
return false;
|
||||
|
||||
// Retrieve the map grid data for the position
|
||||
if (!_rcdSystem.TryGetMapGridData(position, out var mapGridData))
|
||||
return false;
|
||||
|
||||
// Determine if the user is hovering over a target
|
||||
var currentState = _stateManager.CurrentState;
|
||||
|
||||
if (currentState is not GameplayStateBase screen)
|
||||
return false;
|
||||
|
||||
var target = screen.GetClickedEntity(_unalignedMouseCoords.ToMap(_entityManager, _transformSystem));
|
||||
|
||||
// Determine if the RCD operation is valid or not
|
||||
if (!_rcdSystem.IsRCDOperationStillValid(heldEntity.Value, rcd, mapGridData.Value, target, player.Value, false))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
78
Content.Client/RCD/RCDConstructionGhostSystem.cs
Normal file
78
Content.Client/RCD/RCDConstructionGhostSystem.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.RCD;
|
||||
using Content.Shared.RCD.Components;
|
||||
using Content.Shared.RCD.Systems;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Enums;
|
||||
|
||||
namespace Content.Client.RCD;
|
||||
|
||||
public sealed class RCDConstructionGhostSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly RCDSystem _rcdSystem = default!;
|
||||
[Dependency] private readonly IPlacementManager _placementManager = default!;
|
||||
|
||||
private string _placementMode = typeof(AlignRCDConstruction).Name;
|
||||
private Direction _placementDirection = default;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
// Get current placer data
|
||||
var placerEntity = _placementManager.CurrentPermission?.MobUid;
|
||||
var placerProto = _placementManager.CurrentPermission?.EntityType;
|
||||
var placerIsRCD = HasComp<RCDComponent>(placerEntity);
|
||||
|
||||
// Exit if erasing or the current placer is not an RCD (build mode is active)
|
||||
if (_placementManager.Eraser || (placerEntity != null && !placerIsRCD))
|
||||
return;
|
||||
|
||||
// Determine if player is carrying an RCD in their active hand
|
||||
var player = _playerManager.LocalSession?.AttachedEntity;
|
||||
|
||||
if (!TryComp<HandsComponent>(player, out var hands))
|
||||
return;
|
||||
|
||||
var heldEntity = hands.ActiveHand?.HeldEntity;
|
||||
|
||||
if (!TryComp<RCDComponent>(heldEntity, out var rcd))
|
||||
{
|
||||
// If the player was holding an RCD, but is no longer, cancel placement
|
||||
if (placerIsRCD)
|
||||
_placementManager.Clear();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the direction the RCD prototype based on the placer direction
|
||||
if (_placementDirection != _placementManager.Direction)
|
||||
{
|
||||
_placementDirection = _placementManager.Direction;
|
||||
RaiseNetworkEvent(new RCDConstructionGhostRotationEvent(GetNetEntity(heldEntity.Value), _placementDirection));
|
||||
}
|
||||
|
||||
// If the placer has not changed, exit
|
||||
_rcdSystem.UpdateCachedPrototype(heldEntity.Value, rcd);
|
||||
|
||||
if (heldEntity == placerEntity && rcd.CachedPrototype.Prototype == placerProto)
|
||||
return;
|
||||
|
||||
// Create a new placer
|
||||
var newObjInfo = new PlacementInformation
|
||||
{
|
||||
MobUid = heldEntity.Value,
|
||||
PlacementOption = _placementMode,
|
||||
EntityType = rcd.CachedPrototype.Prototype,
|
||||
Range = (int) Math.Ceiling(SharedInteractionSystem.InteractionRange),
|
||||
IsTile = (rcd.CachedPrototype.Mode == RcdMode.ConstructTile),
|
||||
UseEditorContext = false,
|
||||
};
|
||||
|
||||
_placementManager.Clear();
|
||||
_placementManager.BeginPlacing(newObjInfo);
|
||||
}
|
||||
}
|
||||
47
Content.Client/RCD/RCDMenu.xaml
Normal file
47
Content.Client/RCD/RCDMenu.xaml
Normal file
@@ -0,0 +1,47 @@
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:rcd="clr-namespace:Content.Client.RCD"
|
||||
BackButtonStyleClass="RadialMenuBackButton"
|
||||
CloseButtonStyleClass="RadialMenuCloseButton"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
MinSize="450 450">
|
||||
|
||||
<!-- Note: The min size of the window just determine how close to the edge of the screen the center of the radial menu can be placed -->
|
||||
<!-- The radial menu will try to open so that its center is located where the player's cursor is currently -->
|
||||
|
||||
<!-- Entry layer (shows main categories) -->
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-walls-and-flooring'}" TargetLayer="WallsAndFlooring" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/walls_and_flooring.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-windows-and-grilles'}" TargetLayer="WindowsAndGrilles" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/windows_and_grilles.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-airlocks'}" TargetLayer="Airlocks" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/airlocks.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-electrical'}" TargetLayer="Electrical" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/multicoil.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-lighting'}" TargetLayer="Lighting" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/lighting.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
</ui:RadialContainer>
|
||||
|
||||
<!-- Walls and flooring -->
|
||||
<ui:RadialContainer Name="WallsAndFlooring" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
|
||||
<!-- Windows and grilles -->
|
||||
<ui:RadialContainer Name="WindowsAndGrilles" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
|
||||
<!-- Airlocks -->
|
||||
<ui:RadialContainer Name="Airlocks" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
|
||||
<!-- Computer and machine frames -->
|
||||
<ui:RadialContainer Name="Electrical" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
|
||||
<!-- Lighting -->
|
||||
<ui:RadialContainer Name="Lighting" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
|
||||
</ui:RadialMenu>
|
||||
137
Content.Client/RCD/RCDMenu.xaml.cs
Normal file
137
Content.Client/RCD/RCDMenu.xaml.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.RCD;
|
||||
using Content.Shared.RCD.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.RCD;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class RCDMenu : RadialMenu
|
||||
{
|
||||
[Dependency] private readonly EntityManager _entManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
|
||||
public event Action<ProtoId<RCDPrototype>>? SendRCDSystemMessageAction;
|
||||
|
||||
public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_spriteSystem = _entManager.System<SpriteSystem>();
|
||||
|
||||
// Find the main radial container
|
||||
var main = FindControl<RadialContainer>("Main");
|
||||
|
||||
if (main == null)
|
||||
return;
|
||||
|
||||
// Populate secondary radial containers
|
||||
if (!_entManager.TryGetComponent<RCDComponent>(owner, out var rcd))
|
||||
return;
|
||||
|
||||
foreach (var protoId in rcd.AvailablePrototypes)
|
||||
{
|
||||
if (!_protoManager.TryIndex(protoId, out var proto))
|
||||
continue;
|
||||
|
||||
if (proto.Mode == RcdMode.Invalid)
|
||||
continue;
|
||||
|
||||
var parent = FindControl<RadialContainer>(proto.Category);
|
||||
|
||||
if (parent == null)
|
||||
continue;
|
||||
|
||||
var name = Loc.GetString(proto.SetName);
|
||||
name = char.ToUpper(name[0]) + name.Remove(0, 1);
|
||||
|
||||
var button = new RCDMenuButton()
|
||||
{
|
||||
StyleClasses = { "RadialMenuButton" },
|
||||
SetSize = new Vector2(64f, 64f),
|
||||
ToolTip = name,
|
||||
ProtoId = protoId,
|
||||
};
|
||||
|
||||
if (proto.Sprite != null)
|
||||
{
|
||||
var tex = new TextureRect()
|
||||
{
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
Texture = _spriteSystem.Frame0(proto.Sprite),
|
||||
TextureScale = new Vector2(2f, 2f),
|
||||
};
|
||||
|
||||
button.AddChild(tex);
|
||||
}
|
||||
|
||||
parent.AddChild(button);
|
||||
|
||||
// Ensure that the button that transitions the menu to the associated category layer
|
||||
// is visible in the main radial container (as these all start with Visible = false)
|
||||
foreach (var child in main.Children)
|
||||
{
|
||||
var castChild = child as RadialMenuTextureButton;
|
||||
|
||||
if (castChild is not RadialMenuTextureButton)
|
||||
continue;
|
||||
|
||||
if (castChild.TargetLayer == proto.Category)
|
||||
{
|
||||
castChild.Visible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set up menu actions
|
||||
foreach (var child in Children)
|
||||
AddRCDMenuButtonOnClickActions(child);
|
||||
|
||||
OnChildAdded += AddRCDMenuButtonOnClickActions;
|
||||
|
||||
SendRCDSystemMessageAction += bui.SendRCDSystemMessage;
|
||||
}
|
||||
|
||||
private void AddRCDMenuButtonOnClickActions(Control control)
|
||||
{
|
||||
var radialContainer = control as RadialContainer;
|
||||
|
||||
if (radialContainer == null)
|
||||
return;
|
||||
|
||||
foreach (var child in radialContainer.Children)
|
||||
{
|
||||
var castChild = child as RCDMenuButton;
|
||||
|
||||
if (castChild == null)
|
||||
continue;
|
||||
|
||||
castChild.OnButtonUp += _ =>
|
||||
{
|
||||
SendRCDSystemMessageAction?.Invoke(castChild.ProtoId);
|
||||
Close();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RCDMenuButton : RadialMenuTextureButton
|
||||
{
|
||||
public ProtoId<RCDPrototype> ProtoId { get; set; }
|
||||
|
||||
public RCDMenuButton()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
49
Content.Client/RCD/RCDMenuBoundUserInterface.cs
Normal file
49
Content.Client/RCD/RCDMenuBoundUserInterface.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Content.Shared.RCD;
|
||||
using Content.Shared.RCD.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.RCD;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class RCDMenuBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
[Dependency] private readonly IClyde _displayManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
|
||||
private RCDMenu? _menu;
|
||||
|
||||
public RCDMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_menu = new(Owner, this);
|
||||
_menu.OnClose += Close;
|
||||
|
||||
// Open the menu, centered on the mouse
|
||||
var vpSize = _displayManager.ScreenSize;
|
||||
_menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
|
||||
}
|
||||
|
||||
public void SendRCDSystemMessage(ProtoId<RCDPrototype> protoId)
|
||||
{
|
||||
// A predicted message cannot be used here as the RCD UI is closed immediately
|
||||
// after this message is sent, which will stop the server from receiving it
|
||||
SendMessage(new RCDSystemMessage(protoId));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing) return;
|
||||
|
||||
_menu?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -263,9 +263,10 @@ public sealed partial class MapScreen : BoxContainer
|
||||
|
||||
while (mapComps.MoveNext(out var mapComp, out var mapXform, out var mapMetadata))
|
||||
{
|
||||
if (!_shuttles.CanFTLTo(_shuttleEntity.Value, mapComp.MapId))
|
||||
continue;
|
||||
|
||||
if (_console != null && !_shuttles.CanFTLTo(_shuttleEntity.Value, mapComp.MapId, _console.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var mapName = mapMetadata.EntityName;
|
||||
|
||||
if (string.IsNullOrEmpty(mapName))
|
||||
@@ -310,7 +311,6 @@ public sealed partial class MapScreen : BoxContainer
|
||||
};
|
||||
|
||||
_mapHeadings.Add(mapComp.MapId, gridContents);
|
||||
|
||||
foreach (var grid in _mapManager.GetAllMapGrids(mapComp.MapId))
|
||||
{
|
||||
_entManager.TryGetComponent(grid.Owner, out IFFComponent? iffComp);
|
||||
@@ -327,8 +327,8 @@ public sealed partial class MapScreen : BoxContainer
|
||||
{
|
||||
AddMapObject(mapComp.MapId, gridObj);
|
||||
}
|
||||
else if (iffComp == null ||
|
||||
(iffComp.Flags & IFFFlags.Hide) == 0x0)
|
||||
else if (!_shuttles.IsBeaconMap(_mapManager.GetMapEntityId(mapComp.MapId)) && (iffComp == null ||
|
||||
(iffComp.Flags & IFFFlags.Hide) == 0x0))
|
||||
{
|
||||
_pendingMapObjects.Add((mapComp.MapId, gridObj));
|
||||
}
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
using Content.Shared.Store;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Serilog;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Store.Ui;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class StoreBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
[ViewVariables]
|
||||
private StoreMenu? _menu;
|
||||
|
||||
[ViewVariables]
|
||||
private string _windowName = Loc.GetString("store-ui-default-title");
|
||||
|
||||
[ViewVariables]
|
||||
private string _search = "";
|
||||
|
||||
[ViewVariables]
|
||||
private HashSet<ListingData> _listings = new();
|
||||
|
||||
public StoreBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
@@ -49,6 +54,12 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
|
||||
SendMessage(new StoreRequestUpdateInterfaceMessage());
|
||||
};
|
||||
|
||||
_menu.SearchTextUpdated += (_, search) =>
|
||||
{
|
||||
_search = search.Trim().ToLowerInvariant();
|
||||
UpdateListingsWithSearchFilter();
|
||||
};
|
||||
|
||||
_menu.OnRefundAttempt += (_) =>
|
||||
{
|
||||
SendMessage(new StoreRequestRefundMessage());
|
||||
@@ -64,10 +75,10 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
|
||||
switch (state)
|
||||
{
|
||||
case StoreUpdateState msg:
|
||||
_menu.UpdateBalance(msg.Balance);
|
||||
_menu.PopulateStoreCategoryButtons(msg.Listings);
|
||||
_listings = msg.Listings;
|
||||
|
||||
_menu.UpdateListing(msg.Listings.ToList());
|
||||
_menu.UpdateBalance(msg.Balance);
|
||||
UpdateListingsWithSearchFilter();
|
||||
_menu.SetFooterVisibility(msg.ShowFooter);
|
||||
_menu.UpdateRefund(msg.AllowRefund);
|
||||
break;
|
||||
@@ -89,4 +100,19 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
|
||||
_menu?.Close();
|
||||
_menu?.Dispose();
|
||||
}
|
||||
|
||||
private void UpdateListingsWithSearchFilter()
|
||||
{
|
||||
if (_menu == null)
|
||||
return;
|
||||
|
||||
var filteredListings = new HashSet<ListingData>(_listings);
|
||||
if (!string.IsNullOrEmpty(_search))
|
||||
{
|
||||
filteredListings.RemoveWhere(listingData => !ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listingData, _prototypeManager).Trim().ToLowerInvariant().Contains(_search) &&
|
||||
!ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(listingData, _prototypeManager).Trim().ToLowerInvariant().Contains(_search));
|
||||
}
|
||||
_menu.PopulateStoreCategoryButtons(filteredListings);
|
||||
_menu.UpdateListing(filteredListings.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
HorizontalAlignment="Right"
|
||||
Text="Refund" />
|
||||
</BoxContainer>
|
||||
<PanelContainer VerticalExpand="True">
|
||||
<LineEdit Name="SearchBar" Margin="4" PlaceHolder="Search" HorizontalExpand="True"/>
|
||||
<PanelContainer VerticalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Content.Client.Actions;
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Client.Message;
|
||||
@@ -27,6 +26,7 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
|
||||
private StoreWithdrawWindow? _withdrawWindow;
|
||||
|
||||
public event EventHandler<string>? SearchTextUpdated;
|
||||
public event Action<BaseButton.ButtonEventArgs, ListingData>? OnListingButtonPressed;
|
||||
public event Action<BaseButton.ButtonEventArgs, string>? OnCategoryButtonPressed;
|
||||
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
|
||||
@@ -46,6 +46,7 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
|
||||
RefreshButton.OnButtonDown += OnRefreshButtonDown;
|
||||
RefundButton.OnButtonDown += OnRefundButtonDown;
|
||||
SearchBar.OnTextChanged += _ => SearchTextUpdated?.Invoke(this, SearchBar.Text);
|
||||
|
||||
if (Window != null)
|
||||
Window.Title = name;
|
||||
@@ -59,7 +60,7 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
(type.Key, type.Value), type => _prototypeManager.Index<CurrencyPrototype>(type.Key));
|
||||
|
||||
var balanceStr = string.Empty;
|
||||
foreach (var ((type, amount),proto) in currency)
|
||||
foreach (var ((_, amount), proto) in currency)
|
||||
{
|
||||
balanceStr += Loc.GetString("store-ui-balance-display", ("amount", amount),
|
||||
("currency", Loc.GetString(proto.DisplayName, ("amount", 1))));
|
||||
@@ -81,7 +82,6 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
{
|
||||
var sorted = listings.OrderBy(l => l.Priority).ThenBy(l => l.Cost.Values.Sum());
|
||||
|
||||
|
||||
// should probably chunk these out instead. to-do if this clogs the internet tubes.
|
||||
// maybe read clients prototypes instead?
|
||||
ClearListings();
|
||||
@@ -129,8 +129,8 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
if (!listing.Categories.Contains(CurrentCategory))
|
||||
return;
|
||||
|
||||
var listingName = Loc.GetString(listing.Name);
|
||||
var listingDesc = Loc.GetString(listing.Description);
|
||||
var listingName = ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _prototypeManager);
|
||||
var listingDesc = ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(listing, _prototypeManager);
|
||||
var listingPrice = listing.Cost;
|
||||
var canBuy = CanBuyListing(Balance, listingPrice);
|
||||
|
||||
@@ -144,12 +144,6 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
{
|
||||
if (texture == null)
|
||||
texture = spriteSys.GetPrototypeIcon(listing.ProductEntity).Default;
|
||||
|
||||
var proto = _prototypeManager.Index<EntityPrototype>(listing.ProductEntity);
|
||||
if (listingName == string.Empty)
|
||||
listingName = proto.Name;
|
||||
if (listingDesc == string.Empty)
|
||||
listingDesc = proto.Description;
|
||||
}
|
||||
else if (listing.ProductAction != null)
|
||||
{
|
||||
@@ -243,13 +237,16 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
|
||||
allCategories = allCategories.OrderBy(c => c.Priority).ToList();
|
||||
|
||||
// This will reset the Current Category selection if nothing matches the search.
|
||||
if (allCategories.All(category => category.ID != CurrentCategory))
|
||||
CurrentCategory = string.Empty;
|
||||
|
||||
if (CurrentCategory == string.Empty && allCategories.Count > 0)
|
||||
CurrentCategory = allCategories.First().ID;
|
||||
|
||||
if (allCategories.Count <= 1)
|
||||
return;
|
||||
|
||||
CategoryListContainer.Children.Clear();
|
||||
if (allCategories.Count < 1)
|
||||
return;
|
||||
|
||||
foreach (var proto in allCategories)
|
||||
{
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace Content.Client.Stylesheets
|
||||
public const string StyleClassBorderedWindowPanel = "BorderedWindowPanel";
|
||||
public const string StyleClassInventorySlotBackground = "InventorySlotBackground";
|
||||
public const string StyleClassHandSlotHighlight = "HandSlotHighlight";
|
||||
public const string StyleClassChatPanel = "ChatPanel";
|
||||
public const string StyleClassChatSubPanel = "ChatSubPanel";
|
||||
public const string StyleClassTransparentBorderedWindowPanel = "TransparentBorderedWindowPanel";
|
||||
public const string StyleClassHotbarPanel = "HotbarPanel";
|
||||
@@ -144,6 +145,8 @@ namespace Content.Client.Stylesheets
|
||||
public const string StyleClassButtonColorRed = "ButtonColorRed";
|
||||
public const string StyleClassButtonColorGreen = "ButtonColorGreen";
|
||||
|
||||
public static readonly Color ChatBackgroundColor = Color.FromHex("#25252ADD");
|
||||
|
||||
public override Stylesheet Stylesheet { get; }
|
||||
|
||||
public StyleNano(IResourceCache resCache) : base(resCache)
|
||||
@@ -290,7 +293,7 @@ namespace Content.Client.Stylesheets
|
||||
var buttonTex = resCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
|
||||
var topButtonBase = new StyleBoxTexture
|
||||
{
|
||||
Texture = buttonTex,
|
||||
Texture = buttonTex,
|
||||
};
|
||||
topButtonBase.SetPatchMargin(StyleBox.Margin.All, 10);
|
||||
topButtonBase.SetPadding(StyleBox.Margin.All, 0);
|
||||
@@ -298,19 +301,19 @@ namespace Content.Client.Stylesheets
|
||||
|
||||
var topButtonOpenRight = new StyleBoxTexture(topButtonBase)
|
||||
{
|
||||
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(0, 0), new Vector2(14, 24))),
|
||||
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(0, 0), new Vector2(14, 24))),
|
||||
};
|
||||
topButtonOpenRight.SetPatchMargin(StyleBox.Margin.Right, 0);
|
||||
|
||||
var topButtonOpenLeft = new StyleBoxTexture(topButtonBase)
|
||||
{
|
||||
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(14, 24))),
|
||||
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(14, 24))),
|
||||
};
|
||||
topButtonOpenLeft.SetPatchMargin(StyleBox.Margin.Left, 0);
|
||||
|
||||
var topButtonSquare = new StyleBoxTexture(topButtonBase)
|
||||
{
|
||||
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(3, 24))),
|
||||
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(3, 24))),
|
||||
};
|
||||
topButtonSquare.SetPatchMargin(StyleBox.Margin.Horizontal, 0);
|
||||
|
||||
@@ -346,12 +349,16 @@ namespace Content.Client.Stylesheets
|
||||
lineEdit.SetPatchMargin(StyleBox.Margin.All, 3);
|
||||
lineEdit.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
|
||||
|
||||
var chatSubBGTex = resCache.GetTexture("/Textures/Interface/Nano/chat_sub_background.png");
|
||||
var chatSubBG = new StyleBoxTexture
|
||||
var chatBg = new StyleBoxFlat
|
||||
{
|
||||
Texture = chatSubBGTex,
|
||||
BackgroundColor = ChatBackgroundColor
|
||||
};
|
||||
chatSubBG.SetPatchMargin(StyleBox.Margin.All, 2);
|
||||
|
||||
var chatSubBg = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = ChatBackgroundColor,
|
||||
};
|
||||
chatSubBg.SetContentMarginOverride(StyleBox.Margin.All, 2);
|
||||
|
||||
var actionSearchBoxTex = resCache.GetTexture("/Textures/Interface/Nano/black_panel_dark_thin_border.png");
|
||||
var actionSearchBox = new StyleBoxTexture
|
||||
@@ -368,9 +375,9 @@ namespace Content.Client.Stylesheets
|
||||
};
|
||||
tabContainerPanel.SetPatchMargin(StyleBox.Margin.All, 2);
|
||||
|
||||
var tabContainerBoxActive = new StyleBoxFlat {BackgroundColor = new Color(64, 64, 64)};
|
||||
var tabContainerBoxActive = new StyleBoxFlat { BackgroundColor = new Color(64, 64, 64) };
|
||||
tabContainerBoxActive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
|
||||
var tabContainerBoxInactive = new StyleBoxFlat {BackgroundColor = new Color(32, 32, 32)};
|
||||
var tabContainerBoxInactive = new StyleBoxFlat { BackgroundColor = new Color(32, 32, 32) };
|
||||
tabContainerBoxInactive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
|
||||
|
||||
var progressBarBackground = new StyleBoxFlat
|
||||
@@ -409,21 +416,21 @@ namespace Content.Client.Stylesheets
|
||||
|
||||
// Placeholder
|
||||
var placeholderTexture = resCache.GetTexture("/Textures/Interface/Nano/placeholder.png");
|
||||
var placeholder = new StyleBoxTexture {Texture = placeholderTexture};
|
||||
var placeholder = new StyleBoxTexture { Texture = placeholderTexture };
|
||||
placeholder.SetPatchMargin(StyleBox.Margin.All, 19);
|
||||
placeholder.SetExpandMargin(StyleBox.Margin.All, -5);
|
||||
placeholder.Mode = StyleBoxTexture.StretchMode.Tile;
|
||||
|
||||
var itemListBackgroundSelected = new StyleBoxFlat {BackgroundColor = new Color(75, 75, 86)};
|
||||
var itemListBackgroundSelected = new StyleBoxFlat { BackgroundColor = new Color(75, 75, 86) };
|
||||
itemListBackgroundSelected.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
|
||||
itemListBackgroundSelected.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
|
||||
var itemListItemBackgroundDisabled = new StyleBoxFlat {BackgroundColor = new Color(10, 10, 12)};
|
||||
var itemListItemBackgroundDisabled = new StyleBoxFlat { BackgroundColor = new Color(10, 10, 12) };
|
||||
itemListItemBackgroundDisabled.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
|
||||
itemListItemBackgroundDisabled.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
|
||||
var itemListItemBackground = new StyleBoxFlat {BackgroundColor = new Color(55, 55, 68)};
|
||||
var itemListItemBackground = new StyleBoxFlat { BackgroundColor = new Color(55, 55, 68) };
|
||||
itemListItemBackground.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
|
||||
itemListItemBackground.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
|
||||
var itemListItemBackgroundTransparent = new StyleBoxFlat {BackgroundColor = Color.Transparent};
|
||||
var itemListItemBackgroundTransparent = new StyleBoxFlat { BackgroundColor = Color.Transparent };
|
||||
itemListItemBackgroundTransparent.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
|
||||
itemListItemBackgroundTransparent.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
|
||||
|
||||
@@ -489,9 +496,9 @@ namespace Content.Client.Stylesheets
|
||||
sliderForeBox.SetPatchMargin(StyleBox.Margin.All, 12);
|
||||
sliderGrabBox.SetPatchMargin(StyleBox.Margin.All, 12);
|
||||
|
||||
var sliderFillGreen = new StyleBoxTexture(sliderFillBox) {Modulate = Color.LimeGreen};
|
||||
var sliderFillRed = new StyleBoxTexture(sliderFillBox) {Modulate = Color.Red};
|
||||
var sliderFillBlue = new StyleBoxTexture(sliderFillBox) {Modulate = Color.Blue};
|
||||
var sliderFillGreen = new StyleBoxTexture(sliderFillBox) { Modulate = Color.LimeGreen };
|
||||
var sliderFillRed = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Red };
|
||||
var sliderFillBlue = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Blue };
|
||||
var sliderFillWhite = new StyleBoxTexture(sliderFillBox) { Modulate = Color.White };
|
||||
|
||||
var boxFont13 = resCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13);
|
||||
@@ -850,6 +857,13 @@ namespace Content.Client.Stylesheets
|
||||
Element<TextEdit>().Pseudo(TextEdit.StylePseudoClassPlaceholder)
|
||||
.Prop("font-color", Color.Gray),
|
||||
|
||||
// chat subpanels (chat lineedit backing, popup backings)
|
||||
new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {StyleClassChatPanel}, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(PanelContainer.StylePropertyPanel, chatBg),
|
||||
}),
|
||||
|
||||
// Chat lineedit - we don't actually draw a stylebox around the lineedit itself, we put it around the
|
||||
// input + other buttons, so we must clear the default stylebox
|
||||
new StyleRule(new SelectorElement(typeof(LineEdit), new[] {StyleClassChatLineEdit}, null, null),
|
||||
@@ -858,13 +872,6 @@ namespace Content.Client.Stylesheets
|
||||
new StyleProperty(LineEdit.StylePropertyStyleBox, new StyleBoxEmpty()),
|
||||
}),
|
||||
|
||||
// chat subpanels (chat lineedit backing, popup backings)
|
||||
new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {StyleClassChatSubPanel}, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(PanelContainer.StylePropertyPanel, chatSubBG),
|
||||
}),
|
||||
|
||||
// Action searchbox lineedit
|
||||
new StyleRule(new SelectorElement(typeof(LineEdit), new[] {StyleClassActionSearchBox}, null, null),
|
||||
new[]
|
||||
@@ -1468,6 +1475,25 @@ namespace Content.Client.Stylesheets
|
||||
Element<Label>().Class("Disabled")
|
||||
.Prop("font-color", DisabledFore),
|
||||
|
||||
// Radial menu buttons
|
||||
Element<TextureButton>().Class("RadialMenuButton")
|
||||
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/button_normal.png")),
|
||||
Element<TextureButton>().Class("RadialMenuButton")
|
||||
.Pseudo(TextureButton.StylePseudoClassHover)
|
||||
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/button_hover.png")),
|
||||
|
||||
Element<TextureButton>().Class("RadialMenuCloseButton")
|
||||
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/close_normal.png")),
|
||||
Element<TextureButton>().Class("RadialMenuCloseButton")
|
||||
.Pseudo(TextureButton.StylePseudoClassHover)
|
||||
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/close_hover.png")),
|
||||
|
||||
Element<TextureButton>().Class("RadialMenuBackButton")
|
||||
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/back_normal.png")),
|
||||
Element<TextureButton>().Class("RadialMenuBackButton")
|
||||
.Pseudo(TextureButton.StylePseudoClassHover)
|
||||
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/back_hover.png")),
|
||||
|
||||
//PDA - Backgrounds
|
||||
Element<PanelContainer>().Class("PdaContentBackground")
|
||||
.Prop(PanelContainer.StylePropertyPanel, BaseButtonOpenBoth)
|
||||
|
||||
@@ -62,6 +62,8 @@ public sealed class TextScreenSystem : VisualizerSystem<TextScreenVisualsCompone
|
||||
|
||||
SubscribeLocalEvent<TextScreenVisualsComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<TextScreenTimerComponent, ComponentInit>(OnTimerInit);
|
||||
|
||||
UpdatesOutsidePrediction = true;
|
||||
}
|
||||
|
||||
private void OnInit(EntityUid uid, TextScreenVisualsComponent component, ComponentInit args)
|
||||
@@ -110,17 +112,11 @@ public sealed class TextScreenSystem : VisualizerSystem<TextScreenVisualsCompone
|
||||
if (args.AppearanceData.TryGetValue(TextScreenVisuals.Color, out var color) && color is Color)
|
||||
component.Color = (Color) color;
|
||||
|
||||
// DefaultText: broadcast updates from comms consoles
|
||||
// ScreenText: the text accompanying shuttle timers e.g. "ETA"
|
||||
// DefaultText: fallback text e.g. broadcast updates from comms consoles
|
||||
if (args.AppearanceData.TryGetValue(TextScreenVisuals.DefaultText, out var newDefault) && newDefault is string)
|
||||
{
|
||||
string?[] defaultText = SegmentText((string) newDefault, component);
|
||||
component.Text = defaultText;
|
||||
component.TextToDraw = defaultText;
|
||||
ResetText(uid, component);
|
||||
BuildTextLayers(uid, component, args.Sprite);
|
||||
DrawLayers(uid, component.LayerStatesToDraw);
|
||||
}
|
||||
component.Text = SegmentText((string) newDefault, component);
|
||||
|
||||
// ScreenText: currently rendered text e.g. the "ETA" accompanying shuttle timers
|
||||
if (args.AppearanceData.TryGetValue(TextScreenVisuals.ScreenText, out var text) && text is string)
|
||||
{
|
||||
component.TextToDraw = SegmentText((string) text, component);
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using Content.Shared.Toilet;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Toilet;
|
||||
|
||||
public sealed class ToiletVisualsSystem : VisualizerSystem<ToiletComponent>
|
||||
{
|
||||
protected override void OnAppearanceChange(EntityUid uid, ToiletComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null) return;
|
||||
|
||||
AppearanceSystem.TryGetData<bool>(uid, ToiletVisuals.LidOpen, out var lidOpen, args.Component);
|
||||
AppearanceSystem.TryGetData<bool>(uid, ToiletVisuals.SeatUp, out var seatUp, args.Component);
|
||||
|
||||
var state = (lidOpen, seatUp) switch
|
||||
{
|
||||
(false, false) => "closed_toilet_seat_down",
|
||||
(false, true) => "closed_toilet_seat_up",
|
||||
(true, false) => "open_toilet_seat_down",
|
||||
(true, true) => "open_toilet_seat_up"
|
||||
};
|
||||
|
||||
args.Sprite.LayerSetState(0, state);
|
||||
}
|
||||
}
|
||||
105
Content.Client/UserInterface/Controls/RadialContainer.cs
Normal file
105
Content.Client/UserInterface/Controls/RadialContainer.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
[Virtual]
|
||||
public class RadialContainer : LayoutContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the anglular range, in radians, in which child elements will be placed.
|
||||
/// The first value denotes the angle at which the first element is to be placed, and
|
||||
/// the second value denotes the angle at which the last element is to be placed.
|
||||
/// Both values must be between 0 and 2 PI radians
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The top of the screen is at 0 radians, and the bottom of the screen is at PI radians
|
||||
/// </remarks>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Vector2 AngularRange
|
||||
{
|
||||
get
|
||||
{
|
||||
return _angularRange;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
var x = value.X;
|
||||
var y = value.Y;
|
||||
|
||||
x = x > MathF.Tau ? x % MathF.Tau : x;
|
||||
y = y > MathF.Tau ? y % MathF.Tau : y;
|
||||
|
||||
x = x < 0 ? MathF.Tau + x : x;
|
||||
y = y < 0 ? MathF.Tau + y : y;
|
||||
|
||||
_angularRange = new Vector2(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 _angularRange = new Vector2(0f, MathF.Tau - float.Epsilon);
|
||||
|
||||
/// <summary>
|
||||
/// Determines the direction in which child elements will be arranged
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public RAlignment RadialAlignment { get; set; } = RAlignment.Clockwise;
|
||||
|
||||
/// <summary>
|
||||
/// Determines how far from the radial container's center that its child elements will be placed
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Radius { get; set; } = 100f;
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether the container should reserve a space on the layout for child which are not currently visible
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool ReserveSpaceForHiddenChildren { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// This container arranges its children, evenly separated, in a radial pattern
|
||||
/// </summary>
|
||||
public RadialContainer()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
var children = ReserveSpaceForHiddenChildren ? Children : Children.Where(x => x.Visible);
|
||||
var childCount = children.Count();
|
||||
|
||||
// Determine the size of the arc, accounting for clockwise and anti-clockwise arrangements
|
||||
var arc = AngularRange.Y - AngularRange.X;
|
||||
arc = (arc < 0) ? MathF.Tau + arc : arc;
|
||||
arc = (RadialAlignment == RAlignment.AntiClockwise) ? MathF.Tau - arc : arc;
|
||||
|
||||
// Account for both circular arrangements and arc-based arrangements
|
||||
var childMod = MathHelper.CloseTo(arc, MathF.Tau, 0.01f) ? 0 : 1;
|
||||
|
||||
// Determine the separation between child elements
|
||||
var sepAngle = arc / (childCount - childMod);
|
||||
sepAngle *= (RadialAlignment == RAlignment.AntiClockwise) ? -1f : 1f;
|
||||
|
||||
// Adjust the positions of all the child elements
|
||||
foreach (var (i, child) in children.Select((x, i) => (i, x)))
|
||||
{
|
||||
var position = new Vector2(Radius * MathF.Sin(AngularRange.X + sepAngle * i) + Width / 2f - child.Width / 2f, -Radius * MathF.Cos(AngularRange.X + sepAngle * i) + Height / 2f - child.Height / 2f);
|
||||
SetPosition(child, position);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the different radial alignment modes
|
||||
/// </summary>
|
||||
/// <seealso cref="RadialAlignment"/>
|
||||
public enum RAlignment : byte
|
||||
{
|
||||
Clockwise,
|
||||
AntiClockwise,
|
||||
}
|
||||
}
|
||||
255
Content.Client/UserInterface/Controls/RadialMenu.cs
Normal file
255
Content.Client/UserInterface/Controls/RadialMenu.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
[Virtual]
|
||||
public class RadialMenu : BaseWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// Contextual button used to traverse through previous layers of the radial menu
|
||||
/// </summary>
|
||||
public TextureButton? ContextualButton { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set a style class to be applied to the contextual button when it is set to move the user back through previous layers of the radial menu
|
||||
/// </summary>
|
||||
public string? BackButtonStyleClass
|
||||
{
|
||||
get
|
||||
{
|
||||
return _backButtonStyleClass;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_backButtonStyleClass = value;
|
||||
|
||||
if (_path.Count > 0 && ContextualButton != null && _backButtonStyleClass != null)
|
||||
ContextualButton.SetOnlyStyleClass(_backButtonStyleClass);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a style class to be applied to the contextual button when it will close the radial menu
|
||||
/// </summary>
|
||||
public string? CloseButtonStyleClass
|
||||
{
|
||||
get
|
||||
{
|
||||
return _closeButtonStyleClass;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_closeButtonStyleClass = value;
|
||||
|
||||
if (_path.Count == 0 && ContextualButton != null && _closeButtonStyleClass != null)
|
||||
ContextualButton.SetOnlyStyleClass(_closeButtonStyleClass);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Control> _path = new();
|
||||
private string? _backButtonStyleClass;
|
||||
private string? _closeButtonStyleClass;
|
||||
|
||||
/// <summary>
|
||||
/// A free floating menu which enables the quick display of one or more radial containers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only one radial container is visible at a time (each container forming a separate 'layer' within
|
||||
/// the menu), along with a contextual button at the menu center, which will either return the user
|
||||
/// to the previous layer or close the menu if there are no previous layers left to traverse.
|
||||
/// To create a functional radial menu, simply parent one or more named radial containers to it,
|
||||
/// and populate the radial containers with RadialMenuButtons. Setting the TargetLayer field of these
|
||||
/// buttons to the name of a radial conatiner will display the container in question to the user
|
||||
/// whenever it is clicked in additon to any other actions assigned to the button
|
||||
/// </remarks>
|
||||
public RadialMenu()
|
||||
{
|
||||
// Hide all starting children (if any) except the first (this is the active layer)
|
||||
if (ChildCount > 1)
|
||||
{
|
||||
for (int i = 1; i < ChildCount; i++)
|
||||
GetChild(i).Visible = false;
|
||||
}
|
||||
|
||||
// Auto generate a contextual button for moving back through visited layers
|
||||
ContextualButton = new TextureButton()
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SetSize = new Vector2(64f, 64f),
|
||||
};
|
||||
|
||||
ContextualButton.OnButtonUp += _ => ReturnToPreviousLayer();
|
||||
AddChild(ContextualButton);
|
||||
|
||||
// Hide any further add children, unless its promoted to the active layer
|
||||
OnChildAdded += child => child.Visible = (GetCurrentActiveLayer() == child);
|
||||
}
|
||||
|
||||
private Control? GetCurrentActiveLayer()
|
||||
{
|
||||
var children = Children.Where(x => x != ContextualButton);
|
||||
|
||||
if (!children.Any())
|
||||
return null;
|
||||
|
||||
return children.First(x => x.Visible);
|
||||
}
|
||||
|
||||
public bool TryToMoveToNewLayer(string newLayer)
|
||||
{
|
||||
if (newLayer == string.Empty)
|
||||
return false;
|
||||
|
||||
var currentLayer = GetCurrentActiveLayer();
|
||||
|
||||
if (currentLayer == null)
|
||||
return false;
|
||||
|
||||
var result = false;
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child == ContextualButton)
|
||||
continue;
|
||||
|
||||
// Hide layers which are not of interest
|
||||
if (result == true || child.Name != newLayer)
|
||||
{
|
||||
child.Visible = false;
|
||||
}
|
||||
|
||||
// Show the layer of interest
|
||||
else
|
||||
{
|
||||
child.Visible = true;
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the traversal path
|
||||
if (result)
|
||||
_path.Add(currentLayer);
|
||||
|
||||
// Set the style class of the button
|
||||
if (_path.Count > 0 && ContextualButton != null && BackButtonStyleClass != null)
|
||||
ContextualButton.SetOnlyStyleClass(BackButtonStyleClass);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void ReturnToPreviousLayer()
|
||||
{
|
||||
// Close the menu if the traversal path is empty
|
||||
if (_path.Count == 0)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
var lastChild = _path[^1];
|
||||
|
||||
// Hide all children except the contextual button
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child != ContextualButton)
|
||||
child.Visible = false;
|
||||
}
|
||||
|
||||
// Make the last visited layer visible, update the path list
|
||||
lastChild.Visible = true;
|
||||
_path.RemoveAt(_path.Count - 1);
|
||||
|
||||
// Set the style class of the button
|
||||
if (_path.Count == 0 && ContextualButton != null && CloseButtonStyleClass != null)
|
||||
ContextualButton.SetOnlyStyleClass(CloseButtonStyleClass);
|
||||
}
|
||||
}
|
||||
|
||||
[Virtual]
|
||||
public class RadialMenuButton : Button
|
||||
{
|
||||
/// <summary>
|
||||
/// Upon clicking this button the radial menu will transition to the named layer
|
||||
/// </summary>
|
||||
public string? TargetLayer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A simple button that can move the user to a different layer within a radial menu
|
||||
/// </summary>
|
||||
public RadialMenuButton()
|
||||
{
|
||||
OnButtonUp += OnClicked;
|
||||
}
|
||||
|
||||
private void OnClicked(ButtonEventArgs args)
|
||||
{
|
||||
if (TargetLayer == null || TargetLayer == string.Empty)
|
||||
return;
|
||||
|
||||
var parent = FindParentMultiLayerContainer(this);
|
||||
|
||||
if (parent == null)
|
||||
return;
|
||||
|
||||
parent.TryToMoveToNewLayer(TargetLayer);
|
||||
}
|
||||
|
||||
private RadialMenu? FindParentMultiLayerContainer(Control control)
|
||||
{
|
||||
foreach (var ancestor in control.GetSelfAndLogicalAncestors())
|
||||
{
|
||||
if (ancestor is RadialMenu)
|
||||
return ancestor as RadialMenu;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[Virtual]
|
||||
public class RadialMenuTextureButton : TextureButton
|
||||
{
|
||||
/// <summary>
|
||||
/// Upon clicking this button the radial menu will be moved to the named layer
|
||||
/// </summary>
|
||||
public string TargetLayer { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// A simple texture button that can move the user to a different layer within a radial menu
|
||||
/// </summary>
|
||||
public RadialMenuTextureButton()
|
||||
{
|
||||
OnButtonUp += OnClicked;
|
||||
}
|
||||
|
||||
private void OnClicked(ButtonEventArgs args)
|
||||
{
|
||||
if (TargetLayer == string.Empty)
|
||||
return;
|
||||
|
||||
var parent = FindParentMultiLayerContainer(this);
|
||||
|
||||
if (parent == null)
|
||||
return;
|
||||
|
||||
parent.TryToMoveToNewLayer(TargetLayer);
|
||||
}
|
||||
|
||||
private RadialMenu? FindParentMultiLayerContainer(Control control)
|
||||
{
|
||||
foreach (var ancestor in control.GetSelfAndLogicalAncestors())
|
||||
{
|
||||
if (ancestor is RadialMenu)
|
||||
return ancestor as RadialMenu;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using Content.Client.Chat.UI;
|
||||
using Content.Client.Examine;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Ghost;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.Screens;
|
||||
using Content.Client.UserInterface.Systems.Chat.Widgets;
|
||||
using Content.Client.UserInterface.Systems.Gameplay;
|
||||
@@ -54,7 +55,6 @@ public sealed class ChatUIController : UIController
|
||||
[Dependency] private readonly IStateManager _state = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
[UISystemDependency] private readonly ExamineSystem? _examine = default;
|
||||
[UISystemDependency] private readonly GhostSystem? _ghost = default;
|
||||
@@ -179,8 +179,8 @@ public sealed class ChatUIController : UIController
|
||||
_net.RegisterNetMessage<MsgChatMessage>(OnChatMessage);
|
||||
_net.RegisterNetMessage<MsgDeleteChatMessagesBy>(OnDeleteChatMessagesBy);
|
||||
SubscribeNetworkEvent<DamageForceSayEvent>(OnDamageForceSay);
|
||||
_cfg.OnValueChanged(CCVars.ChatEnableColorName, (value) => { _chatNameColorsEnabled = value; });
|
||||
_chatNameColorsEnabled = _cfg.GetCVar(CCVars.ChatEnableColorName);
|
||||
_config.OnValueChanged(CCVars.ChatEnableColorName, (value) => { _chatNameColorsEnabled = value; });
|
||||
_chatNameColorsEnabled = _config.GetCVar(CCVars.ChatEnableColorName);
|
||||
|
||||
_speechBubbleRoot = new LayoutContainer();
|
||||
|
||||
@@ -232,6 +232,9 @@ public sealed class ChatUIController : UIController
|
||||
{
|
||||
_chatNameColors[i] = nameColors[i].ToHex();
|
||||
}
|
||||
|
||||
_config.OnValueChanged(CCVars.ChatWindowOpacity, OnChatWindowOpacityChanged);
|
||||
|
||||
}
|
||||
|
||||
public void OnScreenLoad()
|
||||
@@ -240,6 +243,8 @@ public sealed class ChatUIController : UIController
|
||||
|
||||
var viewportContainer = UIManager.ActiveScreen!.FindControl<LayoutContainer>("ViewportContainer");
|
||||
SetSpeechBubbleRoot(viewportContainer);
|
||||
|
||||
SetChatWindowOpacity(_config.GetCVar(CCVars.ChatWindowOpacity));
|
||||
}
|
||||
|
||||
public void OnScreenUnload()
|
||||
@@ -247,6 +252,34 @@ public sealed class ChatUIController : UIController
|
||||
SetMainChat(false);
|
||||
}
|
||||
|
||||
private void OnChatWindowOpacityChanged(float opacity)
|
||||
{
|
||||
SetChatWindowOpacity(opacity);
|
||||
}
|
||||
|
||||
private void SetChatWindowOpacity(float opacity)
|
||||
{
|
||||
var chatBox = UIManager.ActiveScreen?.GetWidget<ChatBox>() ?? UIManager.ActiveScreen?.GetWidget<ResizableChatBox>();
|
||||
|
||||
var panel = chatBox?.ChatWindowPanel;
|
||||
if (panel is null)
|
||||
return;
|
||||
|
||||
Color color;
|
||||
if (panel.PanelOverride is StyleBoxFlat styleBoxFlat)
|
||||
color = styleBoxFlat.BackgroundColor;
|
||||
else if (panel.TryGetStyleProperty<StyleBox>(PanelContainer.StylePropertyPanel, out var style)
|
||||
&& style is StyleBoxFlat propStyleBoxFlat)
|
||||
color = propStyleBoxFlat.BackgroundColor;
|
||||
else
|
||||
color = StyleNano.ChatBackgroundColor;
|
||||
|
||||
panel.PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = color.WithAlpha(opacity)
|
||||
};
|
||||
}
|
||||
|
||||
public void SetMainChat(bool setting)
|
||||
{
|
||||
if (UIManager.ActiveScreen == null)
|
||||
@@ -770,7 +803,7 @@ public sealed class ChatUIController : UIController
|
||||
ProcessChatMessage(msg);
|
||||
|
||||
if ((msg.Channel & ChatChannel.AdminRelated) == 0 ||
|
||||
_cfg.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||
_config.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||
{
|
||||
_replayRecording.RecordClientMessage(msg);
|
||||
}
|
||||
@@ -830,7 +863,7 @@ public sealed class ChatUIController : UIController
|
||||
break;
|
||||
|
||||
case ChatChannel.LOOC:
|
||||
if (_cfg.GetCVar(CCVars.LoocAboveHeadShow))
|
||||
if (_config.GetCVar(CCVars.LoocAboveHeadShow))
|
||||
AddSpeechBubble(msg, SpeechBubble.SpeechType.Looc);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Chat;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
@@ -44,6 +45,7 @@ public class ChatInputBox : PanelContainer
|
||||
StyleClasses = {"chatFilterOptionButton"}
|
||||
};
|
||||
Container.AddChild(FilterButton);
|
||||
AddStyleClass(StyleNano.StyleClassChatSubPanel);
|
||||
ChannelSelector.OnChannelSelect += UpdateActiveChannel;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,8 @@
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
MinSize="465 225">
|
||||
<PanelContainer HorizontalExpand="True" VerticalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat BackgroundColor="#25252ADD" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<PanelContainer Name="ChatWindowPanel" Access="Public" HorizontalExpand="True" VerticalExpand="True"
|
||||
StyleClasses="StyleNano.StyleClassChatPanel">
|
||||
<BoxContainer Orientation="Vertical" SeparationOverride="4" HorizontalExpand="True" VerticalExpand="True">
|
||||
<OutputPanel Name="Contents" HorizontalExpand="True" VerticalExpand="True" Margin="8 8 8 4" />
|
||||
<controls:ChatInputBox HorizontalExpand="True" Name="ChatInput" Access="Public" Margin="2"/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<controls:ItemStatusPanel
|
||||
<controls:ItemStatusPanel
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Controls"
|
||||
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
@@ -18,7 +18,7 @@
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Orientation="Vertical" SeparationOverride="0">
|
||||
<BoxContainer Name="StatusContents" Orientation="Vertical"/>
|
||||
<Label Name="ItemNameLabel" ClipText="True" StyleClasses="ItemStatus"/>
|
||||
<Label Name="ItemNameLabel" StyleClasses="ItemStatus"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</controls:ItemStatusPanel>
|
||||
|
||||
@@ -46,7 +46,7 @@ public sealed class ZombieSystem : EntitySystem
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnCanDisplayStatusIcons(EntityUid uid, InitialInfectedComponent component, CanDisplayStatusIconsEvent args)
|
||||
private void OnCanDisplayStatusIcons(EntityUid uid, InitialInfectedComponent component, ref CanDisplayStatusIconsEvent args)
|
||||
{
|
||||
if (HasComp<InitialInfectedComponent>(args.User) && !HasComp<ZombieComponent>(args.User))
|
||||
return;
|
||||
|
||||
@@ -57,9 +57,9 @@ namespace Content.IntegrationTests.Tests.Access
|
||||
var reader = new AccessReaderComponent();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "Foo" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "Bar" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<string>(), reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "Foo" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "Bar" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.True);
|
||||
});
|
||||
|
||||
// test deny
|
||||
@@ -67,58 +67,58 @@ namespace Content.IntegrationTests.Tests.Access
|
||||
reader.DenyTags.Add("A");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "Foo" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "A" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "A", "Foo" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<string>(), reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "Foo" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "Foo" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.True);
|
||||
});
|
||||
|
||||
// test one list
|
||||
reader = new AccessReaderComponent();
|
||||
reader.AccessLists.Add(new HashSet<string> { "A" });
|
||||
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "A" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "B" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "A", "B" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<string>(), reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "B" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
|
||||
});
|
||||
|
||||
// test one list - two items
|
||||
reader = new AccessReaderComponent();
|
||||
reader.AccessLists.Add(new HashSet<string> { "A", "B" });
|
||||
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A", "B" });
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "A" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "B" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "A", "B" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<string>(), reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "B" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
|
||||
});
|
||||
|
||||
// test two list
|
||||
reader = new AccessReaderComponent();
|
||||
reader.AccessLists.Add(new HashSet<string> { "A" });
|
||||
reader.AccessLists.Add(new HashSet<string> { "B", "C" });
|
||||
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
|
||||
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "B", "C" });
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "A" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "B" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "A", "B" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "C", "B" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "C", "B", "A" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<string>(), reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "B" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "C", "B" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "C", "B", "A" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
|
||||
});
|
||||
|
||||
// test deny list
|
||||
reader = new AccessReaderComponent();
|
||||
reader.AccessLists.Add(new HashSet<string> { "A" });
|
||||
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
|
||||
reader.DenyTags.Add("B");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "A" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "B" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new[] { "A", "B" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<string>(), reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "B" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.False);
|
||||
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
|
||||
});
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Access;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Administration.Logs;
|
||||
@@ -12,6 +13,7 @@ using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using static Content.Shared.Access.Components.AccessOverriderComponent;
|
||||
|
||||
namespace Content.Server.Access.Systems;
|
||||
@@ -26,6 +28,7 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -108,17 +111,20 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||
var targetLabel = Loc.GetString("access-overrider-window-no-target");
|
||||
var targetLabelColor = Color.Red;
|
||||
|
||||
string[]? possibleAccess = null;
|
||||
string[]? currentAccess = null;
|
||||
string[]? missingAccess = null;
|
||||
ProtoId<AccessLevelPrototype>[]? possibleAccess = null;
|
||||
ProtoId<AccessLevelPrototype>[]? currentAccess = null;
|
||||
ProtoId<AccessLevelPrototype>[]? missingAccess = null;
|
||||
|
||||
if (component.TargetAccessReaderId is { Valid: true } accessReader)
|
||||
{
|
||||
targetLabel = Loc.GetString("access-overrider-window-target-label") + " " + EntityManager.GetComponent<MetaDataComponent>(component.TargetAccessReaderId).EntityName;
|
||||
targetLabelColor = Color.White;
|
||||
|
||||
List<HashSet<string>> currentAccessHashsets = EntityManager.GetComponent<AccessReaderComponent>(accessReader).AccessLists;
|
||||
currentAccess = ConvertAccessHashSetsToList(currentAccessHashsets)?.ToArray();
|
||||
if (!_accessReader.GetMainAccessReader(accessReader, out var accessReaderComponent))
|
||||
return;
|
||||
|
||||
var currentAccessHashsets = accessReaderComponent.AccessLists;
|
||||
currentAccess = ConvertAccessHashSetsToList(currentAccessHashsets).ToArray();
|
||||
}
|
||||
|
||||
if (component.PrivilegedIdSlot.Item is { Valid: true } idCard)
|
||||
@@ -151,15 +157,15 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||
_userInterface.TrySetUiState(uid, AccessOverriderUiKey.Key, newState);
|
||||
}
|
||||
|
||||
private List<string> ConvertAccessHashSetsToList(List<HashSet<string>> accessHashsets)
|
||||
private List<ProtoId<AccessLevelPrototype>> ConvertAccessHashSetsToList(List<HashSet<ProtoId<AccessLevelPrototype>>> accessHashsets)
|
||||
{
|
||||
List<string> accessList = new List<string>();
|
||||
List<ProtoId<AccessLevelPrototype>> accessList = new List<ProtoId<AccessLevelPrototype>>();
|
||||
|
||||
if (accessHashsets != null && accessHashsets.Any())
|
||||
{
|
||||
foreach (HashSet<string> hashSet in accessHashsets)
|
||||
foreach (HashSet<ProtoId<AccessLevelPrototype>> hashSet in accessHashsets)
|
||||
{
|
||||
foreach (string hash in hashSet.ToArray())
|
||||
foreach (ProtoId<AccessLevelPrototype> hash in hashSet.ToArray())
|
||||
{
|
||||
accessList.Add(hash);
|
||||
}
|
||||
@@ -169,15 +175,15 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||
return accessList;
|
||||
}
|
||||
|
||||
private List<HashSet<string>> ConvertAccessListToHashSet(List<string> accessList)
|
||||
private List<HashSet<ProtoId<AccessLevelPrototype>>> ConvertAccessListToHashSet(List<ProtoId<AccessLevelPrototype>> accessList)
|
||||
{
|
||||
List<HashSet<string>> accessHashsets = new List<HashSet<string>>();
|
||||
List<HashSet<ProtoId<AccessLevelPrototype>>> accessHashsets = new List<HashSet<ProtoId<AccessLevelPrototype>>>();
|
||||
|
||||
if (accessList != null && accessList.Any())
|
||||
{
|
||||
foreach (string access in accessList)
|
||||
foreach (ProtoId<AccessLevelPrototype> access in accessList)
|
||||
{
|
||||
accessHashsets.Add(new HashSet<string>() { access });
|
||||
accessHashsets.Add(new HashSet<ProtoId<AccessLevelPrototype>>() { access });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +194,7 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||
/// Called whenever an access button is pressed, adding or removing that access requirement from the target access reader.
|
||||
/// </summary>
|
||||
private void TryWriteToTargetAccessReaderId(EntityUid uid,
|
||||
List<string> newAccessList,
|
||||
List<ProtoId<AccessLevelPrototype>> newAccessList,
|
||||
EntityUid player,
|
||||
AccessOverriderComponent? component = null)
|
||||
{
|
||||
@@ -211,9 +217,7 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||
return;
|
||||
}
|
||||
|
||||
TryComp(component.TargetAccessReaderId, out AccessReaderComponent? accessReader);
|
||||
|
||||
if (accessReader == null)
|
||||
if (!_accessReader.GetMainAccessReader(component.TargetAccessReaderId, out var accessReader))
|
||||
return;
|
||||
|
||||
var oldTags = ConvertAccessHashSetsToList(accessReader.AccessLists);
|
||||
@@ -262,10 +266,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||
if (!Resolve(uid, ref component))
|
||||
return true;
|
||||
|
||||
if (!EntityManager.TryGetComponent<AccessReaderComponent>(uid, out var reader))
|
||||
if (_accessReader.GetMainAccessReader(uid, out var accessReader))
|
||||
return true;
|
||||
|
||||
var privilegedId = component.PrivilegedIdSlot.Item;
|
||||
return privilegedId != null && _accessReader.IsAllowed(privilegedId.Value, uid, reader);
|
||||
return privilegedId != null && _accessReader.IsAllowed(privilegedId.Value, uid, accessReader);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
using static Content.Shared.Access.Components.IdCardConsoleComponent;
|
||||
using Content.Shared.Access;
|
||||
|
||||
namespace Content.Server.Access.Systems;
|
||||
|
||||
@@ -54,11 +55,11 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
return;
|
||||
|
||||
var privilegedIdName = string.Empty;
|
||||
string[]? possibleAccess = null;
|
||||
List<ProtoId<AccessLevelPrototype>>? possibleAccess = null;
|
||||
if (component.PrivilegedIdSlot.Item is { Valid: true } item)
|
||||
{
|
||||
privilegedIdName = EntityManager.GetComponent<MetaDataComponent>(item).EntityName;
|
||||
possibleAccess = _accessReader.FindAccessTags(item).ToArray();
|
||||
possibleAccess = _accessReader.FindAccessTags(item).ToList();
|
||||
}
|
||||
|
||||
IdCardConsoleBoundUserInterfaceState newState;
|
||||
@@ -82,7 +83,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
var targetIdComponent = EntityManager.GetComponent<IdCardComponent>(targetId);
|
||||
var targetAccessComponent = EntityManager.GetComponent<AccessComponent>(targetId);
|
||||
|
||||
var jobProto = string.Empty;
|
||||
var jobProto = new ProtoId<AccessLevelPrototype>(string.Empty);
|
||||
if (TryComp<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
|
||||
&& keyStorage.Key is {} key
|
||||
&& _record.TryGetRecord<GeneralStationRecord>(key, out var record))
|
||||
@@ -96,7 +97,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
true,
|
||||
targetIdComponent.FullName,
|
||||
targetIdComponent.JobTitle,
|
||||
targetAccessComponent.Tags.ToArray(),
|
||||
targetAccessComponent.Tags.ToList(),
|
||||
possibleAccess,
|
||||
jobProto,
|
||||
privilegedIdName,
|
||||
@@ -113,8 +114,8 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
private void TryWriteToTargetId(EntityUid uid,
|
||||
string newFullName,
|
||||
string newJobTitle,
|
||||
List<string> newAccessList,
|
||||
string newJobProto,
|
||||
List<ProtoId<AccessLevelPrototype>> newAccessList,
|
||||
ProtoId<AccessLevelPrototype> newJobProto,
|
||||
EntityUid player,
|
||||
IdCardConsoleComponent? component = null)
|
||||
{
|
||||
@@ -140,7 +141,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
return;
|
||||
}
|
||||
|
||||
var oldTags = _access.TryGetTags(targetId) ?? new List<string>();
|
||||
var oldTags = _access.TryGetTags(targetId) ?? new List<ProtoId<AccessLevelPrototype>>();
|
||||
oldTags = oldTags.ToList();
|
||||
|
||||
var privilegedId = component.PrivilegedIdSlot.Item;
|
||||
@@ -189,7 +190,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
return privilegedId != null && _accessReader.IsAllowed(privilegedId.Value, uid, reader);
|
||||
}
|
||||
|
||||
private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFullName, string newJobTitle, JobPrototype? newJobProto)
|
||||
private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFullName, ProtoId<AccessLevelPrototype> newJobTitle, JobPrototype? newJobProto)
|
||||
{
|
||||
if (!TryComp<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
|
||||
|| keyStorage.Key is not { } key
|
||||
|
||||
@@ -1,72 +1,122 @@
|
||||
using Content.Server.GameTicking;
|
||||
using System.Linq;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Mind;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Mind;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Administration.Commands
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class AGhost : LocalizedCommands
|
||||
{
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class AGhost : IConsoleCommand
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
public override string Command => "aghost";
|
||||
public override string Description => LocalizationManager.GetString("aghost-description");
|
||||
public override string Help => "aghost";
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
|
||||
public string Command => "aghost";
|
||||
public string Description => "Makes you an admin ghost.";
|
||||
public string Help => "aghost";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
if (args.Length == 1)
|
||||
{
|
||||
var player = shell.Player;
|
||||
if (player == null)
|
||||
{
|
||||
shell.WriteLine("Nah");
|
||||
return;
|
||||
}
|
||||
|
||||
var mindSystem = _entities.System<SharedMindSystem>();
|
||||
if (!mindSystem.TryGetMind(player, out var mindId, out var mind))
|
||||
{
|
||||
shell.WriteLine("You can't ghost here!");
|
||||
return;
|
||||
}
|
||||
|
||||
var metaDataSystem = _entities.System<MetaDataSystem>();
|
||||
|
||||
if (mind.VisitingEntity != default && _entities.TryGetComponent<GhostComponent>(mind.VisitingEntity, out var oldGhostComponent))
|
||||
{
|
||||
mindSystem.UnVisit(mindId, mind);
|
||||
// If already an admin ghost, then return to body.
|
||||
if (oldGhostComponent.CanGhostInteract)
|
||||
return;
|
||||
}
|
||||
|
||||
var canReturn = mind.CurrentEntity != null
|
||||
&& !_entities.HasComponent<GhostComponent>(mind.CurrentEntity);
|
||||
var coordinates = player.AttachedEntity != null
|
||||
? _entities.GetComponent<TransformComponent>(player.AttachedEntity.Value).Coordinates
|
||||
: EntitySystem.Get<GameTicker>().GetObserverSpawnPoint();
|
||||
var ghost = _entities.SpawnEntity(GameTicker.AdminObserverPrototypeName, coordinates);
|
||||
_entities.GetComponent<TransformComponent>(ghost).AttachToGridOrMap();
|
||||
|
||||
if (canReturn)
|
||||
{
|
||||
// TODO: Remove duplication between all this and "GamePreset.OnGhostAttempt()"...
|
||||
if (!string.IsNullOrWhiteSpace(mind.CharacterName))
|
||||
metaDataSystem.SetEntityName(ghost, mind.CharacterName);
|
||||
else if (!string.IsNullOrWhiteSpace(mind.Session?.Name))
|
||||
metaDataSystem.SetEntityName(ghost, mind.Session.Name);
|
||||
|
||||
mindSystem.Visit(mindId, ghost, mind);
|
||||
}
|
||||
else
|
||||
{
|
||||
metaDataSystem.SetEntityName(ghost, player.Name);
|
||||
mindSystem.TransferTo(mindId, ghost, mind: mind);
|
||||
}
|
||||
|
||||
var comp = _entities.GetComponent<GhostComponent>(ghost);
|
||||
EntitySystem.Get<SharedGhostSystem>().SetCanReturnToBody(comp, canReturn);
|
||||
var names = _playerManager.Sessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray();
|
||||
return CompletionResult.FromHintOptions(names, LocalizationManager.GetString("shell-argument-username-optional-hint"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length > 1)
|
||||
{
|
||||
shell.WriteError(LocalizationManager.GetString("shell-wrong-arguments-number"));
|
||||
return;
|
||||
}
|
||||
|
||||
var player = shell.Player;
|
||||
var self = player != null;
|
||||
if (player == null)
|
||||
{
|
||||
// If you are not a player, you require a player argument.
|
||||
if (args.Length == 0)
|
||||
{
|
||||
shell.WriteError(LocalizationManager.GetString("shell-need-exactly-one-argument"));
|
||||
return;
|
||||
}
|
||||
|
||||
var didFind = _playerManager.TryGetSessionByUsername(args[0], out player);
|
||||
if (!didFind)
|
||||
{
|
||||
shell.WriteError(LocalizationManager.GetString("shell-target-player-does-not-exist"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If you are a player and a username is provided, a lookup is done to find the target player.
|
||||
if (args.Length == 1)
|
||||
{
|
||||
var didFind = _playerManager.TryGetSessionByUsername(args[0], out player);
|
||||
if (!didFind)
|
||||
{
|
||||
shell.WriteError(LocalizationManager.GetString("shell-target-player-does-not-exist"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var mindSystem = _entities.System<SharedMindSystem>();
|
||||
var metaDataSystem = _entities.System<MetaDataSystem>();
|
||||
var ghostSystem = _entities.System<SharedGhostSystem>();
|
||||
var transformSystem = _entities.System<TransformSystem>();
|
||||
var gameTicker = _entities.System<GameTicker>();
|
||||
|
||||
if (!mindSystem.TryGetMind(player, out var mindId, out var mind))
|
||||
{
|
||||
shell.WriteError(self
|
||||
? LocalizationManager.GetString("aghost-no-mind-self")
|
||||
: LocalizationManager.GetString("aghost-no-mind-other"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (mind.VisitingEntity != default && _entities.TryGetComponent<GhostComponent>(mind.VisitingEntity, out var oldGhostComponent))
|
||||
{
|
||||
mindSystem.UnVisit(mindId, mind);
|
||||
// If already an admin ghost, then return to body.
|
||||
if (oldGhostComponent.CanGhostInteract)
|
||||
return;
|
||||
}
|
||||
|
||||
var canReturn = mind.CurrentEntity != null
|
||||
&& !_entities.HasComponent<GhostComponent>(mind.CurrentEntity);
|
||||
var coordinates = player!.AttachedEntity != null
|
||||
? _entities.GetComponent<TransformComponent>(player.AttachedEntity.Value).Coordinates
|
||||
: gameTicker.GetObserverSpawnPoint();
|
||||
var ghost = _entities.SpawnEntity(GameTicker.AdminObserverPrototypeName, coordinates);
|
||||
transformSystem.AttachToGridOrMap(ghost, _entities.GetComponent<TransformComponent>(ghost));
|
||||
|
||||
if (canReturn)
|
||||
{
|
||||
// TODO: Remove duplication between all this and "GamePreset.OnGhostAttempt()"...
|
||||
if (!string.IsNullOrWhiteSpace(mind.CharacterName))
|
||||
metaDataSystem.SetEntityName(ghost, mind.CharacterName);
|
||||
else if (!string.IsNullOrWhiteSpace(mind.Session?.Name))
|
||||
metaDataSystem.SetEntityName(ghost, mind.Session.Name);
|
||||
|
||||
mindSystem.Visit(mindId, ghost, mind);
|
||||
}
|
||||
else
|
||||
{
|
||||
metaDataSystem.SetEntityName(ghost, player.Name);
|
||||
mindSystem.TransferTo(mindId, ghost, mind: mind);
|
||||
}
|
||||
|
||||
var comp = _entities.GetComponent<GhostComponent>(ghost);
|
||||
ghostSystem.SetCanReturnToBody(comp, canReturn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Administration.Systems;
|
||||
@@ -844,14 +845,14 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
var allAccess = _prototypeManager
|
||||
.EnumeratePrototypes<AccessLevelPrototype>()
|
||||
.Select(p => p.ID).ToArray();
|
||||
.Select(p => new ProtoId<AccessLevelPrototype>(p.ID)).ToArray();
|
||||
|
||||
_accessSystem.TrySetTags(entity, allAccess);
|
||||
}
|
||||
|
||||
private void RevokeAllAccess(EntityUid entity)
|
||||
{
|
||||
_accessSystem.TrySetTags(entity, Array.Empty<string>());
|
||||
_accessSystem.TrySetTags(entity, new List<ProtoId<AccessLevelPrototype>>());
|
||||
}
|
||||
|
||||
public enum TricksVerbPriorities
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Server.Advertise.EntitySystems;
|
||||
using Content.Shared.Advertise;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Advertise.Components;
|
||||
|
||||
@@ -37,9 +36,4 @@ public sealed partial class AdvertiseComponent : Component
|
||||
[DataField]
|
||||
public TimeSpan NextAdvertisementTime { get; set; } = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the entity will say advertisements or not.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -23,115 +23,83 @@ public sealed class AdvertiseSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// The next time the game will check if advertisements should be displayed
|
||||
/// </summary>
|
||||
private TimeSpan _nextCheckTime = TimeSpan.MaxValue;
|
||||
private TimeSpan _nextCheckTime = TimeSpan.MinValue;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<AdvertiseComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<AdvertiseComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
|
||||
SubscribeLocalEvent<ApcPowerReceiverComponent, AdvertiseEnableChangeAttemptEvent>(OnPowerReceiverEnableChangeAttempt);
|
||||
SubscribeLocalEvent<VendingMachineComponent, AdvertiseEnableChangeAttemptEvent>(OnVendingEnableChangeAttempt);
|
||||
SubscribeLocalEvent<ApcPowerReceiverComponent, AttemptAdvertiseEvent>(OnPowerReceiverAttemptAdvertiseEvent);
|
||||
SubscribeLocalEvent<VendingMachineComponent, AttemptAdvertiseEvent>(OnVendingAttemptAdvertiseEvent);
|
||||
|
||||
// The component inits will lower this.
|
||||
_nextCheckTime = TimeSpan.MaxValue;
|
||||
_nextCheckTime = TimeSpan.MinValue;
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, AdvertiseComponent advertise, MapInitEvent args)
|
||||
private void OnMapInit(EntityUid uid, AdvertiseComponent advert, MapInitEvent args)
|
||||
{
|
||||
RefreshTimer(uid, advertise);
|
||||
RandomizeNextAdvertTime(advert);
|
||||
_nextCheckTime = MathHelper.Min(advert.NextAdvertisementTime, _nextCheckTime);
|
||||
}
|
||||
|
||||
private void OnPowerChanged(EntityUid uid, AdvertiseComponent advertise, ref PowerChangedEvent args)
|
||||
private void RandomizeNextAdvertTime(AdvertiseComponent advert)
|
||||
{
|
||||
SetEnabled(uid, args.Powered, advertise);
|
||||
}
|
||||
|
||||
public void RefreshTimer(EntityUid uid, AdvertiseComponent? advertise = null)
|
||||
{
|
||||
if (!Resolve(uid, ref advertise))
|
||||
return;
|
||||
|
||||
if (!advertise.Enabled)
|
||||
return;
|
||||
|
||||
var minDuration = Math.Max(1, advertise.MinimumWait);
|
||||
var maxDuration = Math.Max(minDuration, advertise.MaximumWait);
|
||||
var minDuration = Math.Max(1, advert.MinimumWait);
|
||||
var maxDuration = Math.Max(minDuration, advert.MaximumWait);
|
||||
var waitDuration = TimeSpan.FromSeconds(_random.Next(minDuration, maxDuration));
|
||||
var nextTime = _gameTiming.CurTime + waitDuration;
|
||||
|
||||
advertise.NextAdvertisementTime = nextTime;
|
||||
|
||||
_nextCheckTime = MathHelper.Min(nextTime, _nextCheckTime);
|
||||
advert.NextAdvertisementTime = _gameTiming.CurTime + waitDuration;
|
||||
}
|
||||
|
||||
public void SayAdvertisement(EntityUid uid, AdvertiseComponent? advertise = null)
|
||||
public void SayAdvertisement(EntityUid uid, AdvertiseComponent? advert = null)
|
||||
{
|
||||
if (!Resolve(uid, ref advertise))
|
||||
if (!Resolve(uid, ref advert))
|
||||
return;
|
||||
|
||||
if (_prototypeManager.TryIndex(advertise.Pack, out var advertisements))
|
||||
_chat.TrySendInGameICMessage(uid, Loc.GetString(_random.Pick(advertisements.Messages)), InGameICChatType.Speak, hideChat: true);
|
||||
}
|
||||
|
||||
public void SetEnabled(EntityUid uid, bool enable, AdvertiseComponent? advertise = null)
|
||||
{
|
||||
if (!Resolve(uid, ref advertise))
|
||||
return;
|
||||
|
||||
if (advertise.Enabled == enable)
|
||||
return;
|
||||
|
||||
var attemptEvent = new AdvertiseEnableChangeAttemptEvent(enable);
|
||||
RaiseLocalEvent(uid, attemptEvent);
|
||||
|
||||
var attemptEvent = new AttemptAdvertiseEvent(uid);
|
||||
RaiseLocalEvent(uid, ref attemptEvent);
|
||||
if (attemptEvent.Cancelled)
|
||||
return;
|
||||
|
||||
advertise.Enabled = enable;
|
||||
RefreshTimer(uid, advertise);
|
||||
}
|
||||
|
||||
private static void OnPowerReceiverEnableChangeAttempt(EntityUid uid, ApcPowerReceiverComponent component, AdvertiseEnableChangeAttemptEvent args)
|
||||
{
|
||||
if (args.Enabling && !component.Powered)
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private static void OnVendingEnableChangeAttempt(EntityUid uid, VendingMachineComponent component, AdvertiseEnableChangeAttemptEvent args)
|
||||
{
|
||||
if (args.Enabling && component.Broken)
|
||||
args.Cancel();
|
||||
if (_prototypeManager.TryIndex(advert.Pack, out var advertisements))
|
||||
_chat.TrySendInGameICMessage(uid, Loc.GetString(_random.Pick(advertisements.Messages)), InGameICChatType.Speak, hideChat: true);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
var curTime = _gameTiming.CurTime;
|
||||
if (_nextCheckTime > curTime)
|
||||
var currentGameTime = _gameTiming.CurTime;
|
||||
if (_nextCheckTime > currentGameTime)
|
||||
return;
|
||||
|
||||
_nextCheckTime = curTime + _maximumNextCheckDuration;
|
||||
// _nextCheckTime starts at TimeSpan.MinValue, so this has to SET the value, not just increment it.
|
||||
_nextCheckTime = currentGameTime + _maximumNextCheckDuration;
|
||||
|
||||
var query = EntityQueryEnumerator<AdvertiseComponent>();
|
||||
while (query.MoveNext(out var uid, out var advert))
|
||||
{
|
||||
if (!advert.Enabled)
|
||||
continue;
|
||||
|
||||
// If this isn't advertising yet
|
||||
if (advert.NextAdvertisementTime > curTime)
|
||||
if (currentGameTime > advert.NextAdvertisementTime)
|
||||
{
|
||||
_nextCheckTime = MathHelper.Min(advert.NextAdvertisementTime, _nextCheckTime);
|
||||
continue;
|
||||
SayAdvertisement(uid, advert);
|
||||
// The timer is always refreshed when it expires, to prevent mass advertising (ex: all the vending machines have no power, and get it back at the same time).
|
||||
RandomizeNextAdvertTime(advert);
|
||||
}
|
||||
|
||||
SayAdvertisement(uid, advert);
|
||||
RefreshTimer(uid, advert);
|
||||
_nextCheckTime = MathHelper.Min(advert.NextAdvertisementTime, _nextCheckTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void OnPowerReceiverAttemptAdvertiseEvent(EntityUid uid, ApcPowerReceiverComponent powerReceiver, ref AttemptAdvertiseEvent args)
|
||||
{
|
||||
args.Cancelled |= !powerReceiver.Powered;
|
||||
}
|
||||
|
||||
private static void OnVendingAttemptAdvertiseEvent(EntityUid uid, VendingMachineComponent machine, ref AttemptAdvertiseEvent args)
|
||||
{
|
||||
args.Cancelled |= machine.Broken;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AdvertiseEnableChangeAttemptEvent(bool enabling) : CancellableEntityEventArgs
|
||||
[ByRefEvent]
|
||||
public record struct AttemptAdvertiseEvent(EntityUid? Advertiser)
|
||||
{
|
||||
public bool Enabling { get; } = enabling;
|
||||
public bool Cancelled = false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Anomaly.Components;
|
||||
using Content.Server.Anomaly.Components;
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.DoAfter;
|
||||
@@ -21,6 +21,7 @@ public sealed partial class AnomalySystem
|
||||
|
||||
SubscribeLocalEvent<AnomalySeverityChangedEvent>(OnScannerAnomalySeverityChanged);
|
||||
SubscribeLocalEvent<AnomalyHealthChangedEvent>(OnScannerAnomalyHealthChanged);
|
||||
SubscribeLocalEvent<AnomalyBehaviorChangedEvent>(OnScannerAnomalyBehaviorChanged);
|
||||
}
|
||||
|
||||
private void OnScannerAnomalyShutdown(ref AnomalyShutdownEvent args)
|
||||
@@ -67,6 +68,17 @@ public sealed partial class AnomalySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScannerAnomalyBehaviorChanged(ref AnomalyBehaviorChangedEvent args)
|
||||
{
|
||||
var query = EntityQueryEnumerator<AnomalyScannerComponent>();
|
||||
while (query.MoveNext(out var uid, out var component))
|
||||
{
|
||||
if (component.ScannedAnomaly != args.Anomaly)
|
||||
continue;
|
||||
UpdateScannerUi(uid, component);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScannerUiOpened(EntityUid uid, AnomalyScannerComponent component, BoundUIOpenedEvent args)
|
||||
{
|
||||
UpdateScannerUi(uid, component);
|
||||
@@ -132,29 +144,95 @@ public sealed partial class AnomalySystem
|
||||
return msg;
|
||||
}
|
||||
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-severity-percentage", ("percent", anomalyComp.Severity.ToString("P"))));
|
||||
msg.PushNewline();
|
||||
string stateLoc;
|
||||
if (anomalyComp.Stability < anomalyComp.DecayThreshold)
|
||||
stateLoc = Loc.GetString("anomaly-scanner-stability-low");
|
||||
else if (anomalyComp.Stability > anomalyComp.GrowthThreshold)
|
||||
stateLoc = Loc.GetString("anomaly-scanner-stability-high");
|
||||
TryComp<SecretDataAnomalyComponent>(anomaly, out var secret);
|
||||
|
||||
//Severity
|
||||
if (secret != null && secret.Secret.Contains(AnomalySecretData.Severity))
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-severity-percentage-unknown"));
|
||||
else
|
||||
stateLoc = Loc.GetString("anomaly-scanner-stability-medium");
|
||||
msg.AddMarkup(stateLoc);
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-severity-percentage", ("percent", anomalyComp.Severity.ToString("P"))));
|
||||
msg.PushNewline();
|
||||
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-point-output", ("point", GetAnomalyPointValue(anomaly, anomalyComp))));
|
||||
//Stability
|
||||
if (secret != null && secret.Secret.Contains(AnomalySecretData.Stability))
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-stability-unknown"));
|
||||
else
|
||||
{
|
||||
string stateLoc;
|
||||
if (anomalyComp.Stability < anomalyComp.DecayThreshold)
|
||||
stateLoc = Loc.GetString("anomaly-scanner-stability-low");
|
||||
else if (anomalyComp.Stability > anomalyComp.GrowthThreshold)
|
||||
stateLoc = Loc.GetString("anomaly-scanner-stability-high");
|
||||
else
|
||||
stateLoc = Loc.GetString("anomaly-scanner-stability-medium");
|
||||
msg.AddMarkup(stateLoc);
|
||||
}
|
||||
msg.PushNewline();
|
||||
|
||||
//Point output
|
||||
if (secret != null && secret.Secret.Contains(AnomalySecretData.OutputPoint))
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-point-output-unknown"));
|
||||
else
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-point-output", ("point", GetAnomalyPointValue(anomaly, anomalyComp))));
|
||||
msg.PushNewline();
|
||||
msg.PushNewline();
|
||||
|
||||
//Particles title
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-readout"));
|
||||
msg.PushNewline();
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-danger", ("type", GetParticleLocale(anomalyComp.SeverityParticleType))));
|
||||
|
||||
//Danger
|
||||
if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleDanger))
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-danger-unknown"));
|
||||
else
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-danger", ("type", GetParticleLocale(anomalyComp.SeverityParticleType))));
|
||||
msg.PushNewline();
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-unstable", ("type", GetParticleLocale(anomalyComp.DestabilizingParticleType))));
|
||||
|
||||
//Unstable
|
||||
if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleUnstable))
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-unstable-unknown"));
|
||||
else
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-unstable", ("type", GetParticleLocale(anomalyComp.DestabilizingParticleType))));
|
||||
msg.PushNewline();
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-containment", ("type", GetParticleLocale(anomalyComp.WeakeningParticleType))));
|
||||
|
||||
//Containment
|
||||
if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleContainment))
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-containment-unknown"));
|
||||
else
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-containment", ("type", GetParticleLocale(anomalyComp.WeakeningParticleType))));
|
||||
msg.PushNewline();
|
||||
|
||||
//Transformation
|
||||
if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleTransformation))
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-transformation-unknown"));
|
||||
else
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-transformation", ("type", GetParticleLocale(anomalyComp.TransformationParticleType))));
|
||||
|
||||
|
||||
//Behavior
|
||||
msg.PushNewline();
|
||||
msg.PushNewline();
|
||||
msg.AddMarkup(Loc.GetString("anomaly-behavior-title"));
|
||||
msg.PushNewline();
|
||||
|
||||
if (secret != null && secret.Secret.Contains(AnomalySecretData.Behavior))
|
||||
msg.AddMarkup(Loc.GetString("anomaly-behavior-unknown"));
|
||||
else
|
||||
{
|
||||
if (anomalyComp.CurrentBehavior != null)
|
||||
{
|
||||
var behavior = _prototype.Index(anomalyComp.CurrentBehavior.Value);
|
||||
|
||||
msg.AddMarkup("- " + Loc.GetString(behavior.Description));
|
||||
msg.PushNewline();
|
||||
var mod = Math.Floor((behavior.EarnPointModifier) * 100);
|
||||
msg.AddMarkup("- " + Loc.GetString("anomaly-behavior-point", ("mod", mod)));
|
||||
}
|
||||
else
|
||||
{
|
||||
msg.AddMarkup(Loc.GetString("anomaly-behavior-balanced"));
|
||||
}
|
||||
}
|
||||
|
||||
//The timer at the end here is actually added in the ui itself.
|
||||
return msg;
|
||||
|
||||
@@ -8,13 +8,18 @@ using Content.Server.Radio.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Anomaly.Prototypes;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Anomaly;
|
||||
|
||||
@@ -33,13 +38,20 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
|
||||
[Dependency] private readonly SharedPointLightSystem _pointLight = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly RadioSystem _radio = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly RadiationSystem _radiation = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||
[Dependency] private readonly ISerializationManager _serialization = default!;
|
||||
[Dependency] private readonly IEntityManager _entity = default!;
|
||||
|
||||
public const float MinParticleVariation = 0.8f;
|
||||
public const float MaxParticleVariation = 1.2f;
|
||||
|
||||
[ValidatePrototypeId<WeightedRandomPrototype>]
|
||||
const string WeightListProto = "AnomalyBehaviorList";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -54,25 +66,34 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
|
||||
InitializeCommands();
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, AnomalyComponent component, MapInitEvent args)
|
||||
private void OnMapInit(Entity<AnomalyComponent> anomaly, ref MapInitEvent args)
|
||||
{
|
||||
component.NextPulseTime = Timing.CurTime + GetPulseLength(component) * 3; // longer the first time
|
||||
ChangeAnomalyStability(uid, Random.NextFloat(component.InitialStabilityRange.Item1 , component.InitialStabilityRange.Item2), component);
|
||||
ChangeAnomalySeverity(uid, Random.NextFloat(component.InitialSeverityRange.Item1, component.InitialSeverityRange.Item2), component);
|
||||
anomaly.Comp.NextPulseTime = Timing.CurTime + GetPulseLength(anomaly.Comp) * 3; // longer the first time
|
||||
ChangeAnomalyStability(anomaly, Random.NextFloat(anomaly.Comp.InitialStabilityRange.Item1 , anomaly.Comp.InitialStabilityRange.Item2), anomaly.Comp);
|
||||
ChangeAnomalySeverity(anomaly, Random.NextFloat(anomaly.Comp.InitialSeverityRange.Item1, anomaly.Comp.InitialSeverityRange.Item2), anomaly.Comp);
|
||||
|
||||
ShuffleParticlesEffect(anomaly.Comp);
|
||||
anomaly.Comp.Continuity = _random.NextFloat(anomaly.Comp.MinContituty, anomaly.Comp.MaxContituty);
|
||||
SetBehavior(anomaly, GetRandomBehavior());
|
||||
}
|
||||
|
||||
public void ShuffleParticlesEffect(AnomalyComponent anomaly)
|
||||
{
|
||||
var particles = new List<AnomalousParticleType>
|
||||
{ AnomalousParticleType.Delta, AnomalousParticleType.Epsilon, AnomalousParticleType.Zeta };
|
||||
component.SeverityParticleType = Random.PickAndTake(particles);
|
||||
component.DestabilizingParticleType = Random.PickAndTake(particles);
|
||||
component.WeakeningParticleType = Random.PickAndTake(particles);
|
||||
{ AnomalousParticleType.Delta, AnomalousParticleType.Epsilon, AnomalousParticleType.Zeta, AnomalousParticleType.Sigma };
|
||||
|
||||
anomaly.SeverityParticleType = Random.PickAndTake(particles);
|
||||
anomaly.DestabilizingParticleType = Random.PickAndTake(particles);
|
||||
anomaly.WeakeningParticleType = Random.PickAndTake(particles);
|
||||
anomaly.TransformationParticleType = Random.PickAndTake(particles);
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, AnomalyComponent component, ComponentShutdown args)
|
||||
private void OnShutdown(Entity<AnomalyComponent> anomaly, ref ComponentShutdown args)
|
||||
{
|
||||
EndAnomaly(uid, component);
|
||||
EndAnomaly(anomaly);
|
||||
}
|
||||
|
||||
private void OnStartCollide(EntityUid uid, AnomalyComponent component, ref StartCollideEvent args)
|
||||
private void OnStartCollide(Entity<AnomalyComponent> anomaly, ref StartCollideEvent args)
|
||||
{
|
||||
if (!TryComp<AnomalousParticleComponent>(args.OtherEntity, out var particle))
|
||||
return;
|
||||
@@ -80,21 +101,33 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
|
||||
if (args.OtherFixtureId != particle.FixtureId)
|
||||
return;
|
||||
|
||||
var behaviorMod = 1f;
|
||||
if (anomaly.Comp.CurrentBehavior != null)
|
||||
{
|
||||
var b = _prototype.Index(anomaly.Comp.CurrentBehavior.Value);
|
||||
behaviorMod = b.ParticleSensivity;
|
||||
}
|
||||
// small function to randomize because it's easier to read like this
|
||||
float VaryValue(float v) => v * Random.NextFloat(MinParticleVariation, MaxParticleVariation);
|
||||
float VaryValue(float v) => v * behaviorMod * Random.NextFloat(MinParticleVariation, MaxParticleVariation);
|
||||
|
||||
if (particle.ParticleType == component.DestabilizingParticleType || particle.DestabilzingOverride)
|
||||
if (particle.ParticleType == anomaly.Comp.DestabilizingParticleType || particle.DestabilzingOverride)
|
||||
{
|
||||
ChangeAnomalyStability(uid, VaryValue(particle.StabilityPerDestabilizingHit), component);
|
||||
ChangeAnomalyStability(anomaly, VaryValue(particle.StabilityPerDestabilizingHit), anomaly.Comp);
|
||||
}
|
||||
if (particle.ParticleType == component.SeverityParticleType || particle.SeverityOverride)
|
||||
if (particle.ParticleType == anomaly.Comp.SeverityParticleType || particle.SeverityOverride)
|
||||
{
|
||||
ChangeAnomalySeverity(uid, VaryValue(particle.SeverityPerSeverityHit), component);
|
||||
ChangeAnomalySeverity(anomaly, VaryValue(particle.SeverityPerSeverityHit), anomaly.Comp);
|
||||
}
|
||||
if (particle.ParticleType == component.WeakeningParticleType || particle.WeakeningOverride)
|
||||
if (particle.ParticleType == anomaly.Comp.WeakeningParticleType || particle.WeakeningOverride)
|
||||
{
|
||||
ChangeAnomalyHealth(uid, VaryValue(particle.HealthPerWeakeningeHit), component);
|
||||
ChangeAnomalyStability(uid, VaryValue(particle.StabilityPerWeakeningeHit), component);
|
||||
ChangeAnomalyHealth(anomaly, VaryValue(particle.HealthPerWeakeningeHit), anomaly.Comp);
|
||||
ChangeAnomalyStability(anomaly, VaryValue(particle.StabilityPerWeakeningeHit), anomaly.Comp);
|
||||
}
|
||||
if (particle.ParticleType == anomaly.Comp.TransformationParticleType || particle.TransmutationOverride)
|
||||
{
|
||||
ChangeAnomalySeverity(anomaly, VaryValue(particle.SeverityPerSeverityHit), anomaly.Comp);
|
||||
if (_random.Prob(anomaly.Comp.Continuity))
|
||||
SetBehavior(anomaly, GetRandomBehavior());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +149,13 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
|
||||
//penalty of up to 50% based on health
|
||||
multiplier *= MathF.Pow(1.5f, component.Health) - 0.5f;
|
||||
|
||||
//Apply behavior modifier
|
||||
if (component.CurrentBehavior != null)
|
||||
{
|
||||
var behavior = _prototype.Index(component.CurrentBehavior.Value);
|
||||
multiplier *= behavior.EarnPointModifier;
|
||||
}
|
||||
|
||||
var severityValue = 1 / (1 + MathF.Pow(MathF.E, -7 * (component.Severity - 0.5f)));
|
||||
|
||||
return (int) ((component.MaxPointsPerSecond - component.MinPointsPerSecond) * severityValue * multiplier) + component.MinPointsPerSecond;
|
||||
@@ -133,6 +173,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
|
||||
AnomalousParticleType.Delta => Loc.GetString("anomaly-particles-delta"),
|
||||
AnomalousParticleType.Epsilon => Loc.GetString("anomaly-particles-epsilon"),
|
||||
AnomalousParticleType.Zeta => Loc.GetString("anomaly-particles-zeta"),
|
||||
AnomalousParticleType.Sigma => Loc.GetString("anomaly-particles-sigma"),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
@@ -144,4 +185,40 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
|
||||
UpdateGenerator();
|
||||
UpdateVessels();
|
||||
}
|
||||
|
||||
#region Behavior
|
||||
private string GetRandomBehavior()
|
||||
{
|
||||
var weightList = _prototype.Index<WeightedRandomPrototype>(WeightListProto);
|
||||
return weightList.Pick(_random);
|
||||
}
|
||||
|
||||
private void SetBehavior(Entity<AnomalyComponent> anomaly, ProtoId<AnomalyBehaviorPrototype> behaviorProto)
|
||||
{
|
||||
if (anomaly.Comp.CurrentBehavior == behaviorProto)
|
||||
return;
|
||||
|
||||
if (anomaly.Comp.CurrentBehavior != null)
|
||||
RemoveBehavior(anomaly, anomaly.Comp.CurrentBehavior.Value);
|
||||
|
||||
//event broadcast
|
||||
var ev = new AnomalyBehaviorChangedEvent(anomaly, anomaly.Comp.CurrentBehavior, behaviorProto);
|
||||
anomaly.Comp.CurrentBehavior = behaviorProto;
|
||||
RaiseLocalEvent(anomaly, ref ev, true);
|
||||
|
||||
var behavior = _prototype.Index(behaviorProto);
|
||||
|
||||
EntityManager.AddComponents(anomaly, behavior.Components);
|
||||
}
|
||||
|
||||
private void RemoveBehavior(Entity<AnomalyComponent> anomaly, ProtoId<AnomalyBehaviorPrototype> behaviorProto)
|
||||
{
|
||||
if (anomaly.Comp.CurrentBehavior == null)
|
||||
return;
|
||||
|
||||
var behavior = _prototype.Index(anomaly.Comp.CurrentBehavior.Value);
|
||||
|
||||
EntityManager.RemoveComponents(anomaly, behavior.Components);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -13,58 +13,64 @@ public sealed partial class AnomalousParticleComponent : Component
|
||||
/// The type of particle that the projectile
|
||||
/// imbues onto the anomaly on contact.
|
||||
/// </summary>
|
||||
[DataField("particleType", required: true)]
|
||||
[DataField(required: true)]
|
||||
public AnomalousParticleType ParticleType;
|
||||
|
||||
/// <summary>
|
||||
/// The fixture that's checked on collision.
|
||||
/// </summary>
|
||||
[DataField("fixtureId")]
|
||||
[DataField]
|
||||
public string FixtureId = "projectile";
|
||||
|
||||
/// <summary>
|
||||
/// The amount that the <see cref="AnomalyComponent.Severity"/> increases by when hit
|
||||
/// of an anomalous particle of <seealso cref="AnomalyComponent.SeverityParticleType"/>.
|
||||
/// </summary>
|
||||
[DataField("severityPerSeverityHit")]
|
||||
[DataField]
|
||||
public float SeverityPerSeverityHit = 0.025f;
|
||||
|
||||
/// <summary>
|
||||
/// The amount that the <see cref="AnomalyComponent.Stability"/> increases by when hit
|
||||
/// of an anomalous particle of <seealso cref="AnomalyComponent.DestabilizingParticleType"/>.
|
||||
/// </summary>
|
||||
[DataField("stabilityPerDestabilizingHit")]
|
||||
[DataField]
|
||||
public float StabilityPerDestabilizingHit = 0.04f;
|
||||
|
||||
/// <summary>
|
||||
/// The amount that the <see cref="AnomalyComponent.Stability"/> increases by when hit
|
||||
/// of an anomalous particle of <seealso cref="AnomalyComponent.DestabilizingParticleType"/>.
|
||||
/// </summary>
|
||||
[DataField("healthPerWeakeningeHit")]
|
||||
[DataField]
|
||||
public float HealthPerWeakeningeHit = -0.05f;
|
||||
|
||||
/// <summary>
|
||||
/// The amount that the <see cref="AnomalyComponent.Stability"/> increases by when hit
|
||||
/// of an anomalous particle of <seealso cref="AnomalyComponent.DestabilizingParticleType"/>.
|
||||
/// </summary>
|
||||
[DataField("stabilityPerWeakeningeHit")]
|
||||
[DataField]
|
||||
public float StabilityPerWeakeningeHit = -0.1f;
|
||||
|
||||
/// <summary>
|
||||
/// If this is true then the particle will always affect the stability of the anomaly.
|
||||
/// </summary>
|
||||
[DataField("destabilzingOverride")]
|
||||
[DataField]
|
||||
public bool DestabilzingOverride = false;
|
||||
|
||||
/// <summary>
|
||||
/// If this is true then the particle will always affect the weakeness of the anomaly.
|
||||
/// </summary>
|
||||
[DataField("weakeningOverride")]
|
||||
[DataField]
|
||||
public bool WeakeningOverride = false;
|
||||
|
||||
/// <summary>
|
||||
/// If this is true then the particle will always affect the severity of the anomaly.
|
||||
/// </summary>
|
||||
[DataField("severityOverride")]
|
||||
[DataField]
|
||||
public bool SeverityOverride = false;
|
||||
|
||||
/// <summary>
|
||||
/// If this is true then the particle will always affect the behaviour.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool TransmutationOverride = false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using Content.Server.Anomaly.Effects;
|
||||
|
||||
namespace Content.Server.Anomaly.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Hides some information about the anomaly when scanning it
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(SecretDataAnomalySystem), typeof(AnomalySystem))]
|
||||
public sealed partial class SecretDataAnomalyComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum hidden data elements on MapInit
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int RandomStartSecretMin = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum hidden data elements on MapInit
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int RandomStartSecretMax = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Current secret data
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<AnomalySecretData> Secret = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enum with secret data field variants
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public enum AnomalySecretData : byte
|
||||
{
|
||||
Severity,
|
||||
Stability,
|
||||
OutputPoint,
|
||||
ParticleDanger,
|
||||
ParticleUnstable,
|
||||
ParticleContainment,
|
||||
ParticleTransformation,
|
||||
Behavior,
|
||||
Default
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Content.Server.Anomaly.Effects;
|
||||
|
||||
namespace Content.Server.Anomaly.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Shuffle Particle types in some situations
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(ShuffleParticlesAnomalySystem))]
|
||||
public sealed partial class ShuffleParticlesAnomalyComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Prob() chance to randomize particle types after Anomaly pulation
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool ShuffleOnPulse = false;
|
||||
|
||||
/// <summary>
|
||||
/// Prob() chance to randomize particle types after APE or CHIMP projectile
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool ShuffleOnParticleHit = false;
|
||||
|
||||
/// <summary>
|
||||
/// Chance to random particles
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Prob = 0.5f;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Server.Anomaly.Components;
|
||||
using Content.Shared.Administration.Logs;
|
||||
@@ -33,7 +33,7 @@ public sealed class BluespaceAnomalySystem : EntitySystem
|
||||
{
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var xform = xformQuery.GetComponent(uid);
|
||||
var range = component.MaxShuffleRadius * args.Severity;
|
||||
var range = component.MaxShuffleRadius * args.Severity * args.PowerModifier;
|
||||
var mobs = new HashSet<Entity<MobStateComponent>>();
|
||||
_lookup.GetEntitiesInRange(xform.Coordinates, range, mobs);
|
||||
var allEnts = new ValueList<EntityUid>(mobs.Select(m => m.Owner)) { uid };
|
||||
@@ -56,7 +56,7 @@ public sealed class BluespaceAnomalySystem : EntitySystem
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
var mapPos = _xform.GetWorldPosition(xform);
|
||||
var radius = component.SupercriticalTeleportRadius;
|
||||
var radius = component.SupercriticalTeleportRadius * args.PowerModifier;
|
||||
var gridBounds = new Box2(mapPos - new Vector2(radius, radius), mapPos + new Vector2(radius, radius));
|
||||
var mobs = new HashSet<Entity<MobStateComponent>>();
|
||||
_lookup.GetEntitiesInRange(xform.Coordinates, component.MaxShuffleRadius, mobs);
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class ElectricityAnomalySystem : EntitySystem
|
||||
|
||||
private void OnPulse(Entity<ElectricityAnomalyComponent> anomaly, ref AnomalyPulseEvent args)
|
||||
{
|
||||
var range = anomaly.Comp.MaxElectrocuteRange * args.Stability;
|
||||
var range = anomaly.Comp.MaxElectrocuteRange * args.Stability * args.PowerModifier;
|
||||
|
||||
int boltCount = (int)MathF.Floor(MathHelper.Lerp((float)anomaly.Comp.MinBoltCount, (float)anomaly.Comp.MaxBoltCount, args.Severity));
|
||||
|
||||
@@ -37,7 +37,7 @@ public sealed class ElectricityAnomalySystem : EntitySystem
|
||||
|
||||
private void OnSupercritical(Entity<ElectricityAnomalyComponent> anomaly, ref AnomalySupercriticalEvent args)
|
||||
{
|
||||
var range = anomaly.Comp.MaxElectrocuteRange * 3;
|
||||
var range = anomaly.Comp.MaxElectrocuteRange * 3 * args.PowerModifier;
|
||||
|
||||
_emp.EmpPulse(_transform.GetMapCoordinates(anomaly), range, anomaly.Comp.EmpEnergyConsumption, anomaly.Comp.EmpDisabledDuration);
|
||||
_lightning.ShootRandomLightnings(anomaly, range, anomaly.Comp.MaxBoltCount * 3, arcDepth: 3);
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed class EntityAnomalySystem : SharedEntityAnomalySystem
|
||||
if (!entry.Settings.SpawnOnPulse)
|
||||
continue;
|
||||
|
||||
SpawnEntities(component, entry, args.Stability, args.Severity);
|
||||
SpawnEntities(component, entry, args.Stability, args.Severity, args.PowerModifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public sealed class EntityAnomalySystem : SharedEntityAnomalySystem
|
||||
if (!entry.Settings.SpawnOnSuperCritical)
|
||||
continue;
|
||||
|
||||
SpawnEntities(component, entry, 1, 1);
|
||||
SpawnEntities(component, entry, 1, 1, args.PowerModifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public sealed class EntityAnomalySystem : SharedEntityAnomalySystem
|
||||
if (!entry.Settings.SpawnOnShutdown || args.Supercritical)
|
||||
continue;
|
||||
|
||||
SpawnEntities(component, entry, 1, 1);
|
||||
SpawnEntities(component, entry, 1, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ public sealed class EntityAnomalySystem : SharedEntityAnomalySystem
|
||||
if (!entry.Settings.SpawnOnStabilityChanged)
|
||||
continue;
|
||||
|
||||
SpawnEntities(component, entry, args.Stability, args.Severity);
|
||||
SpawnEntities(component, entry, args.Stability, args.Severity, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,17 +79,17 @@ public sealed class EntityAnomalySystem : SharedEntityAnomalySystem
|
||||
if (!entry.Settings.SpawnOnSeverityChanged)
|
||||
continue;
|
||||
|
||||
SpawnEntities(component, entry, args.Stability, args.Severity);
|
||||
SpawnEntities(component, entry, args.Stability, args.Severity, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnEntities(Entity<EntitySpawnAnomalyComponent> anomaly, EntitySpawnSettingsEntry entry, float stability, float severity)
|
||||
private void SpawnEntities(Entity<EntitySpawnAnomalyComponent> anomaly, EntitySpawnSettingsEntry entry, float stability, float severity, float powerMod)
|
||||
{
|
||||
var xform = Transform(anomaly);
|
||||
if (!TryComp(xform.GridUid, out MapGridComponent? grid))
|
||||
return;
|
||||
|
||||
var tiles = _anomaly.GetSpawningPoints(anomaly, stability, severity, entry.Settings);
|
||||
var tiles = _anomaly.GetSpawningPoints(anomaly, stability, severity, entry.Settings, powerMod);
|
||||
if (tiles == null)
|
||||
return;
|
||||
|
||||
|
||||
@@ -29,12 +29,12 @@ public sealed class InjectionAnomalySystem : EntitySystem
|
||||
|
||||
private void OnPulse(Entity<InjectionAnomalyComponent> entity, ref AnomalyPulseEvent args)
|
||||
{
|
||||
PulseScalableEffect(entity, entity.Comp.InjectRadius, entity.Comp.MaxSolutionInjection * args.Severity);
|
||||
PulseScalableEffect(entity, entity.Comp.InjectRadius * args.PowerModifier, entity.Comp.MaxSolutionInjection * args.Severity * args.PowerModifier);
|
||||
}
|
||||
|
||||
private void OnSupercritical(Entity<InjectionAnomalyComponent> entity, ref AnomalySupercriticalEvent args)
|
||||
{
|
||||
PulseScalableEffect(entity, entity.Comp.SuperCriticalInjectRadius, entity.Comp.SuperCriticalSolutionInjection);
|
||||
PulseScalableEffect(entity, entity.Comp.SuperCriticalInjectRadius * args.PowerModifier, entity.Comp.SuperCriticalSolutionInjection * args.PowerModifier);
|
||||
}
|
||||
|
||||
private void PulseScalableEffect(Entity<InjectionAnomalyComponent> entity, float injectRadius, float maxInject)
|
||||
|
||||
@@ -31,12 +31,12 @@ public sealed class ProjectileAnomalySystem : EntitySystem
|
||||
|
||||
private void OnPulse(EntityUid uid, ProjectileAnomalyComponent component, ref AnomalyPulseEvent args)
|
||||
{
|
||||
ShootProjectilesAtEntities(uid, component, args.Severity);
|
||||
ShootProjectilesAtEntities(uid, component, args.Severity * args.PowerModifier);
|
||||
}
|
||||
|
||||
private void OnSupercritical(EntityUid uid, ProjectileAnomalyComponent component, ref AnomalySupercriticalEvent args)
|
||||
{
|
||||
ShootProjectilesAtEntities(uid, component, 1.0f);
|
||||
ShootProjectilesAtEntities(uid, component, args.PowerModifier);
|
||||
}
|
||||
|
||||
private void ShootProjectilesAtEntities(EntityUid uid, ProjectileAnomalyComponent component, float severity)
|
||||
|
||||
@@ -25,9 +25,10 @@ public sealed class PuddleCreateAnomalySystem : EntitySystem
|
||||
return;
|
||||
|
||||
var xform = Transform(entity.Owner);
|
||||
var puddleSol = _solutionContainer.SplitSolution(sol.Value, entity.Comp.MaxPuddleSize * args.Severity);
|
||||
var puddleSol = _solutionContainer.SplitSolution(sol.Value, entity.Comp.MaxPuddleSize * args.Severity * args.PowerModifier);
|
||||
_puddle.TrySplashSpillAt(entity.Owner, xform.Coordinates, puddleSol, out _);
|
||||
}
|
||||
|
||||
private void OnSupercritical(Entity<PuddleCreateAnomalyComponent> entity, ref AnomalySupercriticalEvent args)
|
||||
{
|
||||
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var sol))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Anomaly.Effects.Components;
|
||||
@@ -24,14 +24,14 @@ public sealed class PyroclasticAnomalySystem : EntitySystem
|
||||
private void OnPulse(EntityUid uid, PyroclasticAnomalyComponent component, ref AnomalyPulseEvent args)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
var ignitionRadius = component.MaximumIgnitionRadius * args.Stability;
|
||||
var ignitionRadius = component.MaximumIgnitionRadius * args.Stability * args.PowerModifier;
|
||||
IgniteNearby(uid, xform.Coordinates, args.Severity, ignitionRadius);
|
||||
}
|
||||
|
||||
private void OnSupercritical(EntityUid uid, PyroclasticAnomalyComponent component, ref AnomalySupercriticalEvent args)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
IgniteNearby(uid, xform.Coordinates, 1, component.MaximumIgnitionRadius * 2);
|
||||
IgniteNearby(uid, xform.Coordinates, 1, component.MaximumIgnitionRadius * 2 * args.PowerModifier);
|
||||
}
|
||||
|
||||
public void IgniteNearby(EntityUid uid, EntityCoordinates coordinates, float severity, float radius)
|
||||
|
||||
40
Content.Server/Anomaly/Effects/SecretDataAnomalySystem.cs
Normal file
40
Content.Server/Anomaly/Effects/SecretDataAnomalySystem.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Content.Server.Anomaly.Components;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Anomaly.Effects;
|
||||
|
||||
public sealed class SecretDataAnomalySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
private readonly List<AnomalySecretData> _deita = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<SecretDataAnomalyComponent, MapInitEvent>(OnMapInit);
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, SecretDataAnomalyComponent anomaly, MapInitEvent args)
|
||||
{
|
||||
RandomizeSecret(uid,_random.Next(anomaly.RandomStartSecretMin, anomaly.RandomStartSecretMax), anomaly);
|
||||
}
|
||||
|
||||
public void RandomizeSecret(EntityUid uid, int count, SecretDataAnomalyComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
component.Secret.Clear();
|
||||
|
||||
// I also considered just adding all the enum values and pruning but that seems more wasteful.
|
||||
_deita.Clear();
|
||||
_deita.AddRange(Enum.GetValues<AnomalySecretData>());
|
||||
var actualCount = Math.Min(count, _deita.Count);
|
||||
|
||||
for (int i = 0; i < actualCount; i++)
|
||||
{
|
||||
component.Secret.Add(_random.PickAndTake(_deita));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using Content.Server.Anomaly.Components;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Anomaly.Effects;
|
||||
public sealed class ShuffleParticlesAnomalySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly AnomalySystem _anomaly = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<ShuffleParticlesAnomalyComponent, AnomalyPulseEvent>(OnPulse);
|
||||
SubscribeLocalEvent<ShuffleParticlesAnomalyComponent, StartCollideEvent>(OnStartCollide);
|
||||
}
|
||||
|
||||
private void OnStartCollide(EntityUid uid, ShuffleParticlesAnomalyComponent shuffle, StartCollideEvent args)
|
||||
{
|
||||
if (!TryComp<AnomalyComponent>(uid, out var anomaly))
|
||||
return;
|
||||
|
||||
if (shuffle.ShuffleOnParticleHit && _random.Prob(shuffle.Prob))
|
||||
_anomaly.ShuffleParticlesEffect(anomaly);
|
||||
|
||||
if (!TryComp<AnomalousParticleComponent>(args.OtherEntity, out var particle))
|
||||
return;
|
||||
}
|
||||
|
||||
private void OnPulse(EntityUid uid, ShuffleParticlesAnomalyComponent shuffle, AnomalyPulseEvent args)
|
||||
{
|
||||
if (!TryComp<AnomalyComponent>(uid, out var anomaly))
|
||||
return;
|
||||
|
||||
if (shuffle.ShuffleOnPulse && _random.Prob(shuffle.Prob))
|
||||
{
|
||||
_anomaly.ShuffleParticlesEffect(anomaly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed class TileAnomalySystem : SharedTileAnomalySystem
|
||||
if (!entry.Settings.SpawnOnPulse)
|
||||
continue;
|
||||
|
||||
SpawnTiles(component, entry, args.Stability, args.Severity);
|
||||
SpawnTiles(component, entry, args.Stability, args.Severity, args.PowerModifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public sealed class TileAnomalySystem : SharedTileAnomalySystem
|
||||
if (!entry.Settings.SpawnOnSuperCritical)
|
||||
continue;
|
||||
|
||||
SpawnTiles(component, entry, 1, 1);
|
||||
SpawnTiles(component, entry, 1, 1, args.PowerModifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ public sealed class TileAnomalySystem : SharedTileAnomalySystem
|
||||
if (!entry.Settings.SpawnOnShutdown || args.Supercritical)
|
||||
continue;
|
||||
|
||||
SpawnTiles(component, entry, 1, 1);
|
||||
SpawnTiles(component, entry, 1, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ public sealed class TileAnomalySystem : SharedTileAnomalySystem
|
||||
if (!entry.Settings.SpawnOnStabilityChanged)
|
||||
continue;
|
||||
|
||||
SpawnTiles(component, entry, args.Stability, args.Severity);
|
||||
SpawnTiles(component, entry, args.Stability, args.Severity, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,17 +78,17 @@ public sealed class TileAnomalySystem : SharedTileAnomalySystem
|
||||
if (!entry.Settings.SpawnOnSeverityChanged)
|
||||
continue;
|
||||
|
||||
SpawnTiles(component, entry, args.Stability, args.Severity);
|
||||
SpawnTiles(component, entry, args.Stability, args.Severity, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnTiles(Entity<TileSpawnAnomalyComponent> anomaly, TileSpawnSettingsEntry entry, float stability, float severity)
|
||||
private void SpawnTiles(Entity<TileSpawnAnomalyComponent> anomaly, TileSpawnSettingsEntry entry, float stability, float severity, float powerMod)
|
||||
{
|
||||
var xform = Transform(anomaly);
|
||||
if (!TryComp<MapGridComponent>(xform.GridUid, out var grid))
|
||||
return;
|
||||
|
||||
var tiles = _anomaly.GetSpawningPoints(anomaly, stability, severity, entry.Settings);
|
||||
var tiles = _anomaly.GetSpawningPoints(anomaly, stability, severity, entry.Settings, powerMod);
|
||||
if (tiles == null)
|
||||
return;
|
||||
|
||||
|
||||
@@ -432,14 +432,16 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
if (!overlay.Chunks.TryGetValue(gIndex, out var value))
|
||||
continue;
|
||||
|
||||
if (previousChunks != null &&
|
||||
previousChunks.Contains(gIndex) &&
|
||||
value.LastUpdate > LastSessionUpdate)
|
||||
// If the chunk was updated since we last sent it, send it again
|
||||
if (value.LastUpdate > LastSessionUpdate)
|
||||
{
|
||||
dataToSend.Add(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
dataToSend.Add(value);
|
||||
// Always send it if we didn't previously send it
|
||||
if (previousChunks == null || !previousChunks.Contains(gIndex))
|
||||
dataToSend.Add(value);
|
||||
}
|
||||
|
||||
previouslySent[netGrid] = gridChunks;
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
|
||||
private void OnFilterUpdated(EntityUid uid, GasFilterComponent filter, ref AtmosDeviceUpdateEvent args)
|
||||
{
|
||||
if (!filter.Enabled
|
||||
|| !_nodeContainer.TryGetNodes(uid, filter.InletName, filter.OutletName, filter.FilterName, out PipeNode? inletNode, out PipeNode? filterNode, out PipeNode? outletNode)
|
||||
|| !_nodeContainer.TryGetNodes(uid, filter.InletName, filter.FilterName, filter.OutletName, out PipeNode? inletNode, out PipeNode? filterNode, out PipeNode? outletNode)
|
||||
|| outletNode.Air.Pressure >= Atmospherics.MaxOutputPressure) // No need to transfer if target is full.
|
||||
{
|
||||
_ambientSoundSystem.SetAmbience(uid, false);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Inventory;
|
||||
|
||||
namespace Content.Server.Chemistry.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for components that inject a solution into a target's bloodstream in response to an event.
|
||||
/// </summary>
|
||||
public abstract partial class BaseSolutionInjectOnEventComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// How much solution to remove from this entity per target when transferring.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that this amount is per target, so the total amount removed will be
|
||||
/// multiplied by the number of targets hit.
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public FixedPoint2 TransferAmount = FixedPoint2.New(1);
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); }
|
||||
|
||||
/// <summary>
|
||||
/// Proportion of the <see cref="TransferAmount"/> that will actually be injected
|
||||
/// into the target's bloodstream. The rest is lost.
|
||||
/// 0 means none of the transferred solution will enter the bloodstream.
|
||||
/// 1 means the entire amount will enter the bloodstream.
|
||||
/// </summary>
|
||||
[DataField("transferEfficiency")]
|
||||
private float _transferEfficiency = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Solution to inject from.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string Solution = "default";
|
||||
|
||||
/// <summary>
|
||||
/// Whether this will inject through hardsuits or not.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool PierceArmor = true;
|
||||
|
||||
/// <summary>
|
||||
/// Contents of popup message to display to the attacker when injection
|
||||
/// fails due to the target wearing a hardsuit.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Passed values: $weapon and $target
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public LocId BlockedByHardsuitPopupMessage = "melee-inject-failed-hardsuit";
|
||||
|
||||
/// <summary>
|
||||
/// If anything covers any of these slots then the injection fails.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SlotFlags BlockSlots = SlotFlags.NONE;
|
||||
}
|
||||
@@ -1,31 +1,8 @@
|
||||
using Content.Shared.FixedPoint;
|
||||
namespace Content.Server.Chemistry.Components;
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class MeleeChemicalInjectorComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("transferAmount")]
|
||||
public FixedPoint2 TransferAmount { get; set; } = FixedPoint2.New(1);
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); }
|
||||
|
||||
[DataField("transferEfficiency")]
|
||||
private float _transferEfficiency = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this will inject through hardsuits or not.
|
||||
/// </summary>
|
||||
[DataField("pierceArmor"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool PierceArmor = true;
|
||||
|
||||
/// <summary>
|
||||
/// Solution to inject from.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("solution")]
|
||||
public string Solution { get; set; } = "default";
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Used for melee weapon entities that should try to inject a
|
||||
/// contained solution into a target when used to hit it.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class MeleeChemicalInjectorComponent : BaseSolutionInjectOnEventComponent { }
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Projectiles;
|
||||
|
||||
namespace Content.Server.Chemistry.Components;
|
||||
|
||||
/// <summary>
|
||||
/// On colliding with an entity that has a bloodstream will dump its solution onto them.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class SolutionInjectOnCollideComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("transferAmount")]
|
||||
public FixedPoint2 TransferAmount = FixedPoint2.New(1);
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); }
|
||||
|
||||
[DataField("transferEfficiency")]
|
||||
private float _transferEfficiency = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// If anything covers any of these slots then the injection fails.
|
||||
/// </summary>
|
||||
[DataField("blockSlots"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public SlotFlags BlockSlots = SlotFlags.MASK;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Content.Server.Chemistry.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Used for embeddable entities that should try to inject a
|
||||
/// contained solution into a target when they become embedded in it.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class SolutionInjectOnEmbedComponent : BaseSolutionInjectOnEventComponent { }
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Content.Server.Chemistry.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Used for projectile entities that should try to inject a
|
||||
/// contained solution into a target when they hit it.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class SolutionInjectOnProjectileHitComponent : BaseSolutionInjectOnEventComponent { }
|
||||
@@ -1,11 +1,12 @@
|
||||
using Content.Server.Chemistry.Components;
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Server.Nutrition.Components;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Dispenser;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Audio;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Chemistry.Components;
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Projectiles;
|
||||
|
||||
namespace Content.Server.Chemistry.EntitySystems;
|
||||
|
||||
public sealed class SolutionInjectOnCollideSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionContainersSystem = default!;
|
||||
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SolutionInjectOnCollideComponent, ProjectileHitEvent>(HandleInjection);
|
||||
}
|
||||
|
||||
private void HandleInjection(Entity<SolutionInjectOnCollideComponent> ent, ref ProjectileHitEvent args)
|
||||
{
|
||||
var component = ent.Comp;
|
||||
var target = args.Target;
|
||||
|
||||
if (!TryComp<BloodstreamComponent>(target, out var bloodstream) ||
|
||||
!_solutionContainersSystem.TryGetInjectableSolution(ent.Owner, out var solution, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (component.BlockSlots != 0x0)
|
||||
{
|
||||
var containerEnumerator = _inventorySystem.GetSlotEnumerator(target, component.BlockSlots);
|
||||
|
||||
// TODO add a helper method for this?
|
||||
if (containerEnumerator.MoveNext(out _))
|
||||
return;
|
||||
}
|
||||
|
||||
var solRemoved = _solutionContainersSystem.SplitSolution(solution.Value, component.TransferAmount);
|
||||
var solRemovedVol = solRemoved.Volume;
|
||||
|
||||
var solToInject = solRemoved.SplitSolution(solRemovedVol * component.TransferEfficiency);
|
||||
|
||||
_bloodstreamSystem.TryAddToChemicals(target, solToInject, bloodstream);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Chemistry.Components;
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Weapons.Melee.Events;
|
||||
using Robust.Shared.Collections;
|
||||
|
||||
namespace Content.Server.Chemistry.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// System for handling the different inheritors of <see cref="BaseSolutionInjectOnEventComponent"/>.
|
||||
/// Subscribes to relevent events and performs solution injections when they are raised.
|
||||
/// </summary>
|
||||
public sealed class SolutionInjectOnCollideSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SolutionInjectOnProjectileHitComponent, ProjectileHitEvent>(HandleProjectileHit);
|
||||
SubscribeLocalEvent<SolutionInjectOnEmbedComponent, EmbedEvent>(HandleEmbed);
|
||||
SubscribeLocalEvent<MeleeChemicalInjectorComponent, MeleeHitEvent>(HandleMeleeHit);
|
||||
}
|
||||
|
||||
private void HandleProjectileHit(Entity<SolutionInjectOnProjectileHitComponent> entity, ref ProjectileHitEvent args)
|
||||
{
|
||||
DoInjection((entity.Owner, entity.Comp), args.Target, args.Shooter);
|
||||
}
|
||||
|
||||
private void HandleEmbed(Entity<SolutionInjectOnEmbedComponent> entity, ref EmbedEvent args)
|
||||
{
|
||||
DoInjection((entity.Owner, entity.Comp), args.Embedded, args.Shooter);
|
||||
}
|
||||
|
||||
private void HandleMeleeHit(Entity<MeleeChemicalInjectorComponent> entity, ref MeleeHitEvent args)
|
||||
{
|
||||
// MeleeHitEvent is weird, so we have to filter to make sure we actually
|
||||
// hit something and aren't just examining the weapon.
|
||||
if (args.IsHit)
|
||||
TryInjectTargets((entity.Owner, entity.Comp), args.HitEntities, args.User);
|
||||
}
|
||||
|
||||
private void DoInjection(Entity<BaseSolutionInjectOnEventComponent> injectorEntity, EntityUid target, EntityUid? source = null)
|
||||
{
|
||||
TryInjectTargets(injectorEntity, [target], source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters <paramref name="targets"/> for valid targets and tries to inject a portion of <see cref="BaseSolutionInjectOnEventComponent.Solution"/> into
|
||||
/// each valid target's bloodstream.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Targets are invalid if any of the following are true:
|
||||
/// <list type="bullet">
|
||||
/// <item>The target does not have a bloodstream.</item>
|
||||
/// <item><see cref="BaseSolutionInjectOnEventComponent.PierceArmor"/> is false and the target is wearing a hardsuit.</item>
|
||||
/// <item><see cref="BaseSolutionInjectOnEventComponent.BlockSlots"/> is not NONE and the target has an item equipped in any of the specified slots.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <returns>true if at least one target was successfully injected, otherwise false</returns>
|
||||
private bool TryInjectTargets(Entity<BaseSolutionInjectOnEventComponent> injector, IReadOnlyList<EntityUid> targets, EntityUid? source = null)
|
||||
{
|
||||
// Make sure we have at least one target
|
||||
if (targets.Count == 0)
|
||||
return false;
|
||||
|
||||
// Get the solution to inject
|
||||
if (!_solutionContainer.TryGetSolution(injector.Owner, injector.Comp.Solution, out var injectorSolution))
|
||||
return false;
|
||||
|
||||
// Build a list of bloodstreams to inject into
|
||||
var targetBloodstreams = new ValueList<Entity<BloodstreamComponent>>();
|
||||
foreach (var target in targets)
|
||||
{
|
||||
if (Deleted(target))
|
||||
continue;
|
||||
|
||||
// Yuck, this is way to hardcodey for my tastes
|
||||
// TODO blocking injection with a hardsuit should probably done with a cancellable event or something
|
||||
if (!injector.Comp.PierceArmor && _inventory.TryGetSlotEntity(target, "outerClothing", out var suit) && _tag.HasTag(suit.Value, "Hardsuit"))
|
||||
{
|
||||
// Only show popup to attacker
|
||||
if (source != null)
|
||||
_popup.PopupEntity(Loc.GetString(injector.Comp.BlockedByHardsuitPopupMessage, ("weapon", injector.Owner), ("target", target)), target, source.Value, PopupType.SmallCaution);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the target has anything equipped in a slot that would block injection
|
||||
if (injector.Comp.BlockSlots != SlotFlags.NONE)
|
||||
{
|
||||
var blocked = false;
|
||||
var containerEnumerator = _inventory.GetSlotEnumerator(target, injector.Comp.BlockSlots);
|
||||
while (containerEnumerator.MoveNext(out var container))
|
||||
{
|
||||
if (container.ContainedEntity != null)
|
||||
{
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (blocked)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make sure the target has a bloodstream
|
||||
if (!TryComp<BloodstreamComponent>(target, out var bloodstream))
|
||||
continue;
|
||||
|
||||
|
||||
// Checks passed; add this target's bloodstream to the list
|
||||
targetBloodstreams.Add((target, bloodstream));
|
||||
}
|
||||
|
||||
// Make sure we got at least one bloodstream
|
||||
if (targetBloodstreams.Count == 0)
|
||||
return false;
|
||||
|
||||
// Extract total needed solution from the injector
|
||||
var removedSolution = _solutionContainer.SplitSolution(injectorSolution.Value, injector.Comp.TransferAmount * targetBloodstreams.Count);
|
||||
// Adjust solution amount based on transfer efficiency
|
||||
var solutionToInject = removedSolution.SplitSolution(removedSolution.Volume * injector.Comp.TransferEfficiency);
|
||||
// Calculate how much of the adjusted solution each target will get
|
||||
var volumePerBloodstream = solutionToInject.Volume * (1f / targetBloodstreams.Count);
|
||||
|
||||
var anySuccess = false;
|
||||
foreach (var targetBloodstream in targetBloodstreams)
|
||||
{
|
||||
// Take our portion of the adjusted solution for this target
|
||||
var individualInjection = solutionToInject.SplitSolution(volumePerBloodstream);
|
||||
// Inject our portion into the target's bloodstream
|
||||
if (_bloodstream.TryAddToChemicals(targetBloodstream.Owner, individualInjection, targetBloodstream.Comp))
|
||||
anySuccess = true;
|
||||
}
|
||||
|
||||
// Huzzah!
|
||||
return anySuccess;
|
||||
}
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Chemistry.EntitySystems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class SolutionTransferSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Default transfer amounts for the set-transfer verb.
|
||||
/// </summary>
|
||||
public static readonly List<int> DefaultTransferAmounts = new() { 1, 5, 10, 25, 50, 100, 250, 500, 1000 };
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SolutionTransferComponent, GetVerbsEvent<AlternativeVerb>>(AddSetTransferVerbs);
|
||||
SubscribeLocalEvent<SolutionTransferComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<SolutionTransferComponent, TransferAmountSetValueMessage>(OnTransferAmountSetValueMessage);
|
||||
}
|
||||
|
||||
private void OnTransferAmountSetValueMessage(Entity<SolutionTransferComponent> entity, ref TransferAmountSetValueMessage message)
|
||||
{
|
||||
var newTransferAmount = FixedPoint2.Clamp(message.Value, entity.Comp.MinimumTransferAmount, entity.Comp.MaximumTransferAmount);
|
||||
entity.Comp.TransferAmount = newTransferAmount;
|
||||
|
||||
if (message.Session.AttachedEntity is { Valid: true } user)
|
||||
_popupSystem.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", newTransferAmount)), entity.Owner, user);
|
||||
}
|
||||
|
||||
private void AddSetTransferVerbs(Entity<SolutionTransferComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
var (uid, component) = entity;
|
||||
|
||||
if (!args.CanAccess || !args.CanInteract || !component.CanChangeTransferAmount || args.Hands == null)
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
// Custom transfer verb
|
||||
AlternativeVerb custom = new();
|
||||
custom.Text = Loc.GetString("comp-solution-transfer-verb-custom-amount");
|
||||
custom.Category = VerbCategory.SetTransferAmount;
|
||||
custom.Act = () => _userInterfaceSystem.TryOpen(uid, TransferAmountUiKey.Key, actor.PlayerSession);
|
||||
custom.Priority = 1;
|
||||
args.Verbs.Add(custom);
|
||||
|
||||
// Add specific transfer verbs according to the container's size
|
||||
var priority = 0;
|
||||
var user = args.User;
|
||||
foreach (var amount in DefaultTransferAmounts)
|
||||
{
|
||||
if (amount < component.MinimumTransferAmount.Int() || amount > component.MaximumTransferAmount.Int())
|
||||
continue;
|
||||
|
||||
AlternativeVerb verb = new();
|
||||
verb.Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount));
|
||||
verb.Category = VerbCategory.SetTransferAmount;
|
||||
verb.Act = () =>
|
||||
{
|
||||
component.TransferAmount = FixedPoint2.New(amount);
|
||||
_popupSystem.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), uid, user);
|
||||
};
|
||||
|
||||
// we want to sort by size, not alphabetically by the verb text.
|
||||
verb.Priority = priority;
|
||||
priority--;
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAfterInteract(Entity<SolutionTransferComponent> entity, ref AfterInteractEvent args)
|
||||
{
|
||||
if (!args.CanReach || args.Target == null)
|
||||
return;
|
||||
|
||||
var target = args.Target!.Value;
|
||||
var (uid, component) = entity;
|
||||
|
||||
//Special case for reagent tanks, because normally clicking another container will give solution, not take it.
|
||||
if (component.CanReceive && !EntityManager.HasComponent<RefillableSolutionComponent>(target) // target must not be refillable (e.g. Reagent Tanks)
|
||||
&& _solutionContainerSystem.TryGetDrainableSolution(target, out var targetSoln, out _) // target must be drainable
|
||||
&& EntityManager.TryGetComponent(uid, out RefillableSolutionComponent? refillComp)
|
||||
&& _solutionContainerSystem.TryGetRefillableSolution((uid, refillComp, null), out var ownerSoln, out var ownerRefill))
|
||||
|
||||
{
|
||||
|
||||
var transferAmount = component.TransferAmount; // This is the player-configurable transfer amount of "uid," not the target reagent tank.
|
||||
|
||||
if (EntityManager.TryGetComponent(uid, out RefillableSolutionComponent? refill) && refill.MaxRefill != null) // uid is the entity receiving solution from target.
|
||||
{
|
||||
transferAmount = FixedPoint2.Min(transferAmount, (FixedPoint2) refill.MaxRefill); // if the receiver has a smaller transfer limit, use that instead
|
||||
}
|
||||
|
||||
var transferred = Transfer(args.User, target, targetSoln.Value, uid, ownerSoln.Value, transferAmount);
|
||||
if (transferred > 0)
|
||||
{
|
||||
var toTheBrim = ownerRefill.AvailableVolume == 0;
|
||||
var msg = toTheBrim
|
||||
? "comp-solution-transfer-fill-fully"
|
||||
: "comp-solution-transfer-fill-normal";
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString(msg, ("owner", args.Target), ("amount", transferred), ("target", uid)), uid, args.User);
|
||||
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// if target is refillable, and owner is drainable
|
||||
if (component.CanSend && _solutionContainerSystem.TryGetRefillableSolution(target, out targetSoln, out var targetRefill)
|
||||
&& _solutionContainerSystem.TryGetDrainableSolution(uid, out ownerSoln, out var ownerDrain))
|
||||
{
|
||||
var transferAmount = component.TransferAmount;
|
||||
|
||||
if (EntityManager.TryGetComponent(target, out RefillableSolutionComponent? refill) && refill.MaxRefill != null)
|
||||
{
|
||||
transferAmount = FixedPoint2.Min(transferAmount, (FixedPoint2) refill.MaxRefill);
|
||||
}
|
||||
|
||||
var transferred = Transfer(args.User, uid, ownerSoln.Value, target, targetSoln.Value, transferAmount);
|
||||
|
||||
if (transferred > 0)
|
||||
{
|
||||
var message = Loc.GetString("comp-solution-transfer-transfer-solution", ("amount", transferred), ("target", target));
|
||||
_popupSystem.PopupEntity(message, uid, args.User);
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transfer from a solution to another.
|
||||
/// </summary>
|
||||
/// <returns>The actual amount transferred.</returns>
|
||||
public FixedPoint2 Transfer(EntityUid user,
|
||||
EntityUid sourceEntity,
|
||||
Entity<SolutionComponent> source,
|
||||
EntityUid targetEntity,
|
||||
Entity<SolutionComponent> target,
|
||||
FixedPoint2 amount)
|
||||
{
|
||||
var transferAttempt = new SolutionTransferAttemptEvent(sourceEntity, targetEntity);
|
||||
|
||||
// Check if the source is cancelling the transfer
|
||||
RaiseLocalEvent(sourceEntity, transferAttempt, broadcast: true);
|
||||
if (transferAttempt.Cancelled)
|
||||
{
|
||||
_popupSystem.PopupEntity(transferAttempt.CancelReason!, sourceEntity, user);
|
||||
return FixedPoint2.Zero;
|
||||
}
|
||||
|
||||
var sourceSolution = source.Comp.Solution;
|
||||
if (sourceSolution.Volume == 0)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("comp-solution-transfer-is-empty", ("target", sourceEntity)), sourceEntity, user);
|
||||
return FixedPoint2.Zero;
|
||||
}
|
||||
|
||||
// Check if the target is cancelling the transfer
|
||||
RaiseLocalEvent(targetEntity, transferAttempt, broadcast: true);
|
||||
if (transferAttempt.Cancelled)
|
||||
{
|
||||
_popupSystem.PopupEntity(transferAttempt.CancelReason!, sourceEntity, user);
|
||||
return FixedPoint2.Zero;
|
||||
}
|
||||
|
||||
var targetSolution = target.Comp.Solution;
|
||||
if (targetSolution.AvailableVolume == 0)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("comp-solution-transfer-is-full", ("target", targetEntity)), targetEntity, user);
|
||||
return FixedPoint2.Zero;
|
||||
}
|
||||
|
||||
var actualAmount = FixedPoint2.Min(amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume));
|
||||
|
||||
var solution = _solutionContainerSystem.Drain(sourceEntity, source, actualAmount);
|
||||
_solutionContainerSystem.Refill(targetEntity, target, solution);
|
||||
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium,
|
||||
$"{EntityManager.ToPrettyString(user):player} transferred {string.Join(", ", solution.Contents)} to {EntityManager.ToPrettyString(targetEntity):entity}, which now contains {SolutionContainerSystem.ToPrettyString(targetSolution)}");
|
||||
|
||||
return actualAmount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when attempting to transfer from one solution to another.
|
||||
/// </summary>
|
||||
public sealed class SolutionTransferAttemptEvent : CancellableEntityEventArgs
|
||||
{
|
||||
public SolutionTransferAttemptEvent(EntityUid from, EntityUid to)
|
||||
{
|
||||
From = from;
|
||||
To = to;
|
||||
}
|
||||
|
||||
public EntityUid From { get; }
|
||||
public EntityUid To { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Why the transfer has been cancelled.
|
||||
/// </summary>
|
||||
public string? CancelReason { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the transfer.
|
||||
/// </summary>
|
||||
public void Cancel(string reason)
|
||||
{
|
||||
base.Cancel();
|
||||
CancelReason = reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using Content.Shared.Construction;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Toilet;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Construction.Conditions
|
||||
{
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed partial class ToiletLidClosed : IGraphCondition
|
||||
{
|
||||
public bool Condition(EntityUid uid, IEntityManager entityManager)
|
||||
{
|
||||
if (!entityManager.TryGetComponent(uid, out ToiletComponent? toilet))
|
||||
return false;
|
||||
|
||||
return !toilet.LidOpen;
|
||||
}
|
||||
|
||||
public bool DoExamine(ExaminedEvent args)
|
||||
{
|
||||
var entity = args.Examined;
|
||||
|
||||
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(entity, out ToiletComponent? toilet)) return false;
|
||||
if (!toilet.LidOpen) return false;
|
||||
|
||||
args.PushMarkup(Loc.GetString("construction-examine-condition-toilet-lid-closed") + "\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEnumerable<ConstructionGuideEntry> GenerateGuideEntry()
|
||||
{
|
||||
yield return new ConstructionGuideEntry()
|
||||
{
|
||||
Localization = "construction-step-condition-toilet-lid-closed"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
namespace Content.Server.Destructible.Thresholds.Behaviors;
|
||||
|
||||
|
||||
@@ -23,6 +23,12 @@ public sealed partial class SignalTimerComponent : Component
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public string Label = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Default max width of a label (how many letters can this render?)
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public int MaxLength = 5;
|
||||
|
||||
/// <summary>
|
||||
/// The port that gets signaled when the timer triggers.
|
||||
/// </summary>
|
||||
|
||||
@@ -39,6 +39,7 @@ public sealed class SignalTimerSystem : EntitySystem
|
||||
|
||||
private void OnInit(EntityUid uid, SignalTimerComponent component, ComponentInit args)
|
||||
{
|
||||
_appearanceSystem.SetData(uid, TextScreenVisuals.DefaultText, component.Label);
|
||||
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, component.Label);
|
||||
_signalSystem.EnsureSinkPorts(uid, component.Trigger);
|
||||
}
|
||||
@@ -66,11 +67,6 @@ public sealed class SignalTimerSystem : EntitySystem
|
||||
{
|
||||
RemComp<ActiveSignalTimerComponent>(uid);
|
||||
|
||||
if (TryComp<AppearanceComponent>(uid, out var appearance))
|
||||
{
|
||||
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, signalTimer.Label, appearance);
|
||||
}
|
||||
|
||||
_audio.PlayPvs(signalTimer.DoneSound, uid);
|
||||
_signalSystem.InvokePort(uid, signalTimer.TriggerPort);
|
||||
|
||||
@@ -139,10 +135,15 @@ public sealed class SignalTimerSystem : EntitySystem
|
||||
if (!IsMessageValid(uid, args))
|
||||
return;
|
||||
|
||||
component.Label = args.Text[..Math.Min(5, args.Text.Length)];
|
||||
component.Label = args.Text[..Math.Min(component.MaxLength, args.Text.Length)];
|
||||
|
||||
if (!HasComp<ActiveSignalTimerComponent>(uid))
|
||||
{
|
||||
// could maybe move the defaulttext update out of this block,
|
||||
// if you delved deep into appearance update batching
|
||||
_appearanceSystem.SetData(uid, TextScreenVisuals.DefaultText, component.Label);
|
||||
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, component.Label);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -166,7 +167,15 @@ public sealed class SignalTimerSystem : EntitySystem
|
||||
{
|
||||
if (!IsMessageValid(uid, args))
|
||||
return;
|
||||
OnStartTimer(uid, component);
|
||||
|
||||
// feedback received: pressing the timer button while a timer is running should cancel the timer.
|
||||
if (HasComp<ActiveSignalTimerComponent>(uid))
|
||||
{
|
||||
_appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, _gameTiming.CurTime);
|
||||
Trigger(uid, component);
|
||||
}
|
||||
else
|
||||
OnStartTimer(uid, component);
|
||||
}
|
||||
|
||||
private void OnSignalReceived(EntityUid uid, SignalTimerComponent component, ref SignalReceivedEvent args)
|
||||
|
||||
@@ -135,8 +135,7 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||
{
|
||||
// This is not an interaction, activation, or alternative verb type because unfortunately most users are
|
||||
// unwilling to accept that this is where they belong and don't want to accidentally climb inside.
|
||||
if (!component.MobsCanEnter ||
|
||||
!args.CanAccess ||
|
||||
if (!args.CanAccess ||
|
||||
!args.CanInteract ||
|
||||
component.Container.ContainedEntities.Contains(args.User) ||
|
||||
!_actionBlockerSystem.CanMove(args.User))
|
||||
@@ -630,10 +629,10 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||
switch (state)
|
||||
{
|
||||
case DisposalsPressureState.Flushed:
|
||||
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Flushing, appearance);
|
||||
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.OverlayFlushing, appearance);
|
||||
break;
|
||||
case DisposalsPressureState.Pressurizing:
|
||||
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Charging, appearance);
|
||||
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.OverlayCharging, appearance);
|
||||
break;
|
||||
case DisposalsPressureState.Ready:
|
||||
_appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Anchored, appearance);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Doors.Electronics;
|
||||
using Content.Shared.Access;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.Doors.Electronics;
|
||||
using Content.Shared.Doors;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Doors.Electronics;
|
||||
|
||||
public sealed class DoorElectronicsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<DoorElectronicsComponent, DoorElectronicsUpdateConfigurationMessage>(OnChangeConfiguration);
|
||||
SubscribeLocalEvent<DoorElectronicsComponent, AccessReaderConfigurationChangedEvent>(OnAccessReaderChanged);
|
||||
SubscribeLocalEvent<DoorElectronicsComponent, BoundUIOpenedEvent>(OnBoundUIOpened);
|
||||
}
|
||||
|
||||
public void UpdateUserInterface(EntityUid uid, DoorElectronicsComponent component)
|
||||
{
|
||||
var accesses = new List<ProtoId<AccessLevelPrototype>>();
|
||||
|
||||
if (TryComp<AccessReaderComponent>(uid, out var accessReader))
|
||||
{
|
||||
foreach (var accessList in accessReader.AccessLists)
|
||||
{
|
||||
var access = accessList.FirstOrDefault();
|
||||
accesses.Add(access);
|
||||
}
|
||||
}
|
||||
|
||||
var state = new DoorElectronicsConfigurationState(accesses);
|
||||
_uiSystem.TrySetUiState(uid, DoorElectronicsConfigurationUiKey.Key, state);
|
||||
}
|
||||
|
||||
private void OnChangeConfiguration(
|
||||
EntityUid uid,
|
||||
DoorElectronicsComponent component,
|
||||
DoorElectronicsUpdateConfigurationMessage args)
|
||||
{
|
||||
var accessReader = EnsureComp<AccessReaderComponent>(uid);
|
||||
_accessReader.SetAccesses(uid, accessReader, args.AccessList);
|
||||
}
|
||||
|
||||
private void OnAccessReaderChanged(
|
||||
EntityUid uid,
|
||||
DoorElectronicsComponent component,
|
||||
AccessReaderConfigurationChangedEvent args)
|
||||
{
|
||||
UpdateUserInterface(uid, component);
|
||||
}
|
||||
|
||||
private void OnBoundUIOpened(
|
||||
EntityUid uid,
|
||||
DoorElectronicsComponent component,
|
||||
BoundUIOpenedEvent args)
|
||||
{
|
||||
UpdateUserInterface(uid, component);
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ public sealed class FireExtinguisherSystem : EntitySystem
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
// TODO: why is this copy paste shit here just have fire extinguisher cancel transfer when safety is on
|
||||
var transfer = containerSolution.AvailableVolume;
|
||||
if (TryComp<SolutionTransferComponent>(entity.Owner, out var solTrans))
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
@@ -11,6 +11,7 @@ using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Fluids.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Spillable;
|
||||
using Content.Shared.Throwing;
|
||||
@@ -29,6 +30,7 @@ public sealed partial class PuddleSystem
|
||||
// Openable handles the event if it's closed
|
||||
SubscribeLocalEvent<SpillableComponent, MeleeHitEvent>(SplashOnMeleeHit, after: [typeof(OpenableSystem)]);
|
||||
SubscribeLocalEvent<SpillableComponent, GotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<SpillableComponent, GotUnequippedEvent>(OnGotUnequipped);
|
||||
SubscribeLocalEvent<SpillableComponent, SolutionContainerOverflowEvent>(OnOverflow);
|
||||
SubscribeLocalEvent<SpillableComponent, SpillDoAfterEvent>(OnDoAfter);
|
||||
SubscribeLocalEvent<SpillableComponent, AttemptPacifiedThrowEvent>(OnAttemptPacifiedThrow);
|
||||
@@ -114,6 +116,9 @@ public sealed partial class PuddleSystem
|
||||
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution))
|
||||
return;
|
||||
|
||||
// block access to the solution while worn
|
||||
AddComp<BlockSolutionAccessComponent>(entity);
|
||||
|
||||
if (solution.Volume == 0)
|
||||
return;
|
||||
|
||||
@@ -122,6 +127,14 @@ public sealed partial class PuddleSystem
|
||||
TrySplashSpillAt(entity.Owner, Transform(args.Equipee).Coordinates, drainedSolution, out _);
|
||||
}
|
||||
|
||||
private void OnGotUnequipped(Entity<SpillableComponent> entity, ref GotUnequippedEvent args)
|
||||
{
|
||||
if (!entity.Comp.SpillWorn)
|
||||
return;
|
||||
|
||||
RemCompDeferred<BlockSolutionAccessComponent>(entity);
|
||||
}
|
||||
|
||||
private void SpillOnLand(Entity<SpillableComponent> entity, ref LandEvent args)
|
||||
{
|
||||
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution))
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Server.Discord;
|
||||
using Content.Server.GameTicking.Events;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Maps;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mind;
|
||||
@@ -194,26 +195,18 @@ namespace Content.Server.GameTicking
|
||||
|
||||
SendServerMessage(Loc.GetString("game-ticker-start-round"));
|
||||
|
||||
// Just in case it hasn't been loaded previously we'll try loading it.
|
||||
LoadMaps();
|
||||
|
||||
// map has been selected so update the lobby info text
|
||||
// applies to players who didn't ready up
|
||||
UpdateInfoText();
|
||||
|
||||
StartGamePresetRules();
|
||||
|
||||
RoundLengthMetric.Set(0);
|
||||
|
||||
var startingEvent = new RoundStartingEvent(RoundId);
|
||||
RaiseLocalEvent(startingEvent);
|
||||
var readyPlayers = new List<ICommonSession>();
|
||||
var readyPlayerProfiles = new Dictionary<NetUserId, HumanoidCharacterProfile>();
|
||||
|
||||
var autoDeAdmin = _cfg.GetCVar(CCVars.AdminDeadminOnJoin);
|
||||
foreach (var (userId, status) in _playerGameStatuses)
|
||||
{
|
||||
if (LobbyEnabled && status != PlayerGameStatus.ReadyToPlay) continue;
|
||||
if (!_playerManager.TryGetSessionById(userId, out var session)) continue;
|
||||
|
||||
if (autoDeAdmin && _adminManager.IsAdmin(session))
|
||||
{
|
||||
_adminManager.DeAdmin(session);
|
||||
}
|
||||
#if DEBUG
|
||||
DebugTools.Assert(_userDb.IsLoadComplete(session), $"Player was readied up but didn't have user DB data loaded yet??");
|
||||
#endif
|
||||
@@ -235,6 +228,20 @@ namespace Content.Server.GameTicking
|
||||
readyPlayerProfiles.Add(userId, profile);
|
||||
}
|
||||
|
||||
// Just in case it hasn't been loaded previously we'll try loading it.
|
||||
LoadMaps();
|
||||
|
||||
// map has been selected so update the lobby info text
|
||||
// applies to players who didn't ready up
|
||||
UpdateInfoText();
|
||||
|
||||
StartGamePresetRules();
|
||||
|
||||
RoundLengthMetric.Set(0);
|
||||
|
||||
var startingEvent = new RoundStartingEvent(RoundId);
|
||||
RaiseLocalEvent(startingEvent);
|
||||
|
||||
var origReadyPlayers = readyPlayers.ToArray();
|
||||
|
||||
if (!StartPreset(origReadyPlayers, force))
|
||||
|
||||
@@ -154,10 +154,6 @@ namespace Content.Server.GameTicking
|
||||
return;
|
||||
}
|
||||
|
||||
// Automatically de-admin players who are joining.
|
||||
if (_cfg.GetCVar(CCVars.AdminDeadminOnJoin) && _adminManager.IsAdmin(player))
|
||||
_adminManager.DeAdmin(player);
|
||||
|
||||
// We raise this event to allow other systems to handle spawning this player themselves. (e.g. late-join wizard, etc)
|
||||
var bev = new PlayerBeforeSpawnEvent(player, character, jobId, lateJoin, station);
|
||||
RaiseLocalEvent(bev);
|
||||
|
||||
@@ -763,10 +763,6 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
_mind.SetUserId(newMind, nukieSession.Session.UserId);
|
||||
_roles.MindAddRole(newMind, new NukeopsRoleComponent { PrototypeId = nukieSession.Type.AntagRoleProto });
|
||||
|
||||
// Automatically de-admin players who are being made nukeops
|
||||
if (_cfg.GetCVar(CCVars.AdminDeadminOnJoin) && _adminManager.IsAdmin(nukieSession.Session))
|
||||
_adminManager.DeAdmin(nukieSession.Session);
|
||||
|
||||
_mind.TransferTo(newMind, mob);
|
||||
}
|
||||
//Otherwise, spawn as a ghost role
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Glue;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Components;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
|
||||
@@ -75,6 +75,9 @@ namespace Content.Server.Hands.Systems
|
||||
|
||||
private void OnExploded(Entity<HandsComponent> ent, ref BeforeExplodeEvent args)
|
||||
{
|
||||
if (ent.Comp.DisableExplosionRecursion)
|
||||
return;
|
||||
|
||||
foreach (var hand in ent.Comp.Hands.Values)
|
||||
{
|
||||
if (hand.HeldEntity is { } uid)
|
||||
|
||||
@@ -15,12 +15,6 @@ public sealed partial class ScramImplantComponent : Component
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public float TeleportRadius = 100f;
|
||||
|
||||
/// <summary>
|
||||
/// How many times to check for a valid tile to teleport to
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadOnly)]
|
||||
public int TeleportAttempts = 20;
|
||||
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg");
|
||||
}
|
||||
|
||||
@@ -14,13 +14,14 @@ using Content.Shared.Popups;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Random;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Movement.Pulling.Components;
|
||||
using Content.Shared.Movement.Pulling.Systems;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Content.Server.Implants;
|
||||
|
||||
@@ -28,7 +29,6 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
|
||||
{
|
||||
[Dependency] private readonly CuffableSystem _cuffable = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly StoreSystem _store = default!;
|
||||
@@ -37,8 +37,11 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
|
||||
[Dependency] private readonly SharedTransformSystem _xform = default!;
|
||||
[Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
|
||||
[Dependency] private readonly PullingSystem _pullingSystem = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
|
||||
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
|
||||
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
private HashSet<Entity<MapGridComponent>> _targetGrids = [];
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -107,41 +110,92 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
|
||||
_pullingSystem.TryStopPull(ent, pull);
|
||||
|
||||
var xform = Transform(ent);
|
||||
var entityCoords = xform.Coordinates.ToMap(EntityManager, _xform);
|
||||
var targetCoords = SelectRandomTileInRange(xform, implant.TeleportRadius);
|
||||
|
||||
// try to find a valid position to teleport to, teleport to whatever works if we can't
|
||||
var targetCoords = new MapCoordinates();
|
||||
for (var i = 0; i < implant.TeleportAttempts; i++)
|
||||
if (targetCoords != null)
|
||||
{
|
||||
var distance = implant.TeleportRadius * MathF.Sqrt(_random.NextFloat()); // to get an uniform distribution
|
||||
targetCoords = entityCoords.Offset(_random.NextAngle().ToVec() * distance);
|
||||
|
||||
// prefer teleporting to grids
|
||||
if (!_mapManager.TryFindGridAt(targetCoords, out var gridUid, out var grid))
|
||||
continue;
|
||||
|
||||
// the implant user probably does not want to be in your walls
|
||||
var valid = true;
|
||||
foreach (var entity in grid.GetAnchoredEntities(targetCoords))
|
||||
{
|
||||
if (!_physicsQuery.TryGetComponent(entity, out var body))
|
||||
continue;
|
||||
|
||||
if (body.BodyType != BodyType.Static ||
|
||||
!body.Hard ||
|
||||
(body.CollisionLayer & (int) CollisionGroup.Impassable) == 0)
|
||||
continue;
|
||||
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
if (valid)
|
||||
break;
|
||||
_xform.SetCoordinates(ent, targetCoords.Value);
|
||||
_audio.PlayPvs(implant.TeleportSound, ent);
|
||||
args.Handled = true;
|
||||
}
|
||||
_xform.SetWorldPosition(ent, targetCoords.Position);
|
||||
_audio.PlayPvs(implant.TeleportSound, ent);
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
private EntityCoordinates? SelectRandomTileInRange(TransformComponent userXform, float radius)
|
||||
{
|
||||
var userCoords = userXform.Coordinates.ToMap(EntityManager, _xform);
|
||||
_targetGrids.Clear();
|
||||
_lookupSystem.GetEntitiesInRange(userCoords, radius, _targetGrids);
|
||||
Entity<MapGridComponent>? targetGrid = null;
|
||||
|
||||
if (_targetGrids.Count == 0)
|
||||
return null;
|
||||
|
||||
// Give preference to the grid the entity is currently on.
|
||||
// This does not guarantee that if the probability fails that the owner's grid won't be picked.
|
||||
// In reality the probability is higher and depends on the number of grids.
|
||||
if (userXform.GridUid != null && TryComp<MapGridComponent>(userXform.GridUid, out var gridComp))
|
||||
{
|
||||
var userGrid = new Entity<MapGridComponent>(userXform.GridUid.Value, gridComp);
|
||||
if (_random.Prob(0.5f))
|
||||
{
|
||||
_targetGrids.Remove(userGrid);
|
||||
targetGrid = userGrid;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetGrid == null)
|
||||
targetGrid = _random.GetRandom().PickAndTake(_targetGrids);
|
||||
|
||||
EntityCoordinates? targetCoords = null;
|
||||
|
||||
do
|
||||
{
|
||||
var valid = false;
|
||||
|
||||
var range = (float) Math.Sqrt(radius);
|
||||
var box = Box2.CenteredAround(userCoords.Position, new Vector2(range, range));
|
||||
var tilesInRange = _mapSystem.GetTilesEnumerator(targetGrid.Value.Owner, targetGrid.Value.Comp, box, false);
|
||||
var tileList = new ValueList<Vector2i>();
|
||||
|
||||
while (tilesInRange.MoveNext(out var tile))
|
||||
{
|
||||
tileList.Add(tile.GridIndices);
|
||||
}
|
||||
|
||||
while (tileList.Count != 0)
|
||||
{
|
||||
var tile = tileList.RemoveSwap(_random.Next(tileList.Count));
|
||||
valid = true;
|
||||
foreach (var entity in _mapSystem.GetAnchoredEntities(targetGrid.Value.Owner, targetGrid.Value.Comp,
|
||||
tile))
|
||||
{
|
||||
if (!_physicsQuery.TryGetComponent(entity, out var body))
|
||||
continue;
|
||||
|
||||
if (body.BodyType != BodyType.Static ||
|
||||
!body.Hard ||
|
||||
(body.CollisionLayer & (int) CollisionGroup.MobMask) == 0)
|
||||
continue;
|
||||
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (valid)
|
||||
{
|
||||
targetCoords = new EntityCoordinates(targetGrid.Value.Owner,
|
||||
_mapSystem.TileCenterToVector(targetGrid.Value, tile));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (valid || _targetGrids.Count == 0) // if we don't do the check here then PickAndTake will blow up on an empty set.
|
||||
break;
|
||||
|
||||
targetGrid = _random.GetRandom().PickAndTake(_targetGrids);
|
||||
} while (true);
|
||||
|
||||
return targetCoords;
|
||||
}
|
||||
|
||||
private void OnDnaScramblerImplant(EntityUid uid, SubdermalImplantComponent component, UseDnaScramblerImplantEvent args)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Glue;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Lube;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Stack;
|
||||
@@ -10,11 +8,13 @@ using Content.Server.Wires;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Materials;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -14,6 +14,7 @@ using Content.Shared.Inventory;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.NPC.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Tools.Systems;
|
||||
using Content.Shared.Weapons.Melee;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace Content.Server.NodeContainer.EntitySystems
|
||||
&& ent.Comp.Nodes.TryGetValue(id2, out var n2)
|
||||
&& n2 is T2 t2
|
||||
&& ent.Comp.Nodes.TryGetValue(id3, out var n3)
|
||||
&& n2 is T3 t3)
|
||||
&& n3 is T3 t3)
|
||||
{
|
||||
node1 = t1;
|
||||
node2 = t2;
|
||||
|
||||
@@ -24,6 +24,7 @@ using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Audio;
|
||||
@@ -410,6 +411,10 @@ public sealed class DrinkSystem : EntitySystem
|
||||
!_body.TryGetBodyOrganComponents<StomachComponent>(ev.User, out var stomachs, body))
|
||||
return;
|
||||
|
||||
// Make sure the solution exists
|
||||
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var solution))
|
||||
return;
|
||||
|
||||
// no drinking from living drinks, have to kill them first.
|
||||
if (_mobState.IsAlive(entity))
|
||||
return;
|
||||
|
||||
@@ -23,6 +23,7 @@ using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Stacks;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Verbs;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
|
||||
namespace Content.Server.Nutrition.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Provides API for openable food and drinks, handles opening on use and preventing transfer when closed.
|
||||
/// </summary>
|
||||
public sealed class OpenableSystem : SharedOpenableSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<OpenableComponent, SolutionTransferAttemptEvent>(OnTransferAttempt);
|
||||
}
|
||||
|
||||
private void OnTransferAttempt(EntityUid uid, OpenableComponent comp, SolutionTransferAttemptEvent args)
|
||||
{
|
||||
if (!comp.Opened)
|
||||
{
|
||||
// message says its just for drinks, shouldn't matter since you typically dont have a food that is openable and can be poured out
|
||||
args.Cancel(Loc.GetString("drink-component-try-use-drink-not-open", ("owner", uid)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Paint;
|
||||
using Content.Shared.Sprite;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Content.Shared.Humanoid;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Shared.Verbs;
|
||||
using Content.Shared.SubFloor;
|
||||
using Content.Server.Nutrition.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
|
||||
namespace Content.Server.Paint;
|
||||
|
||||
/// <summary>
|
||||
/// Colors target and consumes reagent on each color success.
|
||||
/// </summary>
|
||||
public sealed class PaintSystem : SharedPaintSystem
|
||||
{
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly OpenableSystem _openable = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PaintComponent, AfterInteractEvent>(OnInteract);
|
||||
SubscribeLocalEvent<PaintComponent, PaintDoAfterEvent>(OnPaint);
|
||||
SubscribeLocalEvent<PaintComponent, GetVerbsEvent<UtilityVerb>>(OnPaintVerb);
|
||||
}
|
||||
|
||||
private void OnInteract(EntityUid uid, PaintComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (!args.CanReach)
|
||||
return;
|
||||
|
||||
if (args.Target is not { Valid: true } target)
|
||||
return;
|
||||
|
||||
PrepPaint(uid, component, target, args.User);
|
||||
}
|
||||
|
||||
private void OnPaintVerb(EntityUid uid, PaintComponent component, GetVerbsEvent<UtilityVerb> args)
|
||||
{
|
||||
if (!args.CanInteract || !args.CanAccess)
|
||||
return;
|
||||
|
||||
var paintText = Loc.GetString("paint-verb");
|
||||
|
||||
var verb = new UtilityVerb()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
PrepPaint(uid, component, args.Target, args.User);
|
||||
},
|
||||
|
||||
Text = paintText,
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/paint.svg.192dpi.png"))
|
||||
};
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
private void PrepPaint(EntityUid uid, PaintComponent component, EntityUid target, EntityUid user)
|
||||
{
|
||||
|
||||
var doAfterEventArgs = new DoAfterArgs(EntityManager, user, component.Delay, new PaintDoAfterEvent(), uid, target: target, used: uid)
|
||||
{
|
||||
BreakOnMove = true,
|
||||
NeedHand = true,
|
||||
BreakOnHandChange = true
|
||||
};
|
||||
|
||||
_doAfterSystem.TryStartDoAfter(doAfterEventArgs);
|
||||
}
|
||||
|
||||
private void OnPaint(Entity<PaintComponent> entity, ref PaintDoAfterEvent args)
|
||||
{
|
||||
if (args.Target == null || args.Used == null)
|
||||
return;
|
||||
|
||||
if (args.Handled || args.Cancelled)
|
||||
return;
|
||||
|
||||
if (args.Target is not { Valid: true } target)
|
||||
return;
|
||||
|
||||
if (!_openable.IsOpen(entity))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("paint-closed", ("used", args.Used)), args.User, args.User, PopupType.Medium);
|
||||
return;
|
||||
}
|
||||
|
||||
if (HasComp<PaintedComponent>(target) || HasComp<RandomSpriteComponent>(target))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("paint-failure-painted", ("target", args.Target)), args.User, args.User, PopupType.Medium);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entity.Comp.Blacklist?.IsValid(target, EntityManager) != true || HasComp<HumanoidAppearanceComponent>(target) || HasComp<SubFloorHideComponent>(target))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("paint-failure", ("target", args.Target)), args.User, args.User, PopupType.Medium);
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryPaint(entity, target))
|
||||
{
|
||||
EnsureComp<PaintedComponent>(target, out PaintedComponent? paint);
|
||||
EnsureComp<AppearanceComponent>(target);
|
||||
|
||||
paint.Color = entity.Comp.Color; // set the target color to the color specified in the spray paint yml.
|
||||
_audio.PlayPvs(entity.Comp.Spray, entity);
|
||||
paint.Enabled = true;
|
||||
|
||||
if (HasComp<InventoryComponent>(target)) // Paint any clothing the target is wearing.
|
||||
{
|
||||
if (_inventory.TryGetSlots(target, out var slotDefinitions))
|
||||
{
|
||||
foreach (var slot in slotDefinitions)
|
||||
{
|
||||
if (!_inventory.TryGetSlotEntity(target, slot.Name, out var slotEnt))
|
||||
continue;
|
||||
|
||||
if (HasComp<PaintedComponent>(slotEnt.Value) || !entity.Comp.Blacklist?.IsValid(slotEnt.Value,
|
||||
EntityManager) != true
|
||||
|| HasComp<RandomSpriteComponent>(slotEnt.Value) ||
|
||||
HasComp<HumanoidAppearanceComponent>(
|
||||
slotEnt.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EnsureComp<PaintedComponent>(slotEnt.Value, out PaintedComponent? slotpaint);
|
||||
EnsureComp<AppearanceComponent>(slotEnt.Value);
|
||||
slotpaint.Color = entity.Comp.Color;
|
||||
_appearanceSystem.SetData(slotEnt.Value, PaintVisuals.Painted, true);
|
||||
Dirty(slotEnt.Value, slotpaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("paint-success", ("target", args.Target)), args.User, args.User, PopupType.Medium);
|
||||
_appearanceSystem.SetData(target, PaintVisuals.Painted, true);
|
||||
Dirty(target, paint);
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryPaint(entity, target))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("paint-empty", ("used", args.Used)), args.User, args.User, PopupType.Medium);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryPaint(Entity<PaintComponent> reagent, EntityUid target)
|
||||
{
|
||||
if (HasComp<HumanoidAppearanceComponent>(target) || HasComp<SubFloorHideComponent>(target))
|
||||
return false;
|
||||
|
||||
if (_solutionContainer.TryGetSolution(reagent.Owner, reagent.Comp.Solution, out _, out var solution))
|
||||
{
|
||||
var quantity = solution.RemoveReagent(reagent.Comp.Reagent, reagent.Comp.ConsumptionUnit);
|
||||
if (quantity > 0)// checks quantity of solution is more than 0.
|
||||
return true;
|
||||
|
||||
if (quantity < 1)
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
14
Content.Server/Paper/PaperRandomStoryComponent.cs
Normal file
14
Content.Server/Paper/PaperRandomStoryComponent.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Content.Server.Paper;
|
||||
|
||||
/// <summary>
|
||||
/// Adds randomly generated stories to Paper component
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(PaperRandomStorySystem))]
|
||||
public sealed partial class PaperRandomStoryComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public List<string>? StorySegments;
|
||||
|
||||
[DataField]
|
||||
public string StorySeparator = " ";
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user