mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-09 00:13:55 +00:00
* Fixes incorrect operator usage in mecha code (#82570) ## About The Pull Request I completely screwed up and told the original PR author of #82415 (9922d2f237) to use the `XOR` operator instead of the `OR` operator (I wasn't thinking right for some reason when I was reading the ref), anyways this PR just fixes that because I misled the contributor into doing something that wasn't correct and actually would BREAK functionality instead. * Fixes TGUI debugging tools (#82569) This project doesn't interfere with the game logic and aims to fix multiple debugging features that are currently broken. Unfortunately, kitchen sink and debug layout became broken after migration to Redux. This PR aims to fix those features. * Removes unused code for HTML UIs (#82589) ## About The Pull Request This is the final PR for https://hackmd.io/XLt5MoRvRxuhFbwtk4VAUA that I've been slowly inching towards the past few months. This removes ``updateDialog``, ``updateUsrDialog``, ``IN_USE``, ``INTERACT_MACHINE_SET_MACHINE``, and everything surrounding it. Also fixes advanced camera consoles not booting you off when you're moved out of reach. We called ``check_eye`` on mob life whenever they had their machine var set, but their machine var would never be set to anything that actually used it, which I found to be a little funny but was also probably my fault. ## Why It's Good For The Game This is poor and unmaintained code used for HTML UIs that we no longer need thanks to TGUI, we should get rid of it to encourage the use of TGUI in the future instead. ## Changelog 🆑 fix: Advanced camera consoles now boots you off when you're moved out of reach. /🆑 * Fixes a variety of input stalling exploits (#82577) ## About The Pull Request Fixes the following input stalling exploits (maybe missed some): - Changing GPS tag - Setting teleporter destination - Request Console Reply - Various AI law board interactions - Note, I used `is_holding` but technically this means these fail with telekinesis. I can swap them to `can_perform_action(...)`, which allows TK, but I noticed some places explicitly deny TK interactions with Ai law boards. Not sure which is preferred. - Borg Rename Board - Plumbing Machines and Ducts - APCs and SMES terminal placements - Stargazers Telepathy - Go Go Gadget Hat ## Changelog 🆑 Melbert fix: You can't change the GPS tag of something unless you can actually use the GPS fix: You can't set the teleporter to a location unless you can actually use the teleporter fix: You can't reply to request console requests unless you can actually use the console fix: You can't update AI lawboards unless you're actually holding them fix: You can't update a borg rename board unless you're actually holding it fix: You can't mess with plumbing machines unless you can actually use them fix: You can't recolor / relayer ducts unless you're actually holding them fix: You can't magically wire APCs and SMESs unless you're right by them fix: You can't use Stargazer Telepathy on people who you can't see fix: You can't configure the Inspector Hat unless you can actually use it /🆑 * [NO GBP] Power outage operation fixes for chem master (#82591) ## About The Pull Request - If the chem master runs out of power mid printing, it will properly stop the printing process and its animation - When transferring reagents it correctly checks if we have enough power without forcing it ## Changelog 🆑 fix: chem master properly shuts down if it loses power mid printing and won't transfer reagents for the same /🆑 * Refactor renaming UNIQUE_RENAME items from the pen to an element (#82491) ## About The Pull Request So a bit ago someone in code_general wanted to make plushies renamable, but learnt that just adding the `UNIQUE_RENAME` flag wouldn't work as pens would murder the plushie and only THEN let you rename it. I noted refactoring both pens and plushies to use the new `item_interaction(...)` procs would Just Solve This, but, well, they didn't really have any coding experience. But, hey, renaming being hardcoded to the pens has annoyed me ever since I laid my eyes upon the hot mess that is paperwork code. So here we are! ### We're making it an element. There's not really much to this, this is mostly the same code but moved to an element and with some minor cleanups. First, we move it all from `/obj/item/pen` to a new element we called `/datum/element/tool_renaming`. With this, instead of having it proc on `/obj/item/pen/afterattack(...)`, we register it to proc on the `COMSIG_ITEM_INTERACTING_WITH_ATOM` signal.6e36ed9840/code/__DEFINES/dcs/signals/signals_atom/signals_atom_x_act.dm (L59-L62)Secondly, we realize the code is just going through each if statement regardless of whether the previous was correct.6e36ed9840/code/modules/paperwork/pen.dm (L225-L258)And, as we're dealing with text, just make it a switch statement instead. ```dm switch(pen_choice) if("Rename") (...) if("Description") (...) if("Reset") (...) ``` Then, we replace all single letter variables with descriptive ones, replace the if-elses with early returns, and make it actually return item interaction flags. Finally, we slap this onto the pen, and we're done. Now we can slap it onto other fitting renaming tools, and it uses the proper item interaction system. ## Why It's Good For The Game I feel it's generally better to not hardcode this to just pens, we have plenty other writing utensils and possible renaming tools. It's also a bit cleaner than before. Apart from that, moves it from using `afterattack(...)` to the proper item interaction chain by using `COMSIG_ITEM_INTERACTING_WITH_ATOM`, which should reduce janky interactions. ## Changelog 🆑 refactor: Instead of being hardcoded to the pen, renaming items is now an element. Currently only pens have this, and functionality should be the same, but please report it if you find any items that were renamable but now aren't. /🆑 * Adds various quality of life changes for cooking to make it less click intensive. (#82566) ## About The Pull Request - Increases tray item size by 1 item. - Ranges and griddles can now be fed from trays. Click when closed => fill soup pot. Click when open => fill associated oven tray. Right click when open => fill tray from oven tray Click griddle => fill griddle surface. Right click => fill tray from griddle surface - Martian batter is now 5u of each ingredient into 10u of batter. Hopefully will make it bug out less where it makes far fewer reagents than it is supposed to, fixing reagents, or well soups specifically... is out of scope for this PR. - Adds the ability to print soup pots and large trays from the service lathe Soup pot: 5 Iron sheets, 0.4 bluespace crystal (given their size of 200U) Large serving tray: 2 iron sheets ## Why It's Good For The Game Makes cooking a lot less tedious. Especially for people with low precision when it comes to filling oven trays. This also bring the behavior up to parity with how you can click microwaves with trays to fill them, ditto for the food processor. It also allows chef to use the whole capacity of an oven, as previously you couldn't easily click 6 cake batters or other giant sprites onto the tiny tray. The tray is now sized to be able to easily feed a griddle 8 items. ## Changelog 🆑 qol: chef equipment can now deposit and withdraw to/from trays! qol: chef now has access to griddle and oven sized trays! qol: service can now print soup pots /🆑 --------- Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com> * Removes grid usage + heavy refactors (#82571) ## About The Pull Request Grid has been deprecated for quite some time and we still use it. I won't completely remove the component, this way downstreams won't immediately suffer, but I can remove it from usage. Some of these UIs had issues with them and as a hobby project I've refactored them into typescript / rebuilt them. Airlock electronics, for instance, looks substantially better. <details> <summary>before/after as requested</summary> current airlock electronics scrolls into oblivion  updated  </details> ## Why It's Good For The Game Code improvement + probably UI bug fixes ## Changelog 🆑 fix: Airlock electronics and other access-config type UIs should look much better. /🆑 * modular fixes * [No GBP] Removes cogbar from some stealthy actions (#82593) Issue brought some missed hidden actions to my attention. I left cogbars in for _breaking_ handcuffs because resisting is sort of a gray area. On one hand, you don't want someone to see you doing it; on the other, there is a visible warning that you started doing it. So, meet in the the middle, breaking handcuffs is still visible while resisting isn't. Closes #82583 Cogbars are not intended to ruin stealth 🆑 fix: Deviants buffed: Rogue shoelacing, pickpocketing and restraint resisting no longer give cogbar icons. /🆑 * [NO GBP] ...Remember to add SIGNAL_HANDLER (#82630) ## About The Pull Request Just realized I forgot to add `SIGNAL_HANDLER` to the all-nighter `on_removed_limb(...)` proc, even though it handles signals. ## Why It's Good For The Gamefe26373572/code/__DEFINES/dcs/helpers.dm (L9-L11)* React cleanup (#82607) ## About The Pull Request - No defaultHooks in react. Might fix issues where pages were not scrollable on hover. - createRef in a functional component. should be useref ## Why It's Good For The Game Code improvement * Security photobooths have their own ID (#82628) ## About The Pull Request Prevents the HoP's photobooth button from connecting to the security photobooth via having the same ID. ## Why It's Good For The Game I forgot to add this when I made the security photobooth but it's important that by default without any varedits, the HoP and security photobooths stay separate. ## Changelog 🆑 fix: The HoP's photobooth button is now consistently connected to the HoP's photobooth. /🆑 * Fix buckled alert unbuckling not working properly (#82627) ## About The Pull Request So funny thing, while trying to reproduce a different issue on the current master, I coincidentally let my local instance start without reading, latejoined on the shuttle, and I noticed it wasn't letting me unbuckle as easily. Looking into this a bit later, it seems as if it's a line #82593 accidentally changed while moving around the `/mob/living/carbon/resist_buckle()` proc's flow.fe26373572/code/modules/mob/living/carbon/carbon.dm (L238-L241)While before it was ```dm /mob/living/carbon/resist_buckle() if(HAS_TRAIT(src, TRAIT_RESTRAINED)) (...) else buckled.user_unbuckle_mob(src,src) ``` Just changing this to `buckled.user_unbuckle_mob(src, src)` fixes this. ## Why It's Good For The Game Fixes buckled alert unbuckling not working properly. Fixes #82627. ## Changelog 🆑 fix: Clicking the buckled alert unbuckles you again. /🆑 * Advanced camera consoles correctly deactivates when something happens to it or the user (#82619) ## About The Pull Request - Fixes #82520 1. The eye deactivates when the machine is destroyed/deleted 2. The eye deactivates when the machine loses power 3. The computer constantly moniters the users status inside `process()` and will deactivate when anything happens to them. Its not enough to just hook onto to the mobs `COMSIG_MOVABLE_MOVED` signal. Literarly anything can happen to them so we have to check constantly for any changes ## Changelog 🆑 fix: advanced camera consoles correctly deactivate when something happens(no proximity, no power etc) to its user /🆑 * Oven tray checks for ovens (#82615) ## About The Pull Request - Fixes #82610 Only oven trays have this proc not serving trays or other stuff  Also oven trays have a null atom storage which prevents it from being put back in the oven after taking it out. So we remove that check ## Changelog 🆑 fix: you can put back the oven tray after you take it out fix: only oven trays are allowed in ovens preventing baked food runtimes /🆑 * Living Limb fixes (feat: Basic mobs attack random body zones again) (#82556) ## About The Pull Request Reworks Living Limb code to fix a bunch of runtimes and issues I saw while testing Bioscrambler. Specifically, the contained mobs are now initialised via element following attachment so that signal registration can occur at the correct time. This allows limbs to function correctly when added from nullspace via admin panel or bioscrambler. Secondarily (and more wide-ranging) at some point (probably #79563) we inadvertently made basic mobs only attack the target's chest instead of spreading damage. This is problematic for Living Flesh which can only attach itself to damaged limbs but was left unable to attack damaged limbs. I've fixed this in a way which is maybe stupid: adding an element which randomises attack zone pre-attack. Living limbs also limit this to _only_ limbs (although it will fall back to chest if you have no limbs at all). This is _technically_ still different, the previous behaviour used `adjustBruteLoss` and `adjustFireLoss` and would spread the damage across your entire body, but there isn't a route to that via the new interface and this seems close enough. ## Changelog 🆑 fix: Living Limbs created by Bioscrambler will be alive. fix: Living Limbs can once more attach themselves to your body. balance: Living Limbs will prioritise attacking your limbs. fix: Basic Mobs will once again spread their damage across body zones instead of only attacking your chest. /🆑 * RPG Loot: Revisited & READY (#82533) Revival of #72881 A new alt click window with a tarkov-y loading spinner. Replaces the object item window in stat panel. <details> <summary>vids</summary> toggleable grouping:  now lists the floor as first obj:  in action:  </details> - search by name - 515 image generator is much faster than alt click menu - opening a gargantuan amount of items shouldnt freeze your screen - groups similar items together in stacks by default, toggleable - shows tile as first item - <kbd>Shift</kbd> and <kbd>Ctrl</kbd> compatible with LMB 🖱️ - RMB points points at items (sry i could not get MMB working) - key <kbd>Esc</kbd> to exit the window. For devs: - A new image generation tech. - An error refetch mechanic to the Image component - It does not "smart track" the items being added to the pile, just reopen or refresh. This was a design decision. Honestly I just dislike the stat panel Fixes #53824 Fixes  🆑 add: Added a loot window for alt-clicking tiles. del: Removed the item browser from the stat panel. /🆑 --------- Co-authored-by: Zephyr <12817816+ZephyrTFA@users.noreply.github.com> Co-authored-by: AnturK <AnturK@users.noreply.github.com> Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> * Reverts parts of #82602 (nodeath checks) (#82637) ## About The Pull Request Reverts the nodeath checks of #82602 I opened a review thinking these checks were sus and the PR author said they would remove them, but it was merged before that happened. TL;DR 1. I just noticed this now but it only affects carbons / humans it doesn't even cover living or any other subtypes 2. Kinda sus. Some code intentionally skips checking nodeath (I guess? Like removing the brain for example) so we would need a larger audit of this rather than haphazardly throwing it in. * Fixes to battle arcade (#82620) ## About The Pull Request Added gear for world nine, removed the "Gear" gear that did nothing. Made counterattacks to kill an enemy properly kill the enemy. I renamed some gear items to fit the theme of the area they are unlocked in just as a small thing. ## Why It's Good For The Game Closes https://github.com/tgstation/tgstation/issues/82613 ## Changelog 🆑 fix: Battle arcade's higher levels no longer gives you a "Gear" gear, and counterattacks can now properly kill enemies. /🆑 * Fixes SMES terminal placing under the SMES and not under the player (#82665) ## About The Pull Request Changes `src` to`user` to get intended behavior. * Birdshot: Toy crate (#82633) ## About The Pull Request Gives the clown+mime their toy crate. ## Why It's Good For The Game *honk* * tram ai sat starts with a full smes (#82646) ## About The Pull Request consistency and also this is fixes a bug introduced by that one power refactor ## Why It's Good For The Game bug bad ## Changelog 🆑 fix: tramstation AI sat starts full /🆑 * [no gbp] Space Ruin bioscramblers shouldn't chase people around (#82649) ## About The Pull Request See title They wouldn't lock on to people on the station from a space ruin, but would to whoever entered their z level the second it was entered. Also fixes bug where I changed `status_flags` to `status_effects` for some reason which isn't where you look for godmode ## Why It's Good For The Game We have a space ruin whcih several (coreless) anomalies spawn on, the bioscrambler was put as an option because it was already immortal. It's weird though to zone into the ruin and immediately have every anomaly in there lock onto you, the best intended effect is probably for these ones specifically not to be bloodthirsty. We kind of only care about that behaviour on the station. ## Changelog 🆑 fix: Anomalous Research ruin Bioscrambler anomalies won't home in on targets fix: Bioscrambler won't randomly drop its target for no reason /🆑 * Sunders the many unused sprites and organizes what's left in structures.dmi (#82658) ## About The Pull Request Hello again, I noticed the /obj/structures.dmi file had a lot of unused stuff like tables from two generations ago, so I changed some stuff around: - Many unused, old icons deleted, mostly window variants used in old smoothing systems I imagine - Reorganized many sprites in the file so they're more grouped together - Tweaked some barricade sprite naming to be consistent/standardized, and to let others know they're not _too_ old... - Fixed a misnomer that I believe was making directional tinted windows look like frosted windows ## Why It's Good For The Game Saves on file space, and satisfies your brain's pattern recognition bits ### Spriting Old:  New:  also good lord those linen bin sprites are a crime ## Changelog 🆑 fix: Probably fixed directional tinted windows looking like directional frosted windows image: Deleted a bunch of unused structure sprites /🆑 * Birdshot Wall Sanity Pass (#82598) ## About The Pull Request Cleans up minor artifacting in the Birdshot Sec-Tram Closed Turfs ## Why It's Good For The Game Someone definitely didn't mean to place some machines under Closed Turfs. This barely qualifies as player facing. ## Changelog 🆑 fix: Cleans up some rocks on Birdshot /🆑 * [NO GBP] Fixes deconstruction of closets & crates under a special case (#82612) ## About The Pull Request So if a closet/crate has the `NO_DEBRIS_AFTER_DECONSTRUCTION` set on it and if someone/something is still inside, then after deconstruction they get deleted rather than getting dumped out first. Could cause potential hard delete of mobs & stuff. We don't want to deal with that ## Changelog 🆑 fix: closets & crates will dump all contents out first before deleting itself regardless of `NO_DEBRIS_AFTER_DECONSTRUCTION` thus not for e.g. hard deleting mobs inside it /🆑 * Fixes ordinance lab igniter in IceBox (#82595) ## About The Pull Request - Fixes #82294 Basically the same idea of merging ordanance lab with the burn chamber so they share the same apc as already implemented in #82322 ## Changelog 🆑 fix: Ordinance lab igniter in Icebox works again /🆑 * Birdshot: engi wardrope. (#82639) ## About The Pull Request Add engi wardrope on Birdshot. ## Why It's Good For The Game Birdshot doesn't have engi wardrope. 🆑 fix: Birdshot now have engi wardrope /🆑 * Gives shadow walk a new, spookier, and shorter sound effect that no longer ignores walls (#82689) ## About The Pull Request This gives shadow walk a snazzy new sound effect for entering/exiting jaunt. https://github.com/tgstation/tgstation/assets/28870487/c25f720f-5bad-4063-8d6e-140fd41bd740 This also has the sounds it plays no longer passes through walls. ## Why It's Good For The Game The ethereal_entrance/exit sound effects are drawn out, and pretty grating. They work for the other jaunts they're used for because a jaunt typically lasts longer than the sound itself. Nightmares are frequently dancing in and out of jaunt, and the sound effects for entering/exiting tend to overlap. It gets loud and annoying really fast. This sound effect is quicker, spookier, and more distinct. As for making the sound not ignore walls, I think it's pretty dumb how easy it is to detect the spooky scary shadow antag just by sitting in your department. It takes a lot of the initial fear and paranoia they have the potential for is wasted when Joe Geneticist can hear them messing around in their territory without having to leave their chair. ## Changelog 🆑 Rhials sound: Nightmare has a new sound effect for entering/exiting shadow jaunt. It also no longer can be heard through walls. /🆑 * [MIRROR] Alt click refactor (#2029) * Alt click refactor * Some early conflict removal * Big modular refactor * Update console.dm * Update paper.dm --------- Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com> Co-authored-by: Mal <13398309+vinylspiders@users.noreply.github.com> * Yeets `ATTACK_QDELETED`, fixes welding torches not using fuel on attacking non-mobs (2 year old bug) (#82694) ## About The Pull Request - Deletes `ATTACK_QDELETED` - May have been necessary in the past but it's pointless now. All it does is clutter the attack chain. Perish. - Fixes welders not using fuel on attacking non-mobs - #65762 "fixed" welders consuming fuel on clicking turfs by adding an `isliving` check and not an `ismovable` check? ## Changelog 🆑 Melbert fix: Blobs may rejoice, welding torches now consume fuel when attacking objects again after two years. /🆑 * electric_welder fire * Quirks, which give items, now have quirk_item arg specified as obj/item, instead of being just a var (#82650) ## About The Pull Request quirk_item is now /obj/item, since it will allow for calling procs or getting variables from this item It's required for non-modular translation to call for item's name to remove articles ## Why It's Good For The Game It's always an item, and if it's a path, it's already checked for it. Better usage in the future. * turns martial arts gloves into a component (#82599) sleeping carp gloves also work on mind init this means for the sake of deathmatch you dont have to put them off and on fixes #82321 🆑 fix: you no longer need to put your sleeping carp gloves off and on in Deathmatch to get the martial art /🆑 --------- Co-authored-by: san7890 <the@san7890.com> * Regal Rats can now tear down posters (#82673) ## About The Pull Request i was fixing something on bagil and someone who was playing a regal rat (after the round ended) said they wanted to be able to tear down posters as a regal rat so i decided to code it because it made sense. it's an element so literally any mob can tear down posters but i can't think of any other mobs that would make sense to let it tear down posters so we'll leave it just for _The Champion of All Mislaid Creatures_ for now ## Why It's Good For The Game Regal Rats should be all about sludgemaxxing and fucking up maintenance to make it look even more grody than it should be. Being able to tear up those disgusting and well-drawn posters to leave behind nothing but scraps fits that motif. The element has a `do_after()` just to make sure His Holiness doesn't accidentally tear down his posters while clicking (i think all mobs should have this but that's a different issue man) also includes some code improvement and user feedback in some failure cases that already existed in the code. ## Changelog 🆑 add: Regal Rats are now able to tear down those colorful posters those weird grey creatures keep spackling up on the walls of their rightful domain. /🆑 * Adds "Strong Stomach" quirk, a core CDDA/PZ quirk we've sorely been missing. Also Deviant Tastes dirty food re-nerf. (#82562) ## About The Pull Request - Adds Strong Stomach quirk. - 4 points - You can eat dirty food without risk of getting disease. - You suffer less negative effects from vomiting. Vomit stuns you for half the duration, and you lose half as much nutrition. - Reverts https://github.com/tgstation/tgstation/pull/76864 , integrates its effects into Strong Stomach instead. ## Why It's Good For The Game - Lotta people (namely Lizards and sometimes Felines with Deviant Tastes) run gimmicks involving them being a gremlin person and eating trash off the ground, and it's rather hard to accomplish this now since it makes you a public medbay enemy # 1. This quirk should give them an option to avoid that. - Also (as mentioned in the title) both CDDA and PZ have this trait and I can't believe we're missing it! This is something in modifiable-character-traits/quirks-101. - I moved the effects from #76864 to this quirk because 1. I thought it was more fitting and 2. I thought the original PR was kinda wack for what is (generally) a neutral quirk. ## Changelog 🆑 Melbert add: Adds the Strong Stomach quirk, which allows you to eat grimy food without worry about disease, and makes you a bit more resilient to the effects of vomiting. del: Deviant Tastes no longer prevents you from getting a negative moodlet from eating dirty food. Strong Stomach does that now. /🆑 --------- Co-authored-by: Jacquerel <hnevard@gmail.com> * Remove several functions from collections.js which have ES5 equivalents (#82417) * Makes it EVEN EASIER to work with atom item interactions ft. "Leaf and Branch" & "Death to Chains" (#82625) * apc fix * Gulag Adjustments Two (#82561) ## About The Pull Request I have received feedback that after the prior changes in #81971, the gulag is still a little bit too subject to RNG. The main culprit (as in my previous PR) is Iron being kind of cheap and the fact that unlike the old Gulag you no longer have any way of headhunting more valuable materials (everything appears as boulders on your ore scanner). My solution to this is wider than the last one of tweaking point values, but also much simpler: Just make every boulder you mine be worth the same amount of points regardless of what is inside of it. On the average test I made I could comfortably mine about 40-45 boulders in ten minutes. We'll make some adjustments to that rather than leaving 40 as the target number; Most players upon being teleported to the gulag are going to spend a few minutes whining and bemoaning their fate instead of getting straight to work. I had the benefit of being able to make sure my run started as soon as a storm ended so I wouldn't need any kind of midpoint break. I was also always the only person playing on my local instance, there hadn't been any other pesky prisoners before me who had already mined out all the nearest available deposits. And of course, let us not forget, I am an MLG master league ss13 player who was surely performing well above average. So we'll round that down to: Each boulder is worth 33 points, meaning you need to collect 31 boulders to complete a 1000 point (roughly ten minute) sentence. How do I ensure that every boulder is worth the same amount of points? Well it's pretty easy. One boulder = one material sheet. One material sheet = 33 points. Simple. "Now Jacquerel", I hear you not saying because you don't want me to know about this thing you would prefer to do instead of hitting rocks outside; "if I simply smash all of the tables and microwaves and botany trays and bed in the gulag I can easily get like 65 sheets of Iron, which is almost enough to buy the freedom for two entire people!" Unfortunately I knew you were going to try and do that and the prisoner point machine will only give you points for material sheets which have been printed from the material smelter (well, any material smelter actually but you should probably use the one in the gulag). You'll be able to tell because if you examine a valid material sheet it will mention a little maker's mark on it, which is absent in the beat-up iron that you get from smashing furniture to bits. Also glass is worth 0 points. Don't waste time digging up that shit. As glass has had all of its point value removed, I have added a "work pit" to the gulag to compensate. You can pull boulders out of this indefinitely via effort, however it also stamcrits you every time. It's not very fun to do this, but that's because I would prefer you to go find the rocks out in the field instead. This is a last resort. You can do this if there's no boulders left to mine or if you really really really hate mining and would rather very slowly click on one tile repeatedly to get your boulders instead. As a tiny bonus doing this gives workout experience. This isn't a totally ideal solution but I think it'll do for now. ## Why It's Good For The Game What we want out of the gulag is: - Something where officers can vaguely approximate an expected sentence duration. - A task that requires players to actually be spending that time doing something to get out of here. - Produces at least some amount of useful materials. In I think roughly that order. I hope this change accomplishes all three of these in a way that is somewhat predictable rather than throwing darts at a board. ## Changelog 🆑 balance: Gulag mining has been rebalanced so that every boulder is worth the same amount of points to mine for a prisoner regardless of what it contains, and should be more consistent. add: A vent which boulders can be hauled out of by hand has been added to the gulag which you can use if there's nothing left to mine. It is very slow, but at least it gives you a workout... /🆑 * stone * Makes test merge bot continue with other PRs if updating one fails. (#82717) Right now updating https://github.com/tgstation/tgstation/pull/81089#issuecomment-1907296233 fails because it exceeds github character limit for comments. This will make it work until backed is updated. * Fixes the RnD console by adding a removed import (#82750) ## About The Pull Request The 'map' import was removed from this file by #82417 but it's still used in place in code. This re-adds the import ## Why It's Good For The Game Fixes RnD consoles ## Changelog 🆑 fix: Fixed RnD consoles not being able to be opened. /🆑 Co-authored-by: Watermelon914 <3052169-Watermelon914@users.noreply.gitlab.com> * Fixes cargo import (#82755) ## About The Pull Request One of the imports got removed and there were no warnings... Man if only there were a technology that could warn us in advance ## Why It's Good For The Game UI fixes ## Changelog 🆑 fix: Fixed a bluescreen in cargo console /🆑 * fixes * Fixes, fixes. * Pre-emptive mirror of https://github.com/tgstation/tgstation/pull/82892 * Turf weakref persists in changeturf / Fix plasma cutters (#82906) ## About The Pull Request Turf references don't change so logically, turf weakrefs wouldn't change if the turf changes. By not doing this this can cause bugs: See #82886 . (This Fixes #82886) (Projectiles hold a list of weakrefs to atoms hit to determine what they have already hit. Because turf weakrefs reset, we could "hit" the same turf twice if it destroyed the turf. Old behavior - this was fine but now that they're weakrefs, we get two weakref datums in the list that point to the same ref.) Less hacky alternative to #82901 . (Closes #82901) ## Changelog 🆑 Melbert fix: Plasma cutters work again /🆑 --------- Co-authored-by: san7890 <the@san7890.com> Co-authored-by: Interception&? <137328283+intercepti0n@users.noreply.github.com> Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com> Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Co-authored-by: _0Steven <42909981+00-Steven@users.noreply.github.com> Co-authored-by: Ketrai <zottielolly@gmail.com> Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com> Co-authored-by: Jacquerel <hnevard@gmail.com> Co-authored-by: Zephyr <12817816+ZephyrTFA@users.noreply.github.com> Co-authored-by: AnturK <AnturK@users.noreply.github.com> Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Co-authored-by: Iajret <8430839+Iajret@users.noreply.github.com> Co-authored-by: vect0r <71346830+Vect0r2@users.noreply.github.com> Co-authored-by: jimmyl <70376633+mc-oofert@users.noreply.github.com> Co-authored-by: AMyriad <143908044+AMyriad@users.noreply.github.com> Co-authored-by: Zytolg <33048583+Zytolg@users.noreply.github.com> Co-authored-by: Xackii <120736708+Xackii@users.noreply.github.com> Co-authored-by: Rhials <28870487+Rhials@users.noreply.github.com> Co-authored-by: NovaBot <154629622+NovaBot13@users.noreply.github.com> Co-authored-by: Mal <13398309+vinylspiders@users.noreply.github.com> Co-authored-by: larentoun <31931237+larentoun@users.noreply.github.com> Co-authored-by: Arthri <41360489+Arthri@users.noreply.github.com> Co-authored-by: Watermelon914 <37270891+Watermelon914@users.noreply.github.com> Co-authored-by: Watermelon914 <3052169-Watermelon914@users.noreply.gitlab.com> Co-authored-by: Useroth <37159550+Useroth@users.noreply.github.com>
1217 lines
51 KiB
Plaintext
1217 lines
51 KiB
Plaintext
#define MOVES_HITSCAN -1 //Not actually hitscan but close as we get without actual hitscan.
|
|
#define MUZZLE_EFFECT_PIXEL_INCREMENT 17 //How many pixels to move the muzzle flash up so your character doesn't look like they're shitting out lasers.
|
|
|
|
/obj/projectile
|
|
name = "projectile"
|
|
icon = 'icons/obj/weapons/guns/projectiles.dmi'
|
|
icon_state = "bullet"
|
|
density = FALSE
|
|
anchored = TRUE
|
|
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
|
|
movement_type = FLYING
|
|
wound_bonus = CANT_WOUND // can't wound by default
|
|
generic_canpass = FALSE
|
|
blocks_emissive = EMISSIVE_BLOCK_GENERIC
|
|
layer = MOB_LAYER
|
|
//The sound this plays on impact.
|
|
var/hitsound // SKYRAT EDIT CHANGE
|
|
var/hitsound_wall = ""
|
|
|
|
resistance_flags = LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
|
|
var/def_zone = "" //Aiming at
|
|
var/atom/movable/firer = null//Who shot it
|
|
var/datum/fired_from = null // the thing that the projectile was fired from (gun, turret, spell)
|
|
var/suppressed = FALSE //Attack message
|
|
var/yo = null
|
|
var/xo = null
|
|
var/atom/original = null // the original target clicked
|
|
var/turf/starting = null // the projectile's starting turf
|
|
var/p_x = 16
|
|
var/p_y = 16 // the pixel location of the tile that the player clicked. Default is the center
|
|
|
|
//Fired processing vars
|
|
var/fired = FALSE //Have we been fired yet
|
|
var/paused = FALSE //for suspending the projectile midair
|
|
var/last_projectile_move = 0
|
|
var/last_process = 0
|
|
var/time_offset = 0
|
|
var/datum/point/vector/trajectory
|
|
var/trajectory_ignore_forcemove = FALSE //instructs forceMove to NOT reset our trajectory to the new location!
|
|
/// We already impacted these things, do not impact them again. Used to make sure we can pierce things we want to pierce. Lazylist, typecache style (object = TRUE) for performance.
|
|
var/list/impacted = list()
|
|
/// If TRUE, we can hit our firer.
|
|
var/ignore_source_check = FALSE
|
|
/// We are flagged PHASING temporarily to not stop moving when we Bump something but want to keep going anyways.
|
|
var/temporary_unstoppable_movement = FALSE
|
|
|
|
/** PROJECTILE PIERCING
|
|
* WARNING:
|
|
* Projectile piercing MUST be done using these variables.
|
|
* Ordinary passflags will result in can_hit_target being false unless directly clicked on - similar to projectile_phasing but without even going to process_hit.
|
|
* The two flag variables below both use pass flags.
|
|
* In the context of LETPASStHROW, it means the projectile will ignore things that are currently "in the air" from a throw.
|
|
*
|
|
* Also, projectiles sense hits using Bump(), and then pierce them if necessary.
|
|
* They simply do not follow conventional movement rules.
|
|
* NEVER flag a projectile as PHASING movement type.
|
|
* If you so badly need to make one go through *everything*, override check_pierce() for your projectile to always return PROJECTILE_PIERCE_PHASE/HIT.
|
|
*/
|
|
/// The "usual" flags of pass_flags is used in that can_hit_target ignores these unless they're specifically targeted/clicked on. This behavior entirely bypasses process_hit if triggered, rather than phasing which uses prehit_pierce() to check.
|
|
pass_flags = PASSTABLE
|
|
/// If FALSE, allow us to hit something directly targeted/clicked/whatnot even if we're able to phase through it
|
|
var/phasing_ignore_direct_target = FALSE
|
|
/// Bitflag for things the projectile should just phase through entirely - No hitting unless direct target and [phasing_ignore_direct_target] is FALSE. Uses pass_flags flags.
|
|
var/projectile_phasing = NONE
|
|
/// Bitflag for things the projectile should hit, but pierce through without deleting itself. Defers to projectile_phasing. Uses pass_flags flags.
|
|
var/projectile_piercing = NONE
|
|
/// number of times we've pierced something. Incremented BEFORE bullet_act and on_hit proc!
|
|
var/pierces = 0
|
|
|
|
/// If objects are below this layer, we pass through them
|
|
var/hit_threshhold = PROJECTILE_HIT_THRESHHOLD_LAYER
|
|
|
|
/// During each fire of SSprojectiles, the number of deciseconds since the last fire of SSprojectiles
|
|
/// is divided by this var, and the result truncated to the next lowest integer is
|
|
/// the number of times the projectile's `pixel_move` proc will be called.
|
|
var/speed = 0.8
|
|
|
|
/// This var is multiplied by SSprojectiles.global_pixel_speed to get how many pixels
|
|
/// the projectile moves during each iteration of the movement loop
|
|
///
|
|
/// If you want to make a fast-moving projectile, you should keep this equal to 1 and
|
|
/// reduce the value of `speed`. If you want to make a slow-moving projectile, make
|
|
/// `speed` a modest value like 1 and set this to a low value like 0.2.
|
|
var/pixel_speed_multiplier = 1
|
|
|
|
var/Angle = 0
|
|
var/original_angle = 0 //Angle at firing
|
|
var/nondirectional_sprite = FALSE //Set TRUE to prevent projectiles from having their sprites rotated based on firing angle
|
|
var/spread = 0 //amount (in degrees) of projectile spread
|
|
animate_movement = NO_STEPS //Use SLIDE_STEPS in conjunction with legacy
|
|
/// how many times we've ricochet'd so far (instance variable, not a stat)
|
|
var/ricochets = 0
|
|
/// how many times we can ricochet max
|
|
var/ricochets_max = 0
|
|
/// how many times we have to ricochet min (unless we hit an atom we can ricochet off)
|
|
var/min_ricochets = 0
|
|
/// 0-100 (or more, I guess), the base chance of ricocheting, before being modified by the atom we shoot and our chance decay
|
|
var/ricochet_chance = 0
|
|
/// 0-1 (or more, I guess) multiplier, the ricochet_chance is modified by multiplying this after each ricochet
|
|
var/ricochet_decay_chance = 0.7
|
|
/// 0-1 (or more, I guess) multiplier, the projectile's damage is modified by multiplying this after each ricochet
|
|
var/ricochet_decay_damage = 0.7
|
|
/// On ricochet, if nonzero, we consider all mobs within this range of our projectile at the time of ricochet to home in on like Revolver Ocelot, as governed by ricochet_auto_aim_angle
|
|
var/ricochet_auto_aim_range = 0
|
|
/// On ricochet, if ricochet_auto_aim_range is nonzero, we'll consider any mobs within this range of the normal angle of incidence to home in on, higher = more auto aim
|
|
var/ricochet_auto_aim_angle = 30
|
|
/// the angle of impact must be within this many degrees of the struck surface, set to 0 to allow any angle
|
|
var/ricochet_incidence_leeway = 40
|
|
/// Can our ricochet autoaim hit our firer?
|
|
var/ricochet_shoots_firer = TRUE
|
|
|
|
///If the object being hit can pass ths damage on to something else, it should not do it for this bullet
|
|
var/force_hit = FALSE
|
|
|
|
//Hitscan
|
|
var/hitscan = FALSE //Whether this is hitscan. If it is, speed is basically ignored.
|
|
var/list/beam_segments //assoc list of datum/point or datum/point/vector, start = end. Used for hitscan effect generation.
|
|
/// Last turf an angle was changed in for hitscan projectiles.
|
|
var/turf/last_angle_set_hitscan_store
|
|
var/datum/point/beam_index
|
|
var/turf/hitscan_last //last turf touched during hitscanning.
|
|
var/tracer_type
|
|
var/muzzle_type
|
|
var/impact_type
|
|
|
|
//Fancy hitscan lighting effects!
|
|
var/hitscan_light_intensity = 1.5
|
|
var/hitscan_light_range = 0.75
|
|
var/hitscan_light_color_override
|
|
var/muzzle_flash_intensity = 3
|
|
var/muzzle_flash_range = 1.5
|
|
var/muzzle_flash_color_override
|
|
var/impact_light_intensity = 3
|
|
var/impact_light_range = 2
|
|
var/impact_light_color_override
|
|
|
|
//Homing
|
|
var/homing = FALSE
|
|
var/atom/homing_target
|
|
var/homing_turn_speed = 10 //Angle per tick.
|
|
var/homing_inaccuracy_min = 0 //in pixels for these. offsets are set once when setting target.
|
|
var/homing_inaccuracy_max = 0
|
|
var/homing_offset_x = 0
|
|
var/homing_offset_y = 0
|
|
|
|
var/damage = 10
|
|
var/damage_type = BRUTE //BRUTE, BURN, TOX, OXY are the only things that should be in here
|
|
|
|
///Defines what armor to use when it hits things. Must be set to bullet, laser, energy, or bomb
|
|
var/armor_flag = BULLET
|
|
///How much armor this projectile pierces.
|
|
var/armour_penetration = 0
|
|
///Whether or not our bullet lacks penetrative power, and is easily stopped by armor.
|
|
var/weak_against_armour = FALSE
|
|
var/projectile_type = /obj/projectile
|
|
var/range = 50 //This will de-increment every step. When 0, it will deletze the projectile.
|
|
var/decayedRange //stores original range
|
|
var/reflect_range_decrease = 5 //amount of original range that falls off when reflecting, so it doesn't go forever
|
|
var/reflectable = NONE // Can it be reflected or not?
|
|
|
|
// Status effects applied on hit
|
|
var/stun = 0 SECONDS
|
|
var/knockdown = 0 SECONDS
|
|
var/paralyze = 0 SECONDS
|
|
var/immobilize = 0 SECONDS
|
|
var/unconscious = 0 SECONDS
|
|
/// Seconds of blurry eyes applied on projectile hit
|
|
var/eyeblur = 0 SECONDS
|
|
/// Drowsiness applied on projectile hit
|
|
var/drowsy = 0 SECONDS
|
|
/// Jittering applied on projectile hit
|
|
var/jitter = 0 SECONDS
|
|
/// Extra stamina damage applied on projectile hit (in addition to the main damage)
|
|
var/stamina = 0
|
|
/// Stuttering applied on projectile hit
|
|
var/stutter = 0 SECONDS
|
|
/// Slurring applied on projectile hit
|
|
var/slur = 0 SECONDS
|
|
|
|
var/dismemberment = 0 //The higher the number, the greater the bonus to dismembering. 0 will not dismember at all.
|
|
var/catastropic_dismemberment = FALSE //If TRUE, this projectile deals its damage to the chest if it dismembers a limb.
|
|
var/impact_effect_type //what type of impact effect to show when hitting something
|
|
var/log_override = FALSE //is this type spammed enough to not log? (KAs)
|
|
/// If true, the projectile won't cause any logging. Used for hallucinations and shit.
|
|
var/do_not_log = FALSE
|
|
/// We ignore mobs with these factions.
|
|
var/list/ignored_factions
|
|
|
|
///If defined, on hit we create an item of this type then call hitby() on the hit target with this, mainly used for embedding items (bullets) in targets
|
|
var/shrapnel_type
|
|
///If we have a shrapnel_type defined, these embedding stats will be passed to the spawned shrapnel type, which will roll for embedding on the target
|
|
var/list/embedding
|
|
///If TRUE, hit mobs even if they're on the floor and not our target
|
|
var/hit_prone_targets = FALSE
|
|
///For what kind of brute wounds we're rolling for, if we're doing such a thing. Lasers obviously don't care since they do burn instead.
|
|
var/sharpness = NONE
|
|
///How much we want to drop damage per tile as it travels through the air
|
|
var/damage_falloff_tile
|
|
///How much we want to drop stamina damage (defined by the stamina variable) per tile as it travels through the air
|
|
var/stamina_falloff_tile
|
|
///How much we want to drop both wound_bonus and bare_wound_bonus (to a minimum of 0 for the latter) per tile, for falloff purposes
|
|
var/wound_falloff_tile
|
|
///How much we want to drop the embed_chance value, if we can embed, per tile, for falloff purposes
|
|
var/embed_falloff_tile
|
|
var/static/list/projectile_connections = list(
|
|
COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
|
|
COMSIG_ATOM_ATTACK_HAND = PROC_REF(attempt_parry),
|
|
)
|
|
//SKYRAT ADDITION START
|
|
/// If this should be able to hit the target even on direct firing when `ignored_factions` applies
|
|
var/ignore_direct_target = FALSE
|
|
//SKYRAT ADDITION END
|
|
|
|
/// If true directly targeted turfs can be hit
|
|
var/can_hit_turfs = FALSE
|
|
/// If this projectile has been parried before
|
|
var/parried = FALSE
|
|
|
|
/obj/projectile/Initialize(mapload)
|
|
. = ..()
|
|
decayedRange = range
|
|
if(embedding)
|
|
updateEmbedding()
|
|
AddElement(/datum/element/connect_loc, projectile_connections)
|
|
|
|
/obj/projectile/proc/Range()
|
|
range--
|
|
if(wound_bonus != CANT_WOUND)
|
|
wound_bonus += wound_falloff_tile
|
|
bare_wound_bonus = max(0, bare_wound_bonus + wound_falloff_tile)
|
|
if(embedding)
|
|
embedding["embed_chance"] += embed_falloff_tile
|
|
if(damage_falloff_tile && damage >= 0)
|
|
damage += damage_falloff_tile
|
|
if(stamina_falloff_tile && stamina >= 0)
|
|
stamina += stamina_falloff_tile
|
|
|
|
SEND_SIGNAL(src, COMSIG_PROJECTILE_RANGE)
|
|
if(range <= 0 && loc)
|
|
on_range()
|
|
|
|
if(damage_falloff_tile && damage <= 0 || stamina_falloff_tile && stamina <= 0)
|
|
on_range()
|
|
|
|
/obj/projectile/proc/on_range() //if we want there to be effects when they reach the end of their range
|
|
SEND_SIGNAL(src, COMSIG_PROJECTILE_RANGE_OUT)
|
|
qdel(src)
|
|
|
|
/// Returns the string form of the def_zone we have hit.
|
|
/mob/living/proc/check_hit_limb_zone_name(hit_zone)
|
|
if(has_limbs)
|
|
return hit_zone
|
|
|
|
/mob/living/carbon/check_hit_limb_zone_name(hit_zone)
|
|
if(get_bodypart(hit_zone))
|
|
return hit_zone
|
|
else //when a limb is missing the damage is actually passed to the chest
|
|
return BODY_ZONE_CHEST
|
|
|
|
/**
|
|
* Called when the projectile hits something
|
|
*
|
|
* By default parent call will always return [BULLET_ACT_HIT] (unless qdeleted)
|
|
* so it is save to assume a successful hit in children (though not necessarily successfully damaged - it could've been blocked)
|
|
*
|
|
* Arguments
|
|
* * target - thing hit
|
|
* * blocked - percentage of hit blocked (0 to 100)
|
|
* * pierce_hit - boolean, are we piercing through or regular hitting
|
|
*
|
|
* Returns
|
|
* * Returns [BULLET_ACT_HIT] if we hit something. Default return value.
|
|
* * Returns [BULLET_ACT_BLOCK] if we were hit but sustained no effects (blocked it). Note, Being "blocked" =/= "blocked is 100".
|
|
* * Returns [BULLET_ACT_FORCE_PIERCE] to have the projectile keep going instead of "hitting", as if we were not hit at all.
|
|
*/
|
|
/obj/projectile/proc/on_hit(atom/target, blocked = 0, pierce_hit)
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
|
|
// i know that this is probably more with wands and gun mods in mind, but it's a bit silly that the projectile on_hit signal doesn't ping the projectile itself.
|
|
// maybe we care what the projectile thinks! See about combining these via args some time when it's not 5AM
|
|
var/hit_limb_zone
|
|
if(isliving(target))
|
|
var/mob/living/L = target
|
|
hit_limb_zone = L.check_hit_limb_zone_name(def_zone)
|
|
if(fired_from)
|
|
SEND_SIGNAL(fired_from, COMSIG_PROJECTILE_ON_HIT, firer, target, Angle, hit_limb_zone, blocked)
|
|
SEND_SIGNAL(src, COMSIG_PROJECTILE_SELF_ON_HIT, firer, target, Angle, hit_limb_zone, blocked)
|
|
|
|
if(QDELETED(src)) // in case one of the above signals deleted the projectile for whatever reason
|
|
return BULLET_ACT_BLOCK
|
|
var/turf/target_turf = get_turf(target)
|
|
|
|
var/hitx
|
|
var/hity
|
|
if(target == original)
|
|
hitx = target.pixel_x + p_x - 16
|
|
hity = target.pixel_y + p_y - 16
|
|
else
|
|
hitx = target.pixel_x + rand(-8, 8)
|
|
hity = target.pixel_y + rand(-8, 8)
|
|
|
|
// SKYRAT EDIT ADDITION BEGIN - IMPACT SOUNDS
|
|
var/impact_sound
|
|
if(hitsound)
|
|
impact_sound = hitsound
|
|
else
|
|
impact_sound = target.impact_sound
|
|
get_sfx()
|
|
playsound(src, get_sfx_skyrat(impact_sound), vol_by_damage(), TRUE, -1)
|
|
// SKYRAT EDIT ADDITION END
|
|
|
|
if(damage > 0 && (damage_type == BRUTE || damage_type == BURN) && iswallturf(target_turf) && prob(75))
|
|
var/turf/closed/wall/target_wall = target_turf
|
|
if(impact_effect_type && !hitscan)
|
|
new impact_effect_type(target_wall, hitx, hity)
|
|
|
|
target_wall.add_dent(WALL_DENT_SHOT, hitx, hity)
|
|
|
|
return BULLET_ACT_HIT
|
|
|
|
if(!isliving(target))
|
|
if(impact_effect_type && !hitscan)
|
|
new impact_effect_type(target_turf, hitx, hity)
|
|
/* SKYRAT EDIT REMOVAL - IMPACT SOUNDS
|
|
if(isturf(target) && hitsound_wall)
|
|
var/volume = clamp(vol_by_damage() + 20, 0, 100)
|
|
if(suppressed)
|
|
volume = 5
|
|
playsound(loc, hitsound_wall, volume, TRUE, -1)
|
|
SKYRAT EDIT REMOVAL END */
|
|
return BULLET_ACT_HIT
|
|
|
|
var/mob/living/living_target = target
|
|
|
|
if(blocked != 100) // not completely blocked
|
|
var/obj/item/bodypart/hit_bodypart = living_target.get_bodypart(hit_limb_zone)
|
|
if (damage && damage_type == BRUTE)
|
|
if (living_target.blood_volume && (isnull(hit_bodypart) || hit_bodypart.can_bleed()))
|
|
var/splatter_dir = dir
|
|
if(starting)
|
|
splatter_dir = get_dir(starting, target_turf)
|
|
if(isalien(living_target))
|
|
new /obj/effect/temp_visual/dir_setting/bloodsplatter/xenosplatter(target_turf, splatter_dir)
|
|
else
|
|
new /obj/effect/temp_visual/dir_setting/bloodsplatter(target_turf, splatter_dir)
|
|
if(prob(33))
|
|
living_target.add_splatter_floor(target_turf)
|
|
else if (hit_bodypart?.biological_state & (BIO_METAL|BIO_WIRED))
|
|
var/random_damage_mult = RANDOM_DECIMAL(0.85, 1.15) // SOMETIMES you can get more or less sparks
|
|
var/damage_dealt = ((damage / (1 - (blocked / 100))) * random_damage_mult)
|
|
|
|
var/spark_amount = round((damage_dealt / PROJECTILE_DAMAGE_PER_ROBOTIC_SPARK))
|
|
if (spark_amount > 0)
|
|
do_sparks(spark_amount, FALSE, living_target)
|
|
|
|
else if(impact_effect_type && !hitscan)
|
|
new impact_effect_type(target_turf, hitx, hity)
|
|
|
|
var/organ_hit_text = ""
|
|
if(hit_limb_zone)
|
|
organ_hit_text = " in \the [parse_zone(hit_limb_zone)]"
|
|
if(suppressed == SUPPRESSED_VERY)
|
|
//playsound(loc, hitsound, 5, TRUE, -1) SKYRAT EDIT REMOVAL - IMPACT SOUNDS
|
|
else if(suppressed)
|
|
//playsound(loc, hitsound, 5, TRUE, -1) SKYRAT EDIT REMOVAL - IMPACT SOUNDS
|
|
to_chat(living_target, span_userdanger("You're shot by \a [src][organ_hit_text]!"))
|
|
else
|
|
/* SKYRAT EDIT REMOVAL - IMPACT SOUNDS
|
|
if(hitsound)
|
|
var/volume = vol_by_damage()
|
|
playsound(src, hitsound, volume, TRUE, -1)
|
|
SKYRAT EDIT REMOVAL END */
|
|
living_target.visible_message(span_danger("[living_target] is hit by \a [src][organ_hit_text]!"), \
|
|
span_userdanger("You're hit by \a [src][organ_hit_text]!"), null, COMBAT_MESSAGE_RANGE)
|
|
if(living_target.is_blind())
|
|
to_chat(living_target, span_userdanger("You feel something hit you[organ_hit_text]!"))
|
|
|
|
var/reagent_note
|
|
if(reagents?.reagent_list)
|
|
reagent_note = "REAGENTS: [pretty_string_from_reagent_list(reagents.reagent_list)]"
|
|
|
|
if(ismob(firer) && !do_not_log)
|
|
log_combat(firer, living_target, "shot", src, reagent_note)
|
|
return BULLET_ACT_HIT
|
|
|
|
if(isvehicle(firer))
|
|
var/obj/vehicle/firing_vehicle = firer
|
|
|
|
var/list/logging_mobs = firing_vehicle.return_controllers_with_flag(VEHICLE_CONTROL_EQUIPMENT)
|
|
if(!LAZYLEN(logging_mobs))
|
|
logging_mobs = firing_vehicle.return_drivers()
|
|
if(!do_not_log)
|
|
for(var/mob/logged_mob as anything in logging_mobs)
|
|
log_combat(logged_mob, living_target, "shot", src, "from inside [firing_vehicle][logging_mobs.len > 1 ? " with multiple occupants" : null][reagent_note ? " and contained [reagent_note]" : null]")
|
|
return BULLET_ACT_HIT
|
|
if(!do_not_log)
|
|
living_target.log_message("has been shot by [firer] with [src][reagent_note ? " containing [reagent_note]" : null]", LOG_ATTACK, color="orange")
|
|
return BULLET_ACT_HIT
|
|
|
|
/obj/projectile/proc/vol_by_damage()
|
|
if(src.damage)
|
|
return clamp((src.damage) * 0.67, 30, 100)// Multiply projectile damage by 0.67, then CLAMP the value between 30 and 100
|
|
else
|
|
return 50 //if the projectile doesn't do damage, play its hitsound at 50% volume
|
|
|
|
/obj/projectile/proc/on_ricochet(atom/A)
|
|
if(!ricochet_auto_aim_angle || !ricochet_auto_aim_range)
|
|
return
|
|
|
|
var/mob/living/unlucky_sob
|
|
var/best_angle = ricochet_auto_aim_angle
|
|
if(firer && HAS_TRAIT(firer, TRAIT_NICE_SHOT))
|
|
best_angle += NICE_SHOT_RICOCHET_BONUS
|
|
for(var/mob/living/L in range(ricochet_auto_aim_range, src.loc))
|
|
if(L.stat == DEAD || !is_in_sight(src, L) || (!ricochet_shoots_firer && L == firer))
|
|
continue
|
|
var/our_angle = abs(closer_angle_difference(Angle, get_angle(src.loc, L.loc)))
|
|
if(our_angle < best_angle)
|
|
best_angle = our_angle
|
|
unlucky_sob = L
|
|
|
|
if(unlucky_sob)
|
|
set_angle(get_angle(src, unlucky_sob.loc))
|
|
|
|
/obj/projectile/proc/store_hitscan_collision(datum/point/point_cache)
|
|
beam_segments[beam_index] = point_cache
|
|
beam_index = point_cache
|
|
beam_segments[beam_index] = null
|
|
|
|
/obj/projectile/Bump(atom/A)
|
|
SEND_SIGNAL(src, COMSIG_MOVABLE_BUMP, A)
|
|
if(!can_hit_target(A, A == original, TRUE, TRUE))
|
|
return
|
|
Impact(A)
|
|
|
|
/// Signal proc for when a mob attempts to attack this projectile or the turf it's on with an empty hand.
|
|
/obj/projectile/proc/attempt_parry(datum/source, mob/user, list/modifiers)
|
|
SIGNAL_HANDLER
|
|
|
|
if(parried)
|
|
return FALSE
|
|
|
|
if(SEND_SIGNAL(user, COMSIG_LIVING_PROJECTILE_PARRYING, src) & ALLOW_PARRY)
|
|
on_parry(user, modifiers)
|
|
return TRUE
|
|
|
|
return FALSE
|
|
|
|
|
|
/// Called when a mob with PARRY_TRAIT clicks on this projectile or the tile its on, reflecting the projectile within 7 degrees and increasing the bullet's stats.
|
|
/obj/projectile/proc/on_parry(mob/user, list/modifiers)
|
|
if(SEND_SIGNAL(user, COMSIG_LIVING_PROJECTILE_PARRIED, src) & INTERCEPT_PARRY_EFFECTS)
|
|
return
|
|
|
|
parried = TRUE
|
|
set_angle(dir2angle(user.dir) + rand(-3, 3))
|
|
firer = user
|
|
speed *= 0.8 // Go 20% faster when parried
|
|
damage *= 1.15 // And do 15% more damage
|
|
add_atom_colour(COLOR_RED_LIGHT, TEMPORARY_COLOUR_PRIORITY)
|
|
|
|
|
|
/**
|
|
* Called when the projectile hits something
|
|
* This can either be from it bumping something,
|
|
* or it passing over a turf/being crossed and scanning that there is infact
|
|
* a valid target it needs to hit.
|
|
* This target isn't however necessarily WHAT it hits
|
|
* that is determined by process_hit and select_target.
|
|
*
|
|
* Furthermore, this proc shouldn't check can_hit_target - this should only be called if can hit target is already checked.
|
|
* Also, we select_target to find what to process_hit first.
|
|
*/
|
|
/obj/projectile/proc/Impact(atom/A)
|
|
if(!trajectory)
|
|
qdel(src)
|
|
return FALSE
|
|
if(impacted[A.weak_reference]) // NEVER doublehit
|
|
return FALSE
|
|
var/datum/point/point_cache = trajectory.copy_to()
|
|
var/turf/T = get_turf(A)
|
|
if(ricochets < ricochets_max && check_ricochet_flag(A) && check_ricochet(A))
|
|
ricochets++
|
|
if(A.handle_ricochet(src))
|
|
on_ricochet(A)
|
|
impacted = list() // Shoot a x-ray laser at a pair of mirrors I dare you
|
|
ignore_source_check = TRUE // Firer is no longer immune
|
|
decayedRange = max(0, decayedRange - reflect_range_decrease)
|
|
ricochet_chance *= ricochet_decay_chance
|
|
damage *= ricochet_decay_damage
|
|
stamina *= ricochet_decay_damage
|
|
range = decayedRange
|
|
if(hitscan)
|
|
store_hitscan_collision(point_cache)
|
|
return TRUE
|
|
|
|
if(!HAS_TRAIT(src, TRAIT_ALWAYS_HIT_ZONE))
|
|
var/distance = get_dist(T, starting) // Get the distance between the turf shot from and the mob we hit and use that for the calculations.
|
|
def_zone = ran_zone(def_zone, max(100-(7*distance), 5)) //Lower accurancy/longer range tradeoff. 7 is a balanced number to use.
|
|
|
|
return process_hit(T, select_target(T, A, A), A) // SELECT TARGET FIRST!
|
|
|
|
/**
|
|
* The primary workhorse proc of projectile impacts.
|
|
* This is a RECURSIVE call - process_hit is called on the first selected target, and then repeatedly called if the projectile still hasn't been deleted.
|
|
*
|
|
* Order of operations:
|
|
* 1. Checks if we are deleted, or if we're somehow trying to hit a null, in which case, bail out
|
|
* 2. Adds the thing we're hitting to impacted so we can make sure we don't doublehit
|
|
* 3. Checks piercing - stores this.
|
|
* Afterwards:
|
|
* Hit and delete, hit without deleting and pass through, pass through without hitting, or delete without hitting depending on result
|
|
* If we're going through without hitting, find something else to hit if possible and recurse, set unstoppable movement to true
|
|
* If we're deleting without hitting, delete and return
|
|
* Otherwise, send signal of COMSIG_PROJECTILE_PREHIT to target
|
|
* Then, hit, deleting ourselves if necessary.
|
|
* @params
|
|
* T - Turf we're on/supposedly hitting
|
|
* target - target we're hitting
|
|
* bumped - target we originally bumped. it's here to ensure that if something blocks our projectile by means of Cross() failure, we hit it
|
|
* even if it is not dense.
|
|
* hit_something - only should be set by recursive calling by this proc - tracks if we hit something already
|
|
*
|
|
* Returns if we hit something.
|
|
*/
|
|
/obj/projectile/proc/process_hit(turf/T, atom/target, atom/bumped, hit_something = FALSE)
|
|
// 1.
|
|
if(QDELETED(src) || !T || !target)
|
|
return
|
|
// 2.
|
|
impacted[WEAKREF(target)] = TRUE //hash lookup > in for performance in hit-checking
|
|
// 3.
|
|
var/mode = prehit_pierce(target)
|
|
if(mode == PROJECTILE_DELETE_WITHOUT_HITTING)
|
|
qdel(src)
|
|
return hit_something
|
|
else if(mode == PROJECTILE_PIERCE_PHASE)
|
|
if(!(movement_type & PHASING))
|
|
temporary_unstoppable_movement = TRUE
|
|
movement_type |= PHASING
|
|
return process_hit(T, select_target(T, target, bumped), bumped, hit_something) // try to hit something else
|
|
// at this point we are going to hit the thing
|
|
// in which case send signal to it
|
|
if (SEND_SIGNAL(target, COMSIG_PROJECTILE_PREHIT, args, src) & PROJECTILE_INTERRUPT_HIT)
|
|
qdel(src)
|
|
return BULLET_ACT_BLOCK
|
|
if(mode == PROJECTILE_PIERCE_HIT)
|
|
++pierces
|
|
hit_something = TRUE
|
|
var/result = target.bullet_act(src, def_zone, mode == PROJECTILE_PIERCE_HIT)
|
|
if((result == BULLET_ACT_FORCE_PIERCE) || (mode == PROJECTILE_PIERCE_HIT))
|
|
if(!(movement_type & PHASING))
|
|
temporary_unstoppable_movement = TRUE
|
|
movement_type |= PHASING
|
|
return process_hit(T, select_target(T, target, bumped), bumped, TRUE)
|
|
qdel(src)
|
|
return hit_something
|
|
|
|
/**
|
|
* Selects a target to hit from a turf
|
|
*
|
|
* @params
|
|
* T - The turf
|
|
* target - The "preferred" atom to hit, usually what we Bumped() first.
|
|
* bumped - used to track if something is the reason we impacted in the first place.
|
|
* If set, this atom is always treated as dense by can_hit_target.
|
|
*
|
|
* Priority:
|
|
* 0. Anything that is already in impacted is ignored no matter what. Furthermore, in any bracket, if the target atom parameter is in it, that's hit first.
|
|
* Furthermore, can_hit_target is always checked. This (entire proc) is PERFORMANCE OVERHEAD!! But, it shouldn't be ""too"" bad and I frankly don't have a better *generic non snowflakey* way that I can think of right now at 3 AM.
|
|
* FURTHERMORE, mobs/objs have a density check from can_hit_target - to hit non dense objects over a turf, you must click on them, same for mobs that usually wouldn't get hit.
|
|
* 1. Special check on what we bumped to see if it's a border object that intercepts hitting anything behind it
|
|
* 2. The thing originally aimed at/clicked on
|
|
* 3. Mobs - picks lowest buckled mob to prevent scarp piggybacking memes
|
|
* 4. Objs
|
|
* 5. Turf
|
|
* 6. Nothing
|
|
*/
|
|
/obj/projectile/proc/select_target(turf/our_turf, atom/target, atom/bumped)
|
|
// 1. special bumped border object check
|
|
if((bumped?.flags_1 & ON_BORDER_1) && can_hit_target(bumped, original == bumped, FALSE, TRUE))
|
|
return bumped
|
|
// 2. original
|
|
if(can_hit_target(original, TRUE, FALSE, original == bumped))
|
|
return original
|
|
var/list/atom/considering = list() // let's define this ONCE
|
|
// 3. mobs
|
|
for(var/mob/living/iter_possible_target in our_turf)
|
|
if(can_hit_target(iter_possible_target, iter_possible_target == original, TRUE, iter_possible_target == bumped))
|
|
considering |= iter_possible_target
|
|
if(length(considering))
|
|
return pick(considering)
|
|
// 4. objs and other dense things
|
|
for(var/i in our_turf)
|
|
if(can_hit_target(i, i == original, TRUE, i == bumped))
|
|
considering += i
|
|
if(length(considering))
|
|
return pick(considering)
|
|
// 5. turf
|
|
if(can_hit_target(our_turf, our_turf == original, TRUE, our_turf == bumped))
|
|
return our_turf
|
|
// 6. nothing
|
|
// (returns null)
|
|
|
|
//Returns true if the target atom is on our current turf and above the right layer
|
|
//If direct target is true it's the originally clicked target.
|
|
/obj/projectile/proc/can_hit_target(atom/target, direct_target = FALSE, ignore_loc = FALSE, cross_failed = FALSE)
|
|
if(QDELETED(target) || impacted[target.weak_reference])
|
|
return FALSE
|
|
if(!ignore_loc && (loc != target.loc) && !(can_hit_turfs && direct_target && loc == target))
|
|
return FALSE
|
|
// if pass_flags match, pass through entirely - unless direct target is set.
|
|
if((target.pass_flags_self & pass_flags) && !direct_target)
|
|
return FALSE
|
|
if(HAS_TRAIT(target, TRAIT_UNHITTABLE_BY_PROJECTILES))
|
|
return FALSE
|
|
if(!ignore_source_check && firer)
|
|
var/mob/M = firer
|
|
if((target == firer) || ((target == firer.loc) && ismecha(firer.loc)) || (target in firer.buckled_mobs) || (istype(M) && (M.buckled == target)))
|
|
return FALSE
|
|
if(ignored_factions?.len && ismob(target) && (!direct_target || ignore_direct_target)) //SKYRAT EDIT: ignore_direct_target
|
|
var/mob/target_mob = target
|
|
if(faction_check(target_mob.faction, ignored_factions))
|
|
return FALSE
|
|
if(target.density || cross_failed) //This thing blocks projectiles, hit it regardless of layer/mob stuns/etc.
|
|
return TRUE
|
|
if(!isliving(target))
|
|
if(isturf(target)) // non dense turfs
|
|
return can_hit_turfs && direct_target
|
|
if(target.layer < hit_threshhold)
|
|
return FALSE
|
|
else if(!direct_target) // non dense objects do not get hit unless specifically clicked
|
|
return FALSE
|
|
else
|
|
var/mob/living/living_target = target
|
|
if(direct_target)
|
|
return TRUE
|
|
if(living_target.stat == DEAD)
|
|
return FALSE
|
|
if(HAS_TRAIT(living_target, TRAIT_IMMOBILIZED) && HAS_TRAIT(living_target, TRAIT_FLOORED) && HAS_TRAIT(living_target, TRAIT_HANDS_BLOCKED))
|
|
return FALSE
|
|
if(!hit_prone_targets)
|
|
var/mob/living/buckled_to = living_target.lowest_buckled_mob()
|
|
if(!buckled_to.density) // Will just be us if we're not buckled to another mob
|
|
return FALSE
|
|
if(living_target.body_position != LYING_DOWN)
|
|
return TRUE
|
|
return TRUE
|
|
|
|
/**
|
|
* Scan if we should hit something and hit it if we need to
|
|
* The difference between this and handling in Impact is
|
|
* In this we strictly check if we need to Impact() something in specific
|
|
* If we do, we do
|
|
* We don't even check if it got hit already - Impact() does that
|
|
* In impact there's more code for selecting WHAT to hit
|
|
* So this proc is more of checking if we should hit something at all BY having an atom cross us.
|
|
*/
|
|
/obj/projectile/proc/scan_crossed_hit(atom/movable/A)
|
|
if(can_hit_target(A, direct_target = (A == original)))
|
|
Impact(A)
|
|
|
|
/**
|
|
* Scans if we should hit something on the turf we just moved to if we haven't already
|
|
*
|
|
* This proc is a little high in overhead but allows us to not snowflake CanPass in living and other things.
|
|
*/
|
|
/obj/projectile/proc/scan_moved_turf()
|
|
// Optimally, we scan: mobs --> objs --> turf for impact
|
|
// but, overhead is a thing and 2 for loops every time it moves is a no-go.
|
|
// realistically, since we already do select_target in impact, we can not do that
|
|
// and hope projectiles get refactored again in the future to have a less stupid impact detection system
|
|
// that hopefully won't also involve a ton of overhead
|
|
if(can_hit_target(original, TRUE, FALSE))
|
|
Impact(original) // try to hit thing clicked on
|
|
// else, try to hit mobs
|
|
else // because if we impacted original and pierced we'll already have select target'd and hit everything else we should be hitting
|
|
for(var/mob/M in loc) // so I guess we're STILL doing a for loop of mobs because living movement would otherwise have snowflake code for projectile CanPass
|
|
// so the snowflake vs performance is pretty arguable here
|
|
if(can_hit_target(M, M == original, TRUE))
|
|
Impact(M)
|
|
break
|
|
|
|
/**
|
|
* Projectile crossed: When something enters a projectile's tile, make sure the projectile hits it if it should be hitting it.
|
|
*/
|
|
/obj/projectile/proc/on_entered(datum/source, atom/movable/AM)
|
|
SIGNAL_HANDLER
|
|
scan_crossed_hit(AM)
|
|
|
|
/**
|
|
* Projectile can pass through
|
|
* Used to not even attempt to Bump() or fail to Cross() anything we already hit.
|
|
*/
|
|
/obj/projectile/CanPassThrough(atom/blocker, movement_dir, blocker_opinion)
|
|
return ..() || impacted[blocker.weak_reference]
|
|
|
|
/**
|
|
* Projectile moved:
|
|
*
|
|
* If not fired yet, do not do anything. Else,
|
|
*
|
|
* If temporary unstoppable movement used for piercing through things we already hit (impacted list) is set, unset it.
|
|
* Scan turf we're now in for anything we can/should hit. This is useful for hitting non dense objects the user
|
|
* directly clicks on, as well as for PHASING projectiles to be able to hit things at all as they don't ever Bump().
|
|
*/
|
|
/obj/projectile/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
|
|
. = ..()
|
|
if(!fired)
|
|
return
|
|
if(temporary_unstoppable_movement)
|
|
temporary_unstoppable_movement = FALSE
|
|
movement_type &= ~PHASING
|
|
scan_moved_turf() //mostly used for making sure we can hit a non-dense object the user directly clicked on, and for penetrating projectiles that don't bump
|
|
|
|
/**
|
|
* Checks if we should pierce something.
|
|
*
|
|
* NOT meant to be a pure proc, since this replaces prehit() which was used to do things.
|
|
* Return PROJECTILE_DELETE_WITHOUT_HITTING to delete projectile without hitting at all!
|
|
*/
|
|
/obj/projectile/proc/prehit_pierce(atom/A)
|
|
if((projectile_phasing & A.pass_flags_self) && (phasing_ignore_direct_target || original != A))
|
|
return PROJECTILE_PIERCE_PHASE
|
|
if(projectile_piercing & A.pass_flags_self)
|
|
return PROJECTILE_PIERCE_HIT
|
|
if(ismovable(A))
|
|
var/atom/movable/AM = A
|
|
if(AM.throwing)
|
|
return (projectile_phasing & LETPASSTHROW) ? PROJECTILE_PIERCE_PHASE : ((projectile_piercing & LETPASSTHROW)? PROJECTILE_PIERCE_HIT : PROJECTILE_PIERCE_NONE)
|
|
return PROJECTILE_PIERCE_NONE
|
|
|
|
/obj/projectile/proc/check_ricochet(atom/A)
|
|
var/chance = ricochet_chance * A.receive_ricochet_chance_mod
|
|
if(firer && HAS_TRAIT(firer, TRAIT_NICE_SHOT))
|
|
chance += NICE_SHOT_RICOCHET_BONUS
|
|
if(ricochets < min_ricochets || prob(chance))
|
|
return TRUE
|
|
return FALSE
|
|
|
|
/obj/projectile/proc/check_ricochet_flag(atom/A)
|
|
if((armor_flag in list(ENERGY, LASER)) && (A.flags_ricochet & RICOCHET_SHINY))
|
|
return TRUE
|
|
|
|
if((armor_flag in list(BOMB, BULLET)) && (A.flags_ricochet & RICOCHET_HARD))
|
|
return TRUE
|
|
|
|
return FALSE
|
|
|
|
/obj/projectile/proc/return_predicted_turf_after_moves(moves, forced_angle) //I say predicted because there's no telling that the projectile won't change direction/location in flight.
|
|
if(!trajectory && isnull(forced_angle) && isnull(Angle))
|
|
return FALSE
|
|
var/datum/point/vector/current = trajectory
|
|
if(!current)
|
|
var/turf/T = get_turf(src)
|
|
current = new(T.x, T.y, T.z, pixel_x, pixel_y, isnull(forced_angle)? Angle : forced_angle, SSprojectiles.global_pixel_speed)
|
|
var/datum/point/vector/v = current.return_vector_after_increments(moves * SSprojectiles.global_iterations_per_move)
|
|
return v.return_turf()
|
|
|
|
/obj/projectile/proc/return_pathing_turfs_in_moves(moves, forced_angle)
|
|
var/turf/current = get_turf(src)
|
|
var/turf/ending = return_predicted_turf_after_moves(moves, forced_angle)
|
|
return get_line(current, ending)
|
|
|
|
/obj/projectile/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
|
|
return TRUE //Bullets don't drift in space
|
|
|
|
/obj/projectile/process()
|
|
last_process = world.time
|
|
if(!loc || !fired || !trajectory)
|
|
fired = FALSE
|
|
return PROCESS_KILL
|
|
if(paused || !isturf(loc))
|
|
last_projectile_move += world.time - last_process //Compensates for pausing, so it doesn't become a hitscan projectile when unpaused from charged up ticks.
|
|
return
|
|
var/elapsed_time_deciseconds = (world.time - last_projectile_move) + time_offset
|
|
time_offset = 0
|
|
var/required_moves = speed > 0? FLOOR(elapsed_time_deciseconds / speed, 1) : MOVES_HITSCAN //Would be better if a 0 speed made hitscan but everyone hates those so I can't make it a universal system :<
|
|
if(required_moves == MOVES_HITSCAN)
|
|
required_moves = SSprojectiles.global_max_tick_moves
|
|
else
|
|
if(required_moves > SSprojectiles.global_max_tick_moves)
|
|
var/overrun = required_moves - SSprojectiles.global_max_tick_moves
|
|
required_moves = SSprojectiles.global_max_tick_moves
|
|
time_offset += overrun * speed
|
|
time_offset += MODULUS(elapsed_time_deciseconds, speed)
|
|
|
|
for(var/i in 1 to required_moves)
|
|
pixel_move(pixel_speed_multiplier, FALSE)
|
|
|
|
/obj/projectile/proc/fire(angle, atom/direct_target)
|
|
LAZYINITLIST(impacted)
|
|
if(fired_from)
|
|
SEND_SIGNAL(fired_from, COMSIG_PROJECTILE_BEFORE_FIRE, src, original)
|
|
if(firer)
|
|
SEND_SIGNAL(firer, COMSIG_PROJECTILE_FIRER_BEFORE_FIRE, src, fired_from, original)
|
|
if(!log_override && firer && original && !do_not_log)
|
|
log_combat(firer, original, "fired at", src, "from [get_area_name(src, TRUE)]")
|
|
//note: mecha projectile logging is handled in /obj/item/mecha_parts/mecha_equipment/weapon/action(). try to keep these messages roughly the sameish just for consistency's sake.
|
|
if(direct_target && (get_dist(direct_target, get_turf(fired_from)) <= 1)) // point blank shots // SKYRAT EDIT - ORIGINAL: if(direct_target && (get_dist(direct_target, get_turf(src)) <= 1))
|
|
process_hit(get_turf(direct_target), direct_target)
|
|
if(QDELETED(src))
|
|
return
|
|
if(isnum(angle))
|
|
set_angle(angle)
|
|
if(spread)
|
|
set_angle(Angle + ((rand() - 0.5) * spread))
|
|
var/turf/starting = get_turf(src)
|
|
if(isnull(Angle)) //Try to resolve through offsets if there's no angle set.
|
|
if(isnull(xo) || isnull(yo))
|
|
stack_trace("WARNING: Projectile [type] deleted due to being unable to resolve a target after angle was null!")
|
|
qdel(src)
|
|
return
|
|
var/turf/target = locate(clamp(starting + xo, 1, world.maxx), clamp(starting + yo, 1, world.maxy), starting.z)
|
|
set_angle(get_angle(src, target))
|
|
original_angle = Angle
|
|
if(!nondirectional_sprite)
|
|
transform = transform.Turn(Angle)
|
|
trajectory_ignore_forcemove = TRUE
|
|
forceMove(starting)
|
|
trajectory_ignore_forcemove = FALSE
|
|
trajectory = new(starting.x, starting.y, starting.z, pixel_x, pixel_y, Angle, SSprojectiles.global_pixel_speed)
|
|
last_projectile_move = world.time
|
|
fired = TRUE
|
|
play_fov_effect(starting, 6, "gunfire", dir = NORTH, angle = Angle)
|
|
SEND_SIGNAL(src, COMSIG_PROJECTILE_FIRE)
|
|
RegisterSignal(src, COMSIG_ATOM_ATTACK_HAND, PROC_REF(attempt_parry))
|
|
if(hitscan)
|
|
process_hitscan()
|
|
if(!(datum_flags & DF_ISPROCESSING))
|
|
START_PROCESSING(SSprojectiles, src)
|
|
pixel_move(pixel_speed_multiplier, FALSE) //move it now!
|
|
|
|
/obj/projectile/proc/set_angle(new_angle) //wrapper for overrides.
|
|
if(!nondirectional_sprite)
|
|
transform = transform.TurnTo(Angle, new_angle)
|
|
Angle = new_angle
|
|
if(trajectory)
|
|
trajectory.set_angle(new_angle)
|
|
if(fired && hitscan && isloc(loc) && (loc != last_angle_set_hitscan_store))
|
|
last_angle_set_hitscan_store = loc
|
|
var/datum/point/point_cache = new (src)
|
|
point_cache = trajectory.copy_to()
|
|
store_hitscan_collision(point_cache)
|
|
return TRUE
|
|
|
|
/// Same as set_angle, but the reflection continues from the center of the object that reflects it instead of the side
|
|
/obj/projectile/proc/set_angle_centered(new_angle)
|
|
if(!nondirectional_sprite)
|
|
transform = transform.TurnTo(Angle, new_angle)
|
|
Angle = new_angle
|
|
if(trajectory)
|
|
trajectory.set_angle(new_angle)
|
|
|
|
var/list/coordinates = trajectory.return_coordinates()
|
|
trajectory.set_location(coordinates[1], coordinates[2], coordinates[3]) // Sets the trajectory to the center of the tile it bounced at
|
|
|
|
if(fired && hitscan && isloc(loc) && (loc != last_angle_set_hitscan_store)) // Handles hitscan projectiles
|
|
last_angle_set_hitscan_store = loc
|
|
var/datum/point/point_cache = new (src)
|
|
point_cache.initialize_location(coordinates[1], coordinates[2], coordinates[3]) // Take the center of the hitscan collision tile
|
|
store_hitscan_collision(point_cache)
|
|
return TRUE
|
|
|
|
|
|
|
|
/obj/projectile/forceMove(atom/target)
|
|
if(!isloc(target) || !isloc(loc) || !z)
|
|
return ..()
|
|
var/zc = target.z != z
|
|
var/old = loc
|
|
if(zc)
|
|
before_z_change(old, target)
|
|
. = ..()
|
|
if(QDELETED(src)) // we coulda bumped something
|
|
return
|
|
if(trajectory && !trajectory_ignore_forcemove && isturf(target))
|
|
if(hitscan)
|
|
finalize_hitscan_and_generate_tracers(FALSE)
|
|
trajectory.initialize_location(target.x, target.y, target.z, 0, 0)
|
|
if(hitscan)
|
|
record_hitscan_start(RETURN_PRECISE_POINT(src))
|
|
if(zc)
|
|
after_z_change(old, target)
|
|
|
|
/obj/projectile/proc/after_z_change(atom/olcloc, atom/newloc)
|
|
|
|
/obj/projectile/proc/before_z_change(atom/oldloc, atom/newloc)
|
|
|
|
/obj/projectile/vv_edit_var(var_name, var_value)
|
|
switch(var_name)
|
|
if(NAMEOF(src, Angle))
|
|
set_angle(var_value)
|
|
return TRUE
|
|
else
|
|
return ..()
|
|
|
|
/obj/projectile/proc/set_pixel_speed(new_speed)
|
|
if(trajectory)
|
|
trajectory.set_speed(new_speed)
|
|
return TRUE
|
|
return FALSE
|
|
|
|
/obj/projectile/proc/record_hitscan_start(datum/point/point_cache)
|
|
if(point_cache)
|
|
beam_segments = list()
|
|
beam_index = point_cache
|
|
beam_segments[beam_index] = null //record start.
|
|
|
|
/obj/projectile/proc/process_hitscan()
|
|
var/safety = range * 10
|
|
record_hitscan_start(RETURN_POINT_VECTOR_INCREMENT(src, Angle, MUZZLE_EFFECT_PIXEL_INCREMENT, 1))
|
|
while(loc && !QDELETED(src))
|
|
if(paused)
|
|
stoplag(1)
|
|
continue
|
|
if(safety-- <= 0)
|
|
if(loc)
|
|
Bump(loc)
|
|
if(!QDELETED(src))
|
|
qdel(src)
|
|
return //Kill!
|
|
pixel_move(1, TRUE)
|
|
|
|
/obj/projectile/proc/pixel_move(trajectory_multiplier, hitscanning = FALSE)
|
|
if(!loc || !trajectory)
|
|
return
|
|
last_projectile_move = world.time
|
|
if(homing)
|
|
process_homing()
|
|
var/forcemoved = FALSE
|
|
for(var/i in 1 to SSprojectiles.global_iterations_per_move)
|
|
if(QDELETED(src))
|
|
return
|
|
trajectory.increment(trajectory_multiplier)
|
|
var/turf/T = trajectory.return_turf()
|
|
if(!istype(T))
|
|
qdel(src)
|
|
return
|
|
if(T.z != loc.z)
|
|
var/old = loc
|
|
before_z_change(loc, T)
|
|
trajectory_ignore_forcemove = TRUE
|
|
forceMove(T)
|
|
trajectory_ignore_forcemove = FALSE
|
|
after_z_change(old, loc)
|
|
if(!hitscanning)
|
|
pixel_x = trajectory.return_px()
|
|
pixel_y = trajectory.return_py()
|
|
forcemoved = TRUE
|
|
hitscan_last = loc
|
|
else if(T != loc)
|
|
step_towards(src, T)
|
|
hitscan_last = loc
|
|
if(QDELETED(src)) //deleted on last move
|
|
return
|
|
if(!hitscanning && !forcemoved)
|
|
pixel_x = trajectory.return_px() - trajectory.mpx * trajectory_multiplier * SSprojectiles.global_iterations_per_move
|
|
pixel_y = trajectory.return_py() - trajectory.mpy * trajectory_multiplier * SSprojectiles.global_iterations_per_move
|
|
animate(src, pixel_x = trajectory.return_px(), pixel_y = trajectory.return_py(), time = 1, flags = ANIMATION_END_NOW)
|
|
Range()
|
|
|
|
/obj/projectile/proc/process_homing() //may need speeding up in the future performance wise.
|
|
if(!homing_target)
|
|
return FALSE
|
|
var/datum/point/PT = RETURN_PRECISE_POINT(homing_target)
|
|
PT.x += clamp(homing_offset_x, 1, world.maxx)
|
|
PT.y += clamp(homing_offset_y, 1, world.maxy)
|
|
var/angle = closer_angle_difference(Angle, angle_between_points(RETURN_PRECISE_POINT(src), PT))
|
|
set_angle(Angle + clamp(angle, -homing_turn_speed, homing_turn_speed))
|
|
|
|
/obj/projectile/proc/set_homing_target(atom/A)
|
|
if(!A || (!isturf(A) && !isturf(A.loc)))
|
|
return FALSE
|
|
homing = TRUE
|
|
homing_target = A
|
|
homing_offset_x = rand(homing_inaccuracy_min, homing_inaccuracy_max)
|
|
homing_offset_y = rand(homing_inaccuracy_min, homing_inaccuracy_max)
|
|
if(prob(50))
|
|
homing_offset_x = -homing_offset_x
|
|
if(prob(50))
|
|
homing_offset_y = -homing_offset_y
|
|
|
|
/**
|
|
* Aims the projectile at a target.
|
|
*
|
|
* Must be passed at least one of a target or a list of click parameters.
|
|
* If only passed the click modifiers the source atom must be a mob with a client.
|
|
*
|
|
* Arguments:
|
|
* - [target][/atom]: (Optional) The thing that the projectile will be aimed at.
|
|
* - [source][/atom]: The initial location of the projectile or the thing firing it.
|
|
* - [modifiers][/list]: (Optional) A list of click parameters to apply to this operation.
|
|
* - deviation: (Optional) How the trajectory should deviate from the target in degrees.
|
|
* - //Spread is FORCED!
|
|
*/
|
|
/obj/projectile/proc/preparePixelProjectile(atom/target, atom/source, list/modifiers = null, deviation = 0)
|
|
if(!(isnull(modifiers) || islist(modifiers)))
|
|
stack_trace("WARNING: Projectile [type] fired with non-list modifiers, likely was passed click params.")
|
|
modifiers = null
|
|
|
|
var/turf/source_loc = get_turf(source)
|
|
var/turf/target_loc = get_turf(target)
|
|
if(isnull(source_loc))
|
|
stack_trace("WARNING: Projectile [type] fired from nullspace.")
|
|
qdel(src)
|
|
return FALSE
|
|
|
|
trajectory_ignore_forcemove = TRUE
|
|
forceMove(source_loc)
|
|
trajectory_ignore_forcemove = FALSE
|
|
|
|
starting = source_loc
|
|
pixel_x = source.pixel_x
|
|
pixel_y = source.pixel_y
|
|
original = target
|
|
if(length(modifiers))
|
|
var/list/calculated = calculate_projectile_angle_and_pixel_offsets(source, target_loc && target, modifiers)
|
|
|
|
p_x = calculated[2]
|
|
p_y = calculated[3]
|
|
set_angle(calculated[1] + deviation)
|
|
return TRUE
|
|
|
|
if(target_loc)
|
|
yo = target_loc.y - source_loc.y
|
|
xo = target_loc.x - source_loc.x
|
|
set_angle(get_angle(src, target_loc) + deviation)
|
|
return TRUE
|
|
|
|
stack_trace("WARNING: Projectile [type] fired without a target or mouse parameters to aim with.")
|
|
qdel(src)
|
|
return FALSE
|
|
|
|
/**
|
|
* Calculates the pixel offsets and angle that a projectile should be launched at.
|
|
*
|
|
* Arguments:
|
|
* - [source][/atom]: The thing that the projectile is being shot from.
|
|
* - [target][/atom]: (Optional) The thing that the projectile is being shot at.
|
|
* - If this is not provided the source atom must be a mob with a client.
|
|
* - [modifiers][/list]: A list of click parameters used to modify the shot angle.
|
|
*/
|
|
/proc/calculate_projectile_angle_and_pixel_offsets(atom/source, atom/target, modifiers)
|
|
var/angle = 0
|
|
var/p_x = LAZYACCESS(modifiers, ICON_X) ? text2num(LAZYACCESS(modifiers, ICON_X)) : world.icon_size / 2 // ICON_(X|Y) are measured from the bottom left corner of the icon.
|
|
var/p_y = LAZYACCESS(modifiers, ICON_Y) ? text2num(LAZYACCESS(modifiers, ICON_Y)) : world.icon_size / 2 // This centers the target if modifiers aren't passed.
|
|
|
|
if(target)
|
|
var/turf/source_loc = get_turf(source)
|
|
var/turf/target_loc = get_turf(target)
|
|
var/dx = ((target_loc.x - source_loc.x) * world.icon_size) + (target.pixel_x - source.pixel_x) + (p_x - (world.icon_size / 2))
|
|
var/dy = ((target_loc.y - source_loc.y) * world.icon_size) + (target.pixel_y - source.pixel_y) + (p_y - (world.icon_size / 2))
|
|
|
|
angle = ATAN2(dy, dx)
|
|
return list(angle, p_x, p_y)
|
|
|
|
if(!ismob(source) || !LAZYACCESS(modifiers, SCREEN_LOC))
|
|
CRASH("Can't make trajectory calculations without a target or click modifiers and a client.")
|
|
|
|
var/mob/user = source
|
|
if(!user.client)
|
|
CRASH("Can't make trajectory calculations without a target or click modifiers and a client.")
|
|
|
|
//Split screen-loc up into X+Pixel_X and Y+Pixel_Y
|
|
var/list/screen_loc_params = splittext(LAZYACCESS(modifiers, SCREEN_LOC), ",")
|
|
//Split X+Pixel_X up into list(X, Pixel_X)
|
|
var/list/screen_loc_X = splittext(screen_loc_params[1],":")
|
|
//Split Y+Pixel_Y up into list(Y, Pixel_Y)
|
|
var/list/screen_loc_Y = splittext(screen_loc_params[2],":")
|
|
|
|
var/tx = (text2num(screen_loc_X[1]) - 1) * world.icon_size + text2num(screen_loc_X[2])
|
|
var/ty = (text2num(screen_loc_Y[1]) - 1) * world.icon_size + text2num(screen_loc_Y[2])
|
|
|
|
//Calculate the "resolution" of screen based on client's view and world's icon size. This will work if the user can view more tiles than average.
|
|
var/list/screenview = view_to_pixels(user.client.view)
|
|
|
|
var/ox = round(screenview[1] / 2) - user.client.pixel_x //"origin" x
|
|
var/oy = round(screenview[2] / 2) - user.client.pixel_y //"origin" y
|
|
angle = ATAN2(tx - oy, ty - ox)
|
|
return list(angle, p_x, p_y)
|
|
|
|
/obj/projectile/Destroy()
|
|
if(hitscan)
|
|
finalize_hitscan_and_generate_tracers()
|
|
STOP_PROCESSING(SSprojectiles, src)
|
|
cleanup_beam_segments()
|
|
if(trajectory)
|
|
QDEL_NULL(trajectory)
|
|
return ..()
|
|
|
|
/obj/projectile/proc/cleanup_beam_segments()
|
|
QDEL_LIST_ASSOC(beam_segments)
|
|
beam_segments = list()
|
|
QDEL_NULL(beam_index)
|
|
|
|
/obj/projectile/proc/finalize_hitscan_and_generate_tracers(impacting = TRUE)
|
|
if(trajectory && beam_index)
|
|
var/datum/point/point_cache = trajectory.copy_to()
|
|
beam_segments[beam_index] = point_cache
|
|
generate_hitscan_tracers(null, null, impacting)
|
|
|
|
/obj/projectile/proc/generate_hitscan_tracers(cleanup = TRUE, duration = 3, impacting = TRUE)
|
|
if(!length(beam_segments))
|
|
return
|
|
if(tracer_type)
|
|
var/tempref = REF(src)
|
|
for(var/datum/point/p in beam_segments)
|
|
generate_tracer_between_points(p, beam_segments[p], tracer_type, color, duration, hitscan_light_range, hitscan_light_color_override, hitscan_light_intensity, tempref)
|
|
if(muzzle_type && duration > 0)
|
|
var/datum/point/p = beam_segments[1]
|
|
var/atom/movable/thing = new muzzle_type
|
|
p.move_atom_to_src(thing)
|
|
var/matrix/matrix = new
|
|
matrix.Turn(original_angle)
|
|
thing.transform = matrix
|
|
thing.color = color
|
|
thing.set_light(muzzle_flash_range, muzzle_flash_intensity, muzzle_flash_color_override? muzzle_flash_color_override : color)
|
|
QDEL_IN(thing, duration)
|
|
if(impacting && impact_type && duration > 0)
|
|
var/datum/point/p = beam_segments[beam_segments[beam_segments.len]]
|
|
var/atom/movable/thing = new impact_type
|
|
p.move_atom_to_src(thing)
|
|
var/matrix/matrix = new
|
|
matrix.Turn(Angle)
|
|
thing.transform = matrix
|
|
thing.color = color
|
|
thing.set_light(impact_light_range, impact_light_intensity, impact_light_color_override? impact_light_color_override : color)
|
|
QDEL_IN(thing, duration)
|
|
if(cleanup)
|
|
cleanup_beam_segments()
|
|
|
|
/obj/projectile/experience_pressure_difference()
|
|
return
|
|
|
|
///Like [/obj/item/proc/updateEmbedding] but for projectiles instead, call this when you want to add embedding or update the stats on the embedding element
|
|
/obj/projectile/proc/updateEmbedding()
|
|
if(!shrapnel_type || !LAZYLEN(embedding))
|
|
return
|
|
|
|
AddElement(/datum/element/embed,\
|
|
embed_chance = (!isnull(embedding["embed_chance"]) ? embedding["embed_chance"] : EMBED_CHANCE),\
|
|
fall_chance = (!isnull(embedding["fall_chance"]) ? embedding["fall_chance"] : EMBEDDED_ITEM_FALLOUT),\
|
|
pain_chance = (!isnull(embedding["pain_chance"]) ? embedding["pain_chance"] : EMBEDDED_PAIN_CHANCE),\
|
|
pain_mult = (!isnull(embedding["pain_mult"]) ? embedding["pain_mult"] : EMBEDDED_PAIN_MULTIPLIER),\
|
|
remove_pain_mult = (!isnull(embedding["remove_pain_mult"]) ? embedding["remove_pain_mult"] : EMBEDDED_UNSAFE_REMOVAL_PAIN_MULTIPLIER),\
|
|
rip_time = (!isnull(embedding["rip_time"]) ? embedding["rip_time"] : EMBEDDED_UNSAFE_REMOVAL_TIME),\
|
|
ignore_throwspeed_threshold = (!isnull(embedding["ignore_throwspeed_threshold"]) ? embedding["ignore_throwspeed_threshold"] : FALSE),\
|
|
impact_pain_mult = (!isnull(embedding["impact_pain_mult"]) ? embedding["impact_pain_mult"] : EMBEDDED_IMPACT_PAIN_MULTIPLIER),\
|
|
jostle_chance = (!isnull(embedding["jostle_chance"]) ? embedding["jostle_chance"] : EMBEDDED_JOSTLE_CHANCE),\
|
|
jostle_pain_mult = (!isnull(embedding["jostle_pain_mult"]) ? embedding["jostle_pain_mult"] : EMBEDDED_JOSTLE_PAIN_MULTIPLIER),\
|
|
pain_stam_pct = (!isnull(embedding["pain_stam_pct"]) ? embedding["pain_stam_pct"] : EMBEDDED_PAIN_STAM_PCT),\
|
|
projectile_payload = shrapnel_type)
|
|
return TRUE
|
|
|
|
/**
|
|
* Is this projectile considered "hostile"?
|
|
*
|
|
* By default all projectiles which deal damage or impart crowd control effects (including stamina) are hostile
|
|
*
|
|
* This is NOT used for pacifist checks, that's handled by [/obj/item/ammo_casing/var/harmful]
|
|
* This is used in places such as AI responses to determine if they're being threatened or not (among other places)
|
|
*/
|
|
/obj/projectile/proc/is_hostile_projectile()
|
|
if(damage > 0 || stamina > 0)
|
|
return TRUE
|
|
|
|
if(paralyze + stun + immobilize + knockdown > 0 SECONDS)
|
|
return TRUE
|
|
|
|
return FALSE
|
|
|
|
///Checks if the projectile can embed into someone
|
|
/obj/projectile/proc/can_embed_into(atom/hit)
|
|
return embedding && shrapnel_type && iscarbon(hit) && !HAS_TRAIT(hit, TRAIT_PIERCEIMMUNE)
|
|
|
|
/// Reflects the projectile off of something
|
|
/obj/projectile/proc/reflect(atom/hit_atom)
|
|
if(!starting)
|
|
return
|
|
var/new_x = starting.x + pick(0, 0, 0, 0, 0, -1, 1, -2, 2)
|
|
var/new_y = starting.y + pick(0, 0, 0, 0, 0, -1, 1, -2, 2)
|
|
var/turf/current_tile = get_turf(hit_atom)
|
|
|
|
// redirect the projectile
|
|
original = locate(new_x, new_y, z)
|
|
starting = current_tile
|
|
firer = hit_atom
|
|
yo = new_y - current_tile.y
|
|
xo = new_x - current_tile.x
|
|
var/new_angle_s = Angle + rand(120,240)
|
|
while(new_angle_s > 180) // Translate to regular projectile degrees
|
|
new_angle_s -= 360
|
|
set_angle(new_angle_s)
|
|
|
|
#undef MOVES_HITSCAN
|
|
#undef MUZZLE_EFFECT_PIXEL_INCREMENT
|
|
|
|
/// Fire a projectile from this atom at another atom
|
|
/atom/proc/fire_projectile(projectile_type, atom/target, sound, firer, list/ignore_targets = list())
|
|
if (!isnull(sound))
|
|
playsound(src, sound, vol = 100, vary = TRUE)
|
|
|
|
var/turf/startloc = get_turf(src)
|
|
var/obj/projectile/bullet = new projectile_type(startloc)
|
|
bullet.starting = startloc
|
|
for (var/atom/thing as anything in ignore_targets)
|
|
bullet.impacted[WEAKREF(thing)] = TRUE
|
|
bullet.firer = firer || src
|
|
bullet.fired_from = src
|
|
bullet.yo = target.y - startloc.y
|
|
bullet.xo = target.x - startloc.x
|
|
bullet.original = target
|
|
bullet.preparePixelProjectile(target, src)
|
|
bullet.fire()
|
|
return bullet
|