From 0269efcda3531b8668d513913bee21c4a84c2f11 Mon Sep 17 00:00:00 2001 From: SimpleStation14 <130339894+SimpleStation14@users.noreply.github.com> Date: Sat, 29 Jun 2024 15:46:53 -0700 Subject: [PATCH 01/56] Mirror: Throwing Knives: Syndicate Kit (#310) ## Mirror of PR #26026: [Throwing Knives: Syndicate Kit](https://github.com/space-wizards/space-station-14/pull/26026) from space-wizards [space-wizards](https://github.com/space-wizards)/[space-station-14](https://github.com/space-wizards/space-station-14) ###### `393bcbfc1346589075ad960473685c78bdbf46e5` PR opened by UbaserB at 2024-03-12 06:57:41 UTC --- PR changed 13 files with 101 additions and 8 deletions. The PR had the following labels: - No C# - Changes: Sprites ---

Original Body

> > > > ## About the PR > > > This PR adds throwing knives and a syndicate bundle in the bundle category for 6 TC which comes with 4 throwing knives in a box. Each knife does 10 slash + 15 pierce damage on throw, and 5 flash per hit (10 DPS). This makes it better than a kitchen knife, but worse than a combat knife, which is what security is fitted with. > > ## Why / Balance > > > This kit is built for weakening an enemy from a distance (from chasing, etc) while being able to fight back if cornered into a 1v1 CQC fight. Sure, you can kill several people with the kit; hence why it's so expensive, but there are drawbacks. Firstly, once it embeds into a target they are able to pull it out. Secondly, when you are pulling them out of a target, YOU are vulnerable. Thirdly, if you miss your hits the target will probably not die. Good things you have four knives and can stab them to death.. better than punches! > > ## Media > > > - [X] I have added screenshots/videos to this PR showcasing its changes ingame, **or** this PR does not require an ingame showcase > > **It embeds and kills in 4 throws on an unarmoured target.** > ![image](https://github.com/space-wizards/space-station-14/assets/134914314/05f4112d-a08c-4bde-9b34-8a5ecde0b270) > > **Item description.** > ![image](https://github.com/space-wizards/space-station-14/assets/134914314/87704a6e-8501-4f19-a158-bf89d6b4a83e) > > **4 knives.** > ![image](https://github.com/space-wizards/space-station-14/assets/134914314/44dc4c40-add3-46be-b224-b71e2f14f1e9) > > **Kit on uplink.** > [IMG NEEDS UPDATING] > > **Changelog** > > :cl: Ubaser > - add: You can now purchase a set of 4 throwing knives in the uplink as a bundle for 12 TC.
--------- Co-authored-by: SimpleStation14 Co-authored-by: VMSolidus --- .../Locale/en-US/store/uplink-catalog.ftl | 3 ++ .../Catalog/Fills/Boxes/syndicate.yml | 37 +++++++++++++----- .../Prototypes/Catalog/uplink_catalog.yml | 11 ++++++ .../Entities/Objects/Weapons/Melee/knife.yml | 29 ++++++++++++++ .../Objects/Storage/boxes.rsi/meta.json | 17 ++++---- .../Storage/boxes.rsi/throwing_knives.png | Bin 0 -> 1141 bytes .../Objects/Storage/boxicons.rsi/meta.json | 5 ++- .../Storage/boxicons.rsi/throwing_knives.png | Bin 0 -> 1392 bytes .../throwing_knife.rsi/equipped-BELT.png | Bin 0 -> 1289 bytes .../Weapons/Melee/throwing_knife.rsi/icon.png | Bin 0 -> 1593 bytes .../Melee/throwing_knife.rsi/inhand-left.png | Bin 0 -> 1296 bytes .../Melee/throwing_knife.rsi/inhand-right.png | Bin 0 -> 1298 bytes .../Melee/throwing_knife.rsi/meta.json | 26 ++++++++++++ 13 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 Resources/Textures/Objects/Storage/boxes.rsi/throwing_knives.png create mode 100644 Resources/Textures/Objects/Storage/boxicons.rsi/throwing_knives.png create mode 100644 Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/equipped-BELT.png create mode 100644 Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/icon.png create mode 100644 Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/inhand-left.png create mode 100644 Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/inhand-right.png create mode 100644 Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/meta.json diff --git a/Resources/Locale/en-US/store/uplink-catalog.ftl b/Resources/Locale/en-US/store/uplink-catalog.ftl index 70eb998bb40..4836a57d6b1 100644 --- a/Resources/Locale/en-US/store/uplink-catalog.ftl +++ b/Resources/Locale/en-US/store/uplink-catalog.ftl @@ -201,6 +201,9 @@ uplink-decoy-kit-desc = State-of-the-art distraction technology straight from RN uplink-chemistry-kit-name = Chemical Synthesis Kit uplink-chemistry-kit-desc = A starter kit for the aspiring chemist, includes toxin and vestine for all your criminal needs! +uplink-knives-kit-name = Throwing Knives Kit +uplink-knives-kit-desc = A set of 4 syndicate branded throwing knives, perfect for embedding into the body of your victims. + uplink-meds-bundle-name = Medical Bundle uplink-meds-bundle-desc = All you need to get your comrades back in the fight: mainly a combat medkit, a defibrillator and three combat medipens. diff --git a/Resources/Prototypes/Catalog/Fills/Boxes/syndicate.yml b/Resources/Prototypes/Catalog/Fills/Boxes/syndicate.yml index 53c526f0339..7b5b05a49a5 100644 --- a/Resources/Prototypes/Catalog/Fills/Boxes/syndicate.yml +++ b/Resources/Prototypes/Catalog/Fills/Boxes/syndicate.yml @@ -38,14 +38,31 @@ name: observations kit suffix: Filled components: - - type: StorageFill - contents: - - id: SyndiCrewMonitorEmpty - amount: 1 - - id: PowerCellHigh - amount: 1 - - id: ClothingEyesGlassesHiddenSecurity - amount: 1 - - id: SurveillanceCameraMonitorCircuitboard - amount: 1 + - type: StorageFill + contents: + - id: SyndiCrewMonitorEmpty + amount: 1 + - id: PowerCellHigh + amount: 1 + - id: ClothingEyesGlassesHiddenSecurity + amount: 1 + - id: SurveillanceCameraMonitorCircuitboard + amount: 1 +- type: entity + parent: BoxCardboard + id: ThrowingKnivesKit + name: throwing knives kit + description: A set of 4 syndicate branded throwing knives, perfect for embedding into the body of your victims. + components: + - type: Storage + grid: + - 0,0,3,1 + - type: StorageFill + contents: + - id: ThrowingKnife + amount: 4 + - type: Sprite + layers: + - state: box_of_doom + - state: throwing_knives diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index 1e81cdf2dd1..328ace7ba15 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -65,6 +65,17 @@ categories: - UplinkWeapons +- type: listing + id: UplinkThrowingKnivesKit + name: uplink-knives-kit-name + description: uplink-knives-kit-desc + icon: { sprite: /Textures/Objects/Storage/boxicons.rsi, state: throwing_knives } + productEntity: ThrowingKnivesKit + cost: + Telecrystal: 6 + categories: + - UplinkWeapons + - type: listing id: UplinkGlovesNorthStar name: uplink-gloves-north-star-name diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml index b5d597715aa..03654061ced 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml @@ -252,3 +252,32 @@ sprite: Objects/Weapons/Melee/uranium_shiv.rsi - type: Sprite sprite: Objects/Weapons/Melee/uranium_shiv.rsi + +- type: entity + name: throwing knife + parent: BaseKnife + id: ThrowingKnife + description: This bloodred knife is very aerodynamic and easy to throw, but good luck trying to fight someone hand-to-hand. + components: + - type: Tag + tags: + - CombatKnife + - Knife + - type: Sprite + sprite: Objects/Weapons/Melee/throwing_knife.rsi + state: icon + - type: MeleeWeapon + wideAnimationRotation: -135 + attackRate: 2 + damage: + types: + Slash: 5 + - type: EmbeddableProjectile + sound: /Audio/Weapons/star_hit.ogg + - type: DamageOtherOnHit + damage: + types: + Slash: 10 + Piercing: 15 + - type: Item + sprite: Objects/Weapons/Melee/throwing_knife.rsi diff --git a/Resources/Textures/Objects/Storage/boxes.rsi/meta.json b/Resources/Textures/Objects/Storage/boxes.rsi/meta.json index 23868a906f4..53ac39b639b 100644 --- a/Resources/Textures/Objects/Storage/boxes.rsi/meta.json +++ b/Resources/Textures/Objects/Storage/boxes.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/cc65477c04f7403ca8a457bd5bae69a01abadbf0, encryptokey was taken from Baystation12 at https://github.com/infinitystation/Baystation12/blob/073f678cdce92edb8fcd55f9ffc9f0523bf31506/icons/obj/radio.dmi and modified by lapatison. boxwidetoy, shelltoy, swab, flare, inflatable, trashbag, magazine, holo and forensic created by potato1234x (github) for ss14 based on toys.rsi, mouth_swab.rsi, flare.rsi, inflatable_wall.rsi, trashbag.rsi, caseless_pistol_mag.rsi, guardians.rsi and bureaucracy.rsi respectively, candle and darts created by TheShuEd for ss14, vials was drawn by Ubaser, evidence_markers by moomoobeef.", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/cc65477c04f7403ca8a457bd5bae69a01abadbf0, encryptokey was taken from Baystation12 at https://github.com/infinitystation/Baystation12/blob/073f678cdce92edb8fcd55f9ffc9f0523bf31506/icons/obj/radio.dmi and modified by lapatison. boxwidetoy, shelltoy, swab, flare, inflatable, trashbag, magazine, holo and forensic created by potato1234x (github) for ss14 based on toys.rsi, mouth_swab.rsi, flare.rsi, inflatable_wall.rsi, trashbag.rsi, caseless_pistol_mag.rsi, guardians.rsi and bureaucracy.rsi respectively, candle and darts created by TheShuEd for ss14, throwing_knives and vials was drawn by Ubaser, evidence_markers by moomoobeef.", "size": { "x": 32, "y": 32 @@ -35,7 +35,7 @@ "name": "sechud" }, { - "name": "bottle" + "name": "bottle" }, { "name": "box" @@ -142,6 +142,9 @@ { "name": "syringe" }, + { + "name": "throwing_knives" + }, { "name": "trashbag" }, @@ -152,12 +155,12 @@ "name": "writing_of_doom" }, { - "name": "headset" - }, + "name": "headset" + }, { - "name": "encryptokey" - }, - { + "name": "encryptokey" + }, + { "name": "inhand-left", "directions": 4 }, diff --git a/Resources/Textures/Objects/Storage/boxes.rsi/throwing_knives.png b/Resources/Textures/Objects/Storage/boxes.rsi/throwing_knives.png new file mode 100644 index 0000000000000000000000000000000000000000..834410a43ef64bd0f3dbfd9d852247d464cbd5f0 GIT binary patch literal 1141 zcmZ`&O=#0#7*2JTtvC;YZit60f{2)WZB~X2X<#$^3EEL=g=`16Hc7XzG+#)*+BgSJ z4^u%!9HT)<=$0%*`di&DU%@%;&UsW-gtBTI$qnX zp%I554$ONq(!q5-B8aU>&&x>89OD#NW(q1SYK&X9d=&3;_*m9Zfpp7;>2PkE@-;Yo z_QZf9eHAp7rsPbPJOXWv>|x?eoDyOrN%FQ@;&S3Z2*h~Qs5XK)Ri);~s@R6_ zMOA=iXl2br4oENw&8P z^E>(cRx~-Ey}kOY>*2{yt7|vq_uqb=K@7Mycl6Mok8_dE*+}P;Zi~1#+Hs@h!h`!> N%S-*k;zHl}`F|_-V#WXf literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Storage/boxicons.rsi/meta.json b/Resources/Textures/Objects/Storage/boxicons.rsi/meta.json index 858fc7c4e54..935b0b9f8b3 100644 --- a/Resources/Textures/Objects/Storage/boxicons.rsi/meta.json +++ b/Resources/Textures/Objects/Storage/boxicons.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from baystation at https://github.com/Baystation12/Baystation12/commit/bc9fbb1722530596e3aa7522ee407280b323ad43, vials drawn by Ubaser, tracks made by Fazansen(https://github.com/Fazansen).", + "copyright": "Taken from baystation at https://github.com/Baystation12/Baystation12/commit/bc9fbb1722530596e3aa7522ee407280b323ad43, throwing_knives and vials are drawn by Ubaser, tracks made by Fazansen(https://github.com/Fazansen).", "size": { "x": 32, "y": 32 @@ -76,6 +76,9 @@ { "name": "syringe" }, + { + "name": "throwing_knives" + }, { "name": "ziptie" }, diff --git a/Resources/Textures/Objects/Storage/boxicons.rsi/throwing_knives.png b/Resources/Textures/Objects/Storage/boxicons.rsi/throwing_knives.png new file mode 100644 index 0000000000000000000000000000000000000000..b2af7bce88436ed9b060cec8bd6f97019537caa9 GIT binary patch literal 1392 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}e5nzhX}-P; zT0k}j5QD&_;K@Lev%n*=n1MlK76>znTPbd0U|=rE42dX-@b$4u&d=3LOvz75)vL%Y z0PC`;umUo3Q%e#RDspr3imfVamB1>jfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuGfu4bq z9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)BtkRH0j3 znOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXG${Mo`TY%9I!1Z z$@-}|sky0nCB^!NdWQPg^p#|$AzYYO3=Ixo!03ZyfZ7bOYV#~8Nj3q7lxqdhJy8Dv z9hwZbx40xlA4!3}k%57Qu7Q!Rk)=M|e?aHkq$FFFWR~Qlf&&ijA8-gd=9Hj{g4Bb8 zASV+PvQ{~XdFi%F6}l;@X^EvdB}#Tl`4#c`Y4Iuf`MEaw5QV4)Ktxf^wb94o1RIdY zts;X`i}Q0zK|yb4WM*Om4h|GiL?l3TL}1l{EQ+opAS1sdADF0$GLt>?N>Ymoihv0W zVk6W(+$zw`LRcP}msw(G1T_Fn7)cjW1X(#2m6YcfWru(x10^JJs)CvX300(Y3dscE z7y#x1J1!f2c#g8;T5vFZ7clEqd%8G=L~y$v&<~>y})k!b$Ij_*Boec?7c@H{qFghl3r*G!+?2O}8vz5m}+81h%=^c1PKS83O{n;1@QN}0Z7^Xpv`mc7*YUNB{2a+8@u z6VE;{x%R3Bx1%7%JU02a%^lN%F zE%jh-`3kPxCsNfyV%|*TT~Hz7n*90eUlmUe899+_S!k}&6*}S*Quc$Du?mypn?RQoOuZK{=k6;xQ!K#XyN7Ju|PyZWn zMDN`Biup!2tQbXXfBj=p>r*o7QRYk6np)zppMvt=Rf6n1)cS_o?!bBEpWVW zzxWC@_m8>zq*a)GFBhEM{dHDh?X&sHg;q@=(~U%$M(T(8_%FTW^V-_X+1Qs2Nx-^fT8s6w~6 zGOr}DLN~8i8Da>`9GBGM~RsBTId_|A5Z7NlCUU$t=l91qU45Kj08_%qc+?1*r!G zK~5$pWUX=%^U`gVDs)p)(-KQ_N|fv}^D+|iQgn+l(=$qJ^dTxyO@N4^8f>GF#SJzf zpIb!+r55Msl!C(E&dAKf1{@wJqKH_4=!n3o16dSZM?gk?Nj@-D6=f!S=9Q!t6%+we z7{o@XeYjPin}x7EG%vHn&IoD%nlO?sq!_YtEGj9_FUk%9#|COp;#3AT3KFbHDHW0p zz)=9q2XMaQ|Ei{ z!xO@M-(SD!`>3)=h^2Je2J^D5F{d?VMa9-GwO_78j!x3y?Z zAj4G~+4hAyrlqc*dNa%HszigAoqha~oP8H+mOXafayE9a!9u>WGK-y+(MulJo4!

{Sco1*{# literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/icon.png b/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2c61755b52c35b9138c5aa7f93ec6e71bffa7871 GIT binary patch literal 1593 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}e5nzhX}-P; zT0k}j5QD&_;K@Lev%n*=7^vU~2s2LA=96Y%V9v=5i71Ki^|4CM&(%vz$xlkvtH>$0h^0y1+`OA-|-a&z*EttxDlz$&bOY>=?Nk^)#sNw%$0gl~X?bAC~(f|;Iyo`I4b zmx6+VO;JjkRgjAtR6CGotCUevQedU8UtV6WS8lAAUzDzIXlZGwZ(yWvWTXpJp<7&; zSCUwvn^&w1F$89gOKNd)QD#9&W`3Rm$lS!F{L&IzB_)tWZ~$>9$H0x+$q?iKRIuN_L*U?(xRP#^yHq5LKwUA)=^;+UR4k-v;DstH_|# z;{2RaP`KM!n%RJZ0!0uJ0T2xl*fbyuqH74q$S=tUrl+FJWY4^k)S`kSVA=xPX9Tql zw+eK#5O#;=WtP~%3_ue`(uEW^R*pp_<@rU~A>epG1R_o)P-7qgij+Pfi60#Jz|?QY zWup(zNp@V%mxb~$FfjRex;TbJaJ~)o_78Rx`Ind7Qc!9C=1{nZp6(V`<|%VS?rwO| zv`YFX^9e4~PsSA-Axxj#MNAkEdHI&OX76G=)GMZ}=rs8q-}j!mL07V^3U2f6J^AI# zoq03g8>T;Fdis2i^@6E~7#7=l^gJ%xJ+Y%+lXKaHqX+i&-QKcutMlpi*H_-Xt=jRV zd*1~6f=%ZSTsAiTP#JCb>kaF>`@6Xp?f)>h#U}3b>)o#T)52Oly*PYaWOlU5PTu2@ z`>WU4$ImIf^YZ<&n>TcNwn&$7C%_^!e&PWtFDOYqF~P)RrY}dv#vx z+mV|31?|)Bt-W|{n`iU1w>;<0W?wzjc_fLaDOPdysp~DDCN*y^+BSEpyF4%F)yewt zqVAQxxweIq_1K#BTHaROvFh!q{eFz zK3OI`antZn#mM^|`vUTx9X{?q)pV<`M@~&xT=!J%TJAry)DFy+mU5`JKlnhdpM7PB z*G*T;g$Ycu3|n7Vm0iv>{N65lwq#bQ@Z<*>?{c(v2M9F!WqQbu$Pe0yk&>D6*Hq-gd*mBP7>5; hNwHl1#vxGFf@x_avz=0K&TUY!?dj_0vd$@?2>>-FL7@Nu literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/inhand-left.png b/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/inhand-left.png new file mode 100644 index 0000000000000000000000000000000000000000..5988d571dc12ca1c485efc87735bbba3ce635640 GIT binary patch literal 1296 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|_);T0(|mmy zw18|5AO?X;!IOa`XMsm#F$06fED&ZCw^H21z`$IV84^(v;p=0SoS&h?X&sHg;q@=(~U%$M(T(8_%FTW^V-_X+1Qs2Nx-^fT8s6w~6 zGOr}DLN~8i8Da>`9GBGM~RsBTId_|A5Z7NlCUU$t=l91qU45Kj08_%qc+?1*r!G zK~5$pWUX=%^U`gVDs)p)(-KQ_N|fv}^D+|iQgn+l(=$qJ^dTxyO@N4^8f>GF#SJzf zpIb!+r55Msl!C(E&dAKf1{@wJqKH_4=!n3o16dSZM?gk?Nj@-D6=f!S=9Q!t6%+we z7{o@XeYjPin}x7EG%vHn&IoD%nlO?sq!_YtEGj9_FUk%9#|COp;#3AT3KFbHDHW0p zz)=9q2X*mm#I&6k+g^W`6|5prbv@a}`Hz?!tn&ug}I zy!5S_wCuBHzq<U{MO(OH#J#lhTTXtx$E#ub`D;1*mDf3?|1NQ# p_;-Kx%DB?a9Dh3C-ua-+#BaJLkNt{4Ehnfn@^tlcS?83{1OP7tpo#zh literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/inhand-right.png b/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/inhand-right.png new file mode 100644 index 0000000000000000000000000000000000000000..09c015efac56e7935ff49a7488bea47ee21f706a GIT binary patch literal 1298 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|_);T0(|mmy zw18|5AO?X;!IOa`XMsm#F$06fED&ZCw^H21z`$IV84^(v;p=0SoS&h?X&sHg;q@=(~U%$M(T(8_%FTW^V-_X+1Qs2Nx-^fT8s6w~6 zGOr}DLN~8i8Da>`9GBGM~RsBTId_|A5Z7NlCUU$t=l91qU45Kj08_%qc+?1*r!G zK~5$pWUX=%^U`gVDs)p)(-KQ_N|fv}^D+|iQgn+l(=$qJ^dTxyO@N4^8f>GF#SJzf zpIb!+r55Msl!C(E&dAKf1{@wJqKH_4=!n3o16dSZM?gk?Nj@-D6=f!S=9Q!t6%+we z7{o@XeYjPin}x7EG%vHn&IoD%nlO?sq!_YtEGj9_FUk%9#|COp;#3AT3KFbHDHW0p zz)=9q2X1D--Z+E{R|F`Dg_uaXhW6tcgx!1%pi>paAZW9~hvW5@d6(?_2pParSE_c-* z{|Wn+{=e67Ep6>X)9_3GCW~F>Grp7k(%8AgO9|B?RC`-lOEe^_b5>b}$HEbI&63Re#m1*}He%`O5qM s!?yrm-qO5?#!wak2L?tE(OAW5$7GzxZoH}C0;oLlboFyt=akR{01HQ^j{pDw literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/meta.json b/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/meta.json new file mode 100644 index 00000000000..373d2d77701 --- /dev/null +++ b/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Drawn by Ubaser.", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + }, + { + "name": "equipped-BELT", + "directions": 4 + } + ] +} From 6f54ed1b28908264a848c801ae6b86ed27a21734 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Jun 2024 18:26:26 -0700 Subject: [PATCH 02/56] Update Credits (#495) This is an automated Pull Request. This PR updates the GitHub contributors in the credits section. Co-authored-by: SimpleStation Changelogs --- Resources/Credits/GitHub.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Credits/GitHub.txt b/Resources/Credits/GitHub.txt index b5381a4ce29..84796a94662 100644 --- a/Resources/Credits/GitHub.txt +++ b/Resources/Credits/GitHub.txt @@ -1 +1 @@ -0x6273, 2013HORSEMEATSCANDAL, 20kdc, 21Melkuu, 4dplanner, 612git, 778b, Ablankmann, Acruid, actioninja, adamsong, Admiral-Obvious-001, Adrian16199, Aerocrux, Aexxie, africalimedrop, Agoichi, Ahion, AJCM-git, AjexRose, Alekshhh, AlexMorgan3817, AlmondFlour, AlphaQwerty, Altoids1, amylizzle, ancientpower, ArchPigeon, Arendian, arimah, Arteben, AruMoon, as334, AsikKEsel, asperger-sind, aspiringLich, avghdev, AzzyIsNotHere, BananaFlambe, BasedUser, BGare, BingoJohnson-zz, BismarckShuffle, Bixkitts, Blackern5000, Blazeror, Boaz1111, BobdaBiscuit, brainfood1183, Brandon-Huu, Bribrooo, Bright0, brndd, BubblegumBlue, BYONDFuckery, c4llv07e, CaasGit, CakeQ, CaptainSqrBeard, Carbonhell, Carolyn3114, casperr04, CatTheSystem, Centronias, chairbender, Charlese2, Cheackraze, cheesePizza2, Chief-Engineer, chromiumboy, Chronophylos, clement-or, Clyybber, ColdAutumnRain, Colin-Tel, collinlunn, ComicIronic, coolmankid12345, corentt, crazybrain23, creadth, CrigCrag, Crotalus, CrudeWax, CrzyPotato, Cyberboss, d34d10cc, Daemon, daerSeebaer, dahnte, dakamakat, dakimasu, DamianX, DangerRevolution, daniel-cr, Darkenson, DawBla, dch-GH, Deahaka, DEATHB4DEFEAT, DeathCamel58, deathride58, DebugOk, Decappi, deepdarkdepths, Delete69, deltanedas, DeltaV-Bot, DerbyX, DoctorBeard, DogZeroX, dontbetank, Doru991, DoubleRiceEddiedd, DrMelon, DrSmugleaf, drteaspoon420, DTanxxx, DubiousDoggo, Duddino, Dutch-VanDerLinde, Easypoller, eclips_e, EdenTheLiznerd, EEASAS, Efruit, ElectroSR, elthundercloud, Emisse, EmoGarbage404, Endecc, enumerate0, eoineoineoin, ERORR404V1, Errant-4, estacaoespacialpirata, exincore, exp111, Fahasor, FairlySadPanda, ficcialfaint, Fildrance, FillerVK, Fishfish458, Flareguy, FluffiestFloof, FluidRock, FoLoKe, fooberticus, Fortune117, FoxxoTrystan, freeman2651, Fromoriss, FungiFellow, GalacticChimp, gbasood, Geekyhobo, Genkail, Git-Nivrak, github-actions[bot], gituhabu, GNF54, Golinth, GoodWheatley, Gotimanga, graevy, GreyMario, Guess-My-Name, gusxyz, h3half, Hanzdegloker, Hardly3D, harikattar, Hebiman, Henry12116, HerCoyote23, Hmeister-real, HoofedEar, hord-brayden, hubismal, Hugal31, Hyenh, iacore, IamVelcroboy, icekot8, igorsaux, ike709, Illiux, Ilya246, IlyaElDunaev, Injazz, Insineer, IntegerTempest, Interrobang01, IProduceWidgets, ItsMeThom, j-giebel, Jackal298, Jackrost, jamessimo, janekvap, JerryImMouse, Jessetriesagain, jessicamaybe, Jezithyr, jicksaw, JiimBob, JoeHammad1844, joelhed, JohnGinnane, johnku1, joshepvodka, jproads, Jrpl, juliangiebel, JustArt1m, JustCone14, JustinTether, JustinTrotter, KaiShibaa, kalane15, kalanosh, KEEYNy, Keikiru, Kelrak, kerisargit, keronshb, KIBORG04, Killerqu00, KingFroozy, kira-er, Kit0vras, KittenColony, Kmc2000, Ko4ergaPunk, komunre, koteq, Krunklehorn, Kukutis96513, kxvvv, Lamrr, LankLTE, lapatison, Leander-0, leonardo-dabepis, LetterN, Level10Cybermancer, lever1209, LightVillet, liltenhead, LittleBuilderJane, Lomcastar, LordCarve, LordEclipse, LovelyLophi, LudwigVonChesterfield, Lukasz825700516, lunarcomets, luringens, lvvova1, lzimann, lzk228, M3739, MACMAN2003, Macoron, MagnusCrowe, ManelNavola, matthst, Matz05, MehimoNemo, MeltedPixel, MemeProof, Menshin, Mervill, metalgearsloth, mhamsterr, MilenVolf, Minty642, Mirino97, mirrorcult, misandrie, MishaUnity, MisterMecky, Mith-randalf, Moneyl, Moomoobeef, moony, Morb0, Mr0maks, musicmanvr, Myakot, Myctai, N3X15, Nairodian, Naive817, namespace-Memory, NickPowers43, nikthechampiongr, Nimfar11, Nirnael, nmajask, nok-ko, Nopey, notafet, notquitehadouken, noudoit, noverd, nuke-haus, NULL882, OCOtheOmega, OctoRocket, OldDanceJacket, onoira, Owai-Seek, pali6, Pangogie, patrikturi, PaulRitter, Peptide90, peptron1, Phantom-Lily, PHCodes, PixelTheKermit, PJB3005, Plykiya, pofitlo, pointer-to-null, PolterTzi, PoorMansDreams, potato1234x, ProfanedBane, PrPleGoo, ps3moira, Psychpsyo, psykzz, PuroSlavKing, quatre, QuietlyWhisper, qwerltaz, Radosvik, Radrark, Rainbeon, Rainfey, Rane, ravage123321, rbertoche, Redict, RedlineTriad, RednoWCirabrab, RemberBM, RemieRichards, RemTim, rene-descartes2021, RiceMar1244, RieBi, Rinkashikachi, Rockdtben, rolfero, rosieposieeee, Saakra, Samsterious, SaphireLattice, ScalyChimp, scrato, Scribbles0, Serkket, SethLafuente, ShadowCommander, Shadowtheprotogen546, SignalWalker, SimpleStation14, Simyon264, SirDragooon, Sirionaut, siyengar04, Skarletto, Skrauz, Skyedra, SlamBamActionman, slarticodefast, Slava0135, Snowni, snowsignal, SonicHDC, SoulSloth, SpaceManiac, SpeltIncorrectyl, spoogemonster, ssdaniel24, Stealthbomber16, stellar-novas, StrawberryMoses, SweptWasTaken, Szunti, TadJohnson00, takemysoult, TaralGit, Tayrtahn, tday93, TekuNut, TemporalOroboros, tentekal, tgrkzus, thatrandomcanadianguy, TheArturZh, theashtronaut, thedraccx, themias, Theomund, theOperand, TheShuEd, TimrodDX, Titian3, tkdrg, tmtmtl30, tom-leys, tomasalves8, Tomeno, Tornado-Technology, tosatur, Tryded, TsjipTsjip, Tunguso4ka, TurboTrackerss14, Tyler-IN, Tyzemol, UbaserB, UKNOWH, UnicornOnLSD, Uriende, UristMcDorf, Vaaankas, Varen, VasilisThePikachu, veliebm, Veritius, Verslebas, VigersRay, Visne, VMSolidus, volundr-, Voomra, Vordenburg, vulppine, waylon531, weaversam8, Willhelm53, wixoaGit, WlarusFromDaSpace, wrexbe, xRiriq, yathxyz, Ygg01, YotaXP, YuriyKiss, zach-hill, Zandario, Zap527, ZelteHonor, zerorulez, zionnBE, zlodo, ZNixian, ZoldorfTheWizard, Zumorica, Zymem +0x6273, 2013HORSEMEATSCANDAL, 20kdc, 21Melkuu, 4dplanner, 612git, 778b, Ablankmann, Acruid, actioninja, adamsong, Admiral-Obvious-001, Adrian16199, Aerocrux, Aexxie, africalimedrop, Agoichi, Ahion, AJCM-git, AjexRose, Alekshhh, AlexMorgan3817, AlmondFlour, AlphaQwerty, Altoids1, amylizzle, ancientpower, ArchPigeon, Arendian, arimah, Arteben, AruMoon, as334, AsikKEsel, asperger-sind, aspiringLich, avghdev, AzzyIsNotHere, BananaFlambe, BasedUser, BGare, BingoJohnson-zz, BismarckShuffle, Bixkitts, Blackern5000, Blazeror, Boaz1111, BobdaBiscuit, brainfood1183, Brandon-Huu, Bribrooo, Bright0, brndd, BubblegumBlue, BYONDFuckery, c4llv07e, CaasGit, CakeQ, CaptainSqrBeard, Carbonhell, Carolyn3114, casperr04, CatTheSystem, Centronias, chairbender, Charlese2, Cheackraze, cheesePizza2, Chief-Engineer, chromiumboy, Chronophylos, clement-or, Clyybber, ColdAutumnRain, Colin-Tel, collinlunn, ComicIronic, coolmankid12345, corentt, crazybrain23, creadth, CrigCrag, Crotalus, CrudeWax, CrzyPotato, Cyberboss, d34d10cc, Daemon, daerSeebaer, dahnte, dakamakat, dakimasu, DamianX, DangerRevolution, daniel-cr, Darkenson, DawBla, dch-GH, Deahaka, DEATHB4DEFEAT, DeathCamel58, deathride58, DebugOk, Decappi, deepdarkdepths, Delete69, deltanedas, DeltaV-Bot, DerbyX, DoctorBeard, DogZeroX, dontbetank, Doru991, DoubleRiceEddiedd, DrMelon, DrSmugleaf, drteaspoon420, DTanxxx, DubiousDoggo, Duddino, Dutch-VanDerLinde, Easypoller, eclips_e, EdenTheLiznerd, EEASAS, Efruit, ElectroSR, elthundercloud, Emisse, EmoGarbage404, Endecc, enumerate0, eoineoineoin, ERORR404V1, Errant-4, estacaoespacialpirata, exincore, exp111, Fahasor, FairlySadPanda, ficcialfaint, Fildrance, FillerVK, Fishfish458, Flareguy, FluffiestFloof, FluidRock, FoLoKe, fooberticus, Fortune117, FoxxoTrystan, freeman2651, Fromoriss, FungiFellow, GalacticChimp, gbasood, Geekyhobo, Genkail, geraeumig, Git-Nivrak, github-actions[bot], gituhabu, GNF54, Golinth, GoodWheatley, Gotimanga, graevy, GreyMario, Guess-My-Name, gusxyz, h3half, Hanzdegloker, Hardly3D, harikattar, Hebiman, Henry12116, HerCoyote23, Hmeister-real, HoofedEar, hord-brayden, hubismal, Hugal31, Hyenh, iacore, IamVelcroboy, icekot8, igorsaux, ike709, Illiux, Ilya246, IlyaElDunaev, Injazz, Insineer, IntegerTempest, Interrobang01, IProduceWidgets, ItsMeThom, j-giebel, Jackal298, Jackrost, jamessimo, janekvap, JerryImMouse, Jessetriesagain, jessicamaybe, Jezithyr, jicksaw, JiimBob, JoeHammad1844, joelhed, JohnGinnane, johnku1, joshepvodka, jproads, Jrpl, juliangiebel, JustArt1m, JustCone14, JustinTether, JustinTrotter, KaiShibaa, kalane15, kalanosh, KEEYNy, Keikiru, Kelrak, kerisargit, keronshb, KIBORG04, Killerqu00, KingFroozy, kira-er, Kit0vras, KittenColony, Kmc2000, Ko4ergaPunk, komunre, koteq, Krunklehorn, Kukutis96513, kxvvv, Lamrr, LankLTE, lapatison, Leander-0, leonardo-dabepis, LetterN, Level10Cybermancer, lever1209, LightVillet, liltenhead, LittleBuilderJane, Lomcastar, LordCarve, LordEclipse, LovelyLophi, LudwigVonChesterfield, Lukasz825700516, lunarcomets, luringens, lvvova1, lzimann, lzk228, M3739, MACMAN2003, Macoron, MagnusCrowe, ManelNavola, matthst, Matz05, MehimoNemo, MeltedPixel, MemeProof, Menshin, Mervill, metalgearsloth, mhamsterr, MilenVolf, Minty642, Mirino97, mirrorcult, misandrie, MishaUnity, MisterMecky, Mith-randalf, Moneyl, Moomoobeef, moony, Morb0, Mr0maks, musicmanvr, Myakot, Myctai, N3X15, Nairodian, Naive817, namespace-Memory, NickPowers43, nikthechampiongr, Nimfar11, Nirnael, nmajask, nok-ko, Nopey, notafet, notquitehadouken, noudoit, noverd, nuke-haus, NULL882, OCOtheOmega, OctoRocket, OldDanceJacket, onoira, Owai-Seek, pali6, Pangogie, patrikturi, PaulRitter, Peptide90, peptron1, Phantom-Lily, PHCodes, PixelTheKermit, PJB3005, Plykiya, pofitlo, pointer-to-null, PolterTzi, PoorMansDreams, potato1234x, ProfanedBane, PrPleGoo, ps3moira, Psychpsyo, psykzz, PuroSlavKing, quatre, QuietlyWhisper, qwerltaz, Radosvik, Radrark, Rainbeon, Rainfey, Rane, ravage123321, rbertoche, Redict, RedlineTriad, RednoWCirabrab, RemberBM, RemieRichards, RemTim, rene-descartes2021, RiceMar1244, RieBi, Rinkashikachi, Rockdtben, rolfero, rosieposieeee, Saakra, Samsterious, SaphireLattice, ScalyChimp, scrato, Scribbles0, Serkket, SethLafuente, ShadowCommander, Shadowtheprotogen546, SignalWalker, SimpleStation14, Simyon264, SirDragooon, Sirionaut, siyengar04, Skarletto, Skrauz, Skyedra, SlamBamActionman, slarticodefast, Slava0135, Snowni, snowsignal, SonicHDC, SoulSloth, SpaceManiac, SpeltIncorrectyl, spoogemonster, ssdaniel24, Stealthbomber16, stellar-novas, StrawberryMoses, SweptWasTaken, Szunti, TadJohnson00, takemysoult, TaralGit, Tayrtahn, tday93, TekuNut, TemporalOroboros, tentekal, tgrkzus, thatrandomcanadianguy, TheArturZh, theashtronaut, thedraccx, themias, Theomund, theOperand, TheShuEd, TimrodDX, Titian3, tkdrg, tmtmtl30, tom-leys, tomasalves8, Tomeno, Tornado-Technology, tosatur, Tryded, TsjipTsjip, Tunguso4ka, TurboTrackerss14, Tyler-IN, Tyzemol, UbaserB, UKNOWH, UnicornOnLSD, Uriende, UristMcDorf, Vaaankas, Varen, VasilisThePikachu, veliebm, Veritius, Verslebas, VigersRay, Visne, VMSolidus, volundr-, Voomra, Vordenburg, vulppine, waylon531, weaversam8, Willhelm53, wixoaGit, WlarusFromDaSpace, wrexbe, xRiriq, yathxyz, Ygg01, YotaXP, YuriyKiss, zach-hill, Zandario, Zap527, ZelteHonor, zerorulez, zionnBE, zlodo, ZNixian, ZoldorfTheWizard, Zumorica, Zymem From f439eb70b897cd2f1424b1eed778b8a804c8c948 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Sun, 30 Jun 2024 21:22:28 +0300 Subject: [PATCH 03/56] Add Equip Delays to Clothing (#499) # Description For centuries SS14 had equip and unequip delays on the clothing component, and yet that feature remained unused and unheard of, not even nyanotrasen/deltav used it when designing shock collars and headcages, whose descriptions clearly suggested they should take time to take off. For centuries salvage specialists could safely swap suits in space. For centuries you could accidentally unequip your eva suit mid-spacewalk and die. Now the time has come. The time when we change it. This adds equip and unequip delays to the base clothing item. The currently chosen time is half a second - it's meant to not be too annoying, but at the same time prevent people from being able to instantly swap clothes, headsets, other things with a single click. EVA suits take 1.5 seconds to equip and 1 second to take off, so to swap EVA suits, you will have to expose your body to the dangers of space for at least 1.5 seconds. For hardsuits, both values are increased to 2.5 seconds. The values are not final and this PR will probably need polishing before it can be merged - for example, while recording the second preview video, I discovered that jetpacks do not inherit from base clothing and thus do not inherit the delays. There's probably way more such items. --- # TODO Add equip/unequip delays to: - [X] most basic clothing items - [X] hardsuits/softsuits - [X] special items (currently headcages and shock collars, possibly more later?) - [ ] Everything that was missed by the above ---

Media

Basics https://github.com/Simple-Station/Einstein-Engines/assets/69920617/3fc900b8-ee13-4968-bf5d-cddeb9a141b6 Hardsuits, eva suits, shock collars, headcages + demonstration that aghost stripping is unaffected. https://github.com/Simple-Station/Einstein-Engines/assets/69920617/a536578f-2ac3-40e1-9b27-3b167e006397

--- # Changelog :cl: - add: Most items now take time to equip and unequip, especially space suits. --- .../Entities/Clothing/OuterClothing/base_clothingouter.yml | 6 ++++++ Resources/Prototypes/Entities/Clothing/base_clothing.yml | 3 +++ .../Prototypes/Nyanotrasen/Entities/Clothing/Head/hats.yml | 2 ++ .../Nyanotrasen/Entities/Objects/Devices/shock_collar.yml | 2 ++ 4 files changed, 13 insertions(+) diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml index 13524efa9e6..902c57418e4 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml @@ -93,6 +93,9 @@ - Hardsuit - WhitelistChameleon - HidesHarpyWings #DeltaV: Used by harpies to help render their hardsuit sprites + - type: Clothing + equipDelay: 2.5 # Hardsuits are heavy and take a while to put on/off. + unequipDelay: 2.5 - type: entity abstract: true @@ -114,6 +117,9 @@ - type: Tag tags: - HidesHarpyWings #DeltaV: Used by harpies to help render their hardsuit sprites + - type: Clothing + equipDelay: 1.25 # Softsuits are easier to put on and off + unequipDelay: 1 - type: entity parent: ClothingOuterBase diff --git a/Resources/Prototypes/Entities/Clothing/base_clothing.yml b/Resources/Prototypes/Entities/Clothing/base_clothing.yml index 92a698dd301..810ada5429d 100644 --- a/Resources/Prototypes/Entities/Clothing/base_clothing.yml +++ b/Resources/Prototypes/Entities/Clothing/base_clothing.yml @@ -11,6 +11,9 @@ - WhitelistChameleon - type: StaticPrice price: 15 + - type: Clothing + equipDelay: 0.5 + unequipDelay: 0.5 - type: entity abstract: true diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Clothing/Head/hats.yml b/Resources/Prototypes/Nyanotrasen/Entities/Clothing/Head/hats.yml index 8278114d267..2cd9785d989 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Clothing/Head/hats.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Clothing/Head/hats.yml @@ -99,6 +99,8 @@ sprite: Nyanotrasen/Clothing/Head/Hats/cage.rsi - type: Clothing sprite: Nyanotrasen/Clothing/Head/Hats/cage.rsi + equipDelay: 0.5 + unequipDelay: 6 - type: HeadCage - type: entity diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml index 35cdcae6589..1266a721fe2 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml @@ -8,6 +8,8 @@ sprite: Nyanotrasen/Clothing/Neck/Misc/shock.rsi - type: Clothing sprite: Nyanotrasen/Clothing/Neck/Misc/shock.rsi + equipDelay: 1 + unequipDelay: 10 # It's a collar meant to be used on prisoners (or not), so it probably has some sort of safety. - type: ShockCollar - type: UseDelay delay: 3 # DeltaV: prevent clocks instakilling people From 7c83c8b30c016b4f457d8d7398c282d9cba5a0da Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Sun, 30 Jun 2024 18:22:48 +0000 Subject: [PATCH 04/56] Automatic Changelog Update (#499) --- Resources/Changelog/Changelog.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 3aea1324fe4..6fe7b0ff689 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4251,3 +4251,9 @@ Entries: message: 'Height and Width sliders have been added to character creation. ' id: 6131 time: '2024-06-27T17:46:51.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Add + message: Most items now take time to equip and unequip, especially space suits. + id: 6132 + time: '2024-06-30T18:22:28.0000000+00:00' From 89a6bb3ab5897da04c0526c32ff408b90bfc70fc Mon Sep 17 00:00:00 2001 From: SimpleStation14 <130339894+SimpleStation14@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:37:45 -0700 Subject: [PATCH 05/56] Mirror: StrippableSystem doafter overhaul (#205) ## Mirror of PR #25994: [StrippableSystem doafter overhaul](https://github.com/space-wizards/space-station-14/pull/25994) from space-wizards [space-wizards](https://github.com/space-wizards)/[space-station-14](https://github.com/space-wizards/space-station-14) ###### `41ca8f3dfcb986432e1e509247bf239cac137836` PR opened by Krunklehorn at 2024-03-11 12:36:28 UTC --- PR changed 7 files with 465 additions and 305 deletions. The PR had the following labels: - Status: Needs Review ---

Original Body

> ## About the PR > > Refactors Strippable DoAfter events to make them synchronous and organized. > > > ## Technical details > > ### Strippable System & Component > - Synchronous DoAfters > - Made use of `TimeSpan`, `GetStripTimeModifiers()` and `ByRefEvent` > - Reorganized checks, removed some redundant ones > - Resolve pattern where useful > - Added more asserts > - Lots of cleanup > > The DoAfters were grouped under one event to avoid copy-pasting eight separate cancel checks, asserts and function signatures. > > Let me know if this is bad for performance and I'll roll them out instead. > > > ## Media > > - [x] I have added screenshots/videos to this PR showcasing its changes ingame, **or** this PR does not require an ingame showcase > > > ## Breaking changes > > ### TimeSpans > `ThievingComponent`, `InventoryTemplatePrototype` and `ToggleableClothingSystem` use `TimeSpan` in places where they intersect with `StrippableComponent`. > > > **Changelog** > > N/A >
Signed-off-by: VMSolidus Co-authored-by: SimpleStation14 Co-authored-by: VMSolidus --- Content.Server/Strip/StrippableSystem.cs | 676 +++++++++++------- .../EntitySystems/ToggleableClothingSystem.cs | 2 +- .../Inventory/InventoryTemplatePrototype.cs | 2 +- .../Strip/Components/StrippableComponent.cs | 81 +-- .../Strip/Components/ThievingComponent.cs | 2 +- .../Strip/SharedStrippableSystem.cs | 6 +- Content.Shared/Strip/ThievingSystem.cs | 1 + 7 files changed, 465 insertions(+), 305 deletions(-) diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs index 96b2ecc00c6..950411a8e2c 100644 --- a/Content.Server/Strip/StrippableSystem.cs +++ b/Content.Server/Strip/StrippableSystem.cs @@ -1,4 +1,3 @@ -using System.Linq; using Content.Server.Administration.Logs; using Content.Server.Ensnaring; using Content.Shared.CombatMode; @@ -21,18 +20,21 @@ using Robust.Server.GameObjects; using Robust.Shared.Player; using Robust.Shared.Utility; +using System.Linq; namespace Content.Server.Strip { public sealed class StrippableSystem : SharedStrippableSystem { - [Dependency] private readonly SharedCuffableSystem _cuffable = default!; - [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly InventorySystem _inventorySystem = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly EnsnareableSystem _ensnaring = default!; + [Dependency] private readonly EnsnareableSystem _ensnaringSystem = default!; [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; + + [Dependency] private readonly SharedCuffableSystem _cuffableSystem = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; // TODO: ECS popups. Not all of these have ECS equivalents yet. @@ -48,64 +50,58 @@ public override void Initialize() // BUI SubscribeLocalEvent(OnStripButtonPressed); SubscribeLocalEvent(OnStripEnsnareMessage); + + // DoAfters + SubscribeLocalEvent>(OnStrippableDoAfterRunning); + SubscribeLocalEvent(OnStrippableDoAfterFinished); } - private void OnStripEnsnareMessage(EntityUid uid, EnsnareableComponent component, StrippingEnsnareButtonPressed args) + private void AddStripVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent args) { - if (args.Session.AttachedEntity is not {Valid: true} user) + if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User) return; - foreach (var entity in component.Container.ContainedEntities) + if (!HasComp(args.User)) + return; + + Verb verb = new() { - if (!TryComp(entity, out var ensnaring)) - continue; + Text = Loc.GetString("strip-verb-get-data-text"), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")), + Act = () => StartOpeningStripper(args.User, (uid, component), true), + }; - _ensnaring.TryFree(uid, user, entity, ensnaring); - return; - } + args.Verbs.Add(verb); } - private void OnStripButtonPressed(Entity strippable, ref StrippingSlotButtonPressed args) + private void AddStripExamineVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent args) { - if (args.Session.AttachedEntity is not {Valid: true} user || - !TryComp(user, out var userHands)) - return; - - if (args.IsHand) - { - StripHand(user, args.Slot, strippable, userHands); + if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User) return; - } - if (!TryComp(strippable, out var inventory)) + if (!HasComp(args.User)) return; - var hasEnt = _inventorySystem.TryGetSlotEntity(strippable, args.Slot, out var held, inventory); + ExamineVerb verb = new() + { + Text = Loc.GetString("strip-verb-get-data-text"), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")), + Act = () => StartOpeningStripper(args.User, (uid, component), true), + Category = VerbCategory.Examine, + }; - if (userHands.ActiveHandEntity != null && !hasEnt) - PlaceActiveHandItemInInventory(user, strippable, userHands.ActiveHandEntity.Value, args.Slot, strippable); - else if (userHands.ActiveHandEntity == null && hasEnt) - TakeItemFromInventory(user, strippable, held!.Value, args.Slot, strippable); + args.Verbs.Add(verb); } - private void StripHand(EntityUid user, string handId, Entity target, HandsComponent userHands) + private void OnActivateInWorld(EntityUid uid, StrippableComponent component, ActivateInWorldEvent args) { - if (!_handsSystem.TryGetHand(target, handId, out var hand)) + if (args.Target == args.User) return; - // is the target a handcuff? - if (TryComp(hand.HeldEntity, out VirtualItemComponent? virt) - && TryComp(target, out CuffableComponent? cuff) - && _cuffable.GetAllCuffs(cuff).Contains(virt.BlockingEntity)) - { - _cuffable.TryUncuff(target, user, virt.BlockingEntity, cuffable: cuff); + if (!HasComp(args.User)) return; - } - if (userHands.ActiveHandEntity != null && hand.HeldEntity == null) - PlaceActiveHandItemInHands(user, target, userHands.ActiveHandEntity.Value, handId, target); - else if (userHands.ActiveHandEntity == null && hand.HeldEntity != null) - TakeItemFromHands(user, target, hand.HeldEntity.Value, handId, target); + StartOpeningStripper(args.User, (uid, component)); } public override void StartOpeningStripper(EntityUid user, Entity strippable, bool openInCombat = false) @@ -123,352 +119,514 @@ public override void StartOpeningStripper(EntityUid user, Entity args) + private void OnStripButtonPressed(Entity strippable, ref StrippingSlotButtonPressed args) { - if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User) + if (args.Session.AttachedEntity is not { Valid: true } user || + !TryComp(user, out var userHands) || + !TryComp(strippable.Owner, out var targetHands)) return; - if (!HasComp(args.User)) + if (args.IsHand) + { + StripHand((user, userHands), (strippable.Owner, targetHands), args.Slot, strippable); return; + } - Verb verb = new() - { - Text = Loc.GetString("strip-verb-get-data-text"), - Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")), - Act = () => StartOpeningStripper(args.User, (uid, component), true), - }; - args.Verbs.Add(verb); + if (!TryComp(strippable, out var inventory)) + return; + + var hasEnt = _inventorySystem.TryGetSlotEntity(strippable, args.Slot, out var held, inventory); + + if (userHands.ActiveHandEntity != null && !hasEnt) + StartStripInsertInventory((user, userHands), strippable.Owner, userHands.ActiveHandEntity.Value, args.Slot); + else if (userHands.ActiveHandEntity == null && hasEnt) + StartStripRemoveInventory(user, strippable.Owner, held!.Value, args.Slot); } - private void AddStripExamineVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent args) + private void StripHand( + Entity user, + Entity target, + string handId, + StrippableComponent? targetStrippable) { - if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User) + if (!Resolve(user, ref user.Comp) || + !Resolve(target, ref target.Comp) || + !Resolve(target, ref targetStrippable)) return; - if (!HasComp(args.User)) + if (!_handsSystem.TryGetHand(target.Owner, handId, out var handSlot)) return; - ExamineVerb verb = new() + // Is the target a handcuff? + if (TryComp(handSlot.HeldEntity, out var virtualItem) && + TryComp(target.Owner, out var cuffable) && + _cuffableSystem.GetAllCuffs(cuffable).Contains(virtualItem.BlockingEntity)) { - Text = Loc.GetString("strip-verb-get-data-text"), - Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")), - Act = () => StartOpeningStripper(args.User, (uid, component), true), - Category = VerbCategory.Examine, - }; + _cuffableSystem.TryUncuff(target.Owner, user, virtualItem.BlockingEntity, cuffable); + return; + } - args.Verbs.Add(verb); + if (user.Comp.ActiveHandEntity != null && handSlot.HeldEntity == null) + StartStripInsertHand(user, target, user.Comp.ActiveHandEntity.Value, handId, targetStrippable); + else if (user.Comp.ActiveHandEntity == null && handSlot.HeldEntity != null) + StartStripRemoveHand(user, target, handSlot.HeldEntity.Value, handId, targetStrippable); } - private void OnActivateInWorld(EntityUid uid, StrippableComponent component, ActivateInWorldEvent args) + private void OnStripEnsnareMessage(EntityUid uid, EnsnareableComponent component, StrippingEnsnareButtonPressed args) { - if (args.Target == args.User) + if (args.Session.AttachedEntity is not { Valid: true } user) return; - if (!HasComp(args.User)) - return; + foreach (var entity in component.Container.ContainedEntities) + { + if (!TryComp(entity, out var ensnaring)) + continue; - StartOpeningStripper(args.User, (uid, component)); + _ensnaringSystem.TryFree(uid, user, entity, ensnaring); + return; + } } /// - /// Places item in user's active hand to an inventory slot. + /// Checks whether the item is in a user's active hand and whether it can be inserted into the inventory slot. /// - private async void PlaceActiveHandItemInInventory( - EntityUid user, + private bool CanStripInsertInventory( + Entity user, EntityUid target, EntityUid held, - string slot, - StrippableComponent component) + string slot) { - var userHands = Comp(user); + if (!Resolve(user, ref user.Comp)) + return false; + + if (user.Comp.ActiveHand == null) + return false; + + if (user.Comp.ActiveHandEntity == null) + return false; + + if (user.Comp.ActiveHandEntity != held) + return false; + + if (!_handsSystem.CanDropHeld(user, user.Comp.ActiveHand)) + { + _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user); + return false; + } + + if (_inventorySystem.TryGetSlotEntity(target, slot, out _)) + { + _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-occupied", ("owner", target)), user); + return false; + } - bool Check() + if (!_inventorySystem.CanEquip(user, target, held, slot, out _)) { - if (userHands.ActiveHandEntity != held) - return false; - - if (!_handsSystem.CanDropHeld(user, userHands.ActiveHand!)) - { - _popup.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user); - return false; - } - - if (_inventorySystem.TryGetSlotEntity(target, slot, out _)) - { - _popup.PopupCursor(Loc.GetString("strippable-component-item-slot-occupied",("owner", target)), user); - return false; - } - - if (!_inventorySystem.CanEquip(user, target, held, slot, out _)) - { - _popup.PopupCursor(Loc.GetString("strippable-component-cannot-equip-message",("owner", target)), user); - return false; - } - - return true; + _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-equip-message", ("owner", target)), user); + return false; } + return true; + } + + /// + /// Begins a DoAfter to insert the item in the user's active hand into the inventory slot. + /// + private void StartStripInsertInventory( + Entity user, + EntityUid target, + EntityUid held, + string slot) + { + if (!Resolve(user, ref user.Comp)) + return; + + if (!CanStripInsertInventory(user, target, held, slot)) + return; + if (!_inventorySystem.TryGetSlot(target, slot, out var slotDef)) { Log.Error($"{ToPrettyString(user)} attempted to place an item in a non-existent inventory slot ({slot}) on {ToPrettyString(target)}"); return; } - var userEv = new BeforeStripEvent(slotDef.StripTime); - RaiseLocalEvent(user, userEv); - var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth); - RaiseLocalEvent(target, ev); + var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime); + + if (!stealth) + _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large); + + var prefix = stealth ? "stealthily " : ""; + _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot"); - var doAfterArgs = new DoAfterArgs(EntityManager, user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: held) + var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, true, slot), user, target, held) { - ExtraCheck = Check, - Hidden = ev.Stealth, + Hidden = stealth, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, BreakOnUserMove = true, NeedHand = true, - DuplicateCondition = DuplicateConditions.SameTool // Block any other DoAfters featuring this same entity. + DuplicateCondition = DuplicateConditions.SameTool }; - if (!ev.Stealth && Check() && userHands.ActiveHandEntity != null) - { - var message = Loc.GetString("strippable-component-alert-owner-insert", - ("user", Identity.Entity(user, EntityManager)), ("item", userHands.ActiveHandEntity)); - _popup.PopupEntity(message, target, target, PopupType.Large); - } + _doAfterSystem.TryStartDoAfter(doAfterArgs); + } - var prefix = ev.Stealth ? "stealthily " : ""; - _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot"); + /// + /// Inserts the item in the user's active hand into the inventory slot. + /// + private void StripInsertInventory( + Entity user, + EntityUid target, + EntityUid held, + string slot) + { + if (!Resolve(user, ref user.Comp)) + return; - var result = await _doAfter.WaitDoAfter(doAfterArgs); - if (result != DoAfterStatus.Finished) + if (!CanStripInsertInventory(user, target, held, slot)) return; - DebugTools.Assert(userHands.ActiveHand?.HeldEntity == held); + if (!_handsSystem.TryDrop(user, handsComp: user.Comp)) + return; + + _inventorySystem.TryEquip(user, target, held, slot); + _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot"); + } - if (_handsSystem.TryDrop(user, handsComp: userHands)) + /// + /// Checks whether the item can be removed from the target's inventory. + /// + private bool CanStripRemoveInventory( + EntityUid user, + EntityUid target, + EntityUid item, + string slot) + { + if (!_inventorySystem.TryGetSlotEntity(target, slot, out var slotItem)) { - _inventorySystem.TryEquip(user, target, held, slot); + _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", target)), user); + return false; + } - _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot"); + if (slotItem != item) + return false; + + if (!_inventorySystem.CanUnequip(user, target, slot, out var reason)) + { + _popupSystem.PopupCursor(Loc.GetString(reason), user); + return false; } + + return true; } /// - /// Places item in user's active hand in one of the entity's hands. + /// Begins a DoAfter to remove the item from the target's inventory and insert it in the user's active hand. /// - private async void PlaceActiveHandItemInHands( + private void StartStripRemoveInventory( EntityUid user, EntityUid target, - EntityUid held, - string handName, - StrippableComponent component) + EntityUid item, + string slot) { - var hands = Comp(target); - var userHands = Comp(user); + if (!CanStripRemoveInventory(user, target, item, slot)) + return; - bool Check() + if (!_inventorySystem.TryGetSlot(target, slot, out var slotDef)) { - if (userHands.ActiveHandEntity != held) - return false; - - if (!_handsSystem.CanDropHeld(user, userHands.ActiveHand!)) - { - _popup.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user); - return false; - } - - if (!_handsSystem.TryGetHand(target, handName, out var hand, hands) - || !_handsSystem.CanPickupToHand(target, userHands.ActiveHandEntity.Value, hand, checkActionBlocker: false, hands)) - { - _popup.PopupCursor(Loc.GetString("strippable-component-cannot-put-message",("owner", target)), user); - return false; - } - - return true; + Log.Error($"{ToPrettyString(user)} attempted to take an item from a non-existent inventory slot ({slot}) on {ToPrettyString(target)}"); + return; } - var userEv = new BeforeStripEvent(component.HandStripDelay); - RaiseLocalEvent(user, userEv); - var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth); - RaiseLocalEvent(target, ev); + var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime); - var doAfterArgs = new DoAfterArgs(EntityManager, user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: held) + if (!stealth) { - ExtraCheck = Check, - Hidden = ev.Stealth, + if (slotDef.StripHidden) + _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-hidden", ("slot", slot)), target, target, PopupType.Large); + else + _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target, PopupType.Large); + } + + var prefix = stealth ? "stealthily " : ""; + _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot"); + + var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(false, true, slot), user, target, item) + { + Hidden = stealth, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, BreakOnUserMove = true, NeedHand = true, + BreakOnHandChange = false, // Allow simultaneously removing multiple items. DuplicateCondition = DuplicateConditions.SameTool }; - var prefix = ev.Stealth ? "stealthily " : ""; - _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands"); - - var result = await _doAfter.WaitDoAfter(doAfterArgs); - if (result != DoAfterStatus.Finished) return; - - _handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: userHands); - _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: !ev.Stealth, animate: !ev.Stealth, handsComp: hands); - _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands"); - // hand update will trigger strippable update + _doAfterSystem.TryStartDoAfter(doAfterArgs); } /// - /// Takes an item from the inventory and places it in the user's active hand. + /// Removes the item from the target's inventory and inserts it in the user's active hand. /// - private async void TakeItemFromInventory( + private void StripRemoveInventory( EntityUid user, EntityUid target, EntityUid item, string slot, - Entity strippable) + bool stealth) + { + if (!CanStripRemoveInventory(user, target, item, slot)) + return; + + if (!_inventorySystem.TryUnequip(user, target, slot)) + return; + + RaiseLocalEvent(item, new DroppedEvent(user), true); // Gas tank internals etc. + + _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: stealth); + _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot"); + } + + /// + /// Checks whether the item in the user's active hand can be inserted into one of the target's hands. + /// + private bool CanStripInsertHand( + Entity user, + Entity target, + EntityUid held, + string handName) { - bool Check() + if (!Resolve(user, ref user.Comp) || + !Resolve(target, ref target.Comp)) + return false; + + if (user.Comp.ActiveHand == null) + return false; + + if (user.Comp.ActiveHandEntity == null) + return false; + + if (user.Comp.ActiveHandEntity != held) + return false; + + if (!_handsSystem.CanDropHeld(user, user.Comp.ActiveHand)) { - if (!_inventorySystem.TryGetSlotEntity(target, slot, out var ent) && ent == item) - { - _popup.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", target)), user); - return false; - } - - if (!_inventorySystem.CanUnequip(user, target, slot, out var reason)) - { - _popup.PopupCursor(Loc.GetString(reason), user); - return false; - } - - return true; + _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user); + return false; } - if (!_inventorySystem.TryGetSlot(target, slot, out var slotDef)) + if (!_handsSystem.TryGetHand(target, handName, out var handSlot, target.Comp) || + !_handsSystem.CanPickupToHand(target, user.Comp.ActiveHandEntity.Value, handSlot, checkActionBlocker: false, target.Comp)) { - Log.Error($"{ToPrettyString(user)} attempted to take an item from a non-existent inventory slot ({slot}) on {ToPrettyString(target)}"); - return; + _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-put-message", ("owner", target)), user); + return false; } - var userEv = new BeforeStripEvent(slotDef.StripTime); - RaiseLocalEvent(user, userEv); - var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth); - RaiseLocalEvent(target, ev); + return true; + } + + /// + /// Begins a DoAfter to insert the item in the user's active hand into one of the target's hands. + /// + private void StartStripInsertHand( + Entity user, + Entity target, + EntityUid held, + string handName, + StrippableComponent? targetStrippable = null) + { + if (!Resolve(user, ref user.Comp) || + !Resolve(target, ref target.Comp) || + !Resolve(target, ref targetStrippable)) + return; + + if (!CanStripInsertHand(user, target, held, handName)) + return; - var doAfterArgs = new DoAfterArgs(EntityManager, user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: item) + var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay); + + var prefix = stealth ? "stealthily " : ""; + _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands"); + + var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, false, handName), user, target, held) { - ExtraCheck = Check, - Hidden = ev.Stealth, + Hidden = stealth, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, BreakOnUserMove = true, NeedHand = true, - BreakOnHandChange = false, // allow simultaneously removing multiple items. DuplicateCondition = DuplicateConditions.SameTool }; - if (!ev.Stealth && Check()) + _doAfterSystem.TryStartDoAfter(doAfterArgs); + } + + /// + /// Places the item in the user's active hand into one of the target's hands. + /// + private void StripInsertHand( + Entity user, + Entity target, + EntityUid held, + string handName, + bool stealth) + { + if (!Resolve(user, ref user.Comp) || + !Resolve(target, ref target.Comp)) + return; + + _handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: user.Comp); + _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: stealth, animate: stealth, handsComp: target.Comp); + _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands"); + + // Hand update will trigger strippable update. + } + + /// + /// Checks whether the item is in the target's hand and whether it can be dropped. + /// + private bool CanStripRemoveHand( + EntityUid user, + Entity target, + EntityUid item, + string handName) + { + if (!Resolve(target, ref target.Comp)) + return false; + + if (!_handsSystem.TryGetHand(target, handName, out var handSlot, target.Comp)) { - if (slotDef.StripHidden) - { - _popup.PopupEntity(Loc.GetString("strippable-component-alert-owner-hidden", ("slot", slot)), target, - target, PopupType.Large); - } - else if (_inventorySystem.TryGetSlotEntity(strippable, slot, out var slotItem)) - { - _popup.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", slotItem)), target, - target, PopupType.Large); - } + _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", target)), user); + return false; } - var prefix = ev.Stealth ? "stealthily " : ""; - _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot"); + if (HasComp(handSlot.HeldEntity)) + return false; - var result = await _doAfter.WaitDoAfter(doAfterArgs); - if (result != DoAfterStatus.Finished) - return; + if (handSlot.HeldEntity == null) + return false; - if (!_inventorySystem.TryUnequip(user, strippable, slot)) - return; - - // Raise a dropped event, so that things like gas tank internals properly deactivate when stripping - RaiseLocalEvent(item, new DroppedEvent(user), true); + if (handSlot.HeldEntity != item) + return false; - _handsSystem.PickupOrDrop(user, item, animateUser: !ev.Stealth, animate: !ev.Stealth); - _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot"); + if (!_handsSystem.CanDropHeld(target, handSlot, false)) + { + _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop-message", ("owner", target)), user); + return false; + } + return true; } /// - /// Takes an item from a hand and places it in the user's active hand. + /// Begins a DoAfter to remove the item from the target's hand and insert it in the user's active hand. /// - private async void TakeItemFromHands(EntityUid user, EntityUid target, EntityUid item, string handName, Entity strippable) + private void StartStripRemoveHand( + Entity user, + Entity target, + EntityUid item, + string handName, + StrippableComponent? targetStrippable = null) { - var hands = Comp(target); - var userHands = Comp(user); + if (!Resolve(user, ref user.Comp) || + !Resolve(target, ref target.Comp) || + !Resolve(target, ref targetStrippable)) + return; - bool Check() - { - if (!_handsSystem.TryGetHand(target, handName, out var hand, hands) || hand.HeldEntity != item) - { - _popup.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message",("owner", target)), user); - return false; - } - - if (HasComp(hand.HeldEntity)) - return false; - - if (!_handsSystem.CanDropHeld(target, hand, false)) - { - _popup.PopupCursor(Loc.GetString("strippable-component-cannot-drop-message",("owner", target)), user); - return false; - } - - return true; - } + if (!CanStripRemoveHand(user, target, item, handName)) + return; - var userEv = new BeforeStripEvent(strippable.Comp.HandStripDelay); - RaiseLocalEvent(user, userEv); - var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth); - RaiseLocalEvent(target, ev); + var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay); - var doAfterArgs = new DoAfterArgs(EntityManager, user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: item) + if (!stealth) + _popupSystem.PopupEntity( Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target); + + var prefix = stealth ? "stealthily " : ""; + _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands"); + + var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(false, false, handName), user, target, item) { - ExtraCheck = Check, - Hidden = ev.Stealth, + Hidden = stealth, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, BreakOnUserMove = true, NeedHand = true, - BreakOnHandChange = false, // allow simultaneously removing multiple items. + BreakOnHandChange = false, // Allow simultaneously removing multiple items. DuplicateCondition = DuplicateConditions.SameTool }; - if (!ev.Stealth && Check() && _handsSystem.TryGetHand(target, handName, out var handSlot, hands) && handSlot.HeldEntity != null) + _doAfterSystem.TryStartDoAfter(doAfterArgs); + } + + /// + /// Takes the item from the target's hand and inserts it in the user's active hand. + /// + private void StripRemoveHand( + Entity user, + Entity target, + EntityUid item, + bool stealth) + { + if (!Resolve(user, ref user.Comp) || + !Resolve(target, ref target.Comp)) + return; + + _handsSystem.TryDrop(target, item, checkActionBlocker: false, handsComp: target.Comp); + _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: stealth, handsComp: user.Comp); + _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands"); + + // Hand update will trigger strippable update. + } + + private void OnStrippableDoAfterRunning(Entity entity, ref DoAfterAttemptEvent ev) + { + var args = ev.DoAfter.Args; + + DebugTools.Assert(entity.Owner == args.User); + DebugTools.Assert(args.Target != null); + DebugTools.Assert(args.Used != null); + DebugTools.Assert(ev.Event.SlotOrHandName != null); + + if (ev.Event.InventoryOrHand) { - _popup.PopupEntity( - Loc.GetString("strippable-component-alert-owner", - ("user", Identity.Entity(user, EntityManager)), ("item", item)), - strippable.Owner, - strippable.Owner); + if ( ev.Event.InsertOrRemove && !CanStripInsertInventory((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) || + !ev.Event.InsertOrRemove && !CanStripRemoveInventory(entity.Owner, args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName)) + ev.Cancel(); } + else + { + if ( ev.Event.InsertOrRemove && !CanStripInsertHand((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) || + !ev.Event.InsertOrRemove && !CanStripRemoveHand(entity.Owner, args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName)) + ev.Cancel(); + } + } - var prefix = ev.Stealth ? "stealthily " : ""; - _adminLogger.Add(LogType.Stripping, LogImpact.Low, - $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands"); - - var result = await _doAfter.WaitDoAfter(doAfterArgs); - if (result != DoAfterStatus.Finished) + private void OnStrippableDoAfterFinished(Entity entity, ref StrippableDoAfterEvent ev) + { + if (ev.Cancelled) return; - _handsSystem.TryDrop(target, item, checkActionBlocker: false, handsComp: hands); - _handsSystem.PickupOrDrop(user, item, animateUser: !ev.Stealth, animate: !ev.Stealth, handsComp: userHands); - // hand update will trigger strippable update - _adminLogger.Add(LogType.Stripping, LogImpact.Medium, - $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands"); + DebugTools.Assert(entity.Owner == ev.User); + DebugTools.Assert(ev.Target != null); + DebugTools.Assert(ev.Used != null); + DebugTools.Assert(ev.SlotOrHandName != null); + + if (ev.InventoryOrHand) + { + if (ev.InsertOrRemove) + StripInsertInventory((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName); + else StripRemoveInventory(entity.Owner, ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden); + } + else + { + if (ev.InsertOrRemove) + StripInsertHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden); + else StripRemoveHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.Args.Hidden); + } } } } diff --git a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs index 0138de7a98f..22a1d1a8f52 100644 --- a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs @@ -95,7 +95,7 @@ private void StartDoAfter(EntityUid user, EntityUid item, EntityUid wearer, Togg if (component.StripDelay == null) return; - var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, (float) component.StripDelay.Value.TotalSeconds); + var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, component.StripDelay.Value); var args = new DoAfterArgs(EntityManager, user, time, new ToggleClothingDoAfterEvent(), item, wearer, item) { diff --git a/Content.Shared/Inventory/InventoryTemplatePrototype.cs b/Content.Shared/Inventory/InventoryTemplatePrototype.cs index a4779699629..585f80d4ce9 100644 --- a/Content.Shared/Inventory/InventoryTemplatePrototype.cs +++ b/Content.Shared/Inventory/InventoryTemplatePrototype.cs @@ -20,7 +20,7 @@ public sealed partial class SlotDefinition [DataField("slotFlags")] public SlotFlags SlotFlags { get; private set; } = SlotFlags.PREVENTEQUIP; [DataField("showInWindow")] public bool ShowInWindow { get; private set; } = true; [DataField("slotGroup")] public string SlotGroup { get; private set; } = "Default"; - [DataField("stripTime")] public float StripTime { get; private set; } = 4f; + [DataField("stripTime")] public TimeSpan StripTime { get; private set; } = TimeSpan.FromSeconds(4f); [DataField("uiWindowPos", required: true)] public Vector2i UIWindowPosition { get; private set; } diff --git a/Content.Shared/Strip/Components/StrippableComponent.cs b/Content.Shared/Strip/Components/StrippableComponent.cs index fbf99992e3c..8bf09c3f4c6 100644 --- a/Content.Shared/Strip/Components/StrippableComponent.cs +++ b/Content.Shared/Strip/Components/StrippableComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.DoAfter; using Content.Shared.Inventory; using Robust.Shared.GameStates; using Robust.Shared.Serialization; @@ -8,10 +9,10 @@ namespace Content.Shared.Strip.Components public sealed partial class StrippableComponent : Component { /// - /// The strip delay for hands. + /// The strip delay for hands. /// [ViewVariables(VVAccess.ReadWrite), DataField("handDelay")] - public float HandStripDelay = 4f; + public TimeSpan HandStripDelay = TimeSpan.FromSeconds(4f); } [NetSerializable, Serializable] @@ -21,63 +22,63 @@ public enum StrippingUiKey : byte } [NetSerializable, Serializable] - public sealed class StrippingSlotButtonPressed : BoundUserInterfaceMessage + public sealed class StrippingSlotButtonPressed(string slot, bool isHand) : BoundUserInterfaceMessage { - public readonly string Slot; - - public readonly bool IsHand; - - public StrippingSlotButtonPressed(string slot, bool isHand) - { - Slot = slot; - IsHand = isHand; - } + public readonly string Slot = slot; + public readonly bool IsHand = isHand; } [NetSerializable, Serializable] - public sealed class StrippingEnsnareButtonPressed : BoundUserInterfaceMessage - { - public StrippingEnsnareButtonPressed() - { - } - } + public sealed class StrippingEnsnareButtonPressed : BoundUserInterfaceMessage; - public abstract class BaseBeforeStripEvent : EntityEventArgs, IInventoryRelayEvent + [ByRefEvent] + public abstract class BaseBeforeStripEvent(TimeSpan initialTime, bool stealth = false) : EntityEventArgs, IInventoryRelayEvent { - public readonly float InitialTime; - public float Time => MathF.Max(InitialTime * Multiplier + Additive, 0f); - public float Additive = 0; - public float Multiplier = 1f; - public bool Stealth; + public readonly TimeSpan InitialTime = initialTime; + public TimeSpan Multiplier = TimeSpan.FromSeconds(1f); + public TimeSpan Additive = TimeSpan.Zero; + public bool Stealth = stealth; - public SlotFlags TargetSlots { get; } = SlotFlags.GLOVES; + public TimeSpan Time => TimeSpan.FromSeconds(MathF.Max(InitialTime.Seconds * Multiplier.Seconds + Additive.Seconds, 0f)); - public BaseBeforeStripEvent(float initialTime, bool stealth = false) - { - InitialTime = initialTime; - Stealth = stealth; - } + public SlotFlags TargetSlots { get; } = SlotFlags.GLOVES; } /// - /// Used to modify strip times. Raised directed at the user. + /// Used to modify strip times. Raised directed at the user. /// /// - /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. + /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. /// - public sealed class BeforeStripEvent : BaseBeforeStripEvent - { - public BeforeStripEvent(float initialTime, bool stealth = false) : base(initialTime, stealth) { } - } + [ByRefEvent] + public sealed class BeforeStripEvent(TimeSpan initialTime, bool stealth = false) : BaseBeforeStripEvent(initialTime, stealth); /// - /// Used to modify strip times. Raised directed at the target. + /// Used to modify strip times. Raised directed at the target. /// /// - /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. + /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. /// - public sealed class BeforeGettingStrippedEvent : BaseBeforeStripEvent + [ByRefEvent] + public sealed class BeforeGettingStrippedEvent(TimeSpan initialTime, bool stealth = false) : BaseBeforeStripEvent(initialTime, stealth); + + /// + /// Organizes the behavior of DoAfters for . + /// + [Serializable, NetSerializable] + public sealed partial class StrippableDoAfterEvent : DoAfterEvent { - public BeforeGettingStrippedEvent(float initialTime, bool stealth = false) : base(initialTime, stealth) { } + public readonly bool InsertOrRemove; + public readonly bool InventoryOrHand; + public readonly string SlotOrHandName; + + public StrippableDoAfterEvent(bool insertOrRemove, bool inventoryOrHand, string slotOrHandName) + { + InsertOrRemove = insertOrRemove; + InventoryOrHand = inventoryOrHand; + SlotOrHandName = slotOrHandName; + } + + public override DoAfterEvent Clone() => this; } } diff --git a/Content.Shared/Strip/Components/ThievingComponent.cs b/Content.Shared/Strip/Components/ThievingComponent.cs index 83679f132c4..a851dd5ef63 100644 --- a/Content.Shared/Strip/Components/ThievingComponent.cs +++ b/Content.Shared/Strip/Components/ThievingComponent.cs @@ -11,7 +11,7 @@ public sealed partial class ThievingComponent : Component /// [ViewVariables(VVAccess.ReadWrite)] [DataField("stripTimeReduction")] - public float StripTimeReduction = 0.5f; + public TimeSpan StripTimeReduction = TimeSpan.FromSeconds(0.5f); /// /// Should it notify the user if they're stripping a pocket? diff --git a/Content.Shared/Strip/SharedStrippableSystem.cs b/Content.Shared/Strip/SharedStrippableSystem.cs index a698ae5035a..7afd4f245a1 100644 --- a/Content.Shared/Strip/SharedStrippableSystem.cs +++ b/Content.Shared/Strip/SharedStrippableSystem.cs @@ -14,12 +14,12 @@ public override void Initialize() SubscribeLocalEvent(OnDragDrop); } - public (float Time, bool Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, float initialTime) + public (TimeSpan Time, bool Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, TimeSpan initialTime) { var userEv = new BeforeStripEvent(initialTime); - RaiseLocalEvent(user, userEv); + RaiseLocalEvent(user, ref userEv); var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth); - RaiseLocalEvent(target, ev); + RaiseLocalEvent(target, ref ev); return (ev.Time, ev.Stealth); } diff --git a/Content.Shared/Strip/ThievingSystem.cs b/Content.Shared/Strip/ThievingSystem.cs index 0ef4b66571f..2b3d3b38a00 100644 --- a/Content.Shared/Strip/ThievingSystem.cs +++ b/Content.Shared/Strip/ThievingSystem.cs @@ -1,4 +1,5 @@ using Content.Shared.Inventory; +using Content.Shared.Strip; using Content.Shared.Strip.Components; namespace Content.Shared.Strip; From 5f4df32bb532b1f7d86750b266e12135bea6729e Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 1 Jul 2024 15:24:31 -0400 Subject: [PATCH 06/56] Fixes handcuff bug --- Content.Shared/Cuffs/Components/HandcuffComponent.cs | 6 +++--- Content.Shared/Cuffs/SharedCuffableSystem.cs | 2 +- Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Content.Shared/Cuffs/Components/HandcuffComponent.cs b/Content.Shared/Cuffs/Components/HandcuffComponent.cs index a5593cd2712..72663fd8fab 100644 --- a/Content.Shared/Cuffs/Components/HandcuffComponent.cs +++ b/Content.Shared/Cuffs/Components/HandcuffComponent.cs @@ -86,11 +86,11 @@ public sealed partial class HandcuffComponent : Component public SoundSpecifier EndUncuffSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_takeoff_end.ogg"); /// - /// Both a bool and a multiplier combined. If it is 0, handcuffs are unaffected by mass contests. The absolute value of any nonzero acts as a multiplier on how much mass affects uncuff speed. - /// A value of 1 provides the full modifier from MassContest. 0.5 is half the effect of mass contests, and so on. + /// Acts as a two-state option for handcuff speed. When true, handcuffs will be easier to get out of if you are larger than average. Representing the use of strength to break things like zipties. + /// When false, handcuffs are easier to get out of if you are smaller than average, representing the use of dexterity to slip the cuffs. /// [DataField] - public float UncuffMassModifier = 0f; + public bool UncuffMassMultiplies = false; } /// diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs index 0fa77b182c4..92f4576c8b6 100644 --- a/Content.Shared/Cuffs/SharedCuffableSystem.cs +++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs @@ -561,7 +561,7 @@ public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove return; } - var uncuffTime = (isOwner ? cuff.BreakoutTime : cuff.UncuffTime) * (_contests.MassContest(user) * Math.Abs(cuff.UncuffMassModifier)); + var uncuffTime = (isOwner ? cuff.BreakoutTime : cuff.UncuffTime) * (cuff.UncuffMassMultiplies ? 1 / _contests.MassContest(user) : _contests.MassContest(user)); if (isOwner) { diff --git a/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml b/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml index 32269d38888..711ddbc0c28 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml @@ -52,7 +52,7 @@ path: /Audio/Items/Handcuffs/rope_breakout.ogg startBreakoutSound: path: /Audio/Items/Handcuffs/rope_takeoff.ogg - uncuffMassModifier: 1 + uncuffMassMultiplies: true - type: Construction graph: makeshifthandcuffs node: cuffscable @@ -94,7 +94,7 @@ path: /Audio/Items/Handcuffs/rope_breakout.ogg startBreakoutSound: path: /Audio/Items/Handcuffs/rope_takeoff.ogg - uncuffMassModifier: 1 + uncuffMassMultiplies: true - type: Sprite sprite: Objects/Misc/zipties.rsi state: cuff From ba2d53845e3f365a7423ac8f9ab6ee491e76d60d Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 1 Jul 2024 18:37:29 -0400 Subject: [PATCH 07/56] Make Spent Casing Ejection Not Suck (#478) # Description Part of #467, #460, and #474 This is a small PR that corrects a math error in SharedGunSystem, causing shell casings to be "Thrown" directly downwards instead of in an actually cinematic and exciting arc. While I'm at it, I also corrected the fixture of base shell casings to favor "Bounciness", and decreased its mass to approximately 100 grams. Finally, I added a sound for when casings bounce off of walls, which wasn't present before. https://github.com/Simple-Station/Einstein-Engines/assets/16548818/56bb4ecc-d5eb-4b36-853b-42f05374150d :cl: - fix: Spent bullet casings now fly away from a shooter in a cinematic manner, rather than fall at their feet. Co-authored-by: Danger Revolution! <142105406+DangerRevolution@users.noreply.github.com> --- Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs | 2 +- .../Guns/Ammunition/Cartridges/base_cartridge.yml | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs index cadb0a4b215..ff8b102bb57 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -443,7 +443,7 @@ protected void EjectCartridge( { Angle ejectAngle = angle.Value; ejectAngle += 3.7f; // 212 degrees; casings should eject slightly to the right and behind of a gun - ThrowingSystem.TryThrow(entity, ejectAngle.ToVec().Normalized() / 100, 5f); + ThrowingSystem.TryThrow(entity, ejectAngle.ToVec(), 625f); } if (playSound && TryComp(entity, out var cartridge)) { diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/base_cartridge.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/base_cartridge.yml index e188ee8c658..3bef413dffa 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/base_cartridge.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/base_cartridge.yml @@ -11,10 +11,10 @@ shape: !type:PhysShapeAabb bounds: "-0.10,-0.05,0.10,0.05" - density: 20 + density: 0.5 mask: - ItemMask - restitution: 0.3 # fite me + restitution: 0.7 # Small and bouncy friction: 0.2 - type: Tag tags: @@ -23,6 +23,11 @@ size: Tiny - type: SpaceGarbage - type: EmitSoundOnLand + sound: + path: /Audio/Weapons/Guns/Casings/casing_fall_1.ogg + params: + volume: -1 + - type: EmitSoundOnCollide sound: path: /Audio/Weapons/Guns/Casings/casing_fall_2.ogg params: From 2e712c00249cfe5bd254462a1efa1b6c8a1ec6bd Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Mon, 1 Jul 2024 22:37:52 +0000 Subject: [PATCH 08/56] Automatic Changelog Update (#478) --- Resources/Changelog/Changelog.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 6fe7b0ff689..515c1832db1 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4257,3 +4257,11 @@ Entries: message: Most items now take time to equip and unequip, especially space suits. id: 6132 time: '2024-06-30T18:22:28.0000000+00:00' +- author: VMSolidus + changes: + - type: Fix + message: >- + Spent bullet casings now fly away from a shooter in a cinematic manner, + rather than fall at their feet. + id: 6133 + time: '2024-07-01T22:37:29.0000000+00:00' From 8bbcafa009cc04e44f25dac57c56d4492c87afa3 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 1 Jul 2024 18:45:04 -0400 Subject: [PATCH 09/56] New glimmer functions --- .../Abilities/PyrokinesisPowerSystem.cs | 15 ++-- .../Structures/GlimmerStructuresSystem.cs | 12 ++- .../Psionics/Glimmer/GlimmerSystem.cs | 74 +++++++++++++++++-- Content.Shared/Psionics/PsionicComponent.cs | 14 +++- 4 files changed, 96 insertions(+), 19 deletions(-) diff --git a/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs b/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs index 90a28eb6ae1..07ada4c0c5d 100644 --- a/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs +++ b/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs @@ -11,6 +11,7 @@ using Robust.Shared.Timing; using Content.Shared.Popups; using Content.Shared.Psionics.Events; +using Content.Shared.Psionics; namespace Content.Server.Psionics.Abilities { @@ -38,7 +39,7 @@ public override void Initialize() private void OnInit(EntityUid uid, PyrokinesisPowerComponent component, ComponentInit args) { _actions.AddAction(uid, ref component.PyrokinesisPrechargeActionEntity, component.PyrokinesisPrechargeActionId); - _actions.TryGetActionData( component.PyrokinesisPrechargeActionEntity, out var actionData); + _actions.TryGetActionData(component.PyrokinesisPrechargeActionEntity, out var actionData); if (actionData is { UseDelay: not null }) _actions.StartUseDelay(component.PyrokinesisPrechargeActionEntity); if (TryComp(uid, out var psionic)) @@ -107,13 +108,17 @@ private void OnPowerUsed(PyrokinesisPowerActionEvent args) var ent = Spawn("ProjectileAnomalyFireball", spawnCoords); + if (_glimmerSystem.GlimmerOutput >= 25 * psionic.Dampening) + EnsureComp(ent); + if (TryComp(ent, out var fireball)) { - fireball.MaxIntensity = (int) MathF.Round(2 * psionic.Amplification); - fireball.IntensitySlope = (int) MathF.Round(1 * psionic.Amplification); - fireball.TotalIntensity = (int) MathF.Round(25 * psionic.Amplification); + var psionicFactor = psionic.Amplification * _glimmerSystem.GetGlimmerEquilibriumRatio(); + fireball.MaxIntensity = 2 * psionicFactor; + fireball.IntensitySlope = 1 * psionicFactor; + fireball.TotalIntensity = 25 * psionicFactor; - if (_glimmerSystem.GlimmerOutput >= 500) + if (_glimmerSystem.GlimmerOutput >= _glimmerSystem.GlimmerEquilibrium) fireball.CanCreateVacuum = true; else fireball.CanCreateVacuum = false; diff --git a/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs b/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs index 09e33dd4144..b8649f71a5f 100644 --- a/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs +++ b/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs @@ -76,16 +76,20 @@ public override void Update(float frameTime) if (source.Accumulator > source.SecondsPerGlimmer) { + var glimmerEquilibrium = _glimmerSystem.GlimmerEquilibrium; source.Accumulator -= source.SecondsPerGlimmer; + + // Shorthand explanation: + // This makes glimmer far more "Swingy", by making both positive and negative glimmer sources scale quite dramatically with glimmer if (source.AddToGlimmer) { - //If we're above the equilibrium point of 500, increase the output of passive glimmer sources to help fight against linear decay - //Even with this, don't expect probers to ever get to mind swap levels of power without any help - _glimmerSystem.DeltaGlimmerInput(_glimmerSystem.GlimmerOutput > 500 ? MathF.Round(_glimmerSystem.GlimmerOutput / 100) : 1f); + _glimmerSystem.DeltaGlimmerInput((_glimmerSystem.GlimmerOutput > glimmerEquilibrium ? _glimmerSystem.GetGlimmerOutputInteger() : 1f) + * (_glimmerSystem.GlimmerOutput < glimmerEquilibrium ? _glimmerSystem.GetGlimmerEquilibriumRatio() : 1f)); } else { - _glimmerSystem.DeltaGlimmerInput(-1); + _glimmerSystem.DeltaGlimmerInput(-(_glimmerSystem.GlimmerOutput > glimmerEquilibrium ? _glimmerSystem.GetGlimmerOutputInteger() : 1f) + * (_glimmerSystem.GlimmerOutput > glimmerEquilibrium ? _glimmerSystem.GetGlimmerEquilibriumRatio() : 1f)); } } } diff --git a/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs b/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs index d50b6fb2f6d..f40a916f487 100644 --- a/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs +++ b/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs @@ -2,7 +2,6 @@ using Robust.Shared.Configuration; using Content.Shared.CCVar; using Content.Shared.GameTicking; -using Content.Shared.Mobs; namespace Content.Shared.Psionics.Glimmer { @@ -12,6 +11,7 @@ namespace Content.Shared.Psionics.Glimmer public sealed class GlimmerSystem : EntitySystem { [Dependency] private readonly IConfigurationManager _cfg = default!; + private float _glimmerInput = 0; /// /// GlimmerInput represents the system-facing value of the station's glimmer, and is given by f(y) for this graph: https://www.desmos.com/calculator/posutiq38e @@ -27,6 +27,19 @@ public float GlimmerInput } private float _glimmerOutput = 0; + /// + /// This constant is equal to the intersection of the Glimmer Equation(https://www.desmos.com/calculator/posutiq38e) and the line Y = X. + /// + private const float _glimmerEquilibrium = 502.941f; + + /// + /// This constant is equal to the intersection of the Glimmer Equation(https://www.desmos.com/calculator/posutiq38e) and the line Y = X. + /// + public float GlimmerEquilibrium + { + get { return _glimmerEquilibrium; } + } + /// /// Glimmer Output represents the player-facing value of the station's glimmer, and is given by f(x) for this graph: https://www.desmos.com/calculator/posutiq38e /// Where x = GlimmerInput and y = GlimmerOutput @@ -55,7 +68,8 @@ private void Reset(RoundRestartCleanupEvent args) } /// - /// Return an abstracted range of a glimmer count. + /// Return an abstracted range of a glimmer count. This is a legacy system used to support the Prober, + /// and is the lowest form of abstracted glimmer. It's meant more for sprite states than math. /// /// What glimmer count to check. Uses the current glimmer by default. public GlimmerTier GetGlimmerTier(float? glimmer = null) @@ -66,14 +80,30 @@ public GlimmerTier GetGlimmerTier(float? glimmer = null) return glimmer switch { <= 49 => GlimmerTier.Minimal, - >= 50 and <= 99 => GlimmerTier.Low, - >= 100 and <= 299 => GlimmerTier.Moderate, - >= 300 and <= 499 => GlimmerTier.High, - >= 500 and <= 899 => GlimmerTier.Dangerous, + >= 50 and <= 399 => GlimmerTier.Low, + >= 400 and <= 599 => GlimmerTier.Moderate, + >= 600 and <= 699 => GlimmerTier.High, + >= 700 and <= 899 => GlimmerTier.Dangerous, _ => GlimmerTier.Critical, }; } + /// + /// Returns a 0 through 10 range of glimmer. Do not divide by this for any reason. + /// + /// + public int GetGlimmerOutputInteger() + { + if (!_enabled) + return 1; + else return (int) MathF.Round(GlimmerOutput / 1000); + } + + /// + /// This is the public facing function for modifying Glimmer based on the log scale. Simply add or subtract to this with any nonzero number + /// Go through this if you want glimmer to be modified faster if its below 502.941f, and slower if above said equilibrium + /// + /// public void DeltaGlimmerInput(float delta) { if (_enabled && delta != 0) @@ -83,6 +113,11 @@ public void DeltaGlimmerInput(float delta) } } + /// + /// This is the public facing function for modifying Glimmer based on a linear scale. Simply add or subtract to this with any nonzero number. + /// This is primarily intended for load bearing systems such as Probers and Drainers, and should not be called by most things by design. + /// + /// public void DeltaGlimmerOutput(float delta) { if (_enabled && delta != 0) @@ -92,23 +127,46 @@ public void DeltaGlimmerOutput(float delta) } } + /// + /// This directly sets the Player-Facing side of Glimmer to a given value, and is not intended to be called by anything other than admin commands. + /// This is clamped between 0 and 999.999f + /// + /// public void SetGlimmerOutput(float set) { if (_enabled && set != 0) { - GlimmerOutput = set; + GlimmerOutput = Math.Clamp(set, 0, 999.999f); GlimmerInput = 2000 / (1 + MathF.Pow(MathF.E, -.0022f * GlimmerOutput)) - 1000; } } + /// + /// This directly sets the code-facing side of Glimmer to a given value, and is not intended to be called by anything other than admin commands. + /// This accepts any positive float input. + /// + /// public void SetGlimmerInput(float set) { - if (_enabled && set != 0) + if (_enabled && set >= 0) { GlimmerInput = set; GlimmerOutput = 2000 / (1 + MathF.Pow(MathF.E, -.0022f * GlimmerOutput)) - 1000; } } + + /// + /// Outputs the ratio between actual glimmer and glimmer equilibrium(The intersection of the Glimmer Equation and the line y = x). + /// This will return 0.01f if glimmer is 0, and 1 if glimmer is disabled. + /// + public float GetGlimmerEquilibriumRatio() + { + if (!_enabled) + return 1; + else if (GlimmerOutput == 0) + return 0.01f; + else return GlimmerOutput / GlimmerEquilibrium; + } } [Serializable, NetSerializable] diff --git a/Content.Shared/Psionics/PsionicComponent.cs b/Content.Shared/Psionics/PsionicComponent.cs index c955626c53d..748b8ddd92a 100644 --- a/Content.Shared/Psionics/PsionicComponent.cs +++ b/Content.Shared/Psionics/PsionicComponent.cs @@ -19,10 +19,20 @@ public sealed partial class PsionicComponent : Component [DataField("psychicFeedback")] public List PsychicFeedback = new(); - [DataField("amplification")] + /// + /// An abstraction of how "Powerful" a psychic is. This is most commonly used as a multiplier on numerical outputs for psychic powers. + /// + /// + /// For an ordinary human, this will be between 0.5 and 1.2, but may be higher for some entities. + /// + [DataField] public float Amplification = 0.1f; - [DataField("dampening")] + /// + /// An abstraction of how much "Control" a psychic has over their powers. This is most commonly used to decrease glimmer output of powers, + /// or to make obvious powers less likely to be obvious. + /// + [DataField] public float Dampening = 0.1f; public bool Telepath = true; public bool TelepathicMute = false; From c8e0572c675b51d78681637c89de8b4c7d3c5a9f Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 1 Jul 2024 19:22:48 -0400 Subject: [PATCH 10/56] simplify GlimmerEquilibrium constant --- .../Psionics/Abilities/PyrokinesisPowerSystem.cs | 2 +- .../Glimmer/Structures/GlimmerStructuresSystem.cs | 2 +- Content.Shared/Psionics/Glimmer/GlimmerSystem.cs | 9 +-------- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs b/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs index 07ada4c0c5d..f3e2cc69fd5 100644 --- a/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs +++ b/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs @@ -118,7 +118,7 @@ private void OnPowerUsed(PyrokinesisPowerActionEvent args) fireball.IntensitySlope = 1 * psionicFactor; fireball.TotalIntensity = 25 * psionicFactor; - if (_glimmerSystem.GlimmerOutput >= _glimmerSystem.GlimmerEquilibrium) + if (_glimmerSystem.GlimmerOutput >= GlimmerSystem.GlimmerEquilibrium) fireball.CanCreateVacuum = true; else fireball.CanCreateVacuum = false; diff --git a/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs b/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs index b8649f71a5f..309bd732ef7 100644 --- a/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs +++ b/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs @@ -76,7 +76,7 @@ public override void Update(float frameTime) if (source.Accumulator > source.SecondsPerGlimmer) { - var glimmerEquilibrium = _glimmerSystem.GlimmerEquilibrium; + var glimmerEquilibrium = GlimmerSystem.GlimmerEquilibrium; source.Accumulator -= source.SecondsPerGlimmer; // Shorthand explanation: diff --git a/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs b/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs index f40a916f487..b376f8aab79 100644 --- a/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs +++ b/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs @@ -30,15 +30,8 @@ public float GlimmerInput /// /// This constant is equal to the intersection of the Glimmer Equation(https://www.desmos.com/calculator/posutiq38e) and the line Y = X. /// - private const float _glimmerEquilibrium = 502.941f; + public const float GlimmerEquilibrium = 502.941f; - /// - /// This constant is equal to the intersection of the Glimmer Equation(https://www.desmos.com/calculator/posutiq38e) and the line Y = X. - /// - public float GlimmerEquilibrium - { - get { return _glimmerEquilibrium; } - } /// /// Glimmer Output represents the player-facing value of the station's glimmer, and is given by f(x) for this graph: https://www.desmos.com/calculator/posutiq38e From 35eb6f6a0266b26ba605ab330c3c5ee0cf757aaf Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 1 Jul 2024 19:40:53 -0400 Subject: [PATCH 11/56] Some more small optimizations --- Content.Server/Psionics/Abilities/DispelPowerSystem.cs | 6 +++++- Content.Shared/Psionics/Glimmer/GlimmerSystem.cs | 8 ++++++++ Content.Shared/Psionics/SharedPsionicAbilitiesSystem.cs | 3 ++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Content.Server/Psionics/Abilities/DispelPowerSystem.cs b/Content.Server/Psionics/Abilities/DispelPowerSystem.cs index 33c6b5dcaed..73c6f5d339c 100644 --- a/Content.Server/Psionics/Abilities/DispelPowerSystem.cs +++ b/Content.Server/Psionics/Abilities/DispelPowerSystem.cs @@ -84,7 +84,11 @@ private void OnPowerUsed(DispelPowerActionEvent args) { args.Handled = true; _psionics.LogPowerUsed(args.Performer, "dispel", psionic, 1, 1, true); - _glimmerSystem.DeltaGlimmerInput(-_random.NextFloat(2 * psionic.Dampening - psionic.Amplification, 4 * psionic.Dampening - psionic.Amplification)); + + // Redundant check here as well for a small performance optimization + // Dispel has its own niche equations for glimmer. + if (_glimmerSystem.GetGlimmerEnabled()) + _glimmerSystem.DeltaGlimmerOutput(-_random.NextFloat(2 * psionic.Dampening - psionic.Amplification, 4 * psionic.Dampening - psionic.Amplification)); } } diff --git a/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs b/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs index b376f8aab79..9b8d20b295d 100644 --- a/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs +++ b/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs @@ -160,6 +160,14 @@ public float GetGlimmerEquilibriumRatio() return 0.01f; else return GlimmerOutput / GlimmerEquilibrium; } + + /// + /// Returns the GlimmerEnabled CVar, useful for niche early exits in systems that otherwise don't have any calls to CVars. + /// + public bool GetGlimmerEnabled() + { + return _enabled; + } } [Serializable, NetSerializable] diff --git a/Content.Shared/Psionics/SharedPsionicAbilitiesSystem.cs b/Content.Shared/Psionics/SharedPsionicAbilitiesSystem.cs index f905ec4dd44..4dd8fa2442f 100644 --- a/Content.Shared/Psionics/SharedPsionicAbilitiesSystem.cs +++ b/Content.Shared/Psionics/SharedPsionicAbilitiesSystem.cs @@ -88,7 +88,8 @@ public void LogPowerUsed(EntityUid uid, string power, PsionicComponent? psionic var ev = new PsionicPowerUsedEvent(uid, power); RaiseLocalEvent(uid, ev, false); - if (!overrideGlimmer) + //Redundant check for the GlimmerEnabled CVar because I want to skip this math too if its turned off. + if (_glimmerSystem.GetGlimmerEnabled() && !overrideGlimmer) { if (psionic == null) _glimmerSystem.DeltaGlimmerInput(_robustRandom.NextFloat(minGlimmer, maxGlimmer)); From 631a4bfa46fd58b5045380d8de06370a7af416ff Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 1 Jul 2024 20:09:16 -0400 Subject: [PATCH 12/56] Removing the single power requirement We now have a glimmer system that can accommodate and account for lots of psychics with multiple powers. --- Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs index 9de4e412c81..ed75d097c01 100644 --- a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs +++ b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs @@ -37,9 +37,6 @@ public void AddPsionics(EntityUid uid) if (Deleted(uid)) return; - if (HasComp(uid)) - return; - AddRandomPsionicPower(uid); } public void AddRandomPsionicPower(EntityUid uid) From 2f2f658a09e62f5e63196097f92121dffa8ffdb6 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 1 Jul 2024 20:10:44 -0400 Subject: [PATCH 13/56] Ye --- .../Nyanotrasen/StationEvents/Events/NoosphericStormRule.cs | 2 +- Content.Server/Psionics/PsionicsSystem.cs | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericStormRule.cs b/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericStormRule.cs index c26ab1481ac..021c9591022 100644 --- a/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericStormRule.cs +++ b/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericStormRule.cs @@ -30,7 +30,7 @@ protected override void Started(EntityUid uid, NoosphericStormRuleComponent comp continue; // Skip over those who are already psionic or those who are insulated, or zombies. - if (HasComp(potentialPsionic) || HasComp(potentialPsionic) || HasComp(potentialPsionic)) + if (HasComp(potentialPsionic) || HasComp(potentialPsionic)) continue; validList.Add(potentialPsionic); diff --git a/Content.Server/Psionics/PsionicsSystem.cs b/Content.Server/Psionics/PsionicsSystem.cs index 06250f77ba3..6799b0bd3f5 100644 --- a/Content.Server/Psionics/PsionicsSystem.cs +++ b/Content.Server/Psionics/PsionicsSystem.cs @@ -127,9 +127,6 @@ private void OnStamHit(EntityUid uid, AntiPsionicWeaponComponent component, Stam public void RollPsionics(EntityUid uid, PotentialPsionicComponent component, bool applyGlimmer = true, float multiplier = 1f) { - if (HasComp(uid)) - return; - if (!_cfg.GetCVar(CCVars.PsionicRollsEnabled)) return; From 7c7ab3f240a96e5633273168481d1158de67d6ef Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 1 Jul 2024 21:11:01 -0400 Subject: [PATCH 14/56] Update PsionicAbilitiesSystem.cs --- .../Abilities/PsionicAbilitiesSystem.cs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs index ed75d097c01..27e93678f21 100644 --- a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs +++ b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs @@ -7,6 +7,7 @@ using Robust.Shared.Random; using Robust.Shared.Prototypes; using Content.Shared.Popups; +using Content.Server.MassMedia.Components; namespace Content.Server.Psionics.Abilities { @@ -37,25 +38,35 @@ public void AddPsionics(EntityUid uid) if (Deleted(uid)) return; - AddRandomPsionicPower(uid); + var rollCount = 0; + while (rollCount < 3) + { + if (AddRandomPsionicPower(uid)) + break; + + rollCount++; + } + } - public void AddRandomPsionicPower(EntityUid uid) + public bool AddRandomPsionicPower(EntityUid uid) { EnsureComp(uid, out var psionic); if (!_prototypeManager.TryIndex("RandomPsionicPowerPool", out var pool)) { Logger.Error("Can't index the random psionic power pool!"); - return; + return false; } - // uh oh, stinky! - var newComponent = (Component) _componentFactory.GetComponent(pool.Pick()); - newComponent.Owner = uid; - - EntityManager.AddComponent(uid, newComponent); + var newComponent = _componentFactory.GetComponent(pool.Pick()); - _glimmerSystem.DeltaGlimmerInput(_random.NextFloat(psionic.Amplification * psionic.Dampening, psionic.Amplification * psionic.Dampening * 5)); + if (!EntityManager.HasComponent(uid, newComponent.GetType())) + { + EntityManager.AddComponent(uid, newComponent); + _glimmerSystem.DeltaGlimmerInput(_random.NextFloat(psionic.Amplification * psionic.Dampening, psionic.Amplification * psionic.Dampening * 5)); + return true; + } + return false; } public void RemovePsionics(EntityUid uid) @@ -93,7 +104,7 @@ public void RemovePsionics(EntityUid uid) _statusEffectsSystem.TryAddStatusEffect(uid, "Stutter", TimeSpan.FromMinutes(5), false, "StutteringAccent"); - _glimmerSystem.DeltaGlimmerOutput(-_random.NextFloat((int) MathF.Round(psionic.Amplification * psionic.Dampening * 5), (int) MathF.Round(psionic.Amplification * psionic.Dampening * 10))); + _glimmerSystem.DeltaGlimmerOutput(-_random.NextFloat(psionic.Amplification * psionic.Dampening * 5, psionic.Amplification * psionic.Dampening * 10)); RemComp(uid); RemComp(uid); } From 10ea788420d744d3a5ea39a0d34ed10bd89808fc Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 1 Jul 2024 21:32:50 -0400 Subject: [PATCH 15/56] 1984 a while loop and make this actually work without needing one. --- .../Abilities/PsionicAbilitiesSystem.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs index 27e93678f21..b839622014d 100644 --- a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs +++ b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs @@ -38,35 +38,35 @@ public void AddPsionics(EntityUid uid) if (Deleted(uid)) return; - var rollCount = 0; - while (rollCount < 3) - { - if (AddRandomPsionicPower(uid)) - break; - - rollCount++; - } + AddRandomPsionicPower(uid); } - public bool AddRandomPsionicPower(EntityUid uid) + public void AddRandomPsionicPower(EntityUid uid) { EnsureComp(uid, out var psionic); if (!_prototypeManager.TryIndex("RandomPsionicPowerPool", out var pool)) { Logger.Error("Can't index the random psionic power pool!"); - return false; + return; } - var newComponent = _componentFactory.GetComponent(pool.Pick()); + var newPool = pool; + foreach (var component in pool.Weights.Keys) + { + var checkedComponent = _componentFactory.GetComponent(component); + if (EntityManager.HasComponent(uid, checkedComponent.GetType())) + newPool.Weights.Remove(component); + } - if (!EntityManager.HasComponent(uid, newComponent.GetType())) + if (newPool.Weights.Keys != null) { + var newComponent = _componentFactory.GetComponent(newPool.Pick()); + EntityManager.AddComponent(uid, newComponent); _glimmerSystem.DeltaGlimmerInput(_random.NextFloat(psionic.Amplification * psionic.Dampening, psionic.Amplification * psionic.Dampening * 5)); - return true; } - return false; + return; } public void RemovePsionics(EntityUid uid) From 8e9afa0c7ade0e79e3e959cf911051687c5b8d8c Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 1 Jul 2024 21:35:05 -0400 Subject: [PATCH 16/56] Update PsionicAbilitiesSystem.cs --- Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs index b839622014d..24e6bbb8d3b 100644 --- a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs +++ b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs @@ -7,7 +7,6 @@ using Robust.Shared.Random; using Robust.Shared.Prototypes; using Content.Shared.Popups; -using Content.Server.MassMedia.Components; namespace Content.Server.Psionics.Abilities { From 44e29dd8ce0b32b70dad42780583fbdc3e5b49b5 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 1 Jul 2024 22:43:20 -0400 Subject: [PATCH 17/56] Zap now unlocks the next power roll. --- .../StationEvents/Events/NoosphericZapRule.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericZapRule.cs b/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericZapRule.cs index 3672d317d9e..a11faa06933 100644 --- a/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericZapRule.cs +++ b/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericZapRule.cs @@ -35,19 +35,15 @@ protected override void Started(EntityUid uid, NoosphericZapRuleComponent compon _stunSystem.TryParalyze(psion, TimeSpan.FromSeconds(5), false); _statusEffectsSystem.TryAddStatusEffect(psion, "Stutter", TimeSpan.FromSeconds(10), false, "StutteringAccent"); - if (HasComp(psion)) - _popupSystem.PopupEntity(Loc.GetString("noospheric-zap-seize"), psion, psion, Shared.Popups.PopupType.LargeCaution); + if (potentialPsionicComponent.Rerolled) + { + potentialPsionicComponent.Rerolled = false; + _popupSystem.PopupEntity(Loc.GetString("noospheric-zap-seize-potential-regained"), psion, psion, Shared.Popups.PopupType.LargeCaution); + } else { - if (potentialPsionicComponent.Rerolled) - { - potentialPsionicComponent.Rerolled = false; - _popupSystem.PopupEntity(Loc.GetString("noospheric-zap-seize-potential-regained"), psion, psion, Shared.Popups.PopupType.LargeCaution); - } else - { - _psionicsSystem.RollPsionics(psion, potentialPsionicComponent, multiplier: 0.25f); - _popupSystem.PopupEntity(Loc.GetString("noospheric-zap-seize"), psion, psion, Shared.Popups.PopupType.LargeCaution); - } + _psionicsSystem.RollPsionics(psion, potentialPsionicComponent, multiplier: 0.25f); + _popupSystem.PopupEntity(Loc.GetString("noospheric-zap-seize"), psion, psion, Shared.Popups.PopupType.LargeCaution); } } } From 864ba53a5cf17963608a4325a23b6a27afc6f5ea Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 1 Jul 2024 22:45:33 -0400 Subject: [PATCH 18/56] Revenant moved to new crit threshold --- Resources/Prototypes/Nyanotrasen/GameRules/events.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Prototypes/Nyanotrasen/GameRules/events.yml b/Resources/Prototypes/Nyanotrasen/GameRules/events.yml index f5626a0406f..decdcfdda26 100644 --- a/Resources/Prototypes/Nyanotrasen/GameRules/events.yml +++ b/Resources/Prototypes/Nyanotrasen/GameRules/events.yml @@ -145,7 +145,7 @@ noSpawn: true components: - type: GlimmerEvent - minimumGlimmer: 500 + minimumGlimmer: 700 maximumGlimmer: 900 report: glimmer-event-report-signatures - type: GlimmerRevenantRule From c5946ee4b845580821bbb1f016e791eca9e8be53 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 1 Jul 2024 23:02:19 -0400 Subject: [PATCH 19/56] Update AtmosphereSystem.HighPressureDelta.cs --- .../Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs index ac9b5c2b0a7..461435f0624 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs @@ -217,11 +217,11 @@ public void ExperiencePressureDifference( if (throwTarget != EntityCoordinates.Invalid) { var pos = throwTarget.ToMap(EntityManager, _transformSystem).Position - xform.WorldPosition + dirVec; - _throwing.TryThrow(uid, pos.Normalized() * moveForce, moveForce); + _throwing.TryThrow(uid, pos.Normalized() * MathF.Min(moveForce, SpaceWindMaxVelocity), moveForce); } else { - _throwing.TryThrow(uid, dirVec.Normalized() * moveForce, moveForce); + _throwing.TryThrow(uid, dirVec.Normalized() * MathF.Min(moveForce, SpaceWindMaxVelocity), moveForce); } component.LastHighPressureMovementAirCycle = cycle; From d461f4fe176ab24b8278d17b500424c840a6348d Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Tue, 2 Jul 2024 01:35:29 -0400 Subject: [PATCH 20/56] Don't look at this please. --- Content.Client/Entry/EntryPoint.cs | 1 + .../TypingIndicator/TypingIndicatorSystem.cs | 2 +- .../Components/NPCConversationComponent.cs | 152 +++++ .../NPC/Events/NPCConversationEvents.cs | 63 ++ .../NPCConversationTreePrototype.cs | 154 +++++ .../NPC/Systems/NPCConversationSystem.cs | 558 ++++++++++++++++++ .../SophicScribe/SophicScribeSystem.cs | 36 ++ .../Locale/en-US/npc/conversation/sophia.ftl | 82 +++ .../Structures/Research/sophicscribe.yml | 194 +++++- 9 files changed, 1240 insertions(+), 2 deletions(-) create mode 100644 Content.Server/NPC/Components/NPCConversationComponent.cs create mode 100644 Content.Server/NPC/Events/NPCConversationEvents.cs create mode 100644 Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs create mode 100644 Content.Server/NPC/Systems/NPCConversationSystem.cs create mode 100644 Resources/Locale/en-US/npc/conversation/sophia.ftl diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index a1fc68bbd2f..8636e0eb6aa 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -125,6 +125,7 @@ public override void Init() _prototypeManager.RegisterIgnore("alertLevels"); _prototypeManager.RegisterIgnore("nukeopsRole"); _prototypeManager.RegisterIgnore("stationGoal"); + _prototypeManager.RegisterIgnore("npcConversationTree"); _componentFactory.GenerateNetIds(); _adminManager.Initialize(); diff --git a/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs b/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs index c923738930a..443923f675c 100644 --- a/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs +++ b/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs @@ -54,7 +54,7 @@ private void OnClientTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs SetTypingIndicatorEnabled(uid.Value, ev.IsTyping); } - private void SetTypingIndicatorEnabled(EntityUid uid, bool isEnabled, AppearanceComponent? appearance = null) + public void SetTypingIndicatorEnabled(EntityUid uid, bool isEnabled, AppearanceComponent? appearance = null) { if (!Resolve(uid, ref appearance, false)) return; diff --git a/Content.Server/NPC/Components/NPCConversationComponent.cs b/Content.Server/NPC/Components/NPCConversationComponent.cs new file mode 100644 index 00000000000..c2a8ca31d7d --- /dev/null +++ b/Content.Server/NPC/Components/NPCConversationComponent.cs @@ -0,0 +1,152 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Content.Server.NPC.Events; +using Content.Server.NPC.Prototypes; +using Content.Server.NPC.Systems; + +namespace Content.Server.NPC.Components; + +[RegisterComponent] +[Access(typeof(NPCConversationSystem))] +public sealed partial class NPCConversationComponent : Component +{ + /// + /// Whether or not the listening logic is turned on. + /// + /// + /// Queued responses will still play through, but no new attempts to listen will be made. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("enabled")] + public bool Enabled = true; + + /* NYI: + /// + /// The NPC will pay attention when one of these words are said. + /// + [ViewVariables] + [DataField("aliases")] + public List Aliases = new(); + */ + + [ViewVariables] + [DataField("tree", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] + public string? ConversationTreeId; + + /// + /// This is the cached prototype. + /// + [ViewVariables] + public NPCConversationTreePrototype ConversationTree = default!; + + /// + /// Topics that are unlocked in the NPC's conversation tree. + /// + [ViewVariables] + public HashSet UnlockedTopics = new(); + + /// + /// How long until we stop paying attention to someone for a prompt. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("attentionSpan")] + public TimeSpan AttentionSpan = TimeSpan.FromSeconds(20); + + /// + /// This is the minimum delay before the NPC makes a response. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("delayBeforeResponse")] + public TimeSpan DelayBeforeResponse = TimeSpan.FromSeconds(0.3); + + /// + /// This is the approximate delay per letter typed in text. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("typingDelay")] + public TimeSpan TypingDelay = TimeSpan.FromSeconds(0.05); + + [ViewVariables] + public Stack ResponseQueue = new(); + + /// + /// This is when the NPC will respond with its top response. + /// + [ViewVariables] + [DataField("nextResponse", customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextResponse; + + /// + /// This is the direction the NPC was facing before looking towards a conversation partner. + /// + [ViewVariables] + public Angle OriginalFacing; + + /// + /// This is who the NPC is paying attention to for conversation. + /// + [ViewVariables] + public EntityUid? AttendingTo; + + /// + /// This is when the NPC will stop paying attention to a specific person. + /// + [ViewVariables] + [DataField("nextAttentionLoss", customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextAttentionLoss; + + /// + /// This event is fired the next time the NPC hears something from the + /// person they're speaking with and it takes control of the response. + /// + [ViewVariables] + public NPCConversationListenEvent? ListeningEvent; + +#region Idle Chatter + + /// + /// Whether or not the NPC will say things unprompted. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("idleEnabled")] + public bool IdleEnabled = true; + + /// + /// This is the approximate delay between idle chats. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("idleChatDelay")] + public TimeSpan IdleChatDelay = TimeSpan.FromMinutes(3); + + /// + /// This is the order in which idle chat lines are given. + /// + /// + /// This is randomized both on init and when the lines have been exhausted + /// to prevent repeating lines twice in a row and to avoid predictable patterns. + /// + /// It technically reduces randomness, with the benefit of less repetition. + /// + [ViewVariables(VVAccess.ReadWrite)] + public List IdleChatOrder = new(); + + /// + /// This is the next idle chat line that will be used. + /// + [ViewVariables(VVAccess.ReadWrite)] + public int IdleChatIndex = 0; + + /// + /// This is when the NPC will say something out of its list of idle lines. + /// + /// + /// This is reset every time the NPC speaks. + /// + [ViewVariables] + [DataField("nextIdleChat", customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextIdleChat; + +#endregion + +} + diff --git a/Content.Server/NPC/Events/NPCConversationEvents.cs b/Content.Server/NPC/Events/NPCConversationEvents.cs new file mode 100644 index 00000000000..eb04f59bdd5 --- /dev/null +++ b/Content.Server/NPC/Events/NPCConversationEvents.cs @@ -0,0 +1,63 @@ +using Robust.Shared.Audio; +using Content.Server.NPC.Systems; + +namespace Content.Server.NPC.Events; + +/// +/// This is used for dynamic responses and post-response events. +/// +[ImplicitDataDefinitionForInheritors] +[Access(typeof(NPCConversationSystem))] +public abstract partial class NPCConversationEvent : EntityEventArgs +{ + /// + /// This is the entity that the NPC is speaking to. + /// + public EntityUid? TalkingTo; +} + +/// +/// This event type is raised when an NPC hears a response when it was set to listen for one. +/// +/// +/// Set Handled to true when you want the NPC to stop listening. +/// The NPC will otherwise keep listening and block any attempt to find a prompt in the speaker's words. +/// +[ImplicitDataDefinitionForInheritors] +[Access(typeof(NPCConversationSystem))] +public abstract partial class NPCConversationListenEvent : HandledEntityEventArgs +{ + /// + /// This is the entity that said the message. + /// + public EntityUid? Speaker; + + /// + /// This is the original message that the NPC heard. + /// + public string Message = default!; + + /// + /// This is the message, parsed into separate words. + /// + public List Words = default!; +} + +public sealed partial class NPCConversationHelpEvent : NPCConversationEvent +{ + [DataField("text")] + public string? Text; + + [DataField("audio")] + public SoundSpecifier? Audio; +} + +/// +/// This event can be raised after a response to cause an NPC to stop paying attention to someone. +/// +public sealed partial class NPCConversationByeEvent : NPCConversationEvent { } + +// The following classes help demonstrate some of the features of the system. +// They may be separated out at some point. +public sealed partial class NPCConversationToldNameEvent : NPCConversationListenEvent { } + diff --git a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs new file mode 100644 index 00000000000..20a616d8308 --- /dev/null +++ b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs @@ -0,0 +1,154 @@ +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Content.Server.NPC.Events; + +namespace Content.Server.NPC.Prototypes; + +[Prototype("npcConversationTree")] +public sealed class NPCConversationTreePrototype : IPrototype, ISerializationHooks +{ + [ViewVariables] + [IdDataField] + public string ID { get; } = default!; + + /// + /// Dialogue contains all the topics to which an NPC can discuss. + /// + [ViewVariables] + [DataField("dialogue", required: true)] + public readonly NPCTopic[] Dialogue = default!; + + /// + /// Attention responses are what the NPC says when they start paying + /// attention to you without a specific question or prompt to respond to. + /// + [ViewVariables] + [DataField("attention", required: true)] + public readonly NPCResponse[] Attention = default!; + + /// + /// Idle responses are just things the NPC will say when nothing else is + /// going on, after some time. + /// + [ViewVariables] + [DataField("idle", required: true)] + public readonly NPCResponse[] Idle = default!; + + /// + /// Unknown responses are what the NPC says when they can't respond to a + /// particular question or prompt. + /// + [ViewVariables] + [DataField("unknown", required: true)] + public readonly NPCResponse[] Unknown = default!; + + /// + /// Custom responses are available to use in extensions to the NPC + /// Conversation system. + /// + // NOTE: This may be removed in favor of storing NPCResponses on custom + // components, i.e. an NPCShopkeeperComponent, but for now, it lives here + // to help demonstrate some features. + [ViewVariables] + [DataField("custom")] + public readonly Dictionary Custom = default!; + + /// + /// This exists as a quick way to map a prompt to a topic. + /// + public readonly Dictionary PromptToTopic = new(); + + // ISerializationHooks _is_ obsolete, but ConstructionGraphPrototype is using it as of this commit, + // and I'm not quite sure how to otherwise do this. + // + // I will look at that prototype when ISerializationHooks is phased out. + void ISerializationHooks.AfterDeserialization() + { + // Cache the strings mapping to prompts. + foreach (var topic in Dialogue) + { + foreach (var prompt in topic.Prompts) + { + PromptToTopic[prompt] = topic; + } + } + } +} + +[DataDefinition] +public sealed partial class NPCTopic +{ + [DataField] + public string[] Prompts = default!; + + /// + /// This determines the likelihood of this topic being selected over any + /// other, given the existence of multiple candidates. + /// + [DataField] + public float Weight = 1.0f; + + /// + /// Locked topics will not be accessible through dialogue until unlocked. + /// + [DataField] + public bool Locked; + + /// + /// Hidden topics won't show up in any form of "help" question. + /// + [DataField] + public bool Hidden; + + [DataField("responses", required: true)] + public NPCResponse[] Responses = default!; +} + +[DataDefinition] +public sealed partial class NPCResponse +{ + public NPCResponse() { } + + public NPCResponse(string? text, SoundSpecifier? audio = null, NPCConversationEvent? ev = null) + { + Text = text; + Audio = audio; + Event = ev; + } + + public override string ToString() + { + return $"NPCResponse({Text})"; + } + + [DataField] + public string? Text; + + [DataField] + public SoundSpecifier? Audio; + + /* [DataField("emote")] */ + /* public string? Emote; */ + + /// + /// This event is raised when the response is queued, + /// for the purpose of dynamic responses. + /// + [DataField] + public NPCConversationEvent? Is; + + /// + /// This event is raised after the response is made. + /// + [DataField] + public NPCConversationEvent? Event; + + /// + /// This event is raised when the NPC next hears a response, + /// allowing the response to be processed by other systems. + /// + [DataField] + public NPCConversationListenEvent? ListenEvent; +} + diff --git a/Content.Server/NPC/Systems/NPCConversationSystem.cs b/Content.Server/NPC/Systems/NPCConversationSystem.cs new file mode 100644 index 00000000000..015adb19de5 --- /dev/null +++ b/Content.Server/NPC/Systems/NPCConversationSystem.cs @@ -0,0 +1,558 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.RegularExpressions; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timing; +using Content.Server.Chat.Systems; +using Content.Server.Chat.TypingIndicator; +using Content.Server.NPC.HTN; +using Content.Server.NPC.Components; +using Content.Server.NPC.Events; +using Content.Server.NPC.Prototypes; +using Content.Server.Speech; +using Content.Shared.Interaction; +using Content.Server.Radio.Components; + +namespace Content.Server.NPC.Systems; + +public sealed class NPCConversationSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + [Dependency] private readonly ChatSystem _chatSystem = default!; + [Dependency] private readonly NPCSystem _npcSystem = default!; + [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!; + [Dependency] private readonly TransformSystem _transformSystem = default!; + [Dependency] private readonly TypingIndicatorSystem _typingIndicatorSystem = default!; + + private ISawmill _sawmill = default!; + + // TODO: attention attenuation. distance, facing, visible + // TODO: attending to multiple people, multiple streams of conversation + // TODO: multi-word prompts + // TODO: nameless prompting (pointing is good) + // TODO: aliases + + public static readonly string[] QuestionWords = { "who", "what", "when", "why", "where", "how" }; + public static readonly string[] Copulae = { "is", "are" }; + + public override void Initialize() + { + base.Initialize(); + + _sawmill = Logger.GetSawmill("npc.conversation"); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnUnpaused); + SubscribeLocalEvent(OnListenAttempt); + SubscribeLocalEvent(OnListen); + + SubscribeLocalEvent(OnBye); + SubscribeLocalEvent(OnHelp); + + SubscribeLocalEvent(OnToldName); + } + +#region API + + /// + /// Toggle the ability of an NPC to listen for topics. + /// + public void EnableConversation(EntityUid uid, bool enable = true, NPCConversationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + component.Enabled = enable; + } + + /// + /// Toggle the NPC's willingness to make idle comments. + /// + public void EnableIdleChat(EntityUid uid, bool enable = true, NPCConversationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + component.IdleEnabled = enable; + } + + /// + /// Return locked status of a dialogue topic. + /// + public bool IsDialogueLocked(EntityUid uid, string option, NPCConversationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return true; + + if (!component.ConversationTree.PromptToTopic.TryGetValue(option, out var topic)) + { + _sawmill.Warning($"Tried to check locked status of missing dialogue option `{option}` on {ToPrettyString(uid)}"); + return true; + } + + if (component.UnlockedTopics.Contains(topic)) + return false; + + return topic.Locked; + } + + /// + /// Unlock dialogue options normally locked in an NPC's conversation tree. + /// + public void UnlockDialogue(EntityUid uid, string option, NPCConversationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + if (component.ConversationTree.PromptToTopic.TryGetValue(option, out var topic)) + component.UnlockedTopics.Add(topic); + else + _sawmill.Warning($"Tried to unlock missing dialogue option `{option}` on {ToPrettyString(uid)}"); + } + + /// + public void UnlockDialogue(EntityUid uid, HashSet options, NPCConversationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + foreach (var option in options) + UnlockDialogue(uid, option, component); + } + + /// + /// Queue a response for an NPC with a visible typing indicator and delay between messages. + /// + /// + /// This can be used as opposed to the typical method. + /// + public void QueueResponse(EntityUid uid, NPCResponse response, NPCConversationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + if (response.Is is {} ev) + { + // This is a dynamic response which will call QueueResponse with static responses of its own. + ev.TalkingTo = component.AttendingTo; + RaiseLocalEvent(uid, (object) ev); + return; + } + + if (component.ResponseQueue.Count == 0) + { + DelayResponse(uid, component, response); + _typingIndicatorSystem.SetTypingIndicatorEnabled(uid, true); + } + + component.ResponseQueue.Push(response); + } + + /// + /// Make an NPC stop paying attention to someone. + /// + public void LoseAttention(EntityUid uid, NPCConversationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + component.AttendingTo = null; + component.ListeningEvent = null; + _rotateToFaceSystem.TryFaceAngle(uid, component.OriginalFacing); + } + +#endregion + + private void DelayResponse(EntityUid uid, NPCConversationComponent component, NPCResponse response) + { + if (response.Text == null) + return; + + component.NextResponse = _gameTiming.CurTime + + component.DelayBeforeResponse + + component.TypingDelay.TotalSeconds * TimeSpan.FromSeconds(response.Text.Length) * + _random.NextDouble(0.9, 1.1); + } + + private IEnumerable GetAvailableTopics(EntityUid uid, NPCConversationComponent component) + { + HashSet availableTopics = new(); + + foreach (var topic in component.ConversationTree.Dialogue) + { + if (!topic.Locked || component.UnlockedTopics.Contains(topic)) + availableTopics.Add(topic); + } + + return availableTopics; + } + + private IEnumerable GetVisibleTopics(EntityUid uid, NPCConversationComponent component) + { + HashSet visibleTopics = new(); + + foreach (var topic in component.ConversationTree.Dialogue) + { + if (!topic.Hidden && (!topic.Locked || component.UnlockedTopics.Contains(topic))) + visibleTopics.Add(topic); + } + + return visibleTopics; + } + + private void OnInit(EntityUid uid, NPCConversationComponent component, ComponentInit args) + { + if (component.ConversationTreeId == null) + return; + + component.ConversationTree = _prototype.Index(component.ConversationTreeId); + component.NextIdleChat = _gameTiming.CurTime + component.IdleChatDelay; + + for (var i = 0; i < component.ConversationTree.Idle.Length; ++i) + component.IdleChatOrder.Add(i); + + _random.Shuffle(component.IdleChatOrder); + } + + private void OnUnpaused(EntityUid uid, NPCConversationComponent component, ref EntityUnpausedEvent args) + { + component.NextResponse += args.PausedTime; + component.NextAttentionLoss += args.PausedTime; + component.NextIdleChat += args.PausedTime; + } + + private bool TryGetIdleChatLine(EntityUid uid, NPCConversationComponent component, [NotNullWhen(true)] out NPCResponse? line) + { + line = null; + + if (component.IdleChatOrder.Count() == 0) + return false; + + if (++component.IdleChatIndex == component.IdleChatOrder.Count()) + { + // Exhausted all lines in the pre-shuffled order. + // Reset the index and shuffle again. + component.IdleChatIndex = 0; + _random.Shuffle(component.IdleChatOrder); + } + + var index = component.IdleChatOrder[component.IdleChatIndex]; + + line = component.ConversationTree.Idle[index]; + + return true; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var component)) + { + var curTime = _gameTiming.CurTime; + + if (curTime >= component.NextResponse && component.ResponseQueue.Count > 0) + { + // Make a response. + Respond(uid, component, component.ResponseQueue.Pop()); + } + + if (curTime >= component.NextAttentionLoss && component.AttendingTo != null) + { + // Forget who we were talking to. + LoseAttention(uid, component); + } + + if (component.IdleEnabled && + curTime >= component.NextIdleChat && + TryGetIdleChatLine(uid, component, out var line)) + { + Respond(uid, component, line); + } + } + } + + private void OnListenAttempt(EntityUid uid, NPCConversationComponent component, ListenAttemptEvent args) + { + if (!component.Enabled || + // Don't listen to myself... + uid == args.Source || + // Don't bother listening to other NPCs. For now. + HasComp(args.Source) || + // We're already "typing" a response, so do that first. + component.ResponseQueue.Count > 0) + { + args.Cancel(); + } + } + + private void PayAttentionTo(EntityUid uid, NPCConversationComponent component, EntityUid speaker) + { + component.AttendingTo = speaker; + component.NextAttentionLoss = _gameTiming.CurTime + component.AttentionSpan; + component.OriginalFacing = _transformSystem.GetWorldRotation(uid); + } + + private void Respond(EntityUid uid, NPCConversationComponent component, NPCResponse response) + { + if (component.ResponseQueue.Count == 0) + _typingIndicatorSystem.SetTypingIndicatorEnabled(uid, false); + else + DelayResponse(uid, component, component.ResponseQueue.Peek()); + + if (component.AttendingTo != null) + { + // TODO: This line is a mouthful. Maybe write a public API that supports EntityCoordinates later? + var speakerCoords = Transform(component.AttendingTo.Value).Coordinates.ToMap(EntityManager, _transformSystem).Position; + _rotateToFaceSystem.TryFaceCoordinates(uid, speakerCoords); + } + + if (response.Event is {} ev) + { + ev.TalkingTo = component.AttendingTo; + RaiseLocalEvent(uid, (object) ev); + } + + if (response.ListenEvent != null) + component.ListeningEvent = response.ListenEvent; + + if (response.Text != null) + _chatSystem.TrySendInGameICMessage(uid, Loc.GetString(response.Text), InGameICChatType.Speak, false); + + if (response.Audio != null) + _audioSystem.PlayPvs(response.Audio, uid, + // TODO: Allow this to be configured per NPC/response. + AudioParams.Default + .WithVolume(8f) + .WithMaxDistance(9f) + .WithRolloffFactor(0.5f)); + + // Refresh our attention. + component.NextAttentionLoss = _gameTiming.CurTime + component.AttentionSpan; + component.NextIdleChat = component.NextAttentionLoss + component.IdleChatDelay; + } + + private List ParseMessageIntoWords(string message) + { + return Regex.Replace(message.Trim().ToLower(), @"(\p{P})", "") + .Split() + .ToList(); + } + + private bool FindResponse(EntityUid uid, NPCConversationComponent component, List words, [NotNullWhen(true)] out NPCResponse? response) + { + response = null; + + var availableTopics = GetAvailableTopics(uid, component); + + // Some topics are more interesting than others. + var greatestWeight = 0f; + NPCTopic? candidate = null; + + foreach (var word in words) + { + if (component.ConversationTree.PromptToTopic.TryGetValue(word, out var topic) && + availableTopics.Contains(topic) && + topic.Weight > greatestWeight) + { + greatestWeight = topic.Weight; + candidate = topic; + } + } + + if (candidate != null) + { + response = _random.Pick(candidate.Responses); + return true; + } + + return false; + } + + private bool JudgeQuestionLikelihood(EntityUid uid, NPCConversationComponent component, List words, string message) + { + if (message.Length > 0 && message[^1] == '?') + // A question mark is an absolute mark of a question. + return true; + + if (words.Count == 1) + // The usefulness of this is dubious, but it's definitely a question. + return QuestionWords.Contains(words[0]); + + if (words.Count >= 2) + return QuestionWords.Contains(words[0]) && Copulae.Contains(words[1]); + + return false; + } + + private void OnBye(EntityUid uid, NPCConversationComponent component, NPCConversationByeEvent args) + { + LoseAttention(uid, component); + } + + private void OnHelp(EntityUid uid, NPCConversationComponent component, NPCConversationHelpEvent args) + { + if (args.Text == null) + { + _sawmill.Error($"{ToPrettyString(uid)} heard a Help prompt but has no text for it."); + return; + } + + var availableTopics = GetVisibleTopics(uid, component); + var availablePrompts = availableTopics.Select(topic => topic.Prompts.FirstOrDefault()).ToArray(); + + string availablePromptsText; + if (availablePrompts.Count() <= 2) + { + availablePromptsText = Loc.GetString(args.Text, + ("availablePrompts", string.Join(" or ", availablePrompts)) + ); + } + else + { + availablePrompts[^1] = $"or {availablePrompts[^1]}"; + availablePromptsText = Loc.GetString(args.Text, + ("availablePrompts", string.Join(", ", availablePrompts)) + ); + } + + // Unlikely we'll be able to do audio that isn't hard-coded, + // so best to keep it general. + var response = new NPCResponse(availablePromptsText, args.Audio); + QueueResponse(uid, response, component); + } + + private void OnToldName(EntityUid uid, NPCConversationComponent component, NPCConversationListenEvent args) + { + if (!component.ConversationTree.Custom.TryGetValue("toldName", out var responses)) + return; + + var response = _random.Pick(responses); + if (response.Text == null) + { + _sawmill.Error($"{ToPrettyString(uid)} was told a name but had no text response."); + return; + } + + // The world's simplest heuristic for names: + if (args.Words.Count > 3) + { + // It didn't seem like a name, so wait for something that does. + return; + } + + var cleanedName = string.Join(" ", args.Words); + cleanedName = char.ToUpper(cleanedName[0]) + cleanedName.Remove(0, 1); + + var formattedResponse = new NPCResponse(Loc.GetString(response.Text, + ("name", cleanedName)), + response.Audio); + + QueueResponse(uid, formattedResponse, component); + args.Handled = true; + } + + private void OnListen(EntityUid uid, NPCConversationComponent component, ListenEvent args) + { + if (HasComp(args.Source)) + return; + + if (component.AttendingTo != null && component.AttendingTo != args.Source) + // Ignore someone speaking to us if we're already paying attention to someone else. + return; + + var words = ParseMessageIntoWords(args.Message); + if (words.Count == 0) + return; + + if (component.AttendingTo == args.Source) + { + // The person we're talking to said something to us. + + if (component.ListeningEvent is {} ev) + { + // We were waiting on this person to say something, and they've said something. + ev.Handled = false; + ev.Speaker = component.AttendingTo; + ev.Message = args.Message; + ev.Words = words; + RaiseLocalEvent(uid, (object) ev); + + if (ev.Handled) + component.ListeningEvent = null; + + return; + } + + // We're already paying attention to this person, + // so try to figure out if they said something we can talk about. + if (FindResponse(uid, component, words, out var response)) + { + // A response was found so go ahead with it. + QueueResponse(uid, response, component); + } + else if(JudgeQuestionLikelihood(uid, component, words, args.Message)) + { + // The message didn't match any of the prompts, but it seemed like a question. + var unknownResponse = _random.Pick(component.ConversationTree.Unknown); + QueueResponse(uid, unknownResponse, component); + } + + // If the message didn't seem like a question, + // and it didn't raise any of our topics, + // then politely ignore who we're talking with. + // + // It's better than spamming them with "I don't understand." + return; + } + + // See if someone said our name. + var myName = MetaData(uid).EntityName.ToLower(); + + // So this is a rough heuristic, but if our name occurs within the first three words, + // or is the very last one, someone might be trying to talk to us. + var payAttention = words[0] == myName || words[^1] == myName; + if (!payAttention) + { + for (int i = 1; i < Math.Min(2, words.Count); ++i) + { + if (words[i] == myName) + { + payAttention = true; + break; + } + } + } + + if (payAttention) + { + PayAttentionTo(uid, component, args.Source); + + if (!FindResponse(uid, component, words, out var response)) + { + if(JudgeQuestionLikelihood(uid, component, words, args.Message) && + // This subcondition exists to block our name being interpreted as a question in its own right. + words.Count > 1) + { + response = _random.Pick(component.ConversationTree.Unknown); + } + else + { + response = _random.Pick(component.ConversationTree.Attention); + } + } + + QueueResponse(uid, response, component); + } + } +} + diff --git a/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs b/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs index b1a6c1e9de1..ba5ff0a056d 100644 --- a/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs +++ b/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs @@ -1,5 +1,8 @@ using Content.Server.Psionics.Abilities; using Content.Server.Chat.Systems; +using Content.Server.NPC.Events; +using Content.Server.NPC.Systems; +using Content.Server.NPC.Prototypes; using Content.Server.Radio.Components; using Content.Server.Radio.EntitySystems; using Content.Server.StationEvents.Events; @@ -18,6 +21,8 @@ public sealed partial class SophicScribeSystem : EntitySystem [Dependency] private readonly RadioSystem _radioSystem = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly NPCConversationSystem _conversationSystem = default!; + protected ISawmill Sawmill = default!; public override void Update(float frameTime) { @@ -51,6 +56,32 @@ public override void Initialize() SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnGlimmerEventEnded); + SubscribeLocalEvent(OnGetGlimmer); + } + + private void OnGetGlimmer(EntityUid uid, SophicScribeComponent component, NPCConversationGetGlimmerEvent args) + { + if (args.Text == null) + { + Sawmill.Error($"{uid} heard a glimmer reading prompt but has no text for it"); + return; + } + + var tier = _glimmerSystem.GetGlimmerTier() switch + { + GlimmerTier.Minimal => Loc.GetString("glimmer-reading-minimal"), + GlimmerTier.Low => Loc.GetString("glimmer-reading-low"), + GlimmerTier.Moderate => Loc.GetString("glimmer-reading-moderate"), + GlimmerTier.High => Loc.GetString("glimmer-reading-high"), + GlimmerTier.Dangerous => Loc.GetString("glimmer-reading-dangerous"), + _ => Loc.GetString("glimmer-reading-critical"), + }; + + var glimmerReadingText = Loc.GetString(args.Text, + ("glimmer", (int) Math.Round(_glimmerSystem.GlimmerOutput)), ("tier", tier)); + + var response = new NPCResponse(glimmerReadingText); + _conversationSystem.QueueResponse(uid, response); } private void OnInteractHand(EntityUid uid, SophicScribeComponent component, InteractHandEvent args) @@ -83,4 +114,9 @@ private void OnGlimmerEventEnded(GlimmerEventEndedEvent args) _radioSystem.SendRadioMessage(speaker, message, channel, speaker); } } + public sealed partial class NPCConversationGetGlimmerEvent : NPCConversationEvent + { + [DataField] + public string? Text; + } } diff --git a/Resources/Locale/en-US/npc/conversation/sophia.ftl b/Resources/Locale/en-US/npc/conversation/sophia.ftl new file mode 100644 index 00000000000..c832d9fc17f --- /dev/null +++ b/Resources/Locale/en-US/npc/conversation/sophia.ftl @@ -0,0 +1,82 @@ +sophia-response-name = You may call me Sophia. +sophia-response-help = You may inquire about one of the following topics: {$availablePrompts}. + +sophia-response-hello-1 = Greetings. +sophia-response-hello-2 = Salutations. + +sophia-response-bye-1 = Fare thee well. +sophia-response-bye-2 = Gods be with you. +sophia-response-bye-3 = Come back wiser. + +sophia-idle-phrase-1 = Mmmm, another portent. +sophia-idle-phrase-2 = The noösphere is quite beautiful today. However, I don't think I could describe it in a way you could understand. +sophia-idle-phrase-3 = I've been here before. You have, too. + +sophia-response-attention-1 = What is it? +sophia-response-attention-2 = What do you seek? +sophia-response-attention-3 = Out with it. + +sophia-response-sorry-1 = That's not a question for me. +sophia-response-sorry-2 = Ask someone else. +sophia-response-sorry-3 = Maybe I know the answer, maybe I do not. Either way, I will not be answering that question. + +sophia-response-nature = My nature doesn't really matter, does it? I'm fulfilling my purpose. Can you say the same, or are you just wasting time? + +sophia-response-epi = 'Epistemics' is a word. Aspiring Hellenes they are, they wished to displace the Latin 'science.' However, in English, epistemics has undesired connotations as a study of knowledge itself, even though the Greek word is a literal replacement for 'science.' + +sophia-response-mantis = 'Mantis' means seer, soothsayer, or prophet. They must be so named because they seek to uncover the truth. And, fittingly with their psionic aptitude, 'mantis' and 'mind' both descend, to the best of our knowledge, from an absolutely ancient word that sounded something like 'mentis.' + +sophia-response-mystagogue = 'Mystagogue' literally means 'leader of the mystics.' You may know the suffix -gogue from 'demogogue.' + +sophia-response-oracle = Oracle? I don't know much about her, and she isn't keen to share her secrets with me. + +sophia-response-psionics = Psionics are extraordinary abilities originating from one's mind. There doesn't seem to be any dominant word to refer to someone with the ability to practice these, although I prefer 'psion' or 'psychic.' + +sophia-response-noosphere = The noösphere is a field connecting all of consciousness. It's the medium through which psionics works. Its strength and effects on the illusory world of the material are based on its pressure. Colloquially, noöspheric pressure is called 'glimmer.' + +sophia-response-god = 'God' is such a vague term. There are so many entities out there that have defeated mortality. How you choose to regard them is your business. + +sophia-response-morphotype = In the first century PCC, several entities reshaped men into their image. I had done the same, if you would believe it. I can offer no evidence of their existence, other than faint memories. Any specific morphotype you want to know about? + +sophia-response-calendar = It's currently 417 PCC. The casuality crisis neccesitated a new year to count from. Due to the nature of the crisis, it can only be said with certainty that 1 PCC is between 2400 and 2700 CE. + +sophia-response-crisis = The first FTL travel was incompatible with the old ways. Fortunately, its resolution made more apparent the inherent futility in trying to give one history, one narrative, one account. Truth cannot be found in the material world, only higher ones. + +sophia-response-metempsychosis = You've died thousands of times, and you'll die thousands more. Some of those lives you may dedicate to trying to stop the cycle. We all carry at least some memory of past lives, usually temporally recent ones. One of the great mysteries of the persistence of fragments is the high concentration of memories from the early 21st century CE, which, inverse to other periods, seem to be more common among the ignorant. + +sophia-response-truth = If you seek the truth, you're in the wrong place. From a perspective tainted by material reality, the best you can hope is to try and divine higher truths that are not dependent on it. + +sophia-response-job = I observe the glimmer here, and record it. + +sophia-response-human = Humans were the base for all the others. But they, too, were shaped. Long, long before the others. + +sophia-response-felinid = Felinids were the first, and the most willing. In true feline nature, they shaped themselves. + +sophia-response-oni = Oni, it is said, originated in Sirius. The brightest star in the night sky from Earth may have attracted some chromatically inclined entities, explaining their vivid coloring. But, that's just speculation. + +sophia-response-arachne = Arachne are the strangest of them. They're not fully mortal. They took the form of humans, but not their genes. Their creator wrote his name in their stead. + +sophia-response-moth = Moths scarecely look human, but, strangely, their genes confirm they are. Their creator shares his name with a genus of moths, and was responsible for the other outlier. + +sophia-response-lamiae = So, you remember? You must be remembering their mythological namesake. If you've really retained that fleeting memory over so many metempsychoses... Perhaps I've said too much. + +sophia-response-cyno = Were those... no... So faint. Ignorance! You cannot remember them! It's impossible! + +sophia-response-harpy = Harpies, it is said, were once men and women, sculpted by greed for a purpose long gone. They were abandoned by their creators on a world named Valerian 4b. + +sophia-response-valerian = The Harpy homeworld? Magestic mountains gleaming in white, forests of brilliant scarlet, oceans wine dark, yet no light to be seen by mortal eyes. The Harpies were made to thrive there. To them, their world was bathed in beautiful silver light. + +sophia-response-grue = You do not know of those. You cannot. I had so hoped to live a few cycles under normal causality. + +sophia-response-abraxas = That's a name of power, and I avoid speaking of him. He's the least content to rest, and the most infatuated with creating things from ignorance. + +sophia-response-zork = You wander into the slavering fangs of a hungry grue. There, did you enjoy this game? + +sophia-response-glimmer = The current glimmer reading is {$glimmer}. {$tier} + +glimmer-reading-minimal = That is extremely low. Nothing bad will happen, but I hope this is not at the cost of progression in your understanding of the universe. +glimmer-reading-low = That is quite low. Just barely enough to register any psionic activity here. +glimmer-reading-moderate = That is about the expected level on a psionically active station. You may notice manageable, minor effects. +glimmer-reading-high = That is sure to start attracting attention, although still quite manageable. +glimmer-reading-dangerous = That's a bit concerning. You may want to redirect efforts to reducing it. +glimmer-reading-critical = That's apocalyptic, in the original sense of the word. That is, to say, revealing. This is the sort of time and place to acquire secret knowledge. diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml index 8e34a07ea5e..5213608d95e 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml @@ -1,7 +1,7 @@ - type: entity parent: BaseStructure id: SophicScribe - name: sophie + name: Sophie description: Latest reports on the Noösphere! components: - type: Sprite @@ -27,6 +27,10 @@ channels: - Common - Science + - type: ActiveListener + - type: TypingIndicator + - type: NPCConversation + tree: SophiaTree - type: PotentialPsionic #this makes her easier to access for glimmer events, dw about it - type: Psionic psychicFeedback: @@ -39,3 +43,191 @@ - type: GuideHelp guides: - Psionics + +- type: npcConversationTree + id: SophiaTree + dialogue: + - prompts: [glimmer, reading] + responses: + - is: !type:NPCConversationGetGlimmerEvent + text: sophia-response-glimmer + + - prompts: [purpose, job, occupation, profession] + weight: 0.5 + responses: + - text: sophia-response-job + + - prompts: [help, topics] + weight: 0.5 + hidden: true + responses: + - is: !type:NPCConversationHelpEvent + text: sophia-response-help + + - prompts: [nature, statue, snake, being] + weight: 0.3 + responses: + - text: sophia-response-nature + + - prompts: [epistemics, epi] + weight: 0.2 + responses: + - text: sophia-response-epi + + - prompts: [mantis] + weight: 0.2 + responses: + - text: sophia-response-mantis + + - prompts: [mystagogue, mysta] + weight: 0.2 + responses: + - text: sophia-response-mystagogue + + - prompts: [psionics, psychic] + weight: 0.2 + responses: + - text: sophia-response-psionics + + - prompts: [noösphere, noosphere] + weight: 0.2 + responses: + - text: sophia-response-noosphere + + - prompts: [metempsychosis, metempsychoses, reincarnation, death, dying, afterlife] + weight: 0.2 + responses: + - text: sophia-response-metempsychosis + + - prompts: [calendar] + weight: 0.2 + responses: + - text: sophia-response-calendar + + - prompts: [morphotypes, morphotype, species] + weight: 0.2 + responses: + - text: sophia-response-morphotype + + - prompts: [gods, god] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-god + + - prompts: [truth, "true", "false", falsity, falsehood] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-truth + + - prompts: [human, humans, humanoid, unmutated] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-human + + - prompts: [felinid, felinids, felid, felids, catperson, catpeople] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-felinid + + - prompts: [oni, onis] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-oni + + - prompts: [arachne, arachnid, arachnids, spiderperson, spiderpeople] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-arachne + + - prompts: [moth, moths, moff, moths] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-moth + + - prompts: [lamiae, lamia, lamias] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-lamiae + + - prompts: [grue, grues, batperson, batpeople] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-grue + + - prompts: [cynocephalus, cynocephali, cyno, cynos] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-cyno + + - prompts: [harpy, harpies] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-harpy + + - prompts: [valerian, Valerian, 4b] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-valerian + + - prompts: [crisis, causality] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-crisis + + - prompts: [oracle] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-oracle + + - prompts: [abraxas] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-abraxas + + - prompts: [hi, hello, hey, greetings, salutations] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-hello-1 + - text: sophia-response-hello-2 + + - prompts: [bye, goodbye, done, farewell, later, seeya] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-bye-1 + event: !type:NPCConversationByeEvent + - text: sophia-response-bye-2 + event: !type:NPCConversationByeEvent + - text: sophia-response-bye-3 + event: !type:NPCConversationByeEvent + + attention: + - text: sophia-response-attention-1 + - text: sophia-response-attention-2 + - text: sophia-response-attention-3 + + idle: + - text: sophia-idle-phrase-1 + - text: sophia-idle-phrase-2 + - text: sophia-idle-phrase-3 + + unknown: + - text: sophia-response-sorry-1 + - text: sophia-response-sorry-2 + - text: sophia-response-sorry-3 From 4752115623ee410cd30942a1c67beab3c4f073ae Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Tue, 2 Jul 2024 01:54:53 -0400 Subject: [PATCH 21/56] First attempt at the error --- Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs index 20a616d8308..a147cf1f74b 100644 --- a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs +++ b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs @@ -68,8 +68,12 @@ void ISerializationHooks.AfterDeserialization() // Cache the strings mapping to prompts. foreach (var topic in Dialogue) { + if (topic == null) + continue; foreach (var prompt in topic.Prompts) { + if (prompt == null) + continue; PromptToTopic[prompt] = topic; } } From 10803e1cbce75b27750646842ecffeb7071598b3 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Tue, 2 Jul 2024 03:01:49 -0400 Subject: [PATCH 22/56] Trying this --- Content.Server/Entry/EntryPoint.cs | 1 + Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 28687d17cd7..2e8b298d080 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -70,6 +70,7 @@ public override void Init() prototypes.RegisterIgnore("parallax"); prototypes.RegisterIgnore("guideEntry"); + prototypes.RegisterIgnore("npcConversationTree"); ServerContentIoC.Register(); diff --git a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs index a147cf1f74b..d1280723ec8 100644 --- a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs +++ b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs @@ -6,11 +6,11 @@ namespace Content.Server.NPC.Prototypes; [Prototype("npcConversationTree")] -public sealed class NPCConversationTreePrototype : IPrototype, ISerializationHooks +public sealed partial class NPCConversationTreePrototype : IPrototype, ISerializationHooks { [ViewVariables] [IdDataField] - public string ID { get; } = default!; + public string ID { get; private set; } = default!; /// /// Dialogue contains all the topics to which an NPC can discuss. From 7b89ce1326c74e9d813ac56b497951c867b3af3e Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Tue, 2 Jul 2024 03:15:06 -0400 Subject: [PATCH 23/56] Cherrypick "Fix StrippableSystem Blunders" (#504) --- Content.Server/Strip/StrippableSystem.cs | 16 +++++++++++----- .../Strip/Components/StrippableComponent.cs | 4 ++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs index 950411a8e2c..3b38b65a19d 100644 --- a/Content.Server/Strip/StrippableSystem.cs +++ b/Content.Server/Strip/StrippableSystem.cs @@ -122,13 +122,12 @@ public override void StartOpeningStripper(EntityUid user, Entity strippable, ref StrippingSlotButtonPressed args) { if (args.Session.AttachedEntity is not { Valid: true } user || - !TryComp(user, out var userHands) || - !TryComp(strippable.Owner, out var targetHands)) + !TryComp(user, out var userHands)) return; if (args.IsHand) { - StripHand((user, userHands), (strippable.Owner, targetHands), args.Slot, strippable); + StripHand((user, userHands), (strippable.Owner, null), args.Slot, strippable); return; } @@ -478,6 +477,9 @@ private void StripInsertHand( !Resolve(target, ref target.Comp)) return; + if (!CanStripInsertHand(user, target, held, handName)) + return; + _handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: user.Comp); _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: stealth, animate: stealth, handsComp: target.Comp); _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands"); @@ -542,7 +544,7 @@ private void StartStripRemoveHand( var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay); if (!stealth) - _popupSystem.PopupEntity( Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target); + _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target); var prefix = stealth ? "stealthily " : ""; _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands"); @@ -569,12 +571,16 @@ private void StripRemoveHand( Entity user, Entity target, EntityUid item, + string handName, bool stealth) { if (!Resolve(user, ref user.Comp) || !Resolve(target, ref target.Comp)) return; + if (!CanStripRemoveHand(user, target, item, handName)) + return; + _handsSystem.TryDrop(target, item, checkActionBlocker: false, handsComp: target.Comp); _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: stealth, handsComp: user.Comp); _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands"); @@ -625,7 +631,7 @@ private void OnStrippableDoAfterFinished(Entity entity, ref Stri { if (ev.InsertOrRemove) StripInsertHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden); - else StripRemoveHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.Args.Hidden); + else StripRemoveHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden); } } } diff --git a/Content.Shared/Strip/Components/StrippableComponent.cs b/Content.Shared/Strip/Components/StrippableComponent.cs index 8bf09c3f4c6..4faca4d8f21 100644 --- a/Content.Shared/Strip/Components/StrippableComponent.cs +++ b/Content.Shared/Strip/Components/StrippableComponent.cs @@ -35,11 +35,11 @@ public sealed class StrippingEnsnareButtonPressed : BoundUserInterfaceMessage; public abstract class BaseBeforeStripEvent(TimeSpan initialTime, bool stealth = false) : EntityEventArgs, IInventoryRelayEvent { public readonly TimeSpan InitialTime = initialTime; - public TimeSpan Multiplier = TimeSpan.FromSeconds(1f); + public float Multiplier = 1f; public TimeSpan Additive = TimeSpan.Zero; public bool Stealth = stealth; - public TimeSpan Time => TimeSpan.FromSeconds(MathF.Max(InitialTime.Seconds * Multiplier.Seconds + Additive.Seconds, 0f)); + public TimeSpan Time => TimeSpan.FromSeconds(MathF.Max(InitialTime.Seconds * Multiplier + Additive.Seconds, 0f)); public SlotFlags TargetSlots { get; } = SlotFlags.GLOVES; } From 606a28d11e706681ec23f0db32eb01ddc0534a2d Mon Sep 17 00:00:00 2001 From: WarMechanic <69510347+WarMechanic@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:18:27 +1000 Subject: [PATCH 24/56] Fix Loadouts Breaking when You Spend All Your Points (#506) # Description Self explanatory P.S. i genuinely dont know what the fuck i did, who wrote this? # TODO - [X] Fix shit # Media https://youtu.be/hbJbd5SgZ54 # Changelog :cl: - fix: Fixed loadouts becoming uneditable after spending all your points --- Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs index c797f02a754..04810b07719 100644 --- a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs @@ -2086,7 +2086,7 @@ void AddSelector(LoadoutPreferenceSelector selector, int points, string id) selector.PreferenceChanged += preference => { // Make sure they have enough loadout points - preference = preference ? CheckPoints(points, preference) : CheckPoints(-points, preference); + preference = preference ? CheckPoints(-points, preference) : CheckPoints(points, preference); // Update Preferences Profile = Profile?.WithLoadoutPreference(id, preference); From 0d0dd4c01bfd859004b3b399d482187e6ba58ea8 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Tue, 2 Jul 2024 07:18:51 +0000 Subject: [PATCH 25/56] Automatic Changelog Update (#506) --- Resources/Changelog/Changelog.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 515c1832db1..e4e812edeb4 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4265,3 +4265,9 @@ Entries: rather than fall at their feet. id: 6133 time: '2024-07-01T22:37:29.0000000+00:00' +- author: WarMechanic + changes: + - type: Fix + message: Fixed loadouts becoming uneditable after spending all your points + id: 6134 + time: '2024-07-02T07:18:27.0000000+00:00' From eaacdeaa57ee06826f18060553ef78abe575828d Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Tue, 2 Jul 2024 03:37:28 -0400 Subject: [PATCH 26/56] Revert "Trying this" This reverts commit 10803e1cbce75b27750646842ecffeb7071598b3. --- Content.Server/Entry/EntryPoint.cs | 1 - Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 2e8b298d080..28687d17cd7 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -70,7 +70,6 @@ public override void Init() prototypes.RegisterIgnore("parallax"); prototypes.RegisterIgnore("guideEntry"); - prototypes.RegisterIgnore("npcConversationTree"); ServerContentIoC.Register(); diff --git a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs index d1280723ec8..a147cf1f74b 100644 --- a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs +++ b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs @@ -6,11 +6,11 @@ namespace Content.Server.NPC.Prototypes; [Prototype("npcConversationTree")] -public sealed partial class NPCConversationTreePrototype : IPrototype, ISerializationHooks +public sealed class NPCConversationTreePrototype : IPrototype, ISerializationHooks { [ViewVariables] [IdDataField] - public string ID { get; private set; } = default!; + public string ID { get; } = default!; /// /// Dialogue contains all the topics to which an NPC can discuss. From 5fc1ce81388dc91f3a1ae1887e91d9835651bf14 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Tue, 2 Jul 2024 03:37:31 -0400 Subject: [PATCH 27/56] Revert "First attempt at the error" This reverts commit 4752115623ee410cd30942a1c67beab3c4f073ae. --- Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs index a147cf1f74b..20a616d8308 100644 --- a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs +++ b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs @@ -68,12 +68,8 @@ void ISerializationHooks.AfterDeserialization() // Cache the strings mapping to prompts. foreach (var topic in Dialogue) { - if (topic == null) - continue; foreach (var prompt in topic.Prompts) { - if (prompt == null) - continue; PromptToTopic[prompt] = topic; } } From ffb9ec979ff9ffe76d81eeffa84cea0be45ac2fc Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:00:14 +0300 Subject: [PATCH 28/56] Unflip Hands for Felinid, Vulpkanin, Harpy (#503) # Description Cherry-picks https://github.com/DeltaV-Station/Delta-v/pull/1194 This is a minor issue in the yml files of custom species that will become a problem if we ever merge wizden's better hand indicators. This PR shouldn't require a preview; all credit goes to the original author of the fix. --- # Changelog Too minor for a cl. Or as some say, no cl no fun. Co-authored-by: Angelo Fallaria --- Resources/Prototypes/DeltaV/Body/Prototypes/harpy.yml | 5 ++--- Resources/Prototypes/DeltaV/Body/Prototypes/vulpkanin.yml | 6 +++--- .../Nyanotrasen/Entities/Body/Prototypes/felinid.yml | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Resources/Prototypes/DeltaV/Body/Prototypes/harpy.yml b/Resources/Prototypes/DeltaV/Body/Prototypes/harpy.yml index 5b3615c55d8..25988f4a3a8 100644 --- a/Resources/Prototypes/DeltaV/Body/Prototypes/harpy.yml +++ b/Resources/Prototypes/DeltaV/Body/Prototypes/harpy.yml @@ -13,10 +13,10 @@ torso: part: TorsoHarpy connections: - - left arm - right arm - - left leg + - left arm - right leg + - left leg organs: heart: OrganHumanHeart lungs: OrganHarpyLungs @@ -47,4 +47,3 @@ part: RightFootHarpy left foot: part: LeftFootHarpy - diff --git a/Resources/Prototypes/DeltaV/Body/Prototypes/vulpkanin.yml b/Resources/Prototypes/DeltaV/Body/Prototypes/vulpkanin.yml index 3d1552ac81f..cdf787e4736 100644 --- a/Resources/Prototypes/DeltaV/Body/Prototypes/vulpkanin.yml +++ b/Resources/Prototypes/DeltaV/Body/Prototypes/vulpkanin.yml @@ -1,4 +1,4 @@ -- type: body +- type: body name: "vulpkanin" id: Vulpkanin root: torso @@ -19,10 +19,10 @@ liver: OrganAnimalLiver kidneys: OrganHumanKidneys connections: - - left arm - right arm - - left leg + - left arm - right leg + - left leg right arm: part: RightArmVulpkanin connections: diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Body/Prototypes/felinid.yml b/Resources/Prototypes/Nyanotrasen/Entities/Body/Prototypes/felinid.yml index 6dd2a89e5a8..a09f3b6ab7f 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Body/Prototypes/felinid.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Body/Prototypes/felinid.yml @@ -13,10 +13,10 @@ torso: part: TorsoHuman connections: - - left arm - right arm - - left leg + - left arm - right leg + - left leg organs: heart: OrganAnimalHeart lungs: OrganHumanLungs From 7a124612a1b08b0d46f9baa2025c764d1b3a08de Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:01:35 +0300 Subject: [PATCH 29/56] Cherry-Pick the Secwatch Pda App (#502) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description Cherry-picks https://github.com/DeltaV-Station/Delta-v/pull/1237 All credit goes to the original author, deltanedas Adds a PDA app that lets seccies know who's wanted and who's about to be thrown out of an airlock without relying on the sechud and people having their IDs on them. # Media ![image](https://github.com/Simple-Station/Einstein-Engines/assets/69920617/37f5fa1a-27a5-4392-b4bb-be0f1016b499) (see the original PR for a better preview) # Changelog :cl: deltanedas - add: Security can find the new SecWatch™ app in their PDAs to see current suspects and wanted criminals. Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com> Co-authored-by: Azzy --- .../Cartridges/CrimeAssistUi.cs | 9 --- .../Cartridges/CrimeAssistUiFragment.xaml.cs | 3 - .../Cartridges/SecWatchEntryControl.xaml | 19 +++++ .../Cartridges/SecWatchEntryControl.xaml.cs | 21 ++++++ .../CartridgeLoader/Cartridges/SecWatchUi.cs | 27 +++++++ .../Cartridges/SecWatchUiFragment.xaml | 13 ++++ .../Cartridges/SecWatchUiFragment.xaml.cs | 25 +++++++ .../Cartridges/SecWatchCartridgeComponent.cs | 23 ++++++ .../Cartridges/SecWatchCartridgeSystem.cs | 73 +++++++++++++++++++ .../CrimeAssistCartridgeComponent.cs | 5 -- .../CrimeAssistCartridgeSystem.cs | 16 ---- .../Cartridges/CrimeAssistUiState.cs | 18 ----- .../Cartridges/SecWatchUiState.cs | 24 ++++++ .../deltav/cartridge-loader/secwatch.ftl | 5 ++ .../Entities/Objects/Devices/cartridges.yml | 22 +++++- .../DeltaV/Entities/Objects/Devices/pda.yml | 3 +- .../Entities/Objects/Devices/pda.yml | 24 ++++-- .../Entities/Objects/Devices/pda.yml | 3 +- 18 files changed, 271 insertions(+), 62 deletions(-) create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml.cs create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUi.cs create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml.cs create mode 100644 Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeComponent.cs create mode 100644 Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeSystem.cs delete mode 100644 Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeComponent.cs delete mode 100644 Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeSystem.cs delete mode 100644 Content.Shared/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiState.cs create mode 100644 Content.Shared/DeltaV/CartridgeLoader/Cartridges/SecWatchUiState.cs create mode 100644 Resources/Locale/en-US/deltav/cartridge-loader/secwatch.ftl diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs index ea5aa3cf256..2dbe923b2a6 100644 --- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs @@ -18,15 +18,6 @@ public override Control GetUIFragmentRoot() public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner) { _fragment = new CrimeAssistUiFragment(); - - _fragment.OnSync += _ => SendSyncMessage(userInterface); - } - - private void SendSyncMessage(BoundUserInterface userInterface) - { - var syncMessage = new CrimeAssistSyncMessageEvent(); - var message = new CartridgeUiMessage(syncMessage); - userInterface.SendMessage(message); } public override void UpdateState(BoundUserInterfaceState state) diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml.cs index e3163975d12..fb085a8a799 100644 --- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml.cs +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml.cs @@ -1,7 +1,6 @@ using Content.Client.Message; using Content.Shared.DeltaV.CartridgeLoader.Cartridges; using Robust.Client.AutoGenerated; -using Robust.Client.ResourceManagement; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Prototypes; @@ -13,9 +12,7 @@ namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; public sealed partial class CrimeAssistUiFragment : BoxContainer { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IResourceCache _resourceCache = default!; - public event Action? OnSync; private CrimeAssistPage _currentPage; private List? _pages; diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml new file mode 100644 index 00000000000..2de8a37ff77 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml.cs new file mode 100644 index 00000000000..e8dd4eea446 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml.cs @@ -0,0 +1,21 @@ +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class SecWatchEntryControl : BoxContainer +{ + public SecWatchEntryControl(SecWatchEntry entry) + { + RobustXamlLoader.Load(this); + + Status.Text = Loc.GetString($"criminal-records-status-{entry.Status.ToString().ToLower()}"); + Title.Text = Loc.GetString("sec-watch-entry", ("name", entry.Name), ("job", entry.Job)); + + Reason.Text = entry.Reason ?? Loc.GetString("sec-watch-no-reason"); + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUi.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUi.cs new file mode 100644 index 00000000000..da5ff825b91 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUi.cs @@ -0,0 +1,27 @@ +using Content.Client.UserInterface.Fragments; +using Content.Shared.CartridgeLoader; +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Client.UserInterface; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +public sealed partial class SecWatchUi : UIFragment +{ + private SecWatchUiFragment? _fragment; + + public override Control GetUIFragmentRoot() + { + return _fragment!; + } + + public override void Setup(BoundUserInterface ui, EntityUid? owner) + { + _fragment = new SecWatchUiFragment(); + } + + public override void UpdateState(BoundUserInterfaceState state) + { + if (state is SecWatchUiState cast) + _fragment?.UpdateState(cast); + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml new file mode 100644 index 00000000000..7fb2c42debc --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml @@ -0,0 +1,13 @@ + + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml.cs new file mode 100644 index 00000000000..ad152840529 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml.cs @@ -0,0 +1,25 @@ +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class SecWatchUiFragment : BoxContainer +{ + public SecWatchUiFragment() + { + RobustXamlLoader.Load(this); + } + + public void UpdateState(SecWatchUiState state) + { + NoEntries.Visible = state.Entries.Count == 0; + Entries.RemoveAllChildren(); + foreach (var entry in state.Entries) + { + Entries.AddChild(new SecWatchEntryControl(entry)); + } + } +} diff --git a/Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeComponent.cs b/Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeComponent.cs new file mode 100644 index 00000000000..7ccc90ef797 --- /dev/null +++ b/Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeComponent.cs @@ -0,0 +1,23 @@ +using Content.Shared.Security; + +namespace Content.Server.CartridgeLoader.Cartridges; + +[RegisterComponent, Access(typeof(SecWatchCartridgeSystem))] +public sealed partial class SecWatchCartridgeComponent : Component +{ + /// + /// Only show people with these statuses. + /// + [DataField] + public List Statuses = new() + { + SecurityStatus.Suspected, + SecurityStatus.Wanted + }; + + /// + /// Station entity thats getting its records checked. + /// + [DataField] + public EntityUid? Station; +} diff --git a/Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeSystem.cs b/Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeSystem.cs new file mode 100644 index 00000000000..16da24514cb --- /dev/null +++ b/Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeSystem.cs @@ -0,0 +1,73 @@ +using Content.Server.Station.Systems; +using Content.Server.StationRecords; +using Content.Server.StationRecords.Systems; +using Content.Shared.CartridgeLoader; +using Content.Shared.CartridgeLoader.Cartridges; +using Content.Shared.CriminalRecords; +using Content.Shared.StationRecords; + +namespace Content.Server.CartridgeLoader.Cartridges; + +public sealed class SecWatchCartridgeSystem : EntitySystem +{ + [Dependency] private readonly CartridgeLoaderSystem _cartridgeLoader = default!; + [Dependency] private readonly StationRecordsSystem _records = default!; + [Dependency] private readonly StationSystem _station = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnRecordModified); + + SubscribeLocalEvent(OnUiReady); + } + + private void OnRecordModified(RecordModifiedEvent args) + { + // when a record is modified update the ui of every loaded cartridge tuned to the same station + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp, out var cartridge)) + { + if (cartridge.LoaderUid is not {} loader || comp.Station != args.Station) + continue; + + UpdateUI((uid, comp), loader); + } + } + + private void OnUiReady(Entity ent, ref CartridgeUiReadyEvent args) + { + UpdateUI(ent, args.Loader); + } + + private void UpdateUI(Entity ent, EntityUid loader) + { + // if the loader is on a grid, update the station + // if it is off grid use the cached station + if (_station.GetOwningStation(loader) is {} station) + ent.Comp.Station = station; + + if (!TryComp(ent.Comp.Station, out var records)) + return; + + station = ent.Comp.Station.Value; + + var entries = new List(); + foreach (var (id, criminal) in _records.GetRecordsOfType(station, records)) + { + if (!ent.Comp.Statuses.Contains(criminal.Status)) + continue; + + var key = new StationRecordKey(id, station); + if (!_records.TryGetRecord(key, out var general, records)) + continue; + + var status = criminal.Status; + entries.Add(new SecWatchEntry(general.Name, general.JobTitle, criminal.Status, criminal.Reason)); + } + + var state = new SecWatchUiState(entries); + _cartridgeLoader.UpdateCartridgeUiState(loader, state); + } +} diff --git a/Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeComponent.cs b/Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeComponent.cs deleted file mode 100644 index 741a6134580..00000000000 --- a/Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeComponent.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Content.Server.DeltaV.CartridgeLoader.Cartridges; - -[RegisterComponent] -public sealed partial class CrimeAssistCartridgeComponent : Component -{ } diff --git a/Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeSystem.cs b/Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeSystem.cs deleted file mode 100644 index 06732c2c534..00000000000 --- a/Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeSystem.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Content.Shared.CartridgeLoader; -using Content.Server.DeltaV.CartridgeLoader; -using Content.Server.CartridgeLoader.Cartridges; -using Content.Server.CartridgeLoader; - -namespace Content.Server.DeltaV.CartridgeLoader.Cartridges; - -public sealed class CrimeAssistCartridgeSystem : EntitySystem -{ - [Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!; - - public override void Initialize() - { - base.Initialize(); - } -} diff --git a/Content.Shared/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiState.cs b/Content.Shared/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiState.cs deleted file mode 100644 index dd820f1a0b3..00000000000 --- a/Content.Shared/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiState.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Shared.CartridgeLoader; -using Robust.Shared.Serialization; - -namespace Content.Shared.DeltaV.CartridgeLoader.Cartridges; - -[Serializable, NetSerializable] -public sealed class CrimeAssistUiState : BoundUserInterfaceState -{ - public CrimeAssistUiState() - { } -} - -[Serializable, NetSerializable] -public sealed class CrimeAssistSyncMessageEvent : CartridgeMessageEvent -{ - public CrimeAssistSyncMessageEvent() - { } -} diff --git a/Content.Shared/DeltaV/CartridgeLoader/Cartridges/SecWatchUiState.cs b/Content.Shared/DeltaV/CartridgeLoader/Cartridges/SecWatchUiState.cs new file mode 100644 index 00000000000..068b54a6ffb --- /dev/null +++ b/Content.Shared/DeltaV/CartridgeLoader/Cartridges/SecWatchUiState.cs @@ -0,0 +1,24 @@ +using Content.Shared.Security; +using Robust.Shared.Serialization; + +namespace Content.Shared.CartridgeLoader.Cartridges; + +/// +/// Show a list of wanted and suspected people from criminal records. +/// +[Serializable, NetSerializable] +public sealed class SecWatchUiState : BoundUserInterfaceState +{ + public readonly List Entries; + + public SecWatchUiState(List entries) + { + Entries = entries; + } +} + +/// +/// Entry for a person who is wanted or suspected. +/// +[Serializable, NetSerializable] +public record struct SecWatchEntry(string Name, string Job, SecurityStatus Status, string? Reason); diff --git a/Resources/Locale/en-US/deltav/cartridge-loader/secwatch.ftl b/Resources/Locale/en-US/deltav/cartridge-loader/secwatch.ftl new file mode 100644 index 00000000000..a5b96eab08f --- /dev/null +++ b/Resources/Locale/en-US/deltav/cartridge-loader/secwatch.ftl @@ -0,0 +1,5 @@ +sec-watch-program-name = SecWatch +sec-watch-title = SecWatch 1.0 +sec-watch-no-entries = Everything's calm. Why not enjoy a Monkin Donut? +sec-watch-entry = {$name}, {$job} +sec-watch-no-reason = None given??? diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/cartridges.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/cartridges.yml index e3d5e9d2138..def215cee43 100644 --- a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/cartridges.yml +++ b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/cartridges.yml @@ -17,4 +17,24 @@ icon: sprite: DeltaV/Icons/cri.rsi state: cri - - type: CrimeAssistCartridge + +- type: entity + parent: BaseItem + id: SecWatchCartridge + name: sec watch cartridge + description: A cartridge that tracks the status of currently wanted individuals. + components: + - type: Sprite + sprite: DeltaV/Objects/Devices/cartridge.rsi + state: cart-cri + - type: Icon + sprite: DeltaV/Objects/Devices/cartridge.rsi + state: cart-cri + - type: UIFragment + ui: !type:SecWatchUi + - type: Cartridge + programName: sec-watch-program-name + icon: + sprite: Objects/Weapons/Melee/stunbaton.rsi + state: stunbaton_on + - type: SecWatchCartridge diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/pda.yml index 6ee3a7543f7..d9607390cd7 100644 --- a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/pda.yml @@ -20,12 +20,13 @@ map: [ "enum.PdaVisualLayers.IdLight" ] shader: "unshaded" visible: false - - type: CartridgeLoader # DeltaV - Crime Assist + - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch preinstalled: - CrewManifestCartridge - NotekeeperCartridge - NewsReaderCartridge - CrimeAssistCartridge + - SecWatchCartridge - type: Pda id: BrigmedicIDCard state: pda-corpsman diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index 706cbd5dbbf..7155be68d74 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -173,12 +173,13 @@ accentVColor: "#A32D26" - type: Icon state: pda-interncadet - - type: CartridgeLoader # DeltaV - Crime Assist + - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch preinstalled: - CrewManifestCartridge - NotekeeperCartridge - NewsReaderCartridge - CrimeAssistCartridge + - SecWatchCartridge - type: entity parent: BasePDA @@ -430,12 +431,13 @@ borderColor: "#6f6192" - type: Icon state: pda-lawyer - - type: CartridgeLoader # DeltaV - Crime Assist + - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch preinstalled: - CrewManifestCartridge - NotekeeperCartridge - NewsReaderCartridge - CrimeAssistCartridge + - SecWatchCartridge - type: entity parent: BasePDA @@ -643,12 +645,13 @@ accentHColor: "#447987" - type: Icon state: pda-hos - - type: CartridgeLoader # DeltaV - Crime Assist + - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch preinstalled: - CrewManifestCartridge - NotekeeperCartridge - NewsReaderCartridge - CrimeAssistCartridge + - SecWatchCartridge - type: entity parent: BasePDA @@ -664,12 +667,13 @@ accentVColor: "#949137" - type: Icon state: pda-warden - - type: CartridgeLoader # DeltaV - Crime Assist + - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch preinstalled: - CrewManifestCartridge - NotekeeperCartridge - NewsReaderCartridge - CrimeAssistCartridge + - SecWatchCartridge - type: entity parent: BasePDA @@ -684,12 +688,13 @@ borderColor: "#A32D26" - type: Icon state: pda-security - - type: CartridgeLoader # DeltaV - Crime Assist + - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch preinstalled: - CrewManifestCartridge - NotekeeperCartridge - NewsReaderCartridge - CrimeAssistCartridge + - SecWatchCartridge - type: entity parent: BasePDA @@ -979,12 +984,13 @@ borderColor: "#774705" - type: Icon state: pda-detective - - type: CartridgeLoader # DeltaV - Crime Assist + - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch preinstalled: - CrewManifestCartridge - NotekeeperCartridge - NewsReaderCartridge - CrimeAssistCartridge + - SecWatchCartridge - type: entity parent: BaseMedicalPDA @@ -1001,12 +1007,13 @@ accentVColor: "#d7d7d0" - type: Icon state: pda-brigmedic - - type: CartridgeLoader # DeltaV - Crime Assist + - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch preinstalled: - CrewManifestCartridge - NotekeeperCartridge - NewsReaderCartridge - CrimeAssistCartridge + - SecWatchCartridge - type: entity parent: ClownPDA @@ -1092,12 +1099,13 @@ accentVColor: "#DFDFDF" - type: Icon state: pda-seniorofficer - - type: CartridgeLoader # DeltaV - Crime Assist + - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch preinstalled: - CrewManifestCartridge - NotekeeperCartridge - NewsReaderCartridge - CrimeAssistCartridge + - SecWatchCartridge - type: entity parent: SyndiPDA diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/pda.yml index c2fd8786aff..4e6115ba339 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/pda.yml @@ -32,12 +32,13 @@ accentVColor: "#DFDFDF" - type: Icon state: pda-security - - type: CartridgeLoader # DeltaV - Crime Assist + - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch preinstalled: - CrewManifestCartridge - NotekeeperCartridge - NewsReaderCartridge - CrimeAssistCartridge + - SecWatchCartridge - type: entity parent: BasePDA From 2dcab4d57f74e25f4a1aafa23f48463ad6251516 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Tue, 2 Jul 2024 08:01:57 +0000 Subject: [PATCH 30/56] Automatic Changelog Update (#502) --- Resources/Changelog/Changelog.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index e4e812edeb4..cd2ca0c929f 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4271,3 +4271,11 @@ Entries: message: Fixed loadouts becoming uneditable after spending all your points id: 6134 time: '2024-07-02T07:18:27.0000000+00:00' +- author: deltanedas + changes: + - type: Add + message: >- + Security can find the new SecWatch™ app in their PDAs to see current + suspects and wanted criminals. + id: 6135 + time: '2024-07-02T08:01:36.0000000+00:00' From 2e8e56f763a5cda63511a1aaae0d649ff64eedad Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Wed, 3 Jul 2024 22:48:28 +0300 Subject: [PATCH 31/56] Fix Clothing Quick-Equip (#507) # Description For some reason, ClothingSystem.TryEquip would return false if there's a do-after to equip the clothing. This had caused all quick-equip attempts on SMALL ITEMS to fail and ended up with every SMALL clothing item being equipped into one of the pocket slots (which have no equip delays). Also fixes quick swap - see the comments below. # Changelog :cl: - fix: Equipping clothing using the Z key works correctly again. --- Content.Shared/Clothing/EntitySystems/ClothingSystem.cs | 5 +++++ Content.Shared/Inventory/InventorySystem.Equip.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs index f189db005bc..976682c9903 100644 --- a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs @@ -62,6 +62,11 @@ private void QuickEquip( { foreach (var slotDef in userEnt.Comp1.Slots) { + // Do not attempt to quick-equip clothing in pocket slots. + // We should probably add a special flag to SlotDefinition to skip quick equip if more similar slots get added. + if (slotDef.SlotFlags.HasFlag(SlotFlags.POCKET)) + continue; + if (!_invSystem.CanEquip(userEnt, toEquipEnt, slotDef.Name, out _, slotDef, userEnt, toEquipEnt)) continue; diff --git a/Content.Shared/Inventory/InventorySystem.Equip.cs b/Content.Shared/Inventory/InventorySystem.Equip.cs index 24006b0c9f9..7bdd17ee6fa 100644 --- a/Content.Shared/Inventory/InventorySystem.Equip.cs +++ b/Content.Shared/Inventory/InventorySystem.Equip.cs @@ -176,7 +176,7 @@ public bool TryEquip(EntityUid actor, EntityUid target, EntityUid itemUid, strin }; _doAfter.TryStartDoAfter(args); - return false; + return true; // Changed to return true even if the item wasn't equipped instantly } if (!_containerSystem.Insert(itemUid, slotContainer)) From 6aaf4664ad1b9d150da9af30c27bfc31858de8d6 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Wed, 3 Jul 2024 19:48:50 +0000 Subject: [PATCH 32/56] Automatic Changelog Update (#507) --- Resources/Changelog/Changelog.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index cd2ca0c929f..c4056561c9c 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4279,3 +4279,9 @@ Entries: suspects and wanted criminals. id: 6135 time: '2024-07-02T08:01:36.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Fix + message: Equipping clothing using the Z key works correctly again. + id: 6136 + time: '2024-07-03T19:48:29.0000000+00:00' From 8897a4b4732e16baee30be42dd9925419f4cdd0a Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Thu, 4 Jul 2024 00:16:17 -0400 Subject: [PATCH 33/56] Revert "Don't look at this please." This reverts commit d461f4fe176ab24b8278d17b500424c840a6348d. --- Content.Client/Entry/EntryPoint.cs | 1 - .../TypingIndicator/TypingIndicatorSystem.cs | 2 +- .../Components/NPCConversationComponent.cs | 152 ----- .../NPC/Events/NPCConversationEvents.cs | 63 -- .../NPCConversationTreePrototype.cs | 154 ----- .../NPC/Systems/NPCConversationSystem.cs | 558 ------------------ .../SophicScribe/SophicScribeSystem.cs | 36 -- .../Locale/en-US/npc/conversation/sophia.ftl | 82 --- .../Structures/Research/sophicscribe.yml | 194 +----- 9 files changed, 2 insertions(+), 1240 deletions(-) delete mode 100644 Content.Server/NPC/Components/NPCConversationComponent.cs delete mode 100644 Content.Server/NPC/Events/NPCConversationEvents.cs delete mode 100644 Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs delete mode 100644 Content.Server/NPC/Systems/NPCConversationSystem.cs delete mode 100644 Resources/Locale/en-US/npc/conversation/sophia.ftl diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 8636e0eb6aa..a1fc68bbd2f 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -125,7 +125,6 @@ public override void Init() _prototypeManager.RegisterIgnore("alertLevels"); _prototypeManager.RegisterIgnore("nukeopsRole"); _prototypeManager.RegisterIgnore("stationGoal"); - _prototypeManager.RegisterIgnore("npcConversationTree"); _componentFactory.GenerateNetIds(); _adminManager.Initialize(); diff --git a/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs b/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs index 443923f675c..c923738930a 100644 --- a/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs +++ b/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs @@ -54,7 +54,7 @@ private void OnClientTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs SetTypingIndicatorEnabled(uid.Value, ev.IsTyping); } - public void SetTypingIndicatorEnabled(EntityUid uid, bool isEnabled, AppearanceComponent? appearance = null) + private void SetTypingIndicatorEnabled(EntityUid uid, bool isEnabled, AppearanceComponent? appearance = null) { if (!Resolve(uid, ref appearance, false)) return; diff --git a/Content.Server/NPC/Components/NPCConversationComponent.cs b/Content.Server/NPC/Components/NPCConversationComponent.cs deleted file mode 100644 index c2a8ca31d7d..00000000000 --- a/Content.Server/NPC/Components/NPCConversationComponent.cs +++ /dev/null @@ -1,152 +0,0 @@ -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Content.Server.NPC.Events; -using Content.Server.NPC.Prototypes; -using Content.Server.NPC.Systems; - -namespace Content.Server.NPC.Components; - -[RegisterComponent] -[Access(typeof(NPCConversationSystem))] -public sealed partial class NPCConversationComponent : Component -{ - /// - /// Whether or not the listening logic is turned on. - /// - /// - /// Queued responses will still play through, but no new attempts to listen will be made. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("enabled")] - public bool Enabled = true; - - /* NYI: - /// - /// The NPC will pay attention when one of these words are said. - /// - [ViewVariables] - [DataField("aliases")] - public List Aliases = new(); - */ - - [ViewVariables] - [DataField("tree", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? ConversationTreeId; - - /// - /// This is the cached prototype. - /// - [ViewVariables] - public NPCConversationTreePrototype ConversationTree = default!; - - /// - /// Topics that are unlocked in the NPC's conversation tree. - /// - [ViewVariables] - public HashSet UnlockedTopics = new(); - - /// - /// How long until we stop paying attention to someone for a prompt. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("attentionSpan")] - public TimeSpan AttentionSpan = TimeSpan.FromSeconds(20); - - /// - /// This is the minimum delay before the NPC makes a response. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("delayBeforeResponse")] - public TimeSpan DelayBeforeResponse = TimeSpan.FromSeconds(0.3); - - /// - /// This is the approximate delay per letter typed in text. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("typingDelay")] - public TimeSpan TypingDelay = TimeSpan.FromSeconds(0.05); - - [ViewVariables] - public Stack ResponseQueue = new(); - - /// - /// This is when the NPC will respond with its top response. - /// - [ViewVariables] - [DataField("nextResponse", customTypeSerializer: typeof(TimeOffsetSerializer))] - public TimeSpan NextResponse; - - /// - /// This is the direction the NPC was facing before looking towards a conversation partner. - /// - [ViewVariables] - public Angle OriginalFacing; - - /// - /// This is who the NPC is paying attention to for conversation. - /// - [ViewVariables] - public EntityUid? AttendingTo; - - /// - /// This is when the NPC will stop paying attention to a specific person. - /// - [ViewVariables] - [DataField("nextAttentionLoss", customTypeSerializer: typeof(TimeOffsetSerializer))] - public TimeSpan NextAttentionLoss; - - /// - /// This event is fired the next time the NPC hears something from the - /// person they're speaking with and it takes control of the response. - /// - [ViewVariables] - public NPCConversationListenEvent? ListeningEvent; - -#region Idle Chatter - - /// - /// Whether or not the NPC will say things unprompted. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("idleEnabled")] - public bool IdleEnabled = true; - - /// - /// This is the approximate delay between idle chats. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("idleChatDelay")] - public TimeSpan IdleChatDelay = TimeSpan.FromMinutes(3); - - /// - /// This is the order in which idle chat lines are given. - /// - /// - /// This is randomized both on init and when the lines have been exhausted - /// to prevent repeating lines twice in a row and to avoid predictable patterns. - /// - /// It technically reduces randomness, with the benefit of less repetition. - /// - [ViewVariables(VVAccess.ReadWrite)] - public List IdleChatOrder = new(); - - /// - /// This is the next idle chat line that will be used. - /// - [ViewVariables(VVAccess.ReadWrite)] - public int IdleChatIndex = 0; - - /// - /// This is when the NPC will say something out of its list of idle lines. - /// - /// - /// This is reset every time the NPC speaks. - /// - [ViewVariables] - [DataField("nextIdleChat", customTypeSerializer: typeof(TimeOffsetSerializer))] - public TimeSpan NextIdleChat; - -#endregion - -} - diff --git a/Content.Server/NPC/Events/NPCConversationEvents.cs b/Content.Server/NPC/Events/NPCConversationEvents.cs deleted file mode 100644 index eb04f59bdd5..00000000000 --- a/Content.Server/NPC/Events/NPCConversationEvents.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Robust.Shared.Audio; -using Content.Server.NPC.Systems; - -namespace Content.Server.NPC.Events; - -/// -/// This is used for dynamic responses and post-response events. -/// -[ImplicitDataDefinitionForInheritors] -[Access(typeof(NPCConversationSystem))] -public abstract partial class NPCConversationEvent : EntityEventArgs -{ - /// - /// This is the entity that the NPC is speaking to. - /// - public EntityUid? TalkingTo; -} - -/// -/// This event type is raised when an NPC hears a response when it was set to listen for one. -/// -/// -/// Set Handled to true when you want the NPC to stop listening. -/// The NPC will otherwise keep listening and block any attempt to find a prompt in the speaker's words. -/// -[ImplicitDataDefinitionForInheritors] -[Access(typeof(NPCConversationSystem))] -public abstract partial class NPCConversationListenEvent : HandledEntityEventArgs -{ - /// - /// This is the entity that said the message. - /// - public EntityUid? Speaker; - - /// - /// This is the original message that the NPC heard. - /// - public string Message = default!; - - /// - /// This is the message, parsed into separate words. - /// - public List Words = default!; -} - -public sealed partial class NPCConversationHelpEvent : NPCConversationEvent -{ - [DataField("text")] - public string? Text; - - [DataField("audio")] - public SoundSpecifier? Audio; -} - -/// -/// This event can be raised after a response to cause an NPC to stop paying attention to someone. -/// -public sealed partial class NPCConversationByeEvent : NPCConversationEvent { } - -// The following classes help demonstrate some of the features of the system. -// They may be separated out at some point. -public sealed partial class NPCConversationToldNameEvent : NPCConversationListenEvent { } - diff --git a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs deleted file mode 100644 index 20a616d8308..00000000000 --- a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs +++ /dev/null @@ -1,154 +0,0 @@ -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; -using Content.Server.NPC.Events; - -namespace Content.Server.NPC.Prototypes; - -[Prototype("npcConversationTree")] -public sealed class NPCConversationTreePrototype : IPrototype, ISerializationHooks -{ - [ViewVariables] - [IdDataField] - public string ID { get; } = default!; - - /// - /// Dialogue contains all the topics to which an NPC can discuss. - /// - [ViewVariables] - [DataField("dialogue", required: true)] - public readonly NPCTopic[] Dialogue = default!; - - /// - /// Attention responses are what the NPC says when they start paying - /// attention to you without a specific question or prompt to respond to. - /// - [ViewVariables] - [DataField("attention", required: true)] - public readonly NPCResponse[] Attention = default!; - - /// - /// Idle responses are just things the NPC will say when nothing else is - /// going on, after some time. - /// - [ViewVariables] - [DataField("idle", required: true)] - public readonly NPCResponse[] Idle = default!; - - /// - /// Unknown responses are what the NPC says when they can't respond to a - /// particular question or prompt. - /// - [ViewVariables] - [DataField("unknown", required: true)] - public readonly NPCResponse[] Unknown = default!; - - /// - /// Custom responses are available to use in extensions to the NPC - /// Conversation system. - /// - // NOTE: This may be removed in favor of storing NPCResponses on custom - // components, i.e. an NPCShopkeeperComponent, but for now, it lives here - // to help demonstrate some features. - [ViewVariables] - [DataField("custom")] - public readonly Dictionary Custom = default!; - - /// - /// This exists as a quick way to map a prompt to a topic. - /// - public readonly Dictionary PromptToTopic = new(); - - // ISerializationHooks _is_ obsolete, but ConstructionGraphPrototype is using it as of this commit, - // and I'm not quite sure how to otherwise do this. - // - // I will look at that prototype when ISerializationHooks is phased out. - void ISerializationHooks.AfterDeserialization() - { - // Cache the strings mapping to prompts. - foreach (var topic in Dialogue) - { - foreach (var prompt in topic.Prompts) - { - PromptToTopic[prompt] = topic; - } - } - } -} - -[DataDefinition] -public sealed partial class NPCTopic -{ - [DataField] - public string[] Prompts = default!; - - /// - /// This determines the likelihood of this topic being selected over any - /// other, given the existence of multiple candidates. - /// - [DataField] - public float Weight = 1.0f; - - /// - /// Locked topics will not be accessible through dialogue until unlocked. - /// - [DataField] - public bool Locked; - - /// - /// Hidden topics won't show up in any form of "help" question. - /// - [DataField] - public bool Hidden; - - [DataField("responses", required: true)] - public NPCResponse[] Responses = default!; -} - -[DataDefinition] -public sealed partial class NPCResponse -{ - public NPCResponse() { } - - public NPCResponse(string? text, SoundSpecifier? audio = null, NPCConversationEvent? ev = null) - { - Text = text; - Audio = audio; - Event = ev; - } - - public override string ToString() - { - return $"NPCResponse({Text})"; - } - - [DataField] - public string? Text; - - [DataField] - public SoundSpecifier? Audio; - - /* [DataField("emote")] */ - /* public string? Emote; */ - - /// - /// This event is raised when the response is queued, - /// for the purpose of dynamic responses. - /// - [DataField] - public NPCConversationEvent? Is; - - /// - /// This event is raised after the response is made. - /// - [DataField] - public NPCConversationEvent? Event; - - /// - /// This event is raised when the NPC next hears a response, - /// allowing the response to be processed by other systems. - /// - [DataField] - public NPCConversationListenEvent? ListenEvent; -} - diff --git a/Content.Server/NPC/Systems/NPCConversationSystem.cs b/Content.Server/NPC/Systems/NPCConversationSystem.cs deleted file mode 100644 index 015adb19de5..00000000000 --- a/Content.Server/NPC/Systems/NPCConversationSystem.cs +++ /dev/null @@ -1,558 +0,0 @@ -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text.RegularExpressions; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using Robust.Shared.Timing; -using Content.Server.Chat.Systems; -using Content.Server.Chat.TypingIndicator; -using Content.Server.NPC.HTN; -using Content.Server.NPC.Components; -using Content.Server.NPC.Events; -using Content.Server.NPC.Prototypes; -using Content.Server.Speech; -using Content.Shared.Interaction; -using Content.Server.Radio.Components; - -namespace Content.Server.NPC.Systems; - -public sealed class NPCConversationSystem : EntitySystem -{ - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IPrototypeManager _prototype = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - [Dependency] private readonly ChatSystem _chatSystem = default!; - [Dependency] private readonly NPCSystem _npcSystem = default!; - [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!; - [Dependency] private readonly TransformSystem _transformSystem = default!; - [Dependency] private readonly TypingIndicatorSystem _typingIndicatorSystem = default!; - - private ISawmill _sawmill = default!; - - // TODO: attention attenuation. distance, facing, visible - // TODO: attending to multiple people, multiple streams of conversation - // TODO: multi-word prompts - // TODO: nameless prompting (pointing is good) - // TODO: aliases - - public static readonly string[] QuestionWords = { "who", "what", "when", "why", "where", "how" }; - public static readonly string[] Copulae = { "is", "are" }; - - public override void Initialize() - { - base.Initialize(); - - _sawmill = Logger.GetSawmill("npc.conversation"); - - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnUnpaused); - SubscribeLocalEvent(OnListenAttempt); - SubscribeLocalEvent(OnListen); - - SubscribeLocalEvent(OnBye); - SubscribeLocalEvent(OnHelp); - - SubscribeLocalEvent(OnToldName); - } - -#region API - - /// - /// Toggle the ability of an NPC to listen for topics. - /// - public void EnableConversation(EntityUid uid, bool enable = true, NPCConversationComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - component.Enabled = enable; - } - - /// - /// Toggle the NPC's willingness to make idle comments. - /// - public void EnableIdleChat(EntityUid uid, bool enable = true, NPCConversationComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - component.IdleEnabled = enable; - } - - /// - /// Return locked status of a dialogue topic. - /// - public bool IsDialogueLocked(EntityUid uid, string option, NPCConversationComponent? component = null) - { - if (!Resolve(uid, ref component)) - return true; - - if (!component.ConversationTree.PromptToTopic.TryGetValue(option, out var topic)) - { - _sawmill.Warning($"Tried to check locked status of missing dialogue option `{option}` on {ToPrettyString(uid)}"); - return true; - } - - if (component.UnlockedTopics.Contains(topic)) - return false; - - return topic.Locked; - } - - /// - /// Unlock dialogue options normally locked in an NPC's conversation tree. - /// - public void UnlockDialogue(EntityUid uid, string option, NPCConversationComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (component.ConversationTree.PromptToTopic.TryGetValue(option, out var topic)) - component.UnlockedTopics.Add(topic); - else - _sawmill.Warning($"Tried to unlock missing dialogue option `{option}` on {ToPrettyString(uid)}"); - } - - /// - public void UnlockDialogue(EntityUid uid, HashSet options, NPCConversationComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - foreach (var option in options) - UnlockDialogue(uid, option, component); - } - - /// - /// Queue a response for an NPC with a visible typing indicator and delay between messages. - /// - /// - /// This can be used as opposed to the typical method. - /// - public void QueueResponse(EntityUid uid, NPCResponse response, NPCConversationComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (response.Is is {} ev) - { - // This is a dynamic response which will call QueueResponse with static responses of its own. - ev.TalkingTo = component.AttendingTo; - RaiseLocalEvent(uid, (object) ev); - return; - } - - if (component.ResponseQueue.Count == 0) - { - DelayResponse(uid, component, response); - _typingIndicatorSystem.SetTypingIndicatorEnabled(uid, true); - } - - component.ResponseQueue.Push(response); - } - - /// - /// Make an NPC stop paying attention to someone. - /// - public void LoseAttention(EntityUid uid, NPCConversationComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - component.AttendingTo = null; - component.ListeningEvent = null; - _rotateToFaceSystem.TryFaceAngle(uid, component.OriginalFacing); - } - -#endregion - - private void DelayResponse(EntityUid uid, NPCConversationComponent component, NPCResponse response) - { - if (response.Text == null) - return; - - component.NextResponse = _gameTiming.CurTime + - component.DelayBeforeResponse + - component.TypingDelay.TotalSeconds * TimeSpan.FromSeconds(response.Text.Length) * - _random.NextDouble(0.9, 1.1); - } - - private IEnumerable GetAvailableTopics(EntityUid uid, NPCConversationComponent component) - { - HashSet availableTopics = new(); - - foreach (var topic in component.ConversationTree.Dialogue) - { - if (!topic.Locked || component.UnlockedTopics.Contains(topic)) - availableTopics.Add(topic); - } - - return availableTopics; - } - - private IEnumerable GetVisibleTopics(EntityUid uid, NPCConversationComponent component) - { - HashSet visibleTopics = new(); - - foreach (var topic in component.ConversationTree.Dialogue) - { - if (!topic.Hidden && (!topic.Locked || component.UnlockedTopics.Contains(topic))) - visibleTopics.Add(topic); - } - - return visibleTopics; - } - - private void OnInit(EntityUid uid, NPCConversationComponent component, ComponentInit args) - { - if (component.ConversationTreeId == null) - return; - - component.ConversationTree = _prototype.Index(component.ConversationTreeId); - component.NextIdleChat = _gameTiming.CurTime + component.IdleChatDelay; - - for (var i = 0; i < component.ConversationTree.Idle.Length; ++i) - component.IdleChatOrder.Add(i); - - _random.Shuffle(component.IdleChatOrder); - } - - private void OnUnpaused(EntityUid uid, NPCConversationComponent component, ref EntityUnpausedEvent args) - { - component.NextResponse += args.PausedTime; - component.NextAttentionLoss += args.PausedTime; - component.NextIdleChat += args.PausedTime; - } - - private bool TryGetIdleChatLine(EntityUid uid, NPCConversationComponent component, [NotNullWhen(true)] out NPCResponse? line) - { - line = null; - - if (component.IdleChatOrder.Count() == 0) - return false; - - if (++component.IdleChatIndex == component.IdleChatOrder.Count()) - { - // Exhausted all lines in the pre-shuffled order. - // Reset the index and shuffle again. - component.IdleChatIndex = 0; - _random.Shuffle(component.IdleChatOrder); - } - - var index = component.IdleChatOrder[component.IdleChatIndex]; - - line = component.ConversationTree.Idle[index]; - - return true; - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var component)) - { - var curTime = _gameTiming.CurTime; - - if (curTime >= component.NextResponse && component.ResponseQueue.Count > 0) - { - // Make a response. - Respond(uid, component, component.ResponseQueue.Pop()); - } - - if (curTime >= component.NextAttentionLoss && component.AttendingTo != null) - { - // Forget who we were talking to. - LoseAttention(uid, component); - } - - if (component.IdleEnabled && - curTime >= component.NextIdleChat && - TryGetIdleChatLine(uid, component, out var line)) - { - Respond(uid, component, line); - } - } - } - - private void OnListenAttempt(EntityUid uid, NPCConversationComponent component, ListenAttemptEvent args) - { - if (!component.Enabled || - // Don't listen to myself... - uid == args.Source || - // Don't bother listening to other NPCs. For now. - HasComp(args.Source) || - // We're already "typing" a response, so do that first. - component.ResponseQueue.Count > 0) - { - args.Cancel(); - } - } - - private void PayAttentionTo(EntityUid uid, NPCConversationComponent component, EntityUid speaker) - { - component.AttendingTo = speaker; - component.NextAttentionLoss = _gameTiming.CurTime + component.AttentionSpan; - component.OriginalFacing = _transformSystem.GetWorldRotation(uid); - } - - private void Respond(EntityUid uid, NPCConversationComponent component, NPCResponse response) - { - if (component.ResponseQueue.Count == 0) - _typingIndicatorSystem.SetTypingIndicatorEnabled(uid, false); - else - DelayResponse(uid, component, component.ResponseQueue.Peek()); - - if (component.AttendingTo != null) - { - // TODO: This line is a mouthful. Maybe write a public API that supports EntityCoordinates later? - var speakerCoords = Transform(component.AttendingTo.Value).Coordinates.ToMap(EntityManager, _transformSystem).Position; - _rotateToFaceSystem.TryFaceCoordinates(uid, speakerCoords); - } - - if (response.Event is {} ev) - { - ev.TalkingTo = component.AttendingTo; - RaiseLocalEvent(uid, (object) ev); - } - - if (response.ListenEvent != null) - component.ListeningEvent = response.ListenEvent; - - if (response.Text != null) - _chatSystem.TrySendInGameICMessage(uid, Loc.GetString(response.Text), InGameICChatType.Speak, false); - - if (response.Audio != null) - _audioSystem.PlayPvs(response.Audio, uid, - // TODO: Allow this to be configured per NPC/response. - AudioParams.Default - .WithVolume(8f) - .WithMaxDistance(9f) - .WithRolloffFactor(0.5f)); - - // Refresh our attention. - component.NextAttentionLoss = _gameTiming.CurTime + component.AttentionSpan; - component.NextIdleChat = component.NextAttentionLoss + component.IdleChatDelay; - } - - private List ParseMessageIntoWords(string message) - { - return Regex.Replace(message.Trim().ToLower(), @"(\p{P})", "") - .Split() - .ToList(); - } - - private bool FindResponse(EntityUid uid, NPCConversationComponent component, List words, [NotNullWhen(true)] out NPCResponse? response) - { - response = null; - - var availableTopics = GetAvailableTopics(uid, component); - - // Some topics are more interesting than others. - var greatestWeight = 0f; - NPCTopic? candidate = null; - - foreach (var word in words) - { - if (component.ConversationTree.PromptToTopic.TryGetValue(word, out var topic) && - availableTopics.Contains(topic) && - topic.Weight > greatestWeight) - { - greatestWeight = topic.Weight; - candidate = topic; - } - } - - if (candidate != null) - { - response = _random.Pick(candidate.Responses); - return true; - } - - return false; - } - - private bool JudgeQuestionLikelihood(EntityUid uid, NPCConversationComponent component, List words, string message) - { - if (message.Length > 0 && message[^1] == '?') - // A question mark is an absolute mark of a question. - return true; - - if (words.Count == 1) - // The usefulness of this is dubious, but it's definitely a question. - return QuestionWords.Contains(words[0]); - - if (words.Count >= 2) - return QuestionWords.Contains(words[0]) && Copulae.Contains(words[1]); - - return false; - } - - private void OnBye(EntityUid uid, NPCConversationComponent component, NPCConversationByeEvent args) - { - LoseAttention(uid, component); - } - - private void OnHelp(EntityUid uid, NPCConversationComponent component, NPCConversationHelpEvent args) - { - if (args.Text == null) - { - _sawmill.Error($"{ToPrettyString(uid)} heard a Help prompt but has no text for it."); - return; - } - - var availableTopics = GetVisibleTopics(uid, component); - var availablePrompts = availableTopics.Select(topic => topic.Prompts.FirstOrDefault()).ToArray(); - - string availablePromptsText; - if (availablePrompts.Count() <= 2) - { - availablePromptsText = Loc.GetString(args.Text, - ("availablePrompts", string.Join(" or ", availablePrompts)) - ); - } - else - { - availablePrompts[^1] = $"or {availablePrompts[^1]}"; - availablePromptsText = Loc.GetString(args.Text, - ("availablePrompts", string.Join(", ", availablePrompts)) - ); - } - - // Unlikely we'll be able to do audio that isn't hard-coded, - // so best to keep it general. - var response = new NPCResponse(availablePromptsText, args.Audio); - QueueResponse(uid, response, component); - } - - private void OnToldName(EntityUid uid, NPCConversationComponent component, NPCConversationListenEvent args) - { - if (!component.ConversationTree.Custom.TryGetValue("toldName", out var responses)) - return; - - var response = _random.Pick(responses); - if (response.Text == null) - { - _sawmill.Error($"{ToPrettyString(uid)} was told a name but had no text response."); - return; - } - - // The world's simplest heuristic for names: - if (args.Words.Count > 3) - { - // It didn't seem like a name, so wait for something that does. - return; - } - - var cleanedName = string.Join(" ", args.Words); - cleanedName = char.ToUpper(cleanedName[0]) + cleanedName.Remove(0, 1); - - var formattedResponse = new NPCResponse(Loc.GetString(response.Text, - ("name", cleanedName)), - response.Audio); - - QueueResponse(uid, formattedResponse, component); - args.Handled = true; - } - - private void OnListen(EntityUid uid, NPCConversationComponent component, ListenEvent args) - { - if (HasComp(args.Source)) - return; - - if (component.AttendingTo != null && component.AttendingTo != args.Source) - // Ignore someone speaking to us if we're already paying attention to someone else. - return; - - var words = ParseMessageIntoWords(args.Message); - if (words.Count == 0) - return; - - if (component.AttendingTo == args.Source) - { - // The person we're talking to said something to us. - - if (component.ListeningEvent is {} ev) - { - // We were waiting on this person to say something, and they've said something. - ev.Handled = false; - ev.Speaker = component.AttendingTo; - ev.Message = args.Message; - ev.Words = words; - RaiseLocalEvent(uid, (object) ev); - - if (ev.Handled) - component.ListeningEvent = null; - - return; - } - - // We're already paying attention to this person, - // so try to figure out if they said something we can talk about. - if (FindResponse(uid, component, words, out var response)) - { - // A response was found so go ahead with it. - QueueResponse(uid, response, component); - } - else if(JudgeQuestionLikelihood(uid, component, words, args.Message)) - { - // The message didn't match any of the prompts, but it seemed like a question. - var unknownResponse = _random.Pick(component.ConversationTree.Unknown); - QueueResponse(uid, unknownResponse, component); - } - - // If the message didn't seem like a question, - // and it didn't raise any of our topics, - // then politely ignore who we're talking with. - // - // It's better than spamming them with "I don't understand." - return; - } - - // See if someone said our name. - var myName = MetaData(uid).EntityName.ToLower(); - - // So this is a rough heuristic, but if our name occurs within the first three words, - // or is the very last one, someone might be trying to talk to us. - var payAttention = words[0] == myName || words[^1] == myName; - if (!payAttention) - { - for (int i = 1; i < Math.Min(2, words.Count); ++i) - { - if (words[i] == myName) - { - payAttention = true; - break; - } - } - } - - if (payAttention) - { - PayAttentionTo(uid, component, args.Source); - - if (!FindResponse(uid, component, words, out var response)) - { - if(JudgeQuestionLikelihood(uid, component, words, args.Message) && - // This subcondition exists to block our name being interpreted as a question in its own right. - words.Count > 1) - { - response = _random.Pick(component.ConversationTree.Unknown); - } - else - { - response = _random.Pick(component.ConversationTree.Attention); - } - } - - QueueResponse(uid, response, component); - } - } -} - diff --git a/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs b/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs index ba5ff0a056d..b1a6c1e9de1 100644 --- a/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs +++ b/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs @@ -1,8 +1,5 @@ using Content.Server.Psionics.Abilities; using Content.Server.Chat.Systems; -using Content.Server.NPC.Events; -using Content.Server.NPC.Systems; -using Content.Server.NPC.Prototypes; using Content.Server.Radio.Components; using Content.Server.Radio.EntitySystems; using Content.Server.StationEvents.Events; @@ -21,8 +18,6 @@ public sealed partial class SophicScribeSystem : EntitySystem [Dependency] private readonly RadioSystem _radioSystem = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly NPCConversationSystem _conversationSystem = default!; - protected ISawmill Sawmill = default!; public override void Update(float frameTime) { @@ -56,32 +51,6 @@ public override void Initialize() SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnGlimmerEventEnded); - SubscribeLocalEvent(OnGetGlimmer); - } - - private void OnGetGlimmer(EntityUid uid, SophicScribeComponent component, NPCConversationGetGlimmerEvent args) - { - if (args.Text == null) - { - Sawmill.Error($"{uid} heard a glimmer reading prompt but has no text for it"); - return; - } - - var tier = _glimmerSystem.GetGlimmerTier() switch - { - GlimmerTier.Minimal => Loc.GetString("glimmer-reading-minimal"), - GlimmerTier.Low => Loc.GetString("glimmer-reading-low"), - GlimmerTier.Moderate => Loc.GetString("glimmer-reading-moderate"), - GlimmerTier.High => Loc.GetString("glimmer-reading-high"), - GlimmerTier.Dangerous => Loc.GetString("glimmer-reading-dangerous"), - _ => Loc.GetString("glimmer-reading-critical"), - }; - - var glimmerReadingText = Loc.GetString(args.Text, - ("glimmer", (int) Math.Round(_glimmerSystem.GlimmerOutput)), ("tier", tier)); - - var response = new NPCResponse(glimmerReadingText); - _conversationSystem.QueueResponse(uid, response); } private void OnInteractHand(EntityUid uid, SophicScribeComponent component, InteractHandEvent args) @@ -114,9 +83,4 @@ private void OnGlimmerEventEnded(GlimmerEventEndedEvent args) _radioSystem.SendRadioMessage(speaker, message, channel, speaker); } } - public sealed partial class NPCConversationGetGlimmerEvent : NPCConversationEvent - { - [DataField] - public string? Text; - } } diff --git a/Resources/Locale/en-US/npc/conversation/sophia.ftl b/Resources/Locale/en-US/npc/conversation/sophia.ftl deleted file mode 100644 index c832d9fc17f..00000000000 --- a/Resources/Locale/en-US/npc/conversation/sophia.ftl +++ /dev/null @@ -1,82 +0,0 @@ -sophia-response-name = You may call me Sophia. -sophia-response-help = You may inquire about one of the following topics: {$availablePrompts}. - -sophia-response-hello-1 = Greetings. -sophia-response-hello-2 = Salutations. - -sophia-response-bye-1 = Fare thee well. -sophia-response-bye-2 = Gods be with you. -sophia-response-bye-3 = Come back wiser. - -sophia-idle-phrase-1 = Mmmm, another portent. -sophia-idle-phrase-2 = The noösphere is quite beautiful today. However, I don't think I could describe it in a way you could understand. -sophia-idle-phrase-3 = I've been here before. You have, too. - -sophia-response-attention-1 = What is it? -sophia-response-attention-2 = What do you seek? -sophia-response-attention-3 = Out with it. - -sophia-response-sorry-1 = That's not a question for me. -sophia-response-sorry-2 = Ask someone else. -sophia-response-sorry-3 = Maybe I know the answer, maybe I do not. Either way, I will not be answering that question. - -sophia-response-nature = My nature doesn't really matter, does it? I'm fulfilling my purpose. Can you say the same, or are you just wasting time? - -sophia-response-epi = 'Epistemics' is a word. Aspiring Hellenes they are, they wished to displace the Latin 'science.' However, in English, epistemics has undesired connotations as a study of knowledge itself, even though the Greek word is a literal replacement for 'science.' - -sophia-response-mantis = 'Mantis' means seer, soothsayer, or prophet. They must be so named because they seek to uncover the truth. And, fittingly with their psionic aptitude, 'mantis' and 'mind' both descend, to the best of our knowledge, from an absolutely ancient word that sounded something like 'mentis.' - -sophia-response-mystagogue = 'Mystagogue' literally means 'leader of the mystics.' You may know the suffix -gogue from 'demogogue.' - -sophia-response-oracle = Oracle? I don't know much about her, and she isn't keen to share her secrets with me. - -sophia-response-psionics = Psionics are extraordinary abilities originating from one's mind. There doesn't seem to be any dominant word to refer to someone with the ability to practice these, although I prefer 'psion' or 'psychic.' - -sophia-response-noosphere = The noösphere is a field connecting all of consciousness. It's the medium through which psionics works. Its strength and effects on the illusory world of the material are based on its pressure. Colloquially, noöspheric pressure is called 'glimmer.' - -sophia-response-god = 'God' is such a vague term. There are so many entities out there that have defeated mortality. How you choose to regard them is your business. - -sophia-response-morphotype = In the first century PCC, several entities reshaped men into their image. I had done the same, if you would believe it. I can offer no evidence of their existence, other than faint memories. Any specific morphotype you want to know about? - -sophia-response-calendar = It's currently 417 PCC. The casuality crisis neccesitated a new year to count from. Due to the nature of the crisis, it can only be said with certainty that 1 PCC is between 2400 and 2700 CE. - -sophia-response-crisis = The first FTL travel was incompatible with the old ways. Fortunately, its resolution made more apparent the inherent futility in trying to give one history, one narrative, one account. Truth cannot be found in the material world, only higher ones. - -sophia-response-metempsychosis = You've died thousands of times, and you'll die thousands more. Some of those lives you may dedicate to trying to stop the cycle. We all carry at least some memory of past lives, usually temporally recent ones. One of the great mysteries of the persistence of fragments is the high concentration of memories from the early 21st century CE, which, inverse to other periods, seem to be more common among the ignorant. - -sophia-response-truth = If you seek the truth, you're in the wrong place. From a perspective tainted by material reality, the best you can hope is to try and divine higher truths that are not dependent on it. - -sophia-response-job = I observe the glimmer here, and record it. - -sophia-response-human = Humans were the base for all the others. But they, too, were shaped. Long, long before the others. - -sophia-response-felinid = Felinids were the first, and the most willing. In true feline nature, they shaped themselves. - -sophia-response-oni = Oni, it is said, originated in Sirius. The brightest star in the night sky from Earth may have attracted some chromatically inclined entities, explaining their vivid coloring. But, that's just speculation. - -sophia-response-arachne = Arachne are the strangest of them. They're not fully mortal. They took the form of humans, but not their genes. Their creator wrote his name in their stead. - -sophia-response-moth = Moths scarecely look human, but, strangely, their genes confirm they are. Their creator shares his name with a genus of moths, and was responsible for the other outlier. - -sophia-response-lamiae = So, you remember? You must be remembering their mythological namesake. If you've really retained that fleeting memory over so many metempsychoses... Perhaps I've said too much. - -sophia-response-cyno = Were those... no... So faint. Ignorance! You cannot remember them! It's impossible! - -sophia-response-harpy = Harpies, it is said, were once men and women, sculpted by greed for a purpose long gone. They were abandoned by their creators on a world named Valerian 4b. - -sophia-response-valerian = The Harpy homeworld? Magestic mountains gleaming in white, forests of brilliant scarlet, oceans wine dark, yet no light to be seen by mortal eyes. The Harpies were made to thrive there. To them, their world was bathed in beautiful silver light. - -sophia-response-grue = You do not know of those. You cannot. I had so hoped to live a few cycles under normal causality. - -sophia-response-abraxas = That's a name of power, and I avoid speaking of him. He's the least content to rest, and the most infatuated with creating things from ignorance. - -sophia-response-zork = You wander into the slavering fangs of a hungry grue. There, did you enjoy this game? - -sophia-response-glimmer = The current glimmer reading is {$glimmer}. {$tier} - -glimmer-reading-minimal = That is extremely low. Nothing bad will happen, but I hope this is not at the cost of progression in your understanding of the universe. -glimmer-reading-low = That is quite low. Just barely enough to register any psionic activity here. -glimmer-reading-moderate = That is about the expected level on a psionically active station. You may notice manageable, minor effects. -glimmer-reading-high = That is sure to start attracting attention, although still quite manageable. -glimmer-reading-dangerous = That's a bit concerning. You may want to redirect efforts to reducing it. -glimmer-reading-critical = That's apocalyptic, in the original sense of the word. That is, to say, revealing. This is the sort of time and place to acquire secret knowledge. diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml index 5213608d95e..8e34a07ea5e 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml @@ -1,7 +1,7 @@ - type: entity parent: BaseStructure id: SophicScribe - name: Sophie + name: sophie description: Latest reports on the Noösphere! components: - type: Sprite @@ -27,10 +27,6 @@ channels: - Common - Science - - type: ActiveListener - - type: TypingIndicator - - type: NPCConversation - tree: SophiaTree - type: PotentialPsionic #this makes her easier to access for glimmer events, dw about it - type: Psionic psychicFeedback: @@ -43,191 +39,3 @@ - type: GuideHelp guides: - Psionics - -- type: npcConversationTree - id: SophiaTree - dialogue: - - prompts: [glimmer, reading] - responses: - - is: !type:NPCConversationGetGlimmerEvent - text: sophia-response-glimmer - - - prompts: [purpose, job, occupation, profession] - weight: 0.5 - responses: - - text: sophia-response-job - - - prompts: [help, topics] - weight: 0.5 - hidden: true - responses: - - is: !type:NPCConversationHelpEvent - text: sophia-response-help - - - prompts: [nature, statue, snake, being] - weight: 0.3 - responses: - - text: sophia-response-nature - - - prompts: [epistemics, epi] - weight: 0.2 - responses: - - text: sophia-response-epi - - - prompts: [mantis] - weight: 0.2 - responses: - - text: sophia-response-mantis - - - prompts: [mystagogue, mysta] - weight: 0.2 - responses: - - text: sophia-response-mystagogue - - - prompts: [psionics, psychic] - weight: 0.2 - responses: - - text: sophia-response-psionics - - - prompts: [noösphere, noosphere] - weight: 0.2 - responses: - - text: sophia-response-noosphere - - - prompts: [metempsychosis, metempsychoses, reincarnation, death, dying, afterlife] - weight: 0.2 - responses: - - text: sophia-response-metempsychosis - - - prompts: [calendar] - weight: 0.2 - responses: - - text: sophia-response-calendar - - - prompts: [morphotypes, morphotype, species] - weight: 0.2 - responses: - - text: sophia-response-morphotype - - - prompts: [gods, god] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-god - - - prompts: [truth, "true", "false", falsity, falsehood] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-truth - - - prompts: [human, humans, humanoid, unmutated] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-human - - - prompts: [felinid, felinids, felid, felids, catperson, catpeople] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-felinid - - - prompts: [oni, onis] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-oni - - - prompts: [arachne, arachnid, arachnids, spiderperson, spiderpeople] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-arachne - - - prompts: [moth, moths, moff, moths] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-moth - - - prompts: [lamiae, lamia, lamias] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-lamiae - - - prompts: [grue, grues, batperson, batpeople] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-grue - - - prompts: [cynocephalus, cynocephali, cyno, cynos] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-cyno - - - prompts: [harpy, harpies] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-harpy - - - prompts: [valerian, Valerian, 4b] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-valerian - - - prompts: [crisis, causality] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-crisis - - - prompts: [oracle] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-oracle - - - prompts: [abraxas] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-abraxas - - - prompts: [hi, hello, hey, greetings, salutations] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-hello-1 - - text: sophia-response-hello-2 - - - prompts: [bye, goodbye, done, farewell, later, seeya] - weight: 0.1 - hidden: true - responses: - - text: sophia-response-bye-1 - event: !type:NPCConversationByeEvent - - text: sophia-response-bye-2 - event: !type:NPCConversationByeEvent - - text: sophia-response-bye-3 - event: !type:NPCConversationByeEvent - - attention: - - text: sophia-response-attention-1 - - text: sophia-response-attention-2 - - text: sophia-response-attention-3 - - idle: - - text: sophia-idle-phrase-1 - - text: sophia-idle-phrase-2 - - text: sophia-idle-phrase-3 - - unknown: - - text: sophia-response-sorry-1 - - text: sophia-response-sorry-2 - - text: sophia-response-sorry-3 From e092203d1134521f95c44c424bfa57f1ec7ffe53 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Fri, 5 Jul 2024 19:53:25 +0300 Subject: [PATCH 34/56] Frictionfull Space (#514) # Description Makes it so that the station and the ATS get a very tiny bit of friction to prevent cargo tech pros from sending either of those out of this galaxy cluster (which has actually happened multiple times on two servers and required either admin intervention or early round ending). # Technical details Added a PassiveDampeningComponent which defines how much friction an entity receives while in 0g. FrictionRemoverSystem was updated to try to fetch this component from an entity before updating its dampening. A new system was added to automatically add this component (if it's not already defined) to all station grids. # Media See the #when-you-code-it channel for a preview. It's kinda hard to demonstrate, but after a few minutes, stations and the ATS come to an almost complete stop. # Changelog :cl: - tweak: Space stations now have a tiny bit of velocity dampening to prevent them from being flunged into the void. --- .../Station/Systems/StationDampeningSystem.cs | 28 +++++++++++++++++++ .../Physics/FrictionRemoverSystem.cs | 13 +++++++-- .../Physics/PassiveDampeningComponent.cs | 18 ++++++++++++ Resources/Maps/Shuttles/trading_outpost.yml | 3 ++ 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 Content.Server/Station/Systems/StationDampeningSystem.cs create mode 100644 Content.Shared/Physics/PassiveDampeningComponent.cs diff --git a/Content.Server/Station/Systems/StationDampeningSystem.cs b/Content.Server/Station/Systems/StationDampeningSystem.cs new file mode 100644 index 00000000000..f499127031e --- /dev/null +++ b/Content.Server/Station/Systems/StationDampeningSystem.cs @@ -0,0 +1,28 @@ +using Content.Server.Station.Events; +using Content.Shared.Physics; + +namespace Content.Server.Station.Systems; + +public sealed class StationDampeningSystem : EntitySystem +{ + public override void Initialize() + { + SubscribeLocalEvent(OnInitStation); + } + + private void OnInitStation(ref StationPostInitEvent ev) + { + foreach (var grid in ev.Station.Comp.Grids) + { + // If the station grid doesn't have defined dampening, give it a small dampening by default + // This will ensure cargo tech pros won't fling the station 1000 megaparsec away from the galaxy + if (!TryComp(grid, out var dampening)) + { + dampening = AddComp(grid); + dampening.Enabled = true; + dampening.LinearDampening = 0.01f; + dampening.AngularDampening = 0.01f; + } + } + } +} diff --git a/Content.Shared/Physics/FrictionRemoverSystem.cs b/Content.Shared/Physics/FrictionRemoverSystem.cs index 65bbe9e4d23..c8d7521eb01 100644 --- a/Content.Shared/Physics/FrictionRemoverSystem.cs +++ b/Content.Shared/Physics/FrictionRemoverSystem.cs @@ -1,3 +1,4 @@ +using Robust.Shared.Map.Components; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; @@ -19,7 +20,15 @@ public override void Initialize() private void RemoveDampening(EntityUid uid, PhysicsComponent component, PhysicsSleepEvent args) { - _physics.SetAngularDamping(uid, component, 0f, false); - _physics.SetLinearDamping(uid, component, 0f); + var linear = 0f; + var angular = 0f; + if (TryComp(uid, out var dampening) && dampening.Enabled) + { + linear = dampening.LinearDampening; + angular = dampening.AngularDampening; + } + + _physics.SetAngularDamping(uid, component, angular, false); + _physics.SetLinearDamping(uid, component, linear); } } diff --git a/Content.Shared/Physics/PassiveDampeningComponent.cs b/Content.Shared/Physics/PassiveDampeningComponent.cs new file mode 100644 index 00000000000..834569195ee --- /dev/null +++ b/Content.Shared/Physics/PassiveDampeningComponent.cs @@ -0,0 +1,18 @@ +namespace Content.Shared.Physics; + +/// +/// A component that allows an entity to have friction (linear and angular dampening) +/// even when not being affected by gravity. +/// +[RegisterComponent] +public sealed partial class PassiveDampeningComponent : Component +{ + [DataField] + public bool Enabled = true; + + [DataField] + public float LinearDampening = 0.2f; + + [DataField] + public float AngularDampening = 0.2f; +} diff --git a/Resources/Maps/Shuttles/trading_outpost.yml b/Resources/Maps/Shuttles/trading_outpost.yml index f040d58253d..7b968b5c13d 100644 --- a/Resources/Maps/Shuttles/trading_outpost.yml +++ b/Resources/Maps/Shuttles/trading_outpost.yml @@ -60,6 +60,9 @@ entities: linearDamping: 0.05 fixedRotation: False bodyType: Dynamic + - type: PassiveDampening # To prevent cargotechs from flingling it away. + linearDampening: 0.01 + angularDampening: 0.01 - type: Fixtures fixtures: {} - type: OccluderTree From 81dd78259e66b6fda06fcb7c4b76b365eb4f652a Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 16:53:46 +0000 Subject: [PATCH 35/56] Automatic Changelog Update (#514) --- Resources/Changelog/Changelog.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index c4056561c9c..847b9c90fbe 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4285,3 +4285,11 @@ Entries: message: Equipping clothing using the Z key works correctly again. id: 6136 time: '2024-07-03T19:48:29.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Tweak + message: >- + Space stations now have a tiny bit of velocity dampening to prevent them + from being flunged into the void. + id: 6137 + time: '2024-07-05T16:53:25.0000000+00:00' From 58be8504857c54d311e9100ee89b921d2437ee35 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Fri, 5 Jul 2024 20:28:44 +0300 Subject: [PATCH 36/56] Port All Carrying/PseudoItem/EscapeInventory Tweaks From DeltaV (#484) # Description This cherry-picks the following two PRs of mine from delta-v: - https://github.com/DeltaV-Station/Delta-v/pull/1118 (this one is a port of two other frontier PRs, see the original description for details) - https://github.com/DeltaV-Station/Delta-v/pull/1232 Encompassing a total of 8 distinct changes: 1. Fixes dropping the carried person when walking to a different grid (from station to shuttle or vice versa. Walking into space however will still make you drop them) and also makes sure that pressing shift while being carried does not make you escape. 2. Ensures that the carried person is always centered relative to the parent (under certain conditions, such as walking near a gravitational anomaly, their position can change, and that leads to really weird effects) 3. Fixes the mass contest in CarryingSystem that caused stronger entities to take longer to escape than weaker ones. 4. Adds popups for when you're getting picked up or being stuffed into a bag as a pseudo-item (e.g. a felinid) 5. Adds an action to stop escaping an inventory. This action gets added to your action bar when you attempt escaping and gets removed when you stop or escape. It applies both to carrying and items (hampsters, felinids, whatever else). 6. Adds a sleep action for pseudo-items stuffed inside a suitable bag. The bag must have a special component, which is added to the base backpack item and thus inherited by all soft bags (duffels, satchels, etc). Contrary to a popular belief, sleeping IS PURELY COSMETICAL and does not provide healing. (Beds provide healing when you buckle into them and that healing does not depend on whether or not you're sleeping) 7. Makes it so that when you try to take a pseudo-item out of the bag (e.g. a felinid), you automatically try to carry them (if you don't have enough free hands, they will be dropped on the floor like usually), and enables you to insert the carried person into a bag, but only if they're a pseudo-item (e.g. felinid). 8. Allows pseudoitems to be inserted into bags even when there are other items (as long as there's enough space) --- ## For technical details and video showcases, see the original PRs This PR is split into separate commits so different parts can be reverted if deemed unneccessary. --- # Changelog :cl: - fix: Carrying is less likely to behave erratically or suddenly interrupt now. - add: You can now see when someone is trying to pick you up, and also you can interrupt your attempt at escaping from their hands or inventory. - add: You can now properly take Felinids out of bags and place them inside. - add: Scientists have discovered that Felinids can sleep in bags. --- .../Nyanotrasen/Carrying/CarryingSystem.cs | 116 +++++++++++- .../Item/PseudoItem/PseudoItemSystem.cs | 30 +++- .../Resist/CanEscapeInventoryComponent.cs | 6 + .../Resist/EscapeInventorySystem.cs | 26 +++ .../PseudoItem/AllowsSleepInsideComponent.cs | 9 + .../Item/PseudoItem/PseudoItemComponent.cs | 14 +- .../SharedPseudoItemSystem.Checks.cs | 166 ++---------------- .../Item/PseudoItem/SharedPseudoItemSystem.cs | 24 ++- .../Resist/EscapeInventoryCancelEvent.cs | 5 + .../Locale/en-US/actions/actions/sleep.ftl | 2 + .../en-US/nyanotrasen/carrying/carry.ftl | 1 + Resources/Prototypes/Actions/misc.yml | 10 ++ .../Entities/Clothing/Back/backpacks.yml | 3 +- .../escapeinventory.rsi/cancel-escape.png | Bin 0 -> 559 bytes .../Actions/escapeinventory.rsi/meta.json | 14 ++ 15 files changed, 259 insertions(+), 167 deletions(-) create mode 100644 Content.Shared/Nyanotrasen/Item/PseudoItem/AllowsSleepInsideComponent.cs create mode 100644 Content.Shared/Resist/EscapeInventoryCancelEvent.cs create mode 100644 Resources/Prototypes/Actions/misc.yml create mode 100644 Resources/Textures/Actions/escapeinventory.rsi/cancel-escape.png create mode 100644 Resources/Textures/Actions/escapeinventory.rsi/meta.json diff --git a/Content.Server/Nyanotrasen/Carrying/CarryingSystem.cs b/Content.Server/Nyanotrasen/Carrying/CarryingSystem.cs index bb071334fa1..103731b1b04 100644 --- a/Content.Server/Nyanotrasen/Carrying/CarryingSystem.cs +++ b/Content.Server/Nyanotrasen/Carrying/CarryingSystem.cs @@ -1,3 +1,4 @@ +using System.Numerics; using System.Threading; using Content.Server.DoAfter; using Content.Server.Body.Systems; @@ -5,6 +6,7 @@ using Content.Server.Resist; using Content.Server.Popups; using Content.Server.Inventory; +using Content.Server.Nyanotrasen.Item.PseudoItem; using Content.Shared.Climbing; // Shared instead of Server using Content.Shared.Mobs; using Content.Shared.DoAfter; @@ -23,9 +25,12 @@ using Content.Shared.Standing; using Content.Shared.ActionBlocker; using Content.Shared.Inventory.VirtualItem; +using Content.Shared.Item; using Content.Shared.Throwing; using Content.Shared.Physics.Pull; using Content.Shared.Mobs.Systems; +using Content.Shared.Nyanotrasen.Item.PseudoItem; +using Content.Shared.Storage; using Robust.Shared.Map.Components; using Robust.Shared.Physics.Components; @@ -44,11 +49,13 @@ public sealed class CarryingSystem : EntitySystem [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!; [Dependency] private readonly RespiratorSystem _respirator = default!; + [Dependency] private readonly PseudoItemSystem _pseudoItem = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent>(AddCarryVerb); + SubscribeLocalEvent>(AddInsertCarriedVerb); SubscribeLocalEvent(OnVirtualItemDeleted); SubscribeLocalEvent(OnThrow); SubscribeLocalEvent(OnParentChanged); @@ -64,7 +71,6 @@ public override void Initialize() SubscribeLocalEvent(OnDoAfter); } - private void AddCarryVerb(EntityUid uid, CarriableComponent component, GetVerbsEvent args) { if (!args.CanInteract || !args.CanAccess) @@ -97,6 +103,33 @@ private void AddCarryVerb(EntityUid uid, CarriableComponent component, GetVerbsE args.Verbs.Add(verb); } + private void AddInsertCarriedVerb(EntityUid uid, CarryingComponent component, GetVerbsEvent args) + { + // If the person is carrying someone, and the carried person is a pseudo-item, and the target entity is a storage, + // then add an action to insert the carried entity into the target + var toInsert = args.Using; + if (toInsert is not { Valid: true } || !args.CanAccess || !TryComp(toInsert, out var pseudoItem)) + return; + + if (!TryComp(args.Target, out var storageComp)) + return; + + if (!_pseudoItem.CheckItemFits((toInsert.Value, pseudoItem), (args.Target, storageComp))) + return; + + InnateVerb verb = new() + { + Act = () => + { + DropCarried(uid, toInsert.Value); + _pseudoItem.TryInsert(args.Target, toInsert.Value, pseudoItem, storageComp); + }, + Text = Loc.GetString("action-name-insert-other", ("target", toInsert)), + Priority = 2 + }; + args.Verbs.Add(verb); + } + /// /// Since the carried entity is stored as 2 virtual items, when deleted we want to drop them. /// @@ -125,7 +158,12 @@ private void OnThrow(EntityUid uid, CarryingComponent component, BeforeThrowEven private void OnParentChanged(EntityUid uid, CarryingComponent component, ref EntParentChangedMessage args) { - if (Transform(uid).MapID != args.OldMapId) + var xform = Transform(uid); + if (xform.MapID != args.OldMapId) + return; + + // Do not drop the carried entity if the new parent is a grid + if (xform.ParentUid == xform.GridUid) return; DropCarried(uid, component.Carried); @@ -158,9 +196,13 @@ private void OnMoveInput(EntityUid uid, BeingCarriedComponent component, ref Mov if (!TryComp(uid, out var escape)) return; + if (!args.HasDirectionalMovement) + return; + if (_actionBlockerSystem.CanInteract(uid, component.Carrier)) { - _escapeInventorySystem.AttemptEscape(uid, component.Carrier, escape, MassContest(uid, component.Carrier)); + // Note: the mass contest is inverted because weaker entities are supposed to take longer to escape + _escapeInventorySystem.AttemptEscape(uid, component.Carrier, escape, MassContest(component.Carrier, uid)); } } @@ -209,12 +251,7 @@ private void OnDoAfter(EntityUid uid, CarriableComponent component, CarryDoAfter } private void StartCarryDoAfter(EntityUid carrier, EntityUid carried, CarriableComponent component) { - TimeSpan length = TimeSpan.FromSeconds(3); - - var mod = MassContest(carrier, carried); - - if (mod != 0) - length /= mod; + TimeSpan length = GetPickupDuration(carrier, carried); if (length >= TimeSpan.FromSeconds(9)) { @@ -236,6 +273,9 @@ private void StartCarryDoAfter(EntityUid carrier, EntityUid carried, CarriableCo }; _doAfterSystem.TryStartDoAfter(args); + + // Show a popup to the person getting picked up + _popupSystem.PopupEntity(Loc.GetString("carry-started", ("carrier", carrier)), carried, carried); } private void Carry(EntityUid carrier, EntityUid carried) @@ -260,6 +300,26 @@ private void Carry(EntityUid carrier, EntityUid carried) _actionBlockerSystem.UpdateCanMove(carried); } + public bool TryCarry(EntityUid carrier, EntityUid toCarry, CarriableComponent? carriedComp = null) + { + if (!Resolve(toCarry, ref carriedComp, false)) + return false; + + if (!CanCarry(carrier, toCarry, carriedComp)) + return false; + + // The second one means that carrier is a pseudo-item and is inside a bag. + if (HasComp(carrier) || HasComp(carrier)) + return false; + + if (GetPickupDuration(carrier, toCarry) > TimeSpan.FromSeconds(9)) + return false; + + Carry(carrier, toCarry); + + return true; + } + public void DropCarried(EntityUid carrier, EntityUid carried) { RemComp(carrier); // get rid of this first so we don't recusrively fire that event @@ -323,5 +383,43 @@ private float MassContest(EntityUid roller, EntityUid target, PhysicsComponent? return rollerPhysics.FixturesMass / targetPhysics.FixturesMass; } + + private TimeSpan GetPickupDuration(EntityUid carrier, EntityUid carried) + { + var length = TimeSpan.FromSeconds(3); + + var mod = MassContest(carrier, carried); + if (mod != 0) + length /= mod; + + return length; + } + + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var carried, out var comp)) + { + var carrier = comp.Carrier; + if (carrier is not { Valid: true } || carried is not { Valid: true }) + continue; + + // SOMETIMES - when an entity is inserted into disposals, or a cryosleep chamber - it can get re-parented without a proper reparent event + // when this happens, it needs to be dropped because it leads to weird behavior + if (Transform(carried).ParentUid != carrier) + { + DropCarried(carrier, carried); + continue; + } + + // Make sure the carried entity is always centered relative to the carrier, as gravity pulls can offset it otherwise + var xform = Transform(carried); + if (!xform.LocalPosition.Equals(Vector2.Zero)) + { + xform.LocalPosition = Vector2.Zero; + } + } + query.Dispose(); + } } } diff --git a/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs b/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs index 76cfe7d904b..6df387e6ba8 100644 --- a/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs +++ b/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs @@ -1,6 +1,9 @@ -using Content.Server.DoAfter; +using Content.Server.Carrying; +using Content.Server.DoAfter; using Content.Server.Item; +using Content.Server.Popups; using Content.Server.Storage.EntitySystems; +using Content.Shared.Bed.Sleep; using Content.Shared.DoAfter; using Content.Shared.IdentityManagement; using Content.Shared.Item; @@ -17,12 +20,14 @@ public sealed class PseudoItemSystem : SharedPseudoItemSystem [Dependency] private readonly StorageSystem _storage = default!; [Dependency] private readonly ItemSystem _item = default!; [Dependency] private readonly DoAfterSystem _doAfter = default!; - + [Dependency] private readonly CarryingSystem _carrying = default!; + [Dependency] private readonly PopupSystem _popup = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent>(AddInsertAltVerb); + SubscribeLocalEvent(OnTrySleeping); } private void AddInsertAltVerb(EntityUid uid, PseudoItemComponent component, GetVerbsEvent args) @@ -53,4 +58,25 @@ private void AddInsertAltVerb(EntityUid uid, PseudoItemComponent component, GetV }; args.Verbs.Add(verb); } + + protected override void OnGettingPickedUpAttempt(EntityUid uid, PseudoItemComponent component, GettingPickedUpAttemptEvent args) + { + // Try to pick the entity up instead first + if (args.User != args.Item && _carrying.TryCarry(args.User, uid)) + { + args.Cancel(); + return; + } + + // If could not pick up, just take it out onto the ground as per default + base.OnGettingPickedUpAttempt(uid, component, args); + } + + // Show a popup when a pseudo-item falls asleep inside a bag. + private void OnTrySleeping(EntityUid uid, PseudoItemComponent component, TryingToSleepEvent args) + { + var parent = Transform(uid).ParentUid; + if (!HasComp(uid) && parent is { Valid: true } && HasComp(parent)) + _popup.PopupEntity(Loc.GetString("popup-sleep-in-bag", ("entity", uid)), uid); + } } diff --git a/Content.Server/Resist/CanEscapeInventoryComponent.cs b/Content.Server/Resist/CanEscapeInventoryComponent.cs index 19b4abf7d0c..978e03d95f9 100644 --- a/Content.Server/Resist/CanEscapeInventoryComponent.cs +++ b/Content.Server/Resist/CanEscapeInventoryComponent.cs @@ -15,4 +15,10 @@ public sealed partial class CanEscapeInventoryComponent : Component [DataField("doAfter")] public DoAfterId? DoAfter; + + /// + /// Action to cancel inventory escape. + /// + [DataField] + public EntityUid? EscapeCancelAction; } diff --git a/Content.Server/Resist/EscapeInventorySystem.cs b/Content.Server/Resist/EscapeInventorySystem.cs index 127db7d2b34..95a470e9093 100644 --- a/Content.Server/Resist/EscapeInventorySystem.cs +++ b/Content.Server/Resist/EscapeInventorySystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Hands.EntitySystems; using Content.Server.Storage.Components; using Content.Shared.ActionBlocker; +using Content.Shared.Actions; using Content.Shared.DoAfter; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction.Events; @@ -13,6 +14,7 @@ using Content.Shared.Resist; using Content.Shared.Storage; using Robust.Shared.Containers; +using Robust.Shared.Prototypes; namespace Content.Server.Resist; @@ -24,11 +26,17 @@ public sealed class EscapeInventorySystem : EntitySystem [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly CarryingSystem _carryingSystem = default!; // Carrying system from Nyanotrasen. + [Dependency] private readonly SharedActionsSystem _actions = default!; /// /// You can't escape the hands of an entity this many times more massive than you. /// public const float MaximumMassDisadvantage = 6f; + /// + /// Action to cancel inventory escape + /// + [ValidatePrototypeId] + private readonly string _escapeCancelAction = "ActionCancelEscape"; public override void Initialize() { @@ -37,6 +45,7 @@ public override void Initialize() SubscribeLocalEvent(OnRelayMovement); SubscribeLocalEvent(OnEscape); SubscribeLocalEvent(OnDropped); + SubscribeLocalEvent(OnCancelEscape); } private void OnRelayMovement(EntityUid uid, CanEscapeInventoryComponent component, ref MoveInputEvent args) @@ -84,12 +93,20 @@ private void OnRelayMovement(EntityUid uid, CanEscapeInventoryComponent componen _popupSystem.PopupEntity(Loc.GetString("escape-inventory-component-start-resisting"), user, user); _popupSystem.PopupEntity(Loc.GetString("escape-inventory-component-start-resisting-target"), container, container); + + // Add an escape cancel action + if (component.EscapeCancelAction is not { Valid: true }) + _actions.AddAction(user, ref component.EscapeCancelAction, _escapeCancelAction); } private void OnEscape(EntityUid uid, CanEscapeInventoryComponent component, EscapeInventoryEvent args) { component.DoAfter = null; + // Remove the cancel action regardless of do-after result + _actions.RemoveAction(uid, component.EscapeCancelAction); + component.EscapeCancelAction = null; + if (args.Handled || args.Cancelled) return; @@ -109,4 +126,13 @@ private void OnDropped(EntityUid uid, CanEscapeInventoryComponent component, Dro if (component.DoAfter != null) _doAfterSystem.Cancel(component.DoAfter); } + + private void OnCancelEscape(EntityUid uid, CanEscapeInventoryComponent component, EscapeInventoryCancelActionEvent args) + { + if (component.DoAfter != null) + _doAfterSystem.Cancel(component.DoAfter); + + _actions.RemoveAction(uid, component.EscapeCancelAction); + component.EscapeCancelAction = null; + } } diff --git a/Content.Shared/Nyanotrasen/Item/PseudoItem/AllowsSleepInsideComponent.cs b/Content.Shared/Nyanotrasen/Item/PseudoItem/AllowsSleepInsideComponent.cs new file mode 100644 index 00000000000..a28c7698fcd --- /dev/null +++ b/Content.Shared/Nyanotrasen/Item/PseudoItem/AllowsSleepInsideComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.Nyanotrasen.Item.PseudoItem; + +/// +/// Signifies that pseudo-item creatures can sleep inside the container to which this component is applied. +/// +[RegisterComponent] +public sealed partial class AllowsSleepInsideComponent : Component +{ +} diff --git a/Content.Shared/Nyanotrasen/Item/PseudoItem/PseudoItemComponent.cs b/Content.Shared/Nyanotrasen/Item/PseudoItem/PseudoItemComponent.cs index d3774439d36..458b514b969 100644 --- a/Content.Shared/Nyanotrasen/Item/PseudoItem/PseudoItemComponent.cs +++ b/Content.Shared/Nyanotrasen/Item/PseudoItem/PseudoItemComponent.cs @@ -3,10 +3,10 @@ namespace Content.Shared.Nyanotrasen.Item.PseudoItem; - /// - /// For entities that behave like an item under certain conditions, - /// but not under most conditions. - /// +/// +/// For entities that behave like an item under certain conditions, +/// but not under most conditions. +/// [RegisterComponent, AutoGenerateComponentState] public sealed partial class PseudoItemComponent : Component { @@ -24,4 +24,10 @@ public sealed partial class PseudoItemComponent : Component public Vector2i StoredOffset; public bool Active = false; + + /// + /// Action for sleeping while inside a container with . + /// + [DataField] + public EntityUid? SleepAction; } diff --git a/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.Checks.cs b/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.Checks.cs index 7000c654048..906503b3707 100644 --- a/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.Checks.cs +++ b/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.Checks.cs @@ -3,163 +3,33 @@ namespace Content.Shared.Nyanotrasen.Item.PseudoItem; -/// -/// Almost all of this is code taken from other systems, but adapted to use PseudoItem. -/// I couldn't use the original functions because the resolve would fuck shit up, even if I passed a constructed itemcomp -/// -/// This is horrible, and I hate it. But such is life -/// public partial class SharedPseudoItemSystem { - protected bool CheckItemFits(Entity itemEnt, Entity storageEnt) + /// + /// Checks if the pseudo-item can be inserted into the specified storage entity. + /// + /// + /// This function creates and uses a fake item component if the entity doesn't have one. + /// + public bool CheckItemFits(Entity itemEnt, Entity storageEnt) { if (!Resolve(itemEnt, ref itemEnt.Comp) || !Resolve(storageEnt, ref storageEnt.Comp)) return false; - if (Transform(itemEnt).Anchored) + if (!TryComp(itemEnt, out var metadata)) return false; - if (storageEnt.Comp.Whitelist?.IsValid(itemEnt, EntityManager) == false) - return false; - - if (storageEnt.Comp.Blacklist?.IsValid(itemEnt, EntityManager) == true) - return false; - - var maxSize = _storage.GetMaxItemSize(storageEnt); - if (_item.GetSizePrototype(itemEnt.Comp.Size) > maxSize) - return false; - - // The following is shitfucked together straight from TryGetAvailableGridSpace, but eh, it works - - var itemComp = new ItemComponent - { Size = itemEnt.Comp.Size, Shape = itemEnt.Comp.Shape, StoredOffset = itemEnt.Comp.StoredOffset }; - - var storageBounding = storageEnt.Comp.Grid.GetBoundingBox(); - - Angle startAngle; - if (storageEnt.Comp.DefaultStorageOrientation == null) - startAngle = Angle.FromDegrees(-itemComp.StoredRotation); // PseudoItem doesn't support this - else - { - if (storageBounding.Width < storageBounding.Height) - { - startAngle = storageEnt.Comp.DefaultStorageOrientation == StorageDefaultOrientation.Horizontal - ? Angle.Zero - : Angle.FromDegrees(90); - } - else - { - startAngle = storageEnt.Comp.DefaultStorageOrientation == StorageDefaultOrientation.Vertical - ? Angle.Zero - : Angle.FromDegrees(90); - } - } - - for (var y = storageBounding.Bottom; y <= storageBounding.Top; y++) - { - for (var x = storageBounding.Left; x <= storageBounding.Right; x++) - { - for (var angle = startAngle; angle <= Angle.FromDegrees(360 - startAngle); angle += Math.PI / 2f) - { - var location = new ItemStorageLocation(angle, (x, y)); - if (ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation)) - return true; - } - } - } - - return false; - } - - private bool ItemFitsInGridLocation( - Entity itemEnt, - Entity storageEnt, - Vector2i position, - Angle rotation) - { - if (!Resolve(itemEnt, ref itemEnt.Comp) || !Resolve(storageEnt, ref storageEnt.Comp)) - return false; - - var gridBounds = storageEnt.Comp.Grid.GetBoundingBox(); - if (!gridBounds.Contains(position)) - return false; - - var itemShape = GetAdjustedItemShape(itemEnt, rotation, position); - - foreach (var box in itemShape) + TryComp(itemEnt, out var item); + // If the entity doesn't have an item comp, create a fake one + // The fake component is never actually added to the entity + item ??= new ItemComponent { - for (var offsetY = box.Bottom; offsetY <= box.Top; offsetY++) - { - for (var offsetX = box.Left; offsetX <= box.Right; offsetX++) - { - var pos = (offsetX, offsetY); - - if (!IsGridSpaceEmpty(itemEnt, storageEnt, pos, itemShape)) - return false; - } - } - } - - return true; - } - - private IReadOnlyList GetAdjustedItemShape(Entity entity, Angle rotation, - Vector2i position) - { - if (!Resolve(entity, ref entity.Comp)) - return new Box2i[] { }; - - var shapes = entity.Comp.Shape ?? _item.GetSizePrototype(entity.Comp.Size).DefaultShape; - var boundingShape = shapes.GetBoundingBox(); - var boundingCenter = ((Box2) boundingShape).Center; - var matty = Matrix3.CreateTransform(boundingCenter, rotation); - var drift = boundingShape.BottomLeft - matty.TransformBox(boundingShape).BottomLeft; - - var adjustedShapes = new List(); - foreach (var shape in shapes) - { - var transformed = matty.TransformBox(shape).Translated(drift); - var floored = new Box2i(transformed.BottomLeft.Floored(), transformed.TopRight.Floored()); - var translated = floored.Translated(position); - - adjustedShapes.Add(translated); - } - - return adjustedShapes; - } - - private bool IsGridSpaceEmpty(Entity itemEnt, Entity storageEnt, - Vector2i location, IReadOnlyList shape) - { - if (!Resolve(storageEnt, ref storageEnt.Comp)) - return false; - - var validGrid = false; - foreach (var grid in storageEnt.Comp.Grid) - { - if (grid.Contains(location)) - { - validGrid = true; - break; - } - } - - if (!validGrid) - return false; - - foreach (var (ent, storedItem) in storageEnt.Comp.StoredItems) - { - if (ent == itemEnt.Owner) - continue; - - var adjustedShape = shape; - foreach (var box in adjustedShape) - { - if (box.Contains(location)) - return false; - } - } + Owner = itemEnt, + Shape = itemEnt.Comp.Shape, + Size = itemEnt.Comp.Size, + StoredOffset = itemEnt.Comp.StoredOffset + }; - return true; + return _storage.CanInsert(storageEnt, itemEnt, out _, storageEnt.Comp, item, ignoreStacks: true); } } diff --git a/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs b/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs index 4b7910746f1..5f4e6184346 100644 --- a/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs +++ b/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs @@ -1,14 +1,18 @@ +using Content.Shared.Actions; +using Content.Shared.Bed.Sleep; using Content.Shared.DoAfter; using Content.Shared.Hands; using Content.Shared.IdentityManagement; using Content.Shared.Interaction.Events; using Content.Shared.Item; using Content.Shared.Item.PseudoItem; +using Content.Shared.Popups; using Content.Shared.Storage; using Content.Shared.Storage.EntitySystems; using Content.Shared.Tag; using Content.Shared.Verbs; using Robust.Shared.Containers; +using Robust.Shared.Prototypes; namespace Content.Shared.Nyanotrasen.Item.PseudoItem; @@ -18,9 +22,13 @@ public abstract partial class SharedPseudoItemSystem : EntitySystem [Dependency] private readonly SharedItemSystem _item = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; [ValidatePrototypeId] private const string PreventTag = "PreventLabel"; + [ValidatePrototypeId] + private const string SleepActionId = "ActionSleep"; // The action used for sleeping inside bags. Currently uses the default sleep action (same as beds) public override void Initialize() { @@ -64,7 +72,7 @@ private void AddInsertVerb(EntityUid uid, PseudoItemComponent component, GetVerb args.Verbs.Add(verb); } - private bool TryInsert(EntityUid storageUid, EntityUid toInsert, PseudoItemComponent component, + public bool TryInsert(EntityUid storageUid, EntityUid toInsert, PseudoItemComponent component, StorageComponent? storage = null) { if (!Resolve(storageUid, ref storage)) @@ -87,6 +95,10 @@ private bool TryInsert(EntityUid storageUid, EntityUid toInsert, PseudoItemCompo return false; } + // If the storage allows sleeping inside, add the respective action + if (HasComp(storageUid)) + _actions.AddAction(toInsert, ref component.SleepAction, SleepActionId, toInsert); + component.Active = true; return true; } @@ -98,9 +110,11 @@ private void OnEntRemoved(EntityUid uid, PseudoItemComponent component, EntGotRe RemComp(uid); component.Active = false; + + _actions.RemoveAction(uid, component.SleepAction); // Remove sleep action if it was added } - private void OnGettingPickedUpAttempt(EntityUid uid, PseudoItemComponent component, + protected virtual void OnGettingPickedUpAttempt(EntityUid uid, PseudoItemComponent component, GettingPickedUpAttemptEvent args) { if (args.User == args.Item) @@ -154,7 +168,11 @@ protected void StartInsertDoAfter(EntityUid inserter, EntityUid toInsert, Entity NeedHand = true }; - _doAfter.TryStartDoAfter(args); + if (_doAfter.TryStartDoAfter(args)) + { + // Show a popup to the person getting picked up + _popupSystem.PopupEntity(Loc.GetString("carry-started", ("carrier", inserter)), toInsert, toInsert); + } } private void OnAttackAttempt(EntityUid uid, PseudoItemComponent component, AttackAttemptEvent args) diff --git a/Content.Shared/Resist/EscapeInventoryCancelEvent.cs b/Content.Shared/Resist/EscapeInventoryCancelEvent.cs new file mode 100644 index 00000000000..75ee09ff045 --- /dev/null +++ b/Content.Shared/Resist/EscapeInventoryCancelEvent.cs @@ -0,0 +1,5 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Resist; + +public sealed partial class EscapeInventoryCancelActionEvent : InstantActionEvent; diff --git a/Resources/Locale/en-US/actions/actions/sleep.ftl b/Resources/Locale/en-US/actions/actions/sleep.ftl index fd833fd4a5c..6188e1639fe 100644 --- a/Resources/Locale/en-US/actions/actions/sleep.ftl +++ b/Resources/Locale/en-US/actions/actions/sleep.ftl @@ -5,3 +5,5 @@ sleep-examined = [color=lightblue]{CAPITALIZE(SUBJECT($target))} {CONJUGATE-BE($ wake-other-success = You shake {THE($target)} awake. wake-other-failure = You shake {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} not waking up. + +popup-sleep-in-bag = {THE($entity)} curls up and falls asleep. diff --git a/Resources/Locale/en-US/nyanotrasen/carrying/carry.ftl b/Resources/Locale/en-US/nyanotrasen/carrying/carry.ftl index 4fa1abae8bd..490daced3f2 100644 --- a/Resources/Locale/en-US/nyanotrasen/carrying/carry.ftl +++ b/Resources/Locale/en-US/nyanotrasen/carrying/carry.ftl @@ -1,3 +1,4 @@ carry-verb = Carry carry-too-heavy = You're not strong enough. +carry-started = {THE($carrier)} is trying to pick you up! diff --git a/Resources/Prototypes/Actions/misc.yml b/Resources/Prototypes/Actions/misc.yml new file mode 100644 index 00000000000..60fec699210 --- /dev/null +++ b/Resources/Prototypes/Actions/misc.yml @@ -0,0 +1,10 @@ +- type: entity + id: ActionCancelEscape + name: Stop escaping + description: Calm down and sit peacefuly in your carrier's inventory + noSpawn: true + components: + - type: InstantAction + icon: Actions/escapeinventory.rsi/cancel-escape.png + event: !type:EscapeInventoryCancelActionEvent + useDelay: 2 diff --git a/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml b/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml index fbd5a02fa08..d72006f6c41 100644 --- a/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml +++ b/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml @@ -30,6 +30,7 @@ delay: 0.5 - type: ExplosionResistance damageCoefficient: 0.9 + - type: AllowsSleepInside # DeltaV - enable sleeping inside bags - type: entity parent: ClothingBackpack @@ -258,7 +259,7 @@ - type: Sprite sprite: Clothing/Back/Backpacks/syndicate.rsi - type: ExplosionResistance - damageCoefficient: 0.1 + damageCoefficient: 0.1 #Special - type: entity diff --git a/Resources/Textures/Actions/escapeinventory.rsi/cancel-escape.png b/Resources/Textures/Actions/escapeinventory.rsi/cancel-escape.png new file mode 100644 index 0000000000000000000000000000000000000000..609e9e3d199149f20e44d2684167d14ec0353ee9 GIT binary patch literal 559 zcmV+~0?_@5P)C<~Jk;wDqh(aouDd;i};T#PBQ58a& zHyRDGDJ4mgYnmL_Nqgmd!})(FzRWWIEN{&&k(O4!}HkuiK@U*EcKAx@acl>3J9v x$Dd)UhGdTYLIJq2bd+Wom#K+8KmYF&_y*p|(%DtXaRmSX002ovPDHLkV1kRp{4oFk literal 0 HcmV?d00001 diff --git a/Resources/Textures/Actions/escapeinventory.rsi/meta.json b/Resources/Textures/Actions/escapeinventory.rsi/meta.json new file mode 100644 index 00000000000..ba379dedab4 --- /dev/null +++ b/Resources/Textures/Actions/escapeinventory.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Duffelbag icon taken from tgstation at commit https://github.com/tgstation/tgstation/commit/547852588166c8e091b441e4e67169e156bb09c1 | Modified into cancel-escape.png by Mnemotechnician (github)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "cancel-escape" + } + ] +} From 00d786acd8eb2d76b05eb1560c68bdd479557693 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 17:29:07 +0000 Subject: [PATCH 37/56] Automatic Changelog Update (#484) --- Resources/Changelog/Changelog.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 847b9c90fbe..f0ee7cef2cc 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4293,3 +4293,17 @@ Entries: from being flunged into the void. id: 6137 time: '2024-07-05T16:53:25.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Fix + message: Carrying is less likely to behave erratically or suddenly interrupt now. + - type: Add + message: >- + You can now see when someone is trying to pick you up, and also you can + interrupt your attempt at escaping from their hands or inventory. + - type: Add + message: You can now properly take Felinids out of bags and place them inside. + - type: Add + message: Scientists have discovered that Felinids can sleep in bags. + id: 6138 + time: '2024-07-05T17:28:44.0000000+00:00' From 476e6ded461d9c1cfe0b9a4933ffe0880eca2e41 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Fri, 5 Jul 2024 20:33:39 +0300 Subject: [PATCH 38/56] Port Paper Signatures (#456) # Description Ports delta-v paper signatures implemented by me in https://github.com/DeltaV-Station/Delta-v/pull/1172, including the changes later introduced by deltanedas in https://github.com/DeltaV-Station/Delta-v/pull/1345. Everything should be pretty self-explanatory, see the original PR for details. ---

Media

![image](https://github.com/Simple-Station/Einstein-Engines/assets/69920617/50737402-a60d-425a-8938-f6e47427b22b) (see the original PR for a video demonstation)

--- # Changelog :cl: - add: You can now sign paper by alt-clicking it while holding a pen. --------- Signed-off-by: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> --- .../DeltaV/Paper/SignAttemptEvent.cs | 8 ++ .../DeltaV/Paper/SignatureSystem.cs | 104 ++++++++++++++++++ .../Locale/en-US/deltav/paper/signature.ftl | 5 + .../Objects/Misc/bureaucracy.rsi/meta.json | 5 +- .../bureaucracy.rsi/paper_stamp-signature.png | Bin 0 -> 955 bytes 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 Content.Server/DeltaV/Paper/SignAttemptEvent.cs create mode 100644 Content.Server/DeltaV/Paper/SignatureSystem.cs create mode 100644 Resources/Locale/en-US/deltav/paper/signature.ftl create mode 100644 Resources/Textures/Objects/Misc/bureaucracy.rsi/paper_stamp-signature.png diff --git a/Content.Server/DeltaV/Paper/SignAttemptEvent.cs b/Content.Server/DeltaV/Paper/SignAttemptEvent.cs new file mode 100644 index 00000000000..ff2d1b5103a --- /dev/null +++ b/Content.Server/DeltaV/Paper/SignAttemptEvent.cs @@ -0,0 +1,8 @@ +namespace Content.Server.DeltaV.Paper; + +/// +/// Raised on the pen when trying to sign a paper. +/// If it's cancelled the signature wasn't made. +/// +[ByRefEvent] +public record struct SignAttemptEvent(EntityUid Paper, EntityUid User, bool Cancelled = false); diff --git a/Content.Server/DeltaV/Paper/SignatureSystem.cs b/Content.Server/DeltaV/Paper/SignatureSystem.cs new file mode 100644 index 00000000000..07a249399bc --- /dev/null +++ b/Content.Server/DeltaV/Paper/SignatureSystem.cs @@ -0,0 +1,104 @@ +using Content.Server.Access.Systems; +using Content.Server.Paper; +using Content.Server.Popups; +using Content.Shared.Paper; +using Content.Shared.Popups; +using Content.Shared.Tag; +using Content.Shared.Verbs; +using Robust.Server.Audio; +using Robust.Shared.Player; + +namespace Content.Server.DeltaV.Paper; + +public sealed class SignatureSystem : EntitySystem +{ + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly IdCardSystem _idCard = default!; + [Dependency] private readonly PaperSystem _paper = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly TagSystem _tags = default!; + + // The sprite used to visualize "signatures" on paper entities. + private const string SignatureStampState = "paper_stamp-signature"; + + public override void Initialize() + { + SubscribeLocalEvent>(OnGetAltVerbs); + } + + private void OnGetAltVerbs(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + if (args.Using is not {} pen || !_tags.HasTag(pen, "Write")) + return; + + var user = args.User; + AlternativeVerb verb = new() + { + Act = () => + { + TrySignPaper(ent, user, pen); + }, + Text = Loc.GetString("paper-sign-verb"), + DoContactInteraction = true, + Priority = 10 + }; + args.Verbs.Add(verb); + } + + /// + /// Tries add add a signature to the paper with signer's name. + /// + public bool TrySignPaper(Entity paper, EntityUid signer, EntityUid pen) + { + var comp = paper.Comp; + + var ev = new SignAttemptEvent(paper, signer); + RaiseLocalEvent(pen, ref ev); + if (ev.Cancelled) + return false; + + var signatureName = DetermineEntitySignature(signer); + + var stampInfo = new StampDisplayInfo() + { + StampedName = signatureName, + StampedColor = Color.DarkSlateGray, //TODO Make this configurable depending on the pen. + }; + + if (!comp.StampedBy.Contains(stampInfo) && _paper.TryStamp(paper, stampInfo, SignatureStampState, comp)) + { + // Show popups and play a paper writing sound + var signedOtherMessage = Loc.GetString("paper-signed-other", ("user", signer), ("target", paper.Owner)); + _popup.PopupEntity(signedOtherMessage, signer, Filter.PvsExcept(signer, entityManager: EntityManager), true); + + var signedSelfMessage = Loc.GetString("paper-signed-self", ("target", paper.Owner)); + _popup.PopupEntity(signedSelfMessage, signer, signer); + + _audio.PlayPvs(comp.Sound, signer); + + _paper.UpdateUserInterface(paper, comp); + + return true; + } + else + { + // Show an error popup + _popup.PopupEntity(Loc.GetString("paper-signed-failure", ("target", paper.Owner)), signer, signer, PopupType.SmallCaution); + + return false; + } + } + + private string DetermineEntitySignature(EntityUid uid) + { + // If the entity has an ID, use the name on it. + if (_idCard.TryFindIdCard(uid, out var id) && !string.IsNullOrWhiteSpace(id.Comp.FullName)) + return id.Comp.FullName; + + // Alternatively, return the entity name + return Name(uid); + } +} diff --git a/Resources/Locale/en-US/deltav/paper/signature.ftl b/Resources/Locale/en-US/deltav/paper/signature.ftl new file mode 100644 index 00000000000..87741c962c0 --- /dev/null +++ b/Resources/Locale/en-US/deltav/paper/signature.ftl @@ -0,0 +1,5 @@ +paper-sign-verb = Sign + +paper-signed-other = {CAPITALIZE(THE($user))} signs {THE($target)}. +paper-signed-self = You sign {THE($target)}. +paper-signed-failure = You cannot sign {THE($target)} diff --git a/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json b/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json index 5117df77356..b57f9844bc7 100644 --- a/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json +++ b/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/e1142f20f5e4661cb6845cfcf2dd69f864d67432. paper_stamp-syndicate by Veritius. paper_receipt, paper_receipt_horizontal by eoineoineoin. pen_centcom is a resprited version of pen_cap by PuroSlavKing (Github). Luxury pen is drawn by Ubaser. Lawyer and psychologist paper stamp resprited by Guess-My-Name", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/e1142f20f5e4661cb6845cfcf2dd69f864d67432. paper_stamp-syndicate by Veritius. paper_receipt, paper_receipt_horizontal by eoineoineoin. pen_centcom is a resprited version of pen_cap by PuroSlavKing (Github). Luxury pen is drawn by Ubaser. Lawyer and psychologist paper stamp resprited by Guess-My-Name. paper_stamp-signature by Mnemotechnician.", "size": { "x": 32, "y": 32 @@ -259,6 +259,9 @@ }, { "name": "paper_stamp-psychologist" + }, + { + "name": "paper_stamp-signature" } ] } diff --git a/Resources/Textures/Objects/Misc/bureaucracy.rsi/paper_stamp-signature.png b/Resources/Textures/Objects/Misc/bureaucracy.rsi/paper_stamp-signature.png new file mode 100644 index 0000000000000000000000000000000000000000..6a7aa083ee597521958a77c2e68c9adf01ef78f5 GIT binary patch literal 955 zcmZvaziU)M5XXmHV#09&3l%jYK?aEy62VrC-o<1vgcwpNq&TpiN)S9Hkf`8^)fWq| zR2wS`!2v53Erm1|!UXg`h=ri2IL>>4a6f#oyR(m-{e0&;+g@6n8?PR&64AKKH|rk4>Jc$6(Y(UfUBAaGFEjF%Rt0)PO0JM27^LHg@r8)4mA}B zDql8GyU1Yrj~-7{6X^~%pGQ#D5`k^?rJ7L#_FE; zXCUr5a}1~t3x}H;1e$sgu5Mz;Iy79|vOWpVx{K^ySUS_qO9OY2+1wp!N=a~uA8?L( zoy%=^DMgb5<_Z%9ixPXE*OWrL>?Squ%Me&7Tr_(CV(l%rcq0f?hgfz_NVCq~*C5o} zpA<LPcuJrImEY#cq2UZ}NaOsR literal 0 HcmV?d00001 From 692ceff845aa4e2a5a519d40d6d666cfa0d6a8f8 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 17:34:01 +0000 Subject: [PATCH 39/56] Automatic Changelog Update (#456) --- Resources/Changelog/Changelog.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index f0ee7cef2cc..1b79764a611 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4307,3 +4307,9 @@ Entries: message: Scientists have discovered that Felinids can sleep in bags. id: 6138 time: '2024-07-05T17:28:44.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Add + message: You can now sign paper by alt-clicking it while holding a pen. + id: 6139 + time: '2024-07-05T17:33:39.0000000+00:00' From e06045ce586fa29f9404b8ae8f83f887dd5d8e81 Mon Sep 17 00:00:00 2001 From: WarMechanic <69510347+WarMechanic@users.noreply.github.com> Date: Sat, 6 Jul 2024 03:49:24 +1000 Subject: [PATCH 40/56] Felinid Soft Thieving + Trait (#500) # Description Reworks felinid thieving to no longer act like passive thieving gloves which grant invisible stripping (hereon referred to as hard stealing), in favour of soft thievery (hereon referred to as soft stealing). Soft thievery comprises of the following: - A smaller popup, with the thief anonymised. - A visible doafter bar - A 33% faster strip speed, that stacks with Thieving gloves - An additional ability to identify hidden items to better plan your course of action You no longer need to completely avoid felinids to maintain your precious items as long as you pay attention. For a felinid to utilise their thieving passive, they are encouraged to exploit any distractions to make moves on a target. If there is none, create one through conversation or other forms of player interaction. If you are suspected, persuade your victim that the thief is in fact, the other person. A faster strip speed makes thief bonuses diegetic to other players, and also improves the value proposition of thieving gloves on someone who already has thieving bonuses. Any other race can also gain soft thievery via a moderate costing trait. Non-felinid thieves are encouraged to exploit any felinids as a scapegoat. --- # TODO Code - [X] IgnoreStripHidden - allows thieves to look into peoples pockets - [X] StripTimeMultiplier - stripping at a multiplicative rate helps strip bags/belts which creates trait value - [X] Stealthy > Stealth - rather than a bool, distinguishes stealth levels as an enum Balance - [X] Soft thieves can identify items in pockets, which creates player agency - [X] Soft thieves steal 33% faster, which stacks with thieving gloves - [X] Victims to soft stealing get a smaller popup, useful if they're preoccupied - [X] Soft thievery is a trait, which Felinids get for free - [X] Felinids no longer hard steal items Media - [x] Attach media ---

Media

![image](https://github.com/Simple-Station/Einstein-Engines/assets/69510347/c0c6e81f-21e2-48c5-a535-777c1f683ef7) ![video](https://www.youtube.com/embed/elDfOgAPmIs?si=yV5JjOaSYvurGZer)

--- # Changelog :cl: - add: Added the Thievery trait, which provides various soft stripping bonuses - tweak: Felinids no longer have passive thieving gloves, they instead get the Thievery trait by default --------- Signed-off-by: WarMechanic <69510347+WarMechanic@users.noreply.github.com> Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> --- .../Inventory/StrippableBoundUserInterface.cs | 5 +- Content.Server/Strip/StrippableSystem.cs | 51 +++++++++++-------- .../EntitySystems/ToggleableClothingSystem.cs | 10 ++-- .../Strip/Components/StrippableComponent.cs | 8 +-- .../Strip/Components/ThievingComponent.cs | 20 ++++++-- .../Strip/SharedStrippableSystem.cs | 18 ++++++- Content.Shared/Strip/ThievingSystem.cs | 39 +++++++++++++- .../en-US/strip/strippable-component.ftl | 6 ++- Resources/Locale/en-US/traits/traits.ftl | 5 ++ .../Entities/Clothing/Hands/gloves.yml | 2 - .../Entities/Clothing/Hands/specific.yml | 1 - .../Entities/Mobs/Player/admin_ghost.yml | 4 +- .../Entities/Mobs/Species/felinid.yml | 5 ++ Resources/Prototypes/Traits/skills.yml | 14 +++++ 14 files changed, 143 insertions(+), 45 deletions(-) create mode 100644 Resources/Prototypes/Traits/skills.yml diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs index f8eb12df914..4bb49fecc14 100644 --- a/Content.Client/Inventory/StrippableBoundUserInterface.cs +++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs @@ -19,6 +19,7 @@ using Robust.Client.GameObjects; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Client.Player; using Robust.Shared.Input; using Robust.Shared.Map; using Robust.Shared.Prototypes; @@ -31,6 +32,7 @@ namespace Content.Client.Inventory public sealed class StrippableBoundUserInterface : BoundUserInterface { [Dependency] private readonly IUserInterfaceManager _ui = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; private readonly ExamineSystem _examine; private readonly InventorySystem _inv; private readonly SharedCuffableSystem _cuffable; @@ -198,7 +200,8 @@ private void AddInventoryButton(EntityUid invUid, string slotId, InventoryCompon var entity = container.ContainedEntity; // If this is a full pocket, obscure the real entity - if (entity != null && slotDef.StripHidden) + if (entity != null && slotDef.StripHidden + && !(EntMan.TryGetComponent(_playerManager.LocalEntity, out var thiefcomponent) && thiefcomponent.IgnoreStripHidden)) entity = _virtualHiddenEntity; var button = new SlotButton(new SlotData(slotDef, container)); diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs index 3b38b65a19d..686570f7dca 100644 --- a/Content.Server/Strip/StrippableSystem.cs +++ b/Content.Server/Strip/StrippableSystem.cs @@ -36,6 +36,7 @@ public sealed class StrippableSystem : SharedStrippableSystem [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly ThievingSystem _thieving = default!; // TODO: ECS popups. Not all of these have ECS equivalents yet. @@ -251,15 +252,17 @@ private void StartStripInsertInventory( var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime); - if (!stealth) - _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large); + bool hidden = stealth == ThievingStealth.Hidden; - var prefix = stealth ? "stealthily " : ""; + if (!hidden) + StripPopup("strippable-component-alert-owner-insert", stealth, target, user: Identity.Entity(user, EntityManager), item: user.Comp.ActiveHandEntity!.Value); + + var prefix = hidden ? "stealthily " : ""; _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot"); var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, true, slot), user, target, held) { - Hidden = stealth, + Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, @@ -340,20 +343,22 @@ private void StartStripRemoveInventory( var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime); - if (!stealth) + bool hidden = stealth == ThievingStealth.Hidden; + + if (!hidden) { if (slotDef.StripHidden) - _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-hidden", ("slot", slot)), target, target, PopupType.Large); + StripPopup("strippable-component-alert-owner-hidden", stealth, target, slot: slot); else - _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target, PopupType.Large); + StripPopup("strippable-component-alert-owner", stealth, target, user: Identity.Entity(user, EntityManager), item: item); } - var prefix = stealth ? "stealthily " : ""; + var prefix = hidden ? "stealthily " : ""; _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot"); var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(false, true, slot), user, target, item) { - Hidden = stealth, + Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, @@ -374,7 +379,7 @@ private void StripRemoveInventory( EntityUid target, EntityUid item, string slot, - bool stealth) + bool hidden) { if (!CanStripRemoveInventory(user, target, item, slot)) return; @@ -384,7 +389,7 @@ private void StripRemoveInventory( RaiseLocalEvent(item, new DroppedEvent(user), true); // Gas tank internals etc. - _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: stealth); + _handsSystem.PickupOrDrop(user, item, animateUser: hidden, animate: hidden); _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot"); } @@ -446,12 +451,14 @@ private void StartStripInsertHand( var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay); - var prefix = stealth ? "stealthily " : ""; + bool hidden = stealth == ThievingStealth.Hidden; + + var prefix = hidden ? "stealthily " : ""; _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands"); var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, false, handName), user, target, held) { - Hidden = stealth, + Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, @@ -471,7 +478,7 @@ private void StripInsertHand( Entity target, EntityUid held, string handName, - bool stealth) + bool hidden) { if (!Resolve(user, ref user.Comp) || !Resolve(target, ref target.Comp)) @@ -481,7 +488,7 @@ private void StripInsertHand( return; _handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: user.Comp); - _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: stealth, animate: stealth, handsComp: target.Comp); + _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: hidden, animate: hidden, handsComp: target.Comp); _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands"); // Hand update will trigger strippable update. @@ -543,15 +550,17 @@ private void StartStripRemoveHand( var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay); - if (!stealth) - _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target); + bool hidden = stealth == ThievingStealth.Hidden; + + if (!hidden) + StripPopup("strippable-component-alert-owner", stealth, target, user: Identity.Entity(user, EntityManager), item: item); - var prefix = stealth ? "stealthily " : ""; + var prefix = hidden ? "stealthily " : ""; _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands"); var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(false, false, handName), user, target, item) { - Hidden = stealth, + Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, @@ -572,7 +581,7 @@ private void StripRemoveHand( Entity target, EntityUid item, string handName, - bool stealth) + bool hidden) { if (!Resolve(user, ref user.Comp) || !Resolve(target, ref target.Comp)) @@ -582,7 +591,7 @@ private void StripRemoveHand( return; _handsSystem.TryDrop(target, item, checkActionBlocker: false, handsComp: target.Comp); - _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: stealth, handsComp: user.Comp); + _handsSystem.PickupOrDrop(user, item, animateUser: hidden, animate: hidden, handsComp: user.Comp); _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands"); // Hand update will trigger strippable update. diff --git a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs index 22a1d1a8f52..4abe7bc876a 100644 --- a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs @@ -27,6 +27,7 @@ public sealed class ToggleableClothingSystem : EntitySystem [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedStrippableSystem _strippable = default!; + [Dependency] private readonly ThievingSystem _thieving = default!; public override void Initialize() { @@ -97,6 +98,8 @@ private void StartDoAfter(EntityUid user, EntityUid item, EntityUid wearer, Togg var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, component.StripDelay.Value); + bool hidden = (stealth == ThievingStealth.Hidden); + var args = new DoAfterArgs(EntityManager, user, time, new ToggleClothingDoAfterEvent(), item, wearer, item) { BreakOnDamage = true, @@ -110,11 +113,8 @@ private void StartDoAfter(EntityUid user, EntityUid item, EntityUid wearer, Togg if (!_doAfter.TryStartDoAfter(args)) return; - if (!stealth) - { - var popup = Loc.GetString("strippable-component-alert-owner-interact", ("user", Identity.Entity(user, EntityManager)), ("item", item)); - _popupSystem.PopupEntity(popup, wearer, wearer, PopupType.Large); - } + if (!hidden) + _strippable.StripPopup("strippable-component-alert-owner-interact", stealth, wearer, user: Identity.Entity(user, EntityManager), item: item); } private void OnGetAttachedStripVerbsEvent(EntityUid uid, AttachedClothingComponent component, GetVerbsEvent args) diff --git a/Content.Shared/Strip/Components/StrippableComponent.cs b/Content.Shared/Strip/Components/StrippableComponent.cs index 4faca4d8f21..00725808297 100644 --- a/Content.Shared/Strip/Components/StrippableComponent.cs +++ b/Content.Shared/Strip/Components/StrippableComponent.cs @@ -32,12 +32,12 @@ public sealed class StrippingSlotButtonPressed(string slot, bool isHand) : Bound public sealed class StrippingEnsnareButtonPressed : BoundUserInterfaceMessage; [ByRefEvent] - public abstract class BaseBeforeStripEvent(TimeSpan initialTime, bool stealth = false) : EntityEventArgs, IInventoryRelayEvent + public abstract class BaseBeforeStripEvent(TimeSpan initialTime, ThievingStealth stealth = ThievingStealth.Obvious) : EntityEventArgs, IInventoryRelayEvent { public readonly TimeSpan InitialTime = initialTime; public float Multiplier = 1f; public TimeSpan Additive = TimeSpan.Zero; - public bool Stealth = stealth; + public ThievingStealth Stealth = stealth; public TimeSpan Time => TimeSpan.FromSeconds(MathF.Max(InitialTime.Seconds * Multiplier + Additive.Seconds, 0f)); @@ -51,7 +51,7 @@ public abstract class BaseBeforeStripEvent(TimeSpan initialTime, bool stealth = /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. /// [ByRefEvent] - public sealed class BeforeStripEvent(TimeSpan initialTime, bool stealth = false) : BaseBeforeStripEvent(initialTime, stealth); + public sealed class BeforeStripEvent(TimeSpan initialTime, ThievingStealth stealth = ThievingStealth.Obvious) : BaseBeforeStripEvent(initialTime, stealth); /// /// Used to modify strip times. Raised directed at the target. @@ -60,7 +60,7 @@ public sealed class BeforeStripEvent(TimeSpan initialTime, bool stealth = false) /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. /// [ByRefEvent] - public sealed class BeforeGettingStrippedEvent(TimeSpan initialTime, bool stealth = false) : BaseBeforeStripEvent(initialTime, stealth); + public sealed class BeforeGettingStrippedEvent(TimeSpan initialTime, ThievingStealth stealth = ThievingStealth.Obvious) : BaseBeforeStripEvent(initialTime, stealth); /// /// Organizes the behavior of DoAfters for . diff --git a/Content.Shared/Strip/Components/ThievingComponent.cs b/Content.Shared/Strip/Components/ThievingComponent.cs index a851dd5ef63..1d584627727 100644 --- a/Content.Shared/Strip/Components/ThievingComponent.cs +++ b/Content.Shared/Strip/Components/ThievingComponent.cs @@ -9,14 +9,24 @@ public sealed partial class ThievingComponent : Component /// /// How much the strip time should be shortened by /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("stripTimeReduction")] + [DataField] public TimeSpan StripTimeReduction = TimeSpan.FromSeconds(0.5f); + /// + /// A multiplier coefficient for strip time + /// + [DataField] + public float StripTimeMultiplier = 1f; + /// /// Should it notify the user if they're stripping a pocket? /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("stealthy")] - public bool Stealthy; + [DataField] + public ThievingStealth Stealth = ThievingStealth.Hidden; + + /// + /// Should the user be able to see hidden items? (i.e pockets) + /// + [DataField] + public bool IgnoreStripHidden; } diff --git a/Content.Shared/Strip/SharedStrippableSystem.cs b/Content.Shared/Strip/SharedStrippableSystem.cs index 7afd4f245a1..64dd6a81f3a 100644 --- a/Content.Shared/Strip/SharedStrippableSystem.cs +++ b/Content.Shared/Strip/SharedStrippableSystem.cs @@ -1,11 +1,14 @@ using Content.Shared.DragDrop; using Content.Shared.Hands.Components; +using Content.Shared.Popups; using Content.Shared.Strip.Components; namespace Content.Shared.Strip; public abstract class SharedStrippableSystem : EntitySystem { + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly ThievingSystem _thieving = default!; public override void Initialize() { base.Initialize(); @@ -14,7 +17,7 @@ public override void Initialize() SubscribeLocalEvent(OnDragDrop); } - public (TimeSpan Time, bool Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, TimeSpan initialTime) + public (TimeSpan Time, ThievingStealth Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, TimeSpan initialTime) { var userEv = new BeforeStripEvent(initialTime); RaiseLocalEvent(user, ref userEv); @@ -55,4 +58,17 @@ private void OnCanDrop(EntityUid uid, StrippableComponent component, ref CanDrop if (args.CanDrop) args.Handled = true; } + + public void StripPopup(string messageId, ThievingStealth stealth, EntityUid target, EntityUid? user = null, EntityUid? item = null, string slot = "") + { + bool subtle = stealth == ThievingStealth.Subtle; + PopupType? popupSize = _thieving.GetPopupTypeFromStealth(stealth); + + if (popupSize.HasValue) // We should always have a value if we're not hidden + _popup.PopupEntity(Loc.GetString(messageId, + ("user", subtle ? Loc.GetString("thieving-component-user") : user ?? EntityUid.Invalid), + ("item", subtle ? Loc.GetString("thieving-component-item") : item ?? EntityUid.Invalid), + ("slot", slot)), + target, target, popupSize.Value); + } } diff --git a/Content.Shared/Strip/ThievingSystem.cs b/Content.Shared/Strip/ThievingSystem.cs index 2b3d3b38a00..8f523accfea 100644 --- a/Content.Shared/Strip/ThievingSystem.cs +++ b/Content.Shared/Strip/ThievingSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Inventory; -using Content.Shared.Strip; +using Content.Shared.Popups; using Content.Shared.Strip.Components; +using Robust.Shared.Serialization; namespace Content.Shared.Strip; @@ -17,7 +18,41 @@ public override void Initialize() private void OnBeforeStrip(EntityUid uid, ThievingComponent component, BeforeStripEvent args) { - args.Stealth |= component.Stealthy; + args.Stealth = (ThievingStealth) Math.Max((sbyte) args.Stealth, (sbyte) component.Stealth); args.Additive -= component.StripTimeReduction; + args.Multiplier *= component.StripTimeMultiplier; } + + public PopupType? GetPopupTypeFromStealth(ThievingStealth stealth) + { + switch (stealth) + { + case ThievingStealth.Hidden: + return null; + + case ThievingStealth.Subtle: + return PopupType.Small; + + default: + return PopupType.Large; + } + } +} +[Serializable, NetSerializable] +public enum ThievingStealth : sbyte +{ + /// + /// Target sees a large popup indicating that an item is being stolen by who + /// + Obvious = 0, + + /// + /// Target sees a small popup indicating that an item is being stolen + /// + Subtle = 1, + + /// + /// Target does not see any popup regarding the stealing of an item + /// + Hidden = 2 } diff --git a/Resources/Locale/en-US/strip/strippable-component.ftl b/Resources/Locale/en-US/strip/strippable-component.ftl index 7654b20b03f..65d7844ee22 100644 --- a/Resources/Locale/en-US/strip/strippable-component.ftl +++ b/Resources/Locale/en-US/strip/strippable-component.ftl @@ -19,4 +19,8 @@ strip-verb-get-data-text = Strip ## UI strippable-bound-user-interface-stripping-menu-title = {$ownerName}'s inventory -strippable-bound-user-interface-stripping-menu-ensnare-button = Remove Leg Restraints \ No newline at end of file +strippable-bound-user-interface-stripping-menu-ensnare-button = Remove Leg Restraints + +# Stealth +thieving-component-user = Someone +thieving-component-item = something \ No newline at end of file diff --git a/Resources/Locale/en-US/traits/traits.ftl b/Resources/Locale/en-US/traits/traits.ftl index e9163bdb548..80680ac0db2 100644 --- a/Resources/Locale/en-US/traits/traits.ftl +++ b/Resources/Locale/en-US/traits/traits.ftl @@ -31,3 +31,8 @@ trait-description-SocialAnxiety = You are anxious when you speak and stutter. trait-name-Snoring = Snoring trait-description-Snoring = You will snore while sleeping. + +trait-name-Thieving = Thieving +trait-description-Thieving = + You are deft with your hands, and talented at convincing people of their belongings. + You can identify pocketed items, steal them quieter, and steal ~33% faster. diff --git a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml index bf08db78f71..4cd0c04e2be 100644 --- a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml +++ b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml @@ -231,7 +231,6 @@ - type: FingerprintMask - type: Thieving stripTimeReduction: 1 - stealthy: true - type: NinjaGloves - type: entity @@ -332,7 +331,6 @@ tags: [] # ignore "WhitelistChameleon" tag - type: Thieving stripTimeReduction: 1.5 - stealthy: true - type: entity parent: ClothingHandsGlovesColorWhite diff --git a/Resources/Prototypes/Entities/Clothing/Hands/specific.yml b/Resources/Prototypes/Entities/Clothing/Hands/specific.yml index e6a57319999..db34297b42a 100644 --- a/Resources/Prototypes/Entities/Clothing/Hands/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Hands/specific.yml @@ -29,4 +29,3 @@ components: - type: Thieving stripTimeReduction: 2 - stealthy: true diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml index 80e87d3670c..9bdfb18830e 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml @@ -85,8 +85,8 @@ range: 500 - type: StationLimitedNetwork - type: Thieving - stripTimeReduction: 9999 - stealthy: true + stripTimeMultiplier: 0 + ignoreStripHidden: true - type: Stripping - type: SolutionScanner - type: IgnoreUIRange diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml index 5bc02461eed..d23607b16d5 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml @@ -74,6 +74,11 @@ - GalacticCommon - SolCommon - Nekomimetic + - type: Thieving + ignoreStripHidden: true + stealth: Subtle + stripTimeReduction: 0 + stripTimeMultiplier: 0.667 - type: entity save: false diff --git a/Resources/Prototypes/Traits/skills.yml b/Resources/Prototypes/Traits/skills.yml new file mode 100644 index 00000000000..6175834c1fc --- /dev/null +++ b/Resources/Prototypes/Traits/skills.yml @@ -0,0 +1,14 @@ +- type: trait + id: Thieving + category: Physical + points: -4 + components: + - type: Thieving + ignoreStripHidden: true + stealth: Subtle + stripTimeReduction: 0 + stripTimeMultiplier: 0.667 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: Felinid From 4609f9e9b0dae53d99f4aeb46f20cbfbc2206b46 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Fri, 5 Jul 2024 20:49:57 +0300 Subject: [PATCH 41/56] Cherry-Pick Carrypets From Delta-V (#501) # Description Cherry-picks https://github.com/DeltaV-Station/Delta-v/pull/1145 All credit goes to the original author of the PR. Original description is: "adds carriable component to a lot of animals that didn't have it" --- # Why ## Renault my beloved!!! ---

Media

![image](https://github.com/Simple-Station/Einstein-Engines/assets/69920617/12777e9b-7d00-4df2-8703-a7f9e42ea1c6)

--- # Changelog :cl: Froffy025 - add: You can now carry most of the station pets. --------- Signed-off-by: Froffy025 <78222136+Froffy025@users.noreply.github.com> Co-authored-by: Froffy025 <78222136+froffy025@users.noreply.github.com> --- .../Prototypes/Entities/Mobs/NPCs/animals.yml | 30 +++++++++++++++++++ .../Prototypes/Entities/Mobs/NPCs/carp.yml | 1 + .../Entities/Mobs/NPCs/regalrat.yml | 2 ++ .../Prototypes/Entities/Mobs/NPCs/slimes.yml | 3 +- .../Prototypes/Entities/Mobs/NPCs/space.yml | 2 +- 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index e311681ce5f..7fe105f940c 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -13,6 +13,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: bat sprite: Mobs/Animals/bat.rsi + - type: Carriable - type: Speech speechSounds: Squeak speechVerb: SmallMob @@ -188,6 +189,8 @@ noMovementLayers: movement: state: chicken-0 + - type: Carriable + freeHandsRequired: 1 - type: Fixtures fixtures: fix1: @@ -580,6 +583,8 @@ - MobMask layer: - MobLayer + - type: Carriable + freeHandsRequired: 1 - type: Tag tags: - DoorBumpOpener @@ -840,6 +845,8 @@ noMovementLayers: movement: state: crab + - type: Carriable + freeHandsRequired: 1 - type: Physics - type: Fixtures fixtures: @@ -907,6 +914,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: goat sprite: Mobs/Animals/goat.rsi + - type: Carriable - type: Fixtures fixtures: fix1: @@ -999,6 +1007,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: goose sprite: Mobs/Animals/goose.rsi + - type: Carriable - type: Fixtures fixtures: fix1: @@ -1245,6 +1254,7 @@ sprite: "Effects/creampie.rsi" state: "creampie_human" visible: false + - type: Carriable - type: Hands - type: GenericVisualizer visuals: @@ -1767,6 +1777,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: lizard sprite: Mobs/Animals/lizard.rsi + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -1821,6 +1832,8 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: slug sprite: Mobs/Animals/slug.rsi + - type: Carriable + freeHandsRequired: 1 - type: Physics - type: Fixtures fixtures: @@ -1873,6 +1886,7 @@ noMovementLayers: movement: state: frog + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -1936,6 +1950,8 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: parrot sprite: Mobs/Animals/parrot.rsi + - type: Carriable + freeHandsRequired: 1 - type: Fixtures fixtures: fix1: @@ -1991,6 +2007,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: penguin sprite: Mobs/Animals/penguin.rsi + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -2124,6 +2141,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: snake sprite: Mobs/Animals/snake.rsi + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -2186,6 +2204,7 @@ noMovementLayers: movement: state: tarantula + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -2371,6 +2390,7 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: possum + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -2446,6 +2466,7 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: raccoon + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -2510,6 +2531,7 @@ noMovementLayers: movement: state: fox + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -2590,6 +2612,7 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: corgi + - type: Carriable - type: Physics - type: Speech speechVerb: Canine @@ -2746,6 +2769,7 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: cat + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -2925,6 +2949,8 @@ Base: kitten_dead Dead: Base: kitten_dead + - type: Carriable + freeHandsRequired: 1 - type: Butcherable spawned: - id: FoodMeat @@ -2955,6 +2981,7 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: sloth + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -3016,6 +3043,7 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: ferret + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -3217,6 +3245,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: pig sprite: Mobs/Animals/pig.rsi + - type: Carriable - type: Fixtures fixtures: fix1: @@ -3292,6 +3321,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: nymph sprite: Mobs/Animals/nymph.rsi + - type: Carriable - type: Physics - type: Fixtures fixtures: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml index 3e6c603626b..2aae27d31ef 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml @@ -22,6 +22,7 @@ layers: - map: [ "enum.DamageStateVisualLayers.Base" ] state: alive + - type: Carriable # This one is for you, deltanedas o7 - type: CombatMode - type: Physics - type: Fixtures diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index 50fe3b6765e..31a32333f3f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -170,6 +170,8 @@ description: He's da mini rat. He don't make da roolz. noSpawn: true #Must be configured to a King or the AI breaks. components: + - type: Carriable + freeHandsRequired: 1 - type: CombatMode - type: MovementSpeedModifier baseWalkSpeed : 3.5 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index 901bf149cbc..f18b371c4c2 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -1,4 +1,4 @@ -- type: entity +- type: entity name: basic slime id: MobAdultSlimes parent: [ SimpleMobBase, MobCombat ] @@ -17,6 +17,7 @@ layers: - map: [ "enum.DamageStateVisualLayers.Base" ] state: blue_adult_slime + - type: Carriable - type: Fixtures fixtures: fix1: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml index 9ea2d784dbb..9b79d67f408 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml @@ -1,4 +1,4 @@ -- type: entity +- type: entity name: basic id: MobSpaceBasic parent: SimpleSpaceMobBase From 4ebb3cc779b9df30d877f680b44dc287c22f1d19 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 17:53:28 +0000 Subject: [PATCH 42/56] Automatic Changelog Update (#500) --- Resources/Changelog/Changelog.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 1b79764a611..618c03ec9ed 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4313,3 +4313,13 @@ Entries: message: You can now sign paper by alt-clicking it while holding a pen. id: 6139 time: '2024-07-05T17:33:39.0000000+00:00' +- author: WarMechanic + changes: + - type: Add + message: Added the Thievery trait, which provides various soft stripping bonuses + - type: Tweak + message: >- + Felinids no longer have passive thieving gloves, they instead get the + Thievery trait by default + id: 6140 + time: '2024-07-05T17:49:25.0000000+00:00' From d971e7c5119ce3fa57339f15d47fbbf372a0e3b5 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 17:54:28 +0000 Subject: [PATCH 43/56] Automatic Changelog Update (#501) --- Resources/Changelog/Changelog.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 618c03ec9ed..d602db8a69b 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4323,3 +4323,9 @@ Entries: Thievery trait by default id: 6140 time: '2024-07-05T17:49:25.0000000+00:00' +- author: Froffy025 + changes: + - type: Add + message: You can now carry most of the station pets. + id: 6141 + time: '2024-07-05T17:49:57.0000000+00:00' From d5f73ad370982fa57b223099e016a93f033fd378 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Fri, 5 Jul 2024 22:10:24 +0300 Subject: [PATCH 44/56] Fix Them Pesky Job Requirements (#511) # Description Fixes job requirements using wrong locales Happened because one function had role-timer- as its default locale prefix and the other that called the first had null as the default. ---

Media

![image](https://github.com/Simple-Station/Einstein-Engines/assets/69920617/0d02c40a-a58f-4b48-89f5-e0a6b4ff75b9) ![image](https://github.com/Simple-Station/Einstein-Engines/assets/69920617/f3b651dd-492e-47c0-aa9f-74998c78f792)

--- # Changelog :cl: - fix: Job requirements are now displayed correctly. --- .../Players/PlayTimeTracking/JobRequirementsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs index 7688a3b3aaa..ee581186f35 100644 --- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs +++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs @@ -96,7 +96,7 @@ public bool IsAllowed(JobPrototype job, [NotNullWhen(false)] out FormattedMessag return CheckRoleTime(job.Requirements, out reason); } - public bool CheckRoleTime(HashSet? requirements, [NotNullWhen(false)] out FormattedMessage? reason, string? localePrefix = null) + public bool CheckRoleTime(HashSet? requirements, [NotNullWhen(false)] out FormattedMessage? reason, string? localePrefix = "role-timer-") { reason = null; From c4f77007441d8d7fef9067d4d6f70ec7182c3657 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 19:10:53 +0000 Subject: [PATCH 45/56] Automatic Changelog Update (#511) --- Resources/Changelog/Changelog.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index d602db8a69b..f9c1de93996 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4329,3 +4329,9 @@ Entries: message: You can now carry most of the station pets. id: 6141 time: '2024-07-05T17:49:57.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Fix + message: Job requirements are now displayed correctly. + id: 6142 + time: '2024-07-05T19:10:24.0000000+00:00' From aaee45aa0aaa063f9d2044d186f1f5a0966d58e9 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Sat, 6 Jul 2024 00:03:28 +0300 Subject: [PATCH 46/56] Restore Old Event Rates (#509) # Description This restores the basic/survival event rates to how they were before before #486 (however, it keeps the CVars created in it). - 5-25 minutes for basic instead of 15-35 - 4-12 minutes for survival instead of 20-45 --- # Why The PR made it so that survival rounds actually have less events than extended - that is considering that survival rounds are supposed to be troublesome and filled with events while extended ones are supposed to be a chill alternative. Other forks don't bother with editing CVars every 5 minutes, so this "opportunity to configure their experience" was never addressed. Based on the feedback I've received from other players, the change was rather negative. From my personal experience, survival rounds became an extended extended, where during a 2-hour shift you can get a grand total of one power outage and two mice infestations, and nothing else at all. --- # Changelog :cl: - tweak: Events should now occur as frequently as before. Note: server owner can configure the frequency on their server manually. Signed-off-by: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> --- Content.Shared/CCVar/CCVars.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 7bde756f7c3..3961818baaa 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -112,14 +112,14 @@ public static readonly CVarDef /// Close to how long you expect a round to last, so you'll probably have to tweak this on downstreams. ///
public static readonly CVarDef - EventsRampingAverageEndTime = CVarDef.Create("events.ramping_average_end_time", 120f, CVar.ARCHIVE | CVar.SERVERONLY); + EventsRampingAverageEndTime = CVarDef.Create("events.ramping_average_end_time", 40f, CVar.ARCHIVE | CVar.SERVERONLY); /// /// Average ending chaos modifier for the ramping event scheduler. /// Max chaos chosen for a round will deviate from this /// public static readonly CVarDef - EventsRampingAverageChaos = CVarDef.Create("events.ramping_average_chaos", 4f, CVar.ARCHIVE | CVar.SERVERONLY); + EventsRampingAverageChaos = CVarDef.Create("events.ramping_average_chaos", 6f, CVar.ARCHIVE | CVar.SERVERONLY); /* * Game @@ -176,26 +176,26 @@ public static readonly CVarDef /// /// Minimum time between Basic station events in seconds /// - public static readonly CVarDef // 15 Minutes - GameEventsBasicMinimumTime = CVarDef.Create("game.events_basic_minimum_time", 900, CVar.SERVERONLY); + public static readonly CVarDef // 5 Minutes + GameEventsBasicMinimumTime = CVarDef.Create("game.events_basic_minimum_time", 300, CVar.SERVERONLY); /// /// Maximum time between Basic station events in seconds /// - public static readonly CVarDef // 35 Minutes - GameEventsBasicMaximumTime = CVarDef.Create("game.events_basic_maximum_time", 2100, CVar.SERVERONLY); + public static readonly CVarDef // 25 Minutes + GameEventsBasicMaximumTime = CVarDef.Create("game.events_basic_maximum_time", 1500, CVar.SERVERONLY); /// /// Minimum time between Ramping station events in seconds /// - public static readonly CVarDef // 20 Minutes - GameEventsRampingMinimumTime = CVarDef.Create("game.events_ramping_minimum_time", 1200, CVar.SERVERONLY); + public static readonly CVarDef // 4 Minutes + GameEventsRampingMinimumTime = CVarDef.Create("game.events_ramping_minimum_time", 240, CVar.SERVERONLY); /// /// Maximum time between Ramping station events in seconds /// - public static readonly CVarDef // 45 Minutes - GameEventsRampingMaximumTime = CVarDef.Create("game.events_ramping_maximum_time", 2700, CVar.SERVERONLY); + public static readonly CVarDef // 12 Minutes + GameEventsRampingMaximumTime = CVarDef.Create("game.events_ramping_maximum_time", 720, CVar.SERVERONLY); /// /// From 50092e9988ec1718272a72e54507765eb81b61dc Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 21:03:51 +0000 Subject: [PATCH 47/56] Automatic Changelog Update (#509) --- Resources/Changelog/Changelog.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index f9c1de93996..cc18a7b1e00 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4335,3 +4335,11 @@ Entries: message: Job requirements are now displayed correctly. id: 6142 time: '2024-07-05T19:10:24.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Tweak + message: >- + Events should now occur as frequently as before. Note: server owner can + configure the frequency on their server manually. + id: 6143 + time: '2024-07-05T21:03:28.0000000+00:00' From 0a7acbf32dbb141b75d5bef584a07714099fef53 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Fri, 5 Jul 2024 17:49:08 -0400 Subject: [PATCH 48/56] Reapply "Don't look at this please." This reverts commit 8897a4b4732e16baee30be42dd9925419f4cdd0a. --- Content.Client/Entry/EntryPoint.cs | 1 + .../TypingIndicator/TypingIndicatorSystem.cs | 2 +- .../Components/NPCConversationComponent.cs | 152 +++++ .../NPC/Events/NPCConversationEvents.cs | 63 ++ .../NPCConversationTreePrototype.cs | 154 +++++ .../NPC/Systems/NPCConversationSystem.cs | 558 ++++++++++++++++++ .../SophicScribe/SophicScribeSystem.cs | 36 ++ .../Locale/en-US/npc/conversation/sophia.ftl | 82 +++ .../Structures/Research/sophicscribe.yml | 194 +++++- 9 files changed, 1240 insertions(+), 2 deletions(-) create mode 100644 Content.Server/NPC/Components/NPCConversationComponent.cs create mode 100644 Content.Server/NPC/Events/NPCConversationEvents.cs create mode 100644 Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs create mode 100644 Content.Server/NPC/Systems/NPCConversationSystem.cs create mode 100644 Resources/Locale/en-US/npc/conversation/sophia.ftl diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index a1fc68bbd2f..8636e0eb6aa 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -125,6 +125,7 @@ public override void Init() _prototypeManager.RegisterIgnore("alertLevels"); _prototypeManager.RegisterIgnore("nukeopsRole"); _prototypeManager.RegisterIgnore("stationGoal"); + _prototypeManager.RegisterIgnore("npcConversationTree"); _componentFactory.GenerateNetIds(); _adminManager.Initialize(); diff --git a/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs b/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs index c923738930a..443923f675c 100644 --- a/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs +++ b/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs @@ -54,7 +54,7 @@ private void OnClientTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs SetTypingIndicatorEnabled(uid.Value, ev.IsTyping); } - private void SetTypingIndicatorEnabled(EntityUid uid, bool isEnabled, AppearanceComponent? appearance = null) + public void SetTypingIndicatorEnabled(EntityUid uid, bool isEnabled, AppearanceComponent? appearance = null) { if (!Resolve(uid, ref appearance, false)) return; diff --git a/Content.Server/NPC/Components/NPCConversationComponent.cs b/Content.Server/NPC/Components/NPCConversationComponent.cs new file mode 100644 index 00000000000..c2a8ca31d7d --- /dev/null +++ b/Content.Server/NPC/Components/NPCConversationComponent.cs @@ -0,0 +1,152 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Content.Server.NPC.Events; +using Content.Server.NPC.Prototypes; +using Content.Server.NPC.Systems; + +namespace Content.Server.NPC.Components; + +[RegisterComponent] +[Access(typeof(NPCConversationSystem))] +public sealed partial class NPCConversationComponent : Component +{ + /// + /// Whether or not the listening logic is turned on. + /// + /// + /// Queued responses will still play through, but no new attempts to listen will be made. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("enabled")] + public bool Enabled = true; + + /* NYI: + /// + /// The NPC will pay attention when one of these words are said. + /// + [ViewVariables] + [DataField("aliases")] + public List Aliases = new(); + */ + + [ViewVariables] + [DataField("tree", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] + public string? ConversationTreeId; + + /// + /// This is the cached prototype. + /// + [ViewVariables] + public NPCConversationTreePrototype ConversationTree = default!; + + /// + /// Topics that are unlocked in the NPC's conversation tree. + /// + [ViewVariables] + public HashSet UnlockedTopics = new(); + + /// + /// How long until we stop paying attention to someone for a prompt. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("attentionSpan")] + public TimeSpan AttentionSpan = TimeSpan.FromSeconds(20); + + /// + /// This is the minimum delay before the NPC makes a response. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("delayBeforeResponse")] + public TimeSpan DelayBeforeResponse = TimeSpan.FromSeconds(0.3); + + /// + /// This is the approximate delay per letter typed in text. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("typingDelay")] + public TimeSpan TypingDelay = TimeSpan.FromSeconds(0.05); + + [ViewVariables] + public Stack ResponseQueue = new(); + + /// + /// This is when the NPC will respond with its top response. + /// + [ViewVariables] + [DataField("nextResponse", customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextResponse; + + /// + /// This is the direction the NPC was facing before looking towards a conversation partner. + /// + [ViewVariables] + public Angle OriginalFacing; + + /// + /// This is who the NPC is paying attention to for conversation. + /// + [ViewVariables] + public EntityUid? AttendingTo; + + /// + /// This is when the NPC will stop paying attention to a specific person. + /// + [ViewVariables] + [DataField("nextAttentionLoss", customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextAttentionLoss; + + /// + /// This event is fired the next time the NPC hears something from the + /// person they're speaking with and it takes control of the response. + /// + [ViewVariables] + public NPCConversationListenEvent? ListeningEvent; + +#region Idle Chatter + + /// + /// Whether or not the NPC will say things unprompted. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("idleEnabled")] + public bool IdleEnabled = true; + + /// + /// This is the approximate delay between idle chats. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("idleChatDelay")] + public TimeSpan IdleChatDelay = TimeSpan.FromMinutes(3); + + /// + /// This is the order in which idle chat lines are given. + /// + /// + /// This is randomized both on init and when the lines have been exhausted + /// to prevent repeating lines twice in a row and to avoid predictable patterns. + /// + /// It technically reduces randomness, with the benefit of less repetition. + /// + [ViewVariables(VVAccess.ReadWrite)] + public List IdleChatOrder = new(); + + /// + /// This is the next idle chat line that will be used. + /// + [ViewVariables(VVAccess.ReadWrite)] + public int IdleChatIndex = 0; + + /// + /// This is when the NPC will say something out of its list of idle lines. + /// + /// + /// This is reset every time the NPC speaks. + /// + [ViewVariables] + [DataField("nextIdleChat", customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextIdleChat; + +#endregion + +} + diff --git a/Content.Server/NPC/Events/NPCConversationEvents.cs b/Content.Server/NPC/Events/NPCConversationEvents.cs new file mode 100644 index 00000000000..eb04f59bdd5 --- /dev/null +++ b/Content.Server/NPC/Events/NPCConversationEvents.cs @@ -0,0 +1,63 @@ +using Robust.Shared.Audio; +using Content.Server.NPC.Systems; + +namespace Content.Server.NPC.Events; + +/// +/// This is used for dynamic responses and post-response events. +/// +[ImplicitDataDefinitionForInheritors] +[Access(typeof(NPCConversationSystem))] +public abstract partial class NPCConversationEvent : EntityEventArgs +{ + /// + /// This is the entity that the NPC is speaking to. + /// + public EntityUid? TalkingTo; +} + +/// +/// This event type is raised when an NPC hears a response when it was set to listen for one. +/// +/// +/// Set Handled to true when you want the NPC to stop listening. +/// The NPC will otherwise keep listening and block any attempt to find a prompt in the speaker's words. +/// +[ImplicitDataDefinitionForInheritors] +[Access(typeof(NPCConversationSystem))] +public abstract partial class NPCConversationListenEvent : HandledEntityEventArgs +{ + /// + /// This is the entity that said the message. + /// + public EntityUid? Speaker; + + /// + /// This is the original message that the NPC heard. + /// + public string Message = default!; + + /// + /// This is the message, parsed into separate words. + /// + public List Words = default!; +} + +public sealed partial class NPCConversationHelpEvent : NPCConversationEvent +{ + [DataField("text")] + public string? Text; + + [DataField("audio")] + public SoundSpecifier? Audio; +} + +/// +/// This event can be raised after a response to cause an NPC to stop paying attention to someone. +/// +public sealed partial class NPCConversationByeEvent : NPCConversationEvent { } + +// The following classes help demonstrate some of the features of the system. +// They may be separated out at some point. +public sealed partial class NPCConversationToldNameEvent : NPCConversationListenEvent { } + diff --git a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs new file mode 100644 index 00000000000..20a616d8308 --- /dev/null +++ b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs @@ -0,0 +1,154 @@ +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Content.Server.NPC.Events; + +namespace Content.Server.NPC.Prototypes; + +[Prototype("npcConversationTree")] +public sealed class NPCConversationTreePrototype : IPrototype, ISerializationHooks +{ + [ViewVariables] + [IdDataField] + public string ID { get; } = default!; + + /// + /// Dialogue contains all the topics to which an NPC can discuss. + /// + [ViewVariables] + [DataField("dialogue", required: true)] + public readonly NPCTopic[] Dialogue = default!; + + /// + /// Attention responses are what the NPC says when they start paying + /// attention to you without a specific question or prompt to respond to. + /// + [ViewVariables] + [DataField("attention", required: true)] + public readonly NPCResponse[] Attention = default!; + + /// + /// Idle responses are just things the NPC will say when nothing else is + /// going on, after some time. + /// + [ViewVariables] + [DataField("idle", required: true)] + public readonly NPCResponse[] Idle = default!; + + /// + /// Unknown responses are what the NPC says when they can't respond to a + /// particular question or prompt. + /// + [ViewVariables] + [DataField("unknown", required: true)] + public readonly NPCResponse[] Unknown = default!; + + /// + /// Custom responses are available to use in extensions to the NPC + /// Conversation system. + /// + // NOTE: This may be removed in favor of storing NPCResponses on custom + // components, i.e. an NPCShopkeeperComponent, but for now, it lives here + // to help demonstrate some features. + [ViewVariables] + [DataField("custom")] + public readonly Dictionary Custom = default!; + + /// + /// This exists as a quick way to map a prompt to a topic. + /// + public readonly Dictionary PromptToTopic = new(); + + // ISerializationHooks _is_ obsolete, but ConstructionGraphPrototype is using it as of this commit, + // and I'm not quite sure how to otherwise do this. + // + // I will look at that prototype when ISerializationHooks is phased out. + void ISerializationHooks.AfterDeserialization() + { + // Cache the strings mapping to prompts. + foreach (var topic in Dialogue) + { + foreach (var prompt in topic.Prompts) + { + PromptToTopic[prompt] = topic; + } + } + } +} + +[DataDefinition] +public sealed partial class NPCTopic +{ + [DataField] + public string[] Prompts = default!; + + /// + /// This determines the likelihood of this topic being selected over any + /// other, given the existence of multiple candidates. + /// + [DataField] + public float Weight = 1.0f; + + /// + /// Locked topics will not be accessible through dialogue until unlocked. + /// + [DataField] + public bool Locked; + + /// + /// Hidden topics won't show up in any form of "help" question. + /// + [DataField] + public bool Hidden; + + [DataField("responses", required: true)] + public NPCResponse[] Responses = default!; +} + +[DataDefinition] +public sealed partial class NPCResponse +{ + public NPCResponse() { } + + public NPCResponse(string? text, SoundSpecifier? audio = null, NPCConversationEvent? ev = null) + { + Text = text; + Audio = audio; + Event = ev; + } + + public override string ToString() + { + return $"NPCResponse({Text})"; + } + + [DataField] + public string? Text; + + [DataField] + public SoundSpecifier? Audio; + + /* [DataField("emote")] */ + /* public string? Emote; */ + + /// + /// This event is raised when the response is queued, + /// for the purpose of dynamic responses. + /// + [DataField] + public NPCConversationEvent? Is; + + /// + /// This event is raised after the response is made. + /// + [DataField] + public NPCConversationEvent? Event; + + /// + /// This event is raised when the NPC next hears a response, + /// allowing the response to be processed by other systems. + /// + [DataField] + public NPCConversationListenEvent? ListenEvent; +} + diff --git a/Content.Server/NPC/Systems/NPCConversationSystem.cs b/Content.Server/NPC/Systems/NPCConversationSystem.cs new file mode 100644 index 00000000000..015adb19de5 --- /dev/null +++ b/Content.Server/NPC/Systems/NPCConversationSystem.cs @@ -0,0 +1,558 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.RegularExpressions; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timing; +using Content.Server.Chat.Systems; +using Content.Server.Chat.TypingIndicator; +using Content.Server.NPC.HTN; +using Content.Server.NPC.Components; +using Content.Server.NPC.Events; +using Content.Server.NPC.Prototypes; +using Content.Server.Speech; +using Content.Shared.Interaction; +using Content.Server.Radio.Components; + +namespace Content.Server.NPC.Systems; + +public sealed class NPCConversationSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + [Dependency] private readonly ChatSystem _chatSystem = default!; + [Dependency] private readonly NPCSystem _npcSystem = default!; + [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!; + [Dependency] private readonly TransformSystem _transformSystem = default!; + [Dependency] private readonly TypingIndicatorSystem _typingIndicatorSystem = default!; + + private ISawmill _sawmill = default!; + + // TODO: attention attenuation. distance, facing, visible + // TODO: attending to multiple people, multiple streams of conversation + // TODO: multi-word prompts + // TODO: nameless prompting (pointing is good) + // TODO: aliases + + public static readonly string[] QuestionWords = { "who", "what", "when", "why", "where", "how" }; + public static readonly string[] Copulae = { "is", "are" }; + + public override void Initialize() + { + base.Initialize(); + + _sawmill = Logger.GetSawmill("npc.conversation"); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnUnpaused); + SubscribeLocalEvent(OnListenAttempt); + SubscribeLocalEvent(OnListen); + + SubscribeLocalEvent(OnBye); + SubscribeLocalEvent(OnHelp); + + SubscribeLocalEvent(OnToldName); + } + +#region API + + /// + /// Toggle the ability of an NPC to listen for topics. + /// + public void EnableConversation(EntityUid uid, bool enable = true, NPCConversationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + component.Enabled = enable; + } + + /// + /// Toggle the NPC's willingness to make idle comments. + /// + public void EnableIdleChat(EntityUid uid, bool enable = true, NPCConversationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + component.IdleEnabled = enable; + } + + /// + /// Return locked status of a dialogue topic. + /// + public bool IsDialogueLocked(EntityUid uid, string option, NPCConversationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return true; + + if (!component.ConversationTree.PromptToTopic.TryGetValue(option, out var topic)) + { + _sawmill.Warning($"Tried to check locked status of missing dialogue option `{option}` on {ToPrettyString(uid)}"); + return true; + } + + if (component.UnlockedTopics.Contains(topic)) + return false; + + return topic.Locked; + } + + /// + /// Unlock dialogue options normally locked in an NPC's conversation tree. + /// + public void UnlockDialogue(EntityUid uid, string option, NPCConversationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + if (component.ConversationTree.PromptToTopic.TryGetValue(option, out var topic)) + component.UnlockedTopics.Add(topic); + else + _sawmill.Warning($"Tried to unlock missing dialogue option `{option}` on {ToPrettyString(uid)}"); + } + + /// + public void UnlockDialogue(EntityUid uid, HashSet options, NPCConversationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + foreach (var option in options) + UnlockDialogue(uid, option, component); + } + + /// + /// Queue a response for an NPC with a visible typing indicator and delay between messages. + /// + /// + /// This can be used as opposed to the typical method. + /// + public void QueueResponse(EntityUid uid, NPCResponse response, NPCConversationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + if (response.Is is {} ev) + { + // This is a dynamic response which will call QueueResponse with static responses of its own. + ev.TalkingTo = component.AttendingTo; + RaiseLocalEvent(uid, (object) ev); + return; + } + + if (component.ResponseQueue.Count == 0) + { + DelayResponse(uid, component, response); + _typingIndicatorSystem.SetTypingIndicatorEnabled(uid, true); + } + + component.ResponseQueue.Push(response); + } + + /// + /// Make an NPC stop paying attention to someone. + /// + public void LoseAttention(EntityUid uid, NPCConversationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + component.AttendingTo = null; + component.ListeningEvent = null; + _rotateToFaceSystem.TryFaceAngle(uid, component.OriginalFacing); + } + +#endregion + + private void DelayResponse(EntityUid uid, NPCConversationComponent component, NPCResponse response) + { + if (response.Text == null) + return; + + component.NextResponse = _gameTiming.CurTime + + component.DelayBeforeResponse + + component.TypingDelay.TotalSeconds * TimeSpan.FromSeconds(response.Text.Length) * + _random.NextDouble(0.9, 1.1); + } + + private IEnumerable GetAvailableTopics(EntityUid uid, NPCConversationComponent component) + { + HashSet availableTopics = new(); + + foreach (var topic in component.ConversationTree.Dialogue) + { + if (!topic.Locked || component.UnlockedTopics.Contains(topic)) + availableTopics.Add(topic); + } + + return availableTopics; + } + + private IEnumerable GetVisibleTopics(EntityUid uid, NPCConversationComponent component) + { + HashSet visibleTopics = new(); + + foreach (var topic in component.ConversationTree.Dialogue) + { + if (!topic.Hidden && (!topic.Locked || component.UnlockedTopics.Contains(topic))) + visibleTopics.Add(topic); + } + + return visibleTopics; + } + + private void OnInit(EntityUid uid, NPCConversationComponent component, ComponentInit args) + { + if (component.ConversationTreeId == null) + return; + + component.ConversationTree = _prototype.Index(component.ConversationTreeId); + component.NextIdleChat = _gameTiming.CurTime + component.IdleChatDelay; + + for (var i = 0; i < component.ConversationTree.Idle.Length; ++i) + component.IdleChatOrder.Add(i); + + _random.Shuffle(component.IdleChatOrder); + } + + private void OnUnpaused(EntityUid uid, NPCConversationComponent component, ref EntityUnpausedEvent args) + { + component.NextResponse += args.PausedTime; + component.NextAttentionLoss += args.PausedTime; + component.NextIdleChat += args.PausedTime; + } + + private bool TryGetIdleChatLine(EntityUid uid, NPCConversationComponent component, [NotNullWhen(true)] out NPCResponse? line) + { + line = null; + + if (component.IdleChatOrder.Count() == 0) + return false; + + if (++component.IdleChatIndex == component.IdleChatOrder.Count()) + { + // Exhausted all lines in the pre-shuffled order. + // Reset the index and shuffle again. + component.IdleChatIndex = 0; + _random.Shuffle(component.IdleChatOrder); + } + + var index = component.IdleChatOrder[component.IdleChatIndex]; + + line = component.ConversationTree.Idle[index]; + + return true; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var component)) + { + var curTime = _gameTiming.CurTime; + + if (curTime >= component.NextResponse && component.ResponseQueue.Count > 0) + { + // Make a response. + Respond(uid, component, component.ResponseQueue.Pop()); + } + + if (curTime >= component.NextAttentionLoss && component.AttendingTo != null) + { + // Forget who we were talking to. + LoseAttention(uid, component); + } + + if (component.IdleEnabled && + curTime >= component.NextIdleChat && + TryGetIdleChatLine(uid, component, out var line)) + { + Respond(uid, component, line); + } + } + } + + private void OnListenAttempt(EntityUid uid, NPCConversationComponent component, ListenAttemptEvent args) + { + if (!component.Enabled || + // Don't listen to myself... + uid == args.Source || + // Don't bother listening to other NPCs. For now. + HasComp(args.Source) || + // We're already "typing" a response, so do that first. + component.ResponseQueue.Count > 0) + { + args.Cancel(); + } + } + + private void PayAttentionTo(EntityUid uid, NPCConversationComponent component, EntityUid speaker) + { + component.AttendingTo = speaker; + component.NextAttentionLoss = _gameTiming.CurTime + component.AttentionSpan; + component.OriginalFacing = _transformSystem.GetWorldRotation(uid); + } + + private void Respond(EntityUid uid, NPCConversationComponent component, NPCResponse response) + { + if (component.ResponseQueue.Count == 0) + _typingIndicatorSystem.SetTypingIndicatorEnabled(uid, false); + else + DelayResponse(uid, component, component.ResponseQueue.Peek()); + + if (component.AttendingTo != null) + { + // TODO: This line is a mouthful. Maybe write a public API that supports EntityCoordinates later? + var speakerCoords = Transform(component.AttendingTo.Value).Coordinates.ToMap(EntityManager, _transformSystem).Position; + _rotateToFaceSystem.TryFaceCoordinates(uid, speakerCoords); + } + + if (response.Event is {} ev) + { + ev.TalkingTo = component.AttendingTo; + RaiseLocalEvent(uid, (object) ev); + } + + if (response.ListenEvent != null) + component.ListeningEvent = response.ListenEvent; + + if (response.Text != null) + _chatSystem.TrySendInGameICMessage(uid, Loc.GetString(response.Text), InGameICChatType.Speak, false); + + if (response.Audio != null) + _audioSystem.PlayPvs(response.Audio, uid, + // TODO: Allow this to be configured per NPC/response. + AudioParams.Default + .WithVolume(8f) + .WithMaxDistance(9f) + .WithRolloffFactor(0.5f)); + + // Refresh our attention. + component.NextAttentionLoss = _gameTiming.CurTime + component.AttentionSpan; + component.NextIdleChat = component.NextAttentionLoss + component.IdleChatDelay; + } + + private List ParseMessageIntoWords(string message) + { + return Regex.Replace(message.Trim().ToLower(), @"(\p{P})", "") + .Split() + .ToList(); + } + + private bool FindResponse(EntityUid uid, NPCConversationComponent component, List words, [NotNullWhen(true)] out NPCResponse? response) + { + response = null; + + var availableTopics = GetAvailableTopics(uid, component); + + // Some topics are more interesting than others. + var greatestWeight = 0f; + NPCTopic? candidate = null; + + foreach (var word in words) + { + if (component.ConversationTree.PromptToTopic.TryGetValue(word, out var topic) && + availableTopics.Contains(topic) && + topic.Weight > greatestWeight) + { + greatestWeight = topic.Weight; + candidate = topic; + } + } + + if (candidate != null) + { + response = _random.Pick(candidate.Responses); + return true; + } + + return false; + } + + private bool JudgeQuestionLikelihood(EntityUid uid, NPCConversationComponent component, List words, string message) + { + if (message.Length > 0 && message[^1] == '?') + // A question mark is an absolute mark of a question. + return true; + + if (words.Count == 1) + // The usefulness of this is dubious, but it's definitely a question. + return QuestionWords.Contains(words[0]); + + if (words.Count >= 2) + return QuestionWords.Contains(words[0]) && Copulae.Contains(words[1]); + + return false; + } + + private void OnBye(EntityUid uid, NPCConversationComponent component, NPCConversationByeEvent args) + { + LoseAttention(uid, component); + } + + private void OnHelp(EntityUid uid, NPCConversationComponent component, NPCConversationHelpEvent args) + { + if (args.Text == null) + { + _sawmill.Error($"{ToPrettyString(uid)} heard a Help prompt but has no text for it."); + return; + } + + var availableTopics = GetVisibleTopics(uid, component); + var availablePrompts = availableTopics.Select(topic => topic.Prompts.FirstOrDefault()).ToArray(); + + string availablePromptsText; + if (availablePrompts.Count() <= 2) + { + availablePromptsText = Loc.GetString(args.Text, + ("availablePrompts", string.Join(" or ", availablePrompts)) + ); + } + else + { + availablePrompts[^1] = $"or {availablePrompts[^1]}"; + availablePromptsText = Loc.GetString(args.Text, + ("availablePrompts", string.Join(", ", availablePrompts)) + ); + } + + // Unlikely we'll be able to do audio that isn't hard-coded, + // so best to keep it general. + var response = new NPCResponse(availablePromptsText, args.Audio); + QueueResponse(uid, response, component); + } + + private void OnToldName(EntityUid uid, NPCConversationComponent component, NPCConversationListenEvent args) + { + if (!component.ConversationTree.Custom.TryGetValue("toldName", out var responses)) + return; + + var response = _random.Pick(responses); + if (response.Text == null) + { + _sawmill.Error($"{ToPrettyString(uid)} was told a name but had no text response."); + return; + } + + // The world's simplest heuristic for names: + if (args.Words.Count > 3) + { + // It didn't seem like a name, so wait for something that does. + return; + } + + var cleanedName = string.Join(" ", args.Words); + cleanedName = char.ToUpper(cleanedName[0]) + cleanedName.Remove(0, 1); + + var formattedResponse = new NPCResponse(Loc.GetString(response.Text, + ("name", cleanedName)), + response.Audio); + + QueueResponse(uid, formattedResponse, component); + args.Handled = true; + } + + private void OnListen(EntityUid uid, NPCConversationComponent component, ListenEvent args) + { + if (HasComp(args.Source)) + return; + + if (component.AttendingTo != null && component.AttendingTo != args.Source) + // Ignore someone speaking to us if we're already paying attention to someone else. + return; + + var words = ParseMessageIntoWords(args.Message); + if (words.Count == 0) + return; + + if (component.AttendingTo == args.Source) + { + // The person we're talking to said something to us. + + if (component.ListeningEvent is {} ev) + { + // We were waiting on this person to say something, and they've said something. + ev.Handled = false; + ev.Speaker = component.AttendingTo; + ev.Message = args.Message; + ev.Words = words; + RaiseLocalEvent(uid, (object) ev); + + if (ev.Handled) + component.ListeningEvent = null; + + return; + } + + // We're already paying attention to this person, + // so try to figure out if they said something we can talk about. + if (FindResponse(uid, component, words, out var response)) + { + // A response was found so go ahead with it. + QueueResponse(uid, response, component); + } + else if(JudgeQuestionLikelihood(uid, component, words, args.Message)) + { + // The message didn't match any of the prompts, but it seemed like a question. + var unknownResponse = _random.Pick(component.ConversationTree.Unknown); + QueueResponse(uid, unknownResponse, component); + } + + // If the message didn't seem like a question, + // and it didn't raise any of our topics, + // then politely ignore who we're talking with. + // + // It's better than spamming them with "I don't understand." + return; + } + + // See if someone said our name. + var myName = MetaData(uid).EntityName.ToLower(); + + // So this is a rough heuristic, but if our name occurs within the first three words, + // or is the very last one, someone might be trying to talk to us. + var payAttention = words[0] == myName || words[^1] == myName; + if (!payAttention) + { + for (int i = 1; i < Math.Min(2, words.Count); ++i) + { + if (words[i] == myName) + { + payAttention = true; + break; + } + } + } + + if (payAttention) + { + PayAttentionTo(uid, component, args.Source); + + if (!FindResponse(uid, component, words, out var response)) + { + if(JudgeQuestionLikelihood(uid, component, words, args.Message) && + // This subcondition exists to block our name being interpreted as a question in its own right. + words.Count > 1) + { + response = _random.Pick(component.ConversationTree.Unknown); + } + else + { + response = _random.Pick(component.ConversationTree.Attention); + } + } + + QueueResponse(uid, response, component); + } + } +} + diff --git a/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs b/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs index b1a6c1e9de1..ba5ff0a056d 100644 --- a/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs +++ b/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs @@ -1,5 +1,8 @@ using Content.Server.Psionics.Abilities; using Content.Server.Chat.Systems; +using Content.Server.NPC.Events; +using Content.Server.NPC.Systems; +using Content.Server.NPC.Prototypes; using Content.Server.Radio.Components; using Content.Server.Radio.EntitySystems; using Content.Server.StationEvents.Events; @@ -18,6 +21,8 @@ public sealed partial class SophicScribeSystem : EntitySystem [Dependency] private readonly RadioSystem _radioSystem = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly NPCConversationSystem _conversationSystem = default!; + protected ISawmill Sawmill = default!; public override void Update(float frameTime) { @@ -51,6 +56,32 @@ public override void Initialize() SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnGlimmerEventEnded); + SubscribeLocalEvent(OnGetGlimmer); + } + + private void OnGetGlimmer(EntityUid uid, SophicScribeComponent component, NPCConversationGetGlimmerEvent args) + { + if (args.Text == null) + { + Sawmill.Error($"{uid} heard a glimmer reading prompt but has no text for it"); + return; + } + + var tier = _glimmerSystem.GetGlimmerTier() switch + { + GlimmerTier.Minimal => Loc.GetString("glimmer-reading-minimal"), + GlimmerTier.Low => Loc.GetString("glimmer-reading-low"), + GlimmerTier.Moderate => Loc.GetString("glimmer-reading-moderate"), + GlimmerTier.High => Loc.GetString("glimmer-reading-high"), + GlimmerTier.Dangerous => Loc.GetString("glimmer-reading-dangerous"), + _ => Loc.GetString("glimmer-reading-critical"), + }; + + var glimmerReadingText = Loc.GetString(args.Text, + ("glimmer", (int) Math.Round(_glimmerSystem.GlimmerOutput)), ("tier", tier)); + + var response = new NPCResponse(glimmerReadingText); + _conversationSystem.QueueResponse(uid, response); } private void OnInteractHand(EntityUid uid, SophicScribeComponent component, InteractHandEvent args) @@ -83,4 +114,9 @@ private void OnGlimmerEventEnded(GlimmerEventEndedEvent args) _radioSystem.SendRadioMessage(speaker, message, channel, speaker); } } + public sealed partial class NPCConversationGetGlimmerEvent : NPCConversationEvent + { + [DataField] + public string? Text; + } } diff --git a/Resources/Locale/en-US/npc/conversation/sophia.ftl b/Resources/Locale/en-US/npc/conversation/sophia.ftl new file mode 100644 index 00000000000..c832d9fc17f --- /dev/null +++ b/Resources/Locale/en-US/npc/conversation/sophia.ftl @@ -0,0 +1,82 @@ +sophia-response-name = You may call me Sophia. +sophia-response-help = You may inquire about one of the following topics: {$availablePrompts}. + +sophia-response-hello-1 = Greetings. +sophia-response-hello-2 = Salutations. + +sophia-response-bye-1 = Fare thee well. +sophia-response-bye-2 = Gods be with you. +sophia-response-bye-3 = Come back wiser. + +sophia-idle-phrase-1 = Mmmm, another portent. +sophia-idle-phrase-2 = The noösphere is quite beautiful today. However, I don't think I could describe it in a way you could understand. +sophia-idle-phrase-3 = I've been here before. You have, too. + +sophia-response-attention-1 = What is it? +sophia-response-attention-2 = What do you seek? +sophia-response-attention-3 = Out with it. + +sophia-response-sorry-1 = That's not a question for me. +sophia-response-sorry-2 = Ask someone else. +sophia-response-sorry-3 = Maybe I know the answer, maybe I do not. Either way, I will not be answering that question. + +sophia-response-nature = My nature doesn't really matter, does it? I'm fulfilling my purpose. Can you say the same, or are you just wasting time? + +sophia-response-epi = 'Epistemics' is a word. Aspiring Hellenes they are, they wished to displace the Latin 'science.' However, in English, epistemics has undesired connotations as a study of knowledge itself, even though the Greek word is a literal replacement for 'science.' + +sophia-response-mantis = 'Mantis' means seer, soothsayer, or prophet. They must be so named because they seek to uncover the truth. And, fittingly with their psionic aptitude, 'mantis' and 'mind' both descend, to the best of our knowledge, from an absolutely ancient word that sounded something like 'mentis.' + +sophia-response-mystagogue = 'Mystagogue' literally means 'leader of the mystics.' You may know the suffix -gogue from 'demogogue.' + +sophia-response-oracle = Oracle? I don't know much about her, and she isn't keen to share her secrets with me. + +sophia-response-psionics = Psionics are extraordinary abilities originating from one's mind. There doesn't seem to be any dominant word to refer to someone with the ability to practice these, although I prefer 'psion' or 'psychic.' + +sophia-response-noosphere = The noösphere is a field connecting all of consciousness. It's the medium through which psionics works. Its strength and effects on the illusory world of the material are based on its pressure. Colloquially, noöspheric pressure is called 'glimmer.' + +sophia-response-god = 'God' is such a vague term. There are so many entities out there that have defeated mortality. How you choose to regard them is your business. + +sophia-response-morphotype = In the first century PCC, several entities reshaped men into their image. I had done the same, if you would believe it. I can offer no evidence of their existence, other than faint memories. Any specific morphotype you want to know about? + +sophia-response-calendar = It's currently 417 PCC. The casuality crisis neccesitated a new year to count from. Due to the nature of the crisis, it can only be said with certainty that 1 PCC is between 2400 and 2700 CE. + +sophia-response-crisis = The first FTL travel was incompatible with the old ways. Fortunately, its resolution made more apparent the inherent futility in trying to give one history, one narrative, one account. Truth cannot be found in the material world, only higher ones. + +sophia-response-metempsychosis = You've died thousands of times, and you'll die thousands more. Some of those lives you may dedicate to trying to stop the cycle. We all carry at least some memory of past lives, usually temporally recent ones. One of the great mysteries of the persistence of fragments is the high concentration of memories from the early 21st century CE, which, inverse to other periods, seem to be more common among the ignorant. + +sophia-response-truth = If you seek the truth, you're in the wrong place. From a perspective tainted by material reality, the best you can hope is to try and divine higher truths that are not dependent on it. + +sophia-response-job = I observe the glimmer here, and record it. + +sophia-response-human = Humans were the base for all the others. But they, too, were shaped. Long, long before the others. + +sophia-response-felinid = Felinids were the first, and the most willing. In true feline nature, they shaped themselves. + +sophia-response-oni = Oni, it is said, originated in Sirius. The brightest star in the night sky from Earth may have attracted some chromatically inclined entities, explaining their vivid coloring. But, that's just speculation. + +sophia-response-arachne = Arachne are the strangest of them. They're not fully mortal. They took the form of humans, but not their genes. Their creator wrote his name in their stead. + +sophia-response-moth = Moths scarecely look human, but, strangely, their genes confirm they are. Their creator shares his name with a genus of moths, and was responsible for the other outlier. + +sophia-response-lamiae = So, you remember? You must be remembering their mythological namesake. If you've really retained that fleeting memory over so many metempsychoses... Perhaps I've said too much. + +sophia-response-cyno = Were those... no... So faint. Ignorance! You cannot remember them! It's impossible! + +sophia-response-harpy = Harpies, it is said, were once men and women, sculpted by greed for a purpose long gone. They were abandoned by their creators on a world named Valerian 4b. + +sophia-response-valerian = The Harpy homeworld? Magestic mountains gleaming in white, forests of brilliant scarlet, oceans wine dark, yet no light to be seen by mortal eyes. The Harpies were made to thrive there. To them, their world was bathed in beautiful silver light. + +sophia-response-grue = You do not know of those. You cannot. I had so hoped to live a few cycles under normal causality. + +sophia-response-abraxas = That's a name of power, and I avoid speaking of him. He's the least content to rest, and the most infatuated with creating things from ignorance. + +sophia-response-zork = You wander into the slavering fangs of a hungry grue. There, did you enjoy this game? + +sophia-response-glimmer = The current glimmer reading is {$glimmer}. {$tier} + +glimmer-reading-minimal = That is extremely low. Nothing bad will happen, but I hope this is not at the cost of progression in your understanding of the universe. +glimmer-reading-low = That is quite low. Just barely enough to register any psionic activity here. +glimmer-reading-moderate = That is about the expected level on a psionically active station. You may notice manageable, minor effects. +glimmer-reading-high = That is sure to start attracting attention, although still quite manageable. +glimmer-reading-dangerous = That's a bit concerning. You may want to redirect efforts to reducing it. +glimmer-reading-critical = That's apocalyptic, in the original sense of the word. That is, to say, revealing. This is the sort of time and place to acquire secret knowledge. diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml index 8e34a07ea5e..5213608d95e 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml @@ -1,7 +1,7 @@ - type: entity parent: BaseStructure id: SophicScribe - name: sophie + name: Sophie description: Latest reports on the Noösphere! components: - type: Sprite @@ -27,6 +27,10 @@ channels: - Common - Science + - type: ActiveListener + - type: TypingIndicator + - type: NPCConversation + tree: SophiaTree - type: PotentialPsionic #this makes her easier to access for glimmer events, dw about it - type: Psionic psychicFeedback: @@ -39,3 +43,191 @@ - type: GuideHelp guides: - Psionics + +- type: npcConversationTree + id: SophiaTree + dialogue: + - prompts: [glimmer, reading] + responses: + - is: !type:NPCConversationGetGlimmerEvent + text: sophia-response-glimmer + + - prompts: [purpose, job, occupation, profession] + weight: 0.5 + responses: + - text: sophia-response-job + + - prompts: [help, topics] + weight: 0.5 + hidden: true + responses: + - is: !type:NPCConversationHelpEvent + text: sophia-response-help + + - prompts: [nature, statue, snake, being] + weight: 0.3 + responses: + - text: sophia-response-nature + + - prompts: [epistemics, epi] + weight: 0.2 + responses: + - text: sophia-response-epi + + - prompts: [mantis] + weight: 0.2 + responses: + - text: sophia-response-mantis + + - prompts: [mystagogue, mysta] + weight: 0.2 + responses: + - text: sophia-response-mystagogue + + - prompts: [psionics, psychic] + weight: 0.2 + responses: + - text: sophia-response-psionics + + - prompts: [noösphere, noosphere] + weight: 0.2 + responses: + - text: sophia-response-noosphere + + - prompts: [metempsychosis, metempsychoses, reincarnation, death, dying, afterlife] + weight: 0.2 + responses: + - text: sophia-response-metempsychosis + + - prompts: [calendar] + weight: 0.2 + responses: + - text: sophia-response-calendar + + - prompts: [morphotypes, morphotype, species] + weight: 0.2 + responses: + - text: sophia-response-morphotype + + - prompts: [gods, god] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-god + + - prompts: [truth, "true", "false", falsity, falsehood] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-truth + + - prompts: [human, humans, humanoid, unmutated] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-human + + - prompts: [felinid, felinids, felid, felids, catperson, catpeople] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-felinid + + - prompts: [oni, onis] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-oni + + - prompts: [arachne, arachnid, arachnids, spiderperson, spiderpeople] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-arachne + + - prompts: [moth, moths, moff, moths] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-moth + + - prompts: [lamiae, lamia, lamias] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-lamiae + + - prompts: [grue, grues, batperson, batpeople] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-grue + + - prompts: [cynocephalus, cynocephali, cyno, cynos] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-cyno + + - prompts: [harpy, harpies] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-harpy + + - prompts: [valerian, Valerian, 4b] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-valerian + + - prompts: [crisis, causality] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-crisis + + - prompts: [oracle] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-oracle + + - prompts: [abraxas] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-abraxas + + - prompts: [hi, hello, hey, greetings, salutations] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-hello-1 + - text: sophia-response-hello-2 + + - prompts: [bye, goodbye, done, farewell, later, seeya] + weight: 0.1 + hidden: true + responses: + - text: sophia-response-bye-1 + event: !type:NPCConversationByeEvent + - text: sophia-response-bye-2 + event: !type:NPCConversationByeEvent + - text: sophia-response-bye-3 + event: !type:NPCConversationByeEvent + + attention: + - text: sophia-response-attention-1 + - text: sophia-response-attention-2 + - text: sophia-response-attention-3 + + idle: + - text: sophia-idle-phrase-1 + - text: sophia-idle-phrase-2 + - text: sophia-idle-phrase-3 + + unknown: + - text: sophia-response-sorry-1 + - text: sophia-response-sorry-2 + - text: sophia-response-sorry-3 From 8de45e767723a08d824bba4c2fda8411f2081e02 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Fri, 5 Jul 2024 18:53:36 -0400 Subject: [PATCH 49/56] More Loadout Items Wave 1 (#425) # Description The original release of Loadouts to EE did not have much in the way of actual implementation as far as the item list was concerned. This PR seeks to get a head start at fixing that by implementing a fairly sizeable chunk of new items that can be bought with loadout points. One important addition is the use of "non-clothing-job-related items", which helps further set this version of Loadouts apart from the /tg/ style of loadouts implemented elsewhere. One of the key strengths of this loadout system is that it's not limited to just replacing a job's clothing selections, but can also add items unrelated to clothing. My given example for this is the new option for Security characters(provided they have sufficient number of hours as a Security Officer) can spend roughly half their loadout points to start with a spare magazine for their handgun. Secondly, we also now have an opportunity to place non-nanotrasen corporate items in the game, in such a way that players can tie their characters to other corporations. Ostensibly this is to build up to the idea of "Contractor" characters. I hope that this will help broaden people's horizons. # On "Syndie" items I'd like to take this as an opportunity to kind of push people away from the whole thing of "THATS A SYNDICATE ITEM!!!!" wherever possible. For the most part this is very much NRP behavior that I'd like to discourage. With some notable exceptions, this doesn't mean outright antag items should be obtainable through Loadouts. To that end, items such as Interdyne branded smokes, Cybersun branded clothing, and other items with "In-Universe Company Brands" should be moved away from their identity as strictly antag objects, and instead things characters could reasonably have bought with their paycheck. # Technical Changes I had to make some improvements to the way Loadouts handles species, namely it wasn't previously possible to have an item require any entry from a list of species. I updated it so that SpeciesRequirement takes a list of Species, and will accept any entry from that list. My given example for this is the entries for Nitrogen air tanks, which can only be purchased in loadouts by SlimePersons and Vox(who are not currently playable, but I am accounting for them). Other usage of this is in the Oni-specific Security items. I do hope to use this as an opportunity to introduce more species-specific goods in the future. # Media Outerwear ![image](https://github.com/Simple-Station/Einstein-Engines/assets/16548818/8d0d9286-7758-45f3-aaff-506dddd35da6) More job-related selections available to some departments ![image](https://github.com/Simple-Station/Einstein-Engines/assets/16548818/7b214593-7111-42d7-966e-2a340c766f61) More variety of trinkets and items, including instruments! ![image](https://github.com/Simple-Station/Einstein-Engines/assets/16548818/1b7befaf-c94c-4238-b93b-cbdf14e939dd) Species Specific Items! ![image](https://github.com/Simple-Station/Einstein-Engines/assets/16548818/e4a2a7e6-085d-4e1b-9ed9-635af8b70e84) # Changelog :cl: VMSolidus - add: The catalog of items available via Loadouts has been greatly expanded. Have fun customizing your characters! --------- Signed-off-by: VMSolidus Signed-off-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> Co-authored-by: Danger Revolution! <142105406+DangerRevolution@users.noreply.github.com> Co-authored-by: SimpleStation14 <130339894+SimpleStation14@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: SimpleStation Changelogs Co-authored-by: Pspritechologist <81725545+Pspritechologist@users.noreply.github.com> Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> Co-authored-by: stellar-novas --- .../customization/character-requirements.ftl | 2 +- .../Locale/en-US/loadouts/categories.ftl | 7 + Resources/Locale/en-US/loadouts/items.ftl | 2 + .../Entities/Clothing/Belt/belts.yml | 14 + .../Smokeables/Cigarettes/cigarette.yml | 2 +- .../Smokeables/Cigarettes/packs.yml | 2 +- .../Prototypes/Loadouts/Jobs/medical.yml | 264 +++++++++++++++++ .../Prototypes/Loadouts/Jobs/science.yml | 56 ++++ .../Prototypes/Loadouts/Jobs/security.yml | 266 +++++++++++++++++- .../Prototypes/Loadouts/Jobs/service.yml | 17 ++ Resources/Prototypes/Loadouts/categories.yml | 17 +- Resources/Prototypes/Loadouts/eyes.yml | 12 +- Resources/Prototypes/Loadouts/hands.yml | 115 ++++++++ Resources/Prototypes/Loadouts/head.yml | 184 ++++++++++-- Resources/Prototypes/Loadouts/items.yml | 140 ++++++++- Resources/Prototypes/Loadouts/neck.yml | 85 +++++- .../Prototypes/Loadouts/outerClothing.yml | 73 ++++- Resources/Prototypes/Loadouts/shoes.yml | 94 ++++++- Resources/Prototypes/Loadouts/species.yml | 35 +++ Resources/Prototypes/Loadouts/uniform.yml | 240 +++++++++++++++- 20 files changed, 1563 insertions(+), 64 deletions(-) create mode 100644 Resources/Prototypes/Loadouts/hands.yml create mode 100644 Resources/Prototypes/Loadouts/species.yml diff --git a/Resources/Locale/en-US/customization/character-requirements.ftl b/Resources/Locale/en-US/customization/character-requirements.ftl index b073bdb773f..efa1b7e7677 100644 --- a/Resources/Locale/en-US/customization/character-requirements.ftl +++ b/Resources/Locale/en-US/customization/character-requirements.ftl @@ -9,7 +9,7 @@ character-species-requirement = You must {$inverted -> character-trait-requirement = You must {$inverted -> [true] not have *[other] have -} the trait [color=lightblue]{$trait}[/color] +} one of these traits: [color=lightblue]{$traits}[/color] character-backpack-type-requirement = You must {$inverted -> [true] not use *[other] use diff --git a/Resources/Locale/en-US/loadouts/categories.ftl b/Resources/Locale/en-US/loadouts/categories.ftl index 685c5cbcbd9..9770bd8bafd 100644 --- a/Resources/Locale/en-US/loadouts/categories.ftl +++ b/Resources/Locale/en-US/loadouts/categories.ftl @@ -2,7 +2,14 @@ loadout-category-Uncategorized = Uncategorized loadout-category-Accessories = Accessories +loadout-category-Eyes = Eyes +loadout-category-Hands = Hands +loadout-category-Head = Head loadout-category-Items = Items loadout-category-Jobs = Jobs +loadout-category-Mask = Mask +loadout-category-Neck = Neck loadout-category-Outer = Outer +loadout-category-Shoes = Shoes +loadout-category-Species = Species loadout-category-Uniform = Uniform diff --git a/Resources/Locale/en-US/loadouts/items.ftl b/Resources/Locale/en-US/loadouts/items.ftl index a4819011262..b92f56bc7cb 100644 --- a/Resources/Locale/en-US/loadouts/items.ftl +++ b/Resources/Locale/en-US/loadouts/items.ftl @@ -11,3 +11,5 @@ loadout-description-LoadoutItemPlushieSharkBlue = Dive into battle with your ver loadout-description-LoadoutItemPlushieSharkPink = Unleash the power of pink with the Pink Shark Plushie! This rosy-hued predator might not have real teeth, but its sheer adorableness is enough to take a bite out of anyone's resolve. Watch as foes melt away in the face of its cottony charm. loadout-description-LoadoutItemPlushieSharkGrey = Introducing the Grey Shark Plushie, the apex predator of snuggles! With its sleek and understated design, this plushie strikes the perfect balance between cuddle companion and imaginary ocean guardian. Beware; opponents may be mesmerized by its dorsal fin's hypnotic sway! loadout-description-LoadoutItemPlushieCarp = Brace for extraterrestrial antics with the Purple Space Carp Plushie! A fishy invader from the cosmic deep, this plushie brings a splash of humor to zero-gravity escapades. From hostile waters to interstellar giggles, it's a cuddly contradiction that's out of this world +loadout-description-LoadoutSolCommonTranslator = The most common of all translators, such that it can be purchased in any civilized station. + This device translates Sol Common speech into Galactic Common. diff --git a/Resources/Prototypes/Entities/Clothing/Belt/belts.yml b/Resources/Prototypes/Entities/Clothing/Belt/belts.yml index 1f90b421526..e6c08bf90b9 100644 --- a/Resources/Prototypes/Entities/Clothing/Belt/belts.yml +++ b/Resources/Prototypes/Entities/Clothing/Belt/belts.yml @@ -607,6 +607,20 @@ - type: Clothing sprite: Clothing/Belt/securitywebbing.rsi +- type: entity + parent: ClothingBeltSecurityWebbing + id: ClothingBeltSecurityWebbingFilled + name: security webbing + description: Unique and versatile chest rig, can hold security gear. + components: + - type: StorageFill + contents: + - id: GrenadeFlashBang + - id: TearGasGrenade + - id: Stunbaton + - id: Handcuffs + - id: Handcuffs + - type: entity parent: ClothingBeltStorageBase id: ClothingBeltMercWebbing diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/cigarette.yml b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/cigarette.yml index 659cbaa28a2..18ea198697e 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/cigarette.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/cigarette.yml @@ -80,7 +80,7 @@ - ReagentId: Nicotine Quantity: 10 - ReagentId: Omnizine - Quantity: 30 + Quantity: 5 - type: entity id: CigaretteOmnizine diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml index bc9079fb2db..eb8e0a1ffe6 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml @@ -182,7 +182,7 @@ id: CigPackSyndicate parent: CigPackBase name: Interdyne herbals packet - description: Elite cigarettes for elite syndicate agents. Infused with medicine for when you need to do more than calm your nerves. + description: Premium medicinal cigarettes from the Interdyne Corporation. Not endorsed by the Terra-Gov Surgeon General. components: - type: Sprite sprite: Objects/Consumable/Smokeables/Cigarettes/Packs/syndicate.rsi diff --git a/Resources/Prototypes/Loadouts/Jobs/medical.yml b/Resources/Prototypes/Loadouts/Jobs/medical.yml index 5e88006fcee..6dcce11d09f 100644 --- a/Resources/Prototypes/Loadouts/Jobs/medical.yml +++ b/Resources/Prototypes/Loadouts/Jobs/medical.yml @@ -9,6 +9,8 @@ - MedicalDoctor - Paramedic - ChiefMedicalOfficer + - MedicalIntern + - Chemist items: - ClothingHandsGlovesNitrile @@ -35,6 +37,7 @@ jobs: - MedicalDoctor - ChiefMedicalOfficer + - MedicalIntern items: - ClothingNeckStethoscope @@ -47,6 +50,9 @@ - !type:CharacterJobRequirement jobs: - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern items: - UniformScrubsColorBlue @@ -59,6 +65,9 @@ - !type:CharacterJobRequirement jobs: - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern items: - UniformScrubsColorGreen @@ -71,9 +80,85 @@ - !type:CharacterJobRequirement jobs: - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern items: - UniformScrubsColorPurple +- type: loadout + id: LoadoutMedicalUniformScrubsCyan + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - UniformScrubsColorCyan + +- type: loadout + id: LoadoutMedicalUniformScrubsBlack + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - UniformScrubsColorBlack + +- type: loadout + id: LoadoutMedicalUniformScrubsPink + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - UniformScrubsColorPink + +- type: loadout + id: LoadoutMedicalUniformScrubsCybersun + category: Jobs + cost: 3 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + items: + - UniformScrubsColorCybersun + +- type: loadout + id: LoadoutMedicalOuterCybersunWindbreaker + category: Jobs + cost: 5 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + items: + - ClothingOuterCoatCybersunWindbreaker + - type: loadout id: LoadoutMedicalOuterLabcoatChem category: Jobs @@ -130,6 +215,9 @@ - !type:CharacterJobRequirement jobs: - MedicalDoctor + - Chemist + - Paramedic + - ChiefMedicalOfficer - !type:CharacterPlaytimeRequirement tracker: JobChemist min: 21600 # 6 hours @@ -151,6 +239,9 @@ - !type:CharacterJobRequirement jobs: - MedicalDoctor + - Chemist + - Paramedic + - ChiefMedicalOfficer - !type:CharacterPlaytimeRequirement tracker: JobChemist min: 21600 # 6 hours @@ -184,6 +275,9 @@ - !type:CharacterJobRequirement jobs: - MedicalDoctor + - Chemist + - Paramedic + - ChiefMedicalOfficer - !type:CharacterPlaytimeRequirement tracker: JobChemist min: 21600 # 6 hours @@ -195,3 +289,173 @@ min: 216000 # 60 hours items: - ClothingHeadHatBeretSeniorPhysician + +- type: loadout + id: LoadoutMedicalHeadSurgcapBlue + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - ClothingHeadHatSurgcapBlue + +- type: loadout + id: LoadoutMedicalHeadSurgcapPurple + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - ClothingHeadHatSurgcapPurple + +- type: loadout + id: LoadoutMedicalHeadSurgcapGreen + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - ClothingHeadHatSurgcapGreen + +- type: loadout + id: LoadoutMedicalHeadSurgcapCyan + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - ClothingHeadHatSurgcapCyan + +- type: loadout + id: LoadoutMedicalHeadSurgcapBlack + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - ClothingHeadHatSurgcapBlack + +- type: loadout + id: LoadoutMedicalHeadSurgcapPink + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - ClothingHeadHatSurgcapPink + +- type: loadout + id: LoadoutMedicalHeadSurgcapWhite + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - ClothingHeadHatSurgcapWhite + +- type: loadout + id: LoadoutMedicalHeadSurgcapCybersun + category: Jobs + cost: 3 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + items: + - ClothingHeadHatSurgcapCybersun + +- type: loadout + id: LoadoutMedicalEyesHudMedical + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Paramedic + - ChiefMedicalOfficer + - MedicalIntern + - Brigmedic + items: + - ClothingEyesHudMedical + +- type: loadout + id: LoadoutMedicalEyesEyepatchHudMedical + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Paramedic + - ChiefMedicalOfficer + - MedicalIntern + - Brigmedic + items: + - ClothingEyesEyepatchHudMedical + +- type: loadout + id: LoadoutMedicalEyesHudMedicalPrescription + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Paramedic + - ChiefMedicalOfficer + - MedicalIntern + - Brigmedic + - !type:CharacterTraitRequirement + traits: + - Nearsighted + items: + - ClothingEyesPrescriptionMedHud diff --git a/Resources/Prototypes/Loadouts/Jobs/science.yml b/Resources/Prototypes/Loadouts/Jobs/science.yml index ad6f02e589e..b9c815a15b0 100644 --- a/Resources/Prototypes/Loadouts/Jobs/science.yml +++ b/Resources/Prototypes/Loadouts/Jobs/science.yml @@ -84,3 +84,59 @@ - ResearchDirector items: - ClothingHeadHatBeretRND + +- type: loadout + id: LoadoutScienceEyesHudDiagnostic + category: Jobs + cost: 3 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Scientist + - ResearchAssistant + - ResearchDirector + items: + - ClothingEyesHudDiagnostic + +- type: loadout + id: LoadoutScienceEyesEyepatchHudDiag + category: Jobs + cost: 3 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Scientist + - ResearchAssistant + - ResearchDirector + items: + - ClothingEyesEyepatchHudDiag + +## Robes +- type: loadout + id: LoadoutOuterRobeTechPriest + category: Outer + cost: 2 + items: + - ClothingOuterRobeTechPriest + requirements: + - !type:CharacterJobRequirement + jobs: + - Scientist + - ResearchAssistant + - ResearchDirector + +- type: loadout + id: LoadoutHeadHoodTechPriest + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadTechPriest + requirements: + - !type:CharacterJobRequirement + jobs: + - Scientist + - ResearchAssistant + - ResearchDirector diff --git a/Resources/Prototypes/Loadouts/Jobs/security.yml b/Resources/Prototypes/Loadouts/Jobs/security.yml index ecf7e4893a2..e6a6693ec18 100644 --- a/Resources/Prototypes/Loadouts/Jobs/security.yml +++ b/Resources/Prototypes/Loadouts/Jobs/security.yml @@ -1,7 +1,22 @@ +## Uniforms - type: loadout - id: LoadoutSecurityUniformGrey + id: LoadoutSecurityUniformJumpsuitBlue category: Jobs - cost: 2 + cost: 1 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - SecurityCadet + - Warden + items: + - ClothingUniformJumpsuitSecBlue + +- type: loadout + id: LoadoutSecurityUniformJumpsuitGrey + category: Jobs + cost: 1 exclusive: true requirements: - !type:CharacterJobRequirement @@ -12,6 +27,34 @@ items: - ClothingUniformJumpsuitSecGrey +- type: loadout + id: LoadoutSecurityUniformJumpskirtGrey + category: Jobs + cost: 1 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - SecurityCadet + - Warden + items: + - ClothingUniformJumpskirtSecGrey + +- type: loadout + id: LoadoutSecurityUniformJumpskirtBlue + category: Jobs + cost: 1 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - SecurityCadet + - Warden + items: + - ClothingUniformJumpskirtSecBlue + - type: loadout id: LoadoutSecurityUniformJumpskirtSenior category: Jobs @@ -60,6 +103,108 @@ items: - ClothingUniformJumpsuitSeniorOfficer +- type: loadout + id: LoadoutUniformJumpsuitWardenBlue + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Warden + items: + - ClothingUniformJumpsuitWardenBlue + +- type: loadout + id: LoadoutUniformJumpsuitWardenGrey + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Warden + items: + - ClothingUniformJumpsuitWardenGrey + +- type: loadout + id: LoadoutUniformJumpskirtWardenBlue + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Warden + items: + - ClothingUniformJumpskirtWardenBlue + +- type: loadout + id: LoadoutUniformJumpskirtWardenGrey + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Warden + items: + - ClothingUniformJumpskirtWardenGrey + +- type: loadout + id: LoadoutUniformJumpskirtHoSBlue + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - HeadOfSecurity + items: + - ClothingUniformJumpskirtHoSBlue + +- type: loadout + id: LoadoutUniformJumpskirtHoSGrey + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - HeadOfSecurity + items: + - ClothingUniformJumpskirtHoSGrey + +- type: loadout + id: LoadoutUniformJumpsuitSecFormal + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Detective + - SecurityOfficer + - Warden + - HeadOfSecurity + items: + - ClothingUniformJumpsuitSecFormal + +- type: loadout + id: LoadoutUniformJumpsuitSecSummer + category: Jobs + cost: 1 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Detective + - SecurityOfficer + - Warden + - HeadOfSecurity + items: + - ClothingUniformJumpsuitSecSummer +## Mask - type: loadout id: LoadoutSecurityMaskGasSwat category: Jobs @@ -73,6 +218,7 @@ items: - ClothingMaskGasSwat +## Shoes - type: loadout id: LoadoutSecurityShoesJackboots category: Jobs @@ -87,3 +233,119 @@ - HeadOfSecurity items: - ClothingShoesBootsJack + +## Eyes +- type: loadout + id: LoadoutSecurityEyesHudSecurity + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - Warden + - HeadOfSecurity + - Brigmedic + items: + - ClothingEyesHudSecurity + +- type: loadout + id: LoadoutSecurityEyesEyepatchHudSecurity + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - Warden + - HeadOfSecurity + - Brigmedic + items: + - ClothingEyesEyepatchHudSecurity + +- type: loadout + id: LoadoutSecurityEyesHudSecurityPrescription + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - Warden + - HeadOfSecurity + - Brigmedic + - !type:CharacterTraitRequirement + traits: + - Nearsighted + items: + - ClothingEyesPrescriptionHudSecurity + +## Head +- type: loadout + id: LoadoutSecurityHeadHatBeret + category: Jobs + cost: 1 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - Warden + - HeadOfSecurity + - Brigmedic + items: + - ClothingHeadHatBeretSecurity + +- type: loadout + id: LoadoutSecurityHeadHelmetInsulated + category: Jobs + cost: 3 + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - Warden + - HeadOfSecurity + - Brigmedic + items: + - ClothingHeadHelmetInsulated + +## Belt +- type: loadout + id: LoadoutSecurityBeltWebbing + category: Jobs + cost: 1 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Detective + - SecurityOfficer + - SecurityCadet + - Warden + - HeadOfSecurity + items: + - ClothingBeltSecurityWebbingFilled + +## Species +#- type: loadout ##Uncomment this and reassess points when we can make it replace the secoff duty pistol +# id: LoadoutSecurityEquipmentTruncheon +# category: Jobs +# cost: 8 ## TODO: Make this replace the secoff handgun, and thus also make it cheaper +# requirements: +# - !type:CharacterJobRequirement +# jobs: +# - SecurityOfficer +# - Warden +# - HeadOfSecurity +# - Brigmedic +# - !type:CharacterPlaytimeRequirement +# tracker: JobSecurityOfficer +# min: 36000 # 10 hours +# - !type:CharacterSpeciesRequirement +# species: Oni +# items: +# - Truncheon diff --git a/Resources/Prototypes/Loadouts/Jobs/service.yml b/Resources/Prototypes/Loadouts/Jobs/service.yml index 6ef3c3ad485..ed6107db9e9 100644 --- a/Resources/Prototypes/Loadouts/Jobs/service.yml +++ b/Resources/Prototypes/Loadouts/Jobs/service.yml @@ -1,3 +1,4 @@ +## Clown - type: loadout id: LoadoutServiceClownOutfitJester category: Jobs @@ -26,6 +27,7 @@ - ClothingHeadHatJesterAlt - ClothingShoesJester +## Bartender - type: loadout id: LoadoutServiceBartenderUniformPurple category: Jobs @@ -38,6 +40,7 @@ items: - ClothingUniformJumpsuitBartenderPurple +## Botanist - type: loadout id: LoadoutServiceBotanistUniformOveralls category: Jobs @@ -50,6 +53,7 @@ items: - ClothingUniformOveralls +## Lawyer - type: loadout id: LoadoutServiceLawyerUniformBlueSuit category: Jobs @@ -158,6 +162,7 @@ items: - ClothingUniformJumpsuitJournalist +## Reporter - type: loadout id: LoadoutServiceReporterUniformDetectivesuit category: Jobs @@ -181,3 +186,15 @@ - Reporter items: - ClothingUniformJumpskirtDetective + +## Musician +- type: loadout + id: LoadoutItemSynthesizerInstrument + category: Jobs + cost: 8 + requirements: + - !type:CharacterJobRequirement + jobs: + - Musician + items: + - SynthesizerInstrument diff --git a/Resources/Prototypes/Loadouts/categories.yml b/Resources/Prototypes/Loadouts/categories.yml index a4381941acc..79d2d7fe2bf 100644 --- a/Resources/Prototypes/Loadouts/categories.yml +++ b/Resources/Prototypes/Loadouts/categories.yml @@ -4,7 +4,13 @@ id: Uncategorized - type: loadoutCategory - id: Accessories + id: Eyes + +- type: loadoutCategory + id: Hands + +- type: loadoutCategory + id: Head - type: loadoutCategory id: Items @@ -12,8 +18,17 @@ - type: loadoutCategory id: Jobs +- type: loadoutCategory + id: Neck + - type: loadoutCategory id: Outer +- type: loadoutCategory + id: Shoes + +- type: loadoutCategory + id: Species + - type: loadoutCategory id: Uniform diff --git a/Resources/Prototypes/Loadouts/eyes.yml b/Resources/Prototypes/Loadouts/eyes.yml index a7a8cbd7736..74226604e92 100644 --- a/Resources/Prototypes/Loadouts/eyes.yml +++ b/Resources/Prototypes/Loadouts/eyes.yml @@ -1,13 +1,21 @@ - type: loadout id: LoadoutEyesEyepatch - category: Accessories + category: Eyes cost: 1 items: - ClothingEyesEyepatch - type: loadout id: LoadoutEyesBlindfold - category: Accessories + category: Eyes cost: 2 items: - ClothingEyesBlindfold + +- type: loadout + id: LoadoutItemSunglasses + category: Eyes + cost: 5 + exclusive: true + items: + - ClothingEyesGlassesSunglasses diff --git a/Resources/Prototypes/Loadouts/hands.yml b/Resources/Prototypes/Loadouts/hands.yml new file mode 100644 index 00000000000..3604678d387 --- /dev/null +++ b/Resources/Prototypes/Loadouts/hands.yml @@ -0,0 +1,115 @@ +- type: loadout + id: LoadoutHandsColorPurple + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorPurple + +- type: loadout + id: LoadoutHandsColorRed + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorRed + +- type: loadout + id: LoadoutHandsColorBlack + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorBlack + +- type: loadout + id: LoadoutHandsColorBlue + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorBlue + +- type: loadout + id: LoadoutHandsColorBrown + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorBrown + +- type: loadout + id: LoadoutHandsColorGray + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorGray + +- type: loadout + id: LoadoutHandsColorGreen + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorGreen + +- type: loadout + id: LoadoutHandsColorLightBrown + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorLightBrown + +- type: loadout + id: LoadoutHandsColorOrange + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorOrange + +- type: loadout + id: LoadoutHandsColorWhite + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorWhite + +- type: loadout + id: LoadoutHandsColorYellowBudget + category: Hands + cost: 4 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger + items: + - ClothingHandsGlovesColorYellowBudget + +- type: loadout + id: LoadoutHandsGlovesLeather + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesLeather + +- type: loadout + id: LoadoutHandsGlovesPowerglove + category: Hands + cost: 2 + exclusive: true + items: + - ClothingHandsGlovesPowerglove + +- type: loadout + id: LoadoutHandsGlovesRobohands + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesRobohands diff --git a/Resources/Prototypes/Loadouts/head.yml b/Resources/Prototypes/Loadouts/head.yml index 33a2f0b19bc..25cb4dadf11 100644 --- a/Resources/Prototypes/Loadouts/head.yml +++ b/Resources/Prototypes/Loadouts/head.yml @@ -1,125 +1,273 @@ +## Hats - type: loadout id: LoadoutHeadBeaverHat - category: Accessories + category: Head cost: 2 + exclusive: true items: - ClothingHeadHatBeaverHat - type: loadout id: LoadoutHeadTophat - category: Accessories + category: Head cost: 2 + exclusive: true items: - ClothingHeadHatTophat +- type: loadout + id: LoadoutHeadFedoraBlack + category: Head + cost: 2 + exclusive: true + items: + - ClothingHeadHatFedoraBlack + +- type: loadout + id: LoadoutHeadFedoraChoc + category: Head + cost: 2 + exclusive: true + items: + - ClothingHeadHatFedoraChoc + +- type: loadout + id: LoadoutHeadFedoraWhite + category: Head + cost: 2 + exclusive: true + items: + - ClothingHeadHatFedoraWhite + +- type: loadout + id: LoadoutHeadFlatBlack + category: Head + cost: 2 + exclusive: true + items: + - ClothingHeadHatFlatBlack + +- type: loadout + id: LoadoutHeadFlatBrown + category: Head + cost: 2 + exclusive: true + items: + - ClothingHeadHatFlatBrown + +- type: loadout + id: LoadoutHeadTinfoil + category: Head + cost: 3 + exclusive: true + items: + - ClothingHeadTinfoil + +- type: loadout + id: LoadoutHeadBellhop + category: Head + cost: 2 + exclusive: true + items: + - ClothingHeadHatBellhop +## Color Hats - type: loadout id: LoadoutHeadHatBluesoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatBluesoft - type: loadout id: LoadoutHeadHatBluesoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatBluesoftFlipped - type: loadout id: LoadoutHeadHatCorpsoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatCorpsoft - type: loadout id: LoadoutHeadHatCorpsoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatCorpsoftFlipped - type: loadout id: LoadoutHeadHatGreensoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatGreensoft - type: loadout id: LoadoutHeadHatGreensoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatGreensoftFlipped - type: loadout id: LoadoutHeadHatGreysoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatGreysoft - type: loadout id: LoadoutHeadHatGreysoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatGreysoftFlipped - type: loadout id: LoadoutHeadHatOrangesoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatOrangesoft - type: loadout id: LoadoutHeadHatOrangesoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatOrangesoftFlipped - type: loadout id: LoadoutHeadHatPurplesoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatPurplesoft - type: loadout id: LoadoutHeadHatPurplesoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatPurplesoftFlipped - type: loadout id: LoadoutHeadHatRedsoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatRedsoft - type: loadout id: LoadoutHeadHatRedsoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatRedsoftFlipped - type: loadout id: LoadoutHeadHatYellowsoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatYellowsoft - type: loadout id: LoadoutHeadHatYellowsoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatYellowsoftFlipped + +## Headbands +- type: loadout + id: LoadoutHeadBandBlack + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandBlack + +- type: loadout + id: LoadoutHeadBandBlue + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandBlue + +- type: loadout + id: LoadoutHeadBandGold + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandGold + +- type: loadout + id: LoadoutHeadBandGreen + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandGreen + +- type: loadout + id: LoadoutHeadBandGrey + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandGrey + +- type: loadout + id: LoadoutHeadBandRed + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandRed + +- type: loadout + id: LoadoutHeadBandSkull + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandSkull + +- type: loadout + id: LoadoutHeadBandMerc + category: Head + cost: 2 + exclusive: true + items: + - ClothingHeadBandMerc + +- type: loadout + id: LoadoutHeadBandBrown + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandBrown diff --git a/Resources/Prototypes/Loadouts/items.yml b/Resources/Prototypes/Loadouts/items.yml index 6ce8d260613..072061d2e28 100644 --- a/Resources/Prototypes/Loadouts/items.yml +++ b/Resources/Prototypes/Loadouts/items.yml @@ -1,3 +1,4 @@ +#Smokes - type: loadout id: LoadoutItemCig category: Items @@ -34,11 +35,18 @@ - CigPackBlack - type: loadout - id: LoadoutItemPAI + id: LoadoutItemCigsMixed category: Items cost: 3 items: - - PersonalAI + - CigPackMixed + +- type: loadout + id: LoadoutItemCigsSyndicate + category: Items + cost: 4 + items: + - CigPackSyndicate - type: loadout id: LoadoutItemLighter @@ -61,30 +69,138 @@ items: - Matchbox +## Instruments - type: loadout - id: LoadoutItemPlushieSharkBlue + id: LoadoutItemMicrophoneInstrument category: Items - cost: 2 + cost: 4 items: - - PlushieSharkBlue + - MicrophoneInstrument - type: loadout - id: LoadoutItemPlushieSharkPink + id: LoadoutItemKalimbaInstrument category: Items - cost: 2 + cost: 4 + items: + - KalimbaInstrument + +- type: loadout + id: LoadoutItemTrumpetInstrument + category: Items + cost: 6 + items: + - TrumpetInstrument + +- type: loadout + id: LoadoutItemElectricGuitar + category: Items + cost: 7 + items: + - ElectricGuitarInstrument + +- type: loadout + id: LoadoutItemBassGuitar + category: Items + cost: 7 + items: + - BassGuitarInstrument + +- type: loadout + id: LoadoutItemRockGuitar + category: Items + cost: 7 + items: + - RockGuitarInstrument + +- type: loadout + id: LoadoutItemAcousticGuitar + category: Items + cost: 7 + items: + - AcousticGuitarInstrument + +- type: loadout + id: LoadoutItemViolin + category: Items + cost: 6 + items: + - ViolinInstrument + +- type: loadout + id: LoadoutItemHarmonica + category: Items + cost: 3 + items: + - HarmonicaInstrument + +- type: loadout + id: LoadoutItemAccordion + category: Items + cost: 6 + items: + - AccordionInstrument + +- type: loadout + id: LoadoutItemFlute + category: Items + cost: 4 items: - - PlushieSharkPink + - FluteInstrument - type: loadout - id: LoadoutItemPlushieSharkGrey + id: LoadoutItemOcarina + category: Items + cost: 3 + items: + - OcarinaInstrument + +# Survival Kit +- type: loadout + id: LoadoutItemsEmergencyOxygenTank + category: Items + cost: 1 + items: + - EmergencyOxygenTankFilled + +- type: loadout + id: LoadoutItemsExtendedEmergencyOxygenTank category: Items cost: 2 items: - - PlushieSharkGrey + - ExtendedEmergencyOxygenTankFilled + +- type: loadout + id: LoadoutItemsDoubleEmergencyOxygenTank + category: Items + cost: 4 + items: + - DoubleEmergencyOxygenTankFilled + +- type: loadout + id: LoadoutItemsEmergencyCrowbar + category: Items + cost: 3 + items: + - CrowbarRed +#Misc Items - type: loadout - id: LoadoutItemPlushieCarp + id: LoadoutItemPAI + category: Items + cost: 3 + items: + - PersonalAI + +- type: loadout + id: LoadoutItemWaistbag category: Items cost: 2 items: - - PlushieCarp + - ClothingBeltStorageWaistbag + +- type: loadout + id: LoadoutSolCommonTranslator + category: Items + cost: 3 + items: + - SolCommonTranslator diff --git a/Resources/Prototypes/Loadouts/neck.yml b/Resources/Prototypes/Loadouts/neck.yml index 7e5526f966a..eb933de29ee 100644 --- a/Resources/Prototypes/Loadouts/neck.yml +++ b/Resources/Prototypes/Loadouts/neck.yml @@ -1,27 +1,104 @@ - type: loadout id: LoadoutNeckScarfStripedRed - category: Accessories + category: Neck cost: 1 + exclusive: true items: - ClothingNeckScarfStripedRed - type: loadout id: LoadoutNeckScarfStripedBlue - category: Accessories + category: Neck cost: 1 + exclusive: true items: - ClothingNeckScarfStripedBlue - type: loadout id: LoadoutNeckScarfStripedGreen - category: Accessories + category: Neck cost: 1 + exclusive: true items: - ClothingNeckScarfStripedGreen - type: loadout id: LoadoutNeckScarfStripedZebra - category: Accessories + category: Neck cost: 1 + exclusive: true items: - ClothingNeckScarfStripedZebra + +#Pride Accessories +- type: loadout + id: LoadoutItemsPrideLGBTPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckLGBTPin + +- type: loadout + id: LoadoutItemsPrideAromanticPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckAromanticPin + +- type: loadout + id: LoadoutItemsPrideAsexualPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckAsexualPin + +- type: loadout + id: LoadoutItemsPrideBisexualPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckBisexualPin + +- type: loadout + id: LoadoutItemsPrideIntersexPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckIntersexPin + +- type: loadout + id: LoadoutItemsPrideLesbianPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckLesbianPin + +- type: loadout + id: LoadoutItemsPrideNonBinaryPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckNonBinaryPin + +- type: loadout + id: LoadoutItemsPridePansexualPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckPansexualPin + +- type: loadout + id: LoadoutItemsPrideTransPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckTransPin diff --git a/Resources/Prototypes/Loadouts/outerClothing.yml b/Resources/Prototypes/Loadouts/outerClothing.yml index 078cf530ba2..a5932214ce9 100644 --- a/Resources/Prototypes/Loadouts/outerClothing.yml +++ b/Resources/Prototypes/Loadouts/outerClothing.yml @@ -1,10 +1,3 @@ -- type: loadout - id: LoadoutOuterGhostSheet - category: Outer - cost: 2 - items: - - ClothingOuterGhostSheet - - type: loadout id: LoadoutOuterCoatBomberjacket category: Outer @@ -33,6 +26,72 @@ items: - ClothingOuterWinterCoat +- type: loadout + id: LoadoutOuterCoatHyenhSweater + category: Outer + cost: 3 + items: + - ClothingOuterCoatHyenhSweater + +- type: loadout + id: LoadoutOuterWinterCoatLong + category: Outer + cost: 3 + items: + - ClothingOuterWinterCoatLong + +- type: loadout + id: LoadoutOuterWinterCoatPlaid + category: Outer + cost: 3 + items: + - ClothingOuterWinterCoatPlaid + +- type: loadout + id: LoadoutOuterVestValet + category: Outer + cost: 1 + items: + - ClothingOuterVestValet + +## Letterman Jackets +- type: loadout + id: LoadoutOuterCoatLettermanBlue + category: Outer + cost: 3 + items: + - ClothingOuterCoatLettermanBlue + +- type: loadout + id: LoadoutOuterCoatLettermanRed + category: Outer + cost: 3 + items: + - ClothingOuterCoatLettermanRed + +## MNK +- type: loadout + id: LoadoutOuterCoatMNKWhiteHoodie + category: Outer + cost: 3 + items: + - ClothingOuterCoatMNKWhiteHoodie + +- type: loadout + id: LoadoutOuterCoatMNKBlackTopCoat + category: Outer + cost: 3 + items: + - ClothingOuterCoatMNKBlackTopCoat + +- type: loadout + id: LoadoutOuterCoatMNKBlackJacket + category: Outer + cost: 3 + items: + - ClothingOuterCoatMNKBlackJacket + +## Contractor Jackets - type: loadout id: LoadoutOuterCorporateJacket category: Outer diff --git a/Resources/Prototypes/Loadouts/shoes.yml b/Resources/Prototypes/Loadouts/shoes.yml index 4a1880b5e21..3c2e21e631b 100644 --- a/Resources/Prototypes/Loadouts/shoes.yml +++ b/Resources/Prototypes/Loadouts/shoes.yml @@ -1,7 +1,7 @@ # Colored - type: loadout id: LoadoutShoesBlack - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -9,7 +9,7 @@ - type: loadout id: LoadoutShoesBlue - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -17,7 +17,7 @@ - type: loadout id: LoadoutShoesBrown - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -25,7 +25,7 @@ - type: loadout id: LoadoutShoesGreen - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -33,7 +33,7 @@ - type: loadout id: LoadoutShoesOrange - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -41,7 +41,7 @@ - type: loadout id: LoadoutShoesPurple - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -49,7 +49,7 @@ - type: loadout id: LoadoutShoesRed - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -57,7 +57,7 @@ - type: loadout id: LoadoutShoesWhite - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -65,25 +65,93 @@ - type: loadout id: LoadoutShoesYellow - category: Accessories + category: Shoes cost: 1 exclusive: true items: - ClothingShoesColorYellow +- type: loadout + id: LoadoutShoesGeta + category: Shoes + cost: 1 + exclusive: true + items: + - ClothingShoesGeta + +## Boots +- type: loadout + id: LoadoutShoesBootsWork + category: Shoes + cost: 1 + exclusive: true + items: + - ClothingShoesBootsWork + +- type: loadout + id: LoadoutShoesBootsLaceup + category: Shoes + cost: 2 + exclusive: true + items: + - ClothingShoesBootsLaceup + +- type: loadout + id: LoadoutShoesBootsWinter + category: Shoes + cost: 1 + exclusive: true + items: + - ClothingShoesBootsWinter + +- type: loadout + id: LoadoutShoesBootsCowboyBrown + category: Shoes + cost: 2 + exclusive: true + items: + - ClothingShoesBootsCowboyBrown + +- type: loadout + id: LoadoutShoesBootsCowboyBlack + category: Shoes + cost: 2 + exclusive: true + items: + - ClothingShoesBootsCowboyBlack + +- type: loadout + id: LoadoutShoesBootsCowboyWhite + category: Shoes + cost: 2 + exclusive: true + items: + - ClothingShoesBootsCowboyWhite + +- type: loadout + id: LoadoutShoesBootsCowboyFancy + category: Shoes + cost: 2 + exclusive: true + items: + - ClothingShoesBootsCowboyFancy # Miscellaneous - type: loadout id: LoadoutShoesSlippersDuck - category: Accessories - cost: 1 + category: Shoes + cost: 2 exclusive: true items: - ClothingShoeSlippersDuck + requirements: + - !type:CharacterJobRequirement + jobs: + - Clown - type: loadout id: LoadoutShoesLeather - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -91,7 +159,7 @@ - type: loadout id: LoadoutShoesMiscWhite - category: Accessories + category: Shoes cost: 1 exclusive: true items: diff --git a/Resources/Prototypes/Loadouts/species.yml b/Resources/Prototypes/Loadouts/species.yml new file mode 100644 index 00000000000..1d2fb58dc0a --- /dev/null +++ b/Resources/Prototypes/Loadouts/species.yml @@ -0,0 +1,35 @@ +- type: loadout + id: LoadoutSpeciesEmergencyNitrogenTank + category: Species + cost: 0 + requirements: + - !type:CharacterSpeciesRequirement + species: SlimePerson + - !type:CharacterSpeciesRequirement + species: Vox + items: + - EmergencyNitrogenTankFilled + +- type: loadout + id: LoadoutSpeciesExtendedEmergencyNitrogenTank + category: Species + cost: 1 + requirements: + - !type:CharacterSpeciesRequirement + species: SlimePerson + - !type:CharacterSpeciesRequirement + species: Vox + items: + - ExtendedEmergencyNitrogenTankFilled + +- type: loadout + id: LoadoutSpeciesDoubleEmergencyNitrogenTank + category: Species + cost: 3 + requirements: + - !type:CharacterSpeciesRequirement + species: SlimePerson + - !type:CharacterSpeciesRequirement + species: Vox + items: + - DoubleEmergencyNitrogenTankFilled diff --git a/Resources/Prototypes/Loadouts/uniform.yml b/Resources/Prototypes/Loadouts/uniform.yml index 5afdd303c0b..20c5b42c7c2 100644 --- a/Resources/Prototypes/Loadouts/uniform.yml +++ b/Resources/Prototypes/Loadouts/uniform.yml @@ -19,6 +19,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorBlack + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorBlack @@ -27,6 +31,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorBlack + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorBlue @@ -35,6 +43,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorBlue + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorBlue @@ -43,6 +55,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorBlue + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorGreen @@ -51,6 +67,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorGreen + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorGreen @@ -59,6 +79,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorGreen + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorOrange @@ -67,6 +91,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorOrange + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorOrange @@ -75,6 +103,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorOrange + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorPink @@ -83,6 +115,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorPink + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorPink @@ -91,6 +127,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorPink + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorRed @@ -99,6 +139,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorRed + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorRed @@ -107,6 +151,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorRed + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorWhite @@ -115,6 +163,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorWhite + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorWhite @@ -123,6 +175,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorWhite + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorYellow @@ -131,6 +187,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorYellow + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorYellow @@ -139,6 +199,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorYellow + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorDarkBlue @@ -147,6 +211,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorDarkBlue + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorDarkBlue @@ -155,6 +223,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorDarkBlue + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorTeal @@ -163,6 +235,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorTeal + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorTeal @@ -171,6 +247,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorTeal + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorPurple @@ -179,6 +259,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorPurple + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorPurple @@ -187,6 +271,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorPurple + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorDarkGreen @@ -195,6 +283,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorDarkGreen + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorDarkGreen @@ -203,6 +295,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorDarkGreen + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorLightBrown @@ -211,6 +307,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorLightBrown + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorLightBrown @@ -219,6 +319,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorLightBrown + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorBrown @@ -227,6 +331,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorBrown + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorBrown @@ -235,6 +343,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorBrown + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorMaroon @@ -243,6 +355,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorMaroon + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorMaroon @@ -251,11 +367,131 @@ exclusive: true items: - ClothingUniformJumpskirtColorMaroon + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger + +## Kendo +- type: loadout + id: LoadoutUniformKendoHakama + category: Uniform + cost: 2 + exclusive: true + items: + - ClothingUniformKendoHakama + +- type: loadout + id: LoadoutUniformMartialGi + category: Uniform + cost: 2 + exclusive: true + items: + - ClothingUniformMartialGi + +## Kimono +- type: loadout + id: LoadoutClothingKimonoBlue + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingKimonoBlue + +- type: loadout + id: LoadoutClothingKimonoPink + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingKimonoPink + +- type: loadout + id: LoadoutClothingKimonoPurple + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingKimonoPurple + +- type: loadout + id: LoadoutClothingKimonoSky + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingKimonoSky + +- type: loadout + id: LoadoutClothingKimonoGreen + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingKimonoGreen +## Gakuran - type: loadout - id: LoadoutUniformJumpsuitColorRainbow + id: LoadoutUniformSchoolGakuranBlack category: Uniform cost: 2 exclusive: true items: - - ClothingUniformColorRainbow + - ClothingUniformSchoolGakuranBlack + +## MNK Uniforms +- type: loadout + id: LoadoutClothingMNKOfficeSkirt + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingUniformMNKOfficeSkirt + +- type: loadout + id: LoadoutClothingMNKUnderGarment + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingUniformMNKUnderGarment + +- type: loadout + id: LoadoutClothingMNKGymBra + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingUniformMNKGymBra + +- type: loadout + id: LoadoutClothingMNKDressBlack + category: Uniform + cost: 4 + exclusive: true + items: + - ClothingUniformMNKDressBlack + +- type: loadout + id: LoadoutClothingMNKBlackOveralls + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingUniformMNKBlackOveralls + +- type: loadout + id: LoadoutClothingMNKBlackShoulder + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingUniformMNKBlackShoulder + +- type: loadout + id: LoadoutClothingMNKTracksuitBlack + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingUniformMNKTracksuitBlack From 529c5a4a9fd1fd613f9f1931723300183cd37318 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 22:53:58 +0000 Subject: [PATCH 50/56] Automatic Changelog Update (#425) --- Resources/Changelog/Changelog.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index cc18a7b1e00..68ccf158b46 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4343,3 +4343,11 @@ Entries: configure the frequency on their server manually. id: 6143 time: '2024-07-05T21:03:28.0000000+00:00' +- author: VMSolidus + changes: + - type: Add + message: >- + The catalog of items available via Loadouts has been greatly expanded. + Have fun customizing your characters! + id: 6144 + time: '2024-07-05T22:53:36.0000000+00:00' From 5d3648305a621c393375521469a2ae86688cff2f Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Fri, 5 Jul 2024 19:49:31 -0400 Subject: [PATCH 51/56] Trichromat Modification Trait (#505) # Description This PR brings back a previous briefly existing feature, that of the NormalVisionComponent, which now exists as a single point positive trait that can be taken only by Harpies and Vulpkanin(A list to later be expanded if we ever get new species that have modified vision types). This trait is called "Trichromat Modification", and it simply removes any species based vision modifications an entity may have, such as Ultraviolet Vision. :cl: - add: Trichromat Modification has been added as a new positive trait. It can be taken by Harpies and Vulpkanin to buy off their unique vision negative traits. Signed-off-by: VMSolidus --- .../Components/NormalVisionComponent.cs | 12 ++++++++++ .../Assorted/Systems/NormalVisionSystem.cs | 23 +++++++++++++++++++ Resources/Locale/en-US/traits/traits.ftl | 6 +++++ .../Prototypes/DeltaV/Traits/altvision.yml | 22 ++++++++++++++++++ Resources/Prototypes/Traits/neutral.yml | 20 ++++++++++++++++ 5 files changed, 83 insertions(+) create mode 100644 Content.Shared/Traits/Assorted/Components/NormalVisionComponent.cs create mode 100644 Content.Shared/Traits/Assorted/Systems/NormalVisionSystem.cs diff --git a/Content.Shared/Traits/Assorted/Components/NormalVisionComponent.cs b/Content.Shared/Traits/Assorted/Components/NormalVisionComponent.cs new file mode 100644 index 00000000000..442bb6f0084 --- /dev/null +++ b/Content.Shared/Traits/Assorted/Components/NormalVisionComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Traits.Assorted.Components; + +/// +/// This is used for removing species specific vision traits +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class NormalVisionComponent : Component +{ +} + diff --git a/Content.Shared/Traits/Assorted/Systems/NormalVisionSystem.cs b/Content.Shared/Traits/Assorted/Systems/NormalVisionSystem.cs new file mode 100644 index 00000000000..ee25bf364b9 --- /dev/null +++ b/Content.Shared/Traits/Assorted/Systems/NormalVisionSystem.cs @@ -0,0 +1,23 @@ +using Content.Shared.Abilities; +using Content.Shared.Traits.Assorted.Components; + +namespace Content.Shared.Traits.Assorted.Systems; + +/// +/// This handles removing species-specific vision traits. +/// +public sealed class NormalVisionSystem : EntitySystem +{ + /// + public override void Initialize() + { + SubscribeLocalEvent(OnStartup); + } + + + private void OnStartup(EntityUid uid, NormalVisionComponent component, ComponentInit args) + { + RemComp(uid); + RemComp(uid); + } +} diff --git a/Resources/Locale/en-US/traits/traits.ftl b/Resources/Locale/en-US/traits/traits.ftl index 80680ac0db2..2f59d322282 100644 --- a/Resources/Locale/en-US/traits/traits.ftl +++ b/Resources/Locale/en-US/traits/traits.ftl @@ -32,6 +32,12 @@ trait-description-SocialAnxiety = You are anxious when you speak and stutter. trait-name-Snoring = Snoring trait-description-Snoring = You will snore while sleeping. +trait-name-NormalVisionHarpy = Trichromat Modification +trait-description-NormalVisionHarpy = Your eyes have been modified by means of advanced medicine to see in the standard colors of Red, Green, and Blue. + +trait-name-NormalVisionVulpkanin = Trichromat Modification +trait-description-NormalVisionVulpkanin = Your eyes have been modified by means of advanced medicine to see in the standard colors of Red, Green, and Blue. + trait-name-Thieving = Thieving trait-description-Thieving = You are deft with your hands, and talented at convincing people of their belongings. diff --git a/Resources/Prototypes/DeltaV/Traits/altvision.yml b/Resources/Prototypes/DeltaV/Traits/altvision.yml index d1980bc23ad..1257c1eeb09 100644 --- a/Resources/Prototypes/DeltaV/Traits/altvision.yml +++ b/Resources/Prototypes/DeltaV/Traits/altvision.yml @@ -2,6 +2,17 @@ id: UltraVision category: Visual points: -1 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: Vulpkanin + - !type:CharacterSpeciesRequirement + inverted: true + species: Harpy + - !type:CharacterTraitRequirement + inverted: true + traits: + - DogVision components: - type: UltraVision @@ -9,5 +20,16 @@ id: DogVision category: Visual points: -1 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: Vulpkanin + - !type:CharacterSpeciesRequirement + inverted: true + species: Harpy + - !type:CharacterTraitRequirement + inverted: true + traits: + - UltraVision components: - type: DogVision diff --git a/Resources/Prototypes/Traits/neutral.yml b/Resources/Prototypes/Traits/neutral.yml index 3a3dc943cd7..86e12d20b34 100644 --- a/Resources/Prototypes/Traits/neutral.yml +++ b/Resources/Prototypes/Traits/neutral.yml @@ -21,3 +21,23 @@ - type: MothAccent - type: ReplacementAccent accent: dwarf + +- type: trait + id: NormalVisionHarpy + category: Visual + points: -1 + requirements: + - !type:CharacterSpeciesRequirement + species: Harpy + components: + - type: NormalVision + +- type: trait + id: NormalVisionVulpkanin + category: Visual + points: -1 + requirements: + - !type:CharacterSpeciesRequirement + species: Vulpkanin + components: + - type: NormalVision From 9d5f3b65dfbf93b4c4903e8a3ab3361182bed3f9 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Sat, 6 Jul 2024 02:49:47 +0300 Subject: [PATCH 52/56] Refactor the Language System (#459) # Description The language PR was merged early and OH GOD I ALREADY REGRET IT This PR is intended to provide the missing refactors and address the issues that were missed due to the early merge. --- # TODO - [X] Introduced a polymorphic obfuscation property to the LanguagePrototype - now it supports more than just 2 hardcoded methods, and each method can be configured per-language. Currently there are 3 obfuscation methods: replacement (same as replacement accent), obuscation by syllables and obfuscation by phrases. - [X] Refactored the existing obfuscation methods to not be a big hardcoded mess. - [X] Updated the existing languages accordingly: animalistic languages are now less of an unreadable mess and include less syllables. Certain languages like binary and snake seriously benefit from that. - [X] Refactored the existing commands in response to the never-addressed review (it got lost among hundreds of others) - [X] Refactored the commands to be more user-friendly (you can now use the number of the language in saylang and languageselect which can allow using keybinds to switch between languages) - [X] Moved a lot of obfuscation-related stuff from server to shared. The actual obfuscation process, however, is still done on the server. That may or may not be subject to change, too. - [X] Refactored the entire process of resolution of entities' languages. Instead of raising an event every time it's required to learn what languages an entity knows, the lists of ALL languages available to the entity (including via translators) is stored in LanguageSpeakerComponent and only updated when necessary (e.g. when a translator gets toggled). The list of languages the entity knows on its own is now stored in LanguageKnowledgeComponent. - [X] Made handheld translators automatically change your current language when activated. - [X] Rewrote the translator implanter system, now using the real implants and implanters - [ ] Rebalance science stuff (translators are incredibly expensive for what they're worth) - [ ] Uhhh stuff ---

Media

N/A for now

--- # Changelog :cl: - tweak: Translator implants are now proper implants that can be removed. - tweak: Animalistic languages should now look less messy. - fix: Hopefully fixed language menu desync and other issues. --------- Signed-off-by: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> --- .../Language/LanguageMenuWindow.xaml.cs | 13 +- .../Language/Systems/LanguageSystem.cs | 1 - .../Systems/TranslatorImplanterSystem.cs | 8 - Content.Server/Chat/Systems/ChatSystem.cs | 6 +- .../Chemistry/ReagentEffects/MakeSentient.cs | 23 +- .../Language/Commands/ListLanguagesCommand.cs | 27 +- .../Language/Commands/SayLanguageCommand.cs | 6 +- .../Commands/SelectLanguageCommand.cs | 50 +- .../Language/DetermineEntityLanguagesEvent.cs | 32 +- .../Language/LanguageSystem.Networking.cs | 39 +- Content.Server/Language/LanguageSystem.cs | 325 +++---- .../Language}/LanguagesUpdateEvent.cs | 2 +- .../Language/TranslatorImplantSystem.cs | 66 ++ .../Language/TranslatorImplanterSystem.cs | 72 -- Content.Server/Language/TranslatorSystem.cs | 137 ++- .../Mind/Commands/MakeSentientCommand.cs | 7 +- .../Radio/EntitySystems/HeadsetSystem.cs | 2 +- .../Radio/EntitySystems/RadioSystem.cs | 4 +- .../Components/LanguageKnowledgeComponent.cs | 22 + .../Components/LanguageSpeakerComponent.cs | 33 +- .../Components/TranslatorImplantComponent.cs | 21 + .../TranslatorImplanterComponent.cs | 35 - .../Translators/BaseTranslatorComponent.cs | 9 - .../HandheldTranslatorComponent.cs | 15 +- Content.Shared/Language/LanguagePrototype.cs | 15 +- Content.Shared/Language/ObfuscationMethods.cs | 184 ++++ .../Language/Systems/SharedLanguageSystem.cs | 34 +- .../SharedTranslatorImplanterSystem.cs | 36 - Resources/Locale/en-US/language/commands.ftl | 16 +- .../DeltaV/Entities/Mobs/NPCs/animals.yml | 4 +- .../DeltaV/Entities/Mobs/NPCs/familiars.yml | 2 +- .../DeltaV/Entities/Mobs/NPCs/nukiemouse.yml | 2 +- .../DeltaV/Entities/Mobs/Species/harpy.yml | 2 +- .../Entities/Mobs/Species/vulpkanin.yml | 2 +- .../Mobs/Cyborgs/base_borg_chassis.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/animals.yml | 38 +- .../Entities/Mobs/NPCs/argocyte.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/pets.yml | 20 +- .../Entities/Mobs/NPCs/regalrat.yml | 4 +- .../Prototypes/Entities/Mobs/NPCs/shadows.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/silicon.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/slimes.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/space.yml | 6 +- .../Prototypes/Entities/Mobs/NPCs/xeno.yml | 4 +- .../Prototypes/Entities/Mobs/Species/base.yml | 2 +- .../Entities/Mobs/Species/diona.yml | 2 +- .../Entities/Mobs/Species/dwarf.yml | 2 +- .../Entities/Mobs/Species/human.yml | 2 +- .../Prototypes/Entities/Mobs/Species/moth.yml | 2 +- .../Entities/Mobs/Species/reptilian.yml | 2 +- .../Entities/Mobs/Species/slime.yml | 2 +- Resources/Prototypes/Entities/Mobs/base.yml | 1 + .../Objects/Devices/translator_implants.yml | 168 ++-- .../Entities/Objects/Devices/translators.yml | 21 +- .../Objects/Misc/translator_implanters.yml | 77 ++ .../Structures/Machines/vending_machines.yml | 2 +- Resources/Prototypes/Language/languages.yml | 891 ++++++++++-------- .../Nyanotrasen/Entities/Mobs/Species/Oni.yml | 2 +- .../Entities/Mobs/Species/felinid.yml | 2 +- 59 files changed, 1398 insertions(+), 1114 deletions(-) delete mode 100644 Content.Client/Language/Systems/TranslatorImplanterSystem.cs rename {Content.Shared/Language/Events => Content.Server/Language}/LanguagesUpdateEvent.cs (78%) create mode 100644 Content.Server/Language/TranslatorImplantSystem.cs delete mode 100644 Content.Server/Language/TranslatorImplanterSystem.cs create mode 100644 Content.Shared/Language/Components/LanguageKnowledgeComponent.cs create mode 100644 Content.Shared/Language/Components/TranslatorImplantComponent.cs delete mode 100644 Content.Shared/Language/Components/TranslatorImplanterComponent.cs create mode 100644 Content.Shared/Language/ObfuscationMethods.cs delete mode 100644 Content.Shared/Language/Systems/SharedTranslatorImplanterSystem.cs create mode 100644 Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml diff --git a/Content.Client/Language/LanguageMenuWindow.xaml.cs b/Content.Client/Language/LanguageMenuWindow.xaml.cs index 312814aca35..11d1c290d16 100644 --- a/Content.Client/Language/LanguageMenuWindow.xaml.cs +++ b/Content.Client/Language/LanguageMenuWindow.xaml.cs @@ -1,14 +1,8 @@ using Content.Client.Language.Systems; -using Content.Shared.Language; -using Content.Shared.Language.Systems; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; -using Robust.Shared.Console; -using Robust.Shared.Utility; -using Serilog; -using static Content.Shared.Language.Systems.SharedLanguageSystem; namespace Content.Client.Language; @@ -121,8 +115,11 @@ private void AddLanguageEntry(string language) private void OnLanguageChosen(string id) { var proto = _clientLanguageSystem.GetLanguagePrototype(id); - if (proto != null) - _clientLanguageSystem.RequestSetLanguage(proto); + if (proto == null) + return; + + _clientLanguageSystem.RequestSetLanguage(proto); + UpdateState(id, _clientLanguageSystem.SpokenLanguages); } diff --git a/Content.Client/Language/Systems/LanguageSystem.cs b/Content.Client/Language/Systems/LanguageSystem.cs index 9714078b2c5..5dc2fc1f4e7 100644 --- a/Content.Client/Language/Systems/LanguageSystem.cs +++ b/Content.Client/Language/Systems/LanguageSystem.cs @@ -2,7 +2,6 @@ using Content.Shared.Language.Events; using Content.Shared.Language.Systems; using Robust.Client; -using Robust.Shared.Console; namespace Content.Client.Language.Systems; diff --git a/Content.Client/Language/Systems/TranslatorImplanterSystem.cs b/Content.Client/Language/Systems/TranslatorImplanterSystem.cs deleted file mode 100644 index da19b3decf9..00000000000 --- a/Content.Client/Language/Systems/TranslatorImplanterSystem.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Content.Shared.Language.Systems; - -namespace Content.Client.Language.Systems; - -public sealed class TranslatorImplanterSystem : SharedTranslatorImplanterSystem -{ - -} diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 7eecaa32b43..9b6a7f540be 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -502,7 +502,7 @@ private void SendEntityWhisper( if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full) continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them. - var canUnderstandLanguage = _language.CanUnderstand(listener, language); + var canUnderstandLanguage = _language.CanUnderstand(listener, language.ID); // How the entity perceives the message depends on whether it can understand its language var perceivedMessage = canUnderstandLanguage ? message : languageObfuscatedMessage; @@ -717,7 +717,7 @@ private void SendInVoiceRange(ChatChannel channel, string name, string message, // If the channel does not support languages, or the entity can understand the message, send the original message, otherwise send the obfuscated version - if (channel == ChatChannel.LOOC || channel == ChatChannel.Emotes || _language.CanUnderstand(listener, language)) + if (channel == ChatChannel.LOOC || channel == ChatChannel.Emotes || _language.CanUnderstand(listener, language.ID)) { _chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.Channel, author: author); } @@ -846,7 +846,7 @@ public string WrapPublicMessage(EntityUid source, string name, string message) ("verb", verbName), ("fontType", speech.FontId), ("fontSize", speech.FontSize), - ("message", FormattedMessage.EscapeText(message))); + ("message", message)); } /// diff --git a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs index da16529d515..8d5a583f6d8 100644 --- a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs +++ b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs @@ -1,13 +1,17 @@ using System.Linq; using Content.Server.Ghost.Roles.Components; +using Content.Server.Language; +using Content.Server.Language.Events; using Content.Server.Speech.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.Language; using Content.Shared.Language.Systems; using Content.Shared.Mind.Components; using Robust.Shared.Prototypes; -using Content.Server.Psionics; //Nyano - Summary: pulls in the ability for the sentient creature to become psionic. -using Content.Shared.Humanoid; //Delta-V - Banning humanoids from becoming ghost roles. +using Content.Server.Psionics; +using Content.Shared.Body.Part; //Nyano - Summary: pulls in the ability for the sentient creature to become psionic. +using Content.Shared.Humanoid; +using Content.Shared.Language.Components; //Delta-V - Banning humanoids from becoming ghost roles. using Content.Shared.Language.Events; namespace Content.Server.Chemistry.ReagentEffects; @@ -28,19 +32,18 @@ public override void Effect(ReagentEffectArgs args) entityManager.RemoveComponent(uid); entityManager.RemoveComponent(uid); + // Make sure the entity knows at least fallback (Galactic Common) var speaker = entityManager.EnsureComponent(uid); + var knowledge = entityManager.EnsureComponent(uid); var fallback = SharedLanguageSystem.FallbackLanguagePrototype; - if (!speaker.UnderstoodLanguages.Contains(fallback)) - speaker.UnderstoodLanguages.Add(fallback); + if (!knowledge.UnderstoodLanguages.Contains(fallback)) + knowledge.UnderstoodLanguages.Add(fallback); - if (!speaker.SpokenLanguages.Contains(fallback)) - { - speaker.CurrentLanguage = fallback; - speaker.SpokenLanguages.Add(fallback); - } + if (!knowledge.SpokenLanguages.Contains(fallback)) + knowledge.SpokenLanguages.Add(fallback); - args.EntityManager.EventBus.RaiseLocalEvent(uid, new LanguagesUpdateEvent(), true); + IoCManager.Resolve().GetEntitySystem().UpdateEntityLanguages(uid, speaker); // Stops from adding a ghost role to things like people who already have a mind if (entityManager.TryGetComponent(uid, out var mindContainer) && mindContainer.HasMind) diff --git a/Content.Server/Language/Commands/ListLanguagesCommand.cs b/Content.Server/Language/Commands/ListLanguagesCommand.cs index 6698e1b6453..e5787cba48c 100644 --- a/Content.Server/Language/Commands/ListLanguagesCommand.cs +++ b/Content.Server/Language/Commands/ListLanguagesCommand.cs @@ -1,5 +1,5 @@ -using System.Linq; using Content.Shared.Administration; +using Content.Shared.Language; using Robust.Shared.Console; using Robust.Shared.Enums; @@ -30,10 +30,29 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) } var languages = IoCManager.Resolve().GetEntitySystem(); + var currentLang = languages.GetLanguage(playerEntity).ID; - var (spokenLangs, knownLangs) = languages.GetAllLanguages(playerEntity); + shell.WriteLine(Loc.GetString("command-language-spoken")); + var spoken = languages.GetSpokenLanguages(playerEntity); + for (int i = 0; i < spoken.Count; i++) + { + var lang = spoken[i]; + shell.WriteLine(lang == currentLang + ? Loc.GetString("command-language-current-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang))) + : Loc.GetString("command-language-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang)))); + } - shell.WriteLine("Spoken:\n" + string.Join("\n", spokenLangs)); - shell.WriteLine("Understood:\n" + string.Join("\n", knownLangs)); + shell.WriteLine(Loc.GetString("command-language-understood")); + var understood = languages.GetUnderstoodLanguages(playerEntity); + for (int i = 0; i < understood.Count; i++) + { + var lang = understood[i]; + shell.WriteLine(Loc.GetString("command-language-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang)))); + } + } + + private string LanguageName(string id) + { + return Loc.GetString($"language-{id}-name"); } } diff --git a/Content.Server/Language/Commands/SayLanguageCommand.cs b/Content.Server/Language/Commands/SayLanguageCommand.cs index 2e4a27b1dcc..2304781fa04 100644 --- a/Content.Server/Language/Commands/SayLanguageCommand.cs +++ b/Content.Server/Language/Commands/SayLanguageCommand.cs @@ -32,7 +32,6 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) if (args.Length < 2) return; - var languageId = args[0]; var message = string.Join(" ", args, startIndex: 1, count: args.Length - 1).Trim(); if (string.IsNullOrEmpty(message)) @@ -41,10 +40,9 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) var languages = IoCManager.Resolve().GetEntitySystem(); var chats = IoCManager.Resolve().GetEntitySystem(); - var language = languages.GetLanguagePrototype(languageId); - if (language == null || !languages.CanSpeak(playerEntity, language.ID)) + if (!SelectLanguageCommand.TryParseLanguageArgument(languages, playerEntity, args[0], out var failReason, out var language)) { - shell.WriteError($"Language {languageId} is invalid or you cannot speak it!"); + shell.WriteError(failReason); return; } diff --git a/Content.Server/Language/Commands/SelectLanguageCommand.cs b/Content.Server/Language/Commands/SelectLanguageCommand.cs index e3363846539..d340135925d 100644 --- a/Content.Server/Language/Commands/SelectLanguageCommand.cs +++ b/Content.Server/Language/Commands/SelectLanguageCommand.cs @@ -1,5 +1,7 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.Administration; +using Content.Shared.Language; using Robust.Shared.Console; using Robust.Shared.Enums; @@ -32,17 +34,55 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) if (args.Length < 1) return; - var languageId = args[0]; - var languages = IoCManager.Resolve().GetEntitySystem(); - var language = languages.GetLanguagePrototype(languageId); - if (language == null || !languages.CanSpeak(playerEntity, language.ID)) + if (!TryParseLanguageArgument(languages, playerEntity, args[0], out var failReason, out var language)) { - shell.WriteError($"Language {languageId} is invalid or you cannot speak it!"); + shell.WriteError(failReason); return; } languages.SetLanguage(playerEntity, language.ID); } + + // TODO: find a better place for this method + /// + /// Tries to parse the input argument as either a language ID or the position of the language in the list of languages + /// the entity can speak. Returns true if sucessful. + /// + public static bool TryParseLanguageArgument( + LanguageSystem languageSystem, + EntityUid speaker, + string input, + [NotNullWhen(false)] out string? failureReason, + [NotNullWhen(true)] out LanguagePrototype? language) + { + failureReason = null; + language = null; + + if (int.TryParse(input, out var num)) + { + // The argument is a number + var spoken = languageSystem.GetSpokenLanguages(speaker); + if (num > 0 && num - 1 < spoken.Count) + language = languageSystem.GetLanguagePrototype(spoken[num - 1]); + + if (language != null) // the ability to speak it is implied + return true; + + failureReason = Loc.GetString("command-language-invalid-number", ("total", spoken.Count)); + return false; + } + else + { + // The argument is a language ID + language = languageSystem.GetLanguagePrototype(input); + + if (language != null && languageSystem.CanSpeak(speaker, language.ID)) + return true; + + failureReason = Loc.GetString("command-language-invalid-language", ("id", input)); + return false; + } + } } diff --git a/Content.Server/Language/DetermineEntityLanguagesEvent.cs b/Content.Server/Language/DetermineEntityLanguagesEvent.cs index 13ab2cac279..8d6b868d070 100644 --- a/Content.Server/Language/DetermineEntityLanguagesEvent.cs +++ b/Content.Server/Language/DetermineEntityLanguagesEvent.cs @@ -1,29 +1,25 @@ +using Content.Shared.Language; + namespace Content.Server.Language; /// -/// Raised in order to determine the language an entity speaks at the current moment, -/// as well as the list of all languages the entity may speak and understand. +/// Raised in order to determine the list of languages the entity can speak and understand at the given moment. +/// Typically raised on an entity after a language agent (e.g. a translator) has been added to or removed from them. /// -public sealed class DetermineEntityLanguagesEvent : EntityEventArgs +[ByRefEvent] +public record struct DetermineEntityLanguagesEvent { /// - /// The default language of this entity. If empty, remain unchanged. - /// This field has no effect if the entity decides to speak in a concrete language. - /// - public string CurrentLanguage; - /// - /// The list of all languages the entity may speak. Must NOT be held as a reference! + /// The list of all languages the entity may speak. + /// By default, contains the languages this entity speaks intrinsically. /// - public List SpokenLanguages; + public HashSet SpokenLanguages = new(); + /// - /// The list of all languages the entity may understand. Must NOT be held as a reference! + /// The list of all languages the entity may understand. + /// By default, contains the languages this entity understands intrinsically. /// - public List UnderstoodLanguages; + public HashSet UnderstoodLanguages = new(); - public DetermineEntityLanguagesEvent(string currentLanguage, List spokenLanguages, List understoodLanguages) - { - CurrentLanguage = currentLanguage; - SpokenLanguages = spokenLanguages; - UnderstoodLanguages = understoodLanguages; - } + public DetermineEntityLanguagesEvent() {} } diff --git a/Content.Server/Language/LanguageSystem.Networking.cs b/Content.Server/Language/LanguageSystem.Networking.cs index 7517b4185e3..572e2961fde 100644 --- a/Content.Server/Language/LanguageSystem.Networking.cs +++ b/Content.Server/Language/LanguageSystem.Networking.cs @@ -1,5 +1,7 @@ +using Content.Server.Language.Events; using Content.Server.Mind; using Content.Shared.Language; +using Content.Shared.Language.Components; using Content.Shared.Language.Events; using Content.Shared.Mind; using Content.Shared.Mind.Components; @@ -7,11 +9,6 @@ namespace Content.Server.Language; -/// -/// LanguageSystem Networking -/// This is used to update client state when mind change entity. -/// - public sealed partial class LanguageSystem { [Dependency] private readonly MindSystem _mind = default!; @@ -19,6 +16,11 @@ public sealed partial class LanguageSystem public void InitializeNet() { + SubscribeNetworkEvent(OnClientSetLanguage); + SubscribeNetworkEvent((_, session) => SendLanguageStateToClient(session.SenderSession)); + + SubscribeLocalEvent((uid, comp, _) => SendLanguageStateToClient(uid, comp)); + // Refresh the client's state when its mind hops to a different entity SubscribeLocalEvent((uid, _, _) => SendLanguageStateToClient(uid)); SubscribeLocalEvent((_, _, args) => @@ -26,12 +28,21 @@ public void InitializeNet() if (args.Mind.Comp.Session != null) SendLanguageStateToClient(args.Mind.Comp.Session); }); - - SubscribeLocalEvent((uid, comp, _) => SendLanguageStateToClient(uid, comp)); - SubscribeNetworkEvent((_, session) => SendLanguageStateToClient(session.SenderSession)); } + private void OnClientSetLanguage(LanguagesSetMessage message, EntitySessionEventArgs args) + { + if (args.SenderSession.AttachedEntity is not { Valid: true } uid) + return; + + var language = GetLanguagePrototype(message.CurrentLanguage); + if (language == null || !CanSpeak(uid, language.ID)) + return; + + SetLanguage(uid, language.ID); + } + private void SendLanguageStateToClient(EntityUid uid, LanguageSpeakerComponent? comp = null) { // Try to find a mind inside the entity and notify its session @@ -50,10 +61,18 @@ private void SendLanguageStateToClient(ICommonSession session, LanguageSpeakerCo SendLanguageStateToClient(entity, session, comp); } + // TODO this is really stupid and can be avoided if we just make everything shared... private void SendLanguageStateToClient(EntityUid uid, ICommonSession session, LanguageSpeakerComponent? component = null) { - var langs = GetLanguages(uid, component); - var message = new LanguagesUpdatedMessage(langs.CurrentLanguage, langs.SpokenLanguages, langs.UnderstoodLanguages); + var isUniversal = HasComp(uid); + if (!isUniversal) + Resolve(uid, ref component, logMissing: false); + + // I really don't want to call 3 getter methods here, so we'll just have this slightly hardcoded solution + var message = isUniversal || component == null + ? new LanguagesUpdatedMessage(UniversalPrototype, [UniversalPrototype], [UniversalPrototype]) + : new LanguagesUpdatedMessage(component.CurrentLanguage, component.SpokenLanguages, component.UnderstoodLanguages); + RaiseNetworkEvent(message, session); } } diff --git a/Content.Server/Language/LanguageSystem.cs b/Content.Server/Language/LanguageSystem.cs index f1bf44c1f4f..e68489e9e28 100644 --- a/Content.Server/Language/LanguageSystem.cs +++ b/Content.Server/Language/LanguageSystem.cs @@ -1,289 +1,212 @@ using System.Linq; -using System.Text; -using Content.Server.GameTicking.Events; +using Content.Server.Language.Events; using Content.Shared.Language; +using Content.Shared.Language.Components; using Content.Shared.Language.Events; using Content.Shared.Language.Systems; -using Robust.Shared.Random; using UniversalLanguageSpeakerComponent = Content.Shared.Language.Components.UniversalLanguageSpeakerComponent; namespace Content.Server.Language; public sealed partial class LanguageSystem : SharedLanguageSystem { - // Static and re-used event instances used to minimize memory allocations during language processing, which can happen many times per tick. - // These are used in the method GetLanguages and returned from it. They should never be mutated outside of that method or returned outside this system. - private readonly DetermineEntityLanguagesEvent - _determineLanguagesEvent = new(string.Empty, new(), new()), - _universalLanguagesEvent = new(UniversalPrototype, [UniversalPrototype], [UniversalPrototype]); // Returned for universal speakers only - - /// - /// A random number added to each pseudo-random number's seed. Changes every round. - /// - public int RandomRoundSeed { get; private set; } - - public override void Initialize() { base.Initialize(); + InitializeNet(); - SubscribeNetworkEvent(OnClientSetLanguage); SubscribeLocalEvent(OnInitLanguageSpeaker); - SubscribeLocalEvent(_ => RandomRoundSeed = _random.Next()); - - InitializeNet(); } #region public api - /// - /// Obfuscate a message using an entity's default language. - /// - public string ObfuscateSpeech(EntityUid source, string message) - { - var language = GetLanguage(source) ?? Universal; - return ObfuscateSpeech(message, language); - } - /// - /// Obfuscate a message using the given language. - /// - public string ObfuscateSpeech(string message, LanguagePrototype language) + public bool CanUnderstand(EntityUid listener, string language, LanguageSpeakerComponent? component = null) { - var builder = new StringBuilder(); - if (language.ObfuscateSyllables) - ObfuscateSyllables(builder, message, language); - else - ObfuscatePhrases(builder, message, language); + if (language == UniversalPrototype || HasComp(listener)) + return true; - return builder.ToString(); + if (!Resolve(listener, ref component, logMissing: false)) + return false; + + return component.UnderstoodLanguages.Contains(language); } - public bool CanUnderstand(EntityUid listener, LanguagePrototype language, LanguageSpeakerComponent? listenerLanguageComp = null) + public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponent? component = null) { - if (language.ID == UniversalPrototype || HasComp(listener)) + if (HasComp(speaker)) return true; - var listenerLanguages = GetLanguages(listener, listenerLanguageComp)?.UnderstoodLanguages; + if (!Resolve(speaker, ref component, logMissing: false)) + return false; - return listenerLanguages?.Contains(language.ID, StringComparer.Ordinal) ?? false; + return component.SpokenLanguages.Contains(language); } - public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponent? speakerComp = null) + /// + /// Returns the current language of the given entity, assumes Universal if it's not a language speaker. + /// + public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? component = null) { - if (HasComp(speaker)) - return true; + if (HasComp(speaker) || !Resolve(speaker, ref component, logMissing: false)) + return Universal; // Serves both as a fallback and uhhh something (TODO: fix this comment) + + if (string.IsNullOrEmpty(component.CurrentLanguage) || !_prototype.TryIndex(component.CurrentLanguage, out var proto)) + return Universal; - var langs = GetLanguages(speaker, speakerComp)?.UnderstoodLanguages; - return langs?.Contains(language, StringComparer.Ordinal) ?? false; + return proto; } /// - /// Returns the current language of the given entity. - /// Assumes Universal if not specified. + /// Returns the list of languages this entity can speak. /// - public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? languageComp = null) + /// Typically, checking is sufficient. + public List GetSpokenLanguages(EntityUid uid) { - var id = GetLanguages(speaker, languageComp)?.CurrentLanguage; - if (id == null) - return Universal; // Fallback + if (HasComp(uid)) + return [UniversalPrototype]; - _prototype.TryIndex(id, out LanguagePrototype? proto); + if (TryComp(uid, out var component)) + return component.SpokenLanguages; - return proto ?? Universal; + return []; } - public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerComponent? languageComp = null) + /// + /// Returns the list of languages this entity can understand. + /// + /// Typically, checking is sufficient. + public List GetUnderstoodLanguages(EntityUid uid) { - if (!CanSpeak(speaker, language) || HasComp(speaker)) - return; + if (HasComp(uid)) + return [UniversalPrototype]; // This one is tricky because... well, they understand all of them, not just one. - if (languageComp == null && !TryComp(speaker, out languageComp)) - return; + if (TryComp(uid, out var component)) + return component.UnderstoodLanguages; + + return []; + } - if (languageComp.CurrentLanguage == language) + public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerComponent? component = null) + { + if (!CanSpeak(speaker, language) || (HasComp(speaker) && language != UniversalPrototype)) return; - languageComp.CurrentLanguage = language; + if (!Resolve(speaker, ref component) || component.CurrentLanguage == language) + return; + component.CurrentLanguage = language; RaiseLocalEvent(speaker, new LanguagesUpdateEvent(), true); } /// - /// Adds a new language to the lists of understood and/or spoken languages of the given component. + /// Adds a new language to the respective lists of intrinsically known languages of the given entity. /// - public void AddLanguage(LanguageSpeakerComponent comp, string language, bool addSpoken = true, bool addUnderstood = true) + public void AddLanguage( + EntityUid uid, + string language, + bool addSpoken = true, + bool addUnderstood = true, + LanguageKnowledgeComponent? knowledge = null, + LanguageSpeakerComponent? speaker = null) { - if (addSpoken && !comp.SpokenLanguages.Contains(language)) - comp.SpokenLanguages.Add(language); + if (knowledge == null) + knowledge = EnsureComp(uid); - if (addUnderstood && !comp.UnderstoodLanguages.Contains(language)) - comp.UnderstoodLanguages.Add(language); + if (addSpoken && !knowledge.SpokenLanguages.Contains(language)) + knowledge.SpokenLanguages.Add(language); - RaiseLocalEvent(comp.Owner, new LanguagesUpdateEvent(), true); - } + if (addUnderstood && !knowledge.UnderstoodLanguages.Contains(language)) + knowledge.UnderstoodLanguages.Add(language); - public (List spoken, List understood) GetAllLanguages(EntityUid speaker) - { - var languages = GetLanguages(speaker); - // The lists need to be copied because the internal ones are re-used for performance reasons. - return (new List(languages.SpokenLanguages), new List(languages.UnderstoodLanguages)); + UpdateEntityLanguages(uid, speaker); } /// - /// Ensures the given entity has a valid language as its current language. - /// If not, sets it to the first entry of its SpokenLanguages list, or universal if it's empty. + /// Removes a language from the respective lists of intrinsically known languages of the given entity. /// - public void EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp = null) + public void RemoveLanguage( + EntityUid uid, + string language, + bool removeSpoken = true, + bool removeUnderstood = true, + LanguageKnowledgeComponent? knowledge = null, + LanguageSpeakerComponent? speaker = null) { - if (comp == null && !TryComp(entity, out comp)) - return; + if (knowledge == null) + knowledge = EnsureComp(uid); - var langs = GetLanguages(entity, comp); - if (!langs.SpokenLanguages.Contains(comp!.CurrentLanguage, StringComparer.Ordinal)) - { - comp.CurrentLanguage = langs.SpokenLanguages.FirstOrDefault(UniversalPrototype); - RaiseLocalEvent(comp.Owner, new LanguagesUpdateEvent(), true); - } - } - #endregion + if (removeSpoken) + knowledge.SpokenLanguages.Remove(language); - #region event handling - private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent component, ComponentInit args) - { - if (string.IsNullOrEmpty(component.CurrentLanguage)) - component.CurrentLanguage = component.SpokenLanguages.FirstOrDefault(UniversalPrototype); - } - #endregion + if (removeUnderstood) + knowledge.UnderstoodLanguages.Remove(language); - #region internal api - obfuscation - private void ObfuscateSyllables(StringBuilder builder, string message, LanguagePrototype language) - { - // Go through each word. Calculate its hash sum and count the number of letters. - // Replicate it with pseudo-random syllables of pseudo-random (but similar) length. Use the hash code as the seed. - // This means that identical words will be obfuscated identically. Simple words like "hello" or "yes" in different langs can be memorized. - var wordBeginIndex = 0; - var hashCode = 0; - for (var i = 0; i < message.Length; i++) - { - var ch = char.ToLower(message[i]); - // A word ends when one of the following is found: a space, a sentence end, or EOM - if (char.IsWhiteSpace(ch) || IsSentenceEnd(ch) || i == message.Length - 1) - { - var wordLength = i - wordBeginIndex; - if (wordLength > 0) - { - var newWordLength = PseudoRandomNumber(hashCode, 1, 4); - - for (var j = 0; j < newWordLength; j++) - { - var index = PseudoRandomNumber(hashCode + j, 0, language.Replacement.Count); - builder.Append(language.Replacement[index]); - } - } - - builder.Append(ch); - hashCode = 0; - wordBeginIndex = i + 1; - } - else - hashCode = hashCode * 31 + ch; - } + UpdateEntityLanguages(uid, speaker); } - private void ObfuscatePhrases(StringBuilder builder, string message, LanguagePrototype language) + /// + /// Ensures the given entity has a valid language as its current language. + /// If not, sets it to the first entry of its SpokenLanguages list, or universal if it's empty. + /// + /// True if the current language was modified, false otherwise. + public bool EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp = null) { - // In a similar manner, each phrase is obfuscated with a random number of conjoined obfuscation phrases. - // However, the number of phrases depends on the number of characters in the original phrase. - var sentenceBeginIndex = 0; - for (var i = 0; i < message.Length; i++) + if (!Resolve(entity, ref comp)) + return false; + + if (!comp.SpokenLanguages.Contains(comp.CurrentLanguage)) { - var ch = char.ToLower(message[i]); - if (IsSentenceEnd(ch) || i == message.Length - 1) - { - var length = i - sentenceBeginIndex; - if (length > 0) - { - var newLength = (int) Math.Clamp(Math.Cbrt(length) - 1, 1, 4); // 27+ chars for 2 phrases, 64+ for 3, 125+ for 4. - - for (var j = 0; j < newLength; j++) - { - var phrase = _random.Pick(language.Replacement); - builder.Append(phrase); - } - } - sentenceBeginIndex = i + 1; - - if (IsSentenceEnd(ch)) - builder.Append(ch).Append(" "); - } + comp.CurrentLanguage = comp.SpokenLanguages.FirstOrDefault(UniversalPrototype); + RaiseLocalEvent(entity, new LanguagesUpdateEvent()); + return true; } - } - private static bool IsSentenceEnd(char ch) - { - return ch is '.' or '!' or '?'; + return false; } - #endregion - #region internal api - misc /// - /// Dynamically resolves the current language of the entity and the list of all languages it speaks. - /// - /// If the entity is not a language speaker, or is a universal language speaker, then it's assumed to speak Universal, - /// aka all languages at once and none at the same time. + /// Immediately refreshes the cached lists of spoken and understood languages for the given entity. /// - /// - /// The returned event is reused and thus must not be held as a reference anywhere but inside the caller function. - /// - private DetermineEntityLanguagesEvent GetLanguages(EntityUid speaker, LanguageSpeakerComponent? comp = null) + public void UpdateEntityLanguages(EntityUid entity, LanguageSpeakerComponent? languages = null) { - // This is a shortcut for ghosts and entities that should not speak normally (admemes) - if (HasComp(speaker) || !TryComp(speaker, out comp)) - return _universalLanguagesEvent; + if (!Resolve(entity, ref languages)) + return; - var ev = _determineLanguagesEvent; - ev.SpokenLanguages.Clear(); - ev.UnderstoodLanguages.Clear(); + var ev = new DetermineEntityLanguagesEvent(); + // We add the intrinsically known languages first so other systems can manipulate them easily + if (TryComp(entity, out var knowledge)) + { + foreach (var spoken in knowledge.SpokenLanguages) + ev.SpokenLanguages.Add(spoken); - ev.CurrentLanguage = comp.CurrentLanguage; - ev.SpokenLanguages.AddRange(comp.SpokenLanguages); - ev.UnderstoodLanguages.AddRange(comp.UnderstoodLanguages); + foreach (var understood in knowledge.UnderstoodLanguages) + ev.UnderstoodLanguages.Add(understood); + } - RaiseLocalEvent(speaker, ev, true); + RaiseLocalEvent(entity, ref ev); - if (ev.CurrentLanguage.Length == 0) - ev.CurrentLanguage = !string.IsNullOrEmpty(comp.CurrentLanguage) ? comp.CurrentLanguage : UniversalPrototype; // Fall back to account for admemes like admins possessing a bread - return ev; - } + languages.SpokenLanguages.Clear(); + languages.UnderstoodLanguages.Clear(); - /// - /// Generates a stable pseudo-random number in the range (min, max) for the given seed. - /// Each input seed corresponds to exactly one random number. - /// - private int PseudoRandomNumber(int seed, int min, int max) - { - // This is not a uniform distribution, but it shouldn't matter given there's 2^31 possible random numbers, - // the bias of this function should be so tiny it will never be noticed. - seed += RandomRoundSeed; - var random = ((seed * 1103515245) + 12345) & 0x7fffffff; // Source: http://cs.uccs.edu/~cs591/bufferOverflow/glibc-2.2.4/stdlib/random_r.c - return random % (max - min) + min; + languages.SpokenLanguages.AddRange(ev.SpokenLanguages); + languages.UnderstoodLanguages.AddRange(ev.UnderstoodLanguages); + + if (!EnsureValidLanguage(entity)) + RaiseLocalEvent(entity, new LanguagesUpdateEvent()); } - /// - /// Set CurrentLanguage of the client, the client must be able to Understand the language requested. - /// - private void OnClientSetLanguage(LanguagesSetMessage message, EntitySessionEventArgs args) - { - if (args.SenderSession.AttachedEntity is not {Valid: true} speaker) - return; + #endregion - var language = GetLanguagePrototype(message.CurrentLanguage); + #region event handling - if (language == null || !CanSpeak(speaker, language.ID)) - return; + private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent component, ComponentInit args) + { + if (string.IsNullOrEmpty(component.CurrentLanguage)) + component.CurrentLanguage = component.SpokenLanguages.FirstOrDefault(UniversalPrototype); - SetLanguage(speaker, language.ID); + UpdateEntityLanguages(uid, component); } + #endregion } diff --git a/Content.Shared/Language/Events/LanguagesUpdateEvent.cs b/Content.Server/Language/LanguagesUpdateEvent.cs similarity index 78% rename from Content.Shared/Language/Events/LanguagesUpdateEvent.cs rename to Content.Server/Language/LanguagesUpdateEvent.cs index 90ce2f4446b..88ea09916bb 100644 --- a/Content.Shared/Language/Events/LanguagesUpdateEvent.cs +++ b/Content.Server/Language/LanguagesUpdateEvent.cs @@ -1,4 +1,4 @@ -namespace Content.Shared.Language.Events; +namespace Content.Server.Language.Events; /// /// Raised on an entity when its list of languages changes. diff --git a/Content.Server/Language/TranslatorImplantSystem.cs b/Content.Server/Language/TranslatorImplantSystem.cs new file mode 100644 index 00000000000..4d58144481d --- /dev/null +++ b/Content.Server/Language/TranslatorImplantSystem.cs @@ -0,0 +1,66 @@ +using Content.Shared.Implants.Components; +using Content.Shared.Language; +using Content.Shared.Language.Components; +using Robust.Shared.Containers; + +namespace Content.Server.Language; + +public sealed class TranslatorImplantSystem : EntitySystem +{ + [Dependency] private readonly LanguageSystem _language = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnImplant); + SubscribeLocalEvent(OnDeImplant); + SubscribeLocalEvent(OnDetermineLanguages); + } + + private void OnImplant(EntityUid uid, TranslatorImplantComponent component, EntGotInsertedIntoContainerMessage args) + { + if (args.Container.ID != ImplanterComponent.ImplantSlotId) + return; + + var implantee = Transform(uid).ParentUid; + if (implantee is not { Valid: true } || !TryComp(implantee, out var knowledge)) + return; + + component.Enabled = true; + // To operate an implant, you need to know its required language intrinsically, because like... it connects to your brain or something. + // So external translators or other implants can't help you operate it. + component.SpokenRequirementSatisfied = TranslatorSystem.CheckLanguagesMatch( + component.RequiredLanguages, knowledge.SpokenLanguages, component.RequiresAllLanguages); + + component.UnderstoodRequirementSatisfied = TranslatorSystem.CheckLanguagesMatch( + component.RequiredLanguages, knowledge.UnderstoodLanguages, component.RequiresAllLanguages); + + _language.UpdateEntityLanguages(implantee); + } + + private void OnDeImplant(EntityUid uid, TranslatorImplantComponent component, EntGotRemovedFromContainerMessage args) + { + // Even though the description of this event says it gets raised BEFORE reparenting, that's actually false... + component.Enabled = component.SpokenRequirementSatisfied = component.UnderstoodRequirementSatisfied = false; + + if (TryComp(uid, out var subdermal) && subdermal.ImplantedEntity is { Valid: true} implantee) + _language.UpdateEntityLanguages(implantee); + } + + private void OnDetermineLanguages(EntityUid uid, ImplantedComponent component, ref DetermineEntityLanguagesEvent args) + { + // TODO: might wanna find a better solution, i just can't come up with something viable + foreach (var implant in component.ImplantContainer.ContainedEntities) + { + if (!TryComp(implant, out var translator) || !translator.Enabled) + continue; + + if (translator.SpokenRequirementSatisfied) + foreach (var language in translator.SpokenLanguages) + args.SpokenLanguages.Add(language); + + if (translator.UnderstoodRequirementSatisfied) + foreach (var language in translator.UnderstoodLanguages) + args.UnderstoodLanguages.Add(language); + } + } +} diff --git a/Content.Server/Language/TranslatorImplanterSystem.cs b/Content.Server/Language/TranslatorImplanterSystem.cs deleted file mode 100644 index 1e0c13375e4..00000000000 --- a/Content.Server/Language/TranslatorImplanterSystem.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Linq; -using Content.Server.Administration.Logs; -using Content.Server.Popups; -using Content.Shared.Database; -using Content.Shared.Interaction; -using Content.Shared.Language; -using Content.Shared.Language.Components; -using Content.Shared.Language.Events; -using Content.Shared.Language.Systems; -using Content.Shared.Mobs.Components; -using Content.Shared.Language.Components.Translators; - -namespace Content.Server.Language; - -public sealed class TranslatorImplanterSystem : SharedTranslatorImplanterSystem -{ - [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly LanguageSystem _language = default!; - - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnImplant); - } - - - private void OnImplant(EntityUid implanter, TranslatorImplanterComponent component, AfterInteractEvent args) - { - if (component.Used || !args.CanReach || args.Target is not { Valid: true } target) - return; - - if (!TryComp(target, out var speaker)) - return; - - if (component.MobsOnly && !HasComp(target)) - { - _popup.PopupEntity("translator-implanter-refuse", component.Owner); - return; - } - - var understood = _language.GetAllLanguages(target).understood; - if (component.RequiredLanguages.Count > 0 && !component.RequiredLanguages.Any(lang => understood.Contains(lang))) - { - _popup.PopupEntity(Loc.GetString("translator-implanter-refuse", - ("implanter", implanter), ("target", target)), implanter); - return; - } - - var intrinsic = EnsureComp(target); - intrinsic.Enabled = true; - - foreach (var lang in component.SpokenLanguages.Where(lang => !intrinsic.SpokenLanguages.Contains(lang))) - intrinsic.SpokenLanguages.Add(lang); - - foreach (var lang in component.UnderstoodLanguages.Where(lang => !intrinsic.UnderstoodLanguages.Contains(lang))) - intrinsic.UnderstoodLanguages.Add(lang); - - component.Used = true; - _popup.PopupEntity(Loc.GetString("translator-implanter-success", - ("implanter", implanter), ("target", target)), implanter); - - _adminLogger.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(args.User):player} used {ToPrettyString(implanter):implanter} to give {ToPrettyString(target):target} the following languages:" - + $"\nSpoken: {string.Join(", ", component.SpokenLanguages)}; Understood: {string.Join(", ", component.UnderstoodLanguages)}"); - - OnAppearanceChange(implanter, component); - RaiseLocalEvent(target, new LanguagesUpdateEvent(), true); - } -} diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs index 3b7704b9a71..5022e540960 100644 --- a/Content.Server/Language/TranslatorSystem.cs +++ b/Content.Server/Language/TranslatorSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.Language.Events; using Content.Server.Popups; using Content.Server.PowerCell; using Content.Shared.Interaction; @@ -8,6 +9,7 @@ using Content.Shared.Language.Systems; using Content.Shared.PowerCell; using Content.Shared.Language.Components.Translators; +using Robust.Shared.Utility; namespace Content.Server.Language; @@ -23,7 +25,6 @@ public override void Initialize() { base.Initialize(); - // I wanna die. But my death won't help us discover polymorphism. SubscribeLocalEvent(OnDetermineLanguages); SubscribeLocalEvent(OnDetermineLanguages); SubscribeLocalEvent(OnDetermineLanguages); @@ -31,67 +32,36 @@ public override void Initialize() SubscribeLocalEvent(OnTranslatorToggle); SubscribeLocalEvent(OnPowerCellSlotEmpty); - // TODO: why does this use InteractHandEvent?? SubscribeLocalEvent(OnTranslatorInteract); SubscribeLocalEvent(OnTranslatorDropped); } - private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent component, - DetermineEntityLanguagesEvent ev) + private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent component, DetermineEntityLanguagesEvent ev) { - if (!component.Enabled) + if (!component.Enabled || !TryComp(uid, out var speaker)) return; if (!_powerCell.HasActivatableCharge(uid)) return; - var addUnderstood = true; - var addSpoken = true; - if (component.RequiredLanguages.Count > 0) - { - if (component.RequiresAllLanguages) - { - // Add langs when the wielder has all of the required languages - foreach (var language in component.RequiredLanguages) - { - if (!ev.SpokenLanguages.Contains(language, StringComparer.Ordinal)) - addSpoken = false; - - if (!ev.UnderstoodLanguages.Contains(language, StringComparer.Ordinal)) - addUnderstood = false; - } - } - else - { - // Add langs when the wielder has at least one of the required languages - addUnderstood = false; - addSpoken = false; - foreach (var language in component.RequiredLanguages) - { - if (ev.SpokenLanguages.Contains(language, StringComparer.Ordinal)) - addSpoken = true; - - if (ev.UnderstoodLanguages.Contains(language, StringComparer.Ordinal)) - addUnderstood = true; - } - } - } + // The idea here is as follows: + // Required languages are languages that are required to operate the translator. + // The translator has a limited number of languages it can translate to and translate from. + // If the wielder understands the language of the translator, they will be able to understand translations provided by it + // If the wielder also speaks that language, they will be able to use it to translate their own speech by "speaking" in that language + var addSpoken = CheckLanguagesMatch(component.RequiredLanguages, speaker.SpokenLanguages, component.RequiresAllLanguages); + var addUnderstood = CheckLanguagesMatch(component.RequiredLanguages, speaker.UnderstoodLanguages, component.RequiresAllLanguages); if (addSpoken) - { foreach (var language in component.SpokenLanguages) - AddIfNotExists(ev.SpokenLanguages, language); - - if (component.DefaultLanguageOverride != null && ev.CurrentLanguage.Length == 0) - ev.CurrentLanguage = component.DefaultLanguageOverride; - } + ev.SpokenLanguages.Add(language); if (addUnderstood) foreach (var language in component.UnderstoodLanguages) - AddIfNotExists(ev.UnderstoodLanguages, language); + ev.UnderstoodLanguages.Add(language); } - private void OnTranslatorInteract( EntityUid translator, HandheldTranslatorComponent component, InteractHandEvent args) + private void OnTranslatorInteract(EntityUid translator, HandheldTranslatorComponent component, InteractHandEvent args) { var holder = args.User; if (!EntityManager.HasComponent(holder)) @@ -100,7 +70,7 @@ private void OnTranslatorInteract( EntityUid translator, HandheldTranslatorCompo var intrinsic = EnsureComp(holder); UpdateBoundIntrinsicComp(component, intrinsic, component.Enabled); - RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); + _language.UpdateEntityLanguages(holder); } private void OnTranslatorDropped(EntityUid translator, HandheldTranslatorComponent component, DroppedEvent args) @@ -115,59 +85,63 @@ private void OnTranslatorDropped(EntityUid translator, HandheldTranslatorCompone RemCompDeferred(holder, intrinsic); } - _language.EnsureValidLanguage(holder); - - RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); + _language.UpdateEntityLanguages(holder); } - private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponent component, ActivateInWorldEvent args) + private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponent translatorComp, ActivateInWorldEvent args) { - if (!component.ToggleOnInteract) + if (!translatorComp.ToggleOnInteract) return; + // This will show a popup if false var hasPower = _powerCell.HasDrawCharge(translator); if (Transform(args.Target).ParentUid is { Valid: true } holder - && EntityManager.HasComponent(holder)) + && TryComp(holder, out var languageComp)) { // This translator is held by a language speaker and thus has an intrinsic counterpart bound to it. // Make sure it's up-to-date. var intrinsic = EnsureComp(holder); - var isEnabled = !component.Enabled; - if (intrinsic.Issuer != component) + var isEnabled = !translatorComp.Enabled; + if (intrinsic.Issuer != translatorComp) { - // The intrinsic comp wasn't owned by this handheld component, so this comp wasn't the active translator. - // Thus it needs to be turned on regardless of its previous state. - intrinsic.Issuer = component; + // The intrinsic comp wasn't owned by this handheld translator, so this wasn't the active translator. + // Thus, the intrinsic comp needs to be turned on regardless of its previous state. + intrinsic.Issuer = translatorComp; isEnabled = true; } - isEnabled &= hasPower; - UpdateBoundIntrinsicComp(component, intrinsic, isEnabled); - component.Enabled = isEnabled; + + UpdateBoundIntrinsicComp(translatorComp, intrinsic, isEnabled); + translatorComp.Enabled = isEnabled; _powerCell.SetPowerCellDrawEnabled(translator, isEnabled); - _language.EnsureValidLanguage(holder); - RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); + // The first new spoken language added by this translator, or null + var firstNewLanguage = translatorComp.SpokenLanguages.FirstOrDefault(it => !languageComp.SpokenLanguages.Contains(it)); + + _language.UpdateEntityLanguages(holder, languageComp); + + // Update the current language of the entity if necessary + if (isEnabled && translatorComp.SetLanguageOnInteract && firstNewLanguage is {}) + _language.SetLanguage(holder, firstNewLanguage, languageComp); } else { // This is a standalone translator (e.g. lying on the ground), toggle its state. - component.Enabled = !component.Enabled && hasPower; - _powerCell.SetPowerCellDrawEnabled(translator, !component.Enabled && hasPower); + translatorComp.Enabled = !translatorComp.Enabled && hasPower; + _powerCell.SetPowerCellDrawEnabled(translator, !translatorComp.Enabled && hasPower); } - OnAppearanceChange(translator, component); + OnAppearanceChange(translator, translatorComp); - // HasPower shows a popup when there's no power, so we do not proceed in that case if (hasPower) { var message = Loc.GetString( - component.Enabled + translatorComp.Enabled ? "translator-component-turnon" : "translator-component-shutoff", - ("translator", component.Owner)); - _popup.PopupEntity(message, component.Owner, args.User); + ("translator", translatorComp.Owner)); + _popup.PopupEntity(message, translatorComp.Owner, args.User); } } @@ -178,7 +152,7 @@ private void OnPowerCellSlotEmpty(EntityUid translator, HandheldTranslatorCompon OnAppearanceChange(translator, component); if (Transform(translator).ParentUid is { Valid: true } holder - && EntityManager.HasComponent(holder)) + && TryComp(holder, out var languageComp)) { if (!EntityManager.TryGetComponent(holder, out var intrinsic)) return; @@ -186,11 +160,10 @@ private void OnPowerCellSlotEmpty(EntityUid translator, HandheldTranslatorCompon if (intrinsic.Issuer == component) { intrinsic.Enabled = false; - EntityManager.RemoveComponent(holder, intrinsic); + RemComp(holder, intrinsic); } - _language.EnsureValidLanguage(holder); - RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); + _language.UpdateEntityLanguages(holder, languageComp); } } @@ -201,25 +174,29 @@ private void UpdateBoundIntrinsicComp(HandheldTranslatorComponent comp, HoldsTra { if (isEnabled) { - intrinsic.SpokenLanguages = new List(comp.SpokenLanguages); - intrinsic.UnderstoodLanguages = new List(comp.UnderstoodLanguages); - intrinsic.DefaultLanguageOverride = comp.DefaultLanguageOverride; + intrinsic.SpokenLanguages = [..comp.SpokenLanguages]; + intrinsic.UnderstoodLanguages = [..comp.UnderstoodLanguages]; } else { intrinsic.SpokenLanguages.Clear(); intrinsic.UnderstoodLanguages.Clear(); - intrinsic.DefaultLanguageOverride = null; } intrinsic.Enabled = isEnabled; intrinsic.Issuer = comp; } - private static void AddIfNotExists(List list, string item) + /// + /// Checks whether any OR all required languages are provided. Used for utility purposes. + /// + public static bool CheckLanguagesMatch(ICollection required, ICollection provided, bool requireAll) { - if (list.Contains(item)) - return; - list.Add(item); + if (required.Count == 0) + return true; + + return requireAll + ? required.All(provided.Contains) + : required.Any(provided.Contains); } } diff --git a/Content.Server/Mind/Commands/MakeSentientCommand.cs b/Content.Server/Mind/Commands/MakeSentientCommand.cs index cacd499ab8d..b58d782d9c5 100644 --- a/Content.Server/Mind/Commands/MakeSentientCommand.cs +++ b/Content.Server/Mind/Commands/MakeSentientCommand.cs @@ -61,10 +61,11 @@ public static void MakeSentient(EntityUid uid, IEntityManager entityManager, boo var language = IoCManager.Resolve().GetEntitySystem(); var speaker = entityManager.EnsureComponent(uid); - // If the speaker knows any language (like monkey or robot), they keep those - // Otherwise, we give them the fallback + + // If the entity already speaks some language (like monkey or robot), we do nothing else + // Otherwise, we give them the fallback language if (speaker.SpokenLanguages.Count == 0) - language.AddLanguage(speaker, SharedLanguageSystem.FallbackLanguagePrototype); + language.AddLanguage(uid, SharedLanguageSystem.FallbackLanguagePrototype); } entityManager.EnsureComponent(uid); diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index 53517da6cb4..2500138a238 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -106,7 +106,7 @@ private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, ref Rad var parent = Transform(uid).ParentUid; if (TryComp(parent, out ActorComponent? actor)) { - var canUnderstand = _language.CanUnderstand(parent, args.Language); + var canUnderstand = _language.CanUnderstand(parent, args.Language.ID); var msg = new MsgChatMessage { Message = canUnderstand ? args.OriginalChatMsg : args.LanguageObfuscatedChatMsg diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index 60aa7c2f4fb..7ed7574a9ae 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -60,7 +60,7 @@ private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent c // Einstein-Engines - languages mechanic var listener = component.Owner; var msg = args.OriginalChatMsg; - if (listener != null && !_language.CanUnderstand(listener, args.Language)) + if (listener != null && !_language.CanUnderstand(listener, args.Language.ID)) msg = args.LanguageObfuscatedChatMsg; _netMan.ServerSendMessage(new MsgChatMessage { Message = msg}, actor.PlayerSession.Channel); @@ -183,7 +183,7 @@ private string WrapRadioMessage(EntityUid source, RadioChannelPrototype channel, ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), ("channel", $"\\[{channel.LocalizedName}\\]"), ("name", name), - ("message", FormattedMessage.EscapeText(message))); + ("message", message)); } /// diff --git a/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs b/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs new file mode 100644 index 00000000000..0632f5d9cb2 --- /dev/null +++ b/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Shared.Language.Components; + +/// +/// Stores data about entities' intrinsic language knowledge. +/// +[RegisterComponent] +public sealed partial class LanguageKnowledgeComponent : Component +{ + /// + /// List of languages this entity can speak without any external tools. + /// + [DataField("speaks", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] + public List SpokenLanguages = new(); + + /// + /// List of languages this entity can understand without any external tools. + /// + [DataField("understands", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] + public List UnderstoodLanguages = new(); +} diff --git a/Content.Shared/Language/Components/LanguageSpeakerComponent.cs b/Content.Shared/Language/Components/LanguageSpeakerComponent.cs index 95232ffe6ff..e8ebccb3ddf 100644 --- a/Content.Shared/Language/Components/LanguageSpeakerComponent.cs +++ b/Content.Shared/Language/Components/LanguageSpeakerComponent.cs @@ -1,29 +1,32 @@ -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - namespace Content.Shared.Language; -[RegisterComponent, AutoGenerateComponentState] +// TODO: either move all language speaker-related components to server side, or make everything else shared. +// The current approach leads to confusion, as the server never informs the client of updates in these components. + +/// +/// Stores the current state of the languages the entity can speak and understand. +/// +/// +/// All fields of this component are populated during a DetermineEntityLanguagesEvent. +/// They are not to be modified externally. +/// +[RegisterComponent] public sealed partial class LanguageSpeakerComponent : Component { /// - /// The current language the entity may use to speak. + /// The current language the entity uses when speaking. /// Other listeners will hear the entity speak in this language. /// - [ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public string CurrentLanguage = default!; + [DataField] + public string CurrentLanguage = ""; // The language system will override it on init /// - /// List of languages this entity can speak. + /// List of languages this entity can speak at the current moment. /// - [ViewVariables] - [DataField("speaks", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] - public List SpokenLanguages = new(); + public List SpokenLanguages = []; /// - /// List of languages this entity can understand. + /// List of languages this entity can understand at the current moment. /// - [ViewVariables] - [DataField("understands", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] - public List UnderstoodLanguages = new(); + public List UnderstoodLanguages = []; } diff --git a/Content.Shared/Language/Components/TranslatorImplantComponent.cs b/Content.Shared/Language/Components/TranslatorImplantComponent.cs new file mode 100644 index 00000000000..cb8c666c82f --- /dev/null +++ b/Content.Shared/Language/Components/TranslatorImplantComponent.cs @@ -0,0 +1,21 @@ +using Content.Shared.Language.Components.Translators; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Shared.Language.Components; + +/// +/// An implant that allows the implantee to speak and understand other languages. +/// +[RegisterComponent] +public sealed partial class TranslatorImplantComponent : BaseTranslatorComponent +{ + /// + /// Whether the implantee knows the languages necessary to speak using this implant. + /// + public bool SpokenRequirementSatisfied = false; + + /// + /// Whether the implantee knows the languages necessary to understand translations of this implant. + /// + public bool UnderstoodRequirementSatisfied = false; +} diff --git a/Content.Shared/Language/Components/TranslatorImplanterComponent.cs b/Content.Shared/Language/Components/TranslatorImplanterComponent.cs deleted file mode 100644 index 401e8a8b8aa..00000000000 --- a/Content.Shared/Language/Components/TranslatorImplanterComponent.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Shared.Language.Components; - -/// -/// An item that, when used on a mob, adds an intrinsic translator to it. -/// -[RegisterComponent] -public sealed partial class TranslatorImplanterComponent : Component -{ - [DataField("spoken", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List SpokenLanguages = new(); - - [DataField("understood", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List UnderstoodLanguages = new(); - - /// - /// The list of languages the mob must understand in order for this translator to have effect. - /// Knowing one language is enough. - /// - [DataField("requires", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List RequiredLanguages = new(); - - /// - /// If true, only allows to use this implanter on mobs. - /// - [DataField] - public bool MobsOnly = true; - - /// - /// Whether this implant has been used already. - /// - [DataField] - public bool Used = false; -} diff --git a/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs b/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs index a66c9be082e..072480031d5 100644 --- a/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs +++ b/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs @@ -4,15 +4,6 @@ namespace Content.Shared.Language.Components.Translators; public abstract partial class BaseTranslatorComponent : Component { - // TODO may need to be removed completely, it's a part of legacy code that never ended up being used. - /// - /// The language this translator changes the speaker's language to when they don't specify one. - /// If null, does not modify the default language. - /// - [DataField("defaultLanguage")] - [ViewVariables(VVAccess.ReadWrite)] - public string? DefaultLanguageOverride = null; - /// /// The list of additional languages this translator allows the wielder to speak. /// diff --git a/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs b/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs index f900603f01d..7e3de0eca61 100644 --- a/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs +++ b/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs @@ -7,9 +7,18 @@ namespace Content.Shared.Language.Components.Translators; public sealed partial class HandheldTranslatorComponent : Translators.BaseTranslatorComponent { /// - /// Whether or not interacting with this translator - /// toggles it on or off. + /// Whether interacting with this translator toggles it on and off. /// - [DataField("toggleOnInteract")] + [DataField] public bool ToggleOnInteract = true; + + /// + /// If true, when this translator is turned on, the entities' current spoken language will be set + /// to the first new language added by this translator. + /// + /// + /// This should generally be used for translators that translate speech between two languages. + /// + [DataField] + public bool SetLanguageOnInteract = true; } diff --git a/Content.Shared/Language/LanguagePrototype.cs b/Content.Shared/Language/LanguagePrototype.cs index 801ab8a393b..9342c07e91f 100644 --- a/Content.Shared/Language/LanguagePrototype.cs +++ b/Content.Shared/Language/LanguagePrototype.cs @@ -1,4 +1,3 @@ -using System.Runtime.CompilerServices; using Robust.Shared.Prototypes; namespace Content.Shared.Language; @@ -10,18 +9,10 @@ public sealed class LanguagePrototype : IPrototype public string ID { get; private set; } = default!; /// - /// If true, obfuscated phrases of creatures speaking this language will have their syllables replaced with "replacement" syllables. - /// Otherwise entire sentences will be replaced. + /// Obfuscation method used by this language. By default, uses /// - [DataField(required: true)] - public bool ObfuscateSyllables; - - /// - /// Lists all syllables that are used to obfuscate a message a listener cannot understand if obfuscateSyllables is true. - /// Otherwise uses all possible phrases the creature can make when trying to say anything. - /// - [DataField(required: true)] - public List Replacement = []; + [DataField("obfuscation")] + public ObfuscationMethod Obfuscation = ObfuscationMethod.Default; #region utility /// diff --git a/Content.Shared/Language/ObfuscationMethods.cs b/Content.Shared/Language/ObfuscationMethods.cs new file mode 100644 index 00000000000..51230c47970 --- /dev/null +++ b/Content.Shared/Language/ObfuscationMethods.cs @@ -0,0 +1,184 @@ +using System.Text; +using Content.Shared.Language.Systems; + +namespace Content.Shared.Language; + +[ImplicitDataDefinitionForInheritors] +public abstract partial class ObfuscationMethod +{ + /// + /// The fallback obfuscation method, replaces the message with the string "<?>". + /// + public static readonly ObfuscationMethod Default = new ReplacementObfuscation + { + Replacement = new List { "" } + }; + + /// + /// Obfuscates the provided message and writes the result into the provided StringBuilder. + /// Implementations should use the context's pseudo-random number generator and provide stable obfuscations. + /// + internal abstract void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context); + + /// + /// Obfuscates the provided message. This method should only be used for debugging purposes. + /// For all other purposes, use instead. + /// + public string Obfuscate(string message) + { + var builder = new StringBuilder(); + Obfuscate(builder, message, IoCManager.Resolve().GetEntitySystem()); + return builder.ToString(); + } +} + +/// +/// The most primitive method of obfuscation - replaces the entire message with one random replacement phrase. +/// Similar to ReplacementAccent. Base for all replacement-based obfuscation methods. +/// +public partial class ReplacementObfuscation : ObfuscationMethod +{ + /// + /// A list of replacement phrases used in the obfuscation process. + /// + [DataField(required: true)] + public List Replacement = []; + + internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) + { + var idx = context.PseudoRandomNumber(message.GetHashCode(), 0, Replacement.Count - 1); + builder.Append(Replacement[idx]); + } +} + +/// +/// Obfuscates the provided message by replacing each word with a random number of syllables in the range (min, max), +/// preserving the original punctuation to a resonable extent. +/// +/// +/// The words are obfuscated in a stable manner, such that every particular word will be obfuscated the same way throughout one round. +/// This means that particular words can be memorized within a round, but not across rounds. +/// +public sealed partial class SyllableObfuscation : ReplacementObfuscation +{ + [DataField] + public int MinSyllables = 1; + + [DataField] + public int MaxSyllables = 4; + + internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) + { + const char eof = (char) 0; // Special character to mark the end of the message in the code below + + var wordBeginIndex = 0; + var hashCode = 0; + + for (var i = 0; i <= message.Length; i++) + { + var ch = i < message.Length ? char.ToLower(message[i]) : eof; + var isWordEnd = char.IsWhiteSpace(ch) || IsPunctuation(ch) || ch == eof; + + // If this is a normal char, add it to the hash sum + if (!isWordEnd) + hashCode = hashCode * 31 + ch; + + // If a word ends before this character, construct a new word and append it to the new message. + if (isWordEnd) + { + var wordLength = i - wordBeginIndex; + if (wordLength > 0) + { + var newWordLength = context.PseudoRandomNumber(hashCode, MinSyllables, MaxSyllables); + + for (var j = 0; j < newWordLength; j++) + { + var index = context.PseudoRandomNumber(hashCode + j, 0, Replacement.Count - 1); + builder.Append(Replacement[index]); + } + } + + hashCode = 0; + wordBeginIndex = i + 1; + } + + // If this message concludes a word (i.e. is a whitespace or a punctuation mark), append it to the message + if (isWordEnd && ch != eof) + builder.Append(ch); + } + } + + private static bool IsPunctuation(char ch) + { + return ch is '.' or '!' or '?' or ',' or ':'; + } +} + +/// +/// Obfuscates each sentence in the message by concatenating a number of obfuscation phrases. +/// The number of phrases in the obfuscated message is proportional to the length of the original message. +/// +public sealed partial class PhraseObfuscation : ReplacementObfuscation +{ + [DataField] + public int MinPhrases = 1; + + [DataField] + public int MaxPhrases = 4; + + /// + /// A string used to separate individual phrases within one sentence. Default is a space. + /// + [DataField] + public string Separator = " "; + + /// + /// A power to which the number of characters in the original message is raised to determine the number of phrases in the result. + /// Default is 1/3, i.e. the cubic root of the original number. + /// + /// + /// Using the default proportion, you will need at least 27 characters for 2 phrases, at least 64 for 3, at least 125 for 4, etc. + /// Increasing the proportion to 1/4 will result in the numbers changing to 81, 256, 625, etc. + /// + [DataField] + public float Proportion = 1f / 3; + + internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) + { + var sentenceBeginIndex = 0; + var hashCode = 0; + + for (var i = 0; i < message.Length; i++) + { + var ch = char.ToLower(message[i]); + if (!IsPunctuation(ch) && i != message.Length - 1) + { + hashCode = hashCode * 31 + ch; + continue; + } + + var length = i - sentenceBeginIndex; + if (length > 0) + { + var newLength = (int) Math.Clamp(Math.Pow(length, Proportion) - 1, MinPhrases, MaxPhrases); + + for (var j = 0; j < newLength; j++) + { + var phraseIdx = context.PseudoRandomNumber(hashCode + j, 0, Replacement.Count - 1); + var phrase = Replacement[phraseIdx]; + builder.Append(phrase); + builder.Append(Separator); + } + } + sentenceBeginIndex = i + 1; + + if (IsPunctuation(ch)) + builder.Append(ch).Append(' '); // TODO: this will turn '...' into '. . . ' + } + } + + private static bool IsPunctuation(char ch) + { + return ch is '.' or '!' or '?'; // Doesn't include mid-sentence punctuation like the comma + } +} diff --git a/Content.Shared/Language/Systems/SharedLanguageSystem.cs b/Content.Shared/Language/Systems/SharedLanguageSystem.cs index e2eeb8bb493..0a03086ebe1 100644 --- a/Content.Shared/Language/Systems/SharedLanguageSystem.cs +++ b/Content.Shared/Language/Systems/SharedLanguageSystem.cs @@ -1,6 +1,6 @@ -using Content.Shared.Actions; +using System.Text; +using Content.Shared.GameTicking; using Robust.Shared.Prototypes; -using Robust.Shared.Random; namespace Content.Shared.Language.Systems; @@ -24,7 +24,7 @@ public abstract class SharedLanguageSystem : EntitySystem public static LanguagePrototype Universal { get; private set; } = default!; [Dependency] protected readonly IPrototypeManager _prototype = default!; - [Dependency] protected readonly IRobustRandom _random = default!; + [Dependency] protected readonly SharedGameTicker _ticker = default!; public override void Initialize() { @@ -36,4 +36,32 @@ public override void Initialize() _prototype.TryIndex(id, out var proto); return proto; } + + /// + /// Obfuscate a message using the given language. + /// + public string ObfuscateSpeech(string message, LanguagePrototype language) + { + var builder = new StringBuilder(); + var method = language.Obfuscation; + method.Obfuscate(builder, message, this); + + return builder.ToString(); + } + + /// + /// Generates a stable pseudo-random number in the range (min, max) (inclusively) for the given seed. + /// One seed always corresponds to one number, however the resulting number also depends on the current round number. + /// This method is meant to be used in to provide stable obfuscation. + /// + internal int PseudoRandomNumber(int seed, int min, int max) + { + // Using RobustRandom or System.Random here is a bad idea because this method can get called hundreds of times per message. + // Each call would require us to allocate a new instance of random, which would lead to lots of unnecessary calculations. + // Instead, we use a simple but effective algorithm derived from the C language. + // It does not produce a truly random number, but for the purpose of obfuscating messages in an RP-based game it's more than alright. + seed = seed ^ (_ticker.RoundId * 127); + var random = seed * 1103515245 + 12345; + return min + Math.Abs(random) % (max - min + 1); + } } diff --git a/Content.Shared/Language/Systems/SharedTranslatorImplanterSystem.cs b/Content.Shared/Language/Systems/SharedTranslatorImplanterSystem.cs deleted file mode 100644 index a13225378cd..00000000000 --- a/Content.Shared/Language/Systems/SharedTranslatorImplanterSystem.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Content.Shared.Examine; -using Content.Shared.Implants.Components; -using Content.Shared.Language.Components; -using Robust.Shared.Serialization; - -namespace Content.Shared.Language.Systems; - -public abstract class SharedTranslatorImplanterSystem : EntitySystem -{ - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnExamined); - } - - private void OnExamined(EntityUid uid, TranslatorImplanterComponent component, ExaminedEvent args) - { - if (!args.IsInDetailsRange) - return; - - var text = !component.Used - ? Loc.GetString("translator-implanter-ready") - : Loc.GetString("translator-implanter-used"); - - args.PushText(text); - } - - protected void OnAppearanceChange(EntityUid implanter, TranslatorImplanterComponent component) - { - var used = component.Used; - _appearance.SetData(implanter, ImplanterVisuals.Full, !used); - } -} diff --git a/Resources/Locale/en-US/language/commands.ftl b/Resources/Locale/en-US/language/commands.ftl index 32fa5415b8c..ba2b3160094 100644 --- a/Resources/Locale/en-US/language/commands.ftl +++ b/Resources/Locale/en-US/language/commands.ftl @@ -1,8 +1,16 @@ command-list-langs-desc = List languages your current entity can speak at the current moment. command-list-langs-help = Usage: {$command} -command-saylang-desc = Send a message in a specific language. -command-saylang-help = Usage: {$command} . Example: {$command} GalacticCommon "Hello World!" +command-saylang-desc = Send a message in a specific language. To choose a language, you can use either the name of the language, or its position in the list of languages. +command-saylang-help = Usage: {$command} . Example: {$command} GalacticCommon "Hello World!". Example: {$command} 1 "Hello World!" -command-language-select-desc = Select the currently spoken language of your entity. -command-language-select-help = Usage: {$command} . Example: {$command} GalacticCommon +command-language-select-desc = Select the currently spoken language of your entity. You can use either the name of the language, or its position in the list of languages. +command-language-select-help = Usage: {$command} . Example: {$command} 1. Example: {$command} GalacticCommon + +command-language-spoken = Spoken: +command-language-understood = Understood: +command-language-current-entry = {$id}. {$language} - {$name} (current) +command-language-entry = {$id}. {$language} - {$name} + +command-language-invalid-number = The language number must be between 0 and {$total}. Alternatively, use the language name. +command-language-invalid-language = The language {$id} does not exist or you cannot speak it. diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml index e932974a0f4..dd59d74d3f0 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml @@ -66,7 +66,7 @@ - type: Tag tags: - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Fox understands: @@ -179,7 +179,7 @@ tags: - DoorBumpOpener - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml index fa51b99325c..2dad0fe2e65 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml @@ -95,7 +95,7 @@ factions: - PsionicInterloper - NanoTrasen - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon understands: diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml index c2ae33ec0ba..8968f0e77ad 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml @@ -96,7 +96,7 @@ spawned: - id: FoodMeat amount: 1 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml index e51ae91d12c..9b851ffa522 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml @@ -133,7 +133,7 @@ - FootstepSound - DoorBumpOpener - ShoesRequiredStepTriggerImmune - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - SolCommon diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml index 52853d696a2..9e4f80bfb52 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml @@ -97,7 +97,7 @@ Female: FemaleVulpkanin Unsexed: MaleVulpkanin - type: DogVision - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Canilunzt diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index 0645e451af2..196abcfab76 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -213,7 +213,7 @@ visMask: - PsionicInvisibility - Normal - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - RobotTalk diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 7fe105f940c..ec917607abc 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -49,7 +49,7 @@ flavorKind: station-event-random-sentience-flavor-organic - type: Bloodstream bloodMaxVolume: 50 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -243,7 +243,7 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Chicken understands: @@ -522,7 +522,7 @@ prob: 0.5 - type: Extractable grindableSolutionName: food - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Moffic understands: @@ -624,7 +624,7 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Duck understands: @@ -879,7 +879,7 @@ interactSuccessSpawn: EffectHearts interactSuccessSound: path: /Audio/Voice/Arachnid/arachnid_chitter.ogg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Crab understands: @@ -1127,7 +1127,7 @@ - type: Inventory speciesId: kangaroo templateId: kangaroo - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Kangaroo understands: @@ -1321,7 +1321,7 @@ - type: Speech speechSounds: Monkey speechVerb: Monkey - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Monkey understands: @@ -1360,7 +1360,7 @@ - type: Speech speechSounds: Monkey speechVerb: Monkey - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Monkey understands: @@ -1405,7 +1405,7 @@ - type: NameIdentifier group: Kobold - type: LizardAccent - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Kobold understands: @@ -1640,7 +1640,7 @@ spawned: - id: FoodMeatRat amount: 1 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -1989,7 +1989,7 @@ path: /Audio/Animals/parrot_raught.ogg - type: Bloodstream bloodMaxVolume: 50 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon understands: @@ -2255,7 +2255,7 @@ - type: MeleeChemicalInjector transferAmount: 0.75 solution: melee - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: @@ -2594,7 +2594,7 @@ - type: Tag tags: - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Fox understands: @@ -2646,7 +2646,7 @@ spawned: - id: FoodMeat amount: 2 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: @@ -2804,7 +2804,7 @@ spawned: - id: FoodMeat amount: 3 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Cat understands: @@ -2876,7 +2876,7 @@ - type: NpcFactionMember factions: - Syndicate - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: @@ -3185,7 +3185,7 @@ spawned: - id: FoodMeat amount: 1 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -3298,7 +3298,7 @@ interactSuccessSpawn: EffectHearts interactSuccessSound: path: /Audio/Animals/pig_oink.ogg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Pig understands: @@ -3389,7 +3389,7 @@ reformTime: 10 popupText: diona-reform-attempt reformPrototype: MobDionaReformed - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - RootSpeak understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml b/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml index 3bcf8e7a16f..f8d0497b971 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml @@ -15,7 +15,7 @@ - type: Sprite sprite: Mobs/Aliens/Argocyte/argocyte_common.rsi - type: SolutionContainerManager - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index 5141811a8ea..6eb43fb89ae 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -36,7 +36,7 @@ - VimPilot - type: StealTarget stealGroup: AnimalIan - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: @@ -127,7 +127,7 @@ tags: - CannotSuicide - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Cat understands: @@ -151,7 +151,7 @@ tags: - CannotSuicide - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Cat understands: @@ -306,7 +306,7 @@ spawned: - id: FoodMeat amount: 2 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: @@ -411,7 +411,7 @@ spawned: - id: FoodMeat amount: 3 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: @@ -576,7 +576,7 @@ - VimPilot - type: StealTarget stealGroup: AnimalRenault - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Fox understands: @@ -629,7 +629,7 @@ - CannotSuicide - Hamster - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -807,7 +807,7 @@ attributes: proper: true gender: female - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Bubblish understands: @@ -847,7 +847,7 @@ attributes: proper: true gender: male - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Monkey understands: @@ -881,7 +881,7 @@ # - type: AlwaysRevolutionaryConvertible - type: StealTarget stealGroup: AnimalTropico - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Crab understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index 31a32333f3f..42da84323f6 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -119,7 +119,7 @@ attributes: gender: male - type: PotentialPsionic # Nyano - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Mouse @@ -298,7 +298,7 @@ - type: Food - type: Item size: Tiny # Delta V - Make them eatable and pickable. - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml b/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml index 9559ae3a0c0..08cb776c530 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml @@ -50,7 +50,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: cat - type: Physics - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Cat understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index e3166c15f6e..f9cf0db5f6f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -107,7 +107,7 @@ - type: TypingIndicator proto: robot - type: ZombieImmune - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - RobotTalk diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index f18b371c4c2..c4d88e80705 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -112,7 +112,7 @@ successChance: 0.5 interactSuccessString: petting-success-slimes interactFailureString: petting-failure-generic - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Bubblish understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml index 9b79d67f408..adf3af93eab 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml @@ -165,7 +165,7 @@ - type: FootstepModifier footstepSoundCollection: collection: FootstepBounce - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Kangaroo understands: @@ -251,7 +251,7 @@ - type: MeleeChemicalInjector solution: melee transferAmount: 4 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: @@ -357,7 +357,7 @@ - type: MeleeChemicalInjector solution: melee transferAmount: 6 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index 26553a2f1f2..397989643e6 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -125,7 +125,7 @@ chance: -2 - type: Psionic #Nyano - Summary: makes psionic by default. removable: false - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: @@ -239,7 +239,7 @@ - type: Tag tags: - CannotSuicide - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Xeno diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 67212d416fe..da9858105b4 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -219,7 +219,7 @@ price: 1500 # Kidnapping a living person and selling them for cred is a good move. deathPenalty: 0.01 # However they really ought to be living and intact, otherwise they're worth 100x less. - type: CanEscapeInventory # Carrying system from nyanotrasen. - - type: LanguageSpeaker # This is here so all with no LanguageSpeaker at least spawn with the default languages. + - type: LanguageKnowledge # This is here so even if species doesn't have a defined language, they at least speak GC speaks: - GalacticCommon understands: diff --git a/Resources/Prototypes/Entities/Mobs/Species/diona.yml b/Resources/Prototypes/Entities/Mobs/Species/diona.yml index 5cb3de6f168..de9928f0d67 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/diona.yml @@ -102,7 +102,7 @@ actionPrototype: DionaGibAction allowedStates: - Dead - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - RootSpeak diff --git a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml index c5395360187..b38ea2634fd 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml @@ -51,7 +51,7 @@ accent: dwarf - type: Speech speechSounds: Bass - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - SolCommon diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 7c3f857c001..e00e06279e5 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -17,7 +17,7 @@ - id: FoodMeatHuman amount: 5 - type: PotentialPsionic #Nyano - Summary: makes potentially psionic. - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - SolCommon diff --git a/Resources/Prototypes/Entities/Mobs/Species/moth.yml b/Resources/Prototypes/Entities/Mobs/Species/moth.yml index 5959c497186..1c55dcf0df1 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/moth.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/moth.yml @@ -23,7 +23,7 @@ accent: zombieMoth - type: Speech speechVerb: Moth - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Moffic diff --git a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml index bdea4499ed1..35f9e9fa393 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml @@ -59,7 +59,7 @@ types: Heat : 1.5 #per second, scales with temperature & other constants - type: Wagging - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Draconic diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml index a601010ef94..6c3dc952957 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml @@ -74,7 +74,7 @@ types: Asphyxiation: -1.0 maxSaturation: 15 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Bubblish diff --git a/Resources/Prototypes/Entities/Mobs/base.yml b/Resources/Prototypes/Entities/Mobs/base.yml index d5be77eef1c..40cb0da95a1 100644 --- a/Resources/Prototypes/Entities/Mobs/base.yml +++ b/Resources/Prototypes/Entities/Mobs/base.yml @@ -43,6 +43,7 @@ - type: MovementSpeedModifier - type: Polymorphable - type: StatusIcon + - type: LanguageSpeaker # Einstein Engines. This component is required to support speech, although it does not define known languages. - type: RequireProjectileTarget active: False diff --git a/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml b/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml index fc947efe9a3..da42b2774b1 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml @@ -1,132 +1,128 @@ - type: entity - abstract: true - id: BaseTranslatorImplanter - parent: [ BaseItem ] - name: basic translator implant - description: Translates speech. + parent: BaseSubdermalImplant + id: BasicGalacticCommonTranslatorImplant + name: basic common translator implant + description: Provides your illiterate friends the ability to understand the common galactic tongue. + noSpawn: true components: - - type: Sprite - sprite: Objects/Specific/Medical/implanter.rsi - state: implanter0 - layers: - - state: implanter1 - map: [ "implantFull" ] - visible: true - - state: implanter0 - map: [ "implantBroken" ] - - type: Appearance - - type: GenericVisualizer - visuals: - enum.ImplanterVisuals.Full: - implantFull: - True: {visible: true} - False: {visible: false} - implantBroken: - True: {visible: false} - False: {visible: true} - -- type: entity - id: BasicGalaticCommonTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: basic Galactic Common translator implant - description: An implant giving the ability to understand Galactic Common. - components: - - type: TranslatorImplanter + - type: TranslatorImplant understood: - GalacticCommon - type: entity - id: AdvancedGalaticCommonTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: advanced Galactic Common translator implant - description: An implant giving the ability to understand and speak Galactic Common. + parent: BaseSubdermalImplant + id: GalacticCommonTranslatorImplant + name: advanced common translator implant + description: A more advanced version of the translator implant, teaches your illiterate friends the ability to both speak and understand the galactic tongue! + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - GalacticCommon + - type: TranslatorImplant understood: - GalacticCommon + spoken: + - GalacticCommon - type: entity - id: BubblishTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: Bubblish translator implant - description: An implant giving the ability to understand and speak Bubblish. + parent: BaseSubdermalImplant + id: BubblishTranslatorImplant + name: bubblish translator implant + description: An implant that helps you speak and understand the language of slimes! Special vocal chords not included. + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Bubblish + - type: TranslatorImplant understood: - Bubblish + spoken: + - Bubblish + requires: + - GalacticCommon - type: entity - id: NekomimeticTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: Nekomimetic translator implant - description: An implant giving the ability to understand and speak Nekomimetic. Nya~! + parent: BaseSubdermalImplant + id: NekomimeticTranslatorImplant + name: nekomimetic translator implant + description: A translator implant intially designed to help domestic cat owners understand their pets, now granting the ability to understand and speak to Felinids! + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Nekomimetic + - type: TranslatorImplant understood: - Nekomimetic + spoken: + - Nekomimetic + requires: + - GalacticCommon - type: entity - id: DraconicTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: Draconic translator implant - description: An implant giving the ability to understand and speak Draconic. + parent: BaseSubdermalImplant + id: DraconicTranslatorImplant + name: draconic translator implant + description: A translator implant giving the ability to speak to dragons! Subsequently, also allows to communicate with the Unathi. + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Draconic + - type: TranslatorImplant understood: - Draconic + spoken: + - Draconic + requires: + - GalacticCommon - type: entity - id: CanilunztTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: Canilunzt translator implant - description: An implant giving the ability to understand and speak Canilunzt. Yeeps! + parent: BaseSubdermalImplant + id: CanilunztTranslatorImplant + name: canilunzt translator implant + description: A translator implant that helps you communicate with your local yeepers. Yeep! + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Canilunzt + - type: TranslatorImplant understood: - Canilunzt + spoken: + - Canilunzt + requires: + - GalacticCommon - type: entity - id: SolCommonTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: SolCommon translator implant + parent: BaseSubdermalImplant + id: SolCommonTranslatorImplant + name: sol-common translator implant description: An implant giving the ability to understand and speak SolCommon. Raaagh! + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - SolCommon + - type: TranslatorImplant understood: - SolCommon + spoken: + - SolCommon + requires: + - GalacticCommon - type: entity - id: RootSpeakTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: RootSpeak translator implant - description: An implant giving the ability to understand and speak RootSpeak. + parent: BaseSubdermalImplant + id: RootSpeakTranslatorImplant + name: root-speak translator implant + description: An implant that lets you speak for the trees. Or to the trees. + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - RootSpeak + - type: TranslatorImplant understood: - RootSpeak + spoken: + - RootSpeak + requires: + - GalacticCommon - type: entity - id: MofficTranslatorImplanter - parent: [ BaseTranslatorImplanter ] + parent: BaseSubdermalImplant + id: MofficTranslatorImplant name: Moffic translator implant - description: An implant giving the ability to understand and speak Moffic. + description: An implant designed to help domesticate mothroaches. Subsequently, allows you to communicate with the moth people. + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Moffic + - type: TranslatorImplant understood: - Moffic + spoken: + - Moffic + requires: + - GalacticCommon diff --git a/Resources/Prototypes/Entities/Objects/Devices/translators.yml b/Resources/Prototypes/Entities/Objects/Devices/translators.yml index e5ad824c5d9..664626ea4b4 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/translators.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/translators.yml @@ -1,7 +1,7 @@ - type: entity abstract: true id: TranslatorUnpowered - parent: [ BaseItem ] + parent: BaseItem name: translator description: Translates speech. components: @@ -36,7 +36,7 @@ - type: entity abstract: true id: TranslatorEmpty - parent: [ Translator ] + parent: Translator suffix: Empty components: - type: ItemSlots @@ -49,7 +49,7 @@ id: CanilunztTranslator parent: [ TranslatorEmpty ] name: Canilunzt translator - description: Translates speech between Canilunzt and Galactic Common. + description: Translates speech between Canilunzt and Galactic Common, allowing your local yeepers to communicate with the locals and vice versa! components: - type: HandheldTranslator spoken: @@ -66,7 +66,7 @@ id: BubblishTranslator parent: [ TranslatorEmpty ] name: Bubblish translator - description: Translates speech between Bubblish and Galactic Common. + description: Translates speech between Bubblish and Galactic Common, helping communicate with slimes and slime people. components: - type: HandheldTranslator spoken: @@ -83,7 +83,7 @@ id: NekomimeticTranslator parent: [ TranslatorEmpty ] name: Nekomimetic translator - description: Translates speech between Nekomimetic and Galactic Common. Why would you want that? + description: Translates speech between Nekomimetic and Galactic Common, enabling you to communicate with your pet cats. components: - type: HandheldTranslator spoken: @@ -100,7 +100,7 @@ id: DraconicTranslator parent: [ TranslatorEmpty ] name: Draconic translator - description: Translates speech between Draconic and Galactic Common. + description: Translates speech between Draconic and Galactic Common, making it easier to understand your local Uniathi. components: - type: HandheldTranslator spoken: @@ -134,7 +134,7 @@ id: RootSpeakTranslator parent: [ TranslatorEmpty ] name: RootSpeak translator - description: Translates speech between RootSpeak and Galactic Common. Like a true plant? + description: Translates speech between RootSpeak and Galactic Common. You may now speak for the trees. components: - type: HandheldTranslator spoken: @@ -151,7 +151,7 @@ id: MofficTranslator parent: [ TranslatorEmpty ] name: Moffic translator - description: Translates speech between Moffic and Galactic Common. Like a true moth... or bug? + description: Translates speech between Moffic and Galactic Common, helping you understand the buzzes of your pet mothroach! components: - type: HandheldTranslator spoken: @@ -168,7 +168,7 @@ id: XenoTranslator parent: [ TranslatorEmpty ] name: Xeno translator - description: Translates speech between Xeno and Galactic Common. Not sure if that will help. + description: Translates speech between Xeno and Galactic Common. This will probably not help you survive an encounter, though. components: - type: HandheldTranslator spoken: @@ -184,7 +184,7 @@ id: AnimalTranslator parent: [ TranslatorEmpty ] name: Animal translator - description: Translates all the cutes noises that animals make into a more understandable form! + description: Translates all the cutes noises that most animals make into a more understandable form! components: - type: HandheldTranslator understood: @@ -203,3 +203,4 @@ - Kobold requires: - GalacticCommon + setLanguageOnInteract: false diff --git a/Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml b/Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml new file mode 100644 index 00000000000..8b5b262ff8a --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml @@ -0,0 +1,77 @@ +- type: entity + id: BaseTranslatorImplanter + abstract: true + parent: BaseImplantOnlyImplanter + name: basic translator implanter + +- type: entity + id: BasicGalaticCommonTranslatorImplanter + parent: BaseTranslatorImplanter + name: basic common translator implanter + components: + - type: Implanter + implant: BasicGalacticCommonTranslatorImplant + +- type: entity + id: AdvancedGalaticCommonTranslatorImplanter + parent: BaseTranslatorImplanter + name: advanced common translator implanter + components: + - type: Implanter + implant: GalacticCommonTranslatorImplant + +- type: entity + id: BubblishTranslatorImplanter + parent: BaseTranslatorImplanter + name: bubblish translator implant + components: + - type: Implanter + implant: BubblishTranslatorImplant + +- type: entity + id: NekomimeticTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: nekomimetic translator implant + components: + - type: Implanter + implant: NekomimeticTranslatorImplant + +- type: entity + id: DraconicTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: draconic translator implant + components: + - type: Implanter + implant: DraconicTranslatorImplant + +- type: entity + id: CanilunztTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: canilunzt translator implant + components: + - type: Implanter + implant: CanilunztTranslatorImplant + +- type: entity + id: SolCommonTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: sol-common translator implant + components: + - type: Implanter + implant: SolCommonTranslatorImplant + +- type: entity + id: RootSpeakTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: root-speak translator implant + components: + - type: Implanter + implant: RootSpeakTranslatorImplant + +- type: entity + id: MofficTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: moffic translator implant + components: + - type: Implanter + implant: MofficTranslatorImplant diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml index 380f7e012d1..2c4fccb8b3e 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml @@ -104,7 +104,7 @@ price: 100 - type: Appearance - type: WiresVisuals - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - RobotTalk diff --git a/Resources/Prototypes/Language/languages.yml b/Resources/Prototypes/Language/languages.yml index 90bce1baed2..1a874612c2f 100644 --- a/Resources/Prototypes/Language/languages.yml +++ b/Resources/Prototypes/Language/languages.yml @@ -1,493 +1,558 @@ # The universal language, assumed if the entity has a UniversalLanguageSpeakerComponent. -# Do not use otherwise. Try to use the respective component instead of this language. +# Do not use otherwise. Making an entity explicitly understand/speak this language will NOT have the desired effect. - type: language id: Universal - obfuscateSyllables: false - replacement: - - "*incomprehensible*" + obfuscation: + !type:ReplacementObfuscation + replacement: + - "*incomprehensible*" # Never actually used # The common galactic tongue. - type: language id: GalacticCommon - obfuscateSyllables: true - replacement: - - Blah - - Blah - - Blah - - dingle-doingle - - dingle - - dangle - - jibber-jabber - - jubber - - bleh - - zippity - - zoop - - wibble - - wobble - - wiggle - - yada - - meh - - neh - - nah - - wah + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - blah + - blah + - blah + - dingle-doingle + - dingle + - dangle + - jibber-jabber + - jubber + - bleh + - zippity + - zoop + - wibble + - wobble + - wiggle + - yada + - meh + - neh + - nah + - wah # Spoken by slimes. - type: language id: Bubblish - obfuscateSyllables: true - replacement: - - blob - - plop - - pop - - bop - - boop + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - blob + - plop + - pop + - bop + - boop # Spoken by moths. - type: language id: Moffic - obfuscateSyllables: true - replacement: - - år - - i - - går - - sek - - mo - - ff - - ok - - gj - - ø - - gå - - la - - le - - lit - - ygg - - van - - dår - - næ - - møt - - idd - - hvo - - ja - - på - - han - - så - - ån - - det - - att - - nå - - gö - - bra - - int - - tyc - - om - - när - - två - - må - - dag - - sjä - - vii - - vuo - - eil - - tun - - käyt - - teh - - vä - - hei - - huo - - suo - - ää - - ten - - ja - - heu - - stu - - uhr - - kön - - we - - hön + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 # Replacements are really short + maxSyllables: 4 + replacement: + - år + - i + - går + - sek + - mo + - ff + - ok + - gj + - ø + - gå + - la + - le + - lit + - ygg + - van + - dår + - næ + - møt + - idd + - hvo + - ja + - på + - han + - så + - ån + - det + - att + - nå + - gö + - bra + - int + - tyc + - om + - när + - två + - må + - dag + - sjä + - vii + - vuo + - eil + - tun + - käyt + - teh + - vä + - hei + - huo + - suo + - ää + - ten + - ja + - heu + - stu + - uhr + - kön + - we + - hön - # Spoken by dionas. +# Spoken by dionas. - type: language id: RootSpeak - obfuscateSyllables: true - replacement: - - hs - - zt - - kr - - st - - sh + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 5 + replacement: + - hs + - zt + - kr + - st + - sh # A mess of broken Japanese, spoken by Felinds and Oni - type: language id: Nekomimetic - obfuscateSyllables: true - replacement: - - neko - - nyan - - mimi - - moe - - mofu - - fuwa - - kyaa - - kawaii - - poka - - munya - - puni - - munyu - - ufufu - - icha - - doki - - kyun - - kusu - - nya - - nyaa - - desu - - kis - - ama - - chuu - - baka - - hewo - - boop - - gato - - kit - - sune - - yori - - sou - - baka - - chan - - san - - kun - - mahou - - yatta - - suki - - usagi - - domo - - ori - - uwa - - zaazaa - - shiku - - puru - - ira - - heto - - etto + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 # May be too long even, we'll see. + replacement: + - neko + - nyan + - mimi + - moe + - mofu + - fuwa + - kyaa + - kawaii + - poka + - munya + - puni + - munyu + - ufufu + - icha + - doki + - kyun + - kusu + - nya + - nyaa + - desu + - kis + - ama + - chuu + - baka + - hewo + - boop + - gato + - kit + - sune + - yori + - sou + - baka + - chan + - san + - kun + - mahou + - yatta + - suki + - usagi + - domo + - ori + - uwa + - zaazaa + - shiku + - puru + - ira + - heto + - etto # Spoken by the Lizard race. - type: language id: Draconic - obfuscateSyllables: true - replacement: - - za - - az - - ze - - ez - - zi - - iz - - zo - - oz - - zu - - uz - - zs - - sz - - ha - - ah - - he - - eh - - hi - - ih - - ho - - oh - - hu - - uh - - hs - - sh - - la - - al - - le - - el - - li - - il - - lo - - ol - - lu - - ul - - ls - - sl - - ka - - ak - - ke - - ek - - ki - - ik - - ko - - ok - - ku - - uk - - ks - - sk - - sa - - as - - se - - es - - si - - is - - so - - os - - su - - us - - ss - - ss - - ra - - ar - - re - - er - - ri - - ir - - ro - - or - - ru - - ur - - rs - - sr - - a - - a - - e - - e - - i - - i - - o - - o - - u - - u - - s - - s + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 4 + replacement: + - za + - az + - ze + - ez + - zi + - iz + - zo + - oz + - zu + - uz + - zs + - sz + - ha + - ah + - he + - eh + - hi + - ih + - ho + - oh + - hu + - uh + - hs + - sh + - la + - al + - le + - el + - li + - il + - lo + - ol + - lu + - ul + - ls + - sl + - ka + - ak + - ke + - ek + - ki + - ik + - ko + - ok + - ku + - uk + - ks + - sk + - sa + - as + - se + - es + - si + - is + - so + - os + - su + - us + - ss + - ss + - ra + - ar + - re + - er + - ri + - ir + - ro + - or + - ru + - ur + - rs + - sr + - a + - a + - e + - e + - i + - i + - o + - o + - u + - u + - s + - s # Spoken by the Vulpkanin race. - type: language id: Canilunzt - obfuscateSyllables: true - replacement: - - rur - - ya - - cen - - rawr - - bar - - kuk - - tek - - qat - - uk - - wu - - vuh - - tah - - tch - - schz - - auch - - ist - - ein - - entch - - zwichs - - tut - - mir - - wo - - bis - - es - - vor - - nic - - gro - - lll - - enem - - zandt - - tzch - - noch - - hel - - ischt - - far - - wa - - baram - - iereng - - tech - - lach - - sam - - mak - - lich - - gen - - or - - ag - - eck - - gec - - stag - - onn - - bin - - ket - - jarl - - vulf - - einech - - cresthz - - azunein - - ghzth + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 4 + replacement: + - rur + - ya + - cen + - rawr + - bar + - kuk + - tek + - qat + - uk + - wu + - vuh + - tah + - tch + - schz + - auch + - ist + - ein + - entch + - zwichs + - tut + - mir + - wo + - bis + - es + - vor + - nic + - gro + # - lll + - enem + - zandt + - tzch + - noch + - hel + - ischt + - far + - wa + - baram + - iereng + - tech + - lach + - sam + - mak + - lich + - gen + - or + - ag + - eck + - gec + - stag + - onn + - bin + - ket + - jarl + - vulf + - einech + - cresthz + - azunein + - ghzth # The common language of the Sol system. - type: language id: SolCommon - obfuscateSyllables: true - replacement: - - tao - - shi - - tzu - - yi - - com - - be - - is - - i - - op - - vi - - ed - - lec - - mo - - cle - - te - - dis - - e + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 4 + replacement: + - tao + - shi + - tzu + - yi + - com + - be + - is + - i + - op + - vi + - ed + - lec + - mo + - cle + - te + - dis + - e - type: language id: RobotTalk - obfuscateSyllables: true - replacement: - - 0 - - 1 - - 01 - - 10 - - 001 - - 100 - - 011 - - 110 - - 101 - - 010 + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 10 # Crazy + replacement: + - 0 + - 1 # Languages spoken by various critters. - type: language id: Cat - obfuscateSyllables: true - replacement: - - murr - - meow - - purr - - mrow + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - murr + - meow + - purr + - mrow - type: language id: Dog - obfuscateSyllables: true - replacement: - - woof - - bark - - ruff - - bork - - raff - - garr + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - woof + - bark + - ruff + - bork + - raff + - garr - type: language id: Fox - obfuscateSyllables: true - replacement: - - bark - - gecker - - ruff - - raff - - garr + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - ruff + - raff + - garr + - yip + - yap + - myah - type: language id: Xeno - obfuscateSyllables: true - replacement: - - sss - - sSs - - SSS + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 8 # I was crazy once + replacement: + - s + - S - type: language id: Monkey - obfuscateSyllables: true - replacement: - - ok - - ook - - oook - - ooook - - oooook + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 8 # They locked me in a room... + replacement: + - o + - k - type: language id: Mouse - obfuscateSyllables: true - replacement: - - Squeak - - Piep - - Chuu - - Eeee - - Pip - - Fwiep - - Heep + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 3 + replacement: + - squ + - eak + - pi + - ep + - chuu + - ee + - fwi + - he - type: language id: Chicken - obfuscateSyllables: true - replacement: - - Coo - - Coot - - Cooot + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - co + - coo + - ot - type: language id: Duck - obfuscateSyllables: true - replacement: - - Quack - - Quack quack + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - qu + - ack + - quack - type: language id: Cow - obfuscateSyllables: true - replacement: - - Moo - - Mooo + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - moo + - mooo - type: language id: Sheep - obfuscateSyllables: true - replacement: - - Ba - - Baa - - Baaa + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - ba + - baa + - aa - type: language id: Kangaroo - obfuscateSyllables: true - replacement: - - Shreak - - Chuu + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - shre + - ack + - chuu + - choo - type: language id: Pig - obfuscateSyllables: true - replacement: - - Oink - - Oink oink + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - oink # Please someone come up with something better - type: language id: Crab - obfuscateSyllables: true - replacement: - - Click - - Click-clack - - Clack - - Tipi-tap - - Clik-tap - - Cliliick + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - click + - clack + - ti + - pi + - tap + - cli + - ick - type: language id: Kobold - obfuscateSyllables: true - replacement: - - Yip - - Grrar. - - Yap - - Bip - - Screet - - Gronk - - Hiss - - Eeee - - Yip + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 4 + replacement: + - yip + - yap + - gar + - grr + - ar + - scre + - et + - gronk + - hiss + - ss + - ee diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml index ab3f6f3d1c1..e93117b1aad 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml @@ -35,7 +35,7 @@ - MobLayer - type: Stamina critThreshold: 115 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Nekomimetic diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml index d23607b16d5..285c7340b21 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml @@ -65,7 +65,7 @@ Unsexed: MaleFelinid - type: Felinid - type: NoShoesSilentFootsteps - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - SolCommon From 726fd0f7aaeb1ee6ec21b536dbebe42dc2acb351 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 23:50:34 +0000 Subject: [PATCH 53/56] Automatic Changelog Update (#459) --- Resources/Changelog/Changelog.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 68ccf158b46..af45d9f81af 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4351,3 +4351,13 @@ Entries: Have fun customizing your characters! id: 6144 time: '2024-07-05T22:53:36.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Tweak + message: Translator implants are now proper implants that can be removed. + - type: Tweak + message: Animalistic languages should now look less messy. + - type: Fix + message: Hopefully fixed language menu desync and other issues. + id: 6145 + time: '2024-07-05T23:49:47.0000000+00:00' From c8a9002efc6ac89c280e95900f2ab70d5d9e89cb Mon Sep 17 00:00:00 2001 From: Spatison <137375981+Spatison@users.noreply.github.com> Date: Sat, 6 Jul 2024 03:19:35 +0300 Subject: [PATCH 54/56] Add Item Transfer System (#476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description This PR adds the ability to transfer objects from one player's hand to another player's hand, as in SS13. I have little coding experience, so my solutions may not be ideal. --- # TODO - [x] Make the code work - [x] Add popup - [x] Write a summary of the code - [x] Сorrect inaccuracies

Media

https://youtu.be/zTQWTsYm1gw

--- # Changelog :cl: - add: Added system - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --------- Co-authored-by: Danger Revolution! <142105406+DangerRevolution@users.noreply.github.com> --- Content.Client/Input/ContentContexts.cs | 1 + .../OfferItem/OfferItemIndicatorsOverlay.cs | 72 ++++++++ Content.Client/OfferItem/OfferItemSystem.cs | 51 ++++++ .../Options/UI/Tabs/KeyRebindTab.xaml.cs | 1 + Content.Client/Options/UI/Tabs/MiscTab.xaml | 1 + .../Options/UI/Tabs/MiscTab.xaml.cs | 5 + Content.Server/Alert/Click/AcceptingOffer.cs | 24 +++ Content.Server/OfferItem/OfferItemSystem.cs | 83 ++++++++++ Content.Shared/Alert/AlertType.cs | 3 +- Content.Shared/CCVar/CCVars.cs | 3 + Content.Shared/Input/ContentKeyFunctions.cs | 1 + .../OfferItem/OfferItemComponent.cs | 26 +++ .../SharedOfferItemSystem.Interactions.cs | 74 +++++++++ .../OfferItem/SharedOfferItemSystem.cs | 155 ++++++++++++++++++ Resources/Locale/en-US/alerts/alerts.ftl | 3 + .../en-US/escape-menu/ui/options-menu.ftl | 2 + .../en-US/interaction/offer-item-system.ftl | 13 ++ Resources/Prototypes/Alerts/alerts.yml | 10 ++ .../Prototypes/Entities/Mobs/Species/base.yml | 1 + .../Interface/Alerts/offer_item.rsi/meta.json | 14 ++ .../Alerts/offer_item.rsi/offer_item.png | Bin 0 -> 556 bytes .../Misc/give_item.rsi/give_item.png | Bin 0 -> 7276 bytes .../Interface/Misc/give_item.rsi/meta.json | 14 ++ Resources/keybinds.yml | 3 + 24 files changed, 559 insertions(+), 1 deletion(-) create mode 100644 Content.Client/OfferItem/OfferItemIndicatorsOverlay.cs create mode 100644 Content.Client/OfferItem/OfferItemSystem.cs create mode 100644 Content.Server/Alert/Click/AcceptingOffer.cs create mode 100644 Content.Server/OfferItem/OfferItemSystem.cs create mode 100644 Content.Shared/OfferItem/OfferItemComponent.cs create mode 100644 Content.Shared/OfferItem/SharedOfferItemSystem.Interactions.cs create mode 100644 Content.Shared/OfferItem/SharedOfferItemSystem.cs create mode 100644 Resources/Locale/en-US/interaction/offer-item-system.ftl create mode 100644 Resources/Textures/Interface/Alerts/offer_item.rsi/meta.json create mode 100644 Resources/Textures/Interface/Alerts/offer_item.rsi/offer_item.png create mode 100644 Resources/Textures/Interface/Misc/give_item.rsi/give_item.png create mode 100644 Resources/Textures/Interface/Misc/give_item.rsi/meta.json diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index fa631938100..ca22ab095d6 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -68,6 +68,7 @@ public static void SetupContexts(IInputContextContainer contexts) human.AddFunction(ContentKeyFunctions.SmartEquipBelt); human.AddFunction(ContentKeyFunctions.OpenBackpack); human.AddFunction(ContentKeyFunctions.OpenBelt); + human.AddFunction(ContentKeyFunctions.OfferItem); human.AddFunction(ContentKeyFunctions.MouseMiddle); human.AddFunction(ContentKeyFunctions.ArcadeUp); human.AddFunction(ContentKeyFunctions.ArcadeDown); diff --git a/Content.Client/OfferItem/OfferItemIndicatorsOverlay.cs b/Content.Client/OfferItem/OfferItemIndicatorsOverlay.cs new file mode 100644 index 00000000000..16a314a2cf4 --- /dev/null +++ b/Content.Client/OfferItem/OfferItemIndicatorsOverlay.cs @@ -0,0 +1,72 @@ +using System.Numerics; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.UserInterface; +using Robust.Shared.Enums; +using Robust.Shared.Utility; + +namespace Content.Client.OfferItem; + +public sealed class OfferItemIndicatorsOverlay : Overlay +{ + private readonly IInputManager _inputManager; + private readonly IEntityManager _entMan; + private readonly IEyeManager _eye; + private readonly OfferItemSystem _offer; + + private readonly Texture _sight; + + public override OverlaySpace Space => OverlaySpace.ScreenSpace; + + private readonly Color _mainColor = Color.White.WithAlpha(0.3f); + private readonly Color _strokeColor = Color.Black.WithAlpha(0.5f); + private readonly float _scale = 0.6f; // 1 is a little big + + public OfferItemIndicatorsOverlay(IInputManager input, IEntityManager entMan, + IEyeManager eye, OfferItemSystem offerSys) + { + _inputManager = input; + _entMan = entMan; + _eye = eye; + _offer = offerSys; + + var spriteSys = _entMan.EntitySysManager.GetEntitySystem(); + _sight = spriteSys.Frame0(new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Misc/give_item.rsi"), + "give_item")); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (!_offer.IsInOfferMode()) + return false; + + return base.BeforeDraw(in args); + } + + protected override void Draw(in OverlayDrawArgs args) + { + var mouseScreenPosition = _inputManager.MouseScreenPosition; + var mousePosMap = _eye.PixelToMap(mouseScreenPosition); + if (mousePosMap.MapId != args.MapId) + return; + + + var mousePos = mouseScreenPosition.Position; + var uiScale = (args.ViewportControl as Control)?.UIScale ?? 1f; + var limitedScale = uiScale > 1.25f ? 1.25f : uiScale; + + DrawSight(_sight, args.ScreenHandle, mousePos, limitedScale * _scale); + } + + private void DrawSight(Texture sight, DrawingHandleScreen screen, Vector2 centerPos, float scale) + { + var sightSize = sight.Size * scale; + var expandedSize = sightSize + new Vector2(7f, 7f); + + screen.DrawTextureRect(sight, + UIBox2.FromDimensions(centerPos - sightSize * 0.5f, sightSize), _strokeColor); + screen.DrawTextureRect(sight, + UIBox2.FromDimensions(centerPos - expandedSize * 0.5f, expandedSize), _mainColor); + } +} diff --git a/Content.Client/OfferItem/OfferItemSystem.cs b/Content.Client/OfferItem/OfferItemSystem.cs new file mode 100644 index 00000000000..51b8dcbc0bc --- /dev/null +++ b/Content.Client/OfferItem/OfferItemSystem.cs @@ -0,0 +1,51 @@ +using Content.Shared.CCVar; +using Content.Shared.OfferItem; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.Player; +using Robust.Shared.Configuration; + +namespace Content.Client.OfferItem; + +public sealed class OfferItemSystem : SharedOfferItemSystem +{ + [Dependency] private readonly IOverlayManager _overlayManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IInputManager _inputManager = default!; + [Dependency] private readonly IEyeManager _eye = default!; + + public override void Initialize() + { + Subs.CVar(_cfg, CCVars.OfferModeIndicatorsPointShow, OnShowOfferIndicatorsChanged, true); + } + public override void Shutdown() + { + _overlayManager.RemoveOverlay(); + + base.Shutdown(); + } + + public bool IsInOfferMode() + { + var entity = _playerManager.LocalEntity; + + if (entity == null) + return false; + + return IsInOfferMode(entity.Value); + } + private void OnShowOfferIndicatorsChanged(bool isShow) + { + if (isShow) + { + _overlayManager.AddOverlay(new OfferItemIndicatorsOverlay( + _inputManager, + EntityManager, + _eye, + this)); + } + else + _overlayManager.RemoveOverlay(); + } +} diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index 49e8099e0fb..9daca74dd3a 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -183,6 +183,7 @@ void AddCheckBox(string checkBoxName, bool currentState, Action +