diff --git a/.editorconfig b/.editorconfig index a25dbdfc85..df93ae3a16 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,3 +2,7 @@ insert_final_newline = true indent_style = tab indent_size = 4 + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.travis.yml b/.travis.yml index bef3a69ad7..ab3b1003c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,34 @@ language: generic -sudo: false dist: xenial +sudo: false + branches: except: - ___TGS3TempBranch - ___TGSTempBranch + matrix: include: - - env: - - BUILD_TOOLS=true - name: "Build Tools" + - name: "Run Linters" addons: apt: packages: - python3 - python3-pip - python3-setuptools - cache: - directories: - - tgui/node_modules - - env: - - BUILD_TESTING=true - - BUILD_TOOLS=false - name: "Build All Maps" + install: + - tools/travis/install_build_tools.sh + - tools/travis/install_dreamchecker.sh + script: + - tools/travis/check_filedirs.sh tgstation.dme + - tools/travis/check_changelogs.sh + - find . -name "*.php" -print0 | xargs -0 -n1 php -l + - find . -name "*.json" -not -path "./tgui/node_modules/*" -print0 | xargs -0 python3 ./tools/json_verifier.py + - tools/travis/build_tgui.sh + - tools/travis/check_grep.sh + - ~/dreamchecker + + - name: "Compile All Maps" addons: apt: packages: @@ -30,10 +36,15 @@ matrix: cache: directories: - $HOME/BYOND - - env: - - BUILD_TESTING=false - - BUILD_TOOLS=false - name: "Build and Run Unit Tests" + install: + - tools/travis/install_byond.sh + - source $HOME/BYOND/byond/bin/byondsetup + before_script: + - tools/travis/template_dm_generator.py + script: + - tools/travis/dm.sh -DTRAVISBUILDING -DTRAVISTESTING -DALL_MAPS tgstation.dme + + - name: "Compile and Run Tests" addons: mariadb: '10.2' apt: @@ -41,7 +52,6 @@ matrix: - ubuntu-toolchain-r-test packages: - libstdc++6:i386 - - libssl-dev:i386 - gcc-multilib - g++-7 - g++-7-multilib @@ -49,25 +59,19 @@ matrix: - libmariadbd-dev cache: directories: - - $HOME/.cargo - $HOME/BYOND - - $HOME/MariaDB - - $HOME/.rustup - -install: - - tools/travis/install_build_tools.sh - - if [ $BUILD_TOOLS = false ] && [ $BUILD_TESTING = false ]; then mysql -u root -e 'CREATE DATABASE tg_travis;'; fi - - if [ $BUILD_TOOLS = false ] && [ $BUILD_TESTING = false ]; then mysql -u root tg_travis < SQL/tgstation_schema.sql; fi - - if [ $BUILD_TOOLS = false ] && [ $BUILD_TESTING = false ]; then mysql -u root -e 'CREATE DATABASE tg_travis_prefixed;'; fi - - if [ $BUILD_TOOLS = false ] && [ $BUILD_TESTING = false ]; then mysql -u root tg_travis_prefixed < SQL/tgstation_schema_prefixed.sql; fi - -before_script: - - tools/travis/before_build_tools.sh - - tools/travis/before_build_byond.sh - -script: - - tools/travis/check_filedirs.sh tgstation.dme - - tools/travis/build_tools.sh || travis_terminate 1 - - tools/travis/build_dependencies.sh || travis_terminate 1 - - tools/travis/build_byond.sh - + - $HOME/libmariadb + install: + - tools/travis/install_byond.sh + - source $HOME/BYOND/byond/bin/byondsetup + - tools/travis/install_libmariadb.sh + - tools/travis/install_rust_g.sh + before_script: + - mysql -u root -e 'CREATE DATABASE tg_travis;' + - mysql -u root tg_travis < SQL/tgstation_schema.sql + - mysql -u root -e 'CREATE DATABASE tg_travis_prefixed;' + - mysql -u root tg_travis_prefixed < SQL/tgstation_schema_prefixed.sql + - tools/travis/build_bsql.sh + script: + - tools/travis/dm.sh -DTRAVISBUILDING tgstation.dme || travis_terminate 1 + - tools/travis/run_server.sh diff --git a/_maps/RandomRuins/LavaRuins/lavaland_surface_elite_tumor.dmm b/_maps/RandomRuins/LavaRuins/lavaland_surface_elite_tumor.dmm new file mode 100644 index 0000000000..6e44ec3197 --- /dev/null +++ b/_maps/RandomRuins/LavaRuins/lavaland_surface_elite_tumor.dmm @@ -0,0 +1,111 @@ +//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE +"a" = ( +/turf/template_noop, +/area/lavaland/surface/outdoors) +"b" = ( +/obj/structure/elite_tumor, +/turf/open/floor/plating/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) +"c" = ( +/turf/open/floor/plating/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) + +(1,1,1) = {" +a +a +a +a +a +a +a +a +a +"} +(2,1,1) = {" +a +a +a +a +a +a +a +a +a +"} +(3,1,1) = {" +a +a +a +a +a +a +a +a +a +"} +(4,1,1) = {" +a +a +a +c +c +c +a +a +a +"} +(5,1,1) = {" +a +a +a +c +b +c +a +a +a +"} +(6,1,1) = {" +a +a +a +c +c +c +a +a +a +"} +(7,1,1) = {" +a +a +a +a +a +a +a +a +a +"} +(8,1,1) = {" +a +a +a +a +a +a +a +a +a +"} +(9,1,1) = {" +a +a +a +a +a +a +a +a +a +"} diff --git a/_maps/shuttles/emergency_raven.dmm b/_maps/shuttles/emergency_raven.dmm index 2c04837c79..05446968e7 100644 --- a/_maps/shuttles/emergency_raven.dmm +++ b/_maps/shuttles/emergency_raven.dmm @@ -56,7 +56,7 @@ /turf/open/floor/plasteel/dark, /area/shuttle/escape) "ah" = ( -/obj/machinery/computer/shuttle, +/obj/machinery/computer/emergency_shuttle, /turf/open/floor/plasteel/dark, /area/shuttle/escape) "ai" = ( diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index b2f05fd5f8..058fd864bd 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -40,3 +40,19 @@ #define NOT_DOMINATING -1 #define MAX_LEADERS_GANG 4 #define INITIAL_DOM_ATTEMPTS 3 + +//Bloodsucker defines +// Bloodsucker related antag datums +#define ANTAG_DATUM_BLOODSUCKER /datum/antagonist/bloodsucker +#define ANTAG_DATUM_VASSAL /datum/antagonist/vassal +//#define ANTAG_DATUM_HUNTER /datum/antagonist/vamphunter Disabled for now + +// BLOODSUCKER +#define BLOODSUCKER_LEVEL_TO_EMBRACE 3 +#define BLOODSUCKER_FRENZY_TIME 25 // How long the vamp stays in frenzy. +#define BLOODSUCKER_FRENZY_OUT_TIME 300 // How long the vamp goes back into frenzy. +#define BLOODSUCKER_STARVE_VOLUME 5 // Amount of blood, below which a Vamp is at risk of frenzy. + +#define CAT_STRUCTURE "Structures" + +#define MARTIALART_HUNTER "hunter-fu" diff --git a/code/__DEFINES/atom_hud.dm b/code/__DEFINES/atom_hud.dm index 0e4f0f3a6e..322a150617 100644 --- a/code/__DEFINES/atom_hud.dm +++ b/code/__DEFINES/atom_hud.dm @@ -58,6 +58,7 @@ #define ANTAG_HUD_SOULLESS 21 #define ANTAG_HUD_CLOCKWORK 22 #define ANTAG_HUD_BROTHER 23 +#define ANTAG_HUD_BLOODSUCKER 24 // Notification action types #define NOTIFY_JUMP "jump" diff --git a/code/__DEFINES/components.dm b/code/__DEFINES/components.dm index 4f56b07fe7..f95ff93ac3 100644 --- a/code/__DEFINES/components.dm +++ b/code/__DEFINES/components.dm @@ -36,8 +36,10 @@ ////////////////////////////////////////////////////////////////// // /datum signals -#define COMSIG_COMPONENT_ADDED "component_added" //when a component is added to a datum: (/datum/component) -#define COMSIG_COMPONENT_REMOVING "component_removing" //before a component is removed from a datum because of RemoveComponent: (/datum/component) +#define COMSIG_COMPONENT_ADDED "component_added" //sent to the new datum parent when a component is added to them: (/datum/component) +#define COMSIG_COMPONENT_REMOVING "component_removing" //sent to the datum parent before a component is removed from them because of RemoveComponent: (/datum/component) +#define COMSIG_COMPONENT_UNREGISTER_PARENT "component_unregister_parent" //sent to the component itself when unregistered from a parent +#define COMSIG_COMPONENT_REGISTER_PARENT "component_register_parent" //sent to the component itself when registered to a parent #define COMSIG_PARENT_PREQDELETED "parent_preqdeleted" //before a datum's Destroy() is called: (force), returning a nonzero value will cancel the qdel operation #define COMSIG_PARENT_QDELETING "parent_qdeleting" //just before a datum's Destroy() is called: (force), at this point none of the other components chose to interrupt qdel and Destroy will be called @@ -140,19 +142,22 @@ #define HEARING_RAW_MESSAGE 4 /* #define HEARING_RADIO_FREQ 5 #define HEARING_SPANS 6 - #define HEARING_MESSAGE_MODE 7 + #define HEARING_MESSAGE_MODE 7 #define HEARING_SOURCE 8*/ #define COMSIG_MOVABLE_DISPOSING "movable_disposing" //called when the movable is added to a disposal holder object for disposal movement: (obj/structure/disposalholder/holder, obj/machinery/disposal/source) #define COMSIG_MOVABLE_TELEPORTED "movable_teleported" //from base of do_teleport(): (channel, turf/origin, turf/destination) // /mind signals -#define COMSIG_MIND_TRANSFER "mind_transfer" //from base of mind/transfer_to(): (new_character, old_character) +#define COMSIG_PRE_MIND_TRANSFER "pre_mind_transfer" //from base of mind/transfer_to() before it's done: (new_character, old_character) + #define COMPONENT_STOP_MIND_TRANSFER 1 //stops the mind transfer from happening. +#define COMSIG_MIND_TRANSFER "mind_transfer" //from base of mind/transfer_to() when it's done: (new_character, old_character) // /mob signals #define COMSIG_MOB_EXAMINATE "mob_examinate" //from base of /mob/verb/examinate(): (atom/A) #define COMPONENT_ALLOW_EXAMINE 1 #define COMSIG_MOB_DEATH "mob_death" //from base of mob/death(): (gibbed) -#define COMSIG_MOB_GHOSTIZE "mob_ghostize" //from base of mob/Ghostize(): (can_reenter_corpse) + #define COMPONENT_BLOCK_DEATH_BROADCAST 1 //stops the death from being broadcasted in deadchat. +#define COMSIG_MOB_GHOSTIZE "mob_ghostize" //from base of mob/Ghostize(): (can_reenter_corpse, special, penalize) #define COMPONENT_BLOCK_GHOSTING 1 #define COMSIG_MOB_ALLOWED "mob_allowed" //from base of obj/allowed(mob/M): (/obj) returns bool, if TRUE the mob has id access to the obj #define COMSIG_MOB_RECEIVE_MAGIC "mob_receive_magic" //from base of mob/anti_magic_check(): (mob/user, magic, holy, tinfoil, chargecost, self, protection_sources) @@ -164,7 +169,9 @@ #define COMSIG_MOB_ITEM_AFTERATTACK "mob_item_afterattack" //from base of obj/item/afterattack(): (atom/target, mob/user, proximity_flag, click_parameters) #define COMSIG_MOB_ATTACK_RANGED "mob_attack_ranged" //from base of mob/RangedAttack(): (atom/A, params) #define COMSIG_MOB_THROW "mob_throw" //from base of /mob/throw_item(): (atom/target) -#define COMSIG_MOB_KEY_CHANGE "mob_key_change" //from base of /mob/transfer_ckey() +#define COMSIG_MOB_KEY_CHANGE "mob_key_change" //from base of /mob/transfer_ckey(): (new_character, old_character) +#define COMSIG_MOB_PRE_PLAYER_CHANGE "mob_pre_player_change" //sent to the target mob from base of /mob/transfer_ckey() and /mind/transfer_to(): (our_character, their_character) +// #define COMPONENT_STOP_MIND_TRANSFER 1 #define COMSIG_MOB_UPDATE_SIGHT "mob_update_sight" //from base of /mob/update_sight(): () #define COMSIG_MOB_SAY "mob_say" // from /mob/living/say(): (proc args list) #define COMPONENT_UPPERCASE_SPEECH 1 diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm index b316c003a9..d5e3f828b8 100644 --- a/code/__DEFINES/role_preferences.dm +++ b/code/__DEFINES/role_preferences.dm @@ -35,6 +35,8 @@ #define ROLE_LAVALAND "lavaland" #define ROLE_INTERNAL_AFFAIRS "internal affairs agent" #define ROLE_GANG "gangster" +#define ROLE_BLOODSUCKER "bloodsucker" +//#define ROLE_MONSTERHUNTER "monster hunter" Disabled for now //Missing assignment means it's not a gamemode specific role, IT'S NOT A BUG OR ERROR. //The gamemode specific ones are just so the gamemodes can query whether a player is old enough @@ -60,7 +62,9 @@ GLOBAL_LIST_INIT(special_roles, list( ROLE_OVERTHROW = /datum/game_mode/overthrow, ROLE_INTERNAL_AFFAIRS = /datum/game_mode/traitor/internal_affairs, ROLE_SENTIENCE, - ROLE_GANG = /datum/game_mode/gang + ROLE_GANG = /datum/game_mode/gang, + ROLE_BLOODSUCKER = /datum/game_mode/bloodsucker + //ROLE_MONSTERHUNTER Disabled for now )) //Job defines for what happens when you fail to qualify for any job during job selection diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm index f9360117bb..6d52f4640c 100644 --- a/code/__DEFINES/status_effects.dm +++ b/code/__DEFINES/status_effects.dm @@ -46,7 +46,7 @@ #define STATUS_EFFECT_SLEEPING /datum/status_effect/incapacitating/sleeping //the affected is asleep -#define STATUS_EFFECT_TASED /datum/status_effect/electrode //the affected has been tased, preventing fine muscle control +#define STATUS_EFFECT_TASED /datum/status_effect/no_combat_mode/electrode/ //the affected has been tased, preventing fine muscle control #define STATUS_EFFECT_PACIFY /datum/status_effect/pacify //the affected is pacified, preventing direct hostile actions @@ -68,6 +68,8 @@ #define STATUS_EFFECT_SAWBLEED /datum/status_effect/saw_bleed //if the bleed builds up enough, takes a ton of damage +#define STATUS_EFFECT_NECKSLICE /datum/status_effect/neck_slice //Creates the flavor messages for the neck-slice + #define STATUS_EFFECT_NECROPOLIS_CURSE /datum/status_effect/necropolis_curse #define CURSE_BLINDING 1 //makes the edges of the target's screen obscured #define CURSE_SPAWNING 2 //spawns creatures that attack the target only @@ -83,6 +85,9 @@ #define STATUS_EFFECT_BREASTS_ENLARGEMENT /datum/status_effect/chem/breast_enlarger //Applied slowdown due to the ominous bulk. #define STATUS_EFFECT_PENIS_ENLARGEMENT /datum/status_effect/chem/penis_enlarger //More applied slowdown, just like the above. + +#define STATUS_EFFECT_NO_COMBAT_MODE /datum/status_effect/no_combat_mode //Wont allow combat mode and will disable it +#define STATUS_EFFECT_MESMERIZE /datum/status_effect/no_combat_mode/mesmerize //Just reskinned no_combat_mode ///////////// // NEUTRAL // ///////////// diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 14dbd97ffa..d17db07b50 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -66,6 +66,7 @@ #define TRAIT_HUSK "husk" #define TRAIT_NOCLONE "noclone" #define TRAIT_CLUMSY "clumsy" +#define TRAIT_CHUNKYFINGERS "chunkyfingers" //means that you can't use weapons with normal trigger guards. #define TRAIT_DUMB "dumb" #define TRAIT_MONKEYLIKE "monkeylike" //sets IsAdvancedToolUser to FALSE #define TRAIT_PACIFISM "pacifism" @@ -127,6 +128,12 @@ #define TRAIT_ABDUCTOR_TRAINING "abductor-training" #define TRAIT_ABDUCTOR_SCIENTIST_TRAINING "abductor-scientist-training" #define TRAIT_SURGEON "surgeon" +#define TRAIT_COLDBLOODED "coldblooded" // Your body is literal room temperature. Does not make you immune to the temp. +#define TRAIT_NONATURALHEAL "nonaturalheal" // Only Admins can heal you. NOTHING else does it unless it's given the god tag. +#define TRAIT_NORUNNING "norunning" // You walk! +#define TRAIT_NOMARROW "nomarrow" // You don't make blood, with chemicals or nanites. +#define TRAIT_NOPULSE "nopulse" // Your heart doesn't beat. + //non-mob traits #define TRAIT_PARALYSIS "paralysis" //Used for limb-based paralysis, where replacing the limb will fix it @@ -217,5 +224,6 @@ #define LOCKED_HELMET_TRAIT "locked-helmet" #define NINJA_SUIT_TRAIT "ninja-suit" #define ANTI_DROP_IMPLANT_TRAIT "anti-drop-implant" +#define SLEEPING_CARP_TRAIT "sleeping_carp" #define ABDUCTOR_ANTAGONIST "abductor-antagonist" #define MADE_UNCLONEABLE "made-uncloneable" diff --git a/code/__HELPERS/cmp.dm b/code/__HELPERS/cmp.dm index f7131e63fa..4a86f57fcd 100644 --- a/code/__HELPERS/cmp.dm +++ b/code/__HELPERS/cmp.dm @@ -95,4 +95,22 @@ GLOBAL_VAR_INIT(cmp_field, "name") return sorttext(A.sample_object.name, B.sample_object.name) /proc/cmp_numbered_displays_name_dsc(datum/numbered_display/A, datum/numbered_display/B) - return sorttext(B.sample_object.name, A.sample_object.name) \ No newline at end of file + return sorttext(B.sample_object.name, A.sample_object.name) + +/proc/cmp_quirk_asc(datum/quirk/A, datum/quirk/B) + var/a_sign = num2sign(initial(A.value) * -1) + var/b_sign = num2sign(initial(B.value) * -1) + + // Neutral traits go last. + if(a_sign == 0) + a_sign = 2 + if(b_sign == 0) + b_sign = 2 + + var/a_name = initial(A.name) + var/b_name = initial(B.name) + + if(a_sign != b_sign) + return a_sign - b_sign + else + return sorttext(b_name, a_name) \ No newline at end of file diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 9abe42ea8e..29f5331fd3 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -1548,4 +1548,12 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new) /proc/CallAsync(datum/source, proctype, list/arguments) set waitfor = FALSE - return call(source, proctype)(arglist(arguments)) \ No newline at end of file + return call(source, proctype)(arglist(arguments)) + +/proc/num2sign(numeric) + if(numeric > 0) + return 1 + else if(numeric < 0) + return -1 + else + return 0 diff --git a/code/_onclick/ai.dm b/code/_onclick/ai.dm index 7a495b95e7..2a8289697b 100644 --- a/code/_onclick/ai.dm +++ b/code/_onclick/ai.dm @@ -114,7 +114,8 @@ /mob/living/silicon/ai/CtrlClickOn(var/atom/A) A.AICtrlClick(src) /mob/living/silicon/ai/AltClickOn(var/atom/A) - A.AIAltClick(src) + if(!A.AIAltClick(src)) + altclick_listed_turf(A) /* The following criminally helpful code is just the previous code cleaned up; @@ -125,9 +126,10 @@ /* Atom Procs */ /atom/proc/AICtrlClick() return + /atom/proc/AIAltClick(mob/living/silicon/ai/user) - AltClick(user) - return + return AltClick(user) + /atom/proc/AIShiftClick() return /atom/proc/AICtrlShiftClick() @@ -151,6 +153,7 @@ shock_perm(usr) else shock_restore(usr) + return TRUE /obj/machinery/door/airlock/AIShiftClick() // Opens and closes doors! if(obj_flags & EMAGGED) @@ -185,10 +188,12 @@ return toggle_on() add_fingerprint(usr) + return TRUE /* Holopads */ /obj/machinery/holopad/AIAltClick(mob/living/silicon/ai/user) hangup_all_calls() + return TRUE // // Override TurfAdjacent for AltClicking diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index e9f50d9212..4c477f3872 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -353,8 +353,17 @@ Unused except for AI */ /mob/proc/AltClickOn(atom/A) - A.AltClick(src) - return + if(!A.AltClick(src)) + altclick_listed_turf(A) + +/mob/proc/altclick_listed_turf(atom/A) + var/turf/T = get_turf(A) + if(T == A.loc || T == A) + if(T == listed_turf) + listed_turf = null + else if(TurfAdjacent(T)) + listed_turf = T + client.statpanel = T.name /mob/living/carbon/AltClickOn(atom/A) if(!stat && mind && iscarbon(A) && A != src) @@ -366,18 +375,7 @@ ..() /atom/proc/AltClick(mob/user) - SEND_SIGNAL(src, COMSIG_CLICK_ALT, user) - var/turf/T = get_turf(src) - if(T && user.TurfAdjacent(T)) - user.listed_turf = T - user.client.statpanel = T.name - -// Use this instead of /mob/proc/AltClickOn(atom/A) where you only want turf content listing without additional atom alt-click interaction -/atom/proc/AltClickNoInteract(mob/user, atom/A) - var/turf/T = get_turf(A) - if(T && user.TurfAdjacent(T)) - user.listed_turf = T - user.client.statpanel = T.name + . = SEND_SIGNAL(src, COMSIG_CLICK_ALT, user) /mob/proc/TurfAdjacent(turf/T) return T.Adjacent(src) diff --git a/code/_onclick/cyborg.dm b/code/_onclick/cyborg.dm index 7f63b74945..a2d566d566 100644 --- a/code/_onclick/cyborg.dm +++ b/code/_onclick/cyborg.dm @@ -110,7 +110,8 @@ /mob/living/silicon/robot/CtrlClickOn(atom/A) A.BorgCtrlClick(src) /mob/living/silicon/robot/AltClickOn(atom/A) - A.BorgAltClick(src) + if(!A.BorgAltClick(src)) + altclick_listed_turf(A) /atom/proc/BorgCtrlShiftClick(mob/living/silicon/robot/user) //forward to human click if not overridden CtrlShiftClick(user) @@ -154,20 +155,17 @@ ..() /atom/proc/BorgAltClick(mob/living/silicon/robot/user) - AltClick(user) - return + return AltClick(user) /obj/machinery/door/airlock/BorgAltClick(mob/living/silicon/robot/user) // Eletrifies doors. Forwards to AI code. if(get_dist(src,user) <= user.interaction_range) - AIAltClick() - else - ..() + return AIAltClick() + return ..() /obj/machinery/turretid/BorgAltClick(mob/living/silicon/robot/user) //turret lethal on/off. Forwards to AI code. if(get_dist(src,user) <= user.interaction_range) - AIAltClick() - else - ..() + return AIAltClick() + return ..() /* As with AI, these are not used in click code, diff --git a/code/_onclick/hud/_defines.dm b/code/_onclick/hud/_defines.dm index 167fa989b2..9d53703cdd 100644 --- a/code/_onclick/hud/_defines.dm +++ b/code/_onclick/hud/_defines.dm @@ -107,6 +107,10 @@ #define ui_internal "EAST-1:28,CENTER+1:19"//CIT CHANGE - moves internal icon up a little bit to accommodate for the stamina meter #define ui_mood "EAST-1:28,CENTER-3:10" +//living +#define ui_living_pull "EAST-1:28,CENTER-2:15" +#define ui_living_health "EAST-1:28,CENTER:15" + //borgs #define ui_borg_health "EAST-1:28,CENTER-1:15" //borgs have the health display where humans have the pressure damage indicator. diff --git a/code/_onclick/hud/action_button.dm b/code/_onclick/hud/action_button.dm index 2b5af9ddb8..858b8dbd06 100644 --- a/code/_onclick/hud/action_button.dm +++ b/code/_onclick/hud/action_button.dm @@ -113,7 +113,7 @@ name = "Show Buttons" else name = "Hide Buttons" - UpdateIcon() + update_icon() usr.update_action_buttons() /obj/screen/movable/action_button/hide_toggle/AltClick(mob/user) @@ -125,6 +125,7 @@ moved = FALSE user.update_action_buttons(TRUE) to_chat(user, "Action button positions have been reset.") + return TRUE /obj/screen/movable/action_button/hide_toggle/proc/InitialiseIcon(datum/hud/owner_hud) @@ -134,9 +135,9 @@ hide_icon = settings["toggle_icon"] hide_state = settings["toggle_hide"] show_state = settings["toggle_show"] - UpdateIcon() + update_icon() -/obj/screen/movable/action_button/hide_toggle/proc/UpdateIcon() +/obj/screen/movable/action_button/hide_toggle/update_icon() cut_overlays() add_overlay(mutable_appearance(hide_icon, hidden ? show_state : hide_state)) diff --git a/code/_onclick/hud/human.dm b/code/_onclick/hud/human.dm index 7f2fca9d98..3b5f851333 100644 --- a/code/_onclick/hud/human.dm +++ b/code/_onclick/hud/human.dm @@ -349,6 +349,13 @@ devilsouldisplay = new /obj/screen/devil/soul_counter infodisplay += devilsouldisplay + blood_display = new /obj/screen/bloodsucker/blood_counter // Blood Volume + infodisplay += blood_display + vamprank_display = new /obj/screen/bloodsucker/rank_counter // Vampire Rank + infodisplay += vamprank_display + sunlight_display = new /obj/screen/bloodsucker/sunlight_counter // Sunlight + infodisplay += sunlight_display + zone_select = new /obj/screen/zone_sel() zone_select.icon = ui_style zone_select.update_icon(mymob) diff --git a/code/_onclick/hud/lavaland_elite.dm b/code/_onclick/hud/lavaland_elite.dm new file mode 100644 index 0000000000..277ea8b898 --- /dev/null +++ b/code/_onclick/hud/lavaland_elite.dm @@ -0,0 +1,16 @@ +/datum/hud/lavaland_elite + ui_style = 'icons/mob/screen_elite.dmi' + +/datum/hud/lavaland_elite/New(mob/living/simple_animal/hostile/asteroid/elite) + ..() + + pull_icon = new /obj/screen/pull() + pull_icon.icon = ui_style + pull_icon.update_icon() + pull_icon.screen_loc = ui_living_pull + pull_icon.hud = src + static_inventory += pull_icon + + healths = new /obj/screen/healths/lavaland_elite() + healths.hud = src + infodisplay += healths diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index 5f128ff76d..86b4b0c024 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -644,6 +644,12 @@ screen_loc = ui_construct_health mouse_opacity = MOUSE_OPACITY_TRANSPARENT +/obj/screen/healths/lavaland_elite + icon = 'icons/mob/screen_elite.dmi' + icon_state = "elite_health0" + screen_loc = ui_health + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + /obj/screen/healthdoll name = "health doll" screen_loc = ui_healthdoll diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm index 9cb0c52b9a..f082f2ad16 100644 --- a/code/_onclick/item_attack.dm +++ b/code/_onclick/item_attack.dm @@ -110,7 +110,7 @@ totitemdamage *= 0.5 //CIT CHANGES END HERE apply_damage(totitemdamage, I.damtype) //CIT CHANGE - replaces I.force with totitemdamage - if(I.damtype == BRUTE) + if(I.damtype == BRUTE && !HAS_TRAIT(src, TRAIT_NOMARROW)) if(prob(33)) I.add_mob_blood(src) var/turf/location = get_turf(src) @@ -163,4 +163,3 @@ /obj/item/proc/getweight() return total_mass || w_class * 1.25 - diff --git a/code/_onclick/observer.dm b/code/_onclick/observer.dm index 299d7f64f8..f76a745fd2 100644 --- a/code/_onclick/observer.dm +++ b/code/_onclick/observer.dm @@ -31,7 +31,7 @@ ShiftClickOn(A) return if(modifiers["alt"]) - AltClickNoInteract(src, A) + altclick_listed_turf(A) return if(modifiers["ctrl"]) CtrlClickOn(A) diff --git a/code/controllers/subsystem/processing/quirks.dm b/code/controllers/subsystem/processing/quirks.dm index 4af54b8c70..0b9284a144 100644 --- a/code/controllers/subsystem/processing/quirks.dm +++ b/code/controllers/subsystem/processing/quirks.dm @@ -12,27 +12,40 @@ PROCESSING_SUBSYSTEM_DEF(quirks) var/list/quirk_names_by_path = list() var/list/quirk_points = list() //Assoc. list of quirk names and their "point cost"; positive numbers are good traits, and negative ones are bad var/list/quirk_objects = list() //A list of all quirk objects in the game, since some may process + var/list/quirk_blacklist = list() //A list a list of quirks that can not be used with each other. Format: list(quirk1,quirk2),list(quirk3,quirk4) /datum/controller/subsystem/processing/quirks/Initialize(timeofday) if(!quirks.len) SetupQuirks() + quirk_blacklist = list(list("Blind","Nearsighted"),list("Jolly","Depression","Apathetic"),list("Ageusia","Deviant Tastes"),list("Ananas Affinity","Ananas Aversion")) return ..() /datum/controller/subsystem/processing/quirks/proc/SetupQuirks() - for(var/V in subtypesof(/datum/quirk)) +// Sort by Positive, Negative, Neutral; and then by name + var/list/quirk_list = sortList(subtypesof(/datum/quirk), /proc/cmp_quirk_asc) + + for(var/V in quirk_list) var/datum/quirk/T = V quirks[initial(T.name)] = T quirk_points[initial(T.name)] = initial(T.value) quirk_names_by_path[T] = initial(T.name) /datum/controller/subsystem/processing/quirks/proc/AssignQuirks(mob/living/user, client/cli, spawn_effects, roundstart = FALSE, datum/job/job, silent = FALSE, mob/to_chat_target) - GenerateQuirks(cli) - var/list/quirks = cli.prefs.character_quirks.Copy() + var/badquirk = FALSE + var/list/my_quirks = cli.prefs.all_quirks.Copy() var/list/cut - if(job && job.blacklisted_quirks) - cut = filter_quirks(quirks, job) - for(var/V in quirks) - user.add_quirk(V, spawn_effects) + if(job?.blacklisted_quirks) + cut = filter_quirks(my_quirks, job) + for(var/V in my_quirks) + var/datum/quirk/Q = quirks[V] + if(Q) + user.add_quirk(Q, spawn_effects) + else + stack_trace("Invalid quirk \"[V]\" in client [cli.ckey] preferences") + cli.prefs.all_quirks -= V + badquirk = TRUE + if(badquirk) + cli.prefs.save_character() if(!silent && LAZYLEN(cut)) to_chat(to_chat_target || user, "All of your non-neutral character quirks have been cut due to these quirks conflicting with your job assignment: [english_list(cut)].") @@ -50,28 +63,28 @@ PROCESSING_SUBSYSTEM_DEF(quirks) for(var/i in quirk_names) . += quirk_points_by_name(i) -/datum/controller/subsystem/processing/quirks/proc/filter_quirks(list/quirks, datum/job/job) +/datum/controller/subsystem/processing/quirks/proc/filter_quirks(list/our_quirks, datum/job/job) var/list/cut = list() var/list/banned_names = list() for(var/i in job.blacklisted_quirks) var/name = quirk_name_by_path(i) if(name) banned_names += name - var/list/blacklisted = quirks & banned_names + var/list/blacklisted = our_quirks & banned_names if(length(blacklisted)) for(var/i in blacklisted) - quirks -= i + our_quirks -= i cut += i /* //Code to automatically reduce positive quirks until balance is even. - var/points_used = total_points(quirks) + var/points_used = total_points(our_quirks) if(points_used > 0) //they owe us points, let's collect. - for(var/i in quirks) + for(var/i in our_quirks) var/points = quirk_points_by_name(i) if(points > 0) cut += i - quirks -= i + our_quirks -= i points_used -= points if(points_used <= 0) break @@ -79,14 +92,9 @@ PROCESSING_SUBSYSTEM_DEF(quirks) //Nah, let's null all non-neutrals out. if(cut.len) - for(var/i in quirks) + for(var/i in our_quirks) if(quirk_points_by_name(i) != 0) //cut += i -- Commented out: Only show the ones that triggered the quirk purge. - quirks -= i + our_quirks -= i return cut - -/datum/controller/subsystem/processing/quirks/proc/GenerateQuirks(client/user) - if(user.prefs.character_quirks.len) - return - user.prefs.character_quirks = user.prefs.all_quirks diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index 6f619fef0b..890725fbb1 100755 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -384,8 +384,8 @@ SUBSYSTEM_DEF(ticker) captainless=0 if(player.mind.assigned_role != player.mind.special_role) SSjob.EquipRank(N, player.mind.assigned_role, 0) - if(CONFIG_GET(flag/roundstart_traits) && ishuman(N.new_character)) - SSquirks.AssignQuirks(N.new_character, N.client, TRUE, TRUE, SSjob.GetJob(player.mind.assigned_role), FALSE, N) + if(CONFIG_GET(flag/roundstart_traits) && ishuman(N.new_character)) + SSquirks.AssignQuirks(N.new_character, N.client, TRUE, TRUE, SSjob.GetJob(player.mind.assigned_role), FALSE, N) CHECK_TICK if(captainless) for(var/mob/dead/new_player/N in GLOB.player_list) diff --git a/code/datums/components/_component.dm b/code/datums/components/_component.dm index f938a70308..72b9681f91 100644 --- a/code/datums/components/_component.dm +++ b/code/datums/components/_component.dm @@ -53,7 +53,7 @@ // If you want/expect to be moving the component around between parents, use this to register on the parent for signals /datum/component/proc/RegisterWithParent() - return + SEND_SIGNAL(src, COMSIG_COMPONENT_REGISTER_PARENT) //CITADEL EDIT /datum/component/proc/Initialize(...) return @@ -85,7 +85,7 @@ UnregisterFromParent() /datum/component/proc/UnregisterFromParent() - return + SEND_SIGNAL(src, COMSIG_COMPONENT_UNREGISTER_PARENT) //CITADEL EDIT /datum/proc/RegisterSignal(datum/target, sig_type_or_types, proctype, override = FALSE) if(QDELETED(src) || QDELETED(target)) diff --git a/code/datums/components/bane.dm b/code/datums/components/bane.dm index 84f8010270..bdfcfed517 100644 --- a/code/datums/components/bane.dm +++ b/code/datums/components/bane.dm @@ -19,12 +19,14 @@ src.damage_multiplier = damage_multiplier /datum/component/bane/RegisterWithParent() + . = ..() if(speciestype) RegisterSignal(parent, COMSIG_ITEM_AFTERATTACK, .proc/speciesCheck) else RegisterSignal(parent, COMSIG_ITEM_AFTERATTACK, .proc/mobCheck) /datum/component/bane/UnregisterFromParent() + . = ..() UnregisterSignal(parent, COMSIG_ITEM_AFTERATTACK) /datum/component/bane/proc/speciesCheck(obj/item/source, atom/target, mob/user, proximity_flag, click_parameters) diff --git a/code/datums/components/bouncy.dm b/code/datums/components/bouncy.dm index f6a2a89195..c7ca85455b 100644 --- a/code/datums/components/bouncy.dm +++ b/code/datums/components/bouncy.dm @@ -21,9 +21,11 @@ RegisterSignal(parent, bounce, .proc/bounce_up) /datum/component/bouncy/RegisterWithParent() + . = ..() RegisterSignal(parent, bounce_signals, .proc/bounce_up) /datum/component/bouncy/UnregisterFromParent() + . = ..() UnregisterSignal(parent, bounce_signals) /datum/component/bouncy/proc/bounce_up(datum/source) diff --git a/code/datums/components/butchering.dm b/code/datums/components/butchering.dm index 1518a84456..d5af47ea1f 100644 --- a/code/datums/components/butchering.dm +++ b/code/datums/components/butchering.dm @@ -23,17 +23,51 @@ RegisterSignal(parent, COMSIG_ITEM_ATTACK, .proc/onItemAttack) /datum/component/butchering/proc/onItemAttack(obj/item/source, mob/living/M, mob/living/user) - if(user.a_intent == INTENT_HARM && M.stat == DEAD && (M.butcher_results || M.guaranteed_butcher_results)) //can we butcher it? + if(user.a_intent != INTENT_HARM) + return + if(M.stat == DEAD && (M.butcher_results || M.guaranteed_butcher_results)) //can we butcher it? if(butchering_enabled && (can_be_blunt || source.get_sharpness())) INVOKE_ASYNC(src, .proc/startButcher, source, M, user) return COMPONENT_ITEM_NO_ATTACK + if(ishuman(M) && source.force && source.get_sharpness()) + var/mob/living/carbon/human/H = M + if((H.health <= H.crit_threshold || (user.pulling == H && user.grab_state >= GRAB_NECK) || H.IsSleeping()) && user.zone_selected == BODY_ZONE_HEAD) // Only sleeping, neck grabbed, or crit, can be sliced. + if(H.has_status_effect(/datum/status_effect/neck_slice)) + user.show_message("[H]'s neck has already been already cut, you can't make the bleeding any worse!", 1, \ + "Their neck has already been already cut, you can't make the bleeding any worse!") + return COMPONENT_ITEM_NO_ATTACK + INVOKE_ASYNC(src, .proc/startNeckSlice, source, H, user) + return COMPONENT_ITEM_NO_ATTACK + /datum/component/butchering/proc/startButcher(obj/item/source, mob/living/M, mob/living/user) to_chat(user, "You begin to butcher [M]...") playsound(M.loc, butcher_sound, 50, TRUE, -1) if(do_mob(user, M, speed) && M.Adjacent(source)) Butcher(user, M) +/datum/component/butchering/proc/startNeckSlice(obj/item/source, mob/living/carbon/human/H, mob/living/user) + user.visible_message("[user] is slitting [H]'s throat!", \ + "You start slicing [H]'s throat!", \ + "You hear a cutting noise!", ignored_mobs = H) + H.show_message("Your throat is being slit by [user]!", 1, \ + "Something is cutting into your neck!", NONE) + log_combat(user, H, "starts slicing the throat of") + + playsound(H.loc, butcher_sound, 50, TRUE, -1) + if(do_mob(user, H, CLAMP(500 / source.force, 30, 100)) && H.Adjacent(source)) + if(H.has_status_effect(/datum/status_effect/neck_slice)) + user.show_message("[H]'s neck has already been already cut, you can't make the bleeding any worse!", 1, \ + "Their neck has already been already cut, you can't make the bleeding any worse!") + return + + H.visible_message("[user] slits [H]'s throat!", \ + "[user] slits your throat...") + log_combat(user, H, "finishes slicing the throat of") + H.apply_damage(source.force, BRUTE, BODY_ZONE_HEAD) + H.bleed_rate = CLAMP(H.bleed_rate + 20, 0, 30) + H.apply_status_effect(/datum/status_effect/neck_slice) + /datum/component/butchering/proc/Butcher(mob/living/butcher, mob/living/meat) var/turf/T = meat.drop_location() var/final_effectiveness = effectiveness - meat.butcher_difficulty diff --git a/code/datums/components/decal.dm b/code/datums/components/decal.dm index 641dbdb1cf..60317797a7 100644 --- a/code/datums/components/decal.dm +++ b/code/datums/components/decal.dm @@ -17,6 +17,7 @@ apply() /datum/component/decal/RegisterWithParent() + . = ..() if(first_dir) RegisterSignal(parent, COMSIG_ATOM_DIR_CHANGE, .proc/rotate_react) if(cleanable) @@ -25,6 +26,7 @@ RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/examine) /datum/component/decal/UnregisterFromParent() + . = ..() UnregisterSignal(parent, list(COMSIG_ATOM_DIR_CHANGE, COMSIG_COMPONENT_CLEAN_ACT, COMSIG_PARENT_EXAMINE)) /datum/component/decal/Destroy() diff --git a/code/datums/components/fantasy/_fantasy.dm b/code/datums/components/fantasy/_fantasy.dm index 86e016784a..9e8493b6f4 100644 --- a/code/datums/components/fantasy/_fantasy.dm +++ b/code/datums/components/fantasy/_fantasy.dm @@ -30,11 +30,13 @@ return ..() /datum/component/fantasy/RegisterWithParent() + . = ..() var/obj/item/master = parent originalName = master.name modify() /datum/component/fantasy/UnregisterFromParent() + . = ..() unmodify() /datum/component/fantasy/InheritComponent(datum/component/fantasy/newComp, original, list/arguments) diff --git a/code/datums/components/igniter.dm b/code/datums/components/igniter.dm index b40383e828..13944b1200 100644 --- a/code/datums/components/igniter.dm +++ b/code/datums/components/igniter.dm @@ -9,6 +9,7 @@ src.fire_stacks = fire_stacks /datum/component/igniter/RegisterWithParent() + . = ..() if(ismachinery(parent) || isstructure(parent) || isgun(parent)) // turrets, etc RegisterSignal(parent, COMSIG_PROJECTILE_ON_HIT, .proc/projectile_hit) else if(isitem(parent)) @@ -17,6 +18,7 @@ RegisterSignal(parent, COMSIG_HOSTILE_ATTACKINGTARGET, .proc/hostile_attackingtarget) /datum/component/igniter/UnregisterFromParent() + . = ..() UnregisterSignal(parent, list(COMSIG_ITEM_AFTERATTACK, COMSIG_HOSTILE_ATTACKINGTARGET, COMSIG_PROJECTILE_ON_HIT)) /datum/component/igniter/proc/item_afterattack(obj/item/source, atom/target, mob/user, proximity_flag, click_parameters) diff --git a/code/datums/components/knockback.dm b/code/datums/components/knockback.dm index b4fcaa2dd8..988a0e575e 100644 --- a/code/datums/components/knockback.dm +++ b/code/datums/components/knockback.dm @@ -10,6 +10,7 @@ src.throw_anchored = throw_anchored /datum/component/knockback/RegisterWithParent() + . = ..() if(ismachinery(parent) || isstructure(parent) || isgun(parent)) // turrets, etc RegisterSignal(parent, COMSIG_PROJECTILE_ON_HIT, .proc/projectile_hit) else if(isitem(parent)) @@ -18,6 +19,7 @@ RegisterSignal(parent, COMSIG_HOSTILE_ATTACKINGTARGET, .proc/hostile_attackingtarget) /datum/component/knockback/UnregisterFromParent() + . = ..() UnregisterSignal(parent, list(COMSIG_ITEM_AFTERATTACK, COMSIG_HOSTILE_ATTACKINGTARGET, COMSIG_PROJECTILE_ON_HIT)) /datum/component/knockback/proc/item_afterattack(obj/item/source, atom/target, mob/user, proximity_flag, click_parameters) diff --git a/code/datums/components/lifesteal.dm b/code/datums/components/lifesteal.dm index c7a78e10a3..9d62d32866 100644 --- a/code/datums/components/lifesteal.dm +++ b/code/datums/components/lifesteal.dm @@ -10,6 +10,7 @@ src.flat_heal = flat_heal /datum/component/lifesteal/RegisterWithParent() + . = ..() if(isgun(parent)) RegisterSignal(parent, COMSIG_PROJECTILE_ON_HIT, .proc/projectile_hit) else if(isitem(parent)) @@ -18,6 +19,7 @@ RegisterSignal(parent, COMSIG_HOSTILE_ATTACKINGTARGET, .proc/hostile_attackingtarget) /datum/component/lifesteal/UnregisterFromParent() + . = ..() UnregisterSignal(parent, list(COMSIG_ITEM_AFTERATTACK, COMSIG_HOSTILE_ATTACKINGTARGET, COMSIG_PROJECTILE_ON_HIT)) /datum/component/lifesteal/proc/item_afterattack(obj/item/source, atom/target, mob/user, proximity_flag, click_parameters) diff --git a/code/datums/components/mood.dm b/code/datums/components/mood.dm index a0e6f97de0..2ecd77546d 100644 --- a/code/datums/components/mood.dm +++ b/code/datums/components/mood.dm @@ -150,15 +150,6 @@ if(9) setSanity(sanity+0.4, maximum=SANITY_GREAT) - if(HAS_TRAIT(owner, TRAIT_DEPRESSION)) - if(prob(0.05)) - add_event(null, "depression", /datum/mood_event/depression) - clear_event(null, "jolly") - if(HAS_TRAIT(owner, TRAIT_JOLLY)) - if(prob(0.05)) - add_event(null, "jolly", /datum/mood_event/jolly) - clear_event(null, "depression") - HandleNutrition(owner) /datum/component/mood/proc/setSanity(amount, minimum=SANITY_INSANE, maximum=SANITY_NEUTRAL)//I'm sure bunging this in here will have no negative repercussions. diff --git a/code/datums/components/nanites.dm b/code/datums/components/nanites.dm index 362961a24f..0ef13b514b 100644 --- a/code/datums/components/nanites.dm +++ b/code/datums/components/nanites.dm @@ -34,6 +34,7 @@ cloud_sync() /datum/component/nanites/RegisterWithParent() + . = ..() RegisterSignal(parent, COMSIG_HAS_NANITES, .proc/confirm_nanites) RegisterSignal(parent, COMSIG_NANITE_UI_DATA, .proc/nanite_ui_data) RegisterSignal(parent, COMSIG_NANITE_GET_PROGRAMS, .proc/get_programs) @@ -57,6 +58,7 @@ RegisterSignal(parent, COMSIG_NANITE_SIGNAL, .proc/receive_signal) /datum/component/nanites/UnregisterFromParent() + . = ..() UnregisterSignal(parent, list(COMSIG_HAS_NANITES, COMSIG_NANITE_UI_DATA, COMSIG_NANITE_GET_PROGRAMS, diff --git a/code/datums/components/orbiter.dm b/code/datums/components/orbiter.dm index efa0fd14d5..05174c196b 100644 --- a/code/datums/components/orbiter.dm +++ b/code/datums/components/orbiter.dm @@ -20,12 +20,14 @@ begin_orbit(orbiter, radius, clockwise, rotation_speed, rotation_segments, pre_rotation) /datum/component/orbiter/RegisterWithParent() + . = ..() var/atom/target = parent while(ismovableatom(target)) RegisterSignal(target, COMSIG_MOVABLE_MOVED, .proc/move_react) target = target.loc /datum/component/orbiter/UnregisterFromParent() + . = ..() var/atom/target = parent while(ismovableatom(target)) UnregisterSignal(target, COMSIG_MOVABLE_MOVED) diff --git a/code/datums/components/rotation.dm b/code/datums/components/rotation.dm index 81ff2d517d..422d73520e 100644 --- a/code/datums/components/rotation.dm +++ b/code/datums/components/rotation.dm @@ -106,6 +106,7 @@ if(!can_be_rotated.Invoke(user, rotation) || !can_user_rotate.Invoke(user, rotation)) return BaseRot(user, rotation) + return TRUE /datum/component/simple_rotation/proc/WrenchRot(datum/source, obj/item/I, mob/living/user) if(!can_be_rotated.Invoke(user,default_rotation_direction) || !can_user_rotate.Invoke(user,default_rotation_direction)) diff --git a/code/datums/components/shrapnel.dm b/code/datums/components/shrapnel.dm index a911221f26..4d1fe21b95 100644 --- a/code/datums/components/shrapnel.dm +++ b/code/datums/components/shrapnel.dm @@ -13,10 +13,12 @@ src.override_projectile_range = override_projectile_range /datum/component/shrapnel/RegisterWithParent() + . = ..() if(ismachinery(parent) || isstructure(parent) || isgun(parent)) // turrets, etc RegisterSignal(parent, COMSIG_PROJECTILE_ON_HIT, .proc/projectile_hit) /datum/component/shrapnel/UnregisterFromParent() + . = ..() UnregisterSignal(parent, list(COMSIG_PROJECTILE_ON_HIT)) /datum/component/shrapnel/proc/projectile_hit(atom/fired_from, atom/movable/firer, atom/target, Angle) diff --git a/code/datums/components/storage/storage.dm b/code/datums/components/storage/storage.dm index c52427b546..76a9b2e6aa 100644 --- a/code/datums/components/storage/storage.dm +++ b/code/datums/components/storage/storage.dm @@ -762,7 +762,7 @@ if(!isliving(user) || !user.CanReach(parent)) return if(check_locked(source, user, TRUE)) - return + return TRUE var/atom/A = parent if(!quickdraw) @@ -770,19 +770,20 @@ user_show_to_mob(user) if(rustle_sound) playsound(A, "rustle", 50, 1, -5) - return + return TRUE - if(!user.incapacitated()) + if(user.can_hold_items() && !user.incapacitated()) var/obj/item/I = locate() in real_location() if(!I) return A.add_fingerprint(user) remove_from_storage(I, get_turf(user)) if(!user.put_in_hands(I)) - to_chat(user, "You fumble for [I] and it falls on the floor.") - return + user.visible_message("[user] fumbles with the [parent], letting [I] fall on the floor.", \ + "You fumble with [parent], letting [I] fall on the floor.") + return TRUE user.visible_message("[user] draws [I] from [parent]!", "You draw [I] from [parent].") - return + return TRUE /datum/component/storage/proc/action_trigger(datum/signal_source, datum/action/source) gather_mode_switch(source.owner) diff --git a/code/datums/components/summoning.dm b/code/datums/components/summoning.dm index 552959603d..61718301b3 100644 --- a/code/datums/components/summoning.dm +++ b/code/datums/components/summoning.dm @@ -24,6 +24,7 @@ src.faction = faction /datum/component/summoning/RegisterWithParent() + . = ..() if(ismachinery(parent) || isstructure(parent) || isgun(parent)) // turrets, etc RegisterSignal(parent, COMSIG_PROJECTILE_ON_HIT, .proc/projectile_hit) else if(isitem(parent)) @@ -32,6 +33,7 @@ RegisterSignal(parent, COMSIG_HOSTILE_ATTACKINGTARGET, .proc/hostile_attackingtarget) /datum/component/summoning/UnregisterFromParent() + . = ..() UnregisterSignal(parent, list(COMSIG_ITEM_AFTERATTACK, COMSIG_HOSTILE_ATTACKINGTARGET, COMSIG_PROJECTILE_ON_HIT)) /datum/component/summoning/proc/item_afterattack(obj/item/source, atom/target, mob/user, proximity_flag, click_parameters) diff --git a/code/datums/components/tactical.dm b/code/datums/components/tactical.dm index 5917fc3009..ba028e2fd5 100644 --- a/code/datums/components/tactical.dm +++ b/code/datums/components/tactical.dm @@ -9,10 +9,12 @@ src.allowed_slot = allowed_slot /datum/component/tactical/RegisterWithParent() + . = ..() RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, .proc/modify) RegisterSignal(parent, COMSIG_ITEM_DROPPED, .proc/unmodify) /datum/component/tactical/UnregisterFromParent() + . = ..() UnregisterSignal(parent, list(COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED)) unmodify() diff --git a/code/datums/components/virtual_reality.dm b/code/datums/components/virtual_reality.dm index 7bad836e47..2f0405af2e 100644 --- a/code/datums/components/virtual_reality.dm +++ b/code/datums/components/virtual_reality.dm @@ -1,128 +1,245 @@ +/** + * The virtual reality turned component. + * Originally created to overcome issues of mob polymorphing locking the player inside virtual reality + * and allow for a more "immersive" virtual reality in a virtual reality experience. + * It relies on comically complex order of logic, expect things to break if procs such as mind/transfer_to() are revamped. + * In short, a barebone not so hardcoded VR framework. + * If you plan to add more devices that make use of this component, remember to isolate their code outta here where possible. + */ /datum/component/virtual_reality can_transfer = TRUE - var/datum/mind/mastermind // where is my mind t. pixies + //the player's mind (not the parent's), should something happen to them or to their mob. + var/datum/mind/mastermind + //the current mob's mind, which we need to keep track for mind transfer. var/datum/mind/current_mind - var/obj/machinery/vr_sleeper/vr_sleeper + //the action datum used by the mob to quit the vr session. var/datum/action/quit_vr/quit_action + //This one's name should be self explainatory, currently used for emags. var/you_die_in_the_game_you_die_for_real = FALSE - var/datum/component/virtual_reality/inception //The component works on a very fragile link betwixt mind, ckey and death. + //Used to allow people to play recursively playing vr while playing vr without many issues. + var/datum/component/virtual_reality/level_below + var/datum/component/virtual_reality/level_above + //Used to stop the component from executing certain functions that'd cause us some issues otherwise. + //FALSE if there is a connected player, otherwise TRUE. + var/session_paused = TRUE + //Used to stop unwarranted behaviour from happening in cases where the master mind transference is unsupported. Set on Initialize(). + var/allow_mastermind_transfer = FALSE -/datum/component/virtual_reality/Initialize(mob/M, obj/machinery/vr_sleeper/gaming_pod, yolo = FALSE, new_char = TRUE) - if(!ismob(parent) || !istype(M)) +/datum/component/virtual_reality/Initialize(yolo = FALSE, _allow_mastermind_transfer = FALSE) + var/mob/M = parent + if(!istype(M) || !M.mind) return COMPONENT_INCOMPATIBLE - var/mob/vr_M = parent - mastermind = M.mind - RegisterSignal(M, list(COMSIG_MOB_DEATH, COMSIG_PARENT_QDELETING), .proc/game_over) - RegisterSignal(M, COMSIG_MOB_KEY_CHANGE, .proc/switch_player) - RegisterSignal(mastermind, COMSIG_MIND_TRANSFER, .proc/switch_player) you_die_in_the_game_you_die_for_real = yolo - quit_action = new() - if(gaming_pod) - vr_sleeper = gaming_pod - RegisterSignal(vr_sleeper, COMSIG_ATOM_EMAG_ACT, .proc/you_only_live_once) - RegisterSignal(vr_sleeper, COMSIG_MACHINE_EJECT_OCCUPANT, .proc/revert_to_reality) - vr_M.ckey = M.ckey - var/datum/component/virtual_reality/clusterfk = M.GetComponent(/datum/component/virtual_reality) - if(clusterfk && !clusterfk.inception) - clusterfk.inception = src - SStgui.close_user_uis(M, src) + allow_mastermind_transfer = _allow_mastermind_transfer + quit_action = new + +/datum/component/virtual_reality/Destroy() + QDEL_NULL(quit_action) + if(level_above) + level_above.level_below = null + level_above = null + if(level_below) + level_below.level_above = null + level_below = null + return ..() /datum/component/virtual_reality/RegisterWithParent() + . = ..() var/mob/M = parent current_mind = M.mind + if(!quit_action) + quit_action = new quit_action.Grant(M) - RegisterSignal(quit_action, COMSIG_ACTION_TRIGGER, .proc/revert_to_reality) + RegisterSignal(quit_action, COMSIG_ACTION_TRIGGER, .proc/action_trigger) RegisterSignal(M, list(COMSIG_MOB_DEATH, COMSIG_PARENT_QDELETING), .proc/game_over) RegisterSignal(M, COMSIG_MOB_GHOSTIZE, .proc/be_a_quitter) - RegisterSignal(M, COMSIG_MOB_KEY_CHANGE, .proc/pass_me_the_remote) - RegisterSignal(current_mind, COMSIG_MIND_TRANSFER, .proc/pass_me_the_remote) - mastermind.current.audiovisual_redirect = M - if(vr_sleeper) - vr_sleeper.vr_mob = M + RegisterSignal(M, COMSIG_MOB_KEY_CHANGE, .proc/on_player_transfer) + RegisterSignal(current_mind, COMSIG_MIND_TRANSFER, .proc/on_player_transfer) + RegisterSignal(current_mind, COMSIG_PRE_MIND_TRANSFER, .proc/pre_player_transfer) + if(mastermind?.current) + mastermind.current.audiovisual_redirect = M /datum/component/virtual_reality/UnregisterFromParent() - quit_action.Remove(parent) + . = ..() + if(quit_action) + quit_action.Remove(parent) + UnregisterSignal(quit_action, COMSIG_ACTION_TRIGGER) UnregisterSignal(parent, list(COMSIG_MOB_DEATH, COMSIG_PARENT_QDELETING, COMSIG_MOB_KEY_CHANGE, COMSIG_MOB_GHOSTIZE)) - UnregisterSignal(current_mind, COMSIG_MIND_TRANSFER) - UnregisterSignal(quit_action, COMSIG_ACTION_TRIGGER) + UnregisterSignal(current_mind, list(COMSIG_MIND_TRANSFER, COMSIG_PRE_MIND_TRANSFER)) current_mind = null - mastermind.current.audiovisual_redirect = null + if(mastermind?.current) + mastermind.current.audiovisual_redirect = null -/datum/component/virtual_reality/proc/switch_player(datum/source, mob/new_mob, mob/old_mob) - if(vr_sleeper || !new_mob.mind) - // Machineries currently don't deal up with the occupant being polymorphed et similar... Or did something fuck up? - revert_to_reality() - return - old_mob.audiovisual_redirect = null - new_mob.audiovisual_redirect = parent - -/datum/component/virtual_reality/proc/action_trigger(datum/signal_source, datum/action/source) - if(source != quit_action) - return COMPONENT_ACTION_BLOCK_TRIGGER - revert_to_reality(signal_source) +/** + * Called when attempting to connect a mob to a virtual reality mob. + * This will return FALSE if the mob is without player or dead. TRUE otherwise + */ +/datum/component/virtual_reality/proc/connect(mob/M) + var/mob/vr_M = parent + if(!M.mind || M.stat == DEAD || !vr_M.mind || vr_M.stat == DEAD) + return FALSE + var/datum/component/virtual_reality/VR = M.GetComponent(/datum/component/virtual_reality) + if(VR) + VR.level_below = src + level_above = VR + M.transfer_ckey(vr_M, FALSE) + mastermind = M.mind + mastermind.current.audiovisual_redirect = parent + RegisterSignal(mastermind, COMSIG_PRE_MIND_TRANSFER, .proc/switch_player) + RegisterSignal(M, list(COMSIG_MOB_DEATH, COMSIG_PARENT_QDELETING), .proc/game_over) + RegisterSignal(M, COMSIG_MOB_PRE_PLAYER_CHANGE, .proc/player_hijacked) + SStgui.close_user_uis(vr_M, src) + session_paused = FALSE + return TRUE +/** + * emag_act() hook. Makes the game deadlier, killing the mastermind mob too should the parent die. + */ /datum/component/virtual_reality/proc/you_only_live_once() - if(you_die_in_the_game_you_die_for_real || vr_sleeper?.only_current_user_can_interact) + if(you_die_in_the_game_you_die_for_real) return FALSE you_die_in_the_game_you_die_for_real = TRUE return TRUE -/datum/component/virtual_reality/proc/pass_me_the_remote(datum/source, mob/new_mob) - if(new_mob == mastermind.current) - revert_to_reality(source) - return TRUE - new_mob.TakeComponent(src) - return TRUE +/** + * Called when the mastermind mind is transferred to another mob. + * This is pretty much just going to simply quit the session until machineries support polymorphed occupants etcetera. + */ +/datum/component/virtual_reality/proc/switch_player(datum/source, mob/new_mob, mob/old_mob) + if(session_paused) + return + if(!allow_mastermind_transfer) + quit() + return COMPONENT_STOP_MIND_TRANSFER + UnregisterSignal(old_mob, list(COMSIG_MOB_DEATH, COMSIG_PARENT_QDELETING, COMSIG_MOB_PRE_PLAYER_CHANGE)) + RegisterSignal(new_mob, list(COMSIG_MOB_DEATH, COMSIG_PARENT_QDELETING), .proc/game_over) + RegisterSignal(new_mob, COMSIG_MOB_PRE_PLAYER_CHANGE, .proc/player_hijacked) + old_mob.audiovisual_redirect = null + new_mob.audiovisual_redirect = parent +/** + * Called to stop the player mind from being transferred should the new mob happen to be one of our masterminds'. + * Since the target's mind.current is going to be null'd in the mind transfer process, + * This has to be done in a different signal proc than on_player_transfer(), by then the mastermind.current will be null. + */ +/datum/component/virtual_reality/proc/pre_player_transfer(datum/source, mob/new_mob, mob/old_mob) + if(!mastermind || session_paused) + return + if(new_mob == mastermind.current) + quit() + return COMPONENT_STOP_MIND_TRANSFER + if(!level_above) + return + var/datum/component/virtual_reality/VR = level_above + while(VR) + if(VR.mastermind.current == new_mob) + VR.quit() //this will revert the ckey back to new_mob. + return COMPONENT_STOP_MIND_TRANSFER + VR = VR.level_above + +/** + * Called when someone or something else is somewhat about to replace the mastermind's mob key somehow. + * And potentially lock the player in a broken virtual reality plot. Not really something to be proud of. + */ +/datum/component/virtual_reality/proc/player_hijacked(datum/source, mob/our_character, mob/their_character) + if(session_paused) + return + if(!their_character) + quit(cleanup = TRUE) + return + var/will_it_be_handled_in_their_pre_player_transfer = FALSE + var/datum/component/virtual_reality/VR = src + while(VR) + if(VR.parent == their_character) + will_it_be_handled_in_their_pre_player_transfer = TRUE + break + VR = VR.level_below + if(!will_it_be_handled_in_their_pre_player_transfer) //it's not the player playing shenanigeans, abandon all ships. + quit(cleanup = TRUE) + +/** + * Takes care of moving the component from a mob to another when their mind or ckey is transferred. + * The very reason this component even exists (else one would be stuck playing as a monky if monkyified) + */ +/datum/component/virtual_reality/proc/on_player_transfer(datum/source, mob/new_mob, mob/old_mob) + new_mob.TakeComponent(src) + +/** + * Required for the component to be transferable from mob to mob. + */ /datum/component/virtual_reality/PostTransfer() if(!ismob(parent)) return COMPONENT_INCOMPATIBLE +/** + *The following procs simply acts as hooks for quit(), since components do not use callbacks anymore + */ +/datum/component/virtual_reality/proc/action_trigger(datum/signal_source, datum/action/source) + quit() + return COMPONENT_ACTION_BLOCK_TRIGGER + /datum/component/virtual_reality/proc/revert_to_reality(datum/source) - quit_it() + quit() /datum/component/virtual_reality/proc/game_over(datum/source) - quit_it(TRUE, TRUE) + quit(you_die_in_the_game_you_die_for_real, TRUE) + return COMPONENT_BLOCK_DEATH_BROADCAST -/datum/component/virtual_reality/proc/be_a_quitter(datum/source, can_reenter_corpse) - quit_it() - return COMPONENT_BLOCK_GHOSTING +/datum/component/virtual_reality/proc/be_a_quitter(datum/source, can_reenter_corpse, special = FALSE, penalize = FALSE) + if(!special) + quit() + return COMPONENT_BLOCK_GHOSTING -/datum/component/virtual_reality/proc/virtual_reality_in_a_virtual_reality(mob/player, killme = FALSE, datum/component/virtual_reality/yo_dawg) +/datum/component/virtual_reality/proc/machine_destroyed(datum/source) + quit(cleanup = TRUE) + +/** + * Takes care of deleting itself, moving the player back to the mastermind's current and queueing the parent for deletion. + * It supports nested virtual realities by recursively calling vr_in_a_vr(), which in turns calls quit(), + * up to the deepest level, where the ckey will be transferred back to our mastermind's mob instead. + * The above operation is skipped when session_paused is TRUE (ergo no player in control of the current mob). + * vars: + * * deathcheck is used to kill the master, you want this FALSE unless for stuff that doesn't involve emagging. + * * cleanup is used to queue the parent for the next vr_clean_master's run, where they'll be deleted should they be dead. + * * mob/override is used for the recursive virtual reality explained above and shouldn't be used outside of vr_in_a_vr(). + */ +/datum/component/virtual_reality/proc/quit(deathcheck = FALSE, cleanup = FALSE, mob/override) var/mob/M = parent - quit_it(FALSE, killme, player, yo_dawg) - yo_dawg.inception = null - if(killme) - M.death(FALSE) - -/datum/component/virtual_reality/proc/quit_it(deathcheck = FALSE, cleanup = FALSE, mob/override) - var/mob/M = parent - var/mob/dreamer = override ? override : mastermind.current - if(!mastermind) - to_chat(M, "You feel a dreadful sensation, something terrible happened. You try to wake up, but you find yourself unable to...") - else - var/key_transfer = FALSE - if(inception?.parent) - inception.virtual_reality_in_a_virtual_reality(dreamer, cleanup, src) + if(!session_paused) + session_paused = TRUE + var/mob/dreamer = override || mastermind.current + if(!dreamer) //This shouldn't happen. + stack_trace("virtual reality component quit() called without a mob to transfer the parent ckey to.") + to_chat(M, "You feel a dreadful sensation, something terrible happened. You try to wake up, but you find yourself unable to...") + qdel(src) + return + if(level_below?.parent) + level_below.vr_in_a_vr(dreamer, deathcheck, (deathcheck && cleanup)) else - key_transfer = TRUE - if(key_transfer) M.transfer_ckey(dreamer, FALSE) - dreamer.stop_sound_channel(CHANNEL_HEARTBEAT) - dreamer.audiovisual_redirect = null - if(deathcheck && you_die_in_the_game_you_die_for_real) - to_chat(mastermind, "You feel everything fading away...") - dreamer.death(FALSE) - if(cleanup) - var/obj/effect/vr_clean_master/cleanbot = locate() in get_area(M) - if(cleanbot) - LAZYADD(cleanbot.corpse_party, M) - if(vr_sleeper) - vr_sleeper.vr_mob = null - vr_sleeper = null - qdel(src) + if(deathcheck) + to_chat(dreamer, "You feel everything fading away...") + dreamer.death(FALSE) + mastermind.current.audiovisual_redirect = null + if(!cleanup) + if(level_above) + level_above.level_below = null + level_above = null + UnregisterSignal(mastermind.current, list(COMSIG_MOB_DEATH, COMSIG_PARENT_QDELETING, COMSIG_MOB_PRE_PLAYER_CHANGE)) + UnregisterSignal(mastermind, COMSIG_PRE_MIND_TRANSFER) + mastermind = null + if(cleanup) + var/obj/effect/vr_clean_master/cleanbot = locate() in get_area(M) + if(cleanbot) + LAZYOR(cleanbot.corpse_party, M) + qdel(src) -/datum/component/virtual_reality/Destroy() - var/datum/action/quit_vr/delet_me = quit_action - . = ..() - qdel(delet_me) \ No newline at end of file +/** + * Used for recursive virtual realities shenanigeans and should be called only through the above proc. + */ +/datum/component/virtual_reality/proc/vr_in_a_vr(mob/player, deathcheck = FALSE, lethal_cleanup = FALSE) + var/mob/M = parent + quit(deathcheck, lethal_cleanup, player) + M.audiovisual_redirect = null + if(lethal_cleanup) + M.death(FALSE) diff --git a/code/datums/components/wet_floor.dm b/code/datums/components/wet_floor.dm index 38b17993d8..d6c5c0bf83 100644 --- a/code/datums/components/wet_floor.dm +++ b/code/datums/components/wet_floor.dm @@ -34,10 +34,12 @@ last_process = world.time /datum/component/wet_floor/RegisterWithParent() + . = ..() RegisterSignal(parent, COMSIG_TURF_IS_WET, .proc/is_wet) RegisterSignal(parent, COMSIG_TURF_MAKE_DRY, .proc/dry) /datum/component/wet_floor/UnregisterFromParent() + . = ..() UnregisterSignal(parent, list(COMSIG_TURF_IS_WET, COMSIG_TURF_MAKE_DRY)) /datum/component/wet_floor/Destroy() diff --git a/code/datums/datacore.dm b/code/datums/datacore.dm index 60bb24c8c2..cf33fa3633 100644 --- a/code/datums/datacore.dm +++ b/code/datums/datacore.dm @@ -255,7 +255,7 @@ M.fields["alg_d"] = "No allergies have been detected in this patient." M.fields["cdi"] = "None" M.fields["cdi_d"] = "No diseases have been diagnosed at the moment." - M.fields["notes"] = "No notes." + M.fields["notes"] = H.get_trait_string(medical) medical += M //Security Record diff --git a/code/datums/datumvars.dm b/code/datums/datumvars.dm index e44134f01f..feac1de972 100644 --- a/code/datums/datumvars.dm +++ b/code/datums/datumvars.dm @@ -1398,3 +1398,29 @@ var/mob/living/carbon/human/H = locate(href_list["copyoutfit"]) in GLOB.carbon_list if(istype(H)) H.copy_outfit() + else if(href_list["modquirks"]) + if(!check_rights(R_SPAWN)) + return + + var/mob/living/carbon/human/H = locate(href_list["modquirks"]) in GLOB.mob_list + if(!istype(H)) + to_chat(usr, "This can only be done to instances of type /mob/living/carbon/human") + return + + var/list/options = list("Clear"="Clear") + for(var/x in subtypesof(/datum/quirk)) + var/datum/quirk/T = x + var/qname = initial(T.name) + options[H.has_quirk(T) ? "[qname] (Remove)" : "[qname] (Add)"] = T + + var/result = input(usr, "Choose quirk to add/remove","Quirk Mod") as null|anything in options + if(result) + if(result == "Clear") + for(var/datum/quirk/q in H.roundstart_quirks) + H.remove_quirk(q.type) + else + var/T = options[result] + if(H.has_quirk(T)) + H.remove_quirk(T) + else + H.add_quirk(T,TRUE) diff --git a/code/datums/dna.dm b/code/datums/dna.dm index 8a3fd82cb9..ccc5d97cf8 100644 --- a/code/datums/dna.dm +++ b/code/datums/dna.dm @@ -254,7 +254,6 @@ if(icon_update) update_body() update_hair() - update_body_parts() update_mutations_overlay()// no lizard with human hulk overlay please. diff --git a/code/datums/hud.dm b/code/datums/hud.dm index 32bce89f4d..169b08dc98 100644 --- a/code/datums/hud.dm +++ b/code/datums/hud.dm @@ -27,6 +27,7 @@ GLOBAL_LIST_INIT(huds, list( ANTAG_HUD_SOULLESS = new/datum/atom_hud/antag/hidden(), ANTAG_HUD_CLOCKWORK = new/datum/atom_hud/antag(), ANTAG_HUD_BROTHER = new/datum/atom_hud/antag/hidden(), + ANTAG_HUD_BLOODSUCKER = new/datum/atom_hud/antag/bloodsucker() )) /datum/atom_hud diff --git a/code/datums/martial.dm b/code/datums/martial.dm index 26a709590c..32850c69df 100644 --- a/code/datums/martial.dm +++ b/code/datums/martial.dm @@ -8,9 +8,7 @@ var/deflection_chance = 0 //Chance to deflect projectiles var/reroute_deflection = FALSE //Delete the bullet, or actually deflect it in some direction? var/block_chance = 0 //Chance to block melee attacks using items while on throw mode. - var/restraining = 0 //used in cqc's disarm_act to check if the disarmed is being restrained and so whether they should be put in a chokehold or not var/help_verb - var/no_guns = FALSE var/pacifism_check = TRUE //are the martial arts combos/attacks unable to be used by pacifist. var/allow_temp_override = TRUE //if this martial art can be overridden by temporary martial arts @@ -28,14 +26,16 @@ /datum/martial_art/proc/add_to_streak(element,mob/living/carbon/human/D) if(D != current_target) - current_target = D - streak = "" - restraining = 0 + reset_streak(D) streak = streak+element if(length(streak) > max_streak_length) streak = copytext(streak,2) return +/datum/martial_art/proc/reset_streak(mob/living/carbon/human/new_target) + current_target = new_target + streak = "" + /datum/martial_art/proc/basic_hit(mob/living/carbon/human/A,mob/living/carbon/human/D) var/damage = rand(A.dna.species.punchdamagelow, A.dna.species.punchdamagehigh) @@ -81,7 +81,7 @@ D.forcesay(GLOB.hit_appends) return 1 -/datum/martial_art/proc/teach(mob/living/carbon/human/H,make_temporary=0) +/datum/martial_art/proc/teach(mob/living/carbon/human/H, make_temporary = FALSE) if(!istype(H) || !H.mind) return FALSE if(H.mind.martial_art) diff --git a/code/datums/martial/cqc.dm b/code/datums/martial/cqc.dm index 73173a4a9a..09a493a670 100644 --- a/code/datums/martial/cqc.dm +++ b/code/datums/martial/cqc.dm @@ -9,24 +9,13 @@ id = MARTIALART_CQC help_verb = /mob/living/carbon/human/proc/CQC_help block_chance = 75 - var/just_a_cook = FALSE - var/static/list/areas_under_siege = typecacheof(list(/area/crew_quarters/kitchen, - /area/crew_quarters/cafeteria, - /area/crew_quarters/bar)) + var/old_grab_state = null + var/restraining = FALSE -/datum/martial_art/cqc/under_siege - name = "Close Quarters Cooking" - just_a_cook = TRUE - -/datum/martial_art/cqc/proc/drop_restraining() +/datum/martial_art/cqc/reset_streak(mob/living/carbon/human/new_target) + . = ..() restraining = FALSE -/datum/martial_art/cqc/can_use(mob/living/carbon/human/H) - var/area/A = get_area(H) - if(just_a_cook && !(is_type_in_typecache(A, areas_under_siege))) - return FALSE - return ..() - /datum/martial_art/cqc/proc/check_streak(mob/living/carbon/human/A, mob/living/carbon/human/D) if(!can_use(A)) return FALSE @@ -75,6 +64,7 @@ D.apply_damage(10, BRUTE) log_combat(A, D, "kicked (CQC)") if(D.IsKnockdown() && !D.stat) + log_combat(A, D, "knocked out (Head kick)(CQC)") D.visible_message("[A] kicks [D]'s head, knocking [D.p_them()] out!", \ "[A] kicks your head, knocking you out!") playsound(get_turf(A), 'sound/weapons/genhit1.ogg', 50, 1, -1) @@ -85,7 +75,8 @@ /datum/martial_art/cqc/proc/Pressure(mob/living/carbon/human/A, mob/living/carbon/human/D) if(!can_use(A)) return FALSE - D.visible_message("[A] forces their arm on [D]'s neck!") + log_combat(A, D, "pressured (CQC)") + D.visible_message("[A] punches [D]'s neck!") D.adjustStaminaLoss(60) playsound(get_turf(A), 'sound/weapons/cqchit1.ogg', 50, 1, -1) return TRUE @@ -96,18 +87,20 @@ if(!can_use(A)) return FALSE if(!D.stat) + log_combat(A, D, "restrained (CQC)") D.visible_message("[A] locks [D] into a restraining position!", \ "[A] locks you into a restraining position!") D.adjustStaminaLoss(20) D.Stun(100) restraining = TRUE - addtimer(CALLBACK(src, .proc/drop_restraining), 50, TIMER_UNIQUE) + addtimer(VARSET_CALLBACK(src, restraining, FALSE), 50, TIMER_UNIQUE) return TRUE /datum/martial_art/cqc/proc/Consecutive(mob/living/carbon/human/A, mob/living/carbon/human/D) if(!can_use(A)) return FALSE if(!D.stat) + log_combat(A, D, "consecutive CQC'd (CQC)") D.visible_message("[A] strikes [D]'s abdomen, neck and back consecutively", \ "[A] strikes your abdomen, neck and back consecutively!") playsound(get_turf(D), 'sound/weapons/cqchit2.ogg', 50, 1, -1) @@ -119,23 +112,20 @@ return TRUE /datum/martial_art/cqc/grab_act(mob/living/carbon/human/A, mob/living/carbon/human/D) - if(!can_use(A)) - return FALSE - add_to_streak("G",D) - if(check_streak(A,D)) - return TRUE - if(A == D) // no self grab. - return FALSE - if(A.grab_state >= GRAB_AGGRESSIVE) + if(A.a_intent == INTENT_GRAB && A!=D && can_use(A)) // A!=D prevents grabbing yourself + add_to_streak("G",D) + if(check_streak(A,D)) //if a combo is made no grab upgrade is done + return TRUE + old_grab_state = A.grab_state D.grabbedby(A, 1) - else - A.start_pulling(D, 1) - if(A.pulling) - D.stop_pulling() + if(old_grab_state == GRAB_PASSIVE) + D.drop_all_held_items() + A.grab_state = GRAB_AGGRESSIVE //Instant agressive grab if on grab intent log_combat(A, D, "grabbed", addition="aggressively") - A.grab_state = GRAB_AGGRESSIVE //Instant aggressive grab - - return TRUE + D.visible_message("[A] violently grabs [D]!", \ + "[A] violently grabs you!") + return TRUE + return FALSE /datum/martial_art/cqc/harm_act(mob/living/carbon/human/A, mob/living/carbon/human/D) if(!can_use(A)) @@ -190,6 +180,7 @@ playsound(D, 'sound/weapons/punchmiss.ogg', 25, 1, -1) log_combat(A, D, "disarmed (CQC)", "[I ? " grabbing \the [I]" : ""]") if(restraining && A.pulling == D) + log_combat(A, D, "knocked out (Chokehold)(CQC)") D.visible_message("[A] puts [D] into a chokehold!", \ "[A] puts you into a chokehold!") D.SetSleeping(400) @@ -208,9 +199,19 @@ to_chat(usr, "You try to remember some of the basics of CQC.") to_chat(usr, "Slam: Grab Harm. Slam opponent into the ground, knocking them down.") - to_chat(usr, "CQC Kick: Disarm Harm Harm. Knocks opponent away. Knocks out stunned or knocked down opponents.") - to_chat(usr, "Restrain: Grab Grab. Locks opponents into a restraining position, disarm to knock them out with a choke hold.") + to_chat(usr, "CQC Kick: Harm Harm. Knocks opponent away. Knocks out stunned or knocked down opponents.") + to_chat(usr, "Restrain: Grab Grab. Locks opponents into a restraining position, disarm to knock them out with a chokehold.") to_chat(usr, "Pressure: Disarm Grab. Decent stamina damage.") to_chat(usr, "Consecutive CQC: Disarm Disarm Harm. Mainly offensive move, huge damage and decent stamina damage.") to_chat(usr, "In addition, by having your throw mode on when being attacked, you enter an active defense mode where you have a chance to block and sometimes even counter attacks done to you.") + +///Subtype of CQC. Only used for the chef. +/datum/martial_art/cqc/under_siege + name = "Close Quarters Cooking" + +///Prevents use if the cook is not in the kitchen. +/datum/martial_art/cqc/under_siege/can_use(mob/living/carbon/human/H) //this is used to make chef CQC only work in kitchen + if(!istype(get_area(H), /area/crew_quarters/kitchen)) + return FALSE + return ..() diff --git a/code/datums/martial/sleeping_carp.dm b/code/datums/martial/sleeping_carp.dm index f89374dc2a..bb652208ee 100644 --- a/code/datums/martial/sleeping_carp.dm +++ b/code/datums/martial/sleeping_carp.dm @@ -9,35 +9,36 @@ id = MARTIALART_SLEEPINGCARP deflection_chance = 100 reroute_deflection = TRUE - no_guns = TRUE allow_temp_override = FALSE help_verb = /mob/living/carbon/human/proc/sleeping_carp_help + var/old_grab_state = null /datum/martial_art/the_sleeping_carp/proc/check_streak(mob/living/carbon/human/A, mob/living/carbon/human/D) if(findtext(streak,WRIST_WRENCH_COMBO)) streak = "" wristWrench(A,D) - return 1 + return TRUE if(findtext(streak,BACK_KICK_COMBO)) streak = "" backKick(A,D) - return 1 + return TRUE if(findtext(streak,STOMACH_KNEE_COMBO)) streak = "" kneeStomach(A,D) - return 1 + return TRUE if(findtext(streak,HEAD_KICK_COMBO)) streak = "" headKick(A,D) - return 1 + return TRUE if(findtext(streak,ELBOW_DROP_COMBO)) streak = "" elbowDrop(A,D) - return 1 - return 0 + return TRUE + return FALSE /datum/martial_art/the_sleeping_carp/proc/wristWrench(mob/living/carbon/human/A, mob/living/carbon/human/D) if(!D.stat && !D.IsStun() && !D.IsKnockdown()) + log_combat(A, D, "wrist wrenched (Sleeping Carp)") A.do_attack_animation(D, ATTACK_EFFECT_PUNCH) D.visible_message("[A] grabs [D]'s wrist and wrenches it sideways!", \ "[A] grabs your wrist and violently wrenches it to the side!") @@ -46,24 +47,29 @@ D.dropItemToGround(D.get_active_held_item()) D.apply_damage(5, BRUTE, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM)) D.Knockdown(60)//CIT CHANGE - makes sleepingcarp use knockdown() for its stuns instead of stun() - return 1 - log_combat(A, D, "wrist wrenched (Sleeping Carp)") + return TRUE return basic_hit(A,D) /datum/martial_art/the_sleeping_carp/proc/backKick(mob/living/carbon/human/A, mob/living/carbon/human/D) - if(A.dir == D.dir && !D.stat && !D.IsKnockdown()) - A.do_attack_animation(D, ATTACK_EFFECT_PUNCH) - D.visible_message("[A] kicks [D] in the back!", \ - "[A] kicks you in the back, making you stumble and fall!") - step_to(D,get_step(D,D.dir),1) - D.Knockdown(80) - playsound(get_turf(D), 'sound/weapons/punch1.ogg', 50, 1, -1) - return 1 - log_combat(A, D, "back-kicked (Sleeping Carp)") + if(!D.stat && !D.IsKnockdown()) + if(A.dir == D.dir) + log_combat(A, D, "back-kicked (Sleeping Carp)") + A.do_attack_animation(D, ATTACK_EFFECT_PUNCH) + D.visible_message("[A] kicks [D] in the back!", \ + "[A] kicks you in the back, making you stumble and fall!") + step_to(D,get_step(D,D.dir),1) + D.Knockdown(80) + playsound(get_turf(D), 'sound/weapons/punch1.ogg', 50, 1, -1) + return TRUE + else + log_combat(A, D, "missed a back-kick (Sleeping Carp) on") + D.visible_message("[A] tries to kick [D] in the back, but misses!", \ + "[A] tries to kick you in the back, but misses!") return basic_hit(A,D) /datum/martial_art/the_sleeping_carp/proc/kneeStomach(mob/living/carbon/human/A, mob/living/carbon/human/D) if(!D.stat && !D.IsKnockdown()) + log_combat(A, D, "stomach kneed (Sleeping Carp)") A.do_attack_animation(D, ATTACK_EFFECT_KICK) D.visible_message("[A] knees [D] in the stomach!", \ "[A] winds you with a knee in the stomach!") @@ -71,12 +77,12 @@ D.losebreath += 3 D.Knockdown(40)//CIT CHANGE - makes sleepingcarp use knockdown() for its stuns instead of stun() playsound(get_turf(D), 'sound/weapons/punch1.ogg', 50, 1, -1) - return 1 - log_combat(A, D, "stomach kneed (Sleeping Carp)") + return TRUE return basic_hit(A,D) /datum/martial_art/the_sleeping_carp/proc/headKick(mob/living/carbon/human/A, mob/living/carbon/human/D) if(!D.stat && !D.IsKnockdown()) + log_combat(A, D, "head kicked (Sleeping Carp)") A.do_attack_animation(D, ATTACK_EFFECT_KICK) D.visible_message("[A] kicks [D] in the head!", \ "[A] kicks you in the jaw!") @@ -84,12 +90,12 @@ D.drop_all_held_items() playsound(get_turf(D), 'sound/weapons/punch1.ogg', 50, 1, -1) D.Knockdown(80)//CIT CHANGE - makes sleepingcarp use knockdown() for its stuns instead of stun() - return 1 - log_combat(A, D, "head kicked (Sleeping Carp)") + return TRUE return basic_hit(A,D) /datum/martial_art/the_sleeping_carp/proc/elbowDrop(mob/living/carbon/human/A, mob/living/carbon/human/D) if(D.IsKnockdown() || D.resting || D.stat) + log_combat(A, D, "elbow dropped (Sleeping Carp)") A.do_attack_animation(D, ATTACK_EFFECT_PUNCH) D.visible_message("[A] elbow drops [D]!", \ "[A] piledrives you with their elbow!") @@ -97,37 +103,29 @@ D.death() //FINISH HIM! D.apply_damage(50, BRUTE, BODY_ZONE_CHEST) playsound(get_turf(D), 'sound/weapons/punch1.ogg', 75, 1, -1) - return 1 - log_combat(A, D, "elbow dropped (Sleeping Carp)") + return TRUE return basic_hit(A,D) /datum/martial_art/the_sleeping_carp/grab_act(mob/living/carbon/human/A, mob/living/carbon/human/D) - add_to_streak("G",D) - if(check_streak(A,D)) - return 1 - if(A == D) //no self grab stun - return FALSE - if(A.grab_state >= GRAB_AGGRESSIVE) + if(A.a_intent == INTENT_GRAB && A!=D) // A!=D prevents grabbing yourself + add_to_streak("G",D) + if(check_streak(A,D)) //if a combo is made no grab upgrade is done + return TRUE + old_grab_state = A.grab_state D.grabbedby(A, 1) - else - A.start_pulling(D, 1) - if(A.pulling) + if(old_grab_state == GRAB_PASSIVE) D.drop_all_held_items() - D.stop_pulling() - if(A.a_intent == INTENT_GRAB) - log_combat(A, D, "grabbed", addition="aggressively") - D.visible_message("[A] violently grabs [D]!", \ - "[A] violently grabs you!") - A.grab_state = GRAB_AGGRESSIVE //Instant aggressive grab - else - log_combat(A, D, "grabbed", addition="passively") - A.grab_state = GRAB_PASSIVE - return 1 + A.grab_state = GRAB_AGGRESSIVE //Instant agressive grab if on grab intent + log_combat(A, D, "grabbed", addition="aggressively") + D.visible_message("[A] violently grabs [D]!", \ + "[A] violently grabs you!") + return TRUE + return FALSE /datum/martial_art/the_sleeping_carp/harm_act(mob/living/carbon/human/A, mob/living/carbon/human/D) add_to_streak("H",D) if(check_streak(A,D)) - return 1 + return TRUE A.do_attack_animation(D, ATTACK_EFFECT_PUNCH) var/atk_verb = pick("punches", "kicks", "chops", "hits", "slams") D.visible_message("[A] [atk_verb] [D]!", \ @@ -138,15 +136,25 @@ D.visible_message("[D] stumbles and falls!", "The blow sends you to the ground!") D.Knockdown(80) log_combat(A, D, "[atk_verb] (Sleeping Carp)") - return 1 + return TRUE /datum/martial_art/the_sleeping_carp/disarm_act(mob/living/carbon/human/A, mob/living/carbon/human/D) add_to_streak("D",D) if(check_streak(A,D)) - return 1 + return TRUE return ..() +/datum/martial_art/the_sleeping_carp/teach(mob/living/carbon/human/H, make_temporary = FALSE) + . = ..() + if(!.) + return + ADD_TRAIT(H, TRAIT_NOGUNS, SLEEPING_CARP_TRAIT) + +/datum/martial_art/the_sleeping_carp/on_remove(mob/living/carbon/human/H) + . = ..() + REMOVE_TRAIT(H, TRAIT_NOGUNS, SLEEPING_CARP_TRAIT) + /mob/living/carbon/human/proc/sleeping_carp_help() set name = "Recall Teachings" set desc = "Remember the martial techniques of the Sleeping Carp clan." @@ -233,4 +241,4 @@ /obj/item/twohanded/bostaff/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK) if(wielded) return ..() - return 0 + return FALSE diff --git a/code/datums/mind.dm b/code/datums/mind.dm index 0ded5f9912..d1b4e51a7d 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -88,6 +88,9 @@ /datum/mind/proc/transfer_to(mob/new_character, var/force_key_move = 0) var/old_character = current + var/signals = SEND_SIGNAL(new_character, COMSIG_MOB_PRE_PLAYER_CHANGE, new_character, old_character) | SEND_SIGNAL(src, COMSIG_PRE_MIND_TRANSFER, new_character, old_character) + if(signals & COMPONENT_STOP_MIND_TRANSFER) + return if(current) // remove ourself from our old body's mind variable current.mind = null SStgui.on_transfer(current, new_character) @@ -125,7 +128,6 @@ transfer_martial_arts(new_character) if(active || force_key_move) new_character.key = key //now transfer the key to link the client to our new body - SEND_SIGNAL(src, COMSIG_MIND_TRANSFER, new_character, old_character) //CIT CHANGE - makes arousal update when transfering bodies if(isliving(new_character)) //New humans and such are by default enabled arousal. Let's always use the new mind's prefs. @@ -134,6 +136,8 @@ L.canbearoused = L.client.prefs.arousable //Technically this should make taking over a character mean the body gain the new minds setting... L.update_arousal_hud() //Removes the old icon + SEND_SIGNAL(src, COMSIG_MIND_TRANSFER, new_character, old_character) + /datum/mind/proc/store_memory(new_text) if((length(memory) + length(new_text)) <= MAX_MESSAGE_LEN) memory += "[new_text]
" diff --git a/code/datums/mood_events/generic_negative_events.dm b/code/datums/mood_events/generic_negative_events.dm index 94a64c3bd4..a6e4c8ed4f 100644 --- a/code/datums/mood_events/generic_negative_events.dm +++ b/code/datums/mood_events/generic_negative_events.dm @@ -198,3 +198,48 @@ description = "It feels quite cold out here.\n" mood_change = -2 timeout = 1 MINUTES + +/datum/mood_event/vampcandle + description = "Something is making your mind feel... loose...\n" + mood_change = -10 + timeout = 1 MINUTES + +/datum/mood_event/drankblood_bad + description = "I drank the blood of a lesser creature. Disgusting.\n" + mood_change = -4 + timeout = 900 + +/datum/mood_event/drankblood_dead + description = "I drank dead blood. I am better than this.\n" + mood_change = -7 + timeout = 900 + +/datum/mood_event/drankblood_synth + description = "I drank synthetic blood. What is wrong with me?\n" + mood_change = -7 + timeout = 900 + +/datum/mood_event/drankkilled + description = "I drank from my victim until they died. I feel...less human.\n" + mood_change = -12 + timeout = 6000 + +/datum/mood_event/madevamp + description = "A soul has been cursed to undeath by my own hand.\n" + mood_change = -10 + timeout = 10000 + +/datum/mood_event/vampatefood + description = "Mortal nourishment no longer sustains me. I feel unwell.\n" + mood_change = -6 + timeout = 1000 + +/datum/mood_event/daylight_1 + description = "I slept poorly in a makeshift coffin during the day.\n" + mood_change = -3 + timeout = 1000 + +/datum/mood_event/daylight_2 + description = "I have been scorched by the unforgiving rays of the sun.\n" + mood_change = -6 + timeout = 1200 diff --git a/code/datums/mood_events/generic_positive_events.dm b/code/datums/mood_events/generic_positive_events.dm index 9ae7ba2289..19be10e668 100644 --- a/code/datums/mood_events/generic_positive_events.dm +++ b/code/datums/mood_events/generic_positive_events.dm @@ -134,6 +134,15 @@ mood_change = 3 timeout = 3000 +/datum/mood_event/drankblood + description = "I have fed greedly from that which nourishes me.\n" + mood_change = 10 + timeout = 900 + +/datum/mood_event/coffinsleep + description = "I slept in a coffin during the day. I feel whole again.\n" + mood_change = 8 + timeout = 1200 //Cursed stuff below. /datum/mood_event/orgasm @@ -148,3 +157,8 @@ /datum/mood_event/fedprey description = "It feels quite cozy in here.\n" mood_change = 3 + +/datum/mood_event/hope_lavaland + description = "What a peculiar emblem. It makes me feel hopeful for my future.\n" + mood_change = 5 + diff --git a/code/datums/mutations/hulk.dm b/code/datums/mutations/hulk.dm index 3d07e6066f..c536196e95 100644 --- a/code/datums/mutations/hulk.dm +++ b/code/datums/mutations/hulk.dm @@ -13,6 +13,7 @@ return ADD_TRAIT(owner, TRAIT_STUNIMMUNE, TRAIT_HULK) ADD_TRAIT(owner, TRAIT_PUSHIMMUNE, TRAIT_HULK) + ADD_TRAIT(owner, TRAIT_CHUNKYFINGERS, TRAIT_HULK) owner.update_body_parts() SEND_SIGNAL(owner, COMSIG_ADD_MOOD_EVENT, "hulk", /datum/mood_event/hulk) RegisterSignal(owner, COMSIG_MOB_SAY, .proc/handle_speech) @@ -31,6 +32,7 @@ return REMOVE_TRAIT(owner, TRAIT_STUNIMMUNE, TRAIT_HULK) REMOVE_TRAIT(owner, TRAIT_PUSHIMMUNE, TRAIT_HULK) + ADD_TRAIT(owner, TRAIT_CHUNKYFINGERS, TRAIT_HULK) owner.update_body_parts() SEND_SIGNAL(owner, COMSIG_CLEAR_MOOD_EVENT, "hulk") UnregisterSignal(owner, COMSIG_MOB_SAY) diff --git a/code/datums/ruins/lavaland.dm b/code/datums/ruins/lavaland.dm index 4f18e6fd62..7ea2d2615d 100644 --- a/code/datums/ruins/lavaland.dm +++ b/code/datums/ruins/lavaland.dm @@ -229,4 +229,13 @@ id = "puzzle" description = "Mystery to be solved." suffix = "lavaland_surface_puzzle.dmm" - cost = 5 \ No newline at end of file + cost = 5 + +/datum/map_template/ruin/lavaland/elite_tumor + name = "Pulsating Tumor" + id = "tumor" + description = "A strange tumor which houses a powerful beast..." + suffix = "lavaland_surface_elite_tumor.dmm" + cost = 5 + always_place = TRUE + allow_duplicates = TRUE diff --git a/code/datums/status_effects/debuffs.dm b/code/datums/status_effects/debuffs.dm index d1196a7753..f5f012e7f9 100644 --- a/code/datums/status_effects/debuffs.dm +++ b/code/datums/status_effects/debuffs.dm @@ -80,34 +80,55 @@ desc = "You've fallen asleep. Wait a bit and you should wake up. Unless you don't, considering how helpless you are." icon_state = "asleep" -//TASER -/datum/status_effect/electrode - id = "tased" +/datum/status_effect/no_combat_mode/ + id = "no_combat_mode" blocks_combatmode = TRUE - status_type = STATUS_EFFECT_REPLACE alert_type = null + status_type = STATUS_EFFECT_REPLACE -/datum/status_effect/electrode/on_creation(mob/living/new_owner, set_duration) +/datum/status_effect/no_combat_mode/on_creation(mob/living/new_owner, set_duration) if(isnum(set_duration)) duration = set_duration . = ..() + if(iscarbon(owner)) + var/mob/living/carbon/C = owner + if(C.combatmode) + C.toggle_combat_mode(TRUE) + +/datum/status_effect/no_combat_mode/mesmerize + id = "Mesmerize" + alert_type = /obj/screen/alert/status_effect/mesmerized + +/obj/screen/alert/status_effect/mesmerized + name = "Mesmerized" + desc = "You cant tear your sight from who is in front of you...Their gaze is simply too enthralling.." + icon = 'icons/mob/actions/bloodsucker.dmi' + icon_state = "power_mez" + +/datum/status_effect/no_combat_mode/electrode + id = "tased" + +/datum/status_effect/no_combat_mode/electrode/on_creation(mob/living/new_owner, set_duration) + if(isnum(set_duration)) //TODO, figure out how to grab from subtype + duration = set_duration + . = ..() if(iscarbon(owner)) var/mob/living/carbon/C = owner if(C.combatmode) C.toggle_combat_mode(TRUE) C.add_movespeed_modifier(MOVESPEED_ID_TASED_STATUS, TRUE, override = TRUE, multiplicative_slowdown = 8) -/datum/status_effect/electrode/on_remove() +/datum/status_effect/no_combat_mode/electrode/on_remove() if(iscarbon(owner)) var/mob/living/carbon/C = owner C.remove_movespeed_modifier(MOVESPEED_ID_TASED_STATUS) . = ..() -/datum/status_effect/electrode/tick() +/datum/status_effect/no_combat_mode/electrode/tick() if(owner) owner.adjustStaminaLoss(5) //if you really want to try to stamcrit someone with a taser alone, you can, but it'll take time and good timing. -/datum/status_effect/electrode/nextmove_modifier() //why is this a proc. its no big deal since this doesnt get called often at all but literally w h y +/datum/status_effect/no_combat_mode/electrode/nextmove_modifier() //why is this a proc. its no big deal since this doesnt get called often at all but literally w h y return 2 //OTHER DEBUFFS @@ -388,6 +409,19 @@ else new /obj/effect/temp_visual/bleed(get_turf(owner)) +/datum/status_effect/neck_slice + id = "neck_slice" + status_type = STATUS_EFFECT_UNIQUE + alert_type = null + duration = -1 + +/datum/status_effect/neck_slice/tick() + var/mob/living/carbon/human/H = owner + if(H.stat == DEAD || H.bleed_rate <= 8) + H.remove_status_effect(/datum/status_effect/neck_slice) + if(prob(10)) + H.emote(pick("gasp", "gag", "choke")) + /mob/living/proc/apply_necropolis_curse(set_curse, duration = 10 MINUTES) var/datum/status_effect/necropolis_curse/C = has_status_effect(STATUS_EFFECT_NECROPOLIS_CURSE) if(!set_curse) @@ -702,4 +736,4 @@ datum/status_effect/pacify if(LAZYLEN(targets) && I) to_chat(owner, "Your arm spasms!") owner.log_message("threw [I] due to a Muscle Spasm", LOG_ATTACK) - owner.throw_item(pick(targets)) \ No newline at end of file + owner.throw_item(pick(targets)) diff --git a/code/datums/traits/good.dm b/code/datums/traits/good.dm index 09a4f0b060..15ac70a2c7 100644 --- a/code/datums/traits/good.dm +++ b/code/datums/traits/good.dm @@ -8,12 +8,14 @@ mob_trait = TRAIT_ALCOHOL_TOLERANCE gain_text = "You feel like you could drink a whole keg!" lose_text = "You don't feel as resistant to alcohol anymore. Somehow." + medical_record_text = "Patient demonstrates a high tolerance for alcohol." /datum/quirk/apathetic name = "Apathetic" desc = "You just don't care as much as other people. That's nice to have in a place like this, I guess." value = 1 mood_quirk = TRUE + medical_record_text = "Patient was administered the Apathy Evaluation Scale but did not bother to complete it." /datum/quirk/apathetic/add() var/datum/component/mood/mood = quirk_holder.GetComponent(/datum/component/mood) @@ -42,6 +44,7 @@ mob_trait = TRAIT_EMPATH gain_text = "You feel in tune with those around you." lose_text = "You feel isolated from others." + medical_record_text = "Patient is highly perceptive of and sensitive to social cues, or may possibly have ESP. Further testing needed." /datum/quirk/freerunning name = "Freerunning" @@ -50,6 +53,7 @@ mob_trait = TRAIT_FREERUNNING gain_text = "You feel lithe on your feet!" lose_text = "You feel clumsy again." + medical_record_text = "Patient scored highly on cardio tests." /datum/quirk/friendly name = "Friendly" @@ -59,6 +63,7 @@ gain_text = "You want to hug someone." lose_text = "You no longer feel compelled to hug others." mood_quirk = TRUE + medical_record_text = "Patient demonstrates low-inhibitions for physical contact and well-developed arms. Requesting another doctor take over this case." /datum/quirk/jolly name = "Jolly" @@ -66,6 +71,11 @@ value = 1 mob_trait = TRAIT_JOLLY mood_quirk = TRUE + medical_record_text = "Patient demonstrates constant euthymia irregular for environment. It's a bit much, to be honest." + +/datum/quirk/jolly/on_process() + if(prob(0.05)) + SEND_SIGNAL(quirk_holder, COMSIG_ADD_MOOD_EVENT, "jolly", /datum/mood_event/jolly) /datum/quirk/light_step name = "Light Step" @@ -74,6 +84,7 @@ mob_trait = TRAIT_LIGHT_STEP gain_text = "You walk with a little more litheness." lose_text = "You start tromping around like a barbarian." + medical_record_text = "Patient's dexterity belies a strong capacity for stealth." /datum/quirk/quick_step name = "Quick Step" @@ -82,6 +93,7 @@ mob_trait = TRAIT_SPEEDY_STEP gain_text = "You feel determined. No time to lose." lose_text = "You feel less determined. What's the rush, man?" + medical_record_text = "Patient scored highly on racewalking tests." /datum/quirk/musician name = "Musician" @@ -90,6 +102,7 @@ mob_trait = TRAIT_MUSICIAN gain_text = "You know everything about musical instruments." lose_text = "You forget how musical instruments work." + medical_record_text = "Patient brain scans show a highly-developed auditory pathway." /datum/quirk/musician/on_spawn() var/mob/living/carbon/human/H = quirk_holder @@ -108,6 +121,7 @@ mob_trait = TRAIT_PHOTOGRAPHER gain_text = "You know everything about photography." lose_text = "You forget how photo cameras work." + medical_record_text = "Patient mentions photography as a stress-relieving hobby." /datum/quirk/photographer/on_spawn() var/mob/living/carbon/human/H = quirk_holder @@ -121,12 +135,14 @@ desc = "You know your body well, and can accurately assess the extent of your wounds." value = 2 mob_trait = TRAIT_SELF_AWARE + medical_record_text = "Patient demonstrates an uncanny knack for self-diagnosis." /datum/quirk/skittish name = "Skittish" desc = "You can conceal yourself in danger. Ctrl-shift-click a closed locker to jump into it, as long as you have access." value = 2 mob_trait = TRAIT_SKITTISH + medical_record_text = "Patient demonstrates a high aversion to danger and has described hiding in containers out of fear." /datum/quirk/spiritual name = "Spiritual" @@ -135,6 +151,7 @@ mob_trait = TRAIT_SPIRITUAL gain_text = "You feel a little more faithful to the gods today." lose_text = "You feel less faithful in the gods." + medical_record_text = "Patient reports a belief in a higher power." /datum/quirk/tagger name = "Tagger" @@ -143,6 +160,7 @@ mob_trait = TRAIT_TAGGER gain_text = "You know how to tag walls efficiently." lose_text = "You forget how to tag walls properly." + medical_record_text = "Patient was recently seen for possible paint huffing incident." /datum/quirk/tagger/on_spawn() var/mob/living/carbon/human/H = quirk_holder @@ -158,6 +176,7 @@ mob_trait = TRAIT_VORACIOUS gain_text = "You feel HONGRY." lose_text = "You no longer feel HONGRY." + medical_record_text = "Patient demonstrates a disturbing capacity for eating." /datum/quirk/trandening name = "High Luminosity Eyes" @@ -179,6 +198,7 @@ mob_trait = TRAIT_HIGH_BLOOD gain_text = "You feel full of blood!" lose_text = "You feel like your blood pressure went down." + medical_record_text = "Patient's blood tests report an abnormal concentration of red blood cells in their bloodstream." /datum/quirk/bloodpressure/add() var/mob/living/M = quirk_holder diff --git a/code/datums/traits/negative.dm b/code/datums/traits/negative.dm index 178e1aaeb0..3319259381 100644 --- a/code/datums/traits/negative.dm +++ b/code/datums/traits/negative.dm @@ -22,14 +22,19 @@ value = -1 gain_text = "You start feeling depressed." lose_text = "You no longer feel depressed." //if only it were that easy! - medical_record_text = "Patient has a severe mood disorder causing them to experience sudden moments of sadness." + medical_record_text = "Patient has a severe mood disorder, causing them to experience acute episodes of depression." mood_quirk = TRUE +/datum/quirk/depression/on_process() + if(prob(0.05)) + SEND_SIGNAL(quirk_holder, COMSIG_ADD_MOOD_EVENT, "depression", /datum/mood_event/depression) + /datum/quirk/family_heirloom name = "Family Heirloom" desc = "You are the current owner of an heirloom, passed down for generations. You have to keep it safe!" value = -1 mood_quirk = TRUE + medical_record_text = "Patient demonstrates an unnatural attachment to a family heirloom." var/obj/item/heirloom var/where @@ -143,6 +148,7 @@ name = "Nyctophobia" desc = "As far as you can remember, you've always been afraid of the dark. While in the dark without a light source, you instinctually act careful, and constantly feel a sense of dread." value = -1 + medical_record_text = "Patient demonstrates a fear of the dark. (Seriously?)" /datum/quirk/nyctophobia/on_process() var/mob/living/carbon/human/H = quirk_holder @@ -163,7 +169,8 @@ desc = "Bright lights irritate you. Your eyes start to water, your skin feels itchy against the photon radiation, and your hair gets dry and frizzy. Maybe it's a medical condition. If only Nanotrasen was more considerate of your needs..." value = -1 gain_text = "The safty of light feels off..." - lose_text = "Enlighing." + lose_text = "Enlightening." + medical_record_text = "Despite my warnings, the patient refuses turn on the lights, only to end up rolling down a full flight of stairs and into the cellar." /datum/quirk/lightless/on_process() var/turf/T = get_turf(quirk_holder) @@ -332,16 +339,19 @@ medical_record_text = "Patient has an extreme or irrational fear and aversion to an undefined stimuli." var/datum/brain_trauma/mild/phobia/phobia -/datum/quirk/phobia/add() +/datum/quirk/phobia/post_add() var/mob/living/carbon/human/H = quirk_holder phobia = new - H.gain_trauma(phobia, TRAUMA_RESILIENCE_SURGERY) + H.gain_trauma(phobia, TRAUMA_RESILIENCE_ABSOLUTE) + +/datum/quirk/phobia/remove() + var/mob/living/carbon/human/H = quirk_holder + H?.cure_trauma_type(phobia, TRAUMA_RESILIENCE_ABSOLUTE) /datum/quirk/mute name = "Mute" desc = "Due to some accident, medical condition, or simply by choice, you are completely unable to speak." value = -2 //HALP MAINTS - mob_trait = TRAIT_MUTE gain_text = "You find yourself unable to speak!" lose_text = "You feel a growing strength in your vocal chords." medical_record_text = "Functionally mute, patient is unable to use their voice in any capacity." @@ -350,14 +360,17 @@ /datum/quirk/mute/add() var/mob/living/carbon/human/H = quirk_holder mute = new - H.gain_trauma(mute, TRAUMA_RESILIENCE_SURGERY) + H.gain_trauma(mute, TRAUMA_RESILIENCE_ABSOLUTE) + +/datum/quirk/mute/remove() + var/mob/living/carbon/human/H = quirk_holder + H?.cure_trauma_type(mute, TRAUMA_RESILIENCE_ABSOLUTE) /datum/quirk/mute/on_process() if(quirk_holder.mind && LAZYLEN(quirk_holder.mind.antag_datums)) to_chat(quirk_holder, "Your antagonistic nature has caused your voice to be heard.") qdel(src) - /datum/quirk/unstable name = "Unstable" desc = "Due to past troubles, you are unable to recover your sanity if you lose it. Be very careful managing your mood!" @@ -373,7 +386,7 @@ value = -4 gain_text = "You can't see anything." lose_text = "You miraculously gain back your vision." - medical_record_text = "Subject has permanent blindness." + medical_record_text = "Patient has permanent blindness." /datum/quirk/blindness/add() quirk_holder.become_blind(ROUNDSTART_TRAIT) @@ -384,3 +397,6 @@ if(!H.equip_to_slot_if_possible(glasses, SLOT_GLASSES, bypass_equip_delay_self = TRUE)) //if you can't put it on the user's eyes, put it in their hands, otherwise put it on their eyes eyes H.put_in_hands(glasses) H.regenerate_icons() + +/datum/quirk/blindness/remove() + quirk_holder?.cure_blind(ROUNDSTART_TRAIT) diff --git a/code/datums/traits/neutral.dm b/code/datums/traits/neutral.dm index 9e05af03b6..eae2db6a5f 100644 --- a/code/datums/traits/neutral.dm +++ b/code/datums/traits/neutral.dm @@ -16,6 +16,7 @@ value = 0 gain_text = "You feel an intense craving for pineapple." lose_text = "Your feelings towards pineapples seem to return to a lukewarm state." + medical_record_text = "Patient demonstrates a pathological love of pineapple." /datum/quirk/pineapple_liker/add() var/mob/living/carbon/human/H = quirk_holder @@ -34,6 +35,7 @@ value = 0 gain_text = "You find yourself pondering what kind of idiot actually enjoys pineapples..." lose_text = "Your feelings towards pineapples seem to return to a lukewarm state." + medical_record_text = "Patient is correct to think that pineapple is disgusting." /datum/quirk/pineapple_hater/add() var/mob/living/carbon/human/H = quirk_holder @@ -52,6 +54,7 @@ value = 0 gain_text = "You start craving something that tastes strange." lose_text = "You feel like eating normal food again." + medical_record_text = "Patient demonstrates irregular nutrition preferences." /datum/quirk/deviant_tastes/add() var/mob/living/carbon/human/H = quirk_holder @@ -92,7 +95,7 @@ value = 0 gain_text = "You feel more prudish." lose_text = "You don't feel as prudish as before." - medical_record_text = "Patient exhibits a special gene that makes them immune to Crocin and Hexacrocin." + medical_record_text = "Patient exhibits a special gene that makes them immune to aphrodisiacs." /datum/quirk/libido name = "Nymphomania" @@ -134,6 +137,7 @@ value = 0 mob_trait = TRAIT_PHARMA lose_text = "Your liver feels different." + medical_record_text = "Non-invasive tests report that the patient's metabolism is indeed incompatible with a certain \"stimulants\"." var/active = FALSE var/power = 0 var/cachedmoveCalc = 1 diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index 612c3cba42..f5e285a5c2 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -327,7 +327,7 @@ GLOBAL_LIST_EMPTY(teleportlocs) for(var/obj/machinery/light/L in src) L.update() -/area/proc/updateicon() +/area/proc/update_icon() var/weather_icon for(var/V in SSweather.processing) var/datum/weather/W = V @@ -337,7 +337,7 @@ GLOBAL_LIST_EMPTY(teleportlocs) if(!weather_icon) icon_state = null -/area/space/updateicon() +/area/space/update_icon() icon_state = null /* @@ -370,7 +370,7 @@ GLOBAL_LIST_EMPTY(teleportlocs) /area/proc/power_change() for(var/obj/machinery/M in src) // for each machine in the area M.power_change() // reverify power status (to update icons etc.) - updateicon() + update_icon() /area/proc/usage(chan) var/used = 0 diff --git a/code/game/gamemodes/bloodsucker/bloodsucker.dm b/code/game/gamemodes/bloodsucker/bloodsucker.dm new file mode 100644 index 0000000000..e784fd836d --- /dev/null +++ b/code/game/gamemodes/bloodsucker/bloodsucker.dm @@ -0,0 +1,292 @@ + +/datum/game_mode + var/list/datum/mind/bloodsuckers = list() // List of minds belonging to this game mode. + var/list/datum/mind/vassals = list() // List of minds that have been turned into Vassals. + //var/list/datum/mind/vamphunters = list() // List of minds hunting vampires. Disabled at the moment + var/obj/effect/sunlight/bloodsucker_sunlight // Sunlight Timer. Created on first Bloodsucker assign. Destroyed on last removed Bloodsucker. + + // LISTS // + var/list/vassal_allowed_antags = list(/datum/antagonist/brother, /datum/antagonist/traitor, /datum/antagonist/traitor/internal_affairs, /datum/antagonist/survivalist, \ + /datum/antagonist/rev, /datum/antagonist/nukeop, /datum/antagonist/pirate, /datum/antagonist/cult, /datum/antagonist/abductee) + // The antags you're allowed to be if turning Vassal. + +/datum/game_mode/bloodsucker + name = "bloodsucker" + config_tag = "bloodsucker" + traitor_name = "Bloodsucker" + antag_flag = ROLE_BLOODSUCKER + false_report_weight = 1 + restricted_jobs = list("AI","Cyborg") + protected_jobs = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + required_players = 20 + required_enemies = 2 + recommended_enemies = 4 + reroll_friendly = FALSE + enemy_minimum_age = 7 + round_ends_with_antag_death = FALSE + + + announce_span = "danger" + announce_text = "Filthy, bloodsucking vampires are crawling around disguised as crewmembers!\n\ + Bloodsuckers: The crew are cattle, while you are both shepherd and slaughterhouse.\n\ + Crew: Put an end to the undead infestation before the station is overcome!" + +/datum/game_mode/bloodsucker/generate_report() + return "Reports indicate that some of your crew may have toppled statues in the past week, angering the gods and becoming cursed with undeath and a desire for blood. Watch out for crewmembers that seem to shun the light or are found pale and delirious." + +// Seems to be run by game ONCE, and finds all potential players to be antag. +/datum/game_mode/bloodsucker/pre_setup() + + // Set Restricted Jobs + if(CONFIG_GET(flag/protect_roles_from_antagonist)) + restricted_jobs += protected_jobs + + if(CONFIG_GET(flag/protect_assistant_from_antagonist)) + restricted_jobs += "Assistant" + + // Set number of Vamps + recommended_enemies = CLAMP(round(num_players()/10), 1, 6); + + // Select Antags + for(var/i = 0, i < recommended_enemies, i++) + if (!antag_candidates.len) + break + var/datum/mind/bloodsucker = pick(antag_candidates) + // Can we even BE a bloodsucker? + //if (can_make_bloodsucker(bloodsucker, display_warning=FALSE)) + bloodsuckers += bloodsucker + bloodsucker.restricted_roles = restricted_jobs + log_game("[bloodsucker.key] (ckey) has been selected as a Bloodsucker.") + antag_candidates.Remove(bloodsucker) // Apparently you can also write antag_candidates -= bloodsucker + + // Assign Hunters (as many as monsters, plus one) + //assign_monster_hunters(bloodsuckers.len, TRUE, bloodsuckers) // Disabled for now + + // Do we have enough vamps to continue? + return bloodsuckers.len >= required_enemies + + +// Gamemode is all done being set up. We have all our Vamps. We now pick objectives and let them know what's happening. +/datum/game_mode/bloodsucker/post_setup() + + // Sunlight (Creating Bloodsuckers manually will check to create this, too) + check_start_sunlight() + + // Vamps + for(var/datum/mind/bloodsucker in bloodsuckers) + // spawn() --> Run block of code but game continues on past it. + // sleep() --> Run block of code and freeze code there (including whoever called us) until it's resolved. + + //Clean Bloodsucker Species (racist?) + //clean_invalid_species(bloodsucker) + // TO-DO !!! + + // Add Bloodsucker Antag Datum (or remove from list on Fail) + if (!make_bloodsucker(bloodsucker)) + bloodsuckers -= bloodsucker + + // NOTE: Hunters are done in ..() parent proc + + return ..() + +// Checking for ACTUALLY Dead Vamps +/datum/game_mode/bloodsucker/are_special_antags_dead() + // Bloodsucker not Final Dead + for(var/datum/mind/bloodsucker in bloodsuckers) + if(!bloodsucker.AmFinalDeath()) + return FALSE + return TRUE + + +// Init Sunlight (called from datum_bloodsucker.on_gain(), in case game mode isn't even Bloodsucker +/datum/game_mode/proc/check_start_sunlight() + // Already Sunlight (and not about to cancel) + if (istype(bloodsucker_sunlight) && !bloodsucker_sunlight.cancel_me) + return + bloodsucker_sunlight = new () + +// End Sun (last bloodsucker removed) +/datum/game_mode/proc/check_cancel_sunlight() + // No Sunlight + if (!istype(bloodsucker_sunlight)) + return + if (bloodsuckers.len <= 0) + bloodsucker_sunlight.cancel_me = TRUE + qdel(bloodsucker_sunlight) + bloodsucker_sunlight = null + +/datum/game_mode/proc/is_daylight() + return istype(bloodsucker_sunlight) && bloodsucker_sunlight.amDay + +////////////////////////////////////////////////////////////////////////////// + + +/datum/game_mode/proc/can_make_bloodsucker(datum/mind/bloodsucker, datum/mind/creator, display_warning=TRUE) // Creator is just here so we can display fail messages to whoever is turning us. + // No Mind + if(!bloodsucker || !bloodsucker.key) // KEY is client login? + //if(creator) // REMOVED. You wouldn't see their name if there is no mind, so why say anything? + // to_chat(creator, "[bloodsucker] isn't self-aware enough to be raised as a Bloodsucker!") + return FALSE + // Current body is invalid + if(!ishuman(bloodsucker.current))// && !ismonkey(bloodsucker.current)) + if(display_warning && creator) + to_chat(creator, "[bloodsucker] isn't evolved enough to be raised as a Bloodsucker!") + return FALSE + // Species Must have a HEART (Sorry Plasmabois) + var/mob/living/carbon/human/H = bloodsucker.current + if(NOBLOOD in H.dna.species.species_traits) + if(display_warning && creator) + to_chat(creator, "[bloodsucker]'s DNA isn't compatible!") + return FALSE + // Already a Non-Human Antag + if(bloodsucker.has_antag_datum(/datum/antagonist/abductor) || bloodsucker.has_antag_datum(/datum/antagonist/devil) || bloodsucker.has_antag_datum(/datum/antagonist/changeling)) + return FALSE + // Already a vamp + if(bloodsucker.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) + if(display_warning && creator) + to_chat(creator, "[bloodsucker] is already a Bloodsucker!") + return FALSE + // Not High Enough + if(creator) + var/datum/antagonist/bloodsucker/creator_bloodsucker = creator.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(!istype(creator_bloodsucker) || creator_bloodsucker.vamplevel < BLOODSUCKER_LEVEL_TO_EMBRACE) + to_chat(creator, "Your blood is too thin to turn this corpse!") + return FALSE + return TRUE + + +/datum/game_mode/proc/make_bloodsucker(datum/mind/bloodsucker, datum/mind/creator = null) // NOTE: This is a game_mode/proc, NOT a game_mode/bloodsucker/proc! We need to access this function despite the game mode. + if (!can_make_bloodsucker(bloodsucker)) + return FALSE + + // Create Datum: Fledgling + var/datum/antagonist/bloodsucker/A + + // [FLEDGLING] + if (creator) + A = new (bloodsucker) + A.creator = creator + bloodsucker.add_antag_datum(A) + // Log + message_admins("[bloodsucker] has become a Bloodsucker, and was created by [creator].") + log_admin("[bloodsucker] has become a Bloodsucker, and was created by [creator].") + + // [MASTER] + else + A = bloodsucker.add_antag_datum(ANTAG_DATUM_BLOODSUCKER) + + + return TRUE + + +/datum/game_mode/proc/remove_bloodsucker(datum/mind/bloodsucker) + bloodsucker.remove_antag_datum(ANTAG_DATUM_BLOODSUCKER) + + +/datum/game_mode/proc/clean_invalid_species(datum/mind/bloodsucker) + // Only checking for Humans here + if (!ishuman(bloodsucker.current) || !bloodsucker.current.client) + return + var/am_valid = TRUE + var/mob/living/carbon/human/H = bloodsucker.current + + // Check if PLASMAMAN? + if(NOBLOOD in H.dna.species.species_traits) + am_valid = FALSE + + // PROBLEM: + // + // Setting species leaves clothes on. If you were a plasmaman, we need to reassign your entire outfit. Otherwise + // everyone will wonder why you're a human with Plasma clothes (jk they'll know you're antag) + + // Convert to HUMAN (along with ID and PDA) + if (!am_valid) + H.set_species(/datum/species/human) + H.real_name = H.client.prefs.custom_names["human"] + var/obj/item/card/id/ID = H.wear_id?.GetID() + if(ID) + ID.registered_name = H.real_name + ID.update_label() + + +/datum/game_mode/proc/can_make_vassal(mob/living/target, datum/mind/creator, display_warning=TRUE)//, check_antag_or_loyal=FALSE) + // Not Correct Type: Abort + if (!iscarbon(target) || !creator) + return FALSE + if (target.stat > UNCONSCIOUS) + return FALSE + // Check Overdose: Am I even addicted to blood? Do I even have any in me? + //if (!target.reagents.addiction_list || !target.reagents.reagent_list) + //message_admins("DEBUG2: can_make_vassal() Abort: No reagents") + // return 0 + // Check Overdose: Did my current volume go over the Overdose threshold? + //var/am_addicted = 0 + //for (var/datum/reagent/blood/vampblood/blood in target.reagents.addiction_list) // overdosed is tracked in reagent_list, not addiction_list. + //message_admins("DEBUG3: can_make_vassal() Found Blood! [blood] [blood.overdose]") + //if (blood.overdosed) + // am_addicted = 1 // Blood is present in addiction? That's all we need. + // break + + //if (!am_addicted) + //message_admins("DEBUG4: can_make_vassal() Abort: No Blood") + // return 0 + // No Mind! + if (!target.mind || !target.mind.key) + if (display_warning) + to_chat(creator, "[target] isn't self-aware enough to be made into a Vassal.") + return FALSE + // Already MY Vassal + var/datum/antagonist/vassal/V = target.mind.has_antag_datum(ANTAG_DATUM_VASSAL) + if (istype(V) && V.master) + if (V.master.owner == creator) + if (display_warning) + to_chat(creator, "[target] is already your loyal Vassal!") + else + if (display_warning) + to_chat(creator, "[target] is the loyal Vassal of another Bloodsucker!") + return FALSE + // Already Antag or Loyal (Vamp Hunters count as antags) + if (target.mind.enslaved_to || AmInvalidAntag(target.mind)) //!VassalCheckAntagValid(target.mind, check_antag_or_loyal)) // HAS_TRAIT(target, TRAIT_MINDSHIELD, "implant") || + if (display_warning) + to_chat(creator, "[target] resists the power of your blood to dominate their mind!") + return FALSE + return TRUE + + +/datum/game_mode/proc/AmValidAntag(datum/mind/M) + // No List? + if(!islist(M.antag_datums) || M.antag_datums.len == 0) + return FALSE + // Am I NOT an invalid Antag? NOTE: We already excluded non-antags above. Don't worry about the "No List?" check in AmInvalidIntag() + return !AmInvalidAntag(M) + +/datum/game_mode/proc/AmInvalidAntag(datum/mind/M) + // No List? + if(!islist(M.antag_datums) || M.antag_datums.len == 0) + return FALSE + // Does even ONE antag appear in this mind that isn't in the list? Then FAIL! + for(var/datum/antagonist/antag_datum in M.antag_datums) + if (!(antag_datum.type in vassal_allowed_antags)) // vassal_allowed_antags is a list stored in the game mode, above. + //message_admins("DEBUG VASSAL: Found Invalid: [antag_datum] // [antag_datum.type]") + return TRUE + //message_admins("DEBUG VASSAL: Valid Antags! (total of [M.antag_datums.len])") + // WHEN YOU DELETE THE ABOVE: Remove the 3 second timer on converting the vassal too. + return FALSE + +/datum/game_mode/proc/make_vassal(mob/living/target, datum/mind/creator) + if (!can_make_vassal(target,creator)) + return FALSE + // Make Vassal + var/datum/antagonist/vassal/V = new (target.mind) + var/datum/antagonist/bloodsucker/B = creator.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + V.master = B + target.mind.add_antag_datum(V, V.master.get_team()) + // Update Bloodsucker Title (we're a daddy now) + B.SelectTitle(am_fledgling = FALSE) // Only works if you have no title yet. + // Log + message_admins("[target] has become a Vassal, and is enslaved to [creator].") + log_admin("[target] has become a Vassal, and is enslaved to [creator].") + return TRUE + +/datum/game_mode/proc/remove_vassal(datum/mind/vassal) + vassal.remove_antag_datum(ANTAG_DATUM_VASSAL) diff --git a/code/game/gamemodes/bloodsucker/hunter.dm b/code/game/gamemodes/bloodsucker/hunter.dm new file mode 100644 index 0000000000..cec990e9c3 --- /dev/null +++ b/code/game/gamemodes/bloodsucker/hunter.dm @@ -0,0 +1,50 @@ + + +/* +// Called from game mode pre_setup() +/datum/game_mode/proc/assign_monster_hunters(monster_count = 4, guaranteed_hunters = FALSE, list/datum/mind/exclude_from_hunter) + + // Not all game modes GUARANTEE a hunter + if (rand(0,2) == 0) // 50% of the time, we get fewer or NO Hunters + if (!guaranteed_hunters) + return + else + monster_count /= 2 + + var/list/no_hunter_jobs = list("AI","Cyborg") + + // Set Restricted Jobs + if(CONFIG_GET(flag/protect_roles_from_antagonist)) + no_hunter_jobs += list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + + if(CONFIG_GET(flag/protect_assistant_from_antagonist)) + no_hunter_jobs += "Assistant" + + // Find Valid Hunters + var/list/datum/mind/hunter_candidates = get_players_for_role(ROLE_MONSTERHUNTER) + + // Assign Hunters (as many as vamps, plus one) + for(var/i = 1, i < monster_count, i++) // Start at 1 so we skip Hunters if there's only one sucker. + if (!hunter_candidates.len) + break + // Assign Hunter + var/datum/mind/hunter = pick(hunter_candidates) + hunter_candidates.Remove(hunter) // Remove Either Way + // Already Antag? Skip + if (islist(exclude_from_hunter) && (locate(hunter) in exclude_from_hunter)) //if (islist(hunter.antag_datums) && hunter.antag_datums.len) + i -- + continue + // NOTE: + vamphunters += hunter + hunter.restricted_roles = no_hunter_jobs + log_game("[hunter.key] (ckey) has been selected as a Hunter.") + +// Called from game mode post_setup() +/datum/game_mode/proc/finalize_monster_hunters(monster_count = 4) + var/amEvil = TRUE // First hunter is always an evil boi + for(var/datum/mind/hunter in vamphunters) + var/datum/antagonist/vamphunter/A = new (hunter) + A.bad_dude = amEvil + hunter.add_antag_datum(A) + amEvil = FALSE // Every other hunter is just a boring greytider +*/ diff --git a/code/game/gamemodes/clown_ops/clown_weapons.dm b/code/game/gamemodes/clown_ops/clown_weapons.dm index ee96d1fa10..9b52ddda1e 100644 --- a/code/game/gamemodes/clown_ops/clown_weapons.dm +++ b/code/game/gamemodes/clown_ops/clown_weapons.dm @@ -265,6 +265,7 @@ armor = list("melee" = 40, "bullet" = 40, "laser" = 50, "energy" = 35, "bomb" = 20, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 100) max_temperature = 35000 operation_req_access = list(ACCESS_SYNDICATE) + internals_req_access = list(ACCESS_SYNDICATE) wreckage = /obj/structure/mecha_wreckage/honker/dark max_equip = 3 spawn_tracked = FALSE diff --git a/code/game/gamemodes/dynamic/dynamic.dm b/code/game/gamemodes/dynamic/dynamic.dm index cd9500284b..67ea855466 100644 --- a/code/game/gamemodes/dynamic/dynamic.dm +++ b/code/game/gamemodes/dynamic/dynamic.dm @@ -236,6 +236,9 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1) return rule.round_result() return ..() +/datum/game_mode/dynamic/generate_report() + return "Mysterious signals that demonstrate strange dynamics have been detected in your sector. Watch out for oddities." + /datum/game_mode/dynamic/send_intercept() . = "Central Command Status Summary
" switch(round(threat_level)) diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm index 8cda402ebf..01c11d8b0b 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm @@ -94,11 +94,8 @@ var/list/possible_candidates = list() possible_candidates.Add(dead_players) possible_candidates.Add(list_observers) - send_applications(possible_candidates) - if(assigned.len > 0) - return TRUE - else - return FALSE + var/application_successful = send_applications(possible_candidates) + return assigned.len > 0 && application_successful /// This sends a poll to ghosts if they want to be a ghost spawn from a ruleset. /datum/dynamic_ruleset/midround/from_ghosts/proc/send_applications(list/possible_volunteers = list()) @@ -113,25 +110,18 @@ if(!candidates || candidates.len <= required_candidates) message_admins("The ruleset [name] did not receive enough applications.") log_game("DYNAMIC: The ruleset [name] did not receive enough applications.") - mode.refund_threat(cost) - mode.log_threat("Rule [name] refunded [cost] (not receive enough applications)",verbose=TRUE) - mode.executed_rules -= src - return + return FALSE message_admins("[candidates.len] players volunteered for the ruleset [name].") log_game("DYNAMIC: [candidates.len] players volunteered for [name].") review_applications() + return TRUE /// Here is where you can check if your ghost applicants are valid for the ruleset. /// Called by send_applications(). /datum/dynamic_ruleset/midround/from_ghosts/proc/review_applications() for (var/i = 1, i <= required_candidates, i++) if(candidates.len <= 0) - if(i == 1) - // We have found no candidates so far and we are out of applicants. - mode.refund_threat(cost) - mode.log_threat("Rule [name] refunded [cost] (all applications invalid)",verbose=TRUE) - mode.executed_rules -= src break var/mob/applicant = pick(candidates) candidates -= applicant @@ -744,3 +734,31 @@ #undef ABDUCTOR_MAX_TEAMS #undef REVENANT_SPAWN_THRESHOLD + +////////////////////////////////////////////// +// // +// BLOODSUCKERS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/latejoin/bloodsucker + name = "Bloodsucker Infiltrator" + config_tag = "latejoin_bloodsucker" + antag_datum = ANTAG_DATUM_BLOODSUCKER + antag_flag = ROLE_TRAITOR + restricted_roles = list("AI", "Cyborg") + protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + required_candidates = 1 + weight = 3 + cost = 10 + requirements = list(90,80,70,60,55,50,45,40,35,30) + high_population_requirement = 30 + repeatable = TRUE + +/datum/dynamic_ruleset/latejoin/bloodsucker/execute() + var/mob/M = pick(candidates) + assigned += M.mind + M.mind.special_role = antag_flag + if(mode.make_bloodsucker(M.mind)) + mode.bloodsuckers += M + return TRUE diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm index 56b02a1364..2d476eab19 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm @@ -778,3 +778,42 @@ var/ramp_up_final = CLAMP(round(meteorminutes/rampupdelta), 1, 10) spawn_meteors(ramp_up_final, wavetype) + +////////////////////////////////////////////// +// // +// BLOODSUCKERS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/bloodsucker + name = "Bloodsuckers" + config_tag = "bloodsucker" + persistent = TRUE + antag_flag = ROLE_BLOODSUCKER + antag_datum = ANTAG_DATUM_BLOODSUCKER + minimum_required_age = 0 + protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + restricted_roles = list("Cyborg", "AI") + required_candidates = 1 + weight = 2 + cost = 15 + scaling_cost = 10 + requirements = list(90,80,70,60,50,50,50,50,50,50) + high_population_requirement = 50 + antag_cap = list(1,1,1,1,1,2,2,2,2,2) + +/datum/dynamic_ruleset/roundstart/bloodsucker/pre_execute() + var/num_bloodsuckers = antag_cap[indice_pop] * (scaled_times + 1) + for (var/i = 1 to num_bloodsuckers) + var/mob/M = pick_n_take(candidates) + assigned += M.mind + M.mind.special_role = ROLE_BLOODSUCKER + M.mind.restricted_roles = restricted_roles + return TRUE + +/datum/dynamic_ruleset/roundstart/bloodsucker/execute() + mode.check_start_sunlight() + for(var/datum/mind/M in assigned) + if(mode.make_bloodsucker(M)) + mode.bloodsuckers += M + return TRUE diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm index 4d7b346241..3a258599b7 100644 --- a/code/game/gamemodes/game_mode.dm +++ b/code/game/gamemodes/game_mode.dm @@ -81,6 +81,7 @@ ///Everyone should now be on the station and have their normal gear. This is the place to give the special roles extra things /datum/game_mode/proc/post_setup(report) //Gamemodes can override the intercept report. Passing TRUE as the argument will force a report. + //finalize_monster_hunters() Disabled for now if(!report) report = !CONFIG_GET(flag/no_intercept_report) addtimer(CALLBACK(GLOBAL_PROC, .proc/display_roundstart_logout_report), ROUNDSTART_LOGOUT_REPORT_TIME) @@ -305,48 +306,88 @@ // The odds become: // Player A: 150 / 250 = 0.6 = 60% // Player B: 100 / 250 = 0.4 = 40% -/datum/game_mode/proc/antag_pick(list/datum/candidates) + +//Use return list if you want a list, with the arg being the number you want returned. +//WARNING: THIS PROC DOES NOT TAKE INTO ACCOUNT WHAT SSPersistence ALREADY HAS FOR "ADJUST ANTAG REP". If this is used more than once +//and the person rolls more than once, they will not get even more deduction! +//More efficient if you use return list instead of calling this multiple times +//fail_default_pick makes it use pick() instead of antag rep if it can't find anyone +//allow_zero_if_insufficient allows it to pick people with zero rep if there isn't enough antags +/datum/game_mode/proc/antag_pick(list/datum/mind/candidates, return_list = FALSE, fail_default_pick = TRUE, allow_zero_if_insufficient = TRUE) if(!CONFIG_GET(flag/use_antag_rep)) // || candidates.len <= 1) return pick(candidates) - // Tickets start at 100 - var/DEFAULT_ANTAG_TICKETS = CONFIG_GET(number/default_antag_tickets) + //whoever named the config entries is a bad person :( - // You may use up to 100 extra tickets (double your odds) - var/MAX_TICKETS_PER_ROLL = CONFIG_GET(number/max_tickets_per_roll) - - - var/total_tickets = 0 - - MAX_TICKETS_PER_ROLL += DEFAULT_ANTAG_TICKETS - - var/p_ckey - var/p_rep - - for(var/datum/mind/mind in candidates) - p_ckey = ckey(mind.key) - total_tickets += min(SSpersistence.antag_rep[p_ckey] + DEFAULT_ANTAG_TICKETS, MAX_TICKETS_PER_ROLL) - - var/antag_select = rand(1,total_tickets) - var/current = 1 - - for(var/datum/mind/mind in candidates) - p_ckey = ckey(mind.key) - p_rep = SSpersistence.antag_rep[p_ckey] - - var/previous = current - var/spend = min(p_rep + DEFAULT_ANTAG_TICKETS, MAX_TICKETS_PER_ROLL) - current += spend - - if(antag_select >= previous && antag_select <= (current-1)) - SSpersistence.antag_rep_change[p_ckey] = -(spend - DEFAULT_ANTAG_TICKETS) - -// WARNING("AR_DEBUG: Player [mind.key] won spending [spend] tickets from starting value [SSpersistence.antag_rep[p_ckey]]") - - return mind - - WARNING("Something has gone terribly wrong. /datum/game_mode/proc/antag_pick failed to select a candidate. Falling back to pick()") - return pick(candidates) + //Tickets you get for free + var/free_tickets = CONFIG_GET(number/default_antag_tickets) + //Max extra tickets you can use + var/additional_tickets = CONFIG_GET(number/max_tickets_per_roll) + + var/list/ckey_to_mind = list() //this is admittedly shitcode but I'm webediting + var/list/prev_tickets = SSpersistence.antag_rep //cache for hyper-speed in theory. how many tickets someone has stored + var/list/curr_tickets = list() //how many tickets someone has for *this* antag roll, so with the free tickets + var/list/datum/mind/insufficient = list() //who got cucked out of an antag roll due to not having *any* tickets + for(var/datum/mind/M in candidates) + var/mind_ckey = ckey(M.key) + var/can_spend = min(prev_tickets[mind_ckey], additional_tickets) //they can only spend up to config/max_tickets_per_roll + var/amount = can_spend + free_tickets //but they get config/default_antag_tickets for free + if(amount <= 0) //if they don't have any + insufficient += M //too bad! + continue + curr_tickets[mind_ckey] = amount + ckey_to_mind[mind_ckey] = M //make sure we can look them up after picking + + if(!return_list) //return a single guy + var/ckey + if(length(curr_tickets)) + ckey = pickweight(curr_tickets) + SSpersistence.antag_rep_change[ckey] = -(curr_tickets[ckey] - free_tickets) //deduct what they spent + var/mind = ckey_to_mind[ckey] || (allow_zero_if_insufficient? pick(insufficient) : null) //we want their mind + if(!mind) //no mind + var/warning = "WARNING: No antagonists were successfully picked by /datum/gamemode/proc/antag_pick()![fail_default_pick? " Defaulting to pick()!":""]" + message_admins(warning) + log_game(warning) + if(fail_default_pick) + mind = pick(candidates) + return mind + else //the far more efficient and proper use of this, to get a list + var/list/rolled = list() + var/list/spend_tickets = list() + for(var/i in 1 to return_list) + if(!length(curr_tickets)) //ah heck, we're out of candidates.. + break + var/ckey = pickweight(curr_tickets) //pick + rolled += ckey //add + spend_tickets[ckey] = curr_tickets[ckey] - free_tickets + curr_tickets -= ckey //don't roll them again + var/missing = return_list - length(rolled) + var/list/add + if((missing > 0) && allow_zero_if_insufficient) //need more.. + for(var/i in 1 to missing) + if(!length(insufficient)) + break //still not enough + var/datum/mind/M = pick_n_take(insufficient) + add += M + if(!length(rolled) && !length(add)) //if no one could normally roll AND no one can zero roll + var/warning = "WARNING: No antagonists were successfully picked by /datum/gamemode/proc/antag_pick()![fail_default_pick? " Defaulting to pick()!":""]" + message_admins(warning) + log_game(warning) + var/list/failed = list() + if(fail_default_pick) + var/list/C = candidates.Copy() + for(var/i in 1 to return_list) + if(!length(C)) + break + failed += pick_n_take(C) + return failed //Wew, no one qualified! + for(var/i in 1 to length(rolled)) + var/ckey = rolled[i] + SSpersistence.antag_rep_change[ckey] = -(spend_tickets[ckey]) //deduct what all of the folks who rolled spent + rolled[i] = ckey_to_mind[ckey] //whoever called us wants minds, not ckeys + if(add) + rolled += add + return rolled /datum/game_mode/proc/get_players_for_role(role) var/list/players = list() diff --git a/code/game/machinery/Beacon.dm b/code/game/machinery/Beacon.dm index 3d0931d534..c98795c90b 100644 --- a/code/game/machinery/Beacon.dm +++ b/code/game/machinery/Beacon.dm @@ -25,10 +25,10 @@ // update the invisibility and icon /obj/machinery/bluespace_beacon/hide(intact) invisibility = intact ? INVISIBILITY_MAXIMUM : 0 - updateicon() + update_icon() // update the icon_state -/obj/machinery/bluespace_beacon/proc/updateicon() +/obj/machinery/bluespace_beacon/update_icon() var/state="floor_beacon" if(invisibility) @@ -45,4 +45,4 @@ else if (Beacon.loc != loc) Beacon.forceMove(loc) - updateicon() + update_icon() diff --git a/code/game/machinery/Sleeper.dm b/code/game/machinery/Sleeper.dm index 3f3bacc64f..520721560a 100644 --- a/code/game/machinery/Sleeper.dm +++ b/code/game/machinery/Sleeper.dm @@ -181,12 +181,14 @@ open_machine() /obj/machinery/sleeper/AltClick(mob/user) + . = ..() if(!user.canUseTopic(src, !issilicon(user))) return if(state_open) close_machine() else open_machine() + return TRUE /obj/machinery/sleeper/examine(mob/user) . = ..() diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm index ff7383cd9e..5f44fccdac 100644 --- a/code/game/machinery/_machinery.dm +++ b/code/game/machinery/_machinery.dm @@ -181,8 +181,9 @@ Class Procs: if(isliving(A)) var/mob/living/L = A L.update_canmove() - SEND_SIGNAL(src, COMSIG_MACHINE_EJECT_OCCUPANT, occupant) - occupant = null + if(occupant) + SEND_SIGNAL(src, COMSIG_MACHINE_EJECT_OCCUPANT, occupant) + occupant = null /obj/machinery/proc/can_be_occupant(atom/movable/am) return occupant_typecache ? is_type_in_typecache(am, occupant_typecache) : isliving(am) diff --git a/code/game/machinery/aug_manipulator.dm b/code/game/machinery/aug_manipulator.dm index b9d63e5771..b41910f010 100644 --- a/code/game/machinery/aug_manipulator.dm +++ b/code/game/machinery/aug_manipulator.dm @@ -132,8 +132,8 @@ ..() if(!user.canUseTopic(src)) return - else - eject_part(user) + eject_part(user) + return TRUE /obj/machinery/aug_manipulator/power_change() ..() diff --git a/code/game/machinery/bloodbankgen.dm b/code/game/machinery/bloodbankgen.dm index c489f6c9c9..296e79b403 100644 --- a/code/game/machinery/bloodbankgen.dm +++ b/code/game/machinery/bloodbankgen.dm @@ -28,13 +28,6 @@ QDEL_NULL(outbag) return ..() -/obj/machinery/bloodbankgen/contents_explosion(severity, target) - ..() - if(bag) - bag.ex_act(severity, target) - if(outbag) - outbag.ex_act(severity, target) - /obj/machinery/bloodbankgen/handle_atom_del(atom/A) ..() if(A == bag) @@ -188,7 +181,7 @@ if(user.a_intent == INTENT_HARM) return ..() - if(default_deconstruction_screwdriver(user, "bloodbank-off", "bloodbank-off", O)) + if(default_deconstruction_screwdriver(user, "bloodbank-off", "bloodbank-off", O) || default_unfasten_wrench(user, O, 20) == SUCCESSFUL_UNFASTEN) if(bag) var/obj/item/reagent_containers/blood/B = bag B.forceMove(drop_location()) @@ -204,31 +197,37 @@ return if(istype(O, /obj/item/reagent_containers/blood)) - . = 1 //no afterattack + . = TRUE //no afterattack + var/msg = "" if(!panel_open) - if(bag && outbag) - to_chat(user, "This machine already has bags attached.") - - if(!bag && !outbag) - var/choice = alert(user, "Choose where to place [O]", "", "Input", "Cancel", "Output") - switch(choice) - if("Cancel") - return FALSE - if("Input") - attachinput(O, user) - if("Output") - attachoutput(O, user) - else if(!bag) - attachinput(O, user) - else if(!outbag) - attachoutput(O, user) - else - to_chat(user, "Close the maintenance panel first.") - return + . += "Close the maintenance panel" + if(!anchored) + . += "[msg ? " and a" : "A"]nchor its bolts" + if(length(msg)) + to_chat(user, "[msg] first.") + return + if(bag && outbag) + to_chat(user, "This machine already has bags attached.") + if(!bag && !outbag) + var/choice = alert(user, "Choose where to place [O]", "", "Input", "Cancel", "Output") + switch(choice) + if("Cancel") + return FALSE + if("Input") + attachinput(O, user) + if("Output") + attachoutput(O, user) + else if(!bag) + attachinput(O, user) + else if(!outbag) + attachoutput(O, user) else to_chat(user, "You cannot put this in [src]!") +/obj/machinery/bloodbankgen/is_operational() + return ..() && anchored + /obj/machinery/bloodbankgen/ui_interact(mob/user) . = ..() diff --git a/code/game/machinery/cell_charger.dm b/code/game/machinery/cell_charger.dm index 1839e44e3b..1c0635fd3d 100644 --- a/code/game/machinery/cell_charger.dm +++ b/code/game/machinery/cell_charger.dm @@ -13,7 +13,7 @@ var/chargelevel = -1 var/charge_rate = 500 -/obj/machinery/cell_charger/proc/updateicon() +/obj/machinery/cell_charger/update_icon() cut_overlays() if(charging) add_overlay(image(charging.icon, charging.icon_state)) @@ -53,7 +53,7 @@ charging = W user.visible_message("[user] inserts a cell into [src].", "You insert a cell into [src].") chargelevel = -1 - updateicon() + update_icon() else if(!charging && default_deconstruction_screwdriver(user, icon_state, icon_state, W)) return @@ -76,7 +76,7 @@ charging.update_icon() charging = null chargelevel = -1 - updateicon() + update_icon() /obj/machinery/cell_charger/attack_hand(mob/user) . = ..() @@ -127,4 +127,4 @@ use_power(charge_rate) charging.give(charge_rate) //this is 2558, efficient batteries exist - updateicon() + update_icon() diff --git a/code/game/machinery/computer/buildandrepair.dm b/code/game/machinery/computer/buildandrepair.dm index 056beb2e96..e8e15ff586 100644 --- a/code/game/machinery/computer/buildandrepair.dm +++ b/code/game/machinery/computer/buildandrepair.dm @@ -133,12 +133,13 @@ ..() /obj/structure/frame/computer/AltClick(mob/user) - ..() + . = ..() if(!isliving(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user))) return if(anchored) to_chat(usr, "You must unwrench [src] before rotating it!") - return + return TRUE setDir(turn(dir, -90)) + return TRUE diff --git a/code/game/machinery/computer/card.dm b/code/game/machinery/computer/card.dm index e8e8e1cdd6..3bdb635d39 100644 --- a/code/game/machinery/computer/card.dm +++ b/code/game/machinery/computer/card.dm @@ -170,19 +170,19 @@ GLOBAL_VAR_INIT(time_last_changed_position, 0) return TRUE /obj/machinery/computer/card/AltClick(mob/user) - ..() + . = ..() if(!user.canUseTopic(src, !issilicon(user)) || !is_operational()) return if(inserted_modify_id) if(id_eject(user, inserted_modify_id)) inserted_modify_id = null updateUsrDialog() - return + return TRUE if(inserted_scan_id) if(id_eject(user, inserted_scan_id)) inserted_scan_id = null updateUsrDialog() - return + return TRUE /obj/machinery/computer/card/ui_interact(mob/user) . = ..() diff --git a/code/game/machinery/computer/prisoner/_prisoner.dm b/code/game/machinery/computer/prisoner/_prisoner.dm index d07c351a22..dfadb73ab2 100644 --- a/code/game/machinery/computer/prisoner/_prisoner.dm +++ b/code/game/machinery/computer/prisoner/_prisoner.dm @@ -5,7 +5,7 @@ if(contained_id) contained_id.forceMove(get_turf(src)) return ..() - + /obj/machinery/computer/prisoner/examine(mob/user) . = ..() @@ -15,8 +15,9 @@ /obj/machinery/computer/prisoner/AltClick(mob/user) + ..() id_eject(user) - return ..() + return TRUE /obj/machinery/computer/prisoner/proc/id_insert(mob/user, obj/item/card/id/prisoner/P) if(istype(P)) diff --git a/code/game/machinery/defibrillator_mount.dm b/code/game/machinery/defibrillator_mount.dm index 97cc0f5748..40ccc61b2b 100644 --- a/code/game/machinery/defibrillator_mount.dm +++ b/code/game/machinery/defibrillator_mount.dm @@ -115,8 +115,10 @@ return TRUE /obj/machinery/defibrillator_mount/AltClick(mob/living/carbon/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE)) return + . = TRUE if(!defib) to_chat(user, "It'd be hard to remove a defib unit from a mount that has none.") return diff --git a/code/game/machinery/deployable.dm b/code/game/machinery/deployable.dm index b94dbad2de..7614630477 100644 --- a/code/game/machinery/deployable.dm +++ b/code/game/machinery/deployable.dm @@ -153,9 +153,11 @@ . += "Alt-click to toggle modes." /obj/item/grenade/barrier/AltClick(mob/living/carbon/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE)) return toggle_mode(user) + return TRUE /obj/item/grenade/barrier/proc/toggle_mode(mob/user) switch(mode) diff --git a/code/game/machinery/dish_drive.dm b/code/game/machinery/dish_drive.dm index 3aad8d874c..7f21d1c1b3 100644 --- a/code/game/machinery/dish_drive.dm +++ b/code/game/machinery/dish_drive.dm @@ -97,8 +97,10 @@ do_the_dishes(TRUE) /obj/machinery/dish_drive/AltClick(mob/living/user) + . = ..() if(user.canUseTopic(src, !issilicon(user))) do_the_dishes(TRUE) + return TRUE /obj/machinery/dish_drive/proc/do_the_dishes(manual) if(!contents.len) diff --git a/code/game/machinery/dna_scanner.dm b/code/game/machinery/dna_scanner.dm index c9e1e7195b..f62dceff6c 100644 --- a/code/game/machinery/dna_scanner.dm +++ b/code/game/machinery/dna_scanner.dm @@ -164,9 +164,11 @@ toggle_open(user) /obj/machinery/dna_scannernew/AltClick(mob/user) + . = ..() if(!user.canUseTopic(src, !issilicon(user))) return interact(user) + return TRUE /obj/machinery/dna_scannernew/MouseDrop_T(mob/target, mob/user) if(user.stat || user.lying || !Adjacent(user) || !user.Adjacent(target) || !iscarbon(target) || !user.IsAdvancedToolUser()) diff --git a/code/game/machinery/harvester.dm b/code/game/machinery/harvester.dm index e6b8fe3b88..e1f9824524 100644 --- a/code/game/machinery/harvester.dm +++ b/code/game/machinery/harvester.dm @@ -51,10 +51,12 @@ open_machine() /obj/machinery/harvester/AltClick(mob/user) + . = ..() if(harvesting || !user || !isliving(user) || state_open) return if(can_harvest()) start_harvest() + return TRUE /obj/machinery/harvester/proc/can_harvest() if(!powered(EQUIP) || state_open || !occupant || !iscarbon(occupant)) diff --git a/code/game/machinery/lightswitch.dm b/code/game/machinery/lightswitch.dm index 694df02eb5..2a4b903906 100644 --- a/code/game/machinery/lightswitch.dm +++ b/code/game/machinery/lightswitch.dm @@ -21,9 +21,9 @@ name = "light switch ([area.name])" on = area.lightswitch - updateicon() + update_icon() -/obj/machinery/light_switch/proc/updateicon() +/obj/machinery/light_switch/update_icon() if(stat & NOPOWER) icon_state = "light-p" else @@ -41,11 +41,11 @@ on = !on area.lightswitch = on - area.updateicon() + area.update_icon() for(var/obj/machinery/light_switch/L in area) L.on = on - L.updateicon() + L.update_icon() area.power_change() @@ -57,7 +57,7 @@ else stat |= NOPOWER - updateicon() + update_icon() /obj/machinery/light_switch/emp_act(severity) . = ..() diff --git a/code/game/machinery/magnet.dm b/code/game/machinery/magnet.dm index 094db5a676..370621e0c8 100644 --- a/code/game/machinery/magnet.dm +++ b/code/game/machinery/magnet.dm @@ -46,10 +46,10 @@ // update the invisibility and icon /obj/machinery/magnetic_module/hide(intact) invisibility = intact ? INVISIBILITY_MAXIMUM : 0 - updateicon() + update_icon() // update the icon_state -/obj/machinery/magnetic_module/proc/updateicon() +/obj/machinery/magnetic_module/update_icon() var/state="floor_magnet" var/onstate="" if(!on) @@ -161,7 +161,7 @@ else use_power = NO_POWER_USE - updateicon() + update_icon() /obj/machinery/magnetic_module/proc/magnetic_process() // proc that actually does the magneting diff --git a/code/game/machinery/navbeacon.dm b/code/game/machinery/navbeacon.dm index d64ae75e2c..0f57bea656 100644 --- a/code/game/machinery/navbeacon.dm +++ b/code/game/machinery/navbeacon.dm @@ -72,10 +72,10 @@ // hide the object if turf is intact /obj/machinery/navbeacon/hide(intact) invisibility = intact ? INVISIBILITY_MAXIMUM : 0 - updateicon() + update_icon() // update the icon_state -/obj/machinery/navbeacon/proc/updateicon() +/obj/machinery/navbeacon/update_icon() var/state="navbeacon[open]" if(invisibility) @@ -94,7 +94,7 @@ user.visible_message("[user] [open ? "opens" : "closes"] the beacon's cover.", "You [open ? "open" : "close"] the beacon's cover.") - updateicon() + update_icon() else if (istype(I, /obj/item/card/id)||istype(I, /obj/item/pda)) if(open) diff --git a/code/game/machinery/washing_machine.dm b/code/game/machinery/washing_machine.dm index 081d91e56f..2ca0df75ae 100644 --- a/code/game/machinery/washing_machine.dm +++ b/code/game/machinery/washing_machine.dm @@ -16,6 +16,7 @@ . += "Alt-click it to start a wash cycle." /obj/machinery/washing_machine/AltClick(mob/user) + . = ..() if(!user.canUseTopic(src)) return @@ -24,11 +25,11 @@ if(state_open) to_chat(user, "Close the door first") - return + return TRUE if(bloody_mess) to_chat(user, "[src] must be cleaned up first.") - return + return TRUE if(has_corgi) bloody_mess = 1 @@ -37,6 +38,7 @@ update_icon() addtimer(CALLBACK(src, .proc/wash_cycle), 200) START_PROCESSING(SSfastprocess, src) + return TRUE /obj/machinery/washing_machine/process() if (!busy) diff --git a/code/game/mecha/combat/gygax.dm b/code/game/mecha/combat/gygax.dm index c7c180af41..0e6980d6b7 100644 --- a/code/game/mecha/combat/gygax.dm +++ b/code/game/mecha/combat/gygax.dm @@ -27,6 +27,7 @@ max_temperature = 35000 leg_overload_coeff = 100 operation_req_access = list(ACCESS_SYNDICATE) + internals_req_access = list(ACCESS_SYNDICATE) wreckage = /obj/structure/mecha_wreckage/gygax/dark max_equip = 4 spawn_tracked = FALSE diff --git a/code/game/mecha/combat/honker.dm b/code/game/mecha/combat/honker.dm index 3a3d98ad1e..ffe318def5 100644 --- a/code/game/mecha/combat/honker.dm +++ b/code/game/mecha/combat/honker.dm @@ -10,6 +10,7 @@ max_temperature = 25000 infra_luminosity = 5 operation_req_access = list(ACCESS_THEATRE) + internals_req_access = list(ACCESS_THEATRE, ACCESS_ROBOTICS) wreckage = /obj/structure/mecha_wreckage/honker add_req_access = 0 max_equip = 3 diff --git a/code/game/mecha/combat/marauder.dm b/code/game/mecha/combat/marauder.dm index 42817b586c..fa9449937b 100644 --- a/code/game/mecha/combat/marauder.dm +++ b/code/game/mecha/combat/marauder.dm @@ -10,6 +10,7 @@ resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF infra_luminosity = 3 operation_req_access = list(ACCESS_CENT_SPECOPS) + internals_req_access = list(ACCESS_CENT_SPECOPS, ACCESS_ROBOTICS) wreckage = /obj/structure/mecha_wreckage/marauder add_req_access = 0 internal_damage_threshold = 25 @@ -46,6 +47,7 @@ name = "\improper Seraph" icon_state = "seraph" operation_req_access = list(ACCESS_CENT_SPECOPS) + internals_req_access = list(ACCESS_CENT_SPECOPS, ACCESS_ROBOTICS) step_in = 3 max_integrity = 550 wreckage = /obj/structure/mecha_wreckage/seraph @@ -72,6 +74,7 @@ name = "\improper Mauler" icon_state = "mauler" operation_req_access = list(ACCESS_SYNDICATE) + internals_req_access = list(ACCESS_SYNDICATE) wreckage = /obj/structure/mecha_wreckage/mauler max_equip = 5 diff --git a/code/game/mecha/combat/neovgre.dm b/code/game/mecha/combat/neovgre.dm index b1f2cdd02a..8fc1dd97b2 100644 --- a/code/game/mecha/combat/neovgre.dm +++ b/code/game/mecha/combat/neovgre.dm @@ -12,6 +12,8 @@ layer = ABOVE_MOB_LAYER breach_time = 100 //ten seconds till all goes to shit recharge_rate = 100 + internals_req_access = list() + add_req_access = 0 wreckage = /obj/structure/mecha_wreckage/durand/neovgre spawn_tracked = FALSE @@ -47,7 +49,7 @@ for(var/mob/M in src) to_chat(M, "You are consumed by the fires raging within Neovgre...") M.dust() - playsound(src, 'sound/magic/lightning_chargeup.ogg', 100, 0) + playsound(src, 'sound/effects/neovgre_exploding.ogg', 100, 0) src.visible_message("The reactor has gone critical, its going to blow!") addtimer(CALLBACK(src,.proc/go_critical),breach_time) diff --git a/code/game/mecha/combat/reticence.dm b/code/game/mecha/combat/reticence.dm index 7e8c865517..446e2e853c 100644 --- a/code/game/mecha/combat/reticence.dm +++ b/code/game/mecha/combat/reticence.dm @@ -10,6 +10,7 @@ max_temperature = 15000 wreckage = /obj/structure/mecha_wreckage/reticence operation_req_access = list(ACCESS_THEATRE) + internals_req_access = list(ACCESS_THEATRE, ACCESS_ROBOTICS) add_req_access = 0 internal_damage_threshold = 25 max_equip = 2 diff --git a/code/game/mecha/mecha.dm b/code/game/mecha/mecha.dm index 35500bed07..7e860d9315 100644 --- a/code/game/mecha/mecha.dm +++ b/code/game/mecha/mecha.dm @@ -68,7 +68,7 @@ var/internal_damage = 0 //contains bitflags var/list/operation_req_access = list()//required access level for mecha operation - var/list/internals_req_access = list(ACCESS_ENGINE,ACCESS_ROBOTICS)//REQUIRED ACCESS LEVEL TO OPEN CELL COMPARTMENT + var/list/internals_req_access = list(ACCESS_ROBOTICS)//REQUIRED ACCESS LEVEL TO OPEN CELL COMPARTMENT var/wreckage diff --git a/code/game/mecha/mecha_actions.dm b/code/game/mecha/mecha_actions.dm index 7b00e208cc..86ec5e70da 100644 --- a/code/game/mecha/mecha_actions.dm +++ b/code/game/mecha/mecha_actions.dm @@ -138,8 +138,10 @@ chassis.toggle_strafe() /obj/mecha/AltClick(mob/living/user) + . = ..() if((user == occupant) && user.canUseTopic(src)) toggle_strafe() + return TRUE /obj/mecha/proc/toggle_strafe() strafe = !strafe diff --git a/code/game/objects/effects/temporary_visuals/miscellaneous.dm b/code/game/objects/effects/temporary_visuals/miscellaneous.dm index 0990486ae3..a103c0a54f 100644 --- a/code/game/objects/effects/temporary_visuals/miscellaneous.dm +++ b/code/game/objects/effects/temporary_visuals/miscellaneous.dm @@ -442,3 +442,10 @@ animate(src, alpha = 0, transform = skew, time = duration) else return INITIALIZE_HINT_QDEL + +/obj/effect/temp_visual/slugboom + icon = 'icons/effects/96x96.dmi' + icon_state = "slugboom" + randomdir = FALSE + duration = 30 + pixel_x = -24 \ No newline at end of file diff --git a/code/game/objects/items/RCD.dm b/code/game/objects/items/RCD.dm index 016bfb5e8b..24c241aabc 100644 --- a/code/game/objects/items/RCD.dm +++ b/code/game/objects/items/RCD.dm @@ -133,15 +133,14 @@ RLD if(!(A in range(custom_range, get_turf(user)))) to_chat(user, "The \'Out of Range\' light on [src] blinks red.") return FALSE - else - return TRUE - -/obj/item/construction/proc/prox_check(proximity) - if(proximity) - return TRUE - else + var/view_range = user.client ? user.client.view : world.view + //if user can't be seen from A (only checks surroundings' opaqueness) and can't see A. + //jarring, but it should stop people from targetting atoms they can't see... + //excluding darkness, to allow RLD to be used to light pitch black dark areas. + if(!((user in view(view_range, A)) || (user in viewers(view_range, A)))) + to_chat(user, "You focus, pointing \the [src] at whatever outside your field of vision in the given direction... to no avail.") return FALSE - + return TRUE /obj/item/construction/rcd name = "rapid-construction-device (RCD)" @@ -523,7 +522,12 @@ RLD /obj/item/construction/rcd/afterattack(atom/A, mob/user, proximity) . = ..() - if(!prox_check(proximity)) + if(!proximity) + if(!ranged || !range_check(A,user)) //early return not-in-range sanity. + return + if(target_check(A,user)) + user.Beam(A,icon_state="rped_upgrade",time=30) + rcd_create(A,user) return rcd_create(A, user) @@ -635,6 +639,7 @@ RLD max_matter = INFINITY matter = INFINITY upgrade = TRUE + ranged = TRUE // Ranged RCD @@ -650,20 +655,10 @@ RLD item_state = "oldrcd" has_ammobar = FALSE -/obj/item/construction/rcd/arcd/afterattack(atom/A, mob/user) - . = ..() - if(!range_check(A,user)) - return - if(target_check(A,user)) - user.Beam(A,icon_state="rped_upgrade",time=30) - rcd_create(A,user) - - // RAPID LIGHTING DEVICE - /obj/item/construction/rld name = "rapid-light-device (RLD)" desc = "A device used to rapidly provide lighting sources to an area. Reload with metal, plasteel, glass or compressed matter cartridges." diff --git a/code/game/objects/items/cards_ids.dm b/code/game/objects/items/cards_ids.dm index 97f5989eb4..fef0c55f5e 100644 --- a/code/game/objects/items/cards_ids.dm +++ b/code/game/objects/items/cards_ids.dm @@ -517,16 +517,17 @@ update_label("John Doe", "Clowny") return if(user.incapacitated() || !istype(user)) to_chat(user, "You can't do that right now!") - return + return TRUE if(alert("Are you sure you want to recolor your id?", "Confirm Repaint", "Yes", "No") == "Yes") var/energy_color_input = input(usr,"","Choose Energy Color",id_color) as color|null if(!in_range(src, user) || !energy_color_input) - return + return TRUE if(user.incapacitated() || !istype(user)) to_chat(user, "You can't do that right now!") - return + return TRUE id_color = sanitize_hexcolor(energy_color_input, desired_format=6, include_crunch=1) update_icon() + return TRUE /obj/item/card/id/knight/Initialize() . = ..() diff --git a/code/game/objects/items/circuitboards/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machine_circuitboards.dm index ccd96525a3..2392973b22 100644 --- a/code/game/objects/items/circuitboards/machine_circuitboards.dm +++ b/code/game/objects/items/circuitboards/machine_circuitboards.dm @@ -945,10 +945,12 @@ to_chat(user, "You [suction ? "enable" : "disable"] the board's suction function.") /obj/item/circuitboard/machine/dish_drive/AltClick(mob/living/user) + . = ..() if(!user.Adjacent(src)) return transmit = !transmit to_chat(user, "You [transmit ? "enable" : "disable"] the board's automatic disposal transmission.") + return TRUE /obj/item/circuitboard/machine/stacking_unit_console name = "Stacking Machine Console (Machine Board)" diff --git a/code/game/objects/items/crayons.dm b/code/game/objects/items/crayons.dm index 2a7b5d24cd..41ab418cf2 100644 --- a/code/game/objects/items/crayons.dm +++ b/code/game/objects/items/crayons.dm @@ -150,11 +150,13 @@ ui.open() /obj/item/toy/crayon/spraycan/AltClick(mob/user) + . = ..() if(user.canUseTopic(src, BE_CLOSE, ismonkey(user))) if(has_cap) is_capped = !is_capped to_chat(user, "The cap on [src] is now [is_capped ? "on" : "off"].") update_icon() + return TRUE /obj/item/toy/crayon/proc/staticDrawables() diff --git a/code/game/objects/items/devices/PDA/PDA.dm b/code/game/objects/items/devices/PDA/PDA.dm index 836caeaf04..d97c07bf87 100644 --- a/code/game/objects/items/devices/PDA/PDA.dm +++ b/code/game/objects/items/devices/PDA/PDA.dm @@ -828,14 +828,14 @@ GLOBAL_LIST_EMPTY(PDAs) send_message(U,list(P)) /obj/item/pda/AltClick() - ..() - + . = ..() if(id) remove_id() playsound(src, 'sound/machines/terminal_eject_disc.ogg', 50, 1) else remove_pen() playsound(src, 'sound/machines/button4.ogg', 50, 1) + return TRUE /obj/item/pda/CtrlClick() ..() diff --git a/code/game/objects/items/devices/desynchronizer.dm b/code/game/objects/items/devices/desynchronizer.dm index 4a6e2d5a46..ff58af2405 100644 --- a/code/game/objects/items/devices/desynchronizer.dm +++ b/code/game/objects/items/devices/desynchronizer.dm @@ -32,6 +32,7 @@ . += "Can be used again to interrupt the effect early. The recharge time is the same as the time spent in desync." /obj/item/desynchronizer/AltClick(mob/living/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user))) return var/new_duration = input(user, "Set the duration (5-300):", "Desynchronizer", duration / 10) as null|num @@ -40,6 +41,7 @@ new_duration = CLAMP(new_duration, 50, max_duration) duration = new_duration to_chat(user, "You set the duration to [DisplayTimeText(duration)].") + return TRUE /obj/item/desynchronizer/proc/desync(mob/living/user) if(sync_holder) diff --git a/code/game/objects/items/devices/dogborg_sleeper.dm b/code/game/objects/items/devices/dogborg_sleeper.dm index 1a3bc283d9..9b4842b291 100644 --- a/code/game/objects/items/devices/dogborg_sleeper.dm +++ b/code/game/objects/items/devices/dogborg_sleeper.dm @@ -153,6 +153,7 @@ playsound(loc, voracious ? 'sound/effects/splat.ogg' : 'sound/effects/bin_close.ogg', 50, 1) items_preserved.Cut() cleaning = FALSE + patient = null if(hound) update_gut(hound) @@ -525,5 +526,5 @@ playsound(hound, 'sound/effects/bin_close.ogg', 80, 1) /obj/item/dogborg/sleeper/K9/flavour - name = "Mobile Sleeper" + name = "Recreational Sleeper" desc = "A mounted, underslung sleeper, intended for holding willing occupants for leisurely purposes." \ No newline at end of file diff --git a/code/game/objects/items/devices/geiger_counter.dm b/code/game/objects/items/devices/geiger_counter.dm index e6044ceadf..ce0e492393 100644 --- a/code/game/objects/items/devices/geiger_counter.dm +++ b/code/game/objects/items/devices/geiger_counter.dm @@ -182,14 +182,16 @@ return ..() /obj/item/geiger_counter/AltClick(mob/living/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE)) - return ..() + return if(!scanning) to_chat(usr, "[src] must be on to reset its radiation level!") - return 0 + return TRUE radiation_count = 0 to_chat(usr, "You flush [src]'s radiation counts, resetting it to normal.") update_icon() + return TRUE /obj/item/geiger_counter/emag_act(mob/user) . = ..() diff --git a/code/game/objects/items/devices/gps.dm b/code/game/objects/items/devices/gps.dm index 2368cdc4f0..acdb546a34 100644 --- a/code/game/objects/items/devices/gps.dm +++ b/code/game/objects/items/devices/gps.dm @@ -45,9 +45,11 @@ GLOBAL_LIST_EMPTY(GPS_list) add_overlay("working") /obj/item/gps/AltClick(mob/user) + . = ..() if(!user.canUseTopic(src, BE_CLOSE)) return toggletracking(user) + return TRUE /obj/item/gps/proc/toggletracking(mob/user) if(!user.canUseTopic(src, BE_CLOSE)) diff --git a/code/game/objects/items/devices/laserpointer.dm b/code/game/objects/items/devices/laserpointer.dm index 7baea7f488..1654ecde41 100644 --- a/code/game/objects/items/devices/laserpointer.dm +++ b/code/game/objects/items/devices/laserpointer.dm @@ -69,14 +69,9 @@ if (!user.IsAdvancedToolUser()) to_chat(user, "You don't have the dexterity to do this!") return - if(HAS_TRAIT(user, TRAIT_NOGUNS)) + if(HAS_TRAIT(user, TRAIT_CHUNKYFINGERS)) to_chat(user, "Your fingers can't press the button!") return - if(ishuman(user)) - var/mob/living/carbon/human/H = user - if(H.dna.check_mutation(HULK)) - to_chat(user, "Your fingers can't press the button!") - return add_fingerprint(user) diff --git a/code/game/objects/items/devices/quantum_keycard.dm b/code/game/objects/items/devices/quantum_keycard.dm index fc9ccddaf9..33f839fa39 100644 --- a/code/game/objects/items/devices/quantum_keycard.dm +++ b/code/game/objects/items/devices/quantum_keycard.dm @@ -18,12 +18,14 @@ . += "Insert [src] into an active quantum pad to link it." /obj/item/quantum_keycard/AltClick(mob/living/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user))) return to_chat(user, "You start pressing [src]'s unlink button...") if(do_after(user, 40, target = src)) to_chat(user, "The keycard beeps twice and disconnects the quantum link.") qpad = null + return TRUE /obj/item/quantum_keycard/update_icon() if(qpad) diff --git a/code/game/objects/items/devices/radio/headset.dm b/code/game/objects/items/devices/radio/headset.dm index 9f371f1ddb..1d0f94de71 100644 --- a/code/game/objects/items/devices/radio/headset.dm +++ b/code/game/objects/items/devices/radio/headset.dm @@ -326,8 +326,10 @@ GLOBAL_LIST_INIT(channel_tokens, list( secure_radio_connections[ch_name] = add_radio(src, GLOB.radiochannels[ch_name]) /obj/item/radio/headset/AltClick(mob/living/user) + . = ..() if(!istype(user) || !Adjacent(user) || user.incapacitated()) return if (command) use_command = !use_command to_chat(user, "You toggle high-volume mode [use_command ? "on" : "off"].") + return TRUE diff --git a/code/game/objects/items/devices/scanners.dm b/code/game/objects/items/devices/scanners.dm index d1e0f4841d..00381b9838 100644 --- a/code/game/objects/items/devices/scanners.dm +++ b/code/game/objects/items/devices/scanners.dm @@ -412,7 +412,7 @@ SLIME SCANNER if(ishuman(C)) if(H.bleed_rate) msg += "Subject is bleeding!\n" - var/blood_percent = round((C.blood_volume / (BLOOD_VOLUME_NORMAL * C.blood_ratio))*100) + var/blood_percent = round((C.scan_blood_volume() / (BLOOD_VOLUME_NORMAL * C.blood_ratio))*100) var/blood_type = C.dna.blood_type if(blood_id != ("blood" || "jellyblood"))//special blood substance var/datum/reagent/R = GLOB.chemical_reagents_list[blood_id] @@ -420,12 +420,12 @@ SLIME SCANNER blood_type = R.name else blood_type = blood_id - if(C.blood_volume <= (BLOOD_VOLUME_SAFE*C.blood_ratio) && C.blood_volume > (BLOOD_VOLUME_OKAY*C.blood_ratio)) - msg += "LOW blood level [blood_percent] %, [C.blood_volume] cl, type: [blood_type]\n" - else if(C.blood_volume <= (BLOOD_VOLUME_OKAY*C.blood_ratio)) - msg += "CRITICAL blood level [blood_percent] %, [C.blood_volume] cl, type: [blood_type]\n" + if(C.scan_blood_volume() <= (BLOOD_VOLUME_SAFE*C.blood_ratio) && C.scan_blood_volume() > (BLOOD_VOLUME_OKAY*C.blood_ratio)) + msg += "LOW blood level [blood_percent] %, [C.scan_blood_volume()] cl, type: [blood_type]\n" + else if(C.scan_blood_volume() <= (BLOOD_VOLUME_OKAY*C.blood_ratio)) + msg += "CRITICAL blood level [blood_percent] %, [C.scan_blood_volume()] cl, type: [blood_type]\n" else - msg += "Blood level [blood_percent] %, [C.blood_volume] cl, type: [blood_type]\n" + msg += "Blood level [blood_percent] %, [C.scan_blood_volume()] cl, type: [blood_type]\n" var/cyberimp_detect for(var/obj/item/organ/cyberimp/CI in C.internal_organs) @@ -590,10 +590,10 @@ SLIME SCANNER to_chat(user, "Temperature: [round(environment.temperature-T0C, 0.01)] °C ([round(environment.temperature, 0.01)] K)") /obj/item/analyzer/AltClick(mob/user) //Barometer output for measuring when the next storm happens - ..() + . = ..() if(user.canUseTopic(src)) - + . = TRUE if(cooldown) to_chat(user, "[src]'s barometer function is preparing itself.") return diff --git a/code/game/objects/items/flamethrower.dm b/code/game/objects/items/flamethrower.dm index d9525ab931..906572d018 100644 --- a/code/game/objects/items/flamethrower.dm +++ b/code/game/objects/items/flamethrower.dm @@ -138,11 +138,13 @@ toggle_igniter(user) /obj/item/flamethrower/AltClick(mob/user) + . = ..() if(ptank && isliving(user) && user.canUseTopic(src, BE_CLOSE, ismonkey(user))) user.put_in_hands(ptank) ptank = null to_chat(user, "You remove the plasma tank from [src]!") update_icon() + return TRUE /obj/item/flamethrower/examine(mob/user) . = ..() diff --git a/code/game/objects/items/granters.dm b/code/game/objects/items/granters.dm index 6ea68bf548..62a7742628 100644 --- a/code/game/objects/items/granters.dm +++ b/code/game/objects/items/granters.dm @@ -183,6 +183,23 @@ explosion(user.loc, 1, 0, 2, 3, FALSE, FALSE, 2) qdel(src) +/obj/item/book/granter/spell/nuclearfist + spell = /obj/effect/proc_holder/spell/targeted/touch/nuclear_fist + spellname = "nuclear fist" + icon_state ="booknuclearfist" + desc = "This book radiates confidence." + remarks = list("Line them up....", ".. knock em' down...", "Dress in yellow for maximum effect... why?", "The energy comes from spinach... huh", "Work out for three years? No way!", "Oh I'll cast you a spell allright...", "What ho mighty wizard... ho ho ho...") + +/obj/item/book/granter/spell/nuclearfist/recoil(mob/living/carbon/user) + ..() + to_chat(user, "Your arm spontaneously detonates!") + explosion(user.loc, -1, 0, 2, -1, FALSE, FALSE, 2) + var/obj/item/bodypart/part = user.get_holding_bodypart_of_item(src) + if(part) + part.dismember() + qdel(part) + + /obj/item/book/granter/spell/sacredflame spell = /obj/effect/proc_holder/spell/targeted/sacred_flame spellname = "sacred flame" diff --git a/code/game/objects/items/implants/implant_mindshield.dm b/code/game/objects/items/implants/implant_mindshield.dm index 2a99c6e1ec..52f9f3b9a4 100644 --- a/code/game/objects/items/implants/implant_mindshield.dm +++ b/code/game/objects/items/implants/implant_mindshield.dm @@ -26,6 +26,9 @@ if(target.mind.has_antag_datum(/datum/antagonist/brainwashed)) target.mind.remove_antag_datum(/datum/antagonist/brainwashed) + if(target.mind.has_antag_datum(ANTAG_DATUM_VASSAL)) + SSticker.mode.remove_vassal(target.mind) + if(target.mind.has_antag_datum(/datum/antagonist/rev/head) || target.mind.unconvertable || target.mind.has_antag_datum(/datum/antagonist/gang/boss)) if(!silent) target.visible_message("[target] seems to resist the implant!", "You feel something interfering with your mental conditioning, but you resist it!") diff --git a/code/game/objects/items/melee/energy.dm b/code/game/objects/items/melee/energy.dm index 561af8ded6..16262d61c3 100644 --- a/code/game/objects/items/melee/energy.dm +++ b/code/game/objects/items/melee/energy.dm @@ -320,11 +320,12 @@ M.update_inv_hands() /obj/item/melee/transforming/energy/sword/cx/AltClick(mob/living/user) + . = ..() if(!in_range(src, user)) //Basic checks to prevent abuse return if(user.incapacitated() || !istype(user)) to_chat(user, "You can't do that right now!") - return + return TRUE if(alert("Are you sure you want to recolor your blade?", "Confirm Repaint", "Yes", "No") == "Yes") var/energy_color_input = input(usr,"","Choose Energy Color",light_color) as color|null @@ -332,6 +333,7 @@ light_color = sanitize_hexcolor(energy_color_input, desired_format=6, include_crunch=1) update_icon() update_light() + return TRUE /obj/item/melee/transforming/energy/sword/cx/examine(mob/user) . = ..() diff --git a/code/game/objects/items/melee/misc.dm b/code/game/objects/items/melee/misc.dm index d7c2f7f4f6..d8abff70a6 100644 --- a/code/game/objects/items/melee/misc.dm +++ b/code/game/objects/items/melee/misc.dm @@ -79,17 +79,17 @@ final_block_chance = 0 //Don't bring a sword to a gunfight return ..() -/obj/item/melee/sabre/on_exit_storage(obj/item/storage/S) - ..() - var/obj/item/storage/belt/sabre/B = S +/obj/item/melee/sabre/on_exit_storage(datum/component/storage/S) + var/obj/item/storage/belt/sabre/B = S.parent if(istype(B)) playsound(B, 'sound/items/unsheath.ogg', 25, 1) - -/obj/item/melee/sabre/on_enter_storage(obj/item/storage/S) ..() - var/obj/item/storage/belt/sabre/B = S + +/obj/item/melee/sabre/on_enter_storage(datum/component/storage/S) + var/obj/item/storage/belt/sabre/B = S.parent if(istype(B)) playsound(B, 'sound/items/sheath.ogg', 25, 1) + ..() /obj/item/melee/sabre/get_belt_overlay() return mutable_appearance('icons/obj/clothing/belt_overlays.dmi', "sabre") diff --git a/code/game/objects/items/pet_carrier.dm b/code/game/objects/items/pet_carrier.dm index b107010991..5af9c70d9b 100644 --- a/code/game/objects/items/pet_carrier.dm +++ b/code/game/objects/items/pet_carrier.dm @@ -70,6 +70,7 @@ update_icon() /obj/item/pet_carrier/AltClick(mob/living/user) + . = ..() if(open || !user.canUseTopic(src, BE_CLOSE)) return locked = !locked @@ -79,6 +80,7 @@ else playsound(user, 'sound/machines/boltsup.ogg', 30, TRUE) update_icon() + return TRUE /obj/item/pet_carrier/attack(mob/living/target, mob/living/user) if(user.a_intent == INTENT_HARM) diff --git a/code/game/objects/items/pneumaticCannon.dm b/code/game/objects/items/pneumaticCannon.dm index 72d27ae544..e63b4d7a3a 100644 --- a/code/game/objects/items/pneumaticCannon.dm +++ b/code/game/objects/items/pneumaticCannon.dm @@ -213,7 +213,7 @@ loadedWeightClass -= I.w_class else if (A == tank) tank = null - update_icons() + update_icon() /obj/item/pneumatic_cannon/ghetto //Obtainable by improvised methods; more gas per use, less capacity, but smaller name = "improvised pneumatic cannon" @@ -239,14 +239,13 @@ return to_chat(user, "You hook \the [thetank] up to \the [src].") tank = thetank - update_icons() + update_icon() -/obj/item/pneumatic_cannon/proc/update_icons() +/obj/item/pneumatic_cannon/update_icon() cut_overlays() if(!tank) return add_overlay(tank.icon_state) - update_icon() /obj/item/pneumatic_cannon/proc/fill_with_type(type, amount) if(!ispath(type, /obj) && !ispath(type, /mob)) diff --git a/code/game/objects/items/robot/robot_parts.dm b/code/game/objects/items/robot/robot_parts.dm index 95110692c9..ab8030c9c2 100644 --- a/code/game/objects/items/robot/robot_parts.dm +++ b/code/game/objects/items/robot/robot_parts.dm @@ -24,7 +24,7 @@ /obj/item/robot_suit/New() ..() - updateicon() + update_icon() /obj/item/robot_suit/prebuilt/New() l_arm = new(src) @@ -39,7 +39,7 @@ chest.cell = new /obj/item/stock_parts/cell/high/plus(chest) ..() -/obj/item/robot_suit/proc/updateicon() +/obj/item/robot_suit/update_icon() cut_overlays() if(l_arm) add_overlay("[l_arm.icon_state]+o") @@ -96,7 +96,7 @@ to_chat(user, "You disassemble the cyborg shell.") else to_chat(user, "There is nothing to remove from the endoskeleton.") - updateicon() + update_icon() /obj/item/robot_suit/proc/put_in_hand_or_drop(mob/living/user, obj/item/I) //normal put_in_hands() drops the item ontop of the player, this drops it at the suit's loc if(!user.put_in_hands(I)) @@ -160,7 +160,7 @@ W.icon_state = initial(W.icon_state) W.cut_overlays() src.l_leg = W - src.updateicon() + update_icon() else if(istype(W, /obj/item/bodypart/r_leg/robot)) if(src.r_leg) @@ -170,7 +170,7 @@ W.icon_state = initial(W.icon_state) W.cut_overlays() src.r_leg = W - src.updateicon() + update_icon() else if(istype(W, /obj/item/bodypart/l_arm/robot)) if(src.l_arm) @@ -180,7 +180,7 @@ W.icon_state = initial(W.icon_state) W.cut_overlays() src.l_arm = W - src.updateicon() + update_icon() else if(istype(W, /obj/item/bodypart/r_arm/robot)) if(src.r_arm) @@ -190,7 +190,7 @@ W.icon_state = initial(W.icon_state)//in case it is a dismembered robotic limb W.cut_overlays() src.r_arm = W - src.updateicon() + update_icon() else if(istype(W, /obj/item/bodypart/chest/robot)) var/obj/item/bodypart/chest/robot/CH = W @@ -202,7 +202,7 @@ CH.icon_state = initial(CH.icon_state) //in case it is a dismembered robotic limb CH.cut_overlays() src.chest = CH - src.updateicon() + update_icon() else if(!CH.wired) to_chat(user, "You need to attach wires to it first!") else @@ -222,7 +222,7 @@ HD.icon_state = initial(HD.icon_state)//in case it is a dismembered robotic limb HD.cut_overlays() src.head = HD - src.updateicon() + update_icon() else to_chat(user, "You need to attach a flash to it first!") diff --git a/code/game/objects/items/robot/robot_upgrades.dm b/code/game/objects/items/robot/robot_upgrades.dm index a0b78d8b27..bf363f959c 100644 --- a/code/game/objects/items/robot/robot_upgrades.dm +++ b/code/game/objects/items/robot/robot_upgrades.dm @@ -74,7 +74,7 @@ /obj/item/borg/upgrade/vtec/action(mob/living/silicon/robot/R, user = usr) . = ..() if(.) - if(R.speed < 0) + if(!R.cansprint) to_chat(R, "A VTEC unit is already installed!") to_chat(user, "There's no room for another VTEC unit!") return FALSE @@ -82,11 +82,13 @@ //R.speed = -2 // Gotta go fast. //Citadel change - makes vtecs give an ability rather than reducing the borg's speed instantly R.AddAbility(new/obj/effect/proc_holder/silicon/cyborg/vtecControl) + R.cansprint = 0 /obj/item/borg/upgrade/vtec/deactivate(mob/living/silicon/robot/R, user = usr) . = ..() if (.) R.speed = initial(R.speed) + R.cansprint = 1 /obj/item/borg/upgrade/disablercooler name = "cyborg rapid energy blaster cooling module" @@ -409,8 +411,7 @@ icon_state = "cyborg_upgrade3" require_module = 1 module_type = list(/obj/item/robot_module/medical, - /obj/item/robot_module/syndicate_medical, - /obj/item/robot_module/medihound) + /obj/item/robot_module/syndicate_medical) var/list/additional_reagents = list() /obj/item/borg/upgrade/hypospray/action(mob/living/silicon/robot/R, user = usr) @@ -466,23 +467,6 @@ for(var/obj/item/reagent_containers/borghypo/H in R.module.modules) H.bypass_protection = initial(H.bypass_protection) -/obj/item/borg/upgrade/defib - name = "medical cyborg defibrillator" - desc = "An upgrade to the Medical module, installing a built-in \ - defibrillator, for on the scene revival." - icon_state = "cyborg_upgrade3" - require_module = 1 - module_type = list(/obj/item/robot_module/medical, - /obj/item/robot_module/syndicate_medical, - /obj/item/robot_module/medihound) - -/obj/item/borg/upgrade/defib/action(mob/living/silicon/robot/R, user = usr) - . = ..() - if(.) - var/obj/item/twohanded/shockpaddles/cyborg/S = new(R.module) - R.module.basic_modules += S - R.module.add_module(S, FALSE, TRUE) - /obj/item/borg/upgrade/defib/deactivate(mob/living/silicon/robot/R, user = usr) . = ..() if (.) @@ -497,8 +481,7 @@ icon_state = "cyborg_upgrade3" require_module = 1 module_type = list(/obj/item/robot_module/medical, - /obj/item/robot_module/syndicate_medical, - /obj/item/robot_module/medihound) + /obj/item/robot_module/syndicate_medical) /obj/item/borg/upgrade/processor/action(mob/living/silicon/robot/R, user = usr) . = ..() @@ -521,8 +504,7 @@ require_module = 1 module_type = list( /obj/item/robot_module/medical, - /obj/item/robot_module/syndicate_medical, - /obj/item/robot_module/medihound) + /obj/item/robot_module/syndicate_medical) /obj/item/borg/upgrade/advhealth/action(mob/living/silicon/robot/R, user = usr) . = ..() @@ -642,8 +624,7 @@ icon_state = "pinpointer_crew" require_module = TRUE module_type = list(/obj/item/robot_module/medical, - /obj/item/robot_module/syndicate_medical, - /obj/item/robot_module/medihound) + /obj/item/robot_module/syndicate_medical) /obj/item/borg/upgrade/pinpointer/action(mob/living/silicon/robot/R, user = usr) . = ..() diff --git a/code/game/objects/items/stacks/sheets/sheet_types.dm b/code/game/objects/items/stacks/sheets/sheet_types.dm index 33a895cad7..4fc31ea674 100644 --- a/code/game/objects/items/stacks/sheets/sheet_types.dm +++ b/code/game/objects/items/stacks/sheets/sheet_types.dm @@ -228,7 +228,7 @@ GLOBAL_LIST_INIT(wood_recipes, list ( \ /obj/item/stack/sheet/mineral/wood name = "wooden plank" - desc = "One can only guess that this is a bunch of wood." + desc = "One can only guess that this is a bunch of wood. You might be able to make a stake with this if you use something sharp on it" singular_name = "wood plank" icon_state = "sheet-wood" item_state = "sheet-wood" @@ -240,6 +240,35 @@ GLOBAL_LIST_INIT(wood_recipes, list ( \ novariants = TRUE grind_results = list("carbon" = 20) + +/obj/item/stack/sheet/mineral/wood/attackby(obj/item/W, mob/user, params) // NOTE: sheet_types.dm is where the WOOD stack lives. Maybe move this over there. + // Taken from /obj/item/stack/rods/attackby in [rods.dm] + if(W.get_sharpness()) + user.visible_message("[user] begins whittling [src] into a pointy object.", \ + "You begin whittling [src] into a sharp point at one end.", \ + "You hear wood carving.") + // 8 Second Timer + if(!do_after(user, 80, TRUE, src)) + return + // Make Stake + var/obj/item/stake/basic/new_item = new(user.loc) + user.visible_message("[user] finishes carving a stake out of [src].", \ + "You finish carving a stake out of [src].") + // Prepare to Put in Hands (if holding wood) + var/obj/item/stack/sheet/mineral/wood/N = src + var/replace = (user.get_inactive_held_item() == N) + // Use Wood + N.use(1) + // If stack depleted, put item in that hand (if it had one) + if (!N && replace) + user.put_in_hands(new_item) + if(istype(W, merge_type)) + var/obj/item/stack/S = W + if(merge(S)) + to_chat(user, "Your [S.name] stack now contains [S.get_amount()] [S.singular_name]\s.") + else + . = ..() + /obj/item/stack/sheet/mineral/wood/Initialize(mapload, new_amount, merge = TRUE) recipes = GLOB.wood_recipes return ..() @@ -247,6 +276,33 @@ GLOBAL_LIST_INIT(wood_recipes, list ( \ /obj/item/stack/sheet/mineral/wood/fifty amount = 50 +/* + * Bamboo + */ + +GLOBAL_LIST_INIT(bamboo_recipes, list ( \ + new/datum/stack_recipe("punji sticks trap", /obj/structure/punji_sticks, 5, time = 30, one_per_turf = TRUE, on_floor = TRUE), \ + new/datum/stack_recipe("blow gun", /obj/item/gun/syringe/blowgun, 10, time = 70), \ + )) + +/obj/item/stack/sheet/mineral/bamboo + name = "bamboo cuttings" + desc = "Finely cut bamboo sticks." + singular_name = "cut bamboo" + icon_state = "sheet-bamboo" + item_state = "sheet-bamboo" + icon = 'icons/obj/stack_objects.dmi' + throwforce = 15 + armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 50, "acid" = 0) + resistance_flags = FLAMMABLE + merge_type = /obj/item/stack/sheet/mineral/bamboo + grind_results = list("carbon" = 5) + +/obj/item/stack/sheet/mineral/bamboo/Initialize(mapload, new_amount, merge = TRUE) + recipes = GLOB.bamboo_recipes + return ..() + + /* * Cloth */ @@ -696,5 +752,3 @@ new /datum/stack_recipe("paper frame door", /obj/structure/mineral_door/paperfra merge_type = /obj/item/stack/sheet/cotton/durathread pull_effort = 70 loom_result = /obj/item/stack/sheet/durathread - - diff --git a/code/game/objects/items/stacks/stack.dm b/code/game/objects/items/stacks/stack.dm index 965e78036a..e8bd50f19b 100644 --- a/code/game/objects/items/stacks/stack.dm +++ b/code/game/objects/items/stacks/stack.dm @@ -350,6 +350,7 @@ . = ..() /obj/item/stack/AltClick(mob/living/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user))) return if(is_cyborg) @@ -363,10 +364,11 @@ max = get_amount() stackmaterial = min(max, stackmaterial) if(stackmaterial == null || stackmaterial <= 0 || !user.canUseTopic(src, BE_CLOSE, ismonkey(user))) - return + return TRUE else change_stack(user, stackmaterial) to_chat(user, "You take [stackmaterial] sheets out of the stack") + return TRUE /obj/item/stack/proc/change_stack(mob/user, amount) if(!use(amount, TRUE, FALSE)) diff --git a/code/game/objects/items/storage/belt.dm b/code/game/objects/items/storage/belt.dm index 47d2d70db8..3a4579d3f8 100755 --- a/code/game/objects/items/storage/belt.dm +++ b/code/game/objects/items/storage/belt.dm @@ -757,23 +757,13 @@ STR.rustle_sound = FALSE STR.max_w_class = WEIGHT_CLASS_BULKY STR.can_hold = typecacheof(fitting_swords) + STR.quickdraw = TRUE /obj/item/storage/belt/sabre/examine(mob/user) . = ..() if(length(contents)) . += "Alt-click it to quickly draw the blade." -/obj/item/storage/belt/sabre/AltClick(mob/user) - if(!iscarbon(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user))) - return - if(length(contents)) - var/obj/item/I = contents[1] - user.visible_message("[user] takes [I] out of [src].", "You take [I] out of [src].") - user.put_in_hands(I) - update_icon() - else - to_chat(user, "[src] is empty.") - /obj/item/storage/belt/sabre/update_icon() . = ..() if(isliving(loc)) diff --git a/code/game/objects/items/storage/fancy.dm b/code/game/objects/items/storage/fancy.dm index 6cc2cb6882..f05078de81 100644 --- a/code/game/objects/items/storage/fancy.dm +++ b/code/game/objects/items/storage/fancy.dm @@ -157,6 +157,7 @@ to_chat(user, "You take \a [W] out of the pack.") else to_chat(user, "There are no [icon_type]s left in the pack.") + return TRUE /obj/item/storage/fancy/cigarettes/update_icon() if(fancy_open || !contents.len) diff --git a/code/game/objects/items/storage/lockbox.dm b/code/game/objects/items/storage/lockbox.dm index bd234d8188..3d27370334 100644 --- a/code/game/objects/items/storage/lockbox.dm +++ b/code/game/objects/items/storage/lockbox.dm @@ -114,11 +114,12 @@ . += "Alt-click to [open ? "close":"open"] it." /obj/item/storage/lockbox/medal/AltClick(mob/user) + . = ..() if(user.canUseTopic(src, BE_CLOSE)) if(!SEND_SIGNAL(src, COMSIG_IS_STORAGE_LOCKED)) open = (open ? FALSE : TRUE) update_icon() - ..() + return TRUE /obj/item/storage/lockbox/medal/PopulateContents() new /obj/item/clothing/accessory/medal/gold/captain(src) diff --git a/code/game/objects/items/toys.dm b/code/game/objects/items/toys.dm index c491e502a4..08c43f0437 100644 --- a/code/game/objects/items/toys.dm +++ b/code/game/objects/items/toys.dm @@ -338,11 +338,12 @@ M.update_inv_hands() /obj/item/toy/sword/cx/AltClick(mob/living/user) + . = ..() if(!in_range(src, user)) //Basic checks to prevent abuse return if(user.incapacitated() || !istype(user)) to_chat(user, "You can't do that right now!") - return + return TRUE if(alert("Are you sure you want to recolor your blade?", "Confirm Repaint", "Yes", "No") == "Yes") var/energy_color_input = input(usr,"","Choose Energy Color",light_color) as color|null @@ -350,6 +351,7 @@ light_color = sanitize_hexcolor(energy_color_input, desired_format=6, include_crunch=1) update_icon() update_light() + return TRUE /obj/item/toy/sword/cx/worn_overlays(isinhands, icon_file) . = ..() diff --git a/code/game/objects/items/twohanded.dm b/code/game/objects/items/twohanded.dm index f559d2801d..dd7d46d0d7 100644 --- a/code/game/objects/items/twohanded.dm +++ b/code/game/objects/items/twohanded.dm @@ -541,6 +541,7 @@ clean_blood() /obj/item/twohanded/dualsaber/hypereutactic/AltClick(mob/living/user) + . = ..() if(!user.canUseTopic(src, BE_CLOSE, FALSE) || hacked) return if(user.incapacitated() || !istype(user)) @@ -553,6 +554,7 @@ light_color = sanitize_hexcolor(energy_color_input, desired_format=6, include_crunch=1) update_icon() update_light() + return TRUE /obj/item/twohanded/dualsaber/hypereutactic/worn_overlays(isinhands, icon_file) . = ..() @@ -659,6 +661,7 @@ qdel(src) /obj/item/twohanded/spear/AltClick(mob/user) + . = ..() if(user.canUseTopic(src, BE_CLOSE)) ..() if(!explosive) @@ -667,6 +670,7 @@ var/input = stripped_input(user,"What do you want your war cry to be? You will shout it when you hit someone in melee.", ,"", 50) if(input) src.war_cry = input + return TRUE /obj/item/twohanded/spear/CheckParts(list/parts_list) var/obj/item/shard/tip = locate() in parts_list diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index e6c7f987d5..51386c791c 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -234,6 +234,7 @@ . = ..() if(unique_reskin && (!current_skin || always_reskinnable) && user.canUseTopic(src, BE_CLOSE, NO_DEXTERY)) reskin_obj(user) + return TRUE /obj/proc/reskin_obj(mob/M) if(!LAZYLEN(unique_reskin)) diff --git a/code/game/objects/structures/beds_chairs/chair.dm b/code/game/objects/structures/beds_chairs/chair.dm index 9643ff7e07..e61a17b4ec 100644 --- a/code/game/objects/structures/beds_chairs/chair.dm +++ b/code/game/objects/structures/beds_chairs/chair.dm @@ -449,6 +449,9 @@ item_chair = null var/turns = 0 +/obj/structure/chair/brass/ComponentInitialize() + return //it spins with the power of ratvar, not components. + /obj/structure/chair/brass/Destroy() STOP_PROCESSING(SSfastprocess, src) . = ..() @@ -464,6 +467,7 @@ return /obj/structure/chair/brass/AltClick(mob/living/user) + . = ..() turns = 0 if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user))) return @@ -475,6 +479,7 @@ user.visible_message("[user] stops [src]'s uncontrollable spinning.", \ "You grab [src] and stop its wild spinning.") STOP_PROCESSING(SSfastprocess, src) + return TRUE /obj/structure/chair/bronze name = "brass chair" diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm index 1f2e7b2190..38944c0384 100644 --- a/code/game/objects/structures/crates_lockers/closets.dm +++ b/code/game/objects/structures/crates_lockers/closets.dm @@ -522,11 +522,12 @@ to_chat(user, "You fail to break out of [src]!") /obj/structure/closet/AltClick(mob/user) - ..() + . = ..() if(!user.canUseTopic(src, be_close=TRUE) || !isturf(loc)) to_chat(user, "You can't do that right now!") - return + return TRUE togglelock(user) + return TRUE /obj/structure/closet/CtrlShiftClick(mob/living/user) if(!HAS_TRAIT(user, TRAIT_SKITTISH)) diff --git a/code/game/objects/structures/crates_lockers/crates.dm b/code/game/objects/structures/crates_lockers/crates.dm index 23703c7891..e416b2b436 100644 --- a/code/game/objects/structures/crates_lockers/crates.dm +++ b/code/game/objects/structures/crates_lockers/crates.dm @@ -79,6 +79,15 @@ material_drop = /obj/item/stack/sheet/mineral/wood material_drop_amount = 5 +/obj/structure/closet/crate/coffin/examine(mob/user) + . = ..() + if(user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) + . += {"This is a coffin which you can use to regenerate your burns and other wounds faster."} + . += {"You can also thicken your blood if you survive the day, and hide from the sun safely while inside."} + /* if(user.mind.has_antag_datum(ANTAG_DATUM_VASSAL) + . += {"This is a coffin which your master can use to shield himself from the unforgiving sun.\n + You yourself are still human and dont need it. Yet."} */ + /obj/structure/closet/crate/internals desc = "An internals crate." name = "internals crate" diff --git a/code/game/objects/structures/divine.dm b/code/game/objects/structures/divine.dm index 5fd480f6fd..b8137d831b 100644 --- a/code/game/objects/structures/divine.dm +++ b/code/game/objects/structures/divine.dm @@ -40,11 +40,11 @@ last_process = world.time to_chat(user, "The water feels warm and soothing as you touch it. The fountain immediately dries up shortly afterwards.") user.reagents.add_reagent("godblood",20) - update_icons() - addtimer(CALLBACK(src, .proc/update_icons), time_between_uses) + update_icon() + addtimer(CALLBACK(src, .proc/update_icon), time_between_uses) -/obj/structure/healingfountain/proc/update_icons() +/obj/structure/healingfountain/update_icon() if(last_process + time_between_uses > world.time) icon_state = "fountain" else diff --git a/code/game/objects/structures/extinguisher.dm b/code/game/objects/structures/extinguisher.dm index 23643ae9cd..d6beaa2628 100644 --- a/code/game/objects/structures/extinguisher.dm +++ b/code/game/objects/structures/extinguisher.dm @@ -102,9 +102,11 @@ return attack_hand(user) /obj/structure/extinguisher_cabinet/AltClick(mob/living/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user))) return toggle_cabinet(user) + return TRUE /obj/structure/extinguisher_cabinet/proc/toggle_cabinet(mob/user) if(opened && broken) diff --git a/code/game/objects/structures/mirror.dm b/code/game/objects/structures/mirror.dm index a7f751e63c..ba45ae5ef0 100644 --- a/code/game/objects/structures/mirror.dm +++ b/code/game/objects/structures/mirror.dm @@ -20,10 +20,9 @@ return if(broken || !Adjacent(user)) return - + if(ishuman(user)) var/mob/living/carbon/human/H = user - //see code/modules/mob/dead/new_player/preferences.dm at approx line 545 for comments! //this is largely copypasted from there. diff --git a/code/game/objects/structures/morgue.dm b/code/game/objects/structures/morgue.dm index 840597ba4c..cdd4cb5a31 100644 --- a/code/game/objects/structures/morgue.dm +++ b/code/game/objects/structures/morgue.dm @@ -167,11 +167,12 @@ GLOBAL_LIST_EMPTY(bodycontainers) //Let them act as spawnpoints for revenants an . += "The speaker is [beeper ? "enabled" : "disabled"]. Alt-click to toggle it." /obj/structure/bodycontainer/morgue/AltClick(mob/user) - ..() + . = ..() if(!user.canUseTopic(src, !issilicon(user))) return beeper = !beeper to_chat(user, "You turn the speaker function [beeper ? "on" : "off"].") + return TRUE /obj/structure/bodycontainer/morgue/update_icon() if (!connected || connected.loc != src) // Open or tray is gone. diff --git a/code/game/objects/structures/reflector.dm b/code/game/objects/structures/reflector.dm index 77aad5a9dd..cde60e15c1 100644 --- a/code/game/objects/structures/reflector.dm +++ b/code/game/objects/structures/reflector.dm @@ -167,10 +167,12 @@ return TRUE /obj/structure/reflector/AltClick(mob/user) + . = ..() if(!user.canUseTopic(src, BE_CLOSE, ismonkey(user))) return else if(finished) rotate(user) + return TRUE //TYPES OF REFLECTORS, SINGLE, DOUBLE, BOX diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm index 9dee1e26cc..58588c5693 100644 --- a/code/game/objects/structures/window.dm +++ b/code/game/objects/structures/window.dm @@ -285,6 +285,13 @@ . += new /obj/item/shard(location) /obj/structure/window/proc/can_be_rotated(mob/user,rotation_type) + if (get_dist(src,user) > 1) + if (iscarbon(user)) + var/mob/living/carbon/H = user + if (!(H.dna && H.dna.check_mutation(TK) && tkMaxRangeCheck(src,H))) + return FALSE + else + return FALSE if(anchored) to_chat(user, "[src] cannot be rotated while it is fastened to the floor!") return FALSE diff --git a/code/modules/VR/vr_mob.dm b/code/modules/VR/vr_mob.dm index 5c0cea9f60..5f2caffc76 100644 --- a/code/modules/VR/vr_mob.dm +++ b/code/modules/VR/vr_mob.dm @@ -40,5 +40,5 @@ /datum/action/quit_vr/Trigger() //this merely a trigger for /datum/component/virtual_reality . = ..() - if(!.) + if(.) //The component was not there to block the trigger. Remove(owner) diff --git a/code/modules/VR/vr_sleeper.dm b/code/modules/VR/vr_sleeper.dm index 72cbdc1409..7cda24d98b 100644 --- a/code/modules/VR/vr_sleeper.dm +++ b/code/modules/VR/vr_sleeper.dm @@ -52,18 +52,16 @@ flags_1 = NODECONSTRUCT_1 only_current_user_can_interact = TRUE -/obj/machinery/vr_sleeper/hugbox/emag_act(mob/user) - return SEND_SIGNAL(src, COMSIG_ATOM_EMAG_ACT) - /obj/machinery/vr_sleeper/emag_act(mob/user) . = ..() if(!(obj_flags & EMAGGED)) return - obj_flags |= EMAGGED - you_die_in_the_game_you_die_for_real = TRUE - sparks.start() - addtimer(CALLBACK(src, .proc/emagNotify), 150) - return TRUE + if(!only_current_user_can_interact) + obj_flags |= EMAGGED + you_die_in_the_game_you_die_for_real = TRUE + sparks.start() + addtimer(CALLBACK(src, .proc/emagNotify), 150) + return TRUE /obj/machinery/vr_sleeper/update_icon() icon_state = "[initial(icon_state)][state_open ? "-open" : ""]" @@ -76,7 +74,7 @@ return ..() /obj/machinery/vr_sleeper/MouseDrop_T(mob/target, mob/user) - if(user.stat || user.lying || !Adjacent(user) || !user.Adjacent(target) || !iscarbon(target) || !user.IsAdvancedToolUser()) + if(user.lying || !iscarbon(target) || !Adjacent(target) || !user.canUseTopic(src, BE_CLOSE, TRUE, NO_TK)) return close_machine(target) @@ -91,26 +89,25 @@ return switch(action) if("vr_connect") - var/mob/living/carbon/human/human_occupant = occupant - if(human_occupant && human_occupant.mind && usr == occupant) - - to_chat(occupant, "Transferring to virtual reality...") - if(vr_mob && (!istype(vr_mob) || !vr_mob.InCritical()) && !vr_mob.GetComponent(/datum/component/virtual_reality)) - vr_mob.AddComponent(/datum/component/virtual_reality, human_occupant, src, you_die_in_the_game_you_die_for_real) - to_chat(vr_mob, "Transfer successful! You are now playing as [vr_mob] in VR!") - else + var/mob/M = occupant + if(M?.mind && M == usr) + to_chat(M, "Transferring to virtual reality...") + var/datum/component/virtual_reality/VR + if(vr_mob) + VR = vr_mob.GetComponent(/datum/component/virtual_reality) + if(!(VR?.connect(M))) if(allow_creating_vr_mobs) - to_chat(occupant, "Virtual avatar not found, attempting to create one...") + to_chat(occupant, "Virtual avatar [vr_mob ? "corrupted" : "missing"], attempting to create one...") var/obj/effect/landmark/vr_spawn/V = get_vr_spawnpoint() var/turf/T = get_turf(V) if(T) - SStgui.close_user_uis(occupant, src) new_player(occupant, T, V.vr_outfit) - to_chat(vr_mob, "Transfer successful! You are now playing as [vr_mob] in VR!") else to_chat(occupant, "Virtual world misconfigured, aborting transfer") else to_chat(occupant, "The virtual world does not support the creation of new virtual avatars, aborting transfer") + else + to_chat(vr_mob, "Transfer successful! You are now playing as [vr_mob] in VR!") . = TRUE if("delete_avatar") if(!occupant || usr == occupant) @@ -157,17 +154,31 @@ for(var/obj/effect/landmark/vr_spawn/V in GLOB.landmarks_list) GLOB.vr_spawnpoints[V.vr_category] = V -/obj/machinery/vr_sleeper/proc/new_player(mob/living/carbon/human/H, location, datum/outfit/outfit, transfer = TRUE) - if(!H) +/obj/machinery/vr_sleeper/proc/new_player(mob/M, location, datum/outfit/outfit, transfer = TRUE) + if(!M) return cleanup_vr_mob() vr_mob = new virtual_mob_type(location) - if(vr_mob.build_virtual_character(H, outfit)) - var/mob/living/carbon/human/vr_H = vr_mob - vr_H.updateappearance(TRUE, TRUE, TRUE) - if(!transfer || !H.mind) - return - vr_mob.AddComponent(/datum/component/virtual_reality, H, src, you_die_in_the_game_you_die_for_real) + if(vr_mob.build_virtual_character(M, outfit) && iscarbon(vr_mob)) + var/mob/living/carbon/C = vr_mob + C.updateappearance(TRUE, TRUE, TRUE) + var/datum/component/virtual_reality/VR = vr_mob.AddComponent(/datum/component/virtual_reality, you_die_in_the_game_you_die_for_real) + if(VR.connect(M)) + RegisterSignal(VR, COMSIG_COMPONENT_UNREGISTER_PARENT, .proc/unset_vr_mob) + RegisterSignal(VR, COMSIG_COMPONENT_REGISTER_PARENT, .proc/set_vr_mob) + if(!only_current_user_can_interact) + VR.RegisterSignal(src, COMSIG_ATOM_EMAG_ACT, /datum/component/virtual_reality.proc/you_only_live_once) + VR.RegisterSignal(src, COMSIG_MACHINE_EJECT_OCCUPANT, /datum/component/virtual_reality.proc/revert_to_reality) + VR.RegisterSignal(src, COMSIG_PARENT_QDELETING, /datum/component/virtual_reality.proc/machine_destroyed) + to_chat(vr_mob, "Transfer successful! You are now playing as [vr_mob] in VR!") + else + to_chat(M, "Transfer failed! virtual reality data likely corrupted!") + +/obj/machinery/vr_sleeper/proc/unset_vr_mob(datum/component/virtual_reality/VR) + vr_mob = null + +/obj/machinery/vr_sleeper/proc/set_vr_mob(datum/component/virtual_reality/VR) + vr_mob = VR.parent /obj/machinery/vr_sleeper/proc/cleanup_vr_mob() if(vr_mob) @@ -222,6 +233,7 @@ qdel(C) for (var/A in corpse_party) var/mob/M = A - if(get_area(M) == vr_area && M.stat == DEAD) + if(M && M.stat == DEAD && get_area(M) == vr_area) qdel(M) + corpse_party -= M addtimer(CALLBACK(src, .proc/clean_up), 3 MINUTES) diff --git a/code/modules/admin/sound_emitter.dm b/code/modules/admin/sound_emitter.dm index 2901659ce9..702e2071bd 100644 --- a/code/modules/admin/sound_emitter.dm +++ b/code/modules/admin/sound_emitter.dm @@ -52,9 +52,11 @@ edit_emitter(user) /obj/effect/sound_emitter/AltClick(mob/user) + . = ..() if(check_rights_for(user.client, R_SOUNDS)) activate(user) to_chat(user, "Sound emitter activated.") + return TRUE /obj/effect/sound_emitter/proc/edit_emitter(mob/user) var/dat = "" diff --git a/code/modules/antagonists/bloodsucker/bloodsucker_flaws.dm b/code/modules/antagonists/bloodsucker/bloodsucker_flaws.dm new file mode 100644 index 0000000000..77169efd61 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/bloodsucker_flaws.dm @@ -0,0 +1,84 @@ + +// Getting Flaws: +// +// Killing crew +// +// Gaining ranks + + + +// * COMPULSION * Things you must do +// +// SELECTIVE: -Gender/BloodType/Job sustains you, but others give you less. +// + + + + +// * WEAKNESSES * Things that may harm you +// +// LIGHTS: -Bright light nullifies the Examine benefits of Masquerade. +// -Bright lights disable your healing (including in Torpor) +// +// STAKES: -Stakes kill you immediately. +// +// PAINFUL: -Your feed victims scream, despite being unconscious. +// +// FIRE: -You only need your max health (not x2) in fire damage to die. +// +// CORPSE: -Your Masquerade turns off when unconscious or crit. +// +// FERAL: - +// +// CRAVEN + + + + + + + + + + + + + + // BANES // + +// These are basically small weaknesses that affect your character in certain circumstances. +// As a rule, they should be specific as to when they happen, or have only some certain +// drawback. + +// (core ideas) +// SENSITIVE: You are slightly blinded by bright lights. +// DARKFRIEND: Your automatic healing is at a crawl when in bright light. +// TRADITIONAL: Every five minutes spent outside a coffin lowers your rate of automatic healing. +// CONSUMED: Every five minutes spent outside a coffin increases the rate at which your blood ticks down. +// GOURMAND: Animals and blood bags offer you no nourishment when feeding. +// DEATHMASK: You no longer fake having a heartbeat, and always show up as pale when examined. +// BESTIAL: When your blood is low, you will twitch involuntarily. + +// (alternate ideas) +// STERILE: There is a high chance that turning corpses to Bloodsuckers will fail, and further attempts on them by you are impossible. +// FERAL: You're a threat to Vampire-kind: New Bloodsuckers may have an Objective to destroy you. +// UNHOLY: The Chapel, the Bible, and Holy Water set you on fire. +// PARANOID: Only your own claimed coffin counts for healing and banes. + + +// ON LEVEL-UP: +// Burn Damage increases +// Regen Rate increases +// Max Punch Damage increase +// Reset Level Timer +// Select Bane + + +// How to Burn Vamps: +// C.adjustFireLoss(20) +// C.adjust_fire_stacks(6) +// C.IgniteMob() + + +/datum/antagonist/bloodsucker/proc/AssignRandomBane() + return \ No newline at end of file diff --git a/code/modules/antagonists/bloodsucker/bloodsucker_integration.dm b/code/modules/antagonists/bloodsucker/bloodsucker_integration.dm new file mode 100644 index 0000000000..bf4ae4b6b5 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/bloodsucker_integration.dm @@ -0,0 +1,148 @@ +// INTEGRATION: Adding Procs and Datums to existing "classes" +/mob/living/proc/AmBloodsucker(falseIfInDisguise=FALSE) + // No Datum + if(!mind || !mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) + return FALSE + return TRUE + + +/mob/living/proc/HaveBloodsuckerBodyparts(var/displaymessage="") // displaymessage can be something such as "rising from death" for Torpid Sleep. givewarningto is the person receiving messages. + if(!getorganslot(ORGAN_SLOT_HEART)) + if(displaymessage != "") + to_chat(src, "Without a heart, you are incapable of [displaymessage].") + return FALSE + if(!get_bodypart(BODY_ZONE_HEAD)) + if(displaymessage != "") + to_chat(src, "Without a head, you are incapable of [displaymessage].") + return FALSE + if(!getorgan(/obj/item/organ/brain)) // NOTE: This is mostly just here so we can do one scan for all needed parts when creating a vamp. You probably won't be trying to use powers w/out a brain. + if(displaymessage != "") + to_chat(src, "Without a brain, you are incapable of [displaymessage].") + return FALSE + return TRUE + + + +// GET DAMAGE + + +// Do NOT count the damage on prosthetics for this. +/mob/living/proc/getBruteLoss_nonProsthetic() + return getBruteLoss() + +/mob/living/proc/getFireLoss_nonProsthetic() + return getFireLoss() + +/mob/living/carbon/getBruteLoss_nonProsthetic() + var/amount = 0 + for(var/obj/item/bodypart/BP in bodyparts) + if(BP.status < 2) + amount += BP.brute_dam + return amount + +/mob/living/carbon/getFireLoss_nonProsthetic() + var/amount = 0 + for(var/obj/item/bodypart/BP in bodyparts) + if(BP.status < 2) + amount += BP.burn_dam + return amount + +/mob/living/carbon +// EXAMINING +/mob/living/carbon/human/proc/ReturnVampExamine(var/mob/viewer) + if(!mind || !viewer.mind) + return "" + // Target must be a Vamp + var/datum/antagonist/bloodsucker/bloodsuckerdatum = mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(!bloodsuckerdatum) + return "" + // Viewer is Target's Vassal? + if(viewer.mind.has_antag_datum(ANTAG_DATUM_VASSAL) in bloodsuckerdatum.vassals) + var/returnString = "\[This is your Master!\]" + var/returnIcon = "[icon2html('icons/misc/language.dmi', world, "bloodsucker")]" + returnString += "\n" + return returnIcon + returnString + // Viewer not a Vamp AND not the target's vassal? + if(!viewer.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) && !(viewer in bloodsuckerdatum.vassals)) + return "" + // Default String + var/returnString = "\[[bloodsuckerdatum.ReturnFullName(1)]\]" + var/returnIcon = "[icon2html('icons/misc/language.dmi', world, "bloodsucker")]" + + // In Disguise (Veil)? + //if (name_override != null) + // returnString += " ([real_name] in disguise!) " + + //returnString += "\n" Don't need spacers. Using . += "" in examine.dm does this on its own. + return returnIcon + returnString + + +/mob/living/carbon/human/proc/ReturnVassalExamine(var/mob/viewer) + if(!mind || !viewer.mind) + return "" + // Am I not even a Vassal? Then I am not marked. + var/datum/antagonist/vassal/vassaldatum = mind.has_antag_datum(ANTAG_DATUM_VASSAL) + if(!vassaldatum) + return "" + // Only Vassals and Bloodsuckers can recognize marks. + if(!viewer.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) && !viewer.mind.has_antag_datum(ANTAG_DATUM_VASSAL)) + return "" + + // Default String + var/returnString = "\[" + var/returnIcon = "" + // Am I Viewer's Vassal? + if(vassaldatum.master.owner == viewer.mind) + returnString += "This [dna.species.name] bears YOUR mark!" + returnIcon = "[icon2html('icons/misc/mark_icons.dmi', world, "vassal")]" + // Am I someone ELSE'S Vassal? + else if(viewer.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) + returnString += "This [dna.species.name] bears the mark of [vassaldatum.master.ReturnFullName(vassaldatum.master.owner.current,1)]" + returnIcon = "[icon2html('icons/misc/mark_icons.dmi', world, "vassal_grey")]" + // Are you serving the same master as I am? + else if(viewer.mind.has_antag_datum(ANTAG_DATUM_VASSAL) in vassaldatum.master.vassals) + returnString += "[p_they(TRUE)] bears the mark of your Master" + returnIcon = "[icon2html('icons/misc/mark_icons.dmi', world, "vassal")]" + // You serve a different Master than I do. + else + returnString += "[p_they(TRUE)] bears the mark of another Bloodsucker" + returnIcon = "[icon2html('icons/misc/mark_icons.dmi', world, "vassal_grey")]" + + returnString += "\]" // \n" Don't need spacers. Using . += "" in examine.dm does this on its own. + return returnIcon + returnString + + +// Am I "pale" when examined? Bloodsuckers can trick this. +/mob/living/carbon/proc/ShowAsPaleExamine() + + // Normal Creatures: + if(!mind || !mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) + return blood_volume < (BLOOD_VOLUME_SAFE * blood_ratio) + + var/datum/antagonist/bloodsucker/bloodsuckerdatum = mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(bloodsuckerdatum.poweron_masquerade) + return FALSE + + // If a Bloodsucker is malnourished, AND if his temperature matches his surroundings (aka he hasn't fed recently and looks COLD)... + return blood_volume < (BLOOD_VOLUME_OKAY * blood_ratio) // && !(bodytemperature <= get_temperature() + 2) + +/mob/living/carbon/human/ShowAsPaleExamine() + // Check for albino, as per human/examine.dm's check. + if(skin_tone == "albino") + return TRUE + + return ..() // Return vamp check + +/mob/living/carbon/proc/scan_blood_volume() + // Vamps don't show up normally to scanners unless Masquerade power is on ----> scanner.dm + if(mind) + var/datum/antagonist/bloodsucker/bloodsuckerdatum = mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if (istype(bloodsuckerdatum) && bloodsuckerdatum.poweron_masquerade) + return BLOOD_VOLUME_NORMAL + return blood_volume + +/mob/living/proc/IsFrenzied() + return FALSE + +/mob/living/proc/StartFrenzy(inTime = 120) + set waitfor = FALSE diff --git a/code/modules/antagonists/bloodsucker/bloodsucker_life.dm b/code/modules/antagonists/bloodsucker/bloodsucker_life.dm new file mode 100644 index 0000000000..9174692b49 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/bloodsucker_life.dm @@ -0,0 +1,361 @@ + + +// TO PLUG INTO LIFE: + +// Cancel BLOOD life +// Cancel METABOLISM life (or find a way to control what gets digested) +// Create COLDBLOODED trait (thermal homeostasis) + +// EXAMINE +// +// Show as dead when... + +/datum/antagonist/bloodsucker/proc/LifeTick()// Should probably run from life.dm, same as handle_changeling, but will be an utter pain to move + set waitfor = FALSE // Don't make on_gain() wait for this function to finish. This lets this code run on the side. + var/notice_healing = FALSE + while(owner && !AmFinalDeath()) // owner.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) == src + if(owner.current.stat == CONSCIOUS && !poweron_feed && !HAS_TRAIT(owner.current, TRAIT_DEATHCOMA)) // Deduct Blood + AddBloodVolume(-0.1) // -.15 (before tick went from 10 to 30, but we also charge more for faking life now) + if(HandleHealing(1)) // Heal + if(notice_healing == FALSE && owner.current.blood_volume > 0) + to_chat(owner, "The power of your blood begins knitting your wounds...") + notice_healing = TRUE + else if(notice_healing == TRUE) + notice_healing = FALSE // Apply Low Blood Effects + HandleStarving() // Death + HandleDeath() // Standard Update + update_hud()// Daytime Sleep in Coffin + if (SSticker.mode.is_daylight() && !HAS_TRAIT_FROM(owner.current, TRAIT_DEATHCOMA, "bloodsucker")) + if(istype(owner.current.loc, /obj/structure/closet/crate/coffin)) + Torpor_Begin() + // Wait before next pass + sleep(10) + FreeAllVassals() // Free my Vassals! (if I haven't yet) + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// BLOOD + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/datum/antagonist/bloodsucker/proc/AddBloodVolume(value) + owner.current.blood_volume = CLAMP(owner.current.blood_volume + value, 0, maxBloodVolume) + update_hud() + +/datum/antagonist/bloodsucker/proc/HandleFeeding(mob/living/carbon/target, mult=1) + // mult: SILENT feed is 1/3 the amount + var/blood_taken = min(feedAmount, target.blood_volume) * mult // Starts at 15 (now 8 since we doubled the Feed time) + target.blood_volume -= blood_taken + // Simple Animals lose a LOT of blood, and take damage. This is to keep cats, cows, and so forth from giving you insane amounts of blood. + if(!ishuman(target)) + target.blood_volume -= (blood_taken / max(target.mob_size, 0.1)) * 3.5 // max() to prevent divide-by-zero + target.apply_damage_type(blood_taken / 3.5) // Don't do too much damage, or else they die and provide no blood nourishment. + if(target.blood_volume <= 0) + target.blood_volume = 0 + target.death(0) + /////////// + // Shift Body Temp (toward Target's temp, by volume taken) + owner.current.bodytemperature = ((owner.current.blood_volume * owner.current.bodytemperature) + (blood_taken * target.bodytemperature)) / (owner.current.blood_volume + blood_taken) + // our volume * temp, + their volume * temp, / total volume + /////////// + // Reduce Value Quantity + if(target.stat == DEAD) // Penalty for Dead Blood + blood_taken /= 3 + if(!ishuman(target)) // Penalty for Non-Human Blood + blood_taken /= 2 + //if (!iscarbon(target)) // Penalty for Animals (they're junk food) + // Apply to Volume + AddBloodVolume(blood_taken) + // Reagents (NOT Blood!) + if(target.reagents && target.reagents.total_volume) + target.reagents.reaction(owner.current, INGEST, 1) // Run Reaction: what happens when what they have mixes with what I have? + target.reagents.trans_to(owner.current, 1) // Run transfer of 1 unit of reagent from them to me. + // Blood Gulp Sound + owner.current.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, 1) // Play THIS sound for user only. The "null" is where turf would go if a location was needed. Null puts it right in their head. + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// HEALING + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/datum/antagonist/bloodsucker/proc/HandleHealing(mult = 1) + // NOTE: Mult of 0 is just a TEST to see if we are injured and need to go into Torpor! + //It is called from your coffin on close (by you only) + if(poweron_masquerade == TRUE || owner.current.AmStaked()) + return FALSE + owner.current.adjustStaminaLoss(-2 + (regenRate * -10) * mult, 0) // Humans lose stamina damage really quickly. Vamps should heal more. + owner.current.adjustCloneLoss(-1 * (regenRate * 4) * mult, 0) + owner.current.adjustOrganLoss(ORGAN_SLOT_BRAIN, -1 * (regenRate * 4) * mult) //adjustBrainLoss(-1 * (regenRate * 4) * mult, 0) + // No Bleeding + if(ishuman(owner.current)) //NOTE Current bleeding is horrible, not to count the amount of blood ballistics delete. + var/mob/living/carbon/human/H = owner.current + H.bleed_rate = 0 + if(iscarbon(owner.current)) // Damage Heal: Do I have damage to ANY bodypart? + var/mob/living/carbon/C = owner.current + var/costMult = 1 // Coffin makes it cheaper + var/fireheal = 0 // BURN: Heal in Coffin while Fakedeath, or when damage above maxhealth (you can never fully heal fire) + var/amInCoffinWhileTorpor = istype(C.loc, /obj/structure/closet/crate/coffin) && (mult == 0 || HAS_TRAIT(C, TRAIT_DEATHCOMA)) // Check for mult 0 OR death coma. (mult 0 means we're testing from coffin) + if(amInCoffinWhileTorpor) + mult *= 5 // Increase multiplier if we're sleeping in a coffin. + fireheal = min(C.getFireLoss_nonProsthetic(), regenRate) // NOTE: Burn damage ONLY heals in torpor. + costMult = 0.25 + C.ExtinguishMob() + CureDisabilities() // Extinguish Fire + C.remove_all_embedded_objects() // Remove Embedded! + owner.current.regenerate_organs() // Heal Organs (will respawn original eyes etc. but we replace right away, next) + CheckVampOrgans() // Heart, Eyes + else + if(owner.current.blood_volume <= 0) // No Blood? Lower Mult + mult = 0.25 + // Crit from burn? Lower damage to maximum allowed. + //if (C.getFireLoss() > owner.current.getMaxHealth()) + // fireheal = regenRate / 2 + // BRUTE: Always Heal + var/bruteheal = min(C.getBruteLoss_nonProsthetic(), regenRate) + var/toxinheal = min(C.getToxLoss(), regenRate) + // Heal if Damaged + if(bruteheal + fireheal + toxinheal > 0) // Just a check? Don't heal/spend, and return. + if(mult == 0) + return TRUE + // We have damage. Let's heal (one time) + C.adjustBruteLoss(-bruteheal * mult, forced = TRUE)// Heal BRUTE / BURN in random portions throughout the body. + C.adjustFireLoss(-fireheal * mult, forced = TRUE) + C.adjustToxLoss(-toxinheal * mult * 2, forced = TRUE) //Toxin healing because vamps arent immune + //C.heal_overall_damage(bruteheal * mult, fireheal * mult) // REMOVED: We need to FORCE this, because otherwise, vamps won't heal EVER. Swapped to above. + AddBloodVolume((bruteheal * -0.5 + fireheal * -1) / mult * costMult) // Costs blood to heal + return TRUE // Healed! Done for this tick. + if(amInCoffinWhileTorpor) // Limbs? (And I have no other healing) + var/list/missing = owner.current.get_missing_limbs() // Heal Missing + if (missing.len) // Cycle through ALL limbs and regen them! + for (var/targetLimbZone in missing) // 1) Find ONE Limb and regenerate it. + owner.current.regenerate_limb(targetLimbZone, 0) // regenerate_limbs() <--- If you want to EXCLUDE certain parts, do it like this ----> regenerate_limbs(0, list("head")) + var/obj/item/bodypart/L = owner.current.get_bodypart( targetLimbZone ) // 2) Limb returns Damaged + AddBloodVolume(50 * costMult) // Costs blood to heal + L.brute_dam = 60 + to_chat(owner.current, "Your flesh knits as it regrows [L]!") + playsound(owner.current, 'sound/magic/demon_consume.ogg', 50, 1) + // DONE! After regenerating ANY number of limbs, we stop here. + return TRUE + /*else // REMOVED: For now, let's just leave prosthetics on. Maybe you WANT to be a robovamp. + // Remove Prosthetic/False Limb + for(var/obj/item/bodypart/BP in C.bodyparts) + message_admins("T1: [BP] ") + if (istype(BP) && BP.status == 2) + message_admins("T2: [BP] ") + BP.drop_limb() + return TRUE */ + // NOTE: Limbs have a "status", like their hosts "stat". 2 is dead (aka Prosthetic). 1 seems to be idle/alive.*/ + return FALSE + +/datum/antagonist/bloodsucker/proc/CureDisabilities() + var/mob/living/carbon/C = owner.current + C.cure_blind(list(EYE_DAMAGE))//() + C.cure_nearsighted(EYE_DAMAGE) + C.set_blindness(0) // Added 9/2/19 + C.set_blurriness(0) // Added 9/2/19 + C.update_tint() // Added 9/2/19 + C.update_sight() // Added 9/2/19 + for(var/O in C.internal_organs) //owner.current.adjust_eye_damage(-100) // This was removed by TG + var/obj/item/organ/organ = O + organ.setOrganDamage(0) + owner.current.cure_husk() + +// I am thirsty for blud! +/datum/antagonist/bloodsucker/proc/HandleStarving() + + // High: Faster Healing + // Med: Pale + // Low: Twitch + // V.Low: Blur Vision + // EMPTY: Frenzy! + // BLOOD_VOLUME_GOOD: [336] Pale (handled in bloodsucker_integration.dm + // BLOOD_VOLUME_BAD: [224] Jitter + if(owner.current.blood_volume < BLOOD_VOLUME_BAD && !prob(0.5)) + owner.current.Jitter(3) + // BLOOD_VOLUME_SURVIVE: [122] Blur Vision + if(owner.current.blood_volume < BLOOD_VOLUME_BAD / 2) + owner.current.blur_eyes(8 - 8 * (owner.current.blood_volume / BLOOD_VOLUME_BAD)) + // Nutrition + owner.current.nutrition = min(owner.current.blood_volume, NUTRITION_LEVEL_FED) // <-- 350 //NUTRITION_LEVEL_FULL +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// DEATH + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/datum/antagonist/bloodsucker/proc/HandleDeath() + // FINAL DEATH + // Fire Damage? (above double health) + if (owner.current.getFireLoss_nonProsthetic() >= owner.current.getMaxHealth() * 2) + FinalDeath() + return + // Staked while "Temp Death" or Asleep + if (owner.current.StakeCanKillMe() && owner.current.AmStaked()) + FinalDeath() + return + // Not "Alive"? + if (!owner.current || !isliving(owner.current) || isbrain(owner.current) || !get_turf(owner.current)) + FinalDeath() + return + // Missing Brain or Heart? + if (!owner.current.HaveBloodsuckerBodyparts()) + FinalDeath() + return + // Disable Powers: Masquerade * NOTE * This should happen as a FLAW! + //if (stat >= UNCONSCIOUS) + // for (var/datum/action/bloodsucker/masquerade/P in powers) + // P.Deactivate() + // TEMP DEATH + var/total_brute = owner.current.getBruteLoss_nonProsthetic() + var/total_burn = owner.current.getFireLoss_nonProsthetic() + var/total_toxloss = owner.current.getToxLoss() //This is neater than just putting it in total_damage + var/total_damage = total_brute + total_burn + total_toxloss + // Died? Convert to Torpor (fake death) + if (owner.current.stat >= DEAD) + Torpor_Begin() + to_chat(owner, "Your immortal body will not yet relinquish your soul to the abyss. You enter Torpor.") + if (poweron_masquerade == TRUE) + to_chat(owner, "Your wounds will not heal until you disable the Masquerade power.") + // End Torpor: + else // No damage, OR toxin healed AND brute healed and NOT in coffin (since you cannot heal burn) + if (total_damage <= 0 || total_toxloss <= 0 && total_brute <= 0 && !istype(owner.current.loc, /obj/structure/closet/crate/coffin)) + // Not Daytime, Not in Torpor + if (!SSticker.mode.is_daylight() && HAS_TRAIT_FROM(owner.current, TRAIT_DEATHCOMA, "bloodsucker")) + Torpor_End() + // Fake Unconscious + if (poweron_masquerade == TRUE && total_damage >= owner.current.getMaxHealth() - HEALTH_THRESHOLD_FULLCRIT) + owner.current.Unconscious(20,1) + + //HEALTH_THRESHOLD_CRIT 0 + //HEALTH_THRESHOLD_FULLCRIT -30 + //HEALTH_THRESHOLD_DEAD -100 + +/datum/antagonist/bloodsucker/proc/Torpor_Begin(amInCoffin=FALSE) + owner.current.stat = UNCONSCIOUS + owner.current.fakedeath("bloodsucker") // Come after UNCONSCIOUS or else it fails + ADD_TRAIT(owner.current, TRAIT_NODEATH, "bloodsucker") // Without this, you'll just keep dying while you recover. + ADD_TRAIT(owner.current, TRAIT_RESISTHIGHPRESSURE, "bloodsucker") // So you can heal in 0 G. otherwise you just...heal forever. + ADD_TRAIT(owner.current, TRAIT_RESISTLOWPRESSURE, "bloodsucker") // So you can heal in 0 G. otherwise you just...heal forever. + // Visuals + owner.current.update_sight() + owner.current.reload_fullscreen() + // Disable ALL Powers + for (var/datum/action/bloodsucker/power in powers) + if (power.active && !power.can_use_in_torpor) + power.DeactivatePower() + + +/datum/antagonist/bloodsucker/proc/Torpor_End() + owner.current.stat = SOFT_CRIT + owner.current.cure_fakedeath("bloodsucker") // Come after SOFT_CRIT or else it fails + REMOVE_TRAIT(owner.current, TRAIT_NODEATH, "bloodsucker") + REMOVE_TRAIT(owner.current, TRAIT_RESISTHIGHPRESSURE, "bloodsucker") + REMOVE_TRAIT(owner.current, TRAIT_RESISTLOWPRESSURE, "bloodsucker") + to_chat(owner, "You have recovered from Torpor.") + + +/datum/antagonist/proc/AmFinalDeath() + // Standard Antags can be dead OR final death + return owner && (owner.current && owner.current.stat >= DEAD || owner.AmFinalDeath()) + +/datum/antagonist/bloodsucker/AmFinalDeath() + return owner && owner.AmFinalDeath() +/datum/antagonist/changeling/AmFinalDeath() + return owner && owner.AmFinalDeath() + +/datum/mind/proc/AmFinalDeath() + return !current || QDELETED(current) || !isliving(current) || isbrain(current) || !get_turf(current) // NOTE: "isliving()" is not the same as STAT == CONSCIOUS. This is to make sure you're not a BORG (aka silicon) + +/datum/antagonist/bloodsucker/proc/FinalDeath() + if(!iscarbon(owner.current)) //Check for non carbons. + owner.current.gib() + return + playsound(get_turf(owner.current), 'sound/effects/tendril_destroyed.ogg', 60, 1) + owner.current.drop_all_held_items() + owner.current.unequip_everything() + var/mob/living/carbon/C = owner.current + C.remove_all_embedded_objects() + // Make me UN-CLONEABLE + owner.current.hellbound = TRUE // This was done during creation, but let's do it again one more time...to make SURE this guy stays dead, but they dont stay dead because brains can be cloned! + // Free my Vassals! + FreeAllVassals() + // Elders get Dusted + if (vamplevel >= 4) // (vamptitle) + owner.current.visible_message("[owner.current]'s skin crackles and dries, their skin and bones withering to dust. A hollow cry whips from what is now a sandy pile of remains.", \ + "Your soul escapes your withering body as the abyss welcomes you to your Final Death.", \ + "You hear a dry, crackling sound.") + owner.current.dust() + // Fledglings get Gibbed + else + owner.current.visible_message("[owner.current]'s skin bursts forth in a spray of gore and detritus. A horrible cry echoes from what is now a wet pile of decaying meat.", \ + "Your soul escapes your withering body as the abyss welcomes you to your Final Death.", \ + "You hear a wet, bursting sound.") + owner.current.gib(TRUE, FALSE, FALSE)//Brain cloning is wierd and allows hellbounds. Lets destroy the brain for safety. + playsound(owner.current.loc, 'sound/effects/tendril_destroyed.ogg', 40, 1) + + + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// HUMAN FOOD + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/mob/proc/CheckBloodsuckerEatFood(var/food_nutrition) + if (!isliving(src)) + return + var/mob/living/L = src + if (!L.AmBloodsucker()) + return + // We're a vamp? Try to eat food... + var/datum/antagonist/bloodsucker/bloodsuckerdatum = mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + bloodsuckerdatum.handle_eat_human_food(food_nutrition) + + +/datum/antagonist/bloodsucker/proc/handle_eat_human_food(var/food_nutrition) // Called from snacks.dm and drinks.dm + set waitfor = FALSE + if (!owner.current || !iscarbon(owner.current)) + return + var/mob/living/carbon/C = owner.current + // Remove Nutrition, Give Bad Food + C.nutrition -= food_nutrition + foodInGut += food_nutrition + // Already ate some bad clams? Then we can back out, because we're already sick from it. + if (foodInGut != food_nutrition) + return + // Haven't eaten, but I'm in a Human Disguise. + else if (poweron_masquerade) + to_chat(C, "Your stomach turns, but your \"human disguise\" keeps the food down...for now.") + // Keep looping until we purge. If we have activated our Human Disguise, we ignore the food. But it'll come up eventually... + var/sickphase = 0 + while (foodInGut) + sleep(50) + C.adjust_disgust(10 * sickphase) + // Wait an interval... + sleep(50 + 50 * sickphase) // At intervals of 100, 150, and 200. (10 seconds, 15 seconds, and 20 seconds) + // Died? Cancel + if(C.stat == DEAD) + return + // Put up disguise? Then hold off the vomit. + if(poweron_masquerade) + if(sickphase > 0) + to_chat(C, "Your stomach settles temporarily. You regain your composure...for now.") + sickphase = 0 + continue + switch(sickphase) + if (1) + to_chat(C, "You feel unwell. You can taste ash on your tongue.") + C.Stun(10) + if (2) + to_chat(C, "Your stomach turns. Whatever you ate tastes of grave dirt and brimstone.") + C.Dizzy(15) + C.Stun(13) + if (3) + to_chat(C, "You purge the food of the living from your viscera! You've never felt worse.") + C.vomit(foodInGut * 4, foodInGut * 2, 0) // (var/lost_nutrition = 10, var/blood = 0, var/stun = 1, var/distance = 0, var/message = 1, var/toxic = 0) + C.blood_volume = max(0, C.blood_volume - foodInGut * 2) + C.Stun(30) + //C.Dizzy(50) + foodInGut = 0 + sickphase ++ diff --git a/code/modules/antagonists/bloodsucker/bloodsucker_objectives.dm b/code/modules/antagonists/bloodsucker/bloodsucker_objectives.dm new file mode 100644 index 0000000000..63e1470576 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/bloodsucker_objectives.dm @@ -0,0 +1,351 @@ + + +// Hide a random object somewhere on the station: +// var/turf/targetturf = get_random_station_turf() +// var/turf/targetturf = get_safe_random_station_turf() + + + + +/datum/objective/bloodsucker + martyr_compatible = TRUE + +// GENERATE! +/datum/objective/bloodsucker/proc/generate_objective() + update_explanation_text() + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// // PROCS // // + + +/datum/objective/bloodsucker/proc/return_possible_targets() + var/list/possible_targets = list() + + // Look at all crew members, and for/loop through. + for(var/datum/mind/possible_target in get_crewmember_minds()) + // Check One: Default Valid User + if(possible_target != owner && ishuman(possible_target.current) && possible_target.current.stat != DEAD)// && is_unique_objective(possible_target)) + // Check Two: Am Bloodsucker? OR in Bloodsucker list? + if (possible_target.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) || (possible_target in SSticker.mode.bloodsuckers)) + continue + else + possible_targets += possible_target + + return possible_targets + + + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/datum/objective/bloodsucker/lair + +// EXPLANATION +/datum/objective/bloodsucker/lair/update_explanation_text() + explanation_text = "Create a lair by claiming a coffin, and protect it until the end of the shift"// Make sure to keep it safe!" + +// WIN CONDITIONS? +/datum/objective/bloodsucker/lair/check_completion() + var/datum/antagonist/bloodsucker/antagdatum = owner.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if (antagdatum && antagdatum.coffin && antagdatum.lair) + return TRUE + return FALSE + + // Space_Station_13_areas.dm <--- all the areas + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Vassal becomes a Head, or part of a department +/datum/objective/bloodsucker/protege + + // LOOKUP: /datum/crewmonitor/proc/update_data(z) for .assignment to see how to get a person's PDA. + var/list/roles = list( + "Captain", + "Head of Personnel", + "Research Director", + "Chief Engineer", + "Chief Medical Officer", + "Quartermaster" + ) + var/list/departs = list( + "Research Director", + "Chief Engineer", + "Chief Medical Officer", + "Quartermaster" + ) + + + var/target_role // Equals "HEAD" when it's not a department role. + var/department_string + +// GENERATE! +/datum/objective/bloodsucker/protege/generate_objective() + target_role = rand(0,2) == 0 ? "HEAD" : pick(departs) + + // Heads? + if (target_role == "HEAD") + target_amount = rand(1, round(SSticker.mode.num_players() / 20)) + target_amount = CLAMP(target_amount,1,3) + // Department? + else + switch(target_role) + if("Research Director") + department_string = "Science" + if("Chief Engineer") + department_string = "Engineering" + if("Chief Medical Officer") + department_string = "Medical" + if("Quartermaster") + department_string = "Cargo" + target_amount = rand(round(SSticker.mode.num_players() / 20), round(SSticker.mode.num_players() / 10)) + target_amount = CLAMP(target_amount, 2, 4) + ..() + +// EXPLANATION +/datum/objective/bloodsucker/protege/update_explanation_text() + if (target_role == "HEAD") + if (target_amount == 1) + explanation_text = "Guarantee a Vassal ends up as a Department Head or in a Leadership role." + else + explanation_text = "Guarantee [target_amount] Vassals end up as different Leadership or Department Heads." + else + explanation_text = "Have [target_amount] Vassal[target_amount==1?"":"s"] in the [department_string] department." + +// WIN CONDITIONS? +/datum/objective/bloodsucker/protege/check_completion() + + var/datum/antagonist/bloodsucker/antagdatum = owner.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if (!antagdatum || antagdatum.vassals.len == 0) + return FALSE + + // Get list of all jobs that are qualified (for HEAD, this is already done) + var/list/valid_jobs + if (target_role == "HEAD") + valid_jobs = roles + else + valid_jobs = list() + var/list/alljobs = subtypesof(/datum/job) // This is just a list of TYPES, not the actual variables! + for(var/T in alljobs) + var/datum/job/J = SSjob.GetJobType(T) // + if (!istype(J)) + continue + // Found a job whose Dept Head matches either list of heads, or this job IS the head + if ((target_role in J.department_head) || target_role == J.title) + valid_jobs += J.title + + + // Check Vassals, and see if they match + var/objcount = 0 + var/list/counted_roles = list() // So you can't have more than one Captain count. + for(var/datum/antagonist/vassal/V in antagdatum.vassals) + if (!V || !V.owner) // Must exist somewhere, and as a vassal. + continue + + var/thisRole = "none" + + // Mind Assigned + if ((V.owner.assigned_role in valid_jobs) && !(V.owner.assigned_role in counted_roles)) + //to_chat(owner, "PROTEGE OBJECTIVE: (MIND ROLE)") + thisRole = V.owner.assigned_role + // Mob Assigned + else if ((V.owner.current.job in valid_jobs) && !(V.owner.current.job in counted_roles)) + //to_chat(owner, "PROTEGE OBJECTIVE: (MOB JOB)") + thisRole = V.owner.current.job + // PDA Assigned + else if (V.owner.current && ishuman(V.owner.current)) + var/mob/living/carbon/human/H = V.owner.current + var/obj/item/card/id/I = H.wear_id ? H.wear_id.GetID() : null + if (I && (I.assignment in valid_jobs) && !(I.assignment in counted_roles)) + //to_chat(owner, "PROTEGE OBJECTIVE: (GET ID)") + thisRole = I.assignment + + // NO MATCH + if (thisRole == "none") + continue + + // SUCCESS! + objcount ++ + if (target_role == "HEAD") + counted_roles += thisRole // Add to list so we don't count it again (but only if it's a Head) + + // NOTE!!!!!!!!!!! + + // Look for jobs value on mobs! This is assigned at start, but COULD be assigned from HoP? + // + // ALSO - Search through all jobs (look for prefs earlier that look for all jobs, and search through all jobs to see if their head matches the head listed, or it IS the head) + // + // ALSO - registered_account in _vending.dm for banks, and assigning new ones. + + //to_chat(antagdatum.owner, "PROTEGE OBJECTIVE: Final Count: [objcount] of [antagdatum.vassals.len] vassals") + return objcount >= target_amount + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Eat blood from a lot of people +/datum/objective/bloodsucker/gourmand + +// HOW: Track each feed (if human). Count victory. + + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Convert a crewmate +/datum/objective/bloodsucker/embrace + +// HOW: Find crewmate. Check if person is a bloodsucker + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Defile a facility with blood +/datum/objective/bloodsucker/desecrate + + // Space_Station_13_areas.dm <--- all the areas + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Destroy the Solar Arrays +/datum/objective/bloodsucker/solars + +// Space_Station_13_areas.dm <--- all the areas +/datum/objective/bloodsucker/solars/update_explanation_text() + explanation_text = "Prevent all solar arrays on the station from functioning." + +/datum/objective/bloodsucker/solars/check_completion() + // Sort through all /obj/machinery/power/solar_control in the station ONLY, and check that they are functioning. + // Make sure that lastgen is 0 or connected_panels.len is 0. Doesnt matter if it's tracking. + for (var/obj/machinery/power/solar_control/SC in SSsun.solars) + // Check On Station: + var/turf/T = get_turf(SC) + if(!T || !is_station_level(T.z)) // <------ Taken from NukeOp + //message_admins("DEBUG A: [SC] not on station!") + continue // Not on station! We don't care about this. + if (SC && SC.lastgen > 0 && SC.connected_panels.len > 0 && SC.connected_tracker) + return FALSE + return TRUE + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Steal hearts. You just really wanna have some hearts. +/datum/objective/bloodsucker/heartthief + // NOTE: Look up /steal in objective.dm for inspiration. + +// GENERATE! +/datum/objective/bloodsucker/heartthief/generate_objective() + target_amount = rand(2,3) + + update_explanation_text() + //dangerrating += target_amount * 2 + +// EXPLANATION +/datum/objective/bloodsucker/heartthief/update_explanation_text() + explanation_text = "Steal and keep [target_amount] heart[target_amount == 1 ? "" : "s"]." // TO DO: Limit them to Human Only! + +// WIN CONDITIONS? +/datum/objective/bloodsucker/heartthief/check_completion() + // -Must have a body. + if (!owner.current) + return FALSE + // Taken from /steal in objective.dm + var/list/all_items = owner.current.GetAllContents() // Includes items inside other items. + var/itemcount = FALSE + for(var/obj/I in all_items) //Check for items + if(I == /obj/item/organ/heart) + itemcount ++ + if (itemcount >= target_amount) // Got the right amount? + return TRUE + + return FALSE + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/datum/objective/bloodsucker/survive + martyr_compatible = FALSE + + +// EXPLANATION +/datum/objective/bloodsucker/survive/update_explanation_text() + explanation_text = "Survive the entire shift without succumbing to Final Death." + +// WIN CONDITIONS? +/datum/objective/bloodsucker/survive/check_completion() + // -Must have a body. + if (!owner.current || !isliving(owner.current)) + return FALSE + // Dead, without a head or heart? Cya + return owner.current.stat != DEAD// || owner.current.HaveBloodsuckerBodyparts() + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/datum/objective/bloodsucker/vamphunter + +// GENERATE! +/datum/objective/bloodsucker/vamphunter/generate_objective() + update_explanation_text() + +// EXPLANATION +/datum/objective/bloodsucker/vamphunter/update_explanation_text() + explanation_text = "Destroy all Bloodsuckers on [station_name()]." + +// WIN CONDITIONS? +/datum/objective/bloodsucker/vamphunter/check_completion() + for (var/datum/mind/M in SSticker.mode.bloodsuckers) + if (M && M.current && M.current.stat != DEAD && get_turf(M.current)) + return FALSE + return TRUE + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/datum/objective/bloodsucker/monsterhunter + +// GENERATE! +/datum/objective/bloodsucker/monsterhunter/generate_objective() + update_explanation_text() + +// EXPLANATION +/datum/objective/bloodsucker/monsterhunter/update_explanation_text() + explanation_text = "Destroy all monsters on [station_name()]." + +// WIN CONDITIONS? +/datum/objective/bloodsucker/monsterhunter/check_completion() + var/list/datum/mind/monsters = list() + monsters += SSticker.mode.bloodsuckers + monsters += SSticker.mode.devils + monsters += SSticker.mode.cult + monsters += SSticker.mode.wizards + monsters += SSticker.mode.apprentices + //monsters += SSticker.mode.servants_of_ratvar + //monsters += SSticker.mode.changelings disabled anyways + + for (var/datum/mind/M in monsters) + if (M && M != owner && M.current && M.current.stat != DEAD && get_turf(M.current)) + return FALSE + return TRUE + + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/datum/objective/bloodsucker/vassal + +// GENERATE! +/datum/objective/bloodsucker/vassal/generate_objective() + update_explanation_text() + +// EXPLANATION +/datum/objective/bloodsucker/vassal/update_explanation_text() + explanation_text = "Guarantee the success of your Master's mission!" + +// WIN CONDITIONS? +/datum/objective/bloodsucker/vassal/check_completion() + var/datum/antagonist/vassal/antag_datum = owner.has_antag_datum(ANTAG_DATUM_VASSAL) + return antag_datum.master && antag_datum.master.owner && antag_datum.master.owner.current && antag_datum.master.owner.current.stat != DEAD diff --git a/code/modules/antagonists/bloodsucker/bloodsucker_powers.dm b/code/modules/antagonists/bloodsucker/bloodsucker_powers.dm new file mode 100644 index 0000000000..dffdeaf7fc --- /dev/null +++ b/code/modules/antagonists/bloodsucker/bloodsucker_powers.dm @@ -0,0 +1,284 @@ +/datum/action/bloodsucker + name = "Vampiric Gift" + desc = "A vampiric gift." + button_icon = 'icons/mob/actions/bloodsucker.dmi' //This is the file for the BACKGROUND icon + background_icon_state = "vamp_power_off" //And this is the state for the background icon + var/background_icon_state_on = "vamp_power_on" // FULP: Our "ON" icon alternative. + var/background_icon_state_off = "vamp_power_off" // FULP: Our "OFF" icon alternative. + icon_icon = 'icons/mob/actions/bloodsucker.dmi' //This is the file for the ACTION icon + button_icon_state = "power_feed" //And this is the state for the action icon + buttontooltipstyle = "cult" + + // Action-Related + //var/amPassive = FALSE // REMOVED: Just made it its own kind. // Am I just "on" at all times? (aka NO ICON) + var/amTargetted = FALSE // Am I asked to choose a target when enabled? (Shows as toggled ON when armed) + var/amToggle = FALSE // Can I be actively turned on and off? + var/amSingleUse = FALSE // Am I removed after a single use? + var/active = FALSE + var/cooldown = 20 // 10 ticks, 1 second. + var/cooldownUntil = 0 // From action.dm: next_use_time = world.time + cooldown_time + // Power-Related + var/level_current = 0 // Can increase to yield new abilities. Each power goes up in strength each Rank. + //var/level_max = 1 // + var/bloodcost = 10 + var/needs_button = TRUE // Taken from Changeling - for passive abilities that dont need a button + var/bloodsucker_can_buy = FALSE // Must be a bloodsucker to use this power. + var/warn_constant_cost = FALSE // Some powers charge you for staying on. Masquerade, Cloak, Veil, etc. + var/can_use_in_torpor = FALSE // Most powers don't function if you're in torpor. + var/must_be_capacitated = FALSE // Some powers require you to be standing and ready. + var/can_be_immobilized = FALSE // Brawn can be used when incapacitated/laying if it's because you're being immobilized. NOTE: If must_be_capacitated is FALSE, this is irrelevant. + var/can_be_staked = FALSE // Only Feed can happen with a stake in you. + var/cooldown_static = FALSE // Feed, Masquerade, and One-Shot powers don't improve their cooldown. + //var/not_bloodsucker = FALSE // This goes to Vassals or Hunters, but NOT bloodsuckers. + +/datum/action/bloodsucker/New() + if (bloodcost > 0) + desc += "

COST: [bloodcost] Blood" // Modify description to add cost. + if (warn_constant_cost) + desc += "

Your over-time blood consumption increases while [name] is active." + if (amSingleUse) + desc += "

Useable once per night." + ..() + +// NOTES +// +// click.dm <--- Where we can take over mouse clicks +// spells.dm /add_ranged_ability() <--- How we take over the mouse click to use a power on a target. + +/datum/action/bloodsucker/Trigger() + // Active? DEACTIVATE AND END! + if (active && CheckCanDeactivate(TRUE)) + DeactivatePower() + return + if (!CheckCanPayCost(TRUE) || !CheckCanUse(TRUE)) + return + PayCost() + if (amToggle) + active = !active + UpdateButtonIcon() + if (!amToggle || !active) + StartCooldown() // Must come AFTER UpdateButton(), otherwise icon will revert. + ActivatePower() // NOTE: ActivatePower() freezes this power in place until it ends. + if (active) // Did we not manually disable? Handle it here. + DeactivatePower() + if (amSingleUse) + RemoveAfterUse() + +/datum/action/bloodsucker/proc/CheckCanPayCost(display_error) + if(!owner || !owner.mind) + return FALSE + // Cooldown? + if (cooldownUntil > world.time) + if (display_error) + to_chat(owner, "[src] is unavailable. Wait [(cooldownUntil - world.time) / 10] seconds.") + return FALSE + // Have enough blood? + var/mob/living/L = owner + if (L.blood_volume < bloodcost) + if (display_error) + to_chat(owner, "You need at least [bloodcost] blood to activate [name]") + return FALSE + return TRUE + +/datum/action/bloodsucker/proc/CheckCanUse(display_error) // These checks can be scanned every frame while a ranged power is on. + if(!owner || !owner.mind) + return FALSE + // Torpor? + if(!can_use_in_torpor && HAS_TRAIT(owner, TRAIT_DEATHCOMA)) + if(display_error) + to_chat(owner, "Not while you're in Torpor.") + return FALSE + // Stake? + if(!can_be_staked && owner.AmStaked()) + if(display_error) + to_chat(owner, "You have a stake in your chest! Your powers are useless.") + return FALSE + // Incap? + if(must_be_capacitated) + var/mob/living/L = owner + if (L.incapacitated(TRUE, TRUE) || L.resting && !can_be_immobilized) + if(display_error) + to_chat(owner, "Not while you're incapacitated!") + return FALSE + // Constant Cost (out of blood) + if(warn_constant_cost) + var/mob/living/L = owner + if(L.blood_volume <= 0) + if(display_error) + to_chat(owner, "You don't have the blood to upkeep [src].") + return FALSE + return TRUE + +/datum/action/bloodsucker/proc/StartCooldown() + set waitfor = FALSE + // Alpha Out + button.color = rgb(128,0,0,128) + button.alpha = 100 + // Calculate Cooldown (by power's level) + var/this_cooldown = (cooldown_static || amSingleUse) ? cooldown : max(cooldown / 2, cooldown - (cooldown / 16 * (level_current-1))) + // NOTE: With this formula, you'll hit half cooldown at level 8 for that power. + + // Wait for cooldown + cooldownUntil = world.time + this_cooldown + spawn(this_cooldown) + // Alpha In + button.color = rgb(255,255,255,255) + button.alpha = 255 + +/datum/action/bloodsucker/proc/CheckCanDeactivate(display_error) + return TRUE + +/datum/action/bloodsucker/UpdateButtonIcon(force = FALSE) + background_icon_state = active? background_icon_state_on : background_icon_state_off + ..()//UpdateButtonIcon() + + +/datum/action/bloodsucker/proc/PayCost() + // owner for actions is the mob, not mind. + var/mob/living/L = owner + L.blood_volume -= bloodcost + + +/datum/action/bloodsucker/proc/ActivatePower() + + +/datum/action/bloodsucker/proc/DeactivatePower(mob/living/user = owner, mob/living/target) + active = FALSE + UpdateButtonIcon() + StartCooldown() + +/datum/action/bloodsucker/proc/ContinueActive(mob/living/user, mob/living/target) // Used by loops to make sure this power can stay active. + return active && user && (!warn_constant_cost || user.blood_volume > 0) + +/datum/action/bloodsucker/proc/RemoveAfterUse() + // Un-Learn Me! (GO HOME + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if (istype(bloodsuckerdatum)) + bloodsuckerdatum.powers -= src + Remove(owner) + +/datum/action/bloodsucker/proc/Upgrade() + level_current ++ + +/////////////////////////////////// PASSIVE POWERS /////////////////////////////////// + +// New Type: Passive (Always on, no button) +/datum/action/bloodsucker/passive + +/datum/action/bloodsucker/passive/New() + // REMOVED: DO NOTHBING! + ..() + // Don't Display Button! (it doesn't do anything anyhow) + button.screen_loc = DEFAULT_BLOODSPELLS + button.moved = DEFAULT_BLOODSPELLS + button.ordered = FALSE +/datum/action/bloodsucker/passive/Destroy() + if(owner) + Remove(owner) + target = null + +/////////////////////////////////// TARGETTED POWERS /////////////////////////////////// + +/datum/action/bloodsucker/targeted + // NOTE: All Targeted spells are Toggles! We just don't bother checking here. + var/target_range = 99 + var/message_Trigger = "Select a target." + var/obj/effect/proc_holder/bloodsucker/bs_proc_holder + var/power_activates_immediately = TRUE // Most powers happen the moment you click. Some, like Mesmerize, require time and shouldn't cost you if they fail. + + var/power_in_use = FALSE // Is this power LOCKED due to being used? + +/datum/action/bloodsucker/targeted/New(Target) + desc += "
\[Targeted Power\]" // Modify description to add notice that this is aimed. + ..() + // Create Proc Holder for intercepting clicks + bs_proc_holder = new () + bs_proc_holder.linked_power = src + +// Click power: Begin Aim +/datum/action/bloodsucker/targeted/Trigger() + if(active && CheckCanDeactivate(TRUE)) + DeactivateRangedAbility() + DeactivatePower() + return + if(!CheckCanPayCost(TRUE) || !CheckCanUse(TRUE)) + return + active = !active + UpdateButtonIcon() + // Create & Link Targeting Proc + var/mob/living/L = owner + if(L.ranged_ability) + L.ranged_ability.remove_ranged_ability() + bs_proc_holder.add_ranged_ability(L) + + if(message_Trigger != "") + to_chat(owner, "[message_Trigger]") + +/datum/action/bloodsucker/targeted/CheckCanUse(display_error) + . = ..() + if(!.) + return + if(!owner.client) // <--- We don't allow non client usage so that using powers like mesmerize will FAIL if you try to use them as ghost. Why? because ranged_abvility in spell.dm + return FALSE // doesn't let you remove powers if you're not there. So, let's just cancel the power entirely. + return TRUE + +/datum/action/bloodsucker/targeted/DeactivatePower(mob/living/user = owner, mob/living/target) + // Don't run ..(), we don't want to engage the cooldown until we USE this power! + active = FALSE + UpdateButtonIcon() + +/datum/action/bloodsucker/targeted/proc/DeactivateRangedAbility() + // Only Turned off when CLICK is disabled...aka, when you successfully clicked (or + bs_proc_holder.remove_ranged_ability() + +// Check if target is VALID (wall, turf, or character?) +/datum/action/bloodsucker/targeted/proc/CheckValidTarget(atom/A) + return FALSE // FALSE targets nothing. + +// Check if valid target meets conditions +/datum/action/bloodsucker/targeted/proc/CheckCanTarget(atom/A, display_error) + // Out of Range + if(!(A in view(target_range, owner))) + if(display_error && target_range > 1) // Only warn for range if it's greater than 1. Brawn doesn't need to announce itself. + to_chat(owner, "Your target is out of range.") + return FALSE + return istype(A) + +// Click Target +/datum/action/bloodsucker/targeted/proc/ClickWithPower(atom/A) + // CANCEL RANGED TARGET check + if(power_in_use || !CheckValidTarget(A)) + return FALSE + // Valid? (return true means DON'T cancel power!) + if(!CheckCanPayCost(TRUE) || !CheckCanUse(TRUE) || !CheckCanTarget(A, TRUE)) + return TRUE + // Skip this part so we can return TRUE right away. + if(power_activates_immediately) + PowerActivatedSuccessfully() // Mesmerize pays only after success. + power_in_use = TRUE // Lock us into this ability until it successfully fires off. Otherwise, we pay the blood even if we fail. + FireTargetedPower(A) // We use this instead of ActivatePower(), which has no input + power_in_use = FALSE + return TRUE + +/datum/action/bloodsucker/targeted/proc/FireTargetedPower(atom/A) + // Like ActivatePower, but specific to Targeted (and takes an atom input). We don't use ActivatePower for targeted. + +/datum/action/bloodsucker/targeted/proc/PowerActivatedSuccessfully() + // The power went off! We now pay the cost of the power. + PayCost() + DeactivateRangedAbility() + DeactivatePower() + StartCooldown() // Do AFTER UpdateIcon() inside of DeactivatePower. Otherwise icon just gets wiped. + +/datum/action/bloodsucker/targeted/ContinueActive(mob/living/user, mob/living/target) // Used by loops to make sure this power can stay active. + return ..() +// Target Proc Holder +/obj/effect/proc_holder/bloodsucker + var/datum/action/bloodsucker/targeted/linked_power + +/obj/effect/proc_holder/bloodsucker/remove_ranged_ability(msg) + ..() + linked_power.DeactivatePower() + +/obj/effect/proc_holder/bloodsucker/InterceptClickOn(mob/living/caller, params, atom/A) + return linked_power.ClickWithPower(A) diff --git a/code/modules/antagonists/bloodsucker/bloodsucker_sunlight.dm b/code/modules/antagonists/bloodsucker/bloodsucker_sunlight.dm new file mode 100644 index 0000000000..bb6e2a69f9 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/bloodsucker_sunlight.dm @@ -0,0 +1,195 @@ +#define TIME_BLOODSUCKER_NIGHT 900 // 15 minutes +#define TIME_BLOODSUCKER_DAY_WARN 90 // 1.5 minutes +#define TIME_BLOODSUCKER_DAY_FINAL_WARN 25 // 25 sec +#define TIME_BLOODSUCKER_DAY 60 // 1.5 minutes // 10 is a second, 600 is a minute. +#define TIME_BLOODSUCKER_BURN_INTERVAL 40 // 4 sec + + +// Over Time, tick down toward a "Solar Flare" of UV buffeting the station. This period is harmful to vamps. +/obj/effect/sunlight + //var/amDay = FALSE + var/cancel_me = FALSE + var/amDay = FALSE + var/time_til_cycle = 0 + +/obj/effect/sunlight/Initialize() + countdown() + hud_tick() + +/obj/effect/sunlight/proc/countdown() + set waitfor = FALSE + + while(!cancel_me) + + time_til_cycle = TIME_BLOODSUCKER_NIGHT + + // Part 1: Night (all is well) + while(time_til_cycle > TIME_BLOODSUCKER_DAY_WARN) + sleep(10) + if(cancel_me) + return + //sleep(TIME_BLOODSUCKER_NIGHT - TIME_BLOODSUCKER_DAY_WARN) + warn_daylight(1,"Solar Flares will bombard the station with dangerous UV in [TIME_BLOODSUCKER_DAY_WARN / 60] minutes. Prepare to seek cover in a coffin or closet.") // time2text <-- use Help On + give_home_power() // Give VANISHING ACT power to all vamps with a lair! + + // Part 2: Night Ending + while(time_til_cycle > TIME_BLOODSUCKER_DAY_FINAL_WARN) + sleep(10) + if(cancel_me) + return + //sleep(TIME_BLOODSUCKER_DAY_WARN - TIME_BLOODSUCKER_DAY_FINAL_WARN) + message_admins("BLOODSUCKER NOTICE: Daylight beginning in [TIME_BLOODSUCKER_DAY_FINAL_WARN] seconds.)") + warn_daylight(2,"Solar Flares are about to bombard the station! You have [TIME_BLOODSUCKER_DAY_FINAL_WARN] seconds to find cover!",\ + "In [TIME_BLOODSUCKER_DAY_FINAL_WARN / 10], your master will be at risk of a Solar Flare. Make sure they find cover!") + + // (FINAL LIL WARNING) + while(time_til_cycle > 5) + sleep(10) + if (cancel_me) + return + //sleep(TIME_BLOODSUCKER_DAY_FINAL_WARN - 50) + warn_daylight(3,"Seek cover, for Sol rises!") + + // Part 3: Night Ending + while (time_til_cycle > 0) + sleep(10) + if (cancel_me) + return + //sleep(50) + warn_daylight(4,"Solar flares bombard the station with deadly UV light!
Stay in cover for the next [TIME_BLOODSUCKER_DAY / 60] minutes or risk Final Death!",\ + "Solar flares bombard the station with UV light!") + + // Part 4: Day + amDay = TRUE + message_admins("BLOODSUCKER NOTICE: Daylight Beginning (Lasts for [TIME_BLOODSUCKER_DAY / 60] minutes.)") + time_til_cycle = TIME_BLOODSUCKER_DAY + sleep(10) // One second grace period. + //var/daylight_time = TIME_BLOODSUCKER_DAY + var/issued_XP = FALSE + while(time_til_cycle > 0) + punish_vamps() + sleep(TIME_BLOODSUCKER_BURN_INTERVAL) + if (cancel_me) + return + //daylight_time -= TIME_BLOODSUCKER_BURN_INTERVAL + // Issue Level Up! + if(!issued_XP && time_til_cycle <= 15) + issued_XP = TRUE + vamps_rank_up() + + warn_daylight(5,"The solar flare has ended, and the daylight danger has passed...for now.",\ + "The solar flare has ended, and the daylight danger has passed...for now.") + amDay = FALSE + day_end() // Remove VANISHING ACT power from all vamps who have it! Clear Warnings (sunlight, locker protection) + message_admins("BLOODSUCKER NOTICE: Daylight Ended. Resetting to Night (Lasts for [TIME_BLOODSUCKER_NIGHT / 60] minutes.)") + + +/obj/effect/sunlight/proc/hud_tick() + set waitfor = FALSE + while(!cancel_me) + // Update all Bloodsucker sunlight huds + for(var/datum/mind/M in SSticker.mode.bloodsuckers) + if(!istype(M) || !istype(M.current)) + continue + var/datum/antagonist/bloodsucker/bloodsuckerdatum = M.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(istype(bloodsuckerdatum)) + bloodsuckerdatum.update_sunlight(max(0, time_til_cycle), amDay) // This pings all HUDs + sleep(10) + time_til_cycle -- + +/obj/effect/sunlight/proc/warn_daylight(danger_level=0, vampwarn = "", vassalwarn = "") + for(var/datum/mind/M in SSticker.mode.bloodsuckers) + if(!istype(M)) + continue + to_chat(M,vampwarn) + if(M.current) + if(danger_level == 1) + M.current.playsound_local(null, 'sound/chatter/griffin_3.ogg', 50 + danger_level, 1) + else if(danger_level == 2) + M.current.playsound_local(null, 'sound/chatter/griffin_5.ogg', 50 + danger_level, 1) + else if(danger_level == 3) + M.current.playsound_local(null, 'sound/effects/alert.ogg', 75, 1) + else if(danger_level == 4) + M.current.playsound_local(null, 'sound/ambience/ambimystery.ogg', 100, 1) + else if(danger_level == 5) + M.current.playsound_local(null, 'sound/spookoween/ghosty_wind.ogg', 90, 1) + + if(vassalwarn != "") + for(var/datum/mind/M in SSticker.mode.vassals) + if(!istype(M)) + continue + to_chat(M,vassalwarn) + + +/obj/effect/sunlight/proc/punish_vamps() + // Cycle through all vamp antags and check if they're inside a closet. + for(var/datum/mind/M in SSticker.mode.bloodsuckers) + if(!istype(M) || !istype(M.current)) + continue + var/datum/antagonist/bloodsucker/bloodsuckerdatum = M.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(!istype(bloodsuckerdatum)) + continue + // Closets offer SOME protection + if(istype(M.current.loc, /obj/structure)) + // Coffins offer the BEST protection + if(istype(M.current.loc, /obj/structure/closet/crate/coffin)) + SEND_SIGNAL(M.current, COMSIG_ADD_MOOD_EVENT, "vampsleep", /datum/mood_event/coffinsleep) + continue + else + if(!bloodsuckerdatum.warn_sun_locker) + to_chat(M, "Your skin sizzles. The [M.current.loc] doesn't protect well against UV bombardment.") + bloodsuckerdatum.warn_sun_locker = TRUE + M.current.adjustFireLoss(0.5 + bloodsuckerdatum.vamplevel / 2) // M.current.fireloss += 0.5 + bloodsuckerdatum.vamplevel / 2 // Do DIRECT damage. Being spaced was causing this to not occur. setFireLoss(bloodsuckerdatum.vamplevel) + M.current.updatehealth() + SEND_SIGNAL(M.current, COMSIG_ADD_MOOD_EVENT, "vampsleep", /datum/mood_event/daylight_1) + // Out in the Open? Buh Bye + else + if(!bloodsuckerdatum.warn_sun_burn) + if(bloodsuckerdatum.vamplevel > 0) + to_chat(M, "The solar flare sets your skin ablaze!") + else + to_chat(M, "The solar flare scalds your neophyte skin!") + bloodsuckerdatum.warn_sun_burn = TRUE + if(M.current.fire_stacks <= 0) + M.current.fire_stacks = 0 + if(bloodsuckerdatum.vamplevel > 0) + M.current.adjust_fire_stacks(0.2 + bloodsuckerdatum.vamplevel / 10) + M.current.IgniteMob() + M.current.adjustFireLoss(2 + bloodsuckerdatum.vamplevel) // M.current.fireloss += 2 + bloodsuckerdatum.vamplevel // Do DIRECT damage. Being spaced was causing this to not occur. //setFireLoss(2 + bloodsuckerdatum.vamplevel) + M.current.updatehealth() + SEND_SIGNAL(M.current, COMSIG_ADD_MOOD_EVENT, "vampsleep", /datum/mood_event/daylight_2) + +/obj/effect/sunlight/proc/day_end() + for(var/datum/mind/M in SSticker.mode.bloodsuckers) + if(!istype(M) || !istype(M.current)) + continue + var/datum/antagonist/bloodsucker/bloodsuckerdatum = M.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(!istype(bloodsuckerdatum)) + continue + // Reset Warnings + bloodsuckerdatum.warn_sun_locker = FALSE + bloodsuckerdatum.warn_sun_burn = FALSE + // Remove Dawn Powers + for(var/datum/action/bloodsucker/P in bloodsuckerdatum.powers) + if(istype(P, /datum/action/bloodsucker/gohome)) + bloodsuckerdatum.powers -= P + P.Remove(M.current) + +/obj/effect/sunlight/proc/vamps_rank_up() + set waitfor = FALSE + // Cycle through all vamp antags and check if they're inside a closet. + for(var/datum/mind/M in SSticker.mode.bloodsuckers) + if(!istype(M) || !istype(M.current)) + continue + var/datum/antagonist/bloodsucker/bloodsuckerdatum = M.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(istype(bloodsuckerdatum)) + bloodsuckerdatum.RankUp() // Rank up! Must still be in a coffin to level! + +/obj/effect/sunlight/proc/give_home_power() + // It's late...! Give the "Vanishing Act" gohome power to bloodsuckers. + for(var/datum/mind/M in SSticker.mode.bloodsuckers) + if(!istype(M) || !istype(M.current)) + continue + var/datum/antagonist/bloodsucker/bloodsuckerdatum = M.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(istype(bloodsuckerdatum) && bloodsuckerdatum.lair && !(locate(/datum/action/bloodsucker/gohome) in bloodsuckerdatum.powers)) + bloodsuckerdatum.BuyPower(new /datum/action/bloodsucker/gohome) diff --git a/code/modules/antagonists/bloodsucker/bloodsucker_ui.dm b/code/modules/antagonists/bloodsucker/bloodsucker_ui.dm new file mode 100644 index 0000000000..b922af6066 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/bloodsucker_ui.dm @@ -0,0 +1,116 @@ + + +// For all things visual, such as leveling up + + +// Look up: _vending.dm proc/ui_interact() +// Malf_Modules.dm proc/use() + +/* +/datum/antagonist/bloodsucker/proc/LevelUpMenu() + var/list/dat = list() + + dat += "

You have become more ancient.
Direct the path of your blood

" + dat += "
" + + // Step One: Decide powers you CAN buy. + for(var/pickedpower in typesof(/datum/action/bloodsucker)) + var/obj/effect/proc_holder/spell/bloodsucker/power = pickedpower + // NAME + dat += "[power.name]" + // COST + dat += "[power.name] Vend" + dat == "
" + + var/datum/browser/popup = new(owner.current, "bloodsuckerrank", "Bloodsucker Rank Up") + popup.set_content(dat.Join()) + popup.open() + +/datum/antagonist/bloodsucker/Topic(href, href_list) + if(..()) + return +*/ + + +// From browser.dm: /datum/browser/New(nuser, nwindow_id, ntitle = 0, nwidth = 0, nheight = 0, var/atom/nref = null) + + +/* + var/list/dat = list() + dat += "Select use of processing time: (currently #[processing_time] left.)
" + dat += "
" + dat += "Install Module:
" + dat += "The number afterwards is the amount of processing time it consumes.
" + for(var/datum/AI_Module/large/module in possible_modules) + dat += "[module.module_name]\[?\] ([module.cost])
" + for(var/datum/AI_Module/small/module in possible_modules) + dat += "[module.module_name]\[?\] ([module.cost])
" + dat += "
" + if(temp) + dat += "[temp]" + var/datum/browser/popup = new(user, "modpicker", "Malf Module Menu") + popup.set_content(dat.Join()) + popup.open() +*/ + +/* + var/dat = "" + var/datum/bank_account/account + var/mob/living/carbon/human/H + var/obj/item/card/id/C + if(ishuman(user)) + H = user + C = H.get_idcard(TRUE) + + if(!C) + dat += "

No ID Card detected!

" + else if (!C.registered_account) + dat += "

No account on registered ID card!

" + if(onstation && C && C.registered_account) + account = C.registered_account + dat += "

Select an item

" + dat += "
" + if(!product_records.len) + dat += "No product loaded!" + else + var/list/display_records = product_records + coin_records + if(extended_inventory) + display_records = product_records + coin_records + hidden_records + dat += "" + for (var/datum/data/vending_product/R in display_records) + var/price_listed = "$[default_price]" + var/is_hidden = hidden_records.Find(R) + if(is_hidden && !extended_inventory) + continue + if(R.custom_price) + price_listed = "$[R.custom_price]" + if(!onstation || account && account.account_job && account.account_job.paycheck_department == payment_department) + price_listed = "FREE" + if(coin_records.Find(R) || is_hidden) + price_listed = "$[R.custom_premium_price ? R.custom_premium_price : extra_price]" + dat += "" + dat += "" + if(R.amount > 0 && ((C && C.registered_account && onstation) || (!onstation && isliving(user)))) + dat += "" + else + dat += "" + dat += "" + dat += "
[sanitize(R.name)] ([price_listed])[R.amount] VendNot Available
" + dat += "
" + if(onstation && C && C.registered_account) + dat += "Balance: $[account.account_balance]" + if(istype(src, /obj/machinery/vending/snack)) + dat += "

Chef's Food Selection

" + dat += "
" + for (var/O in dish_quants) + if(dish_quants[O] > 0) + var/N = dish_quants[O] + dat += "Dispense " + dat += "[capitalize(O)] ($[default_price]): [N]
" + dat += "
" + + var/datum/browser/popup = new(user, "vending", (name)) + popup.set_content(dat) + popup.set_title_image(user.browse_rsc_icon(icon, icon_state)) + popup.open() +*/ \ No newline at end of file diff --git a/code/modules/antagonists/bloodsucker/datum_bloodsucker.dm b/code/modules/antagonists/bloodsucker/datum_bloodsucker.dm new file mode 100644 index 0000000000..febf53c324 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/datum_bloodsucker.dm @@ -0,0 +1,765 @@ +/datum/team/vampireclan + name = "Clan" // Teravanni, + +/datum/antagonist/bloodsucker + name = "Bloodsucker" + roundend_category = "bloodsuckers" + antagpanel_category = "Bloodsucker" + job_rank = ROLE_BLOODSUCKER + + // NAME + var/vampname // My Dracula name + var/vamptitle // My Dracula title + var/vampreputation // My "Surname" or description of my deeds + // CLAN + var/datum/team/vampireclan/clan + var/list/datum/antagonist/vassal/vassals = list()// Vassals under my control. Periodically remove the dead ones. + var/datum/mind/creator // Who made me? For both Vassals AND Bloodsuckers (though Master Vamps won't have one) + // POWERS + var/list/datum/action/powers = list()// Purchased powers + var/poweron_feed = FALSE // Am I feeding? + var/poweron_masquerade = FALSE + // STATS + var/vamplevel = 0 + var/vamplevel_unspent = 1 + var/regenRate = 0.3 // How many points of Brute do I heal per tick? + var/feedAmount = 15 // Amount of blood drawn from a target per tick. + var/maxBloodVolume = 600 // Maximum blood a Vamp can hold via feeding. // BLOOD_VOLUME_NORMAL 550 // BLOOD_VOLUME_SAFE 475 //BLOOD_VOLUME_OKAY 336 //BLOOD_VOLUME_BAD 224 // BLOOD_VOLUME_SURVIVE 122 + // OBJECTIVES + var/list/datum/objective/objectives_given = list() // For removal if needed. + var/area/lair + var/obj/structure/closet/crate/coffin + // TRACKING + var/foodInGut = 0 // How much food to throw up later. You shouldn't have eaten that. + var/warn_sun_locker = FALSE // So we only get the locker burn message once per day. + var/warn_sun_burn = FALSE // So we only get the sun burn message once per day. + var/had_toxlover = FALSE + // LISTS + var/static/list/defaultTraits = list (TRAIT_STABLEHEART, TRAIT_NOBREATH, TRAIT_SLEEPIMMUNE, TRAIT_NOCRITDAMAGE, TRAIT_RESISTCOLD, TRAIT_RADIMMUNE, TRAIT_VIRUSIMMUNE, TRAIT_NIGHT_VISION, \ + TRAIT_NOSOFTCRIT, TRAIT_NOHARDCRIT, TRAIT_AGEUSIA, TRAIT_COLDBLOODED, TRAIT_NONATURALHEAL, TRAIT_NOMARROW, TRAIT_NOPULSE, TRAIT_NOCLONE) + // NOTES: TRAIT_AGEUSIA <-- Doesn't like flavors. + // REMOVED: TRAIT_NODEATH + // TO ADD: + //var/static/list/defaultOrgans = list (/obj/item/organ/heart/vampheart,/obj/item/organ/heart/vampeyes) + +/datum/antagonist/bloodsucker/on_gain() + SSticker.mode.bloodsuckers |= owner // Add if not already in here (and you might be, if you were picked at round start) + SSticker.mode.check_start_sunlight()// Start Sunlight? (if first Vamp) + SelectFirstName()// Name & Title + SelectTitle(am_fledgling=TRUE) // If I have a creator, then set as Fledgling. + SelectReputation(am_fledgling=TRUE) + AssignStarterPowersAndStats()// Give Powers & Stats + forge_bloodsucker_objectives()// Objectives & Team + update_bloodsucker_icons_added(owner.current, "bloodsucker") // Add Antag HUD + LifeTick() // Run Life Function + . = ..() + + +/datum/antagonist/bloodsucker/on_removal() + SSticker.mode.bloodsuckers -= owner + SSticker.mode.check_cancel_sunlight()// End Sunlight? (if last Vamp) + ClearAllPowersAndStats()// Clear Powers & Stats + clear_bloodsucker_objectives() // Objectives + update_bloodsucker_icons_removed(owner.current)// Clear Antag HUD + . = ..() + + + +/datum/antagonist/bloodsucker/greet() + var/fullname = ReturnFullName(TRUE) + to_chat(owner, "You are [fullname], a bloodsucking vampire!
") + owner.announce_objectives() + to_chat(owner, "* You regenerate your health slowly, you're weak to fire, and you depend on blood to survive. Allow your stolen blood to run too low, and you will find yourself at \ + risk of being discovered!
") + //to_chat(owner, "As an immortal, your power is linked to your age. The older you grow, the more abilities you will have access to.") + var/vamp_greet + vamp_greet += "* Other Bloodsuckers are not necessarily your friends, but your survival may depend on cooperation. Betray them at your own discretion and peril.
" + vamp_greet += "* Use \",b\" to speak your ancient Bloodsucker language.
" + vamp_greet += "Bloodsucker Tip: Rest in a Coffin to claim it, and that area, as your lair.
" + vamp_greet += "Bloodsucker Tip: Fear the daylight! Solar flares will bombard the station periodically, and only your coffin can guarantee your safety.
" + vamp_greet += "Bloodsucker Tip: You wont loose blood if you are unconcious or sleeping. Use this to your advantage to conserve blood.
" + to_chat(owner, vamp_greet) + + owner.current.playsound_local(null, 'sound/bloodsucker/BloodsuckerAlert.ogg', 100, FALSE, pressure_affected = FALSE) + antag_memory += "Although you were born a mortal, in un-death you earned the name [fullname].
" + + +/datum/antagonist/bloodsucker/farewell() + owner.current.visible_message("[owner.current]'s skin flushes with color, their eyes growing glossier. They look...alive.",\ + "With a snap, your curse has ended. You are no longer a Bloodsucker. You live once more!") + // Refill with Blood + owner.current.blood_volume = max(owner.current.blood_volume,BLOOD_VOLUME_SAFE) + + + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +/datum/antagonist/bloodsucker/proc/SelectFirstName() + // Names (EVERYONE gets one)) + if (owner.current.gender == MALE) + vampname = pick("Desmond","Rudolph","Dracul","Vlad","Pyotr","Gregor","Cristian","Christoff","Marcu","Andrei","Constantin","Gheorghe","Grigore","Ilie","Iacob","Luca","Mihail","Pavel","Vasile","Octavian","Sorin", \ + "Sveyn","Aurel","Alexe","Iustin","Theodor","Dimitrie","Octav","Damien","Magnus","Caine","Abel", // Romanian/Ancient + "Lucius","Gaius","Otho","Balbinus","Arcadius","Romanos","Alexios","Vitellius", // Latin + "Melanthus","Teuthras","Orchamus","Amyntor","Axion", // Greek + "Thoth","Thutmose","Osorkon,","Nofret","Minmotu","Khafra", // Egyptian + "Dio") + + else + vampname = pick("Islana","Tyrra","Greganna","Pytra","Hilda","Andra","Crina","Viorela","Viorica","Anemona","Camelia","Narcisa","Sorina","Alessia","Sophia","Gladda","Arcana","Morgan","Lasarra","Ioana","Elena", \ + "Alina","Rodica","Teodora","Denisa","Mihaela","Svetla","Stefania","Diyana","Kelssa","Lilith", // Romanian/Ancient + "Alexia","Athanasia","Callista","Karena","Nephele","Scylla","Ursa", // Latin + "Alcestis","Damaris","Elisavet","Khthonia","Teodora", // Greek + "Nefret","Ankhesenpep") // Egyptian + +/datum/antagonist/bloodsucker/proc/SelectTitle(am_fledgling = 0, forced = FALSE) + // Already have Title + if (!forced && vamptitle != null) + return + // Titles [Master] + if (!am_fledgling) + if (owner.current.gender == MALE) + vamptitle = pick ("Count","Baron","Viscount","Prince","Duke","Tzar","Dreadlord","Lord","Master") + else + vamptitle = pick ("Countess","Baroness","Viscountess","Princess","Duchess","Tzarina","Dreadlady","Lady","Mistress") + to_chat(owner, "You have earned a title! You are now known as [ReturnFullName(TRUE)]!") + // Titles [Fledgling] + else + vamptitle = null + +/datum/antagonist/bloodsucker/proc/SelectReputation(am_fledgling = 0, forced=FALSE) + // Already have Reputation + if (!forced && vampreputation != null) + return + // Reputations [Master] + if (!am_fledgling) + vampreputation = pick("Butcher","Blood Fiend","Crimson","Red","Black","Terror","Nightman","Feared","Ravenous","Fiend","Malevolent","Wicked","Ancient","Plaguebringer","Sinister","Forgotten","Wretched","Baleful", \ + "Inqisitor","Harvester","Reviled","Robust","Betrayer","Destructor","Damned","Accursed","Terrible","Vicious","Profane","Vile","Depraved","Foul","Slayer","Manslayer","Sovereign","Slaughterer", \ + "Forsaken","Mad","Dragon","Savage","Villainous","Nefarious","Inquisitor","Marauder","Horrible","Immortal","Undying","Overlord","Corrupt","Hellspawn","Tyrant","Sanguineous") + if (owner.current.gender == MALE) + if (prob(10)) // Gender override + vampreputation = pick("King of the Damned", "Blood King", "Emperor of Blades", "Sinlord", "God-King") + else + if (prob(10)) // Gender override + vampreputation = pick("Queen of the Damned", "Blood Queen", "Empress of Blades", "Sinlady", "God-Queen") + + to_chat(owner, "You have earned a reputation! You are now known as [ReturnFullName(TRUE)]!") + + // Reputations [Fledgling] + else + vampreputation = pick ("Crude","Callow","Unlearned","Neophyte","Novice","Unseasoned","Fledgling","Young","Neonate","Scrapling","Untested","Unproven","Unknown","Newly Risen","Born","Scavenger","Unknowing",\ + "Unspoiled","Disgraced","Defrocked","Shamed","Meek","Timid","Broken")//,"Fresh") + + +/datum/antagonist/bloodsucker/proc/AmFledgling() + return !vamptitle + +/datum/antagonist/bloodsucker/proc/ReturnFullName(var/include_rep=0) + + var/fullname + // Name First + fullname = (vampname ? vampname : owner.current.name) + // Title + if(vamptitle) + fullname = vamptitle + " " + fullname + // Rep + if(include_rep && vampreputation) + fullname = fullname + " the " + vampreputation + + return fullname + + +/datum/antagonist/bloodsucker/proc/BuyPower(datum/action/bloodsucker/power)//(obj/effect/proc_holder/spell/power) + powers += power + power.Grant(owner.current)// owner.AddSpell(power) + +/datum/antagonist/bloodsucker/proc/AssignStarterPowersAndStats() + // Blood/Rank Counter + add_hud() + update_hud(TRUE) // Set blood value, current rank + // Powers + BuyPower(new /datum/action/bloodsucker/feed) + BuyPower(new /datum/action/bloodsucker/masquerade) + BuyPower(new /datum/action/bloodsucker/veil) + // Traits + for (var/T in defaultTraits) + ADD_TRAIT(owner.current, T, "bloodsucker") + if(HAS_TRAIT(owner.current, TRAIT_TOXINLOVER)) //No slime bonuses here, no thank you + had_toxlover = TRUE + REMOVE_TRAIT(owner.current, TRAIT_TOXINLOVER, "species") + // Traits: Species + if(ishuman(owner.current)) + var/mob/living/carbon/human/H = owner.current + var/datum/species/S = H.dna.species + S.species_traits |= DRINKSBLOOD + // Clear Addictions + owner.current.reagents.addiction_list = list() // Start over from scratch. Lucky you! At least you're not addicted to blood anymore (if you were) + // Stats + if(ishuman(owner.current)) + var/mob/living/carbon/human/H = owner.current + var/datum/species/S = H.dna.species + // Make Changes + S.brutemod *= 0.5 // <-------------------- Start small, but burn mod increases based on rank! + S.coldmod = 0 + S.stunmod *= 0.25 + S.siemens_coeff *= 0.75 //base electrocution coefficient 1 + //S.heatmod += 0.5 // Heat shouldn't affect. Only Fire. + //S.punchstunthreshold = 8 //damage at which punches from this race will stun 9 + S.punchdamagelow += 1 //lowest possible punch damage 0 + S.punchdamagehigh += 1 //highest possible punch damage 9 + // Clown + if(istype(H) && owner.assigned_role == "Clown") + H.dna.remove_mutation(CLOWNMUT) + to_chat(H, "As a vampiric clown, you are no longer a danger to yourself. Your nature is subdued.") + // Physiology + CheckVampOrgans() // Heart, Eyes + // Language + owner.current.grant_language(/datum/language/vampiric) + // Soul + //owner.current.hellbound = TRUE Causes wierd stuff + owner.hasSoul = FALSE // If false, renders the character unable to sell their soul. + owner.isholy = FALSE // is this person a chaplain or admin role allowed to use bibles + // Disabilities + CureDisabilities() + +/datum/antagonist/bloodsucker/proc/ClearAllPowersAndStats() + // Blood/Rank Counter + remove_hud() + // Powers + while(powers.len) + var/datum/action/bloodsucker/power = pick(powers) + powers -= power + power.Remove(owner.current) + // owner.RemoveSpell(power) + // Traits + for(var/T in defaultTraits) + REMOVE_TRAIT(owner.current, T, "bloodsucker") + if(had_toxlover == TRUE) + ADD_TRAIT(owner.current, TRAIT_TOXINLOVER, "species") + + // Traits: Species + if(ishuman(owner.current)) + var/mob/living/carbon/human/H = owner.current + H.set_species(H.dna.species.type) + // Stats + if(ishuman(owner.current)) + var/mob/living/carbon/human/H = owner.current + H.set_species(H.dna.species.type) + // Clown + if(istype(H) && owner.assigned_role == "Clown") + H.dna.add_mutation(CLOWNMUT) + // NOTE: Use initial() to return things to default! + // Physiology + owner.current.regenerate_organs() + // Update Health + owner.current.setMaxHealth(100) + // Language + owner.current.remove_language(/datum/language/vampiric) + // Soul + if (owner.soulOwner == owner) // Return soul, if *I* own it. + owner.hasSoul = TRUE +//owner.current.hellbound = FALSE + +datum/antagonist/bloodsucker/proc/RankUp() + set waitfor = FALSE + if(!owner || !owner.current) + return + vamplevel_unspent ++ + // Spend Rank Immediately? + if(istype(owner.current.loc, /obj/structure/closet/crate/coffin)) + SpendRank() + else + to_chat(owner, "You have grown more ancient! Sleep in a coffin that you have claimed to thicken your blood and become more powerful.") + if(vamplevel_unspent >= 2) + to_chat(owner, "Bloodsucker Tip: If you cannot find or steal a coffin to use, they can be built from wooden planks.
") + +datum/antagonist/bloodsucker/proc/LevelUpPowers() + for(var/datum/action/bloodsucker/power in powers) + power.level_current ++ + +datum/antagonist/bloodsucker/proc/SpendRank() + set waitfor = FALSE + if (vamplevel_unspent <= 0 || !owner || !owner.current || !owner.current.client) + return + ///////// + // Powers + //TODO: Make this into a radial + // Purchase Power Prompt + var/list/options = list() // Taken from gasmask.dm, for Clown Masks. + for(var/pickedpower in typesof(/datum/action/bloodsucker)) + var/datum/action/bloodsucker/power = pickedpower + // If I don't own it, and I'm allowed to buy it. + if(!(locate(power) in powers) && initial(power.bloodsucker_can_buy)) + options[initial(power.name)] = power // TESTING: After working with TGUI, it seems you can use initial() to view the variables inside a path? + options["\[ Not Now \]"] = null + // Abort? + if(options.len > 1) + var/choice = input(owner.current, "You have the opportunity to grow more ancient. Select a power to advance your Rank.", "Your Blood Thickens...") in options + // Cheat-Safety: Can't keep opening/closing coffin to spam levels + if(vamplevel_unspent <= 0) // Already spent all your points, and tried opening/closing your coffin, pal. + return + if(!istype(owner.current.loc, /obj/structure/closet/crate/coffin)) + to_chat(owner.current, "Return to your coffin to advance your Rank.") + return + if(!choice || !options[choice] || (locate(options[choice]) in powers)) // ADDED: Check to see if you already have this power, due to window stacking. + to_chat(owner.current, "You prevent your blood from thickening just yet, but you may try again later.") + return + // Buy New Powers + var/datum/action/bloodsucker/P = options[choice] + BuyPower(new P) + to_chat(owner.current, "You have learned [initial(P.name)]!") + else + to_chat(owner.current, "You grow more ancient by the night!") + ///////// + // Advance Powers (including new) + LevelUpPowers() + //////// + // Advance Stats + if(ishuman(owner.current)) + var/mob/living/carbon/human/H = owner.current + var/datum/species/S = H.dna.species + S.burnmod *= 0.025 // Slightly more burn damage + S.stunmod *= 0.95 // Slightly less stun time. + S.punchdamagelow += 0.5 + S.punchdamagehigh += 0.5 // NOTE: This affects the hitting power of Brawn. + // More Health + owner.current.setMaxHealth(owner.current.maxHealth + 5) + // Vamp Stats + regenRate += 0.05 // Points of brute healed (starts at 0.3) + feedAmount += 2 // Increase how quickly I munch down vics (15) + maxBloodVolume += 50 // Increase my max blood (600) + ///////// + vamplevel ++ + vamplevel_unspent -- + + // Assign True Reputation + if(vamplevel == 4) + SelectReputation(am_fledgling = FALSE, forced = TRUE) + to_chat(owner.current, "You are now a rank [vamplevel] Bloodsucker. Your strength, resistence, health, feed rate, regen rate, and maximum blood have all increased!") + to_chat(owner.current, "Your existing powers have all ranked up as well!") + update_hud(TRUE) + owner.current.playsound_local(null, 'sound/effects/pope_entry.ogg', 25, 1) // Play THIS sound for user only. The "null" is where turf would go if a location was needed. Null puts it right in their head. + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//This handles the application of antag huds/special abilities +/datum/antagonist/bloodsucker/apply_innate_effects(mob/living/mob_override) + return + +//This handles the removal of antag huds/special abilities +/datum/antagonist/bloodsucker/remove_innate_effects(mob/living/mob_override) + return + +//Assign default team and creates one for one of a kind team antagonists +/datum/antagonist/bloodsucker/create_team(datum/team/team) + return + +// Create Objectives +/datum/antagonist/bloodsucker/proc/forge_bloodsucker_objectives() // Fledgling vampires can have different objectives. + + // TEAM + //clan = new /datum/team/vampireclan(owner) + + + // Lair Objective: Create a Lair + var/datum/objective/bloodsucker/lair/lair_objective = new + lair_objective.owner = owner + lair_objective.generate_objective() + add_objective(lair_objective) + + // Protege Objective + var/datum/objective/bloodsucker/protege/protege_objective = new + protege_objective.owner = owner + protege_objective.generate_objective() + add_objective(protege_objective) + + if (rand(0,1) == 0) + // Heart Thief Objective + var/datum/objective/bloodsucker/heartthief/heartthief_objective = new + heartthief_objective.owner = owner + heartthief_objective.generate_objective() + add_objective(heartthief_objective) + + else + // Solars Objective + var/datum/objective/bloodsucker/solars/solars_objective = new + solars_objective.owner = owner + solars_objective.generate_objective() + add_objective(solars_objective) + + // Survive Objective + var/datum/objective/bloodsucker/survive/survive_objective = new + survive_objective.owner = owner + survive_objective.generate_objective() + add_objective(survive_objective) + + +/datum/antagonist/bloodsucker/proc/add_objective(var/datum/objective/O) + objectives += O + objectives_given += O + +/datum/antagonist/bloodsucker/proc/clear_bloodsucker_objectives() + + var/datum/team/team = get_team() + if(team) + team.remove_member(owner) + + for(var/O in objectives_given) + objectives -= O + qdel(O) + objectives_given = list() // Traitors had this, so I added it. Not sure why. + + +/datum/antagonist/bloodsucker/get_team() + return clan + +//Name shown on antag list +/datum/antagonist/bloodsucker/antag_listing_name() + return ..() + "([ReturnFullName(TRUE)])" + +//Whatever interesting things happened to the antag admins should know about +//Include additional information about antag in this part +/datum/antagonist/bloodsucker/antag_listing_status() + if (owner && owner.AmFinalDeath()) + return "Final Death" + return ..() + + + +//Individual roundend report +/datum/antagonist/bloodsucker/roundend_report() + // Get the default Objectives + var/list/report = list() + + // Vamp Name + report += "
\[[ReturnFullName(TRUE)]\]" + + // Default Report + report += ..() + + // Now list their vassals + if (vassals.len > 0) + report += "Their Vassals were..." + for (var/datum/antagonist/vassal/V in vassals) + if (V.owner) + var/jobname = V.owner.assigned_role ? "the [V.owner.assigned_role]" : "" + report += "[V.owner.name] [jobname]" + + return report.Join("
") + +//Displayed at the start of roundend_category section, default to roundend_category header +/datum/antagonist/bloodsucker/roundend_report_header() + return "Lurking in the darkness, the Bloodsuckers were:
" + + + + + +// 2019 Breakdown of Bloodsuckers: + +// G A M E P L A Y +// +// Bloodsuckers should be inherrently powerful: they never stay dead, and they can hide in plain sight +// better than any other antagonist aboard the station. +// +// However, only elder Bloodsuckers are the powerful creatures of legend. Ranking up as a Bloodsucker +// should impart slight strength and health benefits, as well as powers that can grow over time. But +// their weaknesses should grow as well, and not just to fire. + + +// A B I L I T I E S +// +// * Bloodsuckers can FEIGN LIFE + DEATH. +// Feigning LIFE: +// - Warms up the body +// - Creates a heartbeat +// - Fake blood amount (550) +// Feign DEATH: Not yet done +// - When lying down or sitting, you appear "dead and lifeless" + +// * Bloodsuckers REGENERATE +// - Brute damage heals rather rapidly. Burn damage heals slowly. +// - Healing is reduced when hungry or starved. +// - Burn does not heal when starved. A starved vampire remains "dead" until burns can heal. +// - Bodyparts and organs regrow in Torpor (except for the Heart and Brain). +// +// * Bloodsuckers are IMMORTAL +// - Brute damage cannot destroy them (and it caps very low, so they don't stack too much) +// - Burn damage can only kill them at very high amounts. +// - Removing the head kills the vamp forever. +// - Removing the heart kills the vamp until replaced. +// +// * Bloodsuckers are DEAD +// - They do not breathe. +// - Cold affects them less. +// - They are immune to disease (but can spread it) +// - Food is useless and cause sickness. +// - Nothing can heal the vamp other than his own blood. +// +// * Bloodsuckers are PREDATORS +// - They detect life/heartbeats nearby. +// - They know other predators instantly (Vamps, Werewolves, and alien types) regardless of disguise. +// +// +// +// * Bloodsuckers enter Torpor when DEAD or RESTING in coffin +// - Torpid vampires regenerate their health. Coffins negate cost and speed up the process. +// ** To rest in a coffin, either SLEEP or CLOSE THE LID while you're in it. You will be given a prompt to sleep until healed. Healing in a coffin costs NO blood! +// + + + +// O B J E C T I V E S +// +// +// +// +// 1) GROOM AN HEIR: Find a person with appropriate traits (hair, blood type, gender) to be turned as a Vampire. Before they rise, they must be properly trained. Raise them to great power after their change. +// +// 2) BIBLIOPHILE: Research objects of interest, study items looking for clues of ancient secrets, and hunt down the clues to a Vampiric artifact of horrible power. +// +// 3) CRYPT LORD: Build a terrifying sepulcher to your evil, with servants to lavish upon you in undeath. The trappings of a true crypt lord come at grave cost. +// +// 4) GOURMOND: Oh, to taste all the delicacies the station has to offer! DRINK ## BLOOD FROM VICTIMS WHO LIVE, EAT ## ORGANS FROM VICTIMS WHO LIVE + + +// Vassals +// +// - Loyal to (and In Love With) Master +// - Master can speak to, summon, or punish his Vassals, even while asleep or torpid. +// - Master may have as many Vassals as Rank +// - Vassals see their Master's speech emboldened! + + + + + + + + +// Dev Notes +// +// HEALING: Maybe Vamps metabolize specially? Like, they slowly drip their own blood into their system? +// - Give Vamps their own metabolization proc, perhaps? +// ** shadowpeople.dm has rules for healing. +// +// KILLING: It's almost impossible to track who someone has directly killed. But an Admin could be given +// an easy way to whip a Bloodsucker for cruel behavior, as a RP mechanic but not a punishment. +// ** +// +// HUNGER: Just keep adjusting mob's nutrition to Blood Hunger level. No need to cancel nutrition from eating. +// ** mob.dm /set_nutrition() +// ** snacks.dm / attack() <-- Stop food from doing anything? + +// ORGANS: Liver +// ** life.dm /handle_liver() +// +// CORPSE: Most of these effects likely go away when using "Masquerade" to appear alive. +// ** status_procs.dm /adjust_bodytemperature() +// ** traits.dm /TRAIT_NOBREATH /TRAIT_SLEEPIMMUNE /TRAIT_RESISTCOLD /TRAIT_RADIMMUNE /TRAIT_VIRUSIMMUNE +// * MASQUERADE ON/OFF: /TRAIT_FAKEDEATH (M) +// * /TRAIT_NIGHT_VISION +// * /TRAIT_DEATHCOMA <-- This basically makes you immobile. When using status_procs /fakedeath(), make sure to remove Coma unless we're in Torpor! +// * /TRAIT_NODEATH <--- ??? +// ** species /NOZOMBIE +// * ADD: TRAIT_COLDBLOODED <-- add to carbon/life.dm /natural_bodytemperature_stabilization() +// +// MASQUERADE Appear as human! +// ** examine.dm /examine() <-- Change "blood_volume < BLOOD_VOLUME_SAFE" to a new examine +// +// NOSFERATU ** human.add_trait(TRAIT_DISFIGURED, "insert_vamp_datum_here") <-- Makes you UNKNOWN unless your ID says otherwise. +// STEALTH ** human_helpers.dm /get_visible_name() ** shadowpeople.dm has rules for Light. +// +// FRENZY ** living.dm /update_mobility() (USED TO be update_canmove) +// +// PREDATOR See other Vamps! +// * examine.dm /examine() +// +// WEAKNESSES: -Poor mood in Chapel or near Chaplain. -Stamina damage from Bible + + + + //message_admins("DEBUG3: attempt_cast() [name] / [user_C.handcuffed] ") + + +// TODO: +// +// Death (fire, heart, brain, head) +// Disable Life: BLOOD +// Body Temp +// Spend blood over time (more if imitating life) (none if sleeping in coffin) +// Auto-Heal (brute to 0, fire to 99) (toxin/o2 always 0) +// +// Hud Icons +// UI Blood Counter +// Examine Name (+Masquerade, only "Dead and lifeless" if not standing?) +// +// +// Turn vamps +// Create vassals +// + + +// FIX LIST +// + + +///////////////////////////////////// + + // HUD! // + +/datum/antagonist/bloodsucker/proc/update_bloodsucker_icons_added(datum/mind/m) + var/datum/atom_hud/antag/vamphud = GLOB.huds[ANTAG_HUD_BLOODSUCKER] + vamphud.join_hud(owner.current) + set_antag_hud(owner.current, "bloodsucker") // "bloodsucker" + owner.current.hud_list[ANTAG_HUD].icon = image('icons/mob/hud.dmi', owner.current, "bloodsucker") //Check prepare_huds in mob.dm to see why. + +/datum/antagonist/bloodsucker/proc/update_bloodsucker_icons_removed(datum/mind/m) + var/datum/atom_hud/antag/vamphud = GLOB.huds[ANTAG_HUD_BLOODSUCKER] + vamphud.leave_hud(owner.current) + set_antag_hud(owner.current, null) + +/datum/atom_hud/antag/bloodsucker // from hud.dm in /datums/ Also see data_huds.dm + antag_hud.dm + +/datum/atom_hud/antag/bloodsucker/add_to_single_hud(mob/M, atom/A) + if (!check_valid_hud_user(M,A)) // FULP: This checks if the Mob is a Vassal, and if the Atom is his master OR on his team. + return + ..() + +/datum/atom_hud/antag/bloodsucker/proc/check_valid_hud_user(mob/M, atom/A) // Remember: A is being added to M's hud. Because M's hud is a /antag/vassal hud, this means M is the vassal here. + // Ghost Admins always see Bloodsuckers/Vassals + if (isobserver(M)) + return TRUE + // GOAL: Vassals see their Master and his other Vassals. + // GOAL: Vassals can BE seen by their Bloodsucker and his other Vassals. + // GOAL: Bloodsuckers can see each other. + if (!M || !A || !ismob(A) || !M.mind)// || !A.mind) + return FALSE + var/mob/A_mob = A + if (!A_mob.mind) + return FALSE + // Find Datums: Bloodsucker + var/datum/antagonist/bloodsucker/atom_B = A_mob.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + var/datum/antagonist/bloodsucker/mob_B = M.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + // Check 1) Are we both Bloodsuckers? + if (atom_B && mob_B) + return TRUE + // Find Datums: Vassal + var/datum/antagonist/vassal/atom_V = A_mob.mind.has_antag_datum(ANTAG_DATUM_VASSAL) + var/datum/antagonist/vassal/mob_V = M.mind.has_antag_datum(ANTAG_DATUM_VASSAL) + // Check 2) If they are a BLOODSUCKER, then are they my Master? + if (mob_V && atom_B == mob_V.master) + return TRUE // SUCCESS! + // Check 3) If I am a BLOODSUCKER, then are they my Vassal? + if (mob_B && atom_V && (atom_V in mob_B.vassals)) + return TRUE // SUCCESS! + // Check 4) If we are both VASSAL, then do we have the same master? + if (atom_V && mob_V && atom_V.master == mob_V.master) + return TRUE // SUCCESS! + return FALSE + + + + ///////////////////////////////////// + + + // BLOOD COUNTER & RANK MARKER ! // + +#define ui_sunlight_display "WEST:6,CENTER-0:0" // 6 pixels to the right, zero tiles & 5 pixels DOWN. +#define ui_blood_display "WEST:6,CENTER-1:0" // 1 tile down +#define ui_vamprank_display "WEST:6,CENTER-2:-5" // 2 tiles down + +/datum/hud + var/obj/screen/bloodsucker/blood_counter/blood_display + var/obj/screen/bloodsucker/rank_counter/vamprank_display + var/obj/screen/bloodsucker/sunlight_counter/sunlight_display + +/datum/antagonist/bloodsucker/proc/add_hud() + return + +/datum/antagonist/bloodsucker/proc/remove_hud() + // No Hud? Get out. + if (!owner.current.hud_used) + return + owner.current.hud_used.blood_display.invisibility = INVISIBILITY_ABSTRACT + owner.current.hud_used.vamprank_display.invisibility = INVISIBILITY_ABSTRACT + owner.current.hud_used.sunlight_display.invisibility = INVISIBILITY_ABSTRACT + +/datum/antagonist/bloodsucker/proc/update_hud(updateRank=FALSE) + // No Hud? Get out. + if(!owner.current.hud_used) + return + // Update Blood Counter + if (owner.current.hud_used.blood_display) + var/valuecolor = "#FF6666" + if(owner.current.blood_volume > BLOOD_VOLUME_SAFE) + valuecolor = "#FFDDDD" + else if(owner.current.blood_volume > BLOOD_VOLUME_BAD) + valuecolor = "#FFAAAA" + owner.current.hud_used.blood_display.update_counter(owner.current.blood_volume, valuecolor) + + // Update Rank Counter + if(owner.current.hud_used.vamprank_display) + var/valuecolor = vamplevel_unspent ? "#FFFF00" : "#FF0000" + owner.current.hud_used.vamprank_display.update_counter(vamplevel, valuecolor) + if(updateRank) // Only change icon on special request. + owner.current.hud_used.vamprank_display.icon_state = (vamplevel_unspent > 0) ? "rank_up" : "rank" + + +/obj/screen/bloodsucker + invisibility = INVISIBILITY_ABSTRACT + +/obj/screen/bloodsucker/proc/clear() + invisibility = INVISIBILITY_ABSTRACT + +/obj/screen/bloodsucker/proc/update_counter(value, valuecolor) + invisibility = 0 // Make Visible + +/obj/screen/bloodsucker/blood_counter // NOTE: Look up /obj/screen/devil/soul_counter in _onclick / hud / human.dm + icon = 'icons/mob/actions/bloodsucker.dmi'//'icons/mob/screen_gen.dmi' + name = "Blood Consumed" + icon_state = "blood_display"//"power_display" + screen_loc = ui_blood_display + +/obj/screen/bloodsucker/blood_counter/update_counter(value, valuecolor) + ..() + maptext = "
[round(value,1)]
" + +/obj/screen/bloodsucker/rank_counter + name = "Bloodsucker Rank" + icon = 'icons/mob/actions/bloodsucker.dmi' + icon_state = "rank" + screen_loc = ui_vamprank_display + +/obj/screen/bloodsucker/rank_counter/update_counter(value, valuecolor) + ..() + maptext = "
[round(value,1)]
" + +/obj/screen/bloodsucker/sunlight_counter + icon = 'icons/mob/actions/bloodsucker.dmi' + name = "Solar Flare Timer" + icon_state = "sunlight_night" + screen_loc = ui_sunlight_display + +/datum/antagonist/bloodsucker/proc/update_sunlight(value, amDay = FALSE) + // No Hud? Get out. + if (!owner.current.hud_used) + return + // Update Sun Time + if (owner.current.hud_used.sunlight_display) + var/valuecolor = "#BBBBFF" + if (amDay) + valuecolor = "#FF5555" + else if(value <= 25) + valuecolor = "#FFCCCC" + else if(value < 10) + valuecolor = "#FF5555" + var/value_string = (value >= 60) ? "[round(value / 60, 1)] m" : "[round(value,1)] s" + owner.current.hud_used.sunlight_display.update_counter( value_string, valuecolor ) + owner.current.hud_used.sunlight_display.icon_state = "sunlight_" + (amDay ? "day":"night") + + +/obj/screen/bloodsucker/sunlight_counter/update_counter(value, valuecolor) + ..() + maptext = "
[value]
" diff --git a/code/modules/antagonists/bloodsucker/datum_hunter.dm b/code/modules/antagonists/bloodsucker/datum_hunter.dm new file mode 100644 index 0000000000..2d680d0e09 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/datum_hunter.dm @@ -0,0 +1,295 @@ +/* +#define HUNTER_SCAN_MIN_DISTANCE 8 +#define HUNTER_SCAN_MAX_DISTANCE 35 +#define HUNTER_SCAN_PING_TIME 20 //5s update time. + + +/datum/antagonist/vamphunter + name = "Hunter" + roundend_category = "hunters" + antagpanel_category = "Monster Hunter" + job_rank = ROLE_MONSTERHUNTER + var/list/datum/action/powers = list()// Purchased powers + var/list/datum/objective/objectives_given = list() // For removal if needed. + var/datum/martial_art/my_kungfu // Hunters know a lil kung fu. + var/bad_dude = FALSE // Every first hunter spawned is a SHIT LORD. + + +/datum/antagonist/vamphunter/on_gain() + + SSticker.mode.vamphunters |= owner // Add if not already in here (and you might be, if you were picked at round start) + + // Hunter Pinpointer + //owner.current.apply_status_effect(/datum/status_effect/agent_pinpointer/hunter_edition) + + // Give Hunter Power + var/datum/action/P = new /datum/action/bloodsucker/trackvamp + P.Grant(owner.current) + + // Give Hunter Martial Arts + //if (rand(1,3) == 1) + // var/datum/martial_art/pick_type = pick (/datum/martial_art/cqc, /datum/martial_art/krav_maga, /datum/martial_art/cqc, /datum/martial_art/krav_maga, /datum/martial_art/wrestling) // /datum/martial_art/boxing <--- doesn't include grabbing, so don't use! + // my_kungfu = new pick_type //pick (/datum/martial_art/boxing, /datum/martial_art/cqc) // ick_type + // my_kungfu.teach(owner.current, 0) + + // Give Hunter Objective + var/datum/objective/bloodsucker/monsterhunter/monsterhunter_objective = new + monsterhunter_objective.owner = owner + monsterhunter_objective.generate_objective() + objectives += monsterhunter_objective + objectives_given += monsterhunter_objective + // Badguy Hunter? (Give him BADGUY objectives) + if (bad_dude) + // Stolen DIRECTLY from datum_traitor.dm + if(prob(15) && !(locate(/datum/objective/download) in objectives) && !(owner.assigned_role in list("Research Director", "Scientist", "Roboticist"))) + var/datum/objective/download/download_objective = new + download_objective.owner = owner + download_objective.gen_amount_goal() + objectives += download_objective + objectives_given += download_objective + else + var/datum/objective/steal/steal_objective = new + steal_objective.owner = owner + steal_objective.find_target() + objectives += steal_objective + objectives_given += steal_objective + + + . = ..() + +/datum/antagonist/vamphunter/on_removal() + + SSticker.mode.vamphunters -= owner // Add if not already in here (and you might be, if you were picked at round start) + + // Master Pinpointer + //owner.current.remove_status_effect(/datum/status_effect/agent_pinpointer/hunter_edition) + + // Take Hunter Power + if (owner.current) + for (var/datum/action/bloodsucker/P in owner.current.actions) + P.Remove(owner.current) + + // Take Hunter Martial Arts + //my_kungfu.remove(owner.current) + + // Remove Hunter Objectives + for(var/O in objectives_given) + objectives -= O + qdel(O) + objectives_given = list() + + . = ..() + +/datum/antagonist/vamphunter/greet() + var/vamp_hunter_greet = "You are a fearless Monster Hunter!" + vamp_hunter_greet += "You know there's one or more filthy creature onboard the station, though their identities elude you." + vamp_hunter_greet += "It's your job to root them out, destroy their nests, and save the crew." + vamp_hunter_greet += "Use WHATEVER MEANS NECESSARY to find these creatures...no matter who gets hurt or what you have to destroy to do it." + vamp_hunter_greet += "There are greater stakes at hand than the safety of the station!" + vamp_hunter_greet += "However, security may detain you if they discover your mission..." + antag_memory += "You remember your training:
" + antag_memory += " -Bloodsuckers are weak to fire, or a stake to the heart. Removing their head or heart will also destroy them permanently.
" + antag_memory += " -Wooden stakes can be made from planks, and hardened by a welding tool. Your recipes list has ways of making them even stronger.
" + antag_memory += " -Changelings return to life unless their body is destroyed. Not even decapitation can stop them for long.
" + antag_memory += " -Cultists are weak to the Chaplain's holy water.
" + antag_memory += " -Wizards are notoriously hard to outmatch. Rob or steal whatever weapons you need to destroy them, and shoot before asking questions.

" + if (my_kungfu != null) + vamp_hunter_greet += "Hunter Tip: Use your [my_kungfu.name] techniques to give you an advantage over the enemy.
" + antag_memory += "You remember your training: You are skilled in the [my_kungfu.name] style of combat.
" + to_chat(owner, vamp_hunter_greet) + +/datum/antagonist/vamphunter/farewell() + to_chat(owner, "Your hunt has ended: you are no longer a monster hunter!") + + +// TAKEN FROM: /datum/action/changeling/pheromone_receptors // pheromone_receptors.dm for a version of tracking that Changelings have! + + +/datum/status_effect/agent_pinpointer/hunter_edition + alert_type = /obj/screen/alert/status_effect/agent_pinpointer/hunter_edition + minimum_range = HUNTER_SCAN_MIN_DISTANCE + tick_interval = HUNTER_SCAN_PING_TIME + duration = 160 // Lasts 10s + range_fuzz_factor = 5//PINPOINTER_EXTRA_RANDOM_RANGE + +/obj/screen/alert/status_effect/agent_pinpointer/hunter_edition + name = "Monster Tracking" + desc = "You always know where the hellspawn are." + + +/datum/status_effect/agent_pinpointer/hunter_edition/on_creation(mob/living/new_owner, ...) + ..() + + // Pick target + var/turf/my_loc = get_turf(owner) + var/list/mob/living/carbon/vamps = list() + + for(var/datum/mind/M in SSticker.mode.bloodsuckers) + if (!M.current || M.current == owner || !get_turf(M.current) || !get_turf(new_owner)) + continue + var/datum/antagonist/bloodsucker/antag_datum = M.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(!istype(antag_datum)) + continue + var/their_loc = get_turf(M.current) + var/distance = get_dist_euclidian(my_loc, their_loc) + if (distance < HUNTER_SCAN_MAX_DISTANCE) + vamps[M.current] = (HUNTER_SCAN_MAX_DISTANCE ** 2) - (distance ** 2) + // Found one! + if(vamps.len) + scan_target = pickweight(vamps) //Point at a 'random' vamp, biasing heavily towards closer ones. + to_chat(owner, "You detect signs of monsters to the [dir2text(get_dir(my_loc,get_turf(scan_target)))]!") + // Will yield a "?" + else + to_chat(owner, "There are no monsters nearby.") + // Force Point-To Immediately + point_to_target() + +/datum/status_effect/agent_pinpointer/hunter_edition/scan_for_target() + // Lose target? Done. Otherwise, scan for target's current position. + if (!scan_target && owner) + owner.remove_status_effect(/datum/status_effect/agent_pinpointer/hunter_edition) + + // NOTE: Do NOT run ..(), or else we'll remove our target. + + +/datum/status_effect/agent_pinpointer/hunter_edition/Destroy() + if (scan_target) + to_chat(owner, "You've lost the trail.") + ..() +*/ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/* + + +/datum/action/bloodsucker/trackvamp/ + name = "Track Monster"//"Cellular Emporium" + desc = "Take a moment to look for clues of any nearby monsters.
These creatures are slippery, and often look like the crew." + button_icon = 'icons/mob/actions/bloodsucker.dmi' //This is the file for the BACKGROUND icon + background_icon_state = "vamp_power_off" //And this is the state for the background icon + icon_icon = 'icons/mob/actions/bloodsucker.dmi' //This is the file for the ACTION icon + button_icon_state = "power_hunter" //And this is the state for the action icon + amToggle = FALSE // Action-Related + cooldown = 300 // 10 ticks, 1 second. + bloodcost = 0 + +/datum/action/bloodsucker/trackvamp/ActivatePower() + + var/mob/living/user = owner + to_chat(user, "You look around, scanning your environment and discerning signs of any filthy, wretched affronts to the natural order.") + + if (!do_mob(user,owner,80)) + return + // Add Power + // REMOVED //user.apply_status_effect(/datum/status_effect/agent_pinpointer/hunter_edition) + // We don't track direction anymore! + // Return text indicating direction + display_proximity() + // NOTE: DON'T DEACTIVATE! + //DeactivatePower() + +/datum/action/bloodsucker/trackvamp/proc/display_proximity() + // Pick target + var/turf/my_loc = get_turf(owner) + //var/list/mob/living/carbon/vamps = list() + var/best_dist = 9999 + var/mob/living/best_vamp + + // Track ALL MONSTERS in Game Mode + var/list/datum/mind/monsters = list() + monsters += SSticker.mode.bloodsuckers + monsters += SSticker.mode.devils + //monsters += SSticker.mode.cult + monsters += SSticker.mode.wizards + monsters += SSticker.mode.apprentices + monsters += SSticker.mode.servants_of_ratvar + //monsters += SSticker.mode.changelings Disabled anyways + // + for(var/datum/mind/M in monsters) + if (!M.current || M.current == owner)// || !get_turf(M.current) || !get_turf(owner)) + continue + for(var/a in M.antag_datums) + var/datum/antagonist/antag_datum = a // var/datum/antagonist/antag_datum = M.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(!istype(antag_datum) || antag_datum.AmFinalDeath()) + continue + var/their_loc = get_turf(M.current) + var/distance = get_dist_euclidian(my_loc, their_loc) + // Found One: Closer than previous/max distance + if (distance < best_dist && distance <= HUNTER_SCAN_MAX_DISTANCE) + best_dist = distance + best_vamp = M.current + break // Stop searching through my antag datums and go to the next guy + + // Found one! + if(best_vamp) + var/distString = best_dist <= HUNTER_SCAN_MAX_DISTANCE / 2 ? "somewhere closeby!" : "somewhere in the distance." + //to_chat(owner, "You detect signs of Bloodsuckers to the [dir2text(get_dir(my_loc,get_turf(targetVamp)))]!") + to_chat(owner, "You detect signs of monsters [distString]") + + // Will yield a "?" + else + to_chat(owner, "There are no monsters nearby.") + + + + + + + + + + + + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// From martial.dm + +/* +/datum/martial_art/hunter + name = "Hunter-Fu" + id = "MARTIALART_HUNTER" //ID, used by mind/has_martialart + //streak = "" + //max_streak_length = 6 + //current_target + //datum/martial_art/base // The permanent style. This will be null unless the martial art is temporary + //deflection_chance = 0 //Chance to deflect projectiles + //reroute_deflection = FALSE //Delete the bullet, or actually deflect it in some direction? + //block_chance = 0 //Chance to block melee attacks using items while on throw mode. + //restraining = 0 //used in cqc's disarm_act to check if the disarmed is being restrained and so whether they should be put in a chokehold or not + //help_verb + //no_guns = FALSE + //allow_temp_override = TRUE //if this martial art can be overridden by temporary martial arts + +/datum/martial_art/hunter/disarm_act(mob/living/carbon/human/A, mob/living/carbon/human/D) + return FALSE + +/datum/martial_art/hunter/harm_act(mob/living/carbon/human/A, mob/living/carbon/human/D) + return FALSE + +/datum/martial_art/hunter/grab_act(mob/living/carbon/human/A, mob/living/carbon/human/D) + return FALSE + +/datum/martial_art/hunter/can_use(mob/living/carbon/human/H) + return TRUE + + +/datum/martial_art/hunter/add_to_streak(element,mob/living/carbon/human/D) + if(D != current_target) + current_target = D + streak = "" + restraining = 0 + streak = streak+element + if(length(streak) > max_streak_length) + streak = copytext(streak,2) + return + + +/datum/martial_art/hunter/basic_hit(mob/living/carbon/human/A,mob/living/carbon/human/D) + + var/damage = rand(A.dna.species.punchdamagelow, A.dna.species.punchdamagehigh) +*/ +*/ diff --git a/code/modules/antagonists/bloodsucker/datum_vassal.dm b/code/modules/antagonists/bloodsucker/datum_vassal.dm new file mode 100644 index 0000000000..71ee0bcc1d --- /dev/null +++ b/code/modules/antagonists/bloodsucker/datum_vassal.dm @@ -0,0 +1,151 @@ +#define VASSAL_SCAN_MIN_DISTANCE 5 +#define VASSAL_SCAN_MAX_DISTANCE 500 +#define VASSAL_SCAN_PING_TIME 20 //2s update time. + + +/datum/antagonist/bloodsucker/proc/attempt_turn_vassal(mob/living/carbon/C) + C.silent = 0 + return SSticker.mode.make_vassal(C,owner) + +/datum/antagonist/bloodsucker/proc/FreeAllVassals() + for (var/datum/antagonist/vassal/V in vassals) + SSticker.mode.remove_vassal(V.owner) + +/datum/antagonist/vassal + name = "Vassal"//WARNING: DO NOT SELECT" // "Vassal" + roundend_category = "vassals" + antagpanel_category = "Bloodsucker" + job_rank = ROLE_BLOODSUCKER + var/datum/antagonist/bloodsucker/master // Who made me? + var/list/datum/action/powers = list()// Purchased powers + var/list/datum/objective/objectives_given = list() // For removal if needed. + +/datum/antagonist/vassal/can_be_owned(datum/mind/new_owner) + // If we weren't created by a bloodsucker, then we cannot be a vassal (assigned from antag panel) + if (!master) + return FALSE + return ..() + +/datum/antagonist/vassal/on_gain() + SSticker.mode.vassals |= owner // Add if not already in here (and you might be, if you were picked at round start) + // Mindslave Add + if(master) + var/datum/antagonist/bloodsucker/B = master.owner.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(B) + B.vassals |= src + owner.enslave_mind_to_creator(master.owner.current) + // Master Pinpointer + owner.current.apply_status_effect(/datum/status_effect/agent_pinpointer/vassal_edition) + // Powers + var/datum/action/bloodsucker/vassal/recuperate/new_Recuperate = new () + new_Recuperate.Grant(owner.current) + powers += new_Recuperate + // Give Vassal Objective + var/datum/objective/bloodsucker/vassal/vassal_objective = new + vassal_objective.owner = owner + vassal_objective.generate_objective() + objectives += vassal_objective + objectives_given += vassal_objective + give_thrall_eyes() + owner.current.grant_language(/datum/language/vampiric) + // Add Antag HUD + update_vassal_icons_added(owner.current, "vassal") + . = ..() + +/datum/antagonist/vassal/proc/give_thrall_eyes() + var/obj/item/organ/eyes/vassal/E = new + E.Insert(owner.current) + +/obj/item/organ/eyes/vassal/ + lighting_alpha = 180 // LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE <--- This is too low a value at 128. We need to SEE what the darkness is so we can hide in it. + see_in_dark = 12 + flash_protect = -1 //These eyes are weaker to flashes, but let you see in the dark + +/datum/antagonist/vassal/proc/remove_thrall_eyes() + var/obj/item/organ/eyes/E = new + E.Insert(owner.current) + +/datum/antagonist/vassal/on_removal() + SSticker.mode.vassals -= owner // Add if not already in here (and you might be, if you were picked at round start) + // Mindslave Remove + if (master && master.owner) + master.vassals -= src + if (owner.enslaved_to == master.owner.current) + owner.enslaved_to = null + // Master Pinpointer + owner.current.remove_status_effect(/datum/status_effect/agent_pinpointer/vassal_edition) + // Powers + while(powers.len) + var/datum/action/power = pick(powers) + powers -= power + power.Remove(owner.current) + // Remove Hunter Objectives + for(var/O in objectives_given) + objectives -= O + qdel(O) + objectives_given = list() + remove_thrall_eyes() + owner.current.remove_language(/datum/language/vampiric) + // Clear Antag HUD + update_vassal_icons_removed(owner.current) + + . = ..() + +/datum/antagonist/vassal/greet() + to_chat(owner, "You are now the mortal servant of [master.owner.current], a bloodsucking vampire!") + to_chat(owner, "The power of [master.owner.current.p_their()] immortal blood compells you to obey [master.owner.current.p_them()] in all things, even offering your own life to prolong theirs.
\ + You are not required to obey any other Bloodsucker, for only [master.owner.current] is your master. The laws of Nanotransen do not apply to you now; only your vampiric master's word must be obeyed.") + // Effects... + owner.current.playsound_local(null, 'sound/magic/mutate.ogg', 100, FALSE, pressure_affected = FALSE) + //owner.store_memory("You became the mortal servant of [master.owner.current], a bloodsucking vampire!") + antag_memory += "You became the mortal servant of [master.owner.current], a bloodsucking vampire!
" + // And to your new Master... + to_chat(master.owner, "[owner.current] has become addicted to your immortal blood. [owner.current.p_they(TRUE)] [owner.current.p_are()] now your undying servant!") + master.owner.current.playsound_local(null, 'sound/magic/mutate.ogg', 100, FALSE, pressure_affected = FALSE) + +/datum/antagonist/vassal/farewell() + owner.current.visible_message("[owner.current]'s eyes dart feverishly from side to side, and then stop. [owner.current.p_they(TRUE)] seem[owner.current.p_s()] calm, \ + like [owner.current.p_they()] [owner.current.p_have()] regained some lost part of [owner.current.p_them()]self.",\ + "With a snap, you are no longer enslaved to [master.owner]! You breathe in heavily, having regained your free will.") + owner.current.playsound_local(null, 'sound/magic/mutate.ogg', 100, FALSE, pressure_affected = FALSE) + // And to your former Master... + //if (master && master.owner) + // to_chat(master.owner, "You feel the bond with your vassal [owner.current] has somehow been broken!") + +/datum/status_effect/agent_pinpointer/vassal_edition + id = "agent_pinpointer" + alert_type = /obj/screen/alert/status_effect/agent_pinpointer/vassal_edition + minimum_range = VASSAL_SCAN_MIN_DISTANCE + tick_interval = VASSAL_SCAN_PING_TIME + duration = -1 // runs out fast + range_fuzz_factor = 0 + +/obj/screen/alert/status_effect/agent_pinpointer/vassal_edition + name = "Blood Bond" + desc = "You always know where your master is." + //icon = 'icons/obj/device.dmi' + //icon_state = "pinon" + +/datum/status_effect/agent_pinpointer/vassal_edition/on_creation(mob/living/new_owner, ...) + ..() + + var/datum/antagonist/vassal/antag_datum = new_owner.mind.has_antag_datum(ANTAG_DATUM_VASSAL) + scan_target = antag_datum?.master?.owner?.current + +/datum/status_effect/agent_pinpointer/vassal_edition/scan_for_target() + // DO NOTHING. We already have our target, and don't wanna do anything from agent_pinpointer + +/datum/antagonist/vassal/proc/update_vassal_icons_added(mob/living/vassal, icontype="vassal") + var/datum/atom_hud/antag/bloodsucker/hud = GLOB.huds[ANTAG_HUD_BLOODSUCKER] + hud.join_hud(vassal) + set_antag_hud(vassal, icontype) // Located in icons/mob/hud.dmi + owner.current.hud_list[ANTAG_HUD].icon = image('icons/mob/hud.dmi', owner.current, "bloodsucker") + +/datum/antagonist/vassal/proc/update_vassal_icons_removed(mob/living/vassal) + var/datum/atom_hud/antag/hud = GLOB.huds[ANTAG_HUD_BLOODSUCKER] + hud.leave_hud(vassal) + set_antag_hud(vassal, null) + +//Displayed at the start of roundend_category section, default to roundend_category header +/*/datum/antagonist/vassal/roundend_report_header() + return "Loyal to their bloodsucking masters, the Vassals were:

"*/ diff --git a/code/modules/antagonists/bloodsucker/items/bloodsucker_organs.dm b/code/modules/antagonists/bloodsucker/items/bloodsucker_organs.dm new file mode 100644 index 0000000000..5a905857c5 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/items/bloodsucker_organs.dm @@ -0,0 +1,70 @@ + + +/datum/antagonist/bloodsucker/proc/CheckVampOrgans() + // Do I have any parts that need replacing? + var/obj/item/organ/O + // Heart + O = owner.current.getorganslot(ORGAN_SLOT_HEART) + if(!istype(O, /obj/item/organ/heart/vampheart)) + qdel(O) + var/obj/item/organ/heart/vampheart/H = new + H.Insert(owner.current) + H.Stop() // Now...stop beating! + // Eyes + O = owner.current.getorganslot(ORGAN_SLOT_EYES) + if(!istype(O, /obj/item/organ/eyes/vassal/bloodsucker)) + qdel(O) + var/obj/item/organ/eyes/vassal/bloodsucker/E = new + E.Insert(owner.current) + +/datum/antagonist/bloodsucker/proc/RemoveVampOrgans() + // Heart + var/obj/item/organ/heart/H = new + H.Insert(owner.current) + // Eyes + var/obj/item/organ/eyes/E = new + E.Insert(owner.current) +// HEART: OVERWRITE // +// HEART // +/obj/item/organ/heart/vampheart + beating = 0 + var/fakingit = 0 + +/obj/item/organ/heart/vampheart/prepare_eat() + ..() + // Do cool stuff for eating vamp heart? + +/obj/item/organ/heart/vampheart/Restart() + beating = 0 // DONT run ..(). We don't want to start beating again. + return 0 + +/obj/item/organ/heart/vampheart/Stop() + fakingit = 0 + return ..() + +/obj/item/organ/heart/vampheart/proc/FakeStart() + fakingit = 1 // We're pretending to beat, to fool people. + +/obj/item/organ/heart/vampheart/HeartStrengthMessage() + if(fakingit) + return "a healthy" + return "no" // Bloodsuckers don't have a heartbeat at all when stopped (default is "an unstable") +// EYES // + +/obj/item/organ/eyes/vassal/bloodsucker + flash_protect = 2 //Eye healing isnt working properly + sight_flags = SEE_MOBS // Taken from augmented_eyesight.dm + +/* +// LIVER // +/obj/item/organ/liver/vampliver + // Livers run on_life(), which calls reagents.metabolize() in holder.dm, which calls on_mob_life.dm in the cheam (medicine_reagents.dm) + // Holder also calls reagents.reaction_mob for the moment it happens + +/obj/item/organ/liver/vampliver/on_life() + var/mob/living/carbon/C = owner + + if(!istype(C)) + return + +*/ diff --git a/code/modules/antagonists/bloodsucker/items/bloodsucker_stake.dm b/code/modules/antagonists/bloodsucker/items/bloodsucker_stake.dm new file mode 100644 index 0000000000..9db4cae1ff --- /dev/null +++ b/code/modules/antagonists/bloodsucker/items/bloodsucker_stake.dm @@ -0,0 +1,176 @@ + + +// organ_internal.dm -- /obj/item/organ + + + +// Do I have a stake in my heart? +/mob/living/AmStaked() + var/obj/item/bodypart/BP = get_bodypart("chest") + if (!BP) + return FALSE + for(var/obj/item/I in BP.embedded_objects) + if (istype(I,/obj/item/stake/)) + return TRUE + return FALSE +/mob/proc/AmStaked() + return FALSE + + +/mob/living/proc/StakeCanKillMe() + return IsSleeping() || stat >= UNCONSCIOUS || blood_volume <= 0 || HAS_TRAIT(src, TRAIT_DEATHCOMA) // NOTE: You can't go to sleep in a coffin with a stake in you. + + +///obj/item/weapon/melee/stake +/obj/item/stake/ + name = "wooden stake" + desc = "A simple wooden stake carved to a sharp point." + icon = 'icons/obj/items_and_weapons.dmi' + icon_state = "wood" // Inventory Icon + item_state = "wood" // In-hand Icon + lefthand_file = 'icons/mob/inhands/weapons/melee_lefthand.dmi' // File for in-hand icon + righthand_file = 'icons/mob/inhands/weapons/melee_righthand.dmi' + attack_verb = list("staked") + slot_flags = ITEM_SLOT_BELT + w_class = WEIGHT_CLASS_SMALL + hitsound = 'sound/weapons/bladeslice.ogg' + force = 6 + throwforce = 10 + embedding = list("embed_chance" = 25, "embedded_fall_chance" = 0.5) // UPDATE 2/10/18 embedding_behavior.dm is how this is handled + //embed_chance = 25 // Look up "is_pointed" to see where we set stakes able to do this. + //embedded_fall_chance = 0.5 // Chance it will fall out. + obj_integrity = 30 + max_integrity = 30 + //embedded_fall_pain_multiplier + var/staketime = 120 // Time it takes to embed the stake into someone's chest. + +/obj/item/stake/basic + name = "wooden stake" + // This exists so Hardened/Silver Stake can't have a welding torch used on them. + +/obj/item/stake/basic/attackby(obj/item/W, mob/user, params) + if(istype(W, /obj/item/weldingtool)) + //if (amWelded) + // to_chat(user, "This stake has already been treated with fire.") + // return + //amWelded = TRUE + // Weld it + var/obj/item/weldingtool/WT = W + if(WT.use(0))//remove_fuel(0,user)) + user.visible_message("[user.name] scorched the pointy end of [src] with the welding tool.", \ + "You scorch the pointy end of [src] with the welding tool.", \ + "You hear welding.") + // 8 Second Timer + if(!do_mob(user, src, 80)) + return + // Create the Stake + qdel(src) + var/obj/item/stake/hardened/new_item = new(usr.loc) + user.put_in_hands(new_item) + else + return ..() + +/obj/item/stake/afterattack(atom/target, mob/user, proximity) + //to_chat(world, "DEBUG: Staking ") + // Invalid Target, or not targetting chest with HARM intent? + if(!iscarbon(target) || check_zone(user.zone_selected) != "chest" || user.a_intent != INTENT_HARM) + return + var/mob/living/carbon/C = target + // Needs to be Down/Slipped in some way to Stake. + if(!C.can_be_staked() || target == user) + to_chat(user, "You cant stake [target] when they are moving moving about! They have to be laying down!") + return + // Oops! Can't. + if(HAS_TRAIT(C, TRAIT_PIERCEIMMUNE)) + to_chat(user, "[target]'s chest resists the stake. It won't go in.") + return + // Make Attempt... + to_chat(user, "You put all your weight into embedding the stake into [target]'s chest...") + playsound(user, 'sound/magic/Demon_consume.ogg', 50, 1) + if(!do_mob(user, C, staketime, 0, 1, extra_checks=CALLBACK(C, /mob/living/carbon/proc/can_be_staked))) // user / target / time / uninterruptable / show progress bar / extra checks + return + // Drop & Embed Stake + user.visible_message("[user.name] drives the [src] into [target]'s chest!", \ + "You drive the [src] into [target]'s chest!") + playsound(get_turf(target), 'sound/effects/splat.ogg', 40, 1) + user.dropItemToGround(src, TRUE) //user.drop_item() // "drop item" doesn't seem to exist anymore. New proc is user.dropItemToGround() but it doesn't seem like it's needed now? + var/obj/item/bodypart/B = C.get_bodypart("chest") // This was all taken from hitby() in human_defense.dm + B.embedded_objects |= src + add_mob_blood(target)//Place blood on the stake + loc = C // Put INSIDE the character + B.receive_damage(w_class * embedding.embedded_impact_pain_multiplier) + if(C.mind) + var/datum/antagonist/bloodsucker/bloodsucker = C.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(bloodsucker) + // If DEAD or TORPID...kill vamp! + if(C.StakeCanKillMe()) // NOTE: This is the ONLY time a staked Torpid vamp dies. + bloodsucker.FinalDeath() + return + else + to_chat(target, "You have been staked! Your powers are useless, your death forever, while it remains in place.") + to_chat(user, "You missed [C.p_their(TRUE)]'s heart! It would be easier if [C.p_they(TRUE)] weren't struggling so much.") + +// Can this target be staked? If someone stands up before this is complete, it fails. Best used on someone stationary. +/mob/living/carbon/proc/can_be_staked() + //return resting || IsKnockdown() || IsUnconscious() || (stat && (stat != SOFT_CRIT || pulledby)) || (has_trait(TRAIT_FAKEDEATH)) || resting || IsStun() || IsFrozen() || (pulledby && pulledby.grab_state >= GRAB_NECK) + return (src.resting || src.lying) + // ABOVE: Taken from update_mobility() in living.dm + +/obj/item/stake/hardened + // Created by welding and acid-treating a simple stake. + name = "hardened stake" + desc = "A hardened wooden stake carved to a sharp point and scorched at the end." + icon_state = "hardened" // Inventory Icon + force = 8 + throwforce = 12 + armour_penetration = 10 + embedding = list("embed_chance" = 50, "embedded_fall_chance" = 0) // UPDATE 2/10/18 embedding_behavior.dm is how this is handled + obj_integrity = 120 + max_integrity = 120 + + staketime = 80 + +/obj/item/stake/hardened/silver + name = "silver stake" + desc = "Polished and sharp at the end. For when some mofo is always trying to iceskate uphill." + icon_state = "silver" // Inventory Icon + item_state = "silver" // In-hand Icon + siemens_coefficient = 1 //flags = CONDUCT // var/siemens_coefficient = 1 // for electrical admittance/conductance (electrocution checks and shit) + force = 9 + armour_penetration = 25 + embedding = list("embed_chance" = 65) // UPDATE 2/10/18 embedding_behavior.dm is how this is handled + obj_integrity = 300 + max_integrity = 300 + + staketime = 60 + +// Convert back to Silver +/obj/item/stake/hardened/silver/attackby(obj/item/I, mob/user, params) + if(istype(I, /obj/item/weldingtool)) + var/obj/item/weldingtool/WT = I + if(WT.use(0))//remove_fuel(0, user)) + var/obj/item/stack/sheet/mineral/silver/newsheet = new (user.loc) + for(var/obj/item/stack/sheet/mineral/silver/S in user.loc) + if(S == newsheet) + continue + if(S.amount >= S.max_amount) + continue + S.attackby(newsheet, user) + to_chat(user, "You melt down the stake and add it to the stack. It now contains [newsheet.amount] sheet\s.") + qdel(src) + else + return ..() + + +// Look up recipes.dm OR pneumaticCannon.dm +/datum/crafting_recipe/silver_stake + name = "Silver Stake" + result = /obj/item/stake/hardened/silver + tools = list(/obj/item/weldingtool) + reqs = list(/obj/item/stack/sheet/mineral/silver = 1, + /obj/item/stake/hardened = 1) + ///obj/item/stack/packageWrap = 8, + ///obj/item/pipe = 2) + time = 80 + category = CAT_WEAPONRY + subcategory = CAT_WEAPON diff --git a/code/modules/antagonists/bloodsucker/objects/bloodsucker_coffin.dm b/code/modules/antagonists/bloodsucker/objects/bloodsucker_coffin.dm new file mode 100644 index 0000000000..946c472f11 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/objects/bloodsucker_coffin.dm @@ -0,0 +1,242 @@ + + +// TRAIT_DEATHCOMA - Activate this when you're in your coffin to simulate sleep/death. + + +// Coffins... +// -heal all wounds, and quickly. +// -restore limbs & organs +// + +// Without Coffins... +// - +// -limbs stay lost + + + +// To put to sleep: use owner.current.fakedeath("bloodsucker") but change name to "bloodsucker_coffin" so you continue to stay fakedeath despite healing in the main thread! + + +/datum/antagonist/bloodsucker/proc/ClaimCoffin(obj/structure/closet/crate/claimed) // NOTE: This can be any "closet" that you are resting AND inside of. + // ALREADY CLAIMED + if(claimed.resident) + if(claimed.resident == owner.current) + to_chat(owner, "This is your [src].") + else + to_chat(owner, "This [src] has already been claimed by another.") + return FALSE + // Bloodsucker Learns new Recipes! + owner.teach_crafting_recipe(/datum/crafting_recipe/bloodsucker/vassalrack) + owner.teach_crafting_recipe(/datum/crafting_recipe/bloodsucker/candelabrum) + // This is my Lair + coffin = claimed + lair = get_area(claimed) + // DONE + to_chat(owner, "You have claimed the [claimed] as your place of immortal rest! Your lair is now [lair].") + to_chat(owner, "You have learned new construction recipes to improve your lair.") + to_chat(owner, "Bloodsucker Tip: Find new lair recipes in the misc tab of the Crafting Menu at the bottom of the screen, including the Persuasion Rack for converting crew into Vassals.

") + RunLair() // Start + return TRUE + +// crate.dm +/obj/structure/closet/crate + var/mob/living/resident // This lets bloodsuckers claim any "closet" as a Coffin, so long as they could get into it and close it. This locks it in place, too. + +/obj/structure/closet/crate/coffin + var/pryLidTimer = 250 + can_weld_shut = FALSE + breakout_time = 200 + + +/obj/structure/closet/crate/coffin/blackcoffin + name = "black coffin" + desc = "For those departed who are not so dear." + icon_state = "coffin" + icon = 'icons/obj/vamp_obj.dmi' + can_weld_shut = FALSE + resistance_flags = 0 // Start off with no bonuses. + open_sound = 'sound/bloodsucker/coffin_open.ogg' + close_sound = 'sound/bloodsucker/coffin_close.ogg' + breakout_time = 600 + pryLidTimer = 400 + resistance_flags = NONE + +/obj/structure/closet/crate/coffin/meatcoffin + name = "meat coffin" + desc = "When you're ready to meat your maker, the steaks can never be too high." + icon_state = "meatcoffin" + icon = 'icons/obj/vamp_obj.dmi' + can_weld_shut = FALSE + resistance_flags = 0 // Start off with no bonuses. + open_sound = 'sound/effects/footstep/slime1.ogg' + close_sound = 'sound/effects/footstep/slime1.ogg' + breakout_time = 200 + pryLidTimer = 200 + resistance_flags = NONE + material_drop = /obj/item/reagent_containers/food/snacks/meat/slab + material_drop_amount = 3 + +/obj/structure/closet/crate/coffin/metalcoffin + name = "metal coffin" + desc = "A big metal sardine can inside of another big metal sardine can, in space." + icon_state = "metalcoffin" + icon = 'icons/obj/vamp_obj.dmi' + can_weld_shut = FALSE + resistance_flags = FIRE_PROOF | LAVA_PROOF + open_sound = 'sound/effects/pressureplate.ogg' + close_sound = 'sound/effects/pressureplate.ogg' + breakout_time = 300 + pryLidTimer = 200 + resistance_flags = NONE + material_drop = /obj/item/stack/sheet/metal + material_drop_amount = 5 + +////////////////////////////////////////////// + +/obj/structure/closet/crate/proc/ClaimCoffin(mob/living/claimant) // NOTE: This can be any "closet" that you are resting AND inside of. + // Bloodsucker Claim + var/datum/antagonist/bloodsucker/bloodsuckerdatum = claimant.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(bloodsuckerdatum) + // Vamp Successfuly Claims Me? + if(bloodsuckerdatum.ClaimCoffin(src)) + resident = claimant + anchored = 1 // No moving this + +/obj/structure/closet/crate/coffin/Destroy() + UnclaimCoffin() + return ..() + +/obj/structure/closet/crate/proc/UnclaimCoffin() + if (resident) + // Vamp Un-Claim + if (resident.mind) + var/datum/antagonist/bloodsucker/bloodsuckerdatum = resident.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if (bloodsuckerdatum && bloodsuckerdatum.coffin == src) + bloodsuckerdatum.coffin = null + bloodsuckerdatum.lair = null + to_chat(resident, "You sense that the link with your coffin, your sacred place of rest, has been brokem! You will need to seek another.") + resident = null // Remove resident. Because this object isnt removed from the game immediately (GC?) we need to give them a way to see they don't have a home anymore. + +/obj/structure/closet/crate/coffin/can_open(mob/living/user) + // You cannot lock in/out a coffin's owner. SORRY. + if (locked) + if(user == resident) + if (welded) + welded = FALSE + update_icon() + //to_chat(user, "You flip a secret latch and unlock [src].") // Don't bother. We know it's unlocked. + locked = FALSE + return 1 + else + playsound(get_turf(src), 'sound/machines/door_locked.ogg', 20, 1) + to_chat(user, "[src] is locked tight from the inside.") + return ..() + +/obj/structure/closet/crate/coffin/close(mob/living/user) + var/turf/Turf = get_turf(src) + var/area/A = get_area(src) + if (!..()) + return FALSE + // Only the User can put themself into Torpor (if you're already in it, you'll start to heal) + if((user in src)) + // Bloodsucker Only + var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(bloodsuckerdatum) + LockMe(user) + Turf = get_turf(user) //we may have moved. adjust as needed... + A = get_area(src) + // Claim? + if(!bloodsuckerdatum.coffin && !resident && (is_station_level(Turf.z) || !A.map_name == "Space")) + switch(alert(user,"Do you wish to claim this as your coffin? [get_area(src)] will be your lair.","Claim Lair","Yes", "No")) + if("Yes") + ClaimCoffin(user) + if (user.AmStaked()) // Stake? No Heal! + to_chat(bloodsuckerdatum.owner.current, "You are staked! Remove the offending weapon from your heart before sleeping.") + return + // Heal + if(bloodsuckerdatum.HandleHealing(0)) // Healing Mult 0 <--- We only want to check if healing is valid! + to_chat(bloodsuckerdatum.owner.current, "You enter the horrible slumber of deathless Torpor. You will heal until you are renewed.") + bloodsuckerdatum.Torpor_Begin() + // Level Up? + bloodsuckerdatum.SpendRank() // Auto-Fails if not appropriate + return TRUE + +/obj/structure/closet/crate/coffin/attackby(obj/item/W, mob/user, params) + // You cannot weld or deconstruct an owned coffin. STILL NOT SORRY. + if (resident != null && user != resident) // Owner can destroy their own coffin. + if(opened) + if(istype(W, cutting_tool)) + to_chat(user, "This is a much more complex mechanical structure than you thought. You don't know where to begin cutting [src].") + return + else if(anchored && istype(W, /obj/item/wrench)) // Can't unanchor unless owner. + to_chat(user, "The coffin won't come unanchored from the floor.") + return + + if(locked && istype(W, /obj/item/crowbar)) + var/pry_time = pryLidTimer * W.toolspeed // Pry speed must be affected by the speed of the tool. + user.visible_message("[user] tries to pry the lid off of [src] with [W].", \ + "You begin prying the lid off of [src] with [W]. This should take about [DisplayTimeText(pry_time)].") + if (!do_mob(user,src,pry_time)) + return + bust_open() + user.visible_message("[user] snaps the door of [src] wide open.", \ + "The door of [src] snaps open.") + return + ..() + + + +/obj/structure/closet/crate/coffin/AltClick(mob/user) + // Distance Check (Inside Of) + if (user in src) // user.Adjacent(src) + LockMe(user, !locked) + +/obj/structure/closet/crate/proc/LockMe(mob/user, inLocked = TRUE) + // Lock + if (user == resident) + if (!broken) + locked = inLocked + to_chat(user, "You flip a secret latch and [locked?"":"un"]lock yourself inside [src].") + else + to_chat(resident, "The secret latch to lock [src] from the inside is broken. You set it back into place...") + if (do_mob(resident, src, 50))//sleep(10) + if (broken) // Spam Safety + to_chat(resident, "You fix the mechanism and lock it.") + broken = FALSE + locked = TRUE + +// Look up recipes.dm OR pneumaticCannon.dm +/datum/crafting_recipe/bloodsucker/blackcoffin + name = "Black Coffin" + result = /obj/structure/closet/crate/coffin/blackcoffin + tools = list(/obj/item/weldingtool, + /obj/item/screwdriver) + reqs = list(/obj/item/stack/sheet/cloth = 1, + /obj/item/stack/sheet/mineral/wood = 5, + /obj/item/stack/sheet/metal = 1) + ///obj/item/stack/packageWrap = 8, + ///obj/item/pipe = 2) + time = 150 + category = CAT_MISC + always_availible = TRUE + +/datum/crafting_recipe/bloodsucker/meatcoffin + name = "Meat Coffin" + result =/obj/structure/closet/crate/coffin/meatcoffin + tools = list(/obj/item/kitchen/knife, + /obj/item/kitchen/rollingpin) + reqs = list(/obj/item/reagent_containers/food/snacks/meat/slab = 5, + /obj/item/restraints/handcuffs/cable = 1) + time = 150 + category = CAT_MISC + always_availible = TRUE + +/datum/crafting_recipe/bloodsucker/metalcoffin + name = "Metal Coffin" + result =/obj/structure/closet/crate/coffin/metalcoffin + tools = list(/obj/item/weldingtool, + /obj/item/screwdriver) + reqs = list(/obj/item/stack/sheet/metal = 5) + time = 100 + category = CAT_MISC + always_availible = TRUE diff --git a/code/modules/antagonists/bloodsucker/objects/bloodsucker_crypt.dm b/code/modules/antagonists/bloodsucker/objects/bloodsucker_crypt.dm new file mode 100644 index 0000000000..f73912d0f5 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/objects/bloodsucker_crypt.dm @@ -0,0 +1,497 @@ + + + +// IDEAS -- +// An object that disguises your coffin while you're in it! +// +// An object that lets your lair itself protect you from sunlight, like a coffin would (no healing tho) + + + +// Hide a random object somewhere on the station: +// var/turf/targetturf = get_random_station_turf() +// var/turf/targetturf = get_safe_random_station_turf() + + + + +// CRYPT OBJECTS +// +// +// PODIUM Stores your Relics +// +// ALTAR Transmute items into sacred items. +// +// PORTRAIT Gaze into your past to: restore mood boost? +// +// BOOKSHELF Discover secrets about crew and locations. Learn languages. Learn marial arts. +// +// BRAZER Burn rare ingredients to gleen insights. +// +// RUG Ornate, and creaks when stepped upon by any humanoid other than yourself and your vassals. +// +// X COFFIN (Handled elsewhere) +// +// X CANDELABRA (Handled elsewhere) +// +// THRONE Your mental powers work at any range on anyone inside your crypt. +// +// MIRROR Find any person +// +// BUST/STATUE Create terror, but looks just like you (maybe just in Examine?) + + +// RELICS +// +// RITUAL DAGGER +// +// SKULL +// +// VAMPIRIC SCROLL +// +// SAINTS BONES +// +// GRIMOIRE + + +// RARE INGREDIENTS +// Ore +// Books (Manuals) + + +// NOTE: Look up AI and Sentient Disease to see how the game handles the selector logo that only one player is allowed to see. We could add hud for vamps to that? +// ALTERNATIVELY, use the Vamp Huds on relics to mark them, but only show to relevant vamps? + + +/obj/structure/bloodsucker + var/mob/living/owner + +/* +/obj/structure/bloodsucker/bloodthrone + name = "wicked throne" + desc = "Twisted metal shards jut from the arm rests. Very uncomfortable looking. It would take a sadistic sort to sit on this jagged piece of furniture." + +/obj/structure/bloodsucker/bloodaltar + name = "bloody altar" + desc = "It is marble, lined with basalt, and radiates an unnerving chill that puts your skin on edge." + +/obj/structure/bloodsucker/bloodstatue + name = "bloody countenance" + desc = "It looks upsettingly familiar..." + +/obj/structure/bloodsucker/bloodportrait + name = "oil portrait" + desc = "A disturbingly familiar face stares back at you. On second thought, the reds don't seem to be painted in oil..." + +/obj/structure/bloodsucker/bloodbrazer + name = "lit brazer" + desc = "It burns slowly, but doesn't radiate any heat." + +/obj/structure/bloodsucker/bloodmirror + name = "faded mirror" + desc = "You get the sense that the foggy reflection looking back at you has an alien intelligence to it." +*/ + + +/obj/structure/bloodsucker/vassalrack + name = "persuasion rack" + desc = "If this wasn't meant for torture, then someone has some fairly horrifying hobbies." + icon = 'icons/obj/vamp_obj.dmi' + icon_state = "vassalrack" + buckle_lying = FALSE + anchored = FALSE + density = TRUE // Start dense. Once fixed in place, go non-dense. + can_buckle = TRUE + var/useLock = FALSE // So we can't just keep dragging ppl on here. + var/mob/buckled + var/convert_progress = 3 // Resets on each new character to be added to the chair. Some effects should lower it... + var/disloyalty_confirm = FALSE // Command & Antags need to CONFIRM they are willing to lose their role (and will only do it if the Vassal'ing succeeds) + var/disloyalty_offered = FALSE // Has the popup been issued? Don't spam them. + var/convert_cost = 100 + +/obj/structure/bloodsucker/vassalrack/deconstruct(disassembled = TRUE) + new /obj/item/stack/sheet/metal(src.loc, 4) + new /obj/item/stack/rods(loc, 4) + qdel(src) + +/obj/structure/bloodsucker/vassalrack/examine(mob/user) + . = ..() + if((user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) || isobserver(user)) + . += {"This is the vassal rack, which allows you to thrall crewmembers into loyal minions in your service."} + . += {"You need to first secure the vassal rack by clicking on it while it is in your lair."} + . += {"Simply click and hold on a victim, and then drag their sprite on the vassal rack."} + . += {"Make sure that the victim is handcuffed, or else they can simply run away or resist, as the process is not instant."} + . += {"To convert the victim, simply click on the vassal rack itself. Sharp weapons work faster than other tools."} +/* if(user.mind.has_antag_datum(ANTAG_DATUM_VASSAL) + . += {"This is the vassal rack, which allows your master to thrall crewmembers into his minions.\n + Aid your master in bringing their victims here and keeping them secure.\n + You can secure victims to the vassal rack by click dragging the victim onto the rack while it is secured"} */ + +/obj/structure/bloodsucker/vassalrack/MouseDrop_T(atom/movable/O, mob/user) + if(!O.Adjacent(src) || O == user || !isliving(O) || !isliving(user) || useLock || has_buckled_mobs() || user.incapacitated()) + return + if(!anchored && user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) + to_chat(user, "Until this rack is secured in place, it cannot serve its purpose.") + return + // PULL TARGET: Remember if I was pullin this guy, so we can restore this + var/waspulling = (O == owner.pulling) + var/wasgrabstate = owner.grab_state + // * MOVE! * + O.forceMove(drop_location()) + // PULL TARGET: Restore? + if(waspulling) + owner.start_pulling(O, wasgrabstate, TRUE) + // NOTE: in bs_lunge.dm, we use [target.grabbedby(owner)], which simulates doing a grab action. We don't want that though...we're cutting directly back to where we were in a grab. + // Do Action! + useLock = TRUE + if(do_mob(user, O, 50)) + attach_victim(O,user) + useLock = FALSE + +/obj/structure/bloodsucker/vassalrack/AltClick(mob/user) + if(!has_buckled_mobs() || !isliving(user) || useLock) + return + // Attempt Release (Owner vs Non Owner) + var/mob/living/carbon/C = pick(buckled_mobs) + if(C) + if(user == owner) + unbuckle_mob(C) + else + user_unbuckle_mob(C,user) + +/obj/structure/bloodsucker/vassalrack/proc/attach_victim(mob/living/M, mob/living/user) + // Standard Buckle Check + if(!buckle_mob(M)) // force=TRUE)) + return + // Attempt Buckle + user.visible_message("[user] straps [M] into the rack, immobilizing them.", \ + "You secure [M] tightly in place. They won't escape you now.") + + playsound(src.loc, 'sound/effects/pop_expl.ogg', 25, 1) + //M.forceMove(drop_location()) <--- CANT DO! This cancels the buckle_mob() we JUST did (even if we foced the move) + M.setDir(2) + density = TRUE + var/matrix/m180 = matrix(M.transform) + m180.Turn(180)//90)//180 + animate(M, transform = m180, time = 2) + M.pixel_y = -2 //M.get_standard_pixel_y_offset(120)//180) + update_icon() + // Torture Stuff + convert_progress = 2 // Goes down unless you start over. + disloyalty_confirm = FALSE // New guy gets the chance to say NO if he's special. + disloyalty_offered = FALSE // Prevents spamming torture window. + +/obj/structure/bloodsucker/vassalrack/user_unbuckle_mob(mob/living/M, mob/user) + // Attempt Unbuckle + if(!user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) + if(M == user) + M.visible_message("[user] tries to release themself from the rack!",\ + "You attempt to release yourself from the rack!") // For sound if not seen --> "You hear a squishy wet noise.") + else + M.visible_message("[user] tries to pull [M] rack!",\ + "[user] attempts to release you from the rack!") // For sound if not seen --> "You hear a squishy wet noise.") + if(!do_mob(user, M, 100)) + return + // Did the time. Now try to do it. + ..() + unbuckle_mob(M) + +/obj/structure/bloodsucker/vassalrack/unbuckle_mob(mob/living/buckled_mob, force = FALSE) + if(!..()) + return + var/matrix/m180 = matrix(buckled_mob.transform) + m180.Turn(180)//-90)//180 + animate(buckled_mob, transform = m180, time = 2) + buckled_mob.pixel_y = buckled_mob.get_standard_pixel_y_offset(180) + src.visible_message(text("[buckled_mob][buckled_mob.stat==DEAD?"'s corpse":""] slides off of the rack.")) + density = FALSE + buckled_mob.AdjustKnockdown(30) + update_icon() + useLock = FALSE // Failsafe + +/obj/structure/bloodsucker/vassalrack/attackby(obj/item/W, mob/user, params) + if(has_buckled_mobs()) // Attack w/weapon vs guy standing there? Don't do an attack. + attack_hand(user) + return FALSE + return ..() + +/obj/structure/bloodsucker/vassalrack/attack_hand(mob/user) + //. = ..() // Taken from sacrificial altar in divine.dm + //if(.) + // return + // Go away. Torturing. + if(useLock) + return + var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + // CHECK ONE: Am I claiming this? Is it in the right place? + if(istype(bloodsuckerdatum) && !owner) + if(!bloodsuckerdatum.lair) + to_chat(user, "You don't have a lair. Claim a coffin to make that location your lair.") + if(bloodsuckerdatum.lair != get_area(src)) + to_chat(user, "You may only activate this structure in your lair: [bloodsuckerdatum.lair].") + return + switch(alert(user,"Do you wish to afix this structure here? Be aware you wont be able to unsecure it anymore","Secure [src]","Yes", "No")) + if("Yes") + owner = user + density = FALSE + anchored = TRUE + return //No, you cant move this ever again + // No One Home + if(!has_buckled_mobs()) + return + // CHECK TWO: Am I a non-bloodsucker? + var/mob/living/carbon/C = pick(buckled_mobs) + if(!istype(bloodsuckerdatum)) + // Try to release this guy + user_unbuckle_mob(C, user) + return + // Bloodsucker Owner! Let the boy go. + if(C.mind) + var/datum/antagonist/vassal/vassaldatum = C.mind.has_antag_datum(ANTAG_DATUM_VASSAL) + if (istype(vassaldatum) && vassaldatum.master == bloodsuckerdatum || C.stat >= DEAD) + unbuckle_mob(C) + useLock = FALSE // Failsafe + return + // Just torture the boy + torture_victim(user, C) + +/obj/structure/bloodsucker/vassalrack/proc/torture_victim(mob/living/user, mob/living/target) + // Check Bloodmob/living/M, force = FALSE, check_loc = TRUE + if(user.blood_volume < convert_cost + 5) + to_chat(user, "You don't have enough blood to initiate the Dark Communion with [target].") + return + // Prep... + useLock = TRUE + // Step One: Tick Down Conversion from 3 to 0 + // Step Two: Break mindshielding/antag (on approve) + // Step Three: Blood Ritual + // Conversion Process + if(convert_progress > 0) + to_chat(user, "You prepare to initiate [target] into your service.") + if(!do_torture(user,target)) + to_chat(user, "The ritual has been interrupted!") + else + convert_progress -- // Ouch. Stop. Don't. + // All done! + if(convert_progress <= 0) + // FAIL: Can't be Vassal + if(!SSticker.mode.can_make_vassal(target, user, display_warning=FALSE) && HAS_TRAIT(target, TRAIT_MINDSHIELD)) // If I'm an unconvertable Antag ONLY + to_chat(user, "[target] doesn't respond to your persuasion. It doesn't appear they can be converted to follow you, they either have a mindshield or their external loyalties are too difficult for you to break.\[ALT+click to release\]") + convert_progress ++ // Pop it back up some. Avoids wasting Blood on a lost cause. + // SUCCESS: All done! + else + if(RequireDisloyalty(target)) + to_chat(user, "[target] has external loyalties! [target.p_they(TRUE)] will require more persuasion to break [target.p_them()] to your will!") + else + to_chat(user, "[target] looks ready for the Dark Communion.") + // Still Need More Persuasion... + else + to_chat(user, "[target] could use [convert_progress == 1?"a little":"some"] more persuasion.") + useLock = FALSE + return + // Check: Mindshield & Antag + if(!disloyalty_confirm && RequireDisloyalty(target)) + if(!do_disloyalty(user,target)) + to_chat(user, "The ritual has been interrupted!") + else if (!disloyalty_confirm) + to_chat(user, "[target] refuses to give into your persuasion. Perhaps a little more?") + else + to_chat(user, "[target] looks ready for the Dark Communion.") + useLock = FALSE + return + // Check: Blood + if(user.blood_volume < convert_cost) + to_chat(user, "You don't have enough blood to initiate the Dark Communion with [target].") + useLock = FALSE + return + var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + bloodsuckerdatum.AddBloodVolume(-convert_cost) + target.add_mob_blood(user) + user.visible_message("[user] marks a bloody smear on [target]'s forehead and puts a wrist up to [target.p_their()] mouth!", \ + "You paint a bloody marking across [target]'s forehead, place your wrist to [target.p_their()] mouth, and subject [target.p_them()] to the Dark Communion.") + if(!do_mob(user, src, 50)) + to_chat(user, "The ritual has been interrupted!") + useLock = FALSE + return + // Convert to Vassal! + if(bloodsuckerdatum && bloodsuckerdatum.attempt_turn_vassal(target)) + //remove_loyalties(target) // In case of Mindshield, or appropriate Antag (Traitor, Internal, etc) + //if (!target.buckled) + // to_chat(user, "The ritual has been interrupted!") + // useLock = FALSE + // return + user.playsound_local(null, 'sound/effects/explosion_distant.ogg', 40, 1) // Play THIS sound for user only. The "null" is where turf would go if a location was needed. Null puts it right in their head. + target.playsound_local(null, 'sound/effects/explosion_distant.ogg', 40, 1) // Play THIS sound for user only. The "null" is where turf would go if a location was needed. Null puts it right in their head. + target.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, 1) // Play THIS sound for user only. The "null" is where turf would go if a location was needed. Null puts it right in their head. + target.Jitter(25) + target.emote("laugh") + //remove_victim(target) // Remove on CLICK ONLY! + useLock = FALSE + +/obj/structure/bloodsucker/vassalrack/proc/do_torture(mob/living/user, mob/living/target, mult=1) + var/torture_time = 15 // Fifteen seconds if you aren't using anything. Shorter with weapons and such. + var/torture_dmg_brute = 2 + var/torture_dmg_burn = 0 + // Get Bodypart + var/target_string = "" + var/obj/item/bodypart/BP = null + if(iscarbon(target)) + var/mob/living/carbon/C = target + BP = pick(C.bodyparts) + if(BP) + target_string += BP.name + // Get Weapon + var/obj/item/I = user.get_active_held_item() + if(!istype(I)) + I = user.get_inactive_held_item() + // Create Strings + var/method_string = I?.attack_verb?.len ? pick(I.attack_verb) : pick("harmed","tortured","wrenched","twisted","scoured","beaten","lashed","scathed") + var/weapon_string = I ? I.name : pick("bare hands","hands","fingers","fists") + // Weapon Bonus + SFX + if(I) + torture_time -= I.force / 4 + torture_dmg_brute += I.force / 4 + //torture_dmg_burn += I. + if(I.sharpness == IS_SHARP) + torture_time -= 1 + else if(I.sharpness == IS_SHARP_ACCURATE) + torture_time -= 2 + if(istype(I, /obj/item/weldingtool)) + var/obj/item/weldingtool/welder = I + welder.welding = TRUE + torture_time -= 5 + torture_dmg_burn += 5 + I.play_tool_sound(target) + torture_time = max(50, torture_time * 10) // Minimum 5 seconds. + // Now run process. + if(!do_mob(user, target, torture_time * mult)) + return FALSE + // SUCCESS + if(I) + playsound(loc, I.hitsound, 30, 1, -1) + I.play_tool_sound(target) + target.visible_message("[user] has [method_string] [target]'s [target_string] with [user.p_their()] [weapon_string]!", \ + "[user] has [method_string] your [target_string] with [user.p_their()] [weapon_string]!") + if(!target.is_muzzled()) + target.emote("scream") + target.Jitter(5) + target.apply_damages(brute = torture_dmg_brute, burn = torture_dmg_burn, def_zone = (BP ? BP.body_zone : null)) // take_overall_damage(6,0) + return TRUE + +/obj/structure/bloodsucker/vassalrack/proc/do_disloyalty(mob/living/user, mob/living/target) + + // OFFER YES/NO NOW! + spawn(10) + if(useLock && target && target.client) // Are we still torturing? Did we cancel? Are they still here? + to_chat(user, "[target] has been given the opportunity for servitude. You await their decision...") + var/alert_text = "You are being tortured! Do you want to give in and pledge your undying loyalty to [user]?" + /* if(HAS_TRAIT(target, TRAIT_MINDSHIELD)) + alert_text += "\n\nYou will no longer be loyal to the station!" + if(SSticker.mode.AmValidAntag(target.mind)) */ + alert_text += "\n\nYou will not lose your current objectives, but they come second to the will of your new master!" + switch(alert(target, alert_text,"THE HORRIBLE PAIN! WHEN WILL IT END?!","Yes, Master!", "NEVER!")) + if("Yes, Master!") + disloyalty_accept(target) + else + disloyalty_refuse(target) + if(!do_torture(user,target, 2)) + return FALSE + + // NOTE: We only remove loyalties when we're CONVERTED! + return TRUE + +/obj/structure/bloodsucker/vassalrack/proc/RequireDisloyalty(mob/living/target) + return SSticker.mode.AmValidAntag(target.mind) //|| HAS_TRAIT(target, TRAIT_MINDSHIELD) + +/obj/structure/bloodsucker/vassalrack/proc/disloyalty_accept(mob/living/target) + // FAILSAFE: Still on the rack? + if(!(locate(target) in buckled_mobs)) + return + // NOTE: You can say YES after torture. It'll apply to next time. + disloyalty_confirm = TRUE + /*if(HAS_TRAIT(target, TRAIT_MINDSHIELD)) + to_chat(target, "You give in to the will of your torturer. If they are successful, you will no longer be loyal to the station!") +*/ +/obj/structure/bloodsucker/vassalrack/proc/disloyalty_refuse(mob/living/target) + // FAILSAFE: Still on the rack? + if(!(locate(target) in buckled_mobs)) + return + // Failsafe: You already said YES. + if(disloyalty_confirm) + return + to_chat(target, "You refuse to give in! You will not break!") + + +/obj/structure/bloodsucker/vassalrack/proc/remove_loyalties(mob/living/target) + // Find Mind Implant & Destroy + if(HAS_TRAIT(target, TRAIT_MINDSHIELD)) + for(var/obj/item/implant/I in target.implants) + if(I.type == /obj/item/implant/mindshield) + I.removed(target,silent=TRUE) + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/obj/structure/bloodsucker/candelabrum + name = "candelabrum" + desc = "It burns slowly, but doesn't radiate any heat." + icon = 'icons/obj/vamp_obj.dmi' + icon_state = "candelabrum" + light_color = "#66FFFF"//LIGHT_COLOR_BLUEGREEN // lighting.dm + light_power = 3 + light_range = 0 // to 2 + density = FALSE + anchored = FALSE + var/lit = FALSE +///obj/structure/bloodsucker/candelabrum/is_hot() // candle.dm + //return FALSE + +/obj/structure/bloodsucker/candelabrum/Destroy() + STOP_PROCESSING(SSobj, src) + +/obj/structure/bloodsucker/candelabrum/update_icon() + icon_state = "candelabrum[lit ? "_lit" : ""]" + +/obj/structure/bloodsucker/candelabrum/examine(mob/user) + . = ..() + if((user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) || isobserver(user)) + . += {"This is a magical candle which drains at the sanity of mortals who are not under your command while it is active."} + . += {"You can alt click on it from any range to turn it on remotely, or simply be next to it and click on it to turn it on and off normally."} +/* if(user.mind.has_antag_datum(ANTAG_DATUM_VASSAL) + . += {"This is a magical candle which drains at the sanity of the fools who havent yet accepted your master, as long as it is active.\n + You can turn it on and off by clicking on it while you are next to it"} */ + +/obj/structure/bloodsucker/candelabrum/attack_hand(mob/user) + var/datum/antagonist/bloodsucker/V = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) //I wish there was a better way to do this + var/datum/antagonist/vassal/T = user.mind.has_antag_datum(ANTAG_DATUM_VASSAL) + if(istype(V) || istype(T)) + toggle() + +/obj/structure/bloodsucker/candelabrum/AltClick(mob/user) + var/datum/antagonist/bloodsucker/V = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + // Bloodsuckers can turn their candles on from a distance. SPOOOOKY. + if(istype(V)) + toggle() + +/obj/structure/bloodsucker/candelabrum/proc/toggle(mob/user) + lit = !lit + if(lit) + set_light(2, 3, "#66FFFF") + START_PROCESSING(SSobj, src) + else + set_light(0) + STOP_PROCESSING(SSobj, src) + update_icon() + +/obj/structure/bloodsucker/candelabrum/process() + if(lit) + for(var/mob/living/carbon/human/H in viewers(7, src)) + var/datum/antagonist/vassal/T = H.mind.has_antag_datum(ANTAG_DATUM_VASSAL) + var/datum/antagonist/bloodsucker/V = H.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if(V || T) //We dont want vassals or vampires affected by this + return + H.hallucination = 20 + SEND_SIGNAL(H, COMSIG_ADD_MOOD_EVENT, "vampcandle", /datum/mood_event/vampcandle) +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// OTHER THINGS TO USE: HUMAN BLOOD. /obj/effect/decal/cleanable/blood + +/obj/item/restraints/legcuffs/beartrap/bloodsucker diff --git a/code/modules/antagonists/bloodsucker/objects/bloodsucker_lair.dm b/code/modules/antagonists/bloodsucker/objects/bloodsucker_lair.dm new file mode 100644 index 0000000000..f68191d6d7 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/objects/bloodsucker_lair.dm @@ -0,0 +1,88 @@ + + +// Created by claiming a Coffin. + + + +// THINGS TO SPAWN: +// +// /obj/effect/decal/cleanable/cobweb && /obj/effect/decal/cleanable/cobweb/cobweb2 +// /obj/effect/decal/cleanable/generic +// /obj/effect/decal/cleanable/dirt/dust <-- Pretty cool, just stains the tile itself. +// /obj/effect/decal/cleanable/blood/old + +/* +/area/ + // All coffins assigned to this area + var/list/obj/structure/closet/crate/laircoffins = new list() + +// Called by Coffin when an area is claimed as a vamp's lair +/area/proc/ClaimAsLair(/obj/structure/closet/crate/inClaimant) + set waitfor = FALSE // Don't make on_gain() wait for this function to finish. This lets this code run on the side. + + laircoffins += laircoffins + sleep() + + // Cancel! + if (laircoffins.len == 0) + return + */ + +/datum/antagonist/bloodsucker/proc/RunLair() + set waitfor = FALSE // Don't make on_gain() wait for this function to finish. This lets this code run on the side. + while(!AmFinalDeath() && coffin && lair) + // WAit 2 min and Repeat + sleep(120) + // Coffin Moved SOMEHOW? + if(lair != get_area(coffin)) + if(coffin) + coffin.UnclaimCoffin() + //lair = get_area(coffin) + break // DONE + var/list/turf/area_turfs = get_area_turfs(lair) + // Create Dirt etc. + var/turf/T_Dirty = pick(area_turfs) + if(T_Dirty && !T_Dirty.density) + // Default: Dirt + // CHECK: Cobweb already there? + //if (!locate(var/obj/effect/decal/cleanable/cobweb) in T_Dirty) // REMOVED! Cleanables don't stack. + // STEP ONE: COBWEBS + // CHECK: Wall to North? + var/turf/check_N = get_step(T_Dirty, NORTH) + if(istype(check_N, /turf/closed/wall)) + // CHECK: Wall to West? + var/turf/check_W = get_step(T_Dirty, WEST) + if(istype(check_W, /turf/closed/wall)) + new /obj/effect/decal/cleanable/cobweb (T_Dirty) + // CHECK: Wall to East? + var/turf/check_E = get_step(T_Dirty, EAST) + if(istype(check_E, /turf/closed/wall)) + new /obj/effect/decal/cleanable/cobweb/cobweb2 (T_Dirty) + // STEP TWO: DIRT + new /obj/effect/decal/cleanable/dirt (T_Dirty) + // Find Animals in Area + /* if(rand(0,2) == 0) + var/mobCount = 0 + var/mobMax = CLAMP(area_turfs.len / 25, 1, 4) + for (var/turf/T in area_turfs) + if(!T) continue + var/mob/living/simple_animal/SA = locate() in T + if(SA) + mobCount ++ + if (mobCount >= mobMax) // Already at max + break + Spawn One + if(mobCount < mobMax) + Seek Out Location + while(area_turfs.len > 0) + var/turf/T = pick(area_turfs) // We use while&pick instead of a for/loop so it's random, rather than from the top of the list. + if(T && !T.density) + var/mob/living/simple_animal/SA = /mob/living/simple_animal/mouse // pick(/mob/living/simple_animal/mouse,/mob/living/simple_animal/mouse,/mob/living/simple_animal/mouse, /mob/living/simple_animal/hostile/retaliate/bat) //prob(300) /mob/living/simple_animal/mouse, + new SA (T) + break + area_turfs -= T*/ + // NOTE: area_turfs is now cleared out! + if(coffin) + coffin.UnclaimCoffin() + // Done (somehow) + lair = null diff --git a/code/modules/antagonists/bloodsucker/powers/bs_brawn.dm b/code/modules/antagonists/bloodsucker/powers/bs_brawn.dm new file mode 100644 index 0000000000..49aada3611 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/powers/bs_brawn.dm @@ -0,0 +1,174 @@ + + +/datum/action/bloodsucker/targeted/brawn + name = "Brawn"//"Cellular Emporium" + desc = "Snap restraints with ease, or deal terrible damage with your bare hands." + button_icon_state = "power_strength" + bloodcost = 10 + cooldown = 130 + target_range = 1 + power_activates_immediately = TRUE + message_Trigger = ""//"Whom will you subvert to your will?" + must_be_capacitated = TRUE + can_be_immobilized = TRUE + bloodsucker_can_buy = TRUE + // Level Up + var/upgrade_canLocker = FALSE + var/upgrade_canDoor = FALSE + +/datum/action/bloodsucker/targeted/brawn/CheckCanUse(display_error) + . = ..() + if(!.) + return + . = TRUE + // Break Out of Restraints! (And then cancel) + if(CheckBreakRestraints()) + //PowerActivatedSuccessfully() // PAY COST! BEGIN COOLDOWN!DEACTIVATE! + . = FALSE //return FALSE + // Throw Off Attacker! (And then cancel) + if(CheckEscapePuller()) + //PowerActivatedSuccessfully() // PAY COST! BEGIN COOLDOWN!DEACTIVATE! + . = FALSE //return FALSE + /*if(CheckBreakLocker()) + .= FALSE */ + // Did we successfuly use power to BREAK CUFFS and/or ESCAPE PULLER and/or escape from a locker? + // Then PAY COST! + if(. == FALSE) + PowerActivatedSuccessfully() // PAY COST! BEGIN COOLDOWN!DEACTIVATE! + + // NOTE: We use . = FALSE so that we can break cuffs AND throw off our attacker in one use! + //return TRUE +/datum/action/bloodsucker/targeted/brawn/CheckValidTarget(atom/A) + return isliving(A) || istype(A, /obj/machinery/door) + +/datum/action/bloodsucker/targeted/brawn/CheckCanTarget(atom/A, display_error) + // DEFAULT CHECKS (Distance) + if(!..()) // Disable range notice for Brawn. + return FALSE + // Must outside Closet to target anyone! + if(!isturf(owner.loc)) + return FALSE + // Check: Self + if(A == owner) + return FALSE + // Target Type: Living + if(isliving(A)) + return TRUE + // Target Type: Door + else if(istype(A, /obj/machinery/door)) + return TRUE + return ..() // yes, FALSE! You failed if you got here! BAD TARGET + +/datum/action/bloodsucker/targeted/brawn/FireTargetedPower(atom/A) + // set waitfor = FALSE <---- DONT DO THIS!We WANT this power to hold up ClickWithPower(), so that we can unlock the power when it's done. + var/mob/living/carbon/target = A + var/mob/living/user = owner + // Target Type: Mob + if(isliving(target)) + var/mob/living/carbon/user_C = user + var/hitStrength = user_C.dna.species.punchdamagehigh * 1.3 + 5 + // Knockdown! + var/powerlevel = min(5, 1 + level_current) + if(rand(5 + powerlevel) >= 5) + target.visible_message("[user] lands a vicious punch, sending [target] away!", \ + "[user] has landed a horrifying punch on you, sending you flying!!", null, COMBAT_MESSAGE_RANGE) + target.Knockdown(min(5, rand(10, 10 * powerlevel)) ) + // Chance of KO + if(rand(6 + powerlevel) >= 6 && target.stat <= UNCONSCIOUS) + target.Unconscious(40) + // Attack! + playsound(get_turf(target), 'sound/weapons/punch4.ogg', 60, 1, -1) + user.do_attack_animation(target, ATTACK_EFFECT_SMASH) + var/obj/item/bodypart/affecting = target.get_bodypart(ran_zone(target.zone_selected)) + target.apply_damage(hitStrength, BRUTE, affecting) + // Knockback + var/send_dir = get_dir(owner, target) + var/turf/T = get_ranged_target_turf(target, send_dir, powerlevel) + owner.newtonian_move(send_dir) // Bounce back in 0 G + target.throw_at(T, powerlevel, TRUE, owner) //new /datum/forced_movement(target, get_ranged_target_turf(target, send_dir, (hitStrength / 4)), 1, FALSE) + // Target Type: Door + else if(istype(target, /obj/machinery/door)) + var/obj/machinery/door/D = target + playsound(get_turf(usr), 'sound/machines/airlock_alien_prying.ogg', 40, 1, -1) + to_chat(user, "You prepare to tear open [D].") + if(do_mob(usr,target,25)) + if (D.Adjacent(user)) + to_chat(user, "You tear open the [D].") + user.Stun(10) + user.do_attack_animation(D, ATTACK_EFFECT_SMASH) + playsound(get_turf(D), 'sound/effects/bang.ogg', 30, 1, -1) + D.open(2) // open(2) is like a crowbar or jaws of life. + // Target Type: Closet + +/datum/action/bloodsucker/targeted/brawn/proc/CheckBreakRestraints() + if(!iscarbon(owner)) // || !owner.restrained() + return FALSE + // (NOTE: Just like biodegrade.dm, we only remove one thing per use // + // Destroy Cuffs + var/mob/living/carbon/user_C = owner + //message_admins("DEBUG3: attempt_cast() [name] / [user_C.handcuffed] ") + if(user_C.handcuffed) + var/obj/O = user_C.get_item_by_slot(SLOT_HANDCUFFED) + if(istype(O)) + //user_C.visible_message("[user_C] attempts to remove [O]!", \ + // "You snap [O] like it's nothing!") + user_C.clear_cuffs(O,TRUE) + playsound(get_turf(usr), 'sound/effects/grillehit.ogg', 80, 1, -1) + return TRUE +/* Doesnt work + // Destroy Straightjacket + if(ishuman(owner)) + var/mob/living/carbon/human/user_H = owner + if(user_H.wear_suit && user_H.wear_suit.breakouttime) + var/obj/item/clothing/suit/straight_jacket/S = user_H.get_item_by_slot(ITEM_SLOT_ICLOTHING) + if(istype(S)) + user_C.visible_message("[user_C] attempts to remove [S]!", \ + "You rip through [S] like it's nothing!") + user_C.clear_cuffs(S,TRUE) + playsound(get_turf(usr), 'sound/effects/grillehit.ogg', 80, 1, -1) + return TRUE */ + // Destroy Leg Cuffs + if(user_C.legcuffed) + var/obj/O = user_C.get_item_by_slot(SLOT_LEGCUFFED) + if(istype(O)) + //user_C.visible_message("[user_C] attempts to remove [O]!", \ + // "You snap [O] like it's nothing!") + user_C.clear_cuffs(O,TRUE) + playsound(get_turf(usr), 'sound/effects/grillehit.ogg', 80, 1, -1) + return TRUE + return FALSE + +/datum/action/bloodsucker/targeted/brawn/proc/CheckEscapePuller() + if(!owner.pulledby)// || owner.pulledby.grab_state <= GRAB_PASSIVE) + return FALSE + var/mob/M = owner.pulledby + var/pull_power = M.grab_state + playsound(get_turf(M), 'sound/effects/woodhit.ogg', 75, 1, -1) + // Knock Down (if Living) + if (isliving(M)) + var/mob/living/L = M + L.Knockdown(pull_power * 10 + 20) + // Knock Back (before Knockdown, which probably cancels pull) + var/send_dir = get_dir(owner, M) + var/turf/T = get_ranged_target_turf(M, send_dir, pull_power) + owner.newtonian_move(send_dir) // Bounce back in 0 G + M.throw_at(T, pull_power, TRUE, owner, FALSE) // Throw distance based on grab state! Harder grabs punished more aggressively. + // /proc/log_combat(atom/user, atom/target, what_done, atom/object=null, addition=null) + log_combat(owner, M, "used Brawn power") + owner.visible_message("[owner] tears free of [M]'s grasp!", \ + "You shrug off [M]'s grasp!") + owner.pulledby = null // It's already done, but JUST IN CASE. + return TRUE +/* Doesnt work +/datum/action/bloodsucker/targeted/brawn/proc/CheckBreakLocker() + if(!istype(owner.loc, /obj/structure/closet)) + return FALSE + playsound(get_turf(owner), 'sound/machines/airlock_alien_prying.ogg', 40, 1, -1) + if(do_mob(owner ,target, 25)) + var/obj/structure/closet/C = owner.loc + to_chat(owner, "You prepare to tear open the [C].") + owner.do_attack_animation(C, ATTACK_EFFECT_SMASH) + playsound(get_turf(C), 'sound/effects/bang.ogg', 30, 1, -1) + C.bust_open() + return TRUE +*/ diff --git a/code/modules/antagonists/bloodsucker/powers/bs_cloak.dm b/code/modules/antagonists/bloodsucker/powers/bs_cloak.dm new file mode 100644 index 0000000000..cea942e26d --- /dev/null +++ b/code/modules/antagonists/bloodsucker/powers/bs_cloak.dm @@ -0,0 +1,57 @@ + + +/datum/action/bloodsucker/cloak + name = "Cloak of Darkness" + desc = "Blend into the shadows and become invisible to the untrained eye." + button_icon_state = "power_cloak" + bloodcost = 5 + cooldown = 50 + bloodsucker_can_buy = TRUE + amToggle = TRUE + warn_constant_cost = TRUE + + var/light_min = 0.5 // If lum is above this, no good. + +/datum/action/bloodsucker/cloak/CheckCanUse(display_error) + . = ..() + if(!.) + return + // Must be Dark + var/turf/T = owner.loc + if(istype(T) && T.get_lumcount() > light_min) + to_chat(owner, "This area is not dark enough to blend in") + return FALSE + return TRUE + +/datum/action/bloodsucker/cloak/ActivatePower() + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + var/mob/living/user = owner + var/was_running = (user.m_intent == MOVE_INTENT_RUN) + if(was_running) + user.toggle_move_intent() + ADD_TRAIT(user, TRAIT_NORUNNING, "cloak of darkness") + while(bloodsuckerdatum && ContinueActive(user) || user.m_intent == MOVE_INTENT_RUN) + // Pay Blood Toll (if awake) + owner.alpha = max(0, owner.alpha - min(75, 20 + 15 * level_current)) + bloodsuckerdatum.AddBloodVolume(-0.2) + sleep(5) // Check every few ticks that we haven't disabled this power + // Return to Running (if you were before) + if(was_running && user.m_intent != MOVE_INTENT_RUN) + user.toggle_move_intent() + +/datum/action/bloodsucker/cloak/ContinueActive(mob/living/user, mob/living/target) + if (!..()) + return FALSE + if(user.stat == !CONSCIOUS) // Must be CONSCIOUS + to_chat(owner, "Your cloak failed due to you falling unconcious! ") + return FALSE + var/turf/T = owner.loc // Must be DARK + if(istype(T) && T.get_lumcount() > light_min) + to_chat(owner, "Your cloak failed due to there being too much light!") + return FALSE + return TRUE + +/datum/action/bloodsucker/cloak/DeactivatePower(mob/living/user = owner, mob/living/target) + ..() + REMOVE_TRAIT(user, TRAIT_NORUNNING, "cloak of darkness") + user.alpha = 255 diff --git a/code/modules/antagonists/bloodsucker/powers/bs_feed.dm b/code/modules/antagonists/bloodsucker/powers/bs_feed.dm new file mode 100644 index 0000000000..7c4cf46e3c --- /dev/null +++ b/code/modules/antagonists/bloodsucker/powers/bs_feed.dm @@ -0,0 +1,324 @@ + + +/datum/action/bloodsucker/feed + name = "Feed" + desc = "Draw the heartsblood of living victims in your grasp.
None/Passive: Feed silently and unnoticed by your victim.
Aggressive: Subdue your target quickly." + button_icon_state = "power_feed" + + bloodcost = 0 + cooldown = 30 + amToggle = TRUE + bloodsucker_can_buy = TRUE + can_be_staked = TRUE + cooldown_static = TRUE + + var/notice_range = 2 // Distance before silent feeding is noticed. + var/mob/living/feed_target // So we can validate more than just the guy we're grappling. + var/target_grappled = FALSE // If you started grappled, then ending it will end your Feed. + +/datum/action/bloodsucker/feed/CheckCanUse(display_error) + . = ..() + if(!.) + return + // Wearing mask + var/mob/living/L = owner + if (L.is_mouth_covered()) + if (display_error) + to_chat(owner, "You cannot feed with your mouth covered! Remove your mask.") + return FALSE + // Find my Target! + if (!FindMyTarget(display_error)) // Sets feed_target within after Validating + return FALSE + // Not in correct state + // DONE! + return TRUE + +/datum/action/bloodsucker/feed/proc/ValidateTarget(mob/living/target, display_error) // Called twice: validating a subtle victim, or validating your grapple victim. + // Bloodsuckers + Animals MUST be grabbed aggressively! + if (!owner.pulling || target == owner.pulling && owner.grab_state < GRAB_AGGRESSIVE) + // NOTE: It's OKAY that we are checking if(!target) below, AFTER animals here. We want passive check vs animal to warn you first, THEN the standard warning. + // Animals: + if (isliving(target) && !iscarbon(target)) + if (display_error) + to_chat(owner, "Lesser beings require a tighter grip.") + return FALSE + // Bloodsuckers: + else if (iscarbon(target) && target.mind && target.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) + if (display_error) + to_chat(owner, "Other Bloodsuckers will not fall for your subtle approach.") + return FALSE + // Must have Target + if (!target) // || !ismob(target) + if (display_error) + to_chat(owner, "You must be next to or grabbing a victim to feed from them.") + return FALSE + // Not even living! + if (!isliving(target) || issilicon(target)) + if (display_error) + to_chat(owner, "You may only feed from living beings.") + return FALSE + if (target.blood_volume <= 0) + if (display_error) + to_chat(owner, "Your victim has no blood to take.") + return FALSE + if (ishuman(target)) + var/mob/living/carbon/human/H = target + if(NOBLOOD in H.dna.species.species_traits)// || owner.get_blood_id() != target.get_blood_id()) + if (display_error) + to_chat(owner, "Your victim's blood is not suitable for you to take.") + return FALSE + return TRUE + +// If I'm not grabbing someone, find me someone nearby. +/datum/action/bloodsucker/feed/proc/FindMyTarget(display_error) + // Default + feed_target = null + target_grappled = FALSE + // If you are pulling a mob, that's your target. If you don't like it, then release them. + if (owner.pulling && ismob(owner.pulling)) + // Check grapple target Valid + if (!ValidateTarget(owner.pulling, display_error)) // Grabbed targets display error. + return FALSE + target_grappled = TRUE + feed_target = owner.pulling + return TRUE + // Find Targets + var/list/mob/living/seen_targets = view(1, owner) + var/list/mob/living/seen_mobs = list() + for(var/mob/living/M in seen_targets) + if (isliving(M) && M != owner) + seen_mobs += M + // None Seen! + if (seen_mobs.len == 0) + if (display_error) + to_chat(owner, "You must be next to or grabbing a victim to feed from them.") + return FALSE + // Check Valids... + var/list/targets_valid = list() + var/list/targets_dead = list() + for(var/mob/living/M in seen_mobs) + // Check adjecent Valid target + if (M != owner && ValidateTarget(M, display_error = FALSE)) // Do NOT display errors. We'll be doing this again in CheckCanUse(), which will rule out grabbed targets. + // Prioritize living, but remember dead as backup + if (M.stat < DEAD) + targets_valid += M + else + targets_dead += M + // No Living? Try dead. + if (targets_valid.len == 0 && targets_dead.len > 0) + targets_valid = targets_dead + // No Targets + if (targets_valid.len == 0) + // Did I see targets? Then display at least one error + if (seen_mobs.len > 1) + if (display_error) + to_chat(owner, "None of these are valid targets to feed from subtly.") + else + ValidateTarget(seen_mobs[1], display_error) + return FALSE + // Too Many Targets + //else if (targets.len > 1) + // if (display_error) + // to_chat(owner, "You are adjecent to too many witnesses. Either grab your victim or move away.") + // return FALSE + // One Target! + else + feed_target = pick(targets_valid)//targets[1] + return TRUE + +/datum/action/bloodsucker/feed/ActivatePower() + // set waitfor = FALSE <---- DONT DO THIS!We WANT this power to hold up Activate(), so Deactivate() can happen after. + var/mob/living/target = feed_target // Stored during CheckCanUse(). Can be a grabbed OR adjecent character. + var/mob/living/user = owner + var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + // Am I SECRET or LOUD? It stays this way the whole time! I must END IT to try it the other way. + var/amSilent = (!target_grappled || owner.grab_state <= GRAB_PASSIVE) // && iscarbon(target) // Non-carbons (animals) not passive. They go straight into aggressive. + // Initial Wait + var/feed_time = (amSilent ? 45 : 25) - (2.5 * level_current) + feed_time = max(15, feed_time) + if (amSilent) + to_chat(user, "You lean quietly toward [target] and secretly draw out your fangs...") + else + to_chat(user, "You pull [target] close to you and draw out your fangs...") + if (!do_mob(user, target, feed_time,0,1,extra_checks=CALLBACK(src, .proc/ContinueActive, user, target)))//sleep(10) + to_chat(user, "Your feeding was interrupted.") + //DeactivatePower(user,target) + return + // Put target to Sleep (Bloodsuckers are immune to their own bite's sleep effect) + if (!amSilent) + ApplyVictimEffects(target) // Sleep, paralysis, immobile, unconscious, and mute + if(target.stat <= UNCONSCIOUS) + sleep(1) + // Wait, then Cancel if Invalid + if (!ContinueActive(user,target)) // Cancel. They're gone. + //DeactivatePower(user,target) + return + // Pull Target Close + if (!target.density) // Pull target to you if they don't take up space. + target.Move(user.loc) + // Broadcast Message + if (amSilent) + //if (!iscarbon(target)) + // user.visible_message("[user] shifts [target] closer to [user.p_their()] mouth.", \ + // "You secretly slip your fangs into [target]'s flesh.", \ + // vision_distance = 2, ignored_mobs=target) // Only people who AREN'T the target will notice this action. + //else + var/deadmessage = target.stat == DEAD ? "" : " [target.p_they(TRUE)] looks dazed, and will not remember this." + user.visible_message("[user] puts [target]'s wrist up to [user.p_their()] mouth.", \ + "You secretly slip your fangs into [target]'s wrist.[deadmessage]", \ + vision_distance = notice_range, ignored_mobs=target) // Only people who AREN'T the target will notice this action. + // Warn Feeder about Witnesses... + var/was_unnoticed = TRUE + for(var/mob/living/M in viewers(notice_range, owner)) + if(M != owner && M != target && iscarbon(M) && M.mind && !M.has_unlimited_silicon_privilege && !M.eye_blind && !M.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) + was_unnoticed = FALSE + break + if (was_unnoticed) + to_chat(user, "You think no one saw you...") + else + to_chat(user, "Someone may have noticed...") + + else // /atom/proc/visible_message(message, self_message, blind_message, vision_distance, ignored_mobs) + user.visible_message("[user] closes [user.p_their()] mouth around [target]'s neck!", \ + "You sink your fangs into [target]'s neck.") + // My mouth is full! + ADD_TRAIT(user, TRAIT_MUTE, "bloodsucker_feed") + + // Begin Feed Loop + var/warning_target_inhuman = FALSE + var/warning_target_dead = FALSE + var/warning_full = FALSE + var/warning_target_bloodvol = 99999 + var/amount_taken = 0 + var/blood_take_mult = amSilent ? 0.3 : 1 // Quantity to take per tick, based on Silent or not. + var/was_alive = target.stat < DEAD && ishuman(target) + // Activate Effects + //target.add_trait(TRAIT_MUTE, "bloodsucker_victim") // <----- Make mute a power you buy? + + // FEEEEEEEEED!!! // + bloodsuckerdatum.poweron_feed = TRUE + while (bloodsuckerdatum && target && active) + //user.mobility_flags &= ~MOBILITY_MOVE // user.canmove = 0 // Prevents spilling blood accidentally. + + // Abort? A bloody mistake. + if (!do_mob(user, target, 20, 0, 0, extra_checks=CALLBACK(src, .proc/ContinueActive, user, target))) + // May have disabled Feed during do_mob + if (!active || !ContinueActive(user, target)) + break + + if (amSilent) + to_chat(user, "Your feeding has been interrupted...but [target.p_they()] didn't seem to notice you.") + else + to_chat(user, "Your feeding has been interrupted!") + user.visible_message("[user] is ripped from [target]'s throat. [target.p_their(TRUE)] blood sprays everywhere!", \ + "Your teeth are ripped from [target]'s throat. [target.p_their(TRUE)] blood sprays everywhere!") + + // Deal Damage to Target (should have been more careful!) + if (iscarbon(target)) + var/mob/living/carbon/C = target + C.bleed(15) + playsound(get_turf(target), 'sound/effects/splat.ogg', 40, 1) + if (ishuman(target)) + var/mob/living/carbon/human/H = target + H.bleed_rate += 5 + target.add_splatter_floor(get_turf(target)) + user.add_mob_blood(target) // Put target's blood on us. The donor goes in the ( ) + target.add_mob_blood(target) + target.take_overall_damage(10,0) + target.emote("scream") + + // Killed Target? + if (was_alive) + CheckKilledTarget(user,target) + + return + + /////////////////////////////////////////////////////////// + // Handle Feeding! User & Victim Effects (per tick) + bloodsuckerdatum.HandleFeeding(target, blood_take_mult) + amount_taken += amSilent ? 0.3 : 1 + if (!amSilent) + ApplyVictimEffects(target) // Sleep, paralysis, immobile, unconscious, and mute + if (amount_taken > 5 && target.stat < DEAD && ishuman(target)) + SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "drankblood", /datum/mood_event/drankblood) // GOOD // in bloodsucker_life.dm + + /////////////////////////////////////////////////////////// + // Not Human? + if (!ishuman(target)) + SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "drankblood", /datum/mood_event/drankblood_bad) // BAD // in bloodsucker_life.dm + if (!warning_target_inhuman) + to_chat(user, "You recoil at the taste of a lesser lifeform.") + warning_target_inhuman = TRUE + // Dead Blood? + if (target.stat >= DEAD) + if (ishuman(target)) + SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "drankblood", /datum/mood_event/drankblood_dead) // BAD // in bloodsucker_life.dm + if (!warning_target_dead) + to_chat(user, "Your victim is dead. [target.p_their(TRUE)] blood barely nourishes you.") + warning_target_dead = TRUE + // Full? + if (!warning_full && user.blood_volume >= bloodsuckerdatum.maxBloodVolume) + to_chat(user, "You are full. Further blood will be wasted.") + warning_full = TRUE + // Blood Remaining? (Carbons/Humans only) + if (iscarbon(target) && !target.AmBloodsucker(1)) + if (target.blood_volume <= BLOOD_VOLUME_BAD && warning_target_bloodvol > BLOOD_VOLUME_BAD) + to_chat(user, "Your victim's blood volume is fatally low!") + else if (target.blood_volume <= BLOOD_VOLUME_OKAY && warning_target_bloodvol > BLOOD_VOLUME_OKAY) + to_chat(user, "Your victim's blood volume is dangerously low.") + else if (target.blood_volume <= BLOOD_VOLUME_SAFE && warning_target_bloodvol > BLOOD_VOLUME_SAFE) + to_chat(user, "Your victim's blood is at an unsafe level.") + warning_target_bloodvol = target.blood_volume // If we had a warning to give, it's been given by now. + // Done? + if (target.blood_volume <= 0) + to_chat(user, "You have bled your victim dry.") + break + + // Blood Gulp Sound + owner.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, 1) // Play THIS sound for user only. The "null" is where turf would go if a location was needed. Null puts it right in their head. + + // DONE! + //DeactivatePower(user,target) + if (amSilent) + to_chat(user, "You slowly release [target]'s wrist." + (target.stat == 0 ? " [target.p_their(TRUE)] face lacks expression, like you've already been forgotten." : "")) + else + user.visible_message("[user] unclenches their teeth from [target]'s neck.", \ + "You retract your fangs and release [target] from your bite.") + + // /proc/log_combat(atom/user, atom/target, what_done, atom/object=null, addition=null) + log_combat(owner, target, "fed on blood", addition="(and took [amount_taken] blood)") + + // Killed Target? + if (was_alive) + CheckKilledTarget(user,target) + + +/datum/action/bloodsucker/feed/proc/CheckKilledTarget(mob/living/user, mob/living/target) + // Bad Vampire. You shouldn't do that. + if (target && target.stat >= DEAD && ishuman(target)) + SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "drankkilled", /datum/mood_event/drankkilled) // BAD // in bloodsucker_life.dm + +/datum/action/bloodsucker/feed/ContinueActive(mob/living/user, mob/living/target) + return ..() && target && (!target_grappled || user.pulling == target)// Active, and still Antag, + // NOTE: We only care about pulling if target started off that way. Mostly only important for Aggressive feed. + +/datum/action/bloodsucker/feed/proc/ApplyVictimEffects(mob/living/target) + // Bloodsuckers not affected by "the Kiss" of another vampire + if (!target.mind || !target.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) + target.Unconscious(50,0) + target.Knockdown(40 + 5 * level_current,1) + // NOTE: THis is based on level of power! + if (ishuman(target)) + target.adjustStaminaLoss(5, forced = TRUE)// Base Stamina Damage + +/datum/action/bloodsucker/feed/DeactivatePower(mob/living/user = owner, mob/living/target) + ..() // activate = FALSE + var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + // No longer Feeding + if (bloodsuckerdatum) + bloodsuckerdatum.poweron_feed = FALSE + feed_target = null + // My mouth is no longer full + REMOVE_TRAIT(owner, TRAIT_MUTE, "bloodsucker_feed") + // Let me move immediately + user.update_canmove() diff --git a/code/modules/antagonists/bloodsucker/powers/bs_fortitude.dm b/code/modules/antagonists/bloodsucker/powers/bs_fortitude.dm new file mode 100644 index 0000000000..3305504c15 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/powers/bs_fortitude.dm @@ -0,0 +1,54 @@ + + + + +/datum/action/bloodsucker/fortitude + name = "Fortitude"//"Cellular Emporium" + desc = "Withstand egregious physical wounds and walk away from attacks that would stun, pierce, and dismember lesser beings. You cannot run while active." + button_icon_state = "power_fortitude" + bloodcost = 5 + cooldown = 80 + bloodsucker_can_buy = TRUE + amToggle = TRUE + warn_constant_cost = TRUE + + var/this_resist // So we can raise and lower your brute resist based on what your level_current WAS. + +/datum/action/bloodsucker/fortitude/ActivatePower() + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + var/mob/living/user = owner + to_chat(user, "Your flesh, skin, and muscles become as steel.") + // Traits & Effects + ADD_TRAIT(user, TRAIT_PIERCEIMMUNE, "fortitude") + ADD_TRAIT(user, TRAIT_NODISMEMBER, "fortitude") + ADD_TRAIT(user, TRAIT_STUNIMMUNE, "fortitude") + ADD_TRAIT(user, TRAIT_NORUNNING, "fortitude") + if (ishuman(owner)) + var/mob/living/carbon/human/H = owner + this_resist = max(0.3, 0.7 - level_current * 0.1) + H.physiology.brute_mod *= this_resist//0.5 + H.physiology.burn_mod *= this_resist//0.5 + // Stop Running (Taken from /datum/quirk/nyctophobia in negative.dm) + var/was_running = (user.m_intent == MOVE_INTENT_RUN) + if(was_running) + user.toggle_move_intent() + while(bloodsuckerdatum && ContinueActive(user) || user.m_intent == MOVE_INTENT_RUN) + // Pay Blood Toll (if awake) + if (user.stat == CONSCIOUS) + bloodsuckerdatum.AddBloodVolume(-0.5) // Used to be 0.3 blood per 2 seconds, but we're making it more expensive to keep on. + sleep(20) // Check every few ticks that we haven't disabled this power + // Return to Running (if you were before) + if(was_running && user.m_intent != MOVE_INTENT_RUN) + user.toggle_move_intent() + +/datum/action/bloodsucker/fortitude/DeactivatePower(mob/living/user = owner, mob/living/target) + ..() + // Restore Traits & Effects + REMOVE_TRAIT(user, TRAIT_PIERCEIMMUNE, "fortitude") + REMOVE_TRAIT(user, TRAIT_NODISMEMBER, "fortitude") + REMOVE_TRAIT(user, TRAIT_STUNIMMUNE, "fortitude") + REMOVE_TRAIT(user, TRAIT_NORUNNING, "fortitude") + if (ishuman(owner)) + var/mob/living/carbon/human/H = owner + H.physiology.brute_mod /= this_resist//0.5 + H.physiology.burn_mod /= this_resist//0.5 diff --git a/code/modules/antagonists/bloodsucker/powers/bs_gohome.dm b/code/modules/antagonists/bloodsucker/powers/bs_gohome.dm new file mode 100644 index 0000000000..0da9ab5ba3 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/powers/bs_gohome.dm @@ -0,0 +1,115 @@ + + +/datum/action/bloodsucker/gohome + name = "Vanishing Act" + desc = "As dawn aproaches, disperse into mist and return directly to your Lair.
WARNING: You will drop ALL of your possessions if observed by mortals." + button_icon_state = "power_gohome" + background_icon_state_on = "vamp_power_off_oneshot" // Even though this never goes off. + background_icon_state_off = "vamp_power_off_oneshot" + + bloodcost = 25 + cooldown = 99999 // It'll never come back. + amToggle = FALSE + amSingleUse = TRUE + + bloodsucker_can_buy = FALSE // You only get this if you've claimed a lair, and only just before sunrise. + can_use_in_torpor = TRUE + must_be_capacitated = TRUE + can_be_immobilized = TRUE + +/datum/action/bloodsucker/gohome/CheckCanUse(display_error) + . = ..() + if(!.) + return + // Have No Lair (NOTE: You only got this power if you had a lair, so this means it's destroyed) + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + if (!istype(bloodsuckerdatum) || !bloodsuckerdatum.coffin) + if (display_error) + to_chat(owner, "Your coffin has been destroyed!") + return FALSE + return TRUE + +/datum/action/bloodsucker/gohome/ActivatePower() + var/mob/living/carbon/user = owner + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + // IMPORTANT: Check for lair at every step! It might get destroyed. + to_chat(user, "You focus on separating your consciousness from your physical form...") + // STEP ONE: Flicker Lights + for(var/obj/machinery/light/L in view(3, get_turf(owner))) // /obj/machinery/light/proc/flicker(var/amount = rand(10, 20)) + L.flicker(5) + playsound(get_turf(owner), 'sound/effects/singlebeat.ogg', 20, 1) + sleep(50) + for(var/obj/machinery/light/L in view(3, get_turf(owner))) // /obj/machinery/light/proc/flicker(var/amount = rand(10, 20)) + L.flicker(5) + playsound(get_turf(owner), 'sound/effects/singlebeat.ogg', 40, 1) + sleep(50) + for(var/obj/machinery/light/L in view(6, get_turf(owner))) // /obj/machinery/light/proc/flicker(var/amount = rand(10, 20)) + L.flicker(5) + playsound(get_turf(owner), 'sound/effects/singlebeat.ogg', 60, 1) + // ( STEP TWO: Lights OFF? ) + // CHECK: Still have Coffin? + if (!istype(bloodsuckerdatum) || !bloodsuckerdatum.coffin) + to_chat(user, "Your coffin has been destroyed! You no longer have a destination.") + return FALSE + if (!owner) + return + // SEEN?: (effects ONLY if there are witnesses! Otherwise you just POOF) + // NOTE: Stolen directly from statue.dm, thanks guys! + var/am_seen = FALSE // Do Effects (seen by anyone) + var/drop_item = FALSE // Drop Stuff (seen by non-vamp) + if (isturf(owner.loc)) // Only check if I'm not in a Locker or something. + // A) Check for Darkness (we can just leave) + var/turf/T = get_turf(user) + if(T && T.lighting_object && T.get_lumcount()>= 0.1) + // B) Check for Viewers + for(var/mob/living/M in viewers(owner)) + if(M != owner && isliving(M) && M.mind && !M.has_unlimited_silicon_privilege && !M.eye_blind) // M.client <--- add this in after testing! + am_seen = TRUE + if (!M.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) + drop_item = TRUE + break + // LOSE CUFFS + if(user.handcuffed) + var/obj/O = user.handcuffed + user.dropItemToGround(O) + if(user.legcuffed) + var/obj/O = user.legcuffed + user.dropItemToGround(O) + // SEEN! + if (drop_item) + // DROP: Clothes, held items, and cuffs etc + // NOTE: Taken from unequip_everything() in inventory.dm. We need to + // *force* all items to drop, so we had to just gut the code out of it. + var/list/items = list() + items |= user.get_equipped_items() + for(var/I in items) + user.dropItemToGround(I,TRUE) + for(var/obj/item/I in owner.held_items) // drop_all_held_items() + user.dropItemToGround(I, TRUE) + if (am_seen) + // POOF EFFECTS + playsound(get_turf(owner), 'sound/magic/summon_karp.ogg', 60, 1) + var/datum/effect_system/steam_spread/puff = new /datum/effect_system/steam_spread/() + puff.effect_type = /obj/effect/particle_effect/smoke/vampsmoke + puff.set_up(3, 0, get_turf(owner)) + puff.start() + // TELEPORT: Move to Coffin & Close it! + do_teleport(owner, bloodsuckerdatum.coffin, no_effects = TRUE, forced = TRUE, channel = TELEPORT_CHANNEL_QUANTUM) // in teleport.dm? + // SLEEP + user.resting = TRUE + //user.Unconscious(30,0) + user.Stun(30,1) + // CLOSE LID: If fail, force me in. + if (!bloodsuckerdatum.coffin.close(owner)) + bloodsuckerdatum.coffin.insert(owner) // Puts me inside. + // The following was taken from close() proc in closets.dm + // (but we had to do it this way because there is no way to force entry) + playsound(bloodsuckerdatum.coffin.loc, bloodsuckerdatum.coffin.close_sound, 15, 1, -3) + bloodsuckerdatum.coffin.opened = FALSE + bloodsuckerdatum.coffin.density = TRUE + bloodsuckerdatum.coffin.update_icon() + // Lock Coffin + bloodsuckerdatum.coffin.LockMe(owner) + // ( STEP FIVE: Create animal at prev location? ) + //var/mob/living/simple_animal/SA = /mob/living/simple_animal/hostile/retaliate/bat // pick(/mob/living/simple_animal/mouse,/mob/living/simple_animal/mouse,/mob/living/simple_animal/mouse, /mob/living/simple_animal/hostile/retaliate/bat) //prob(300) /mob/living/simple_animal/mouse, + //new SA (owner.loc) diff --git a/code/modules/antagonists/bloodsucker/powers/bs_haste.dm b/code/modules/antagonists/bloodsucker/powers/bs_haste.dm new file mode 100644 index 0000000000..ee1dd3007c --- /dev/null +++ b/code/modules/antagonists/bloodsucker/powers/bs_haste.dm @@ -0,0 +1,85 @@ + +// Level 1: Speed to location +// Level 2: Dodge Bullets +// Level 3: Stun People Passed + +/datum/action/bloodsucker/targeted/haste + name = "Immortal Haste" + desc = "Dash somewhere with supernatural speed. Those nearby may be knocked away, stunned, or left empty-handed." + button_icon_state = "power_speed" + bloodcost = 6 + cooldown = 30 + target_range = 15 + power_activates_immediately = TRUE + message_Trigger = ""//"Whom will you subvert to your will?" + bloodsucker_can_buy = TRUE + must_be_capacitated = TRUE + +/datum/action/bloodsucker/targeted/haste/CheckCanUse(display_error) + . = ..() + if(!.) + return + // Being Grabbed + if(owner.pulledby && owner.pulledby.grab_state >= GRAB_AGGRESSIVE) + if(display_error) + to_chat(owner, "You're being grabbed!") + return FALSE + if(!owner.has_gravity(owner.loc)) //We dont want people to be able to use this to fly around in space + if(display_error) + to_chat(owner, "You cant dash while floating!") + return FALSE + return TRUE + +/datum/action/bloodsucker/targeted/haste/CheckValidTarget(atom/A) + return isturf(A) || A.loc != owner.loc // Anything will do, if it's not me or my square + +/datum/action/bloodsucker/targeted/haste/CheckCanTarget(atom/A, display_error) + // DEFAULT CHECKS (Distance) + if (!..()) + return FALSE + // Check: Range + //if (!(A in view(target_range, get_turf(owner)))) + // return FALSE + return TRUE + +/datum/action/bloodsucker/targeted/haste/FireTargetedPower(atom/A) + // set waitfor = FALSE <---- DONT DO THIS!We WANT this power to hold up ClickWithPower(), so that we can unlock the power when it's done. + var/mob/living/user = owner + var/turf/T = isturf(A) ? A : get_turf(A) + // Pulled? Not anymore. + owner.pulledby = null + // Step One: Heatseek toward Target's Turf + walk_to(owner, T, 0, 0.01, 20) // NOTE: this runs in the background! to cancel it, you need to use walk(owner.current,0), or give them a new path. + playsound(get_turf(owner), 'sound/weapons/punchmiss.ogg', 25, 1, -1) + var/safety = 20 + while(get_turf(owner) != T && safety > 0 && !(isliving(target) && target.Adjacent(owner))) + user.canmove = FALSE //Dont move while doing the thing, or itll break + safety -- + // Did I get knocked down? + if(owner && owner.incapacitated(ignore_restraints=TRUE, ignore_grab=TRUE))// owner.incapacitated()) + // We're gonna cancel. But am I on the ground? Spin me! + if(user.resting) + var/send_dir = get_dir(owner, T) + new /datum/forced_movement(owner, get_ranged_target_turf(owner, send_dir, 1), 1, FALSE) + owner.spin(10) + break + // Spin/Stun people we pass. + //var/mob/living/newtarget = locate(/mob/living) in oview(1, owner) + var/list/mob/living/foundtargets = list() + for(var/mob/living/newtarget in oview(1, owner)) + if (newtarget && newtarget != target && !(newtarget in foundtargets))//!newtarget.IsKnockdown()) + if (rand(0, 5) < level_current) + playsound(get_turf(newtarget), "sound/weapons/punch[rand(1,4)].ogg", 15, 1, -1) + newtarget.Knockdown(10 + level_current * 5) + if(newtarget.IsStun()) + newtarget.spin(10,1) + if (rand(0,4)) + newtarget.drop_all_held_items() + foundtargets += newtarget + sleep(1) + if(user) + user.update_canmove() //Let the poor guy move again + +/datum/action/bloodsucker/targeted/haste/DeactivatePower(mob/living/user = owner, mob/living/target) + ..() // activate = FALSE + user.update_canmove() diff --git a/code/modules/antagonists/bloodsucker/powers/bs_lunge.dm b/code/modules/antagonists/bloodsucker/powers/bs_lunge.dm new file mode 100644 index 0000000000..2c1b8d6321 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/powers/bs_lunge.dm @@ -0,0 +1,83 @@ +// Level 1: Grapple level 2 +// Level 2: Grapple 3 from Behind +// Level 3: Grapple 3 from Shadows +/datum/action/bloodsucker/targeted/lunge + name = "Predatory Lunge" + desc = "Spring at your target and aggressively grapple them without warning. Attacks from concealment or the rear may even knock them down." + button_icon_state = "power_lunge" + bloodcost = 10 + cooldown = 100 + target_range = 3 + power_activates_immediately = TRUE + message_Trigger = ""//"Whom will you subvert to your will?" + must_be_capacitated = TRUE + bloodsucker_can_buy = TRUE + +/datum/action/bloodsucker/targeted/lunge/CheckCanUse(display_error) + if(!..(display_error))// DEFAULT CHECKS + return FALSE + // Being Grabbed + if(owner.pulledby && owner.pulledby.grab_state >= GRAB_AGGRESSIVE) + if(display_error) + to_chat(owner, "You're being grabbed!") + return FALSE + if(!owner.has_gravity(owner.loc))//TODO figure out how to check if theyre able to move while in nograv + if(display_error) + to_chat(owner, "You cant lunge while floating!") + return FALSE + return TRUE + +/datum/action/bloodsucker/targeted/lunge/CheckValidTarget(atom/A) + return isliving(A) + +/datum/action/bloodsucker/targeted/lunge/CheckCanTarget(atom/A, display_error) + // Check: Self + if(target == owner) + return FALSE + // Check: Range + //if (!(target in view(target_range, get_turf(owner)))) + // if (display_error) + // to_chat(owner, "Your victim is too far away.") + // return FALSE + // DEFAULT CHECKS (Distance) + if(!..()) + return FALSE + // Check: Turf + var/mob/living/L = A + if(!isturf(L.loc)) + return FALSE + return TRUE + +/datum/action/bloodsucker/targeted/lunge/FireTargetedPower(atom/A) + // set waitfor = FALSE <---- DONT DO THIS!We WANT this power to hold up ClickWithPower(), so that we can unlock the power when it's done. + var/mob/living/carbon/target = A + var/turf/T = get_turf(target) + // Clear Vars + owner.pulling = null + // Will we Knock them Down? + var/do_knockdown = !is_A_facing_B(target,owner) || owner.alpha <= 0 || istype(owner.loc, /obj/structure/closet) + // CAUSES: Target has their back to me, I'm invisible, or I'm in a Closet + // Step One: Heatseek toward Target's Turf + + walk_towards(owner, T, 0.1, 10) // NOTE: this runs in the background! to cancel it, you need to use walk(owner.current,0), or give them a new path. + addtimer(CALLBACK(owner, .proc/_walk, 0), 2 SECONDS) + if(get_turf(owner) != T && !(isliving(target) && target.Adjacent(owner)) && owner.incapacitated() && owner.resting) + var/send_dir = get_dir(owner, T) + new /datum/forced_movement(owner, get_ranged_target_turf(owner, send_dir, 1), 1, FALSE) + owner.spin(10) + // Step Two: Check if I'm at/adjectent to Target's CURRENT turf (not original...that was just a destination) + sleep(1) + if(target.Adjacent(owner)) + // LEVEL 2: If behind target, mute or unconscious! + if(do_knockdown) // && level_current >= 1) + target.Knockdown(15 + 10 * level_current,1) + target.adjustStaminaLoss(40 + 10 * level_current) + // Cancel Walk (we were close enough to contact them) + walk(owner, 0) + target.Stun(10,1) //Without this the victim can just walk away + target.grabbedby(owner) // Taken from mutations.dm under changelings + target.grippedby(owner, instant = TRUE) //instant aggro grab + +/datum/action/bloodsucker/targeted/lunge/DeactivatePower(mob/living/user = owner, mob/living/target) + ..() // activate = FALSE + user.update_canmove() diff --git a/code/modules/antagonists/bloodsucker/powers/bs_masquerade.dm b/code/modules/antagonists/bloodsucker/powers/bs_masquerade.dm new file mode 100644 index 0000000000..6ee17b3014 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/powers/bs_masquerade.dm @@ -0,0 +1,97 @@ + + + +// WITHOUT THIS POWER: +// +// - Mid-Blood: SHOW AS PALE +// - Low-Blood: SHOW AS DEAD +// - No Heartbeat +// - Examine shows actual blood +// - Thermal homeostasis (ColdBlooded) + + + +// WITH THIS POWER: +// - Normal body temp -- remove Cold Blooded (return on deactivate) +// - + + +/datum/action/bloodsucker/masquerade + name = "Masquerade" + desc = "Feign the vital signs of a mortal, and escape both casual and medical notice as the monster you truly are." + button_icon_state = "power_human" + bloodcost = 10 + cooldown = 50 + amToggle = TRUE + bloodsucker_can_buy = TRUE + warn_constant_cost = TRUE + can_use_in_torpor = TRUE // Masquerade is maybe the only one that can do this. It stops your healing. + cooldown_static = TRUE + +// NOTE: Firing off vulgar powers disables your Masquerade! + +/*/datum/action/bloodsucker/masquerade/CheckCanUse(display_error) + if(!..(display_error))// DEFAULT CHECKS + return FALSE + // DONE! + return TRUE +*/ + + +/datum/action/bloodsucker/masquerade/ActivatePower() + + var/mob/living/user = owner + var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + + to_chat(user, "Your heart beats falsely within your lifeless chest. You may yet pass for a mortal.") + to_chat(user, "Your vampiric healing is halted while imitating life.") + + + // Remove ColdBlooded & Hard/SoftCrit + REMOVE_TRAIT(user, TRAIT_COLDBLOODED, "bloodsucker") + REMOVE_TRAIT(user, TRAIT_NOHARDCRIT, "bloodsucker") + REMOVE_TRAIT(user, TRAIT_NOSOFTCRIT, "bloodsucker") + var/obj/item/organ/heart/vampheart/H = user.getorganslot(ORGAN_SLOT_HEART) + + // WE ARE ALIVE! // + bloodsuckerdatum.poweron_masquerade = TRUE + while(bloodsuckerdatum && ContinueActive(user)) + + // HEART + if (istype(H)) + H.FakeStart() + + // PASSIVE (done from LIFE) + // Don't Show Pale/Dead on low blood + // Don't vomit food + // Don't Heal + + // Pay Blood Toll (if awake) + if (user.stat == CONSCIOUS) + bloodsuckerdatum.AddBloodVolume(-0.2) + + sleep(20) // Check every few ticks that we haven't disabled this power + + +/datum/action/bloodsucker/masquerade/ContinueActive(mob/living/user) + // Disable if unable to use power anymore. + //if (user.stat == DEAD || user.blood_volume <= 0) // not conscious or soft critor uncon, just dead + // return FALSE + return ..() // Active, and still Antag + + +/datum/action/bloodsucker/masquerade/DeactivatePower(mob/living/user = owner, mob/living/target) + ..() // activate = FALSE + + var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + bloodsuckerdatum.poweron_masquerade = FALSE + + ADD_TRAIT(user, TRAIT_COLDBLOODED, "bloodsucker") + ADD_TRAIT(user, TRAIT_NOHARDCRIT, "bloodsucker") + ADD_TRAIT(user, TRAIT_NOSOFTCRIT, "bloodsucker") + + // HEART + var/obj/item/organ/heart/H = user.getorganslot(ORGAN_SLOT_HEART) + H.Stop() + + to_chat(user, "Your heart beats one final time, while your skin dries out and your icy pallor returns.") diff --git a/code/modules/antagonists/bloodsucker/powers/bs_mesmerize.dm b/code/modules/antagonists/bloodsucker/powers/bs_mesmerize.dm new file mode 100644 index 0000000000..94bc0e11d0 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/powers/bs_mesmerize.dm @@ -0,0 +1,114 @@ + +// * MEZMERIZE +// LOVE: Target falls in love with you. Being harmed directly causes them harm if they see it? +// STAY: Target will do everything they can to stand in the same place. +// FOLLOW: Target follows you, spouting random phrases from their history (or maybe Poly's or NPC's vocab?) +// ATTACK: Target finds a nearby non-Bloodsucker victim to attack. + +/datum/action/bloodsucker/targeted/mesmerize + name = "Mesmerize" + desc = "Dominate the mind of a mortal who can see your eyes." + button_icon_state = "power_mez" + bloodcost = 30 + cooldown = 200 + target_range = 1 + power_activates_immediately = FALSE + message_Trigger = "Whom will you subvert to your will?" + must_be_capacitated = TRUE + bloodsucker_can_buy = TRUE + +/datum/action/bloodsucker/targeted/mesmerize/CheckCanUse(display_error) + . = ..() + if(!.) + return + if (!owner.getorganslot(ORGAN_SLOT_EYES)) + if (display_error) + to_chat(owner, "You have no eyes with which to mesmerize.") + return FALSE + // Check: Eyes covered? + var/mob/living/L = owner + if (istype(L) && L.is_eyes_covered() || !isturf(owner.loc)) + if (display_error) + to_chat(owner, "Your eyes are concealed from sight.") + return FALSE + return TRUE + +/datum/action/bloodsucker/targeted/mesmerize/CheckValidTarget(atom/A) + return iscarbon(A) + +/datum/action/bloodsucker/targeted/mesmerize/CheckCanTarget(atom/A,display_error) + // Check: Self + if (A == owner) + return FALSE + var/mob/living/carbon/target = A // We already know it's carbon due to CheckValidTarget() + // Bloodsucker + if (target.mind && target.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) + if (display_error) + to_chat(owner, "Bloodsuckers are immune to [src].") + return FALSE + // Dead/Unconscious + if (target.stat > CONSCIOUS) + if (display_error) + to_chat(owner, "Your victim is not [(target.stat == DEAD || HAS_TRAIT(target, TRAIT_FAKEDEATH))?"alive":"conscious"].") + return FALSE + // Check: Target has eyes? + if (!target.getorganslot(ORGAN_SLOT_EYES)) + if (display_error) + to_chat(owner, "They have no eyes!") + return FALSE + // Check: Target blind? + if (target.eye_blind > 0) + if (display_error) + to_chat(owner, "Your victim's eyes are glazed over. They cannot perceive you.") + return FALSE + // Check: Target See Me? (behind wall) + if (!(owner in view(target_range, get_turf(target)))) + // Sub-Check: GET CLOSER + //if (!(owner in range(target_range, get_turf(target))) + // if (display_error) + // to_chat(owner, "You're too far from your victim.") + if (display_error) + to_chat(owner, "You're too far outside your victim's view.") + return FALSE + // Check: Facing target? + if (!is_A_facing_B(owner,target)) // in unsorted.dm + if (display_error) + to_chat(owner, "You must be facing your victim.") + return FALSE + // Check: Target facing me? + if (!target.resting && !is_A_facing_B(target,owner)) + if (display_error) + to_chat(owner, "Your victim must be facing you to see into your eyes.") + return FALSE + return TRUE + +/datum/action/bloodsucker/targeted/mesmerize/FireTargetedPower(atom/A) + // set waitfor = FALSE <---- DONT DO THIS!We WANT this power to hold up ClickWithPower(), so that we can unlock the power when it's done. + var/mob/living/carbon/target = A + var/mob/living/user = owner + + if(istype(target)) + target.Stun(40) //Utterly useless without this, its okay since there are so many checks to go through + target.silent = 45 //Shhhh little lamb + target.apply_status_effect(STATUS_EFFECT_MESMERIZE, 45) //So you cant rotate with combat mode, plus fancy status alert + + if(do_mob(user, target, 40, 0, TRUE, extra_checks=CALLBACK(src, .proc/ContinueActive, user, target))) + PowerActivatedSuccessfully() // PAY COST! BEGIN COOLDOWN! + var/power_time = 90 + level_current * 12 + target.silent = power_time + 20 + target.apply_status_effect(STATUS_EFFECT_MESMERIZE, 100 + level_current * 15) + to_chat(user, "[target] is fixed in place by your hypnotic gaze.") + target.Stun(power_time) + //target.silent += power_time / 10 // Silent isn't based on ticks. + target.next_move = world.time + power_time // <--- Use direct change instead. We want an unmodified delay to their next move // target.changeNext_move(power_time) // check click.dm + target.notransform = TRUE // <--- Fuck it. We tried using next_move, but they could STILL resist. We're just doing a hard freeze. + spawn(power_time) + if(istype(target)) + target.notransform = FALSE + // They Woke Up! (Notice if within view) + if(istype(user) && target.stat == CONSCIOUS && (target in view(10, get_turf(user))) ) + to_chat(user, "[target] has snapped out of their trance.") + + +/datum/action/bloodsucker/targeted/mesmerize/ContinueActive(mob/living/user, mob/living/target) + return ..() && CheckCanUse() && CheckCanTarget(target) diff --git a/code/modules/antagonists/bloodsucker/powers/bs_trespass.dm b/code/modules/antagonists/bloodsucker/powers/bs_trespass.dm new file mode 100644 index 0000000000..c650a6af15 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/powers/bs_trespass.dm @@ -0,0 +1,124 @@ + + +/datum/action/bloodsucker/targeted/trespass + name = "Trespass" + desc = "Become mist and advance two tiles in one direction, ignoring all obstacles except for walls. Useful for skipping past doors and barricades." + button_icon_state = "power_tres" + + bloodcost = 10 + cooldown = 60 + amToggle = FALSE + //target_range = 2 + + bloodsucker_can_buy = TRUE + must_be_capacitated = FALSE + can_be_immobilized = TRUE + + var/turf/target_turf // We need to decide where we're going based on where we clicked. It's not actually the tile we clicked. + +/datum/action/bloodsucker/targeted/trespass/CheckCanUse(display_error) + . = ..() + if(!.) + return + if(owner.notransform || !get_turf(owner)) + return FALSE + + return TRUE + + +/datum/action/bloodsucker/targeted/trespass/CheckValidTarget(atom/A) + // Can't target my tile + if (A == get_turf(owner) || get_turf(A) == get_turf(owner)) + return FALSE + + return TRUE // All we care about is destination. Anything you click is fine. + + +/datum/action/bloodsucker/targeted/trespass/CheckCanTarget(atom/A, display_error) + // NOTE: Do NOT use ..()! We don't want to check distance or anything. + + // Get clicked tile + var/final_turf = isturf(A) ? A : get_turf(A) + + // Are either tiles WALLS? + var/turf/from_turf = get_turf(owner) + var/this_dir // = get_dir(from_turf, target_turf) + for (var/i=1 to 2) + // Keep Prev Direction if we've reached final turf + if (from_turf != final_turf) + this_dir = get_dir(from_turf, final_turf) // Recalculate dir so we don't overshoot on a diagonal. + from_turf = get_step(from_turf, this_dir) + // ERROR! Wall! + if (iswallturf(from_turf)) + if (display_error) + var/wallwarning = (i == 1) ? "in the way" : "at your destination" + to_chat(owner, "There is a solid wall [wallwarning].") + return FALSE + // Done + target_turf = from_turf + + return TRUE + + +/datum/action/bloodsucker/targeted/trespass/FireTargetedPower(atom/A) + // set waitfor = FALSE <---- DONT DO THIS!We WANT this power to hold up ClickWithPower(), so that we can unlock the power when it's done. + + // Find target turf, at or below Atom + var/mob/living/carbon/user = owner + var/turf/my_turf = get_turf(owner) + + user.visible_message("[user]'s form dissipates into a cloud of mist!", \ + "You disspiate into formless mist.") + + + // Effect Origin + playsound(get_turf(owner), 'sound/magic/summon_karp.ogg', 60, 1) + var/datum/effect_system/steam_spread/puff = new /datum/effect_system/steam_spread/() + puff.effect_type = /obj/effect/particle_effect/smoke/vampsmoke + puff.set_up(3, 0, my_turf) + puff.start() + + var/mist_delay = max(5, 20 - level_current * 2.5) // Level up and do this faster. + + // Freeze Me + user.next_move = world.time + mist_delay + user.Stun(mist_delay, ignore_canstun = TRUE) + user.notransform = TRUE + user.density = 0 + var/invis_was = user.invisibility + user.invisibility = INVISIBILITY_MAXIMUM + + // LOSE CUFFS + if(user.handcuffed) + var/obj/O = user.handcuffed + user.dropItemToGround(O) + if(user.legcuffed) + var/obj/O = user.legcuffed + user.dropItemToGround(O) + + // Wait... + sleep(mist_delay / 2) + + // Move & Freeze + if (isturf(target_turf)) + do_teleport(owner, target_turf, no_effects=TRUE, channel = TELEPORT_CHANNEL_QUANTUM) // in teleport.dm? + user.next_move = world.time + mist_delay / 2 + user.Stun(mist_delay / 2, ignore_canstun = TRUE) + + // Wait... + sleep(mist_delay / 2) + + // Un-Hide & Freeze + user.dir = get_dir(my_turf, target_turf) + user.next_move = world.time + mist_delay / 2 + user.Stun(mist_delay / 2, ignore_canstun = TRUE) + user.notransform = FALSE + user.density = 1 + user.invisibility = invis_was + + // Effect Destination + playsound(get_turf(owner), 'sound/magic/summon_karp.ogg', 60, 1) + puff = new /datum/effect_system/steam_spread/() + puff.effect_type = /obj/effect/particle_effect/smoke/vampsmoke + puff.set_up(3, 0, target_turf) + puff.start() diff --git a/code/modules/antagonists/bloodsucker/powers/bs_veil.dm b/code/modules/antagonists/bloodsucker/powers/bs_veil.dm new file mode 100644 index 0000000000..0c83cc2d44 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/powers/bs_veil.dm @@ -0,0 +1,163 @@ + +/datum/action/bloodsucker/veil + name = "Veil of Many Faces" + desc = "Disguise yourself in the illusion of another identity." + button_icon_state = "power_veil" + bloodcost = 15 + cooldown = 100 + amToggle = TRUE + bloodsucker_can_buy = TRUE + warn_constant_cost = TRUE + + // Outfit Vars + var/list/original_items = list() + + // Identity Vars + var/prev_gender + var/prev_skin_tone + var/prev_hair_style + var/prev_facial_hair_style + var/prev_hair_color + var/prev_facial_hair_color + var/prev_underwear + var/prev_undie_color + var/prev_undershirt + var/prev_shirt_color + var/prev_socks + var/prev_socks_color + var/prev_disfigured + var/list/prev_features // For lizards and such + + +/datum/action/bloodsucker/veil/CheckCanUse(display_error) + . = ..() + if(!.) + return + + return TRUE + + +/datum/action/bloodsucker/veil/ActivatePower() + + cast_effect() // POOF + + //if (blahblahblah) + // Disguise_Outfit() + + Disguise_FaceName() + + +/datum/action/bloodsucker/veil/proc/Disguise_Outfit() + + // Step One: Back up original items + + + + +/datum/action/bloodsucker/veil/proc/Disguise_FaceName() + + // Change Name/Voice + var/mob/living/carbon/human/H = owner + H.name_override = H.dna.species.random_name(H.gender) + H.name = H.name_override + H.SetSpecialVoice(H.name_override) + to_chat(owner, "You mystify the air around your person. Your identity is now altered.") + + // Store Prev Appearance + prev_gender = H.gender + prev_skin_tone = H.skin_tone + prev_hair_style = H.hair_style + prev_facial_hair_style = H.facial_hair_style + prev_hair_color = H.hair_color + prev_facial_hair_color = H.facial_hair_color + prev_underwear = H.underwear + prev_undie_color = H.undie_color + prev_undershirt = H.undershirt + prev_shirt_color = H.shirt_color + prev_socks = H.socks + prev_socks_color = H.socks_color + //prev_eye_color + prev_disfigured = HAS_TRAIT(H, TRAIT_DISFIGURED) // I was disfigured! //prev_disabilities = H.disabilities + prev_features = H.dna.features + + // Change Appearance, not randomizing clothes colour, itll just be janky + H.gender = pick(MALE, FEMALE) + H.skin_tone = random_skin_tone() + H.hair_style = random_hair_style(H.gender) + H.facial_hair_style = pick(random_facial_hair_style(H.gender),"Shaved") + H.hair_color = random_short_color() + H.facial_hair_color = H.hair_color + H.underwear = random_underwear(H.gender) + H.undershirt = random_undershirt(H.gender) + H.socks = random_socks(H.gender) + //H.eye_color = random_eye_color() + REMOVE_TRAIT(H, TRAIT_DISFIGURED, null) // + H.dna.features = random_features() + + // Apply Appearance + H.update_body() // Outfit and underware, also body. + //H.update_mutant_bodyparts() // Lizard tails etc + H.update_hair() + H.update_body_parts() + + // Wait here til we deactivate power or go unconscious + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) + while (ContinueActive(owner) && istype(bloodsuckerdatum))//active && owner && owner.stat == CONSCIOUS) + bloodsuckerdatum.AddBloodVolume(-0.2) + sleep(10) + + // Wait for a moment if you fell unconscious... + if (owner && owner.stat > CONSCIOUS) + sleep(50) + + +/datum/action/bloodsucker/veil/DeactivatePower(mob/living/user = owner, mob/living/target) + ..() + if (ishuman(user)) + var/mob/living/carbon/human/H = user + + // Revert Identity + H.UnsetSpecialVoice() + H.name_override = null + H.name = H.real_name + + // Revert Appearance + H.gender = prev_gender + H.skin_tone = prev_skin_tone + H.hair_style = prev_hair_style + H.facial_hair_style = prev_facial_hair_style + H.hair_color = prev_hair_color + H.facial_hair_color = prev_facial_hair_color + H.underwear = prev_underwear + H.undie_color = prev_undie_color + H.undershirt = prev_undershirt + H.shirt_color = prev_shirt_color + H.socks = prev_socks + H.socks_color = prev_socks_color + + //H.disabilities = prev_disabilities // Restore HUSK, CLUMSY, etc. + if (prev_disfigured) + ADD_TRAIT(H, TRAIT_DISFIGURED, "husk") // NOTE: We are ASSUMING husk. // H.status_flags |= DISFIGURED // Restore "Unknown" disfigurement + H.dna.features = prev_features + // Apply Appearance + H.update_body() // Outfit and underware, also body. + H.update_hair() + H.update_body_parts() // Body itself, maybe skin color? + cast_effect() // POOF + + // CAST EFFECT // // General effect (poof, splat, etc) when you cast. Doesn't happen automatically! +/datum/action/bloodsucker/veil/proc/cast_effect() + // Effect + playsound(get_turf(owner), 'sound/magic/smoke.ogg', 20, 1) + var/datum/effect_system/steam_spread/puff = new /datum/effect_system/steam_spread/() + puff.effect_type = /obj/effect/particle_effect/smoke/vampsmoke + puff.set_up(3, 0, get_turf(owner)) + puff.attach(owner) // OPTIONAL + puff.start() + owner.spin(8, 1) // Spin around like a loon. + +/obj/effect/particle_effect/smoke/vampsmoke + opaque = FALSE + lifetime = 0 +/obj/effect/particle_effect/smoke/vampsmoke/fade_out(frames = 6) + ..(frames) diff --git a/code/modules/antagonists/bloodsucker/powers/v_recuperate.dm b/code/modules/antagonists/bloodsucker/powers/v_recuperate.dm new file mode 100644 index 0000000000..6b8795ea02 --- /dev/null +++ b/code/modules/antagonists/bloodsucker/powers/v_recuperate.dm @@ -0,0 +1,38 @@ +/datum/action/bloodsucker/vassal/recuperate + name = "Sanguine Recuperation" + desc = "Slowly heal brute damage while active. This process is exhausting, and requires some of your tainted blood." + button_icon_state = "power_recup" + amToggle = TRUE + bloodcost = 5 + cooldown = 100 + +/datum/action/bloodsucker/vassal/recuperate/CheckCanUse(display_error) + . = ..() + if(!.) + return + if (owner.stat >= DEAD) + return FALSE + return TRUE + +/datum/action/bloodsucker/vassal/recuperate/ActivatePower() + to_chat(owner, "Your muscles clench and your skin crawls as your master's immortal blood knits your wounds and gives you stamina.") + var/mob/living/carbon/C = owner + var/mob/living/carbon/human/H + if(ishuman(owner)) + H = owner + while(ContinueActive(owner)) + C.adjustBruteLoss(-1.5) + C.adjustFireLoss(-0.5) + C.adjustToxLoss(-2, forced = TRUE) + C.blood_volume -= 0.2 + C.adjustStaminaLoss(-15) + // Stop Bleeding + if(istype(H) && H.bleed_rate > 0 && rand(20) == 0) + H.bleed_rate -- + C.Jitter(5) + sleep(10) + // DONE! + //DeactivatePower(owner) + +/datum/action/bloodsucker/vassal/recuperate/ContinueActive(mob/living/user, mob/living/target) + return ..() && user.stat <= DEAD && user.blood_volume > 500 diff --git a/code/modules/antagonists/clockcult/clock_items/clockwork_slab.dm b/code/modules/antagonists/clockcult/clock_items/clockwork_slab.dm index 58835e0cd4..d4d5349c70 100644 --- a/code/modules/antagonists/clockcult/clock_items/clockwork_slab.dm +++ b/code/modules/antagonists/clockcult/clock_items/clockwork_slab.dm @@ -162,9 +162,11 @@ access_display(user) /obj/item/clockwork/slab/AltClick(mob/living/user) + . = ..() if(is_servant_of_ratvar(user) && linking && user.canUseTopic(src, BE_CLOSE, ismonkey(user))) linking = null to_chat(user, "Object link canceled.") + return TRUE /obj/item/clockwork/slab/proc/access_display(mob/living/user) if(!is_servant_of_ratvar(user)) diff --git a/code/modules/antagonists/clockcult/clock_scriptures/scripture_applications.dm b/code/modules/antagonists/clockcult/clock_scriptures/scripture_applications.dm index 3d467350ff..755b324d71 100644 --- a/code/modules/antagonists/clockcult/clock_scriptures/scripture_applications.dm +++ b/code/modules/antagonists/clockcult/clock_scriptures/scripture_applications.dm @@ -146,6 +146,6 @@ /datum/clockwork_scripture/create_object/summon_arbiter/check_special_requirements() if(GLOB.neovgre_exists) - to_chat(invoker, "\"You've already got one...\"") + to_chat(invoker, "\"Only one of my weapons may exist in this temporal stream!\"") return FALSE return ..() diff --git a/code/modules/antagonists/revenant/revenant_abilities.dm b/code/modules/antagonists/revenant/revenant_abilities.dm index 17d200a685..d6582a294f 100644 --- a/code/modules/antagonists/revenant/revenant_abilities.dm +++ b/code/modules/antagonists/revenant/revenant_abilities.dm @@ -5,7 +5,7 @@ ShiftClickOn(A) return if(modifiers["alt"]) - AltClickNoInteract(src, A) + altclick_listed_turf(A) return if(ishuman(A)) diff --git a/code/modules/antagonists/slaughter/slaughter.dm b/code/modules/antagonists/slaughter/slaughter.dm index f27da5f667..c669ff7877 100644 --- a/code/modules/antagonists/slaughter/slaughter.dm +++ b/code/modules/antagonists/slaughter/slaughter.dm @@ -33,6 +33,7 @@ melee_damage_lower = 30 melee_damage_upper = 30 see_in_dark = 8 + blood_volume = 0 //No bleeding on getting shot, for skeddadles lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE bloodcrawl = BLOODCRAWL_EAT var/playstyle_string = "You are a slaughter demon, a terrible creature from another realm. You have a single desire: To kill. \ diff --git a/code/modules/antagonists/wizard/equipment/spellbook.dm b/code/modules/antagonists/wizard/equipment/spellbook.dm index c5308cf0c0..dfde04836d 100644 --- a/code/modules/antagonists/wizard/equipment/spellbook.dm +++ b/code/modules/antagonists/wizard/equipment/spellbook.dm @@ -141,6 +141,10 @@ name = "Disintegrate" spell_type = /obj/effect/proc_holder/spell/targeted/touch/disintegrate +/datum/spellbook_entry/nuclearfist + name = "Nuclear Fist" + spell_type = /obj/effect/proc_holder/spell/targeted/touch/nuclear_fist + /datum/spellbook_entry/disabletech name = "Disable Tech" spell_type = /obj/effect/proc_holder/spell/targeted/emplosion/disable_tech @@ -392,6 +396,12 @@ dynamic_requirement = 50 dynamic_cost = 10 +/datum/spellbook_entry/item/plasmafist + name = "Plasma Fist" + desc = "A forbidden martial art designed on the surging power of plasma. Use it to harness the ancient power." + item_path = /obj/item/book/granter/martial/plasma_fist + cost = 3 + /datum/spellbook_entry/item/guardian name = "Guardian Deck" desc = "A deck of guardian tarot cards, capable of binding a personal guardian to your body. There are multiple types of guardian available, but all of them will transfer some amount of damage to you. \ diff --git a/code/modules/atmospherics/machinery/airalarm.dm b/code/modules/atmospherics/machinery/airalarm.dm index 58ab9c6183..cbbfc05f39 100644 --- a/code/modules/atmospherics/machinery/airalarm.dm +++ b/code/modules/atmospherics/machinery/airalarm.dm @@ -823,11 +823,11 @@ return ..() /obj/machinery/airalarm/AltClick(mob/user) - ..() + . = ..() if(!user.canUseTopic(src, !issilicon(user)) || !isturf(loc)) return - else - togglelock(user) + togglelock(user) + return TRUE /obj/machinery/airalarm/proc/togglelock(mob/living/user) if(stat & (NOPOWER|BROKEN)) diff --git a/code/modules/atmospherics/machinery/atmosmachinery.dm b/code/modules/atmospherics/machinery/atmosmachinery.dm index 2e4cccf5a3..ee4d1bda11 100644 --- a/code/modules/atmospherics/machinery/atmosmachinery.dm +++ b/code/modules/atmospherics/machinery/atmosmachinery.dm @@ -338,10 +338,9 @@ Pipelines + Other Objects -> Pipe network /obj/machinery/atmospherics/AltClick(mob/living/L) - if(is_type_in_list(src, GLOB.ventcrawl_machinery)) - L.handle_ventcrawl(src) - return - ..() + if(is_type_in_typecache(src, GLOB.ventcrawl_machinery)) + return L.handle_ventcrawl(src) + return ..() /obj/machinery/atmospherics/proc/can_crawl_through() diff --git a/code/modules/atmospherics/machinery/components/binary_devices/pump.dm b/code/modules/atmospherics/machinery/components/binary_devices/pump.dm index b95b15efbd..c05c3bb3c9 100644 --- a/code/modules/atmospherics/machinery/components/binary_devices/pump.dm +++ b/code/modules/atmospherics/machinery/components/binary_devices/pump.dm @@ -44,6 +44,7 @@ Thus, the two variables affect pump operation are set in New(): return ..() /obj/machinery/atmospherics/components/binary/pump/AltClick(mob/user) + . = ..() var/area/A = get_area(src) var/turf/T = get_turf(src) if(user.canUseTopic(src, BE_CLOSE, FALSE,)) @@ -51,6 +52,7 @@ Thus, the two variables affect pump operation are set in New(): to_chat(user,"You maximize the pressure on the [src].") investigate_log("Pump, [src.name], was maximized by [key_name(usr)] at [x], [y], [z], [A]", INVESTIGATE_ATMOS) message_admins("Pump, [src.name], was maximized by [ADMIN_LOOKUPFLW(usr)] at [ADMIN_COORDJMP(T)], [A]") + return TRUE /obj/machinery/atmospherics/components/binary/pump/layer1 piping_layer = PIPING_LAYER_MIN diff --git a/code/modules/atmospherics/machinery/components/trinary_devices/filter.dm b/code/modules/atmospherics/machinery/components/trinary_devices/filter.dm index ae3eb8a877..adca444b10 100644 --- a/code/modules/atmospherics/machinery/components/trinary_devices/filter.dm +++ b/code/modules/atmospherics/machinery/components/trinary_devices/filter.dm @@ -28,6 +28,7 @@ return ..() /obj/machinery/atmospherics/components/trinary/filter/AltClick(mob/user) + . = ..() var/area/A = get_area(src) var/turf/T = get_turf(src) if(user.canUseTopic(src, BE_CLOSE, FALSE,)) @@ -35,6 +36,7 @@ to_chat(user,"You maximize the flow rate on the [src].") investigate_log("Filter, [src.name], was maximized by [key_name(usr)] at [x], [y], [z], [A]", INVESTIGATE_ATMOS) message_admins("Filter, [src.name], was maximized by [ADMIN_LOOKUPFLW(usr)] at [ADMIN_COORDJMP(T)], [A]") + return TRUE /obj/machinery/atmospherics/components/trinary/filter/layer1 piping_layer = PIPING_LAYER_MIN diff --git a/code/modules/atmospherics/machinery/components/trinary_devices/mixer.dm b/code/modules/atmospherics/machinery/components/trinary_devices/mixer.dm index 9e4deaf3d8..9646b78321 100644 --- a/code/modules/atmospherics/machinery/components/trinary_devices/mixer.dm +++ b/code/modules/atmospherics/machinery/components/trinary_devices/mixer.dm @@ -30,6 +30,7 @@ return ..() /obj/machinery/atmospherics/components/trinary/mixer/AltClick(mob/user) + . = ..() var/area/A = get_area(src) var/turf/T = get_turf(src) if(user.canUseTopic(src, BE_CLOSE, FALSE,)) @@ -37,6 +38,7 @@ to_chat(user,"You maximize the pressure on the [src].") investigate_log("Mixer, [src.name], was maximized by [key_name(usr)] at [x], [y], [z], [A]", INVESTIGATE_ATMOS) message_admins("Mixer, [src.name], was maximized by [ADMIN_LOOKUPFLW(usr)] at [ADMIN_COORDJMP(T)], [A]") + return TRUE //node 3 is the outlet, nodes 1 & 2 are intakes /obj/machinery/atmospherics/components/trinary/mixer/layer1 diff --git a/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm b/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm index ca8b21593f..0a54503be5 100644 --- a/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm +++ b/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm @@ -399,13 +399,14 @@ return ..() /obj/machinery/atmospherics/components/unary/cryo_cell/AltClick(mob/user) + . = ..() if(user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK)) if(state_open) close_machine() else open_machine() update_icon() - return ..() + return TRUE /obj/machinery/atmospherics/components/unary/cryo_cell/update_remote_sight(mob/living/user) return // we don't see the pipe network while inside cryo. diff --git a/code/modules/atmospherics/machinery/components/unary_devices/thermomachine.dm b/code/modules/atmospherics/machinery/components/unary_devices/thermomachine.dm index b52dc158e4..2e06e68709 100644 --- a/code/modules/atmospherics/machinery/components/unary_devices/thermomachine.dm +++ b/code/modules/atmospherics/machinery/components/unary_devices/thermomachine.dm @@ -210,13 +210,15 @@ min_temperature = max(T0C - (initial(min_temperature) + L * 15), TCMB) //73.15K with T1 stock parts /obj/machinery/atmospherics/components/unary/thermomachine/freezer/AltClick(mob/living/user) + . = ..() var/area/A = get_area(src) var/turf/T = get_turf(src) if(!istype(user) || !user.canUseTopic(src, BE_CLOSE)) return - target_temperature = min_temperature + target_temperature = min_temperature investigate_log("was set to [target_temperature] K by [key_name(usr)]", INVESTIGATE_ATMOS) message_admins("[src.name] was minimized by [ADMIN_LOOKUPFLW(usr)] at [ADMIN_COORDJMP(T)], [A]") + return TRUE /obj/machinery/atmospherics/components/unary/thermomachine/heater name = "heater" @@ -240,6 +242,7 @@ max_temperature = T20C + (initial(max_temperature) * L) //573.15K with T1 stock parts /obj/machinery/atmospherics/components/unary/thermomachine/heater/AltClick(mob/living/user) + . = ..() var/area/A = get_area(src) var/turf/T = get_turf(src) if(!istype(user) || !user.canUseTopic(src, BE_CLOSE)) @@ -247,3 +250,4 @@ target_temperature = max_temperature investigate_log("was set to [target_temperature] K by [key_name(usr)]", INVESTIGATE_ATMOS) message_admins("[src.name] was maximized by [ADMIN_LOOKUPFLW(usr)] at [ADMIN_COORDJMP(T)], [A]") + return TRUE diff --git a/code/modules/atmospherics/machinery/portable/portable_atmospherics.dm b/code/modules/atmospherics/machinery/portable/portable_atmospherics.dm index 81540b0698..b47b6b42c1 100644 --- a/code/modules/atmospherics/machinery/portable/portable_atmospherics.dm +++ b/code/modules/atmospherics/machinery/portable/portable_atmospherics.dm @@ -81,11 +81,13 @@ return air_contents /obj/machinery/portable_atmospherics/AltClick(mob/living/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, !ismonkey(user))) return if(holding) to_chat(user, "You remove [holding] from [src].") replace_tank(user, TRUE) + return TRUE /obj/machinery/portable_atmospherics/examine(mob/user) . = ..() diff --git a/code/modules/cargo/packs/engine.dm b/code/modules/cargo/packs/engine.dm index a4438c7ef8..499881a110 100644 --- a/code/modules/cargo/packs/engine.dm +++ b/code/modules/cargo/packs/engine.dm @@ -27,7 +27,7 @@ /datum/supply_pack/engine/am_shielding name = "Antimatter Shielding Crate" - desc = "Contains ten Antimatter shields, somehow crammed into a crate." + desc = "Contains nine Antimatter shields, somehow crammed into a crate." cost = 2500 contains = list(/obj/item/am_shielding_container, /obj/item/am_shielding_container, @@ -37,8 +37,7 @@ /obj/item/am_shielding_container, /obj/item/am_shielding_container, /obj/item/am_shielding_container, - /obj/item/am_shielding_container, - /obj/item/am_shielding_container) //10 shields: 3x3 containment and a core + /obj/item/am_shielding_container) //9 shields: 3x3 containment and a core crate_name = "antimatter shielding crate" /datum/supply_pack/engine/emitter diff --git a/code/modules/cargo/packs/organic.dm b/code/modules/cargo/packs/organic.dm index 3ffe24ac1c..e124fea345 100644 --- a/code/modules/cargo/packs/organic.dm +++ b/code/modules/cargo/packs/organic.dm @@ -47,6 +47,26 @@ /obj/item/storage/fancy/donut_box) crate_name = "candy crate" +/datum/supply_pack/organic/exoticseeds + name = "Exotic Seeds Crate" + desc = "Any entrepreneuring botanist's dream. Contains twelve different seeds, including three replica-pod seeds and two mystery seeds!" + cost = 1500 + contains = list(/obj/item/seeds/nettle, + /obj/item/seeds/replicapod, + /obj/item/seeds/replicapod, + /obj/item/seeds/replicapod, + /obj/item/seeds/plump, + /obj/item/seeds/liberty, + /obj/item/seeds/amanita, + /obj/item/seeds/reishi, + /obj/item/seeds/banana, + /obj/item/seeds/bamboo, + /obj/item/seeds/eggplant/eggy, + /obj/item/seeds/random, + /obj/item/seeds/random) + crate_name = "exotic seeds crate" + crate_type = /obj/structure/closet/crate/hydroponics + /datum/supply_pack/organic/food name = "Food Crate" desc = "Get things cooking with this crate full of useful ingredients! Contains a two dozen eggs, three bananas, and two bags of flour and rice, two cartons of milk, soymilk, as well as salt and pepper shakers, an enzyme and sugar bottle, and three slabs of monkeymeat." diff --git a/code/modules/cargo/supplypod_beacon.dm b/code/modules/cargo/supplypod_beacon.dm index 915d671fe8..006f1a8084 100644 --- a/code/modules/cargo/supplypod_beacon.dm +++ b/code/modules/cargo/supplypod_beacon.dm @@ -76,12 +76,14 @@ to_chat(user, "[src] linked to [C].") /obj/item/supplypod_beacon/AltClick(mob/user) + . = ..() if (!user.canUseTopic(src, !issilicon(user))) return if (express_console) unlink_console() else to_chat(user, "There is no linked console!") + return TRUE /obj/item/supplypod_beacon/attackby(obj/item/W, mob/user) if(istype(W, /obj/item/pen)) //give a tag that is visible from the linked express console diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index 322f79b8c3..5b134915f6 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -170,12 +170,8 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/prefered_security_department = SEC_DEPT_RANDOM var/custom_species = null - //Quirk list - var/list/positive_quirks = list() - var/list/negative_quirks = list() - var/list/neutral_quirks = list() + //Quirk list var/list/all_quirks = list() - var/list/character_quirks = list() //Job preferences 2.0 - indexed by job title , no key or value implies never var/list/job_preferences = list() @@ -241,7 +237,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) /datum/preferences/proc/ShowChoices(mob/user) if(!user || !user.client) return - update_preview_icon() + update_preview_icon(current_tab != 2) var/list/dat = list("
") dat += "Character Settings" @@ -333,7 +329,6 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "[name] " dat += "
" - update_preview_icon() dat += "
" dat += "

Flavor Text

" dat += "Set Examine Text
" @@ -1201,7 +1196,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "
Done
" dat += "
" dat += "
Current quirks: [all_quirks.len ? all_quirks.Join(", ") : "None"]
" - dat += "
[positive_quirks.len] / [MAX_QUIRKS] max positive quirks
\ + dat += "
[GetPositiveQuirkCount()] / [MAX_QUIRKS] max positive quirks
\ Quirk balance remaining: [GetQuirkBalance()]

" for(var/V in SSquirks.quirks) var/datum/quirk/T = SSquirks.quirks[V] @@ -1232,12 +1227,12 @@ GLOBAL_LIST_EMPTY(preferences_datums) LOCKED: [lock_reason]
" else if(has_quirk) - dat += "[quirk_name] - [initial(T.desc)] \ - [has_quirk ? "Lose" : "Take"] ([quirk_cost] pts.)
" + dat += "[has_quirk ? "Remove" : "Take"] ([quirk_cost] pts.) \ + [quirk_name] - [initial(T.desc)]
" else - dat += "[quirk_name] - [initial(T.desc)] \ - [has_quirk ? "Lose" : "Take"] ([quirk_cost] pts.)
" - dat += "
Reset Traits
" + dat += "[has_quirk ? "Remove" : "Take"] ([quirk_cost] pts.) \ + [quirk_name] - [initial(T.desc)]
" + dat += "
Reset Quirks
" var/datum/browser/popup = new(user, "mob_occupation", "
Quirk Preferences
", 900, 600) //no reason not to reuse the occupation window, as it's cleaner that way popup.set_window_options("can_close=0") @@ -1251,6 +1246,12 @@ GLOBAL_LIST_EMPTY(preferences_datums) bal -= initial(T.value) return bal +/datum/preferences/proc/GetPositiveQuirkCount() + . = 0 + for(var/q in all_quirks) + if(SSquirks.quirk_points[q] > 0) + .++ + /datum/preferences/Topic(href, href_list, hsrc) //yeah, gotta do this I guess.. . = ..() if(href_list["close"]) @@ -1316,43 +1317,30 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/quirk = href_list["trait"] if(!SSquirks.quirks[quirk]) return + for(var/V in SSquirks.quirk_blacklist) //V is a list + var/list/L = V + for(var/Q in all_quirks) + if((quirk in L) && (Q in L) && !(Q == quirk)) //two quirks have lined up in the list of the list of quirks that conflict with each other, so return (see quirks.dm for more details) + to_chat(user, "[quirk] is incompatible with [Q].") + return var/value = SSquirks.quirk_points[quirk] - if(value == 0) - if(quirk in neutral_quirks) - neutral_quirks -= quirk - all_quirks -= quirk - else - neutral_quirks += quirk - all_quirks += quirk + var/balance = GetQuirkBalance() + if(quirk in all_quirks) + if(balance + value < 0) + to_chat(user, "Refunding this would cause you to go below your balance!") + return + all_quirks -= quirk else - var/balance = GetQuirkBalance() - if(quirk in positive_quirks) - positive_quirks -= quirk - all_quirks -= quirk - else if(quirk in negative_quirks) - if(balance + value < 0) - to_chat(user, "Refunding this would cause you to go below your balance!") - return - negative_quirks -= quirk - all_quirks -= quirk - else if(value > 0) - if(positive_quirks.len >= MAX_QUIRKS) - to_chat(user, "You can't have more than [MAX_QUIRKS] positive quirks!") - return - if(balance - value < 0) - to_chat(user, "You don't have enough balance to gain this quirk!") - return - positive_quirks += quirk - all_quirks += quirk - else - negative_quirks += quirk - all_quirks += quirk + if(GetPositiveQuirkCount() >= MAX_QUIRKS) + to_chat(user, "You can't have more than [MAX_QUIRKS] positive quirks!") + return + if(balance - value < 0) + to_chat(user, "You don't have enough balance to gain this quirk!") + return + all_quirks += quirk SetQuirks(user) if("reset") all_quirks = list() - positive_quirks = list() - negative_quirks = list() - neutral_quirks = list() SetQuirks(user) else SetQuirks(user) @@ -1565,10 +1553,8 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/temp_hsv = RGBtoHSV(new_mutantcolor) if(new_mutantcolor == "#000000") features["mcolor"] = pref_species.default_color - update_preview_icon() else if((MUTCOLORS_PARTSONLY in pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV("#202020")[3]) // mutantcolors must be bright, but only if they affect the skin features["mcolor"] = sanitize_hexcolor(new_mutantcolor) - update_preview_icon() else to_chat(user, "Invalid color. Your color is not bright enough.") @@ -1578,10 +1564,8 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/temp_hsv = RGBtoHSV(new_mutantcolor) if(new_mutantcolor == "#000000") features["mcolor2"] = pref_species.default_color - update_preview_icon() else if((MUTCOLORS_PARTSONLY in pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV("#202020")[3]) // mutantcolors must be bright, but only if they affect the skin features["mcolor2"] = sanitize_hexcolor(new_mutantcolor) - update_preview_icon() else to_chat(user, "Invalid color. Your color is not bright enough.") @@ -1591,10 +1575,8 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/temp_hsv = RGBtoHSV(new_mutantcolor) if(new_mutantcolor == "#000000") features["mcolor3"] = pref_species.default_color - update_preview_icon() else if((MUTCOLORS_PARTSONLY in pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV("#202020")[3]) // mutantcolors must be bright, but only if they affect the skin features["mcolor3"] = sanitize_hexcolor(new_mutantcolor) - update_preview_icon() else to_chat(user, "Invalid color. Your color is not bright enough.") @@ -1736,14 +1718,12 @@ GLOBAL_LIST_EMPTY(preferences_datums) features["body_markings"] = new_body_markings if(new_body_markings != "None") features["mam_body_markings"] = "None" - update_preview_icon() if("legs") var/new_legs new_legs = input(user, "Choose your character's legs:", "Character Preference") as null|anything in GLOB.legs_list if(new_legs) features["legs"] = new_legs - update_preview_icon() if("insect_wings") var/new_insect_wings @@ -1829,7 +1809,6 @@ GLOBAL_LIST_EMPTY(preferences_datums) else if(new_mam_body_markings == "None") features["mam_body_markings"] = "Plain" features["body_markings"] = "None" - update_preview_icon() //Xeno Bodyparts if("xenohead")//Head or caste type @@ -2307,8 +2286,8 @@ GLOBAL_LIST_EMPTY(preferences_datums) pref_species = new /datum/species/human save_character() - character.set_species(chosen_species, icon_update = FALSE, pref_load = TRUE) character.dna.features = features.Copy() + character.set_species(chosen_species, icon_update = FALSE, pref_load = TRUE) character.dna.real_name = character.real_name character.dna.nameless = character.nameless character.dna.custom_species = character.custom_species @@ -2337,7 +2316,6 @@ GLOBAL_LIST_EMPTY(preferences_datums) if(icon_updates) character.update_body() character.update_hair() - character.update_body_parts() /datum/preferences/proc/get_default_name(name_id) switch(name_id) diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index 29ea8f5821..f7f49a66af 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -112,7 +112,6 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car if(current_version < 24 && S["feature_exhibitionist"]) var/datum/quirk/exhibitionism/E var/quirk_name = initial(E.name) - neutral_quirks += quirk_name all_quirks += quirk_name /datum/preferences/proc/load_path(ckey,filename="preferences.sav") @@ -386,9 +385,6 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car //Quirks S["all_quirks"] >> all_quirks - S["positive_quirks"] >> positive_quirks - S["negative_quirks"] >> negative_quirks - S["neutral_quirks"] >> neutral_quirks //Citadel code S["feature_genitals_use_skintone"] >> features["genitals_use_skintone"] @@ -519,10 +515,6 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car all_quirks = SANITIZE_LIST(all_quirks) - positive_quirks = SANITIZE_LIST(positive_quirks) - negative_quirks = SANITIZE_LIST(negative_quirks) - neutral_quirks = SANITIZE_LIST(neutral_quirks) - cit_character_pref_load(S) return 1 @@ -598,9 +590,6 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car //Quirks WRITE_FILE(S["all_quirks"] , all_quirks) - WRITE_FILE(S["positive_quirks"] , positive_quirks) - WRITE_FILE(S["negative_quirks"] , negative_quirks) - WRITE_FILE(S["neutral_quirks"] , neutral_quirks) cit_character_pref_save(S) diff --git a/code/modules/clothing/clothing.dm b/code/modules/clothing/clothing.dm index f8f1a67e5a..dc9e46c4b4 100644 --- a/code/modules/clothing/clothing.dm +++ b/code/modules/clothing/clothing.dm @@ -253,16 +253,14 @@ BLIND // can't see anything H.update_suit_sensors() /obj/item/clothing/under/AltClick(mob/user) - if(..()) - return 1 - + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user))) return + if(attached_accessory) + remove_accessory(user) else - if(attached_accessory) - remove_accessory(user) - else - rolldown() + rolldown() + return TRUE /obj/item/clothing/under/verb/jumpsuit_adjust() set name = "Adjust Jumpsuit Style" diff --git a/code/modules/clothing/glasses/_glasses.dm b/code/modules/clothing/glasses/_glasses.dm index 889bac5faf..206165a495 100644 --- a/code/modules/clothing/glasses/_glasses.dm +++ b/code/modules/clothing/glasses/_glasses.dm @@ -428,19 +428,17 @@ ..() /obj/item/clothing/glasses/AltClick(mob/user) + . = ..() if(glass_colour_type && ishuman(user)) var/mob/living/carbon/human/H = user - if(H.client) - if(H.client.prefs) - if(src == H.glasses) - H.client.prefs.uses_glasses_colour = !H.client.prefs.uses_glasses_colour - if(H.client.prefs.uses_glasses_colour) - to_chat(H, "You will now see glasses colors.") - else - to_chat(H, "You will no longer see glasses colors.") - H.update_glasses_color(src, 1) - else - return ..() + if(H.client?.prefs && src == H.glasses) + H.client.prefs.uses_glasses_colour = !H.client.prefs.uses_glasses_colour + if(H.client.prefs.uses_glasses_colour) + to_chat(H, "You will now see glasses colors.") + else + to_chat(H, "You will no longer see glasses colors.") + H.update_glasses_color(src, 1) + return TRUE /obj/item/clothing/glasses/proc/change_glass_color(mob/living/carbon/human/H, datum/client_colour/glass_colour/new_color_type) var/old_colour_type = glass_colour_type diff --git a/code/modules/clothing/head/hardhat.dm b/code/modules/clothing/head/hardhat.dm index fa61b81177..19f03dfd0a 100644 --- a/code/modules/clothing/head/hardhat.dm +++ b/code/modules/clothing/head/hardhat.dm @@ -115,8 +115,10 @@ toggle_helmet_light(user) /obj/item/clothing/head/hardhat/weldhat/AltClick(mob/user) + . = ..() if(user.canUseTopic(src, BE_CLOSE)) toggle_welding_screen(user) + return TRUE /obj/item/clothing/head/hardhat/weldhat/proc/toggle_welding_screen(mob/living/user) if(weldingvisortoggle(user)) diff --git a/code/modules/clothing/head/jobs.dm b/code/modules/clothing/head/jobs.dm index 8ff6fe8bd1..05116c8b97 100644 --- a/code/modules/clothing/head/jobs.dm +++ b/code/modules/clothing/head/jobs.dm @@ -104,16 +104,16 @@ . += "Alt-click to take a candy corn." /obj/item/clothing/head/fedora/det_hat/AltClick(mob/user) - if(user.canUseTopic(src, BE_CLOSE, ismonkey(user))) - ..() - if(loc == user) - if(candy_cooldown < world.time) - var/obj/item/reagent_containers/food/snacks/candy_corn/CC = new /obj/item/reagent_containers/food/snacks/candy_corn(src) - user.put_in_hands(CC) - to_chat(user, "You slip a candy corn from your hat.") - candy_cooldown = world.time+1200 - else - to_chat(user, "You just took a candy corn! You should wait a couple minutes, lest you burn through your stash.") + . = ..() + if(loc == user && user.canUseTopic(src, BE_CLOSE, ismonkey(user))) + if(candy_cooldown < world.time) + var/obj/item/reagent_containers/food/snacks/candy_corn/CC = new /obj/item/reagent_containers/food/snacks/candy_corn(src) + user.put_in_hands(CC) + to_chat(user, "You slip a candy corn from your hat.") + candy_cooldown = world.time+1200 + else + to_chat(user, "You just took a candy corn! You should wait a couple minutes, lest you burn through your stash.") + return TRUE //Mime diff --git a/code/modules/clothing/head/misc.dm b/code/modules/clothing/head/misc.dm index dabfaf06cd..3fa5d56d1e 100644 --- a/code/modules/clothing/head/misc.dm +++ b/code/modules/clothing/head/misc.dm @@ -370,13 +370,25 @@ /obj/item/clothing/head/hotel name = "Telegram cap" desc = "A bright red cap warn by hotel staff. Or people who want to be a singing telegram" - icon_state = "telegramhat" - item_color = "telegramhat" - dog_fashion = null + icon_state = "telegram" + item_color = "telegram" + dog_fashion = /datum/dog_fashion/head/telegram /obj/item/clothing/head/colour name = "Singer cap" desc = "A light white hat that has bands of color. Just makes you want to sing and dance!" icon_state = "colour" item_color = "colour" - dog_fashion = /datum/dog_fashion/head/colour \ No newline at end of file + dog_fashion = /datum/dog_fashion/head/colour + +/obj/item/clothing/head/christmashat + name = "red santa hat" + desc = "A red Christmas Hat! How festive!" + icon_state = "christmashat" + item_state = "christmashat" + +/obj/item/clothing/head/christmashatg + name = "green santa hat" + desc = "A green Christmas Hat! How festive!" + icon_state = "christmashatg" + item_state = "christmashatg" diff --git a/code/modules/clothing/head/soft_caps.dm b/code/modules/clothing/head/soft_caps.dm index 4daa737877..69ec1ccedd 100644 --- a/code/modules/clothing/head/soft_caps.dm +++ b/code/modules/clothing/head/soft_caps.dm @@ -22,11 +22,11 @@ /obj/item/clothing/head/soft/AltClick(mob/user) - ..() + . = ..() if(!user.canUseTopic(src, BE_CLOSE, ismonkey(user))) return - else - flip(user) + flip(user) + return TRUE /obj/item/clothing/head/soft/proc/flip(mob/user) diff --git a/code/modules/clothing/masks/breath.dm b/code/modules/clothing/masks/breath.dm index 7d0c4a455e..f4335d17e8 100644 --- a/code/modules/clothing/masks/breath.dm +++ b/code/modules/clothing/masks/breath.dm @@ -23,11 +23,11 @@ adjustmask(user) /obj/item/clothing/mask/breath/AltClick(mob/user) - ..() + . = ..() if(!user.canUseTopic(src, BE_CLOSE, ismonkey(user))) return - else - adjustmask(user) + adjustmask(user) + return TRUE /obj/item/clothing/mask/breath/examine(mob/user) . = ..() diff --git a/code/modules/clothing/neck/_neck.dm b/code/modules/clothing/neck/_neck.dm index 2356bb16eb..e863dc8860 100644 --- a/code/modules/clothing/neck/_neck.dm +++ b/code/modules/clothing/neck/_neck.dm @@ -66,7 +66,9 @@ var/obj/item/organ/heart/heart = M.getorganslot(ORGAN_SLOT_HEART) var/obj/item/organ/lungs/lungs = M.getorganslot(ORGAN_SLOT_LUNGS) - if(!(M.stat == DEAD || (HAS_TRAIT(M, TRAIT_FAKEDEATH)))) + if (!do_mob(user,M,60)) // Stethoscope should take a moment to listen + return // FAIL + if(!(M.stat == DEAD || (HAS_TRAIT(M, TRAIT_FAKEDEATH)) || (HAS_TRAIT(M, TRAIT_NOPULSE)))) if(heart && istype(heart)) heart_strength = "an unstable" if(heart.beating) diff --git a/code/modules/clothing/shoes/miscellaneous.dm b/code/modules/clothing/shoes/miscellaneous.dm index 080d9281cf..3d318db761 100644 --- a/code/modules/clothing/shoes/miscellaneous.dm +++ b/code/modules/clothing/shoes/miscellaneous.dm @@ -129,6 +129,21 @@ max_heat_protection_temperature = SHOES_MAX_TEMP_PROTECT pocket_storage_component_path = /datum/component/storage/concrete/pockets/shoes +/obj/item/clothing/shoes/winterboots/christmasbootsr + name = "red christmas boots" + desc = "A pair of fluffy red christmas boots!" + icon_state = "christmasbootsr" + +/obj/item/clothing/shoes/winterboots/christmasbootsg + name = "green christmas boots" + desc = "A pair of fluffy green christmas boots!" + icon_state = "christmasbootsg" + +/obj/item/clothing/shoes/winterboots/santaboots + name = "santa boots" + desc = "A pair of santa boots! How traditional!!" + icon_state = "santaboots" + /obj/item/clothing/shoes/workboots name = "work boots" desc = "Nanotrasen-issue Engineering lace-up work boots for the especially blue-collar." diff --git a/code/modules/clothing/spacesuits/hardsuit.dm b/code/modules/clothing/spacesuits/hardsuit.dm index eda12d66a9..c6eac4ac56 100644 --- a/code/modules/clothing/spacesuits/hardsuit.dm +++ b/code/modules/clothing/spacesuits/hardsuit.dm @@ -964,13 +964,12 @@ . += energy_overlay /obj/item/clothing/suit/space/hardsuit/lavaknight/AltClick(mob/living/user) - if(user.incapacitated() || !istype(user)) + . = ..() + if(!in_range(src, user) || !istype(user)) + return + if(user.incapacitated()) to_chat(user, "You can't do that right now!") - return - if(!in_range(src, user)) - return - if(user.incapacitated() || !istype(user) || !in_range(src, user)) - return + return TRUE if(alert("Are you sure you want to recolor your armor stripes?", "Confirm Repaint", "Yes", "No") == "Yes") var/energy_color_input = input(usr,"","Choose Energy Color",energy_color) as color|null @@ -986,6 +985,7 @@ user.update_inv_wear_suit() light_color = energy_color update_light() + return TRUE /obj/item/clothing/suit/space/hardsuit/lavaknight/examine(mob/user) . = ..() diff --git a/code/modules/clothing/suits/miscellaneous.dm b/code/modules/clothing/suits/miscellaneous.dm index b67fda421e..6548b4e7fe 100644 --- a/code/modules/clothing/suits/miscellaneous.dm +++ b/code/modules/clothing/suits/miscellaneous.dm @@ -828,3 +828,33 @@ body_parts_covered = CHEST|GROIN|ARMS|LEGS flags_inv = HIDEJUMPSUIT resistance_flags = NONE + +/obj/item/clothing/suit/hooded/wintercoat/christmascoatr + name = "red christmas coat" + desc = "A festive red Christmas coat! Smells like Candy Cane!" + icon_state = "christmascoatr" + item_state = "christmascoatr" + hoodtype = /obj/item/clothing/head/hooded/winterhood/christmashoodr + +/obj/item/clothing/head/hooded/winterhood/christmashoodr + icon_state = "christmashoodr" + +/obj/item/clothing/suit/hooded/wintercoat/christmascoatg + name = "green christmas coat" + desc = "A festive green Christmas coat! Smells like Candy Cane!" + icon_state = "christmascoatg" + item_state = "christmascoatg" + hoodtype = /obj/item/clothing/head/hooded/winterhood/christmashoodg + +/obj/item/clothing/head/hooded/winterhood/christmashoodg + icon_state = "christmashoodg" + +/obj/item/clothing/suit/hooded/wintercoat/christmascoatrg + name = "red and green christmas coat" + desc = "A festive red and green Christmas coat! Smells like Candy Cane!" + icon_state = "christmascoatrg" + item_state = "christmascoatrg" + hoodtype = /obj/item/clothing/head/hooded/winterhood/christmashoodrg + +/obj/item/clothing/head/hooded/winterhood/christmashoodrg + icon_state = "christmashoodrg" diff --git a/code/modules/clothing/suits/toggles.dm b/code/modules/clothing/suits/toggles.dm index f869eb5d44..559d3006a1 100644 --- a/code/modules/clothing/suits/toggles.dm +++ b/code/modules/clothing/suits/toggles.dm @@ -92,11 +92,11 @@ //Toggle exosuits for different aesthetic styles (hoodies, suit jacket buttons, etc) /obj/item/clothing/suit/toggle/AltClick(mob/user) - ..() + . = ..() if(!user.canUseTopic(src, BE_CLOSE, ismonkey(user))) return - else - suit_toggle(user) + suit_toggle(user) + return TRUE /obj/item/clothing/suit/toggle/ui_action_click() suit_toggle() diff --git a/code/modules/clothing/under/accessories.dm b/code/modules/clothing/under/accessories.dm index 0a0499b5f9..a4d2c17fd6 100644 --- a/code/modules/clothing/under/accessories.dm +++ b/code/modules/clothing/under/accessories.dm @@ -1,4 +1,4 @@ -/obj/item/clothing/accessory //Ties moved to neck slot items, but as there are still things like medals, pokadots, and armbands, this accessory system is being kept as-is +/obj/item/clothing/accessory //Ties moved to neck slot items, but as there are still things like medals and armbands, this accessory system is being kept as-is name = "Accessory" desc = "Something has gone wrong!" icon = 'icons/obj/clothing/accessories.dmi' @@ -67,10 +67,12 @@ return /obj/item/clothing/accessory/AltClick(mob/user) + . = ..() if(istype(user) && user.canUseTopic(src, BE_CLOSE, ismonkey(user))) if(initial(above_suit)) above_suit = !above_suit to_chat(user, "[src] will be worn [above_suit ? "above" : "below"] your suit.") + return TRUE /obj/item/clothing/accessory/examine(mob/user) . = ..() @@ -364,7 +366,7 @@ armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 0, "acid" = 0) ///////////////////// -//Synda Accessories// +//Syndie Accessories// ///////////////////// /obj/item/clothing/accessory/padding @@ -387,35 +389,3 @@ icon_state = "plastics" item_color = "nothing" armor = list("melee" = 0, "bullet" = 0, "laser" = 20, "energy" = 10, "bomb" = 0, "bio" = 30, "rad" = 0, "fire" = 0, "acid" = -40) - -///////////////////// -//Pokadots On Pants// -///////////////////// - -/obj/item/clothing/accessory/attrocious_pokadots - name = "atrocious pokadots" - desc = "They look like something out of a thrift store. Attaches to clothing not to be worn by itself." - icon_state = "attrocious_pokadots" - item_color = "attrocious_pokadots" - attack_verb = list("horrifed", "eye bleeded") - -/obj/item/clothing/accessory/black_white_pokadots - name = "checkered pokadots" - desc = "You can play a game of chess on these! Attaches to clothing not to be worn by itself." - icon_state = "black_white_pokadots" - item_color = "black_white_pokadots" - attack_verb = list("check", "mate") - -/obj/item/clothing/accessory/nt_pokadots - name = "blue and white pokadots" - desc = "To show your pride in your workplace, in the most annoying possable way. Attaches to clothing not to be worn by itself." - icon_state = "nt_pokadots" - item_color = "nt_pokadots" - attack_verb = list("eye bleeded", "annoyed") - -/obj/item/clothing/accessory/syndi_pokadots - name = "black and red pokadots" - desc = "King me. Attaches to clothing not to be worn by itself." //checkers! - icon_state = "syndi_pokadots" - item_color = "syndi_pokadots" - attack_verb = list("jumped", "taken") \ No newline at end of file diff --git a/code/modules/clothing/under/miscellaneous.dm b/code/modules/clothing/under/miscellaneous.dm index 51e34c7b3e..58df911412 100644 --- a/code/modules/clothing/under/miscellaneous.dm +++ b/code/modules/clothing/under/miscellaneous.dm @@ -820,4 +820,36 @@ item_color = "durathread" can_adjust = FALSE body_parts_covered = CHEST|GROIN|ARMS - armor = list("melee" = 10, "laser" = 10, "fire" = 40, "acid" = 10, "bomb" = 5) \ No newline at end of file + armor = list("melee" = 10, "laser" = 10, "fire" = 40, "acid" = 10, "bomb" = 5) + +/obj/item/clothing/under/christmas/christmasmaler + name = "red masculine christmas suit" + desc = "A simple red christmas suit that looks close to Santa's!" + icon_state = "christmasmaler" + item_state = "christmasmaler" + body_parts_covered = CHEST|GROIN + can_adjust = FALSE + +/obj/item/clothing/under/christmas/christmasmaleg + name = "green masculine christmas suit" + desc = "A simple green christmas suit that smells minty!" + icon_state = "christmasmaleg" + item_state = "christmasmaleg" + body_parts_covered = CHEST|GROIN + can_adjust = FALSE + +/obj/item/clothing/under/christmas/christmasfemaler + name = "red feminine christmas suit" + desc = "A simple red christmas suit that looks like Mrs Claus!" + icon_state = "christmasfemaler" + item_state = "christmasfemaler" + body_parts_covered = CHEST|GROIN + can_adjust = FALSE + +/obj/item/clothing/under/christmas/christmasfemaleg + name = "green feminine christmas suit" + desc = "A simple green christmas suit that smells minty!" + icon_state = "christmasfemaleg" + item_state = "christmasfemaleg" + body_parts_covered = CHEST|GROIN + can_adjust = FALSE \ No newline at end of file diff --git a/code/modules/crafting/craft.dm b/code/modules/crafting/craft.dm index 795515835e..0e14f9f5be 100644 --- a/code/modules/crafting/craft.dm +++ b/code/modules/crafting/craft.dm @@ -439,4 +439,4 @@ /datum/mind/proc/teach_crafting_recipe(R) if(!learned_recipes) learned_recipes = list() - learned_recipes |= R \ No newline at end of file + learned_recipes |= R diff --git a/code/modules/crafting/recipes/recipes_misc.dm b/code/modules/crafting/recipes/recipes_misc.dm index 00878d1c59..70dd4d1370 100644 --- a/code/modules/crafting/recipes/recipes_misc.dm +++ b/code/modules/crafting/recipes/recipes_misc.dm @@ -320,3 +320,41 @@ reqs = list(/obj/item/stack/rods = 2, /obj/item/clothing/under/rank/security = 1) category = CAT_MISC + + /datum/crafting_recipe/bloodsucker/vassalrack + name = "Persuasion Rack" + //desc = "For converting crewmembers into loyal Vassals." + result = /obj/structure/bloodsucker/vassalrack + tools = list(/obj/item/weldingtool, + ///obj/item/screwdriver, + /obj/item/wrench + ) + reqs = list(/obj/item/stack/sheet/mineral/wood = 3, + /obj/item/stack/sheet/metal = 2, + /obj/item/restraints/handcuffs/cable = 2, + ///obj/item/storage/belt = 1 + ///obj/item/stack/sheet/animalhide = 1, // /obj/item/stack/sheet/leather = 1, + ///obj/item/stack/sheet/plasteel = 5 + ) + //parts = list(/obj/item/storage/belt = 1 + // ) + + time = 150 + category = CAT_MISC + always_availible = FALSE // Disabled til learned + + + /datum/crafting_recipe/bloodsucker/candelabrum + name = "Candelabrum" + //desc = "For converting crewmembers into loyal Vassals." + result = /obj/structure/bloodsucker/candelabrum + tools = list(/obj/item/weldingtool, + /obj/item/wrench + ) + reqs = list(/obj/item/stack/sheet/metal = 3, + /obj/item/stack/rods = 1, + /obj/item/candle = 1 + ) + time = 100 + category = CAT_MISC + always_availible = FALSE // Disabled til learned diff --git a/code/modules/detectivework/scanner.dm b/code/modules/detectivework/scanner.dm index eaa31c01bf..944fbb8df8 100644 --- a/code/modules/detectivework/scanner.dm +++ b/code/modules/detectivework/scanner.dm @@ -187,9 +187,11 @@ return time2text(world.time + 432000, ":ss") /obj/item/detective_scanner/AltClick(mob/living/user) + . = ..() // Best way for checking if a player can use while not incapacitated, etc if(!user.canUseTopic(src, be_close=TRUE)) return + . = TRUE if(!LAZYLEN(log)) to_chat(user, "Cannot clear logs, the scanner has no logs.") return diff --git a/code/modules/food_and_drinks/kitchen_machinery/icecream_vat.dm b/code/modules/food_and_drinks/kitchen_machinery/icecream_vat.dm index a58435959c..8db6956da9 100644 --- a/code/modules/food_and_drinks/kitchen_machinery/icecream_vat.dm +++ b/code/modules/food_and_drinks/kitchen_machinery/icecream_vat.dm @@ -131,7 +131,6 @@ I.add_ice_cream(flavour_name, beaker.reagents) else I.add_ice_cream(flavour_name) - I.add_ice_cream(flavour_name) if(I.reagents.total_volume < 10) I.reagents.add_reagent("sugar", 10 - I.reagents.total_volume) updateDialog() @@ -252,7 +251,7 @@ desc = "Delicious [cone_name] cone, but no ice cream." -/obj/item/reagent_containers/food/snacks/icecream/proc/add_ice_cream(flavour_name, datum/reagents/R = null) +/obj/item/reagent_containers/food/snacks/icecream/proc/add_ice_cream(flavour_name, datum/reagents/R) name = "[flavour_name] icecream" switch (flavour_name) // adding the actual reagents advertised in the ingredient list if ("vanilla") @@ -311,9 +310,11 @@ qdel(src) /obj/machinery/icecream_vat/AltClick(mob/living/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK)) return replace_beaker(user) + return TRUE /obj/machinery/icecream_vat/proc/replace_beaker(mob/living/user, obj/item/reagent_containers/new_beaker) if(beaker) diff --git a/code/modules/food_and_drinks/kitchen_machinery/microwave.dm b/code/modules/food_and_drinks/kitchen_machinery/microwave.dm index e67de92773..4efac6f508 100644 --- a/code/modules/food_and_drinks/kitchen_machinery/microwave.dm +++ b/code/modules/food_and_drinks/kitchen_machinery/microwave.dm @@ -186,8 +186,10 @@ ..() /obj/machinery/microwave/AltClick(mob/user) + . = ..() if(user.canUseTopic(src, !issilicon(usr))) cook() + return TRUE /obj/machinery/microwave/ui_interact(mob/user) . = ..() diff --git a/code/modules/games/cas.dm b/code/modules/games/cas.dm index fe038ce3f1..4fbb931b32 100644 --- a/code/modules/games/cas.dm +++ b/code/modules/games/cas.dm @@ -130,9 +130,11 @@ update_icon() /obj/item/toy/cards/singlecard/cas/AltClick(mob/living/user) + . = ..() if(!ishuman(user) || !user.canUseTopic(src, BE_CLOSE)) return Flip() + return TRUE /obj/item/toy/cards/singlecard/cas/update_icon() if(flipped) diff --git a/code/modules/hydroponics/grown/misc.dm b/code/modules/hydroponics/grown/misc.dm index 16992b3e96..b37e494233 100644 --- a/code/modules/hydroponics/grown/misc.dm +++ b/code/modules/hydroponics/grown/misc.dm @@ -141,8 +141,9 @@ endurance = 50 maturation = 3 yield = 4 - growthstages = 3 + growthstages = 2 reagents_add = list("sugar" = 0.25) + mutatelist = list(/obj/item/seeds/bamboo) /obj/item/reagent_containers/food/snacks/grown/sugarcane seed = /obj/item/seeds/sugarcane diff --git a/code/modules/hydroponics/grown/replicapod.dm b/code/modules/hydroponics/grown/replicapod.dm index ce0ca0220b..bac480bee6 100644 --- a/code/modules/hydroponics/grown/replicapod.dm +++ b/code/modules/hydroponics/grown/replicapod.dm @@ -41,7 +41,7 @@ blood_type = B.data["blood_type"] features = B.data["features"] factions = B.data["factions"] - factions = B.data["quirks"] + quirks = B.data["quirks"] contains_sample = TRUE visible_message("The [src] is injected with a fresh blood sample.") else diff --git a/code/modules/hydroponics/grown/towercap.dm b/code/modules/hydroponics/grown/towercap.dm index 9d7081ad65..982122e314 100644 --- a/code/modules/hydroponics/grown/towercap.dm +++ b/code/modules/hydroponics/grown/towercap.dm @@ -98,6 +98,49 @@ /obj/item/grown/log/steel/CheckAccepted(obj/item/I) return FALSE +/obj/item/seeds/bamboo + name = "pack of bamboo seeds" + desc = "A plant known for its flexible and resistant logs." + icon_state = "seed-bamboo" + species = "bamboo" + plantname = "Bamboo" + product = /obj/item/grown/log/bamboo + lifespan = 80 + endurance = 70 + maturation = 15 + production = 2 + yield = 5 + potency = 50 + growthstages = 2 + growing_icon = 'icons/obj/hydroponics/growing.dmi' + icon_dead = "bamboo-dead" + genes = list(/datum/plant_gene/trait/repeated_harvest) + +/obj/item/grown/log/bamboo + seed = /obj/item/seeds/bamboo + name = "bamboo log" + desc = "A long and resistant bamboo log." + icon_state = "bamboo" + plank_type = /obj/item/stack/sheet/mineral/bamboo + plank_name = "bamboo sticks" + +/obj/item/grown/log/bamboo/CheckAccepted(obj/item/I) + return FALSE + +/obj/structure/punji_sticks + name = "punji sticks" + desc = "Don't step on this." + icon = 'icons/obj/hydroponics/equipment.dmi' + icon_state = "punji" + resistance_flags = FLAMMABLE + max_integrity = 30 + density = FALSE + anchored = TRUE + +/obj/structure/punji_sticks/Initialize(mapload) + . = ..() + AddComponent(/datum/component/caltrop, 20, 30, 100, CALTROP_BYPASS_SHOES) + /////////BONFIRES////////// /obj/structure/bonfire diff --git a/code/modules/language/vampiric.dm b/code/modules/language/vampiric.dm new file mode 100644 index 0000000000..6da54ce844 --- /dev/null +++ b/code/modules/language/vampiric.dm @@ -0,0 +1,25 @@ +// VAMPIRE LANGUAGE // + +/datum/language/vampiric + name = "Blah-Sucker" + desc = "The native language of the Bloodsucker elders, learned intuitively by Fledglings and as they pass from death into immortality. Thralls are also given the ability to speak this as apart of their conversion ritual." + speech_verb = "growls" + ask_verb = "growls" + exclaim_verb = "snarls" + whisper_verb = "hisses" + key = "b" + space_chance = 40 + default_priority = 90 + icon_state = "bloodsucker" + + flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD // Hide the icon next to your text if someone doesn't know this language. + syllables = list( + "luk","cha","no","kra","pru","chi","busi","tam","pol","spu","och", // Start: Vampiric + "umf","ora","stu","si","ri","li","ka","red","ani","lup","ala","pro", + "to","siz","nu","pra","ga","ump","ort","a","ya","yach","tu","lit", + "wa","mabo","mati","anta","tat","tana","prol", + "tsa","si","tra","te","ele","fa","inz", // Start: Romanian + "nza","est","sti","ra","pral","tsu","ago","esch","chi","kys","praz", // Start: Custom + "froz","etz","tzil", + "t'","k'","t'","k'","th'","tz'" + ) diff --git a/code/modules/mining/abandoned_crates.dm b/code/modules/mining/abandoned_crates.dm index f98f0755c8..4cfd2e447f 100644 --- a/code/modules/mining/abandoned_crates.dm +++ b/code/modules/mining/abandoned_crates.dm @@ -181,10 +181,11 @@ else return ..() +//this helps you not blow up so easily by overriding unlocking which results in an immediate boom. /obj/structure/closet/crate/secure/loot/AltClick(mob/living/user) - if(!user.canUseTopic(src, BE_CLOSE)) - return - return attack_hand(user) //this helps you not blow up so easily by overriding unlocking which results in an immediate boom. + if(user.canUseTopic(src, BE_CLOSE)) + attack_hand(user) + return TRUE /obj/structure/closet/crate/secure/loot/attackby(obj/item/W, mob/user) if(locked) diff --git a/code/modules/mining/equipment/marker_beacons.dm b/code/modules/mining/equipment/marker_beacons.dm index 00ce37b79a..9d595664ff 100644 --- a/code/modules/mining/equipment/marker_beacons.dm +++ b/code/modules/mining/equipment/marker_beacons.dm @@ -59,6 +59,7 @@ GLOBAL_LIST_INIT(marker_beacon_colors, list( /obj/item/stack/marker_beacon/AltClick(mob/living/user) if(!istype(user) || !user.canUseTopic(src, BE_CLOSE)) return + . = TRUE var/input_color = input(user, "Choose a color.", "Beacon Color") as null|anything in GLOB.marker_beacon_colors if(!istype(user) || !user.canUseTopic(src, BE_CLOSE)) return @@ -128,9 +129,10 @@ GLOBAL_LIST_INIT(marker_beacon_colors, list( return ..() /obj/structure/marker_beacon/AltClick(mob/living/user) - ..() + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE)) return + . = TRUE var/input_color = input(user, "Choose a color.", "Beacon Color") as null|anything in GLOB.marker_beacon_colors if(!istype(user) || !user.canUseTopic(src, BE_CLOSE)) return diff --git a/code/modules/mob/dead/new_player/preferences_setup.dm b/code/modules/mob/dead/new_player/preferences_setup.dm index d54b8256a9..7aae26526e 100644 --- a/code/modules/mob/dead/new_player/preferences_setup.dm +++ b/code/modules/mob/dead/new_player/preferences_setup.dm @@ -25,7 +25,7 @@ features = random_features() age = rand(AGE_MIN,AGE_MAX) -/datum/preferences/proc/update_preview_icon() +/datum/preferences/proc/update_preview_icon(equip_job = TRUE) // Determine what job is marked as 'High' priority, and dress them up as such. var/datum/job/previewJob var/highest_pref = 0 @@ -45,12 +45,11 @@ // Set up the dummy for its photoshoot var/mob/living/carbon/human/dummy/mannequin = generate_or_wait_for_human_dummy(DUMMY_HUMAN_SLOT_PREFERENCES) - mannequin.cut_overlays() // Apply the Dummy's preview background first so we properly layer everything else on top of it. mannequin.add_overlay(mutable_appearance('modular_citadel/icons/ui/backgrounds.dmi', bgstate, layer = SPACE_LAYER)) copy_to(mannequin) - if(previewJob) + if(previewJob && equip_job) mannequin.job = previewJob.title previewJob.equip(mannequin, TRUE, preference_source = parent) diff --git a/code/modules/mob/dead/new_player/sprite_accessories/socks.dm b/code/modules/mob/dead/new_player/sprite_accessories/socks.dm index ddb5c4e21b..19ec677a72 100644 --- a/code/modules/mob/dead/new_player/sprite_accessories/socks.dm +++ b/code/modules/mob/dead/new_player/sprite_accessories/socks.dm @@ -46,6 +46,18 @@ name = "Knee-High - UK" icon_state = "uk_knee" +/datum/sprite_accessory/underwear/socks/christmas_knee + name = "Knee-High - Christmas" + icon_state = "christmas_knee" + +/datum/sprite_accessory/underwear/socks/candycaner_knee + name = "Knee-High - Red Candy Cane" + icon_state = "candycaner_knee" + +/datum/sprite_accessory/underwear/socks/candycaneg_knee + name = "Knee-High - Green Candy Cane" + icon_state = "candycaneg_knee" + /datum/sprite_accessory/underwear/socks/socks_norm name = "Normal" icon_state = "socks_norm" @@ -55,6 +67,18 @@ name = "Normal - Bee" icon_state = "bee_norm" +/datum/sprite_accessory/underwear/socks/christmas_norm + name = "Normal - Christmas" + icon_state = "christmas_norm" + +/datum/sprite_accessory/underwear/socks/candycaner_norm + name = "Normal - Red Candy Cane" + icon_state = "candycaner_norm" + +/datum/sprite_accessory/underwear/socks/candycaneg_norm + name = "Normal - Green Candy Cane" + icon_state = "candycaneg_norm" + /datum/sprite_accessory/underwear/socks/pantyhose name = "Pantyhose" icon_state = "pantyhose" @@ -134,3 +158,15 @@ /datum/sprite_accessory/underwear/socks/uk_thigh name = "Thigh-high - UK" icon_state = "uk_thigh" + +/datum/sprite_accessory/underwear/socks/christmas_thigh + name = "Thigh-high - Christmas" + icon_state = "christmas_thigh" + +/datum/sprite_accessory/underwear/socks/candycaner_thigh + name = "Thigh-high - Red Candy Cane" + icon_state = "candycaner_thigh" + +/datum/sprite_accessory/underwear/socks/candycaneg_thigh + name = "Thigh-high - Green Candy Cane" + icon_state = "candycaneg_thigh" \ No newline at end of file diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index fbbd43bbe1..5bdfd174b5 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -265,7 +265,7 @@ Works together with spawning an observer, noted above. /mob/proc/ghostize(can_reenter_corpse = TRUE, special = FALSE, penalize = FALSE) penalize = suiciding || penalize // suicide squad. - if(!key || cmptext(copytext(key,1,2),"@") || (!special && SEND_SIGNAL(src, COMSIG_MOB_GHOSTIZE, can_reenter_corpse) & COMPONENT_BLOCK_GHOSTING)) + if(!key || cmptext(copytext(key,1,2),"@") || (SEND_SIGNAL(src, COMSIG_MOB_GHOSTIZE, can_reenter_corpse, special, penalize) & COMPONENT_BLOCK_GHOSTING)) return //mob has no key, is an aghost or some component hijacked. stop_sound_channel(CHANNEL_HEARTBEAT) //Stop heartbeat sounds because You Are A Ghost Now var/mob/dead/observer/ghost = new(src) // Transfer safety to observer spawning proc. diff --git a/code/modules/mob/living/blood.dm b/code/modules/mob/living/blood.dm index 757aadaea1..8195b3d84b 100644 --- a/code/modules/mob/living/blood.dm +++ b/code/modules/mob/living/blood.dm @@ -36,6 +36,9 @@ if(bleed_rate <= 0) bleed_rate = 0 + if(HAS_TRAIT(src, TRAIT_NOMARROW)) //Bloodsuckers don't need to be here. + return + if(bodytemperature >= TCRYO && !(HAS_TRAIT(src, TRAIT_NOCLONE))) //cryosleep or husked people do not pump the blood. //Blood regeneration if there is some space diff --git a/code/modules/mob/living/brain/MMI.dm b/code/modules/mob/living/brain/MMI.dm index aa3c209016..2a57506df6 100644 --- a/code/modules/mob/living/brain/MMI.dm +++ b/code/modules/mob/living/brain/MMI.dm @@ -125,6 +125,7 @@ else if(!brain) brain = new(src) brain.name = "[L.real_name]'s brain" + brain.organ_flags |= ORGAN_FROZEN name = "Man-Machine Interface: [brainmob.real_name]" update_icon() diff --git a/code/modules/mob/living/carbon/damage_procs.dm b/code/modules/mob/living/carbon/damage_procs.dm index 21541e8c28..d893108bcd 100644 --- a/code/modules/mob/living/carbon/damage_procs.dm +++ b/code/modules/mob/living/carbon/damage_procs.dm @@ -65,6 +65,8 @@ /mob/living/carbon/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE) + if (!forced && amount < 0 && HAS_TRAIT(src,TRAIT_NONATURALHEAL)) + return FALSE if(!forced && (status_flags & GODMODE)) return FALSE if(amount > 0) @@ -74,6 +76,8 @@ return amount /mob/living/carbon/adjustFireLoss(amount, updating_health = TRUE, forced = FALSE) + if (!forced && amount < 0 && HAS_TRAIT(src,TRAIT_NONATURALHEAL)) //Vamps don't heal naturally. + return FALSE if(!forced && (status_flags & GODMODE)) return FALSE if(amount > 0) diff --git a/code/modules/mob/living/carbon/human/dummy.dm b/code/modules/mob/living/carbon/human/dummy.dm index 6da188dd2d..74593a20a0 100644 --- a/code/modules/mob/living/carbon/human/dummy.dm +++ b/code/modules/mob/living/carbon/human/dummy.dm @@ -17,6 +17,7 @@ INITIALIZE_IMMEDIATE(/mob/living/carbon/human/dummy) /mob/living/carbon/human/dummy/proc/wipe_state() delete_equipment() + icon_render_key = null cut_overlays(TRUE) //Inefficient pooling/caching way. diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm index 04ec0454f8..310ab6beeb 100644 --- a/code/modules/mob/living/carbon/human/examine.dm +++ b/code/modules/mob/living/carbon/human/examine.dm @@ -15,6 +15,13 @@ . = list("*---------*\nThis is [!obscure_name ? name : "Unknown"]!") + var/vampDesc = ReturnVampExamine(user) // Vamps recognize the names of other vamps. + var/vassDesc = ReturnVassalExamine(user) // Vassals recognize each other's marks. + if (vampDesc != "") // If we don't do it this way, we add a blank space to the string...something to do with this --> . += "" + . += vampDesc + if (vassDesc != "") + . += vassDesc + var/list/obscured = check_obscured_slots() var/skipface = (wear_mask && (wear_mask.flags_inv & HIDEFACE)) || (head && (head.flags_inv & HIDEFACE)) @@ -237,13 +244,13 @@ if(DISGUST_LEVEL_DISGUSTED to INFINITY) msg += "[t_He] look[p_s()] extremely disgusted.\n" - if(blood_volume < (BLOOD_VOLUME_SAFE*blood_ratio)) + if(ShowAsPaleExamine()) msg += "[t_He] [t_has] pale skin.\n" if(bleedsuppress) msg += "[t_He] [t_is] bandaged with something.\n" else if(bleed_rate) - if(reagents.has_reagent("heparin")) + if(bleed_rate >= 8) //8 is the rate at which heparin causes you to bleed msg += "[t_He] [t_is] bleeding uncontrollably!\n" else msg += "[t_He] [t_is] bleeding!\n" diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 3ae2e3bdbd..45211f7f44 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -858,6 +858,7 @@ .["Make slime"] = "?_src_=vars;[HrefToken()];makeslime=[REF(src)]" .["Toggle Purrbation"] = "?_src_=vars;[HrefToken()];purrbation=[REF(src)]" .["Copy outfit"] = "?_src_=vars;[HrefToken()];copyoutfit=[REF(src)]" + .["Add/Remove Quirks"] = "?_src_=vars;[HrefToken()];modquirks=[REF(src)]" /mob/living/carbon/human/MouseDrop_T(mob/living/target, mob/living/user) if(pulling == target && grab_state >= GRAB_AGGRESSIVE && stat == CONSCIOUS) @@ -867,8 +868,9 @@ return //If you dragged them to you and you're aggressively grabbing try to fireman carry them else if(user != target) - fireman_carry(target) - return + if(user.a_intent == INTENT_GRAB) + fireman_carry(target) + return . = ..() //src is the user that will be carrying, target is the mob to be carried @@ -890,7 +892,10 @@ return visible_message("[src] fails to fireman carry [target]!") else - to_chat(src, "You can't fireman carry [target] while they're standing!") + if (ishuman(target)) + to_chat(src, "You can't fireman carry [target] while they're standing!") + else + to_chat(src, "You can't seem to fireman carry that kind of species.") /mob/living/carbon/human/proc/piggyback(mob/living/carbon/target) if(can_piggyback(target)) diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index f9fa5d84da..042b4af4fe 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -23,7 +23,7 @@ if(!d_type) return 0 var/protection = 0 - var/list/body_parts = list(head, wear_mask, wear_suit, w_uniform, back, gloves, shoes, belt, s_store, glasses, ears, wear_id) //Everything but pockets. Pockets are l_store and r_store. (if pockets were allowed, putting something armored, gloves or hats for example, would double up on the armor) + var/list/body_parts = list(head, wear_mask, wear_suit, w_uniform, back, gloves, shoes, belt, s_store, glasses, ears, wear_id, wear_neck) //Everything but pockets. Pockets are l_store and r_store. (if pockets were allowed, putting something armored, gloves or hats for example, would double up on the armor) for(var/bp in body_parts) if(!bp) continue @@ -116,6 +116,10 @@ var/final_block_chance = w_uniform.block_chance - (CLAMP((armour_penetration-w_uniform.armour_penetration)/2,0,100)) + block_chance_modifier if(w_uniform.hit_reaction(src, AM, attack_text, final_block_chance, damage, attack_type)) return 1 + if(wear_neck) + var/final_block_chance = wear_neck.block_chance - (CLAMP((armour_penetration-wear_neck.armour_penetration)/2,0,100)) + block_chance_modifier + if(wear_neck.hit_reaction(src, AM, attack_text, final_block_chance, damage, attack_type)) + return 1 return 0 /mob/living/carbon/human/proc/check_block() @@ -388,43 +392,34 @@ return var/b_loss = 0 var/f_loss = 0 - var/bomb_armor = getarmor(null, "bomb") + var/bomb_armor = max(0,(100-getarmor(null, "bomb"))/100) switch (severity) if (1) - if(prob(bomb_armor)) - b_loss = 500 + if(bomb_armor) + b_loss = 500*bomb_armor var/atom/throw_target = get_edge_target_turf(src, get_dir(src, get_step_away(src, src))) throw_at(throw_target, 200, 4) - damage_clothes(400 - bomb_armor, BRUTE, "bomb") + damage_clothes(400*bomb_armor, BRUTE, "bomb") else - for(var/I in contents) - var/atom/A = I - A.ex_act(severity) + damage_clothes(400,BRUTE,"bomb") gib() return if (2) - b_loss = 60 - f_loss = 60 - if(bomb_armor) - b_loss = 30*(2 - round(bomb_armor*0.01, 0.05)) - f_loss = b_loss - damage_clothes(200 - bomb_armor, BRUTE, "bomb") + b_loss = 60*bomb_armor + f_loss = 60*bomb_armor + damage_clothes(200*bomb_armor, BRUTE, "bomb") if (!istype(ears, /obj/item/clothing/ears/earmuffs)) adjustEarDamage(30, 120) - if (prob(max(70 - (bomb_armor * 0.5), 0))) - Unconscious(200) + Unconscious(200*bomb_armor) if(3) - b_loss = 30 - if(bomb_armor) - b_loss = 15*(2 - round(bomb_armor*0.01, 0.05)) + b_loss = 30*bomb_armor damage_clothes(max(50 - bomb_armor, 0), BRUTE, "bomb") if (!istype(ears, /obj/item/clothing/ears/earmuffs)) adjustEarDamage(15,60) - if (prob(max(50 - (bomb_armor * 0.5), 0))) - Unconscious(160) + Unconscious(100*bomb_armor) take_overall_damage(b_loss,f_loss) diff --git a/code/modules/mob/living/carbon/human/human_helpers.dm b/code/modules/mob/living/carbon/human/human_helpers.dm index 8071d632ea..63d02af31a 100644 --- a/code/modules/mob/living/carbon/human/human_helpers.dm +++ b/code/modules/mob/living/carbon/human/human_helpers.dm @@ -119,16 +119,12 @@ . = ..() if(G.trigger_guard == TRIGGER_GUARD_NORMAL) - if(src.dna.check_mutation(HULK)) + if(HAS_TRAIT(src, TRAIT_CHUNKYFINGERS)) to_chat(src, "Your meaty finger is much too large for the trigger guard!") return FALSE if(HAS_TRAIT(src, TRAIT_NOGUNS)) to_chat(src, "Your fingers don't fit in the trigger guard!") return FALSE - if(mind) - if(mind.martial_art && mind.martial_art.no_guns) //great dishonor to famiry - to_chat(src, "Use of ranged weaponry would bring dishonor to the clan.") - return FALSE return . /* diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm index ddf258077b..7c630c7d39 100644 --- a/code/modules/mob/living/carbon/human/species.dm +++ b/code/modules/mob/living/carbon/human/species.dm @@ -1282,6 +1282,8 @@ GLOBAL_LIST_EMPTY(roundstart_race_names) //////// /datum/species/proc/handle_digestion(mob/living/carbon/human/H) + if(HAS_TRAIT(src, TRAIT_NOHUNGER)) + return //hunger is for BABIES //The fucking TRAIT_FAT mutation is the dumbest shit ever. It makes the code so difficult to work with if(HAS_TRAIT(H, TRAIT_FAT))//I share your pain, past coder. diff --git a/code/modules/mob/living/carbon/human/species_types/abductors.dm b/code/modules/mob/living/carbon/human/species_types/abductors.dm index ffd129ebf7..6e54e320ff 100644 --- a/code/modules/mob/living/carbon/human/species_types/abductors.dm +++ b/code/modules/mob/living/carbon/human/species_types/abductors.dm @@ -4,7 +4,7 @@ say_mod = "gibbers" sexes = FALSE species_traits = list(NOBLOOD,NOEYES,NOGENITALS,NOAROUSAL) - inherent_traits = list(TRAIT_VIRUSIMMUNE,TRAIT_NOGUNS,TRAIT_NOHUNGER,TRAIT_NOBREATH) + inherent_traits = list(TRAIT_VIRUSIMMUNE,TRAIT_CHUNKYFINGERS,TRAIT_NOHUNGER,TRAIT_NOBREATH) mutanttongue = /obj/item/organ/tongue/abductor /datum/species/abductor/on_species_gain(mob/living/carbon/C, datum/species/old_species) diff --git a/code/modules/mob/living/carbon/human/species_types/golems.dm b/code/modules/mob/living/carbon/human/species_types/golems.dm index 48ce55a052..3d6c5092e1 100644 --- a/code/modules/mob/living/carbon/human/species_types/golems.dm +++ b/code/modules/mob/living/carbon/human/species_types/golems.dm @@ -3,7 +3,7 @@ name = "Golem" id = "iron golem" species_traits = list(NOBLOOD,MUTCOLORS,NO_UNDERWEAR,NOGENITALS,NOAROUSAL) - inherent_traits = list(TRAIT_RESISTHEAT,TRAIT_NOBREATH,TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_NOFIRE,TRAIT_NOGUNS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER) + inherent_traits = list(TRAIT_RESISTHEAT,TRAIT_NOBREATH,TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_NOFIRE,TRAIT_CHUNKYFINGERS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER) inherent_biotypes = list(MOB_INORGANIC, MOB_HUMANOID) mutant_organs = list(/obj/item/organ/adamantine_resonator) speedmod = 2 @@ -88,7 +88,7 @@ fixed_mut_color = "a3d" meat = /obj/item/stack/ore/plasma //Can burn and takes damage from heat - inherent_traits = list(TRAIT_NOBREATH, TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_NOGUNS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER) //no RESISTHEAT, NOFIRE + inherent_traits = list(TRAIT_NOBREATH, TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_CHUNKYFINGERS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER) //no RESISTHEAT, NOFIRE info_text = "As a Plasma Golem, you burn easily. Be careful, if you get hot enough while burning, you'll blow up!" heatmod = 0 //fine until they blow up prefix = "Plasma" @@ -266,7 +266,7 @@ fixed_mut_color = "9E704B" meat = /obj/item/stack/sheet/mineral/wood //Can burn and take damage from heat - inherent_traits = list(TRAIT_NOBREATH, TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_NOGUNS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER) + inherent_traits = list(TRAIT_NOBREATH, TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_CHUNKYFINGERS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER) armor = 30 burnmod = 1.25 heatmod = 1.5 @@ -693,7 +693,7 @@ info_text = "As a Cloth Golem, you are able to reform yourself after death, provided your remains aren't burned or destroyed. You are, of course, very flammable. \ Being made of cloth, your body is magic resistant and faster than that of other golems, but weaker and less resilient." species_traits = list(NOBLOOD,NO_UNDERWEAR,NOGENITALS,NOAROUSAL) //no mutcolors, and can burn - inherent_traits = list(TRAIT_RESISTCOLD,TRAIT_NOBREATH,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER,TRAIT_NOGUNS) + inherent_traits = list(TRAIT_RESISTCOLD,TRAIT_NOBREATH,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER,TRAIT_CHUNKYFINGERS) inherent_biotypes = list(MOB_UNDEAD, MOB_HUMANOID) armor = 15 //feels no pain, but not too resistant burnmod = 2 // don't get burned @@ -893,7 +893,7 @@ special_names = list("Box") info_text = "As a Cardboard Golem, you aren't very strong, but you are a bit quicker and can easily create more brethren by using cardboard on yourself." species_traits = list(NOBLOOD,NO_UNDERWEAR,NOGENITALS,NOAROUSAL,MUTCOLORS) - inherent_traits = list(TRAIT_NOBREATH, TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_NOGUNS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER) + inherent_traits = list(TRAIT_NOBREATH, TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_CHUNKYFINGERS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER) fixed_mut_color = "ffffff" limbs_id = "c_golem" //special sprites attack_verb = "bash" @@ -936,7 +936,7 @@ name = "Leather Golem" id = "leather golem" special_names = list("Face", "Man", "Belt") //Ah dude 4 strength 4 stam leather belt AHHH - inherent_traits = list(TRAIT_NOBREATH, TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_NOGUNS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER, TRAIT_STRONG_GRABBER) + inherent_traits = list(TRAIT_NOBREATH, TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_CHUNKYFINGERS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER, TRAIT_STRONG_GRABBER) prefix = "Leather" fixed_mut_color = "624a2e" info_text = "As a Leather Golem, you are flammable, but you can grab things with incredible ease, allowing all your grabs to start at a strong level." @@ -952,7 +952,7 @@ special_names = list("Boll","Weave") species_traits = list(NOBLOOD,NO_UNDERWEAR,NOEYES) fixed_mut_color = null - inherent_traits = list(TRAIT_NOBREATH, TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_NOGUNS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER) + inherent_traits = list(TRAIT_NOBREATH, TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_CHUNKYFINGERS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER) info_text = "As a Durathread Golem, your strikes will cause those your targets to start choking, but your woven body won't withstand fire as well." /datum/species/golem/durathread/spec_unarmedattacked(mob/living/carbon/human/user, mob/living/carbon/human/target) @@ -974,7 +974,7 @@ fixed_mut_color = "ffffff" attack_verb = "rattl" species_traits = list(NOBLOOD,NO_UNDERWEAR,NOGENITALS,NOAROUSAL,MUTCOLORS) - inherent_traits = list(TRAIT_RESISTHEAT,TRAIT_NOBREATH,TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_NOFIRE,TRAIT_NOGUNS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER,TRAIT_FAKEDEATH,TRAIT_CALCIUM_HEALER) + inherent_traits = list(TRAIT_RESISTHEAT,TRAIT_NOBREATH,TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_NOFIRE,TRAIT_CHUNKYFINGERS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER,TRAIT_FAKEDEATH,TRAIT_CALCIUM_HEALER) info_text = "As a Bone Golem, You have a powerful spell that lets you chill your enemies with fear, and milk heals you! Just make sure to watch our for bone-hurting juice." var/datum/action/innate/bonechill/bonechill diff --git a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm index 7d568ed510..5f91e3ae66 100644 --- a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm @@ -44,7 +44,7 @@ C.faction |= "slime" /datum/species/jelly/spec_life(mob/living/carbon/human/H) - if(H.stat == DEAD) //can't farm slime jelly from a dead slime/jelly person indefinitely + if(H.stat == DEAD || HAS_TRAIT(H, TRAIT_NOMARROW)) //can't farm slime jelly from a dead slime/jelly person indefinitely, and no regeneration for vampires return if(!H.blood_volume) H.blood_volume += 5 @@ -116,6 +116,33 @@ return to_chat(H, "...but there is not enough of you to go around! You must attain more mass to heal!") +/datum/species/jelly/spec_death(gibbed, mob/living/carbon/human/H) + if(H) + stop_wagging_tail(H) + +/datum/species/jelly/spec_stun(mob/living/carbon/human/H,amount) + if(H) + stop_wagging_tail(H) + . = ..() + +/datum/species/jelly/can_wag_tail(mob/living/carbon/human/H) + return ("mam_tail" in mutant_bodyparts) || ("mam_waggingtail" in mutant_bodyparts) + +/datum/species/jelly/is_wagging_tail(mob/living/carbon/human/H) + return ("mam_waggingtail" in mutant_bodyparts) + +/datum/species/jelly/start_wagging_tail(mob/living/carbon/human/H) + if("mam_tail" in mutant_bodyparts) + mutant_bodyparts -= "mam_tail" + mutant_bodyparts |= "mam_waggingtail" + H.update_body() + +/datum/species/jelly/stop_wagging_tail(mob/living/carbon/human/H) + if("mam_waggingtail" in mutant_bodyparts) + mutant_bodyparts -= "mam_waggingtail" + mutant_bodyparts |= "mam_tail" + H.update_body() + ////////////////////////////////////////////////////////SLIMEPEOPLE/////////////////////////////////////////////////////////////////// //Slime people are able to split like slimes, retaining a single mind that can swap between bodies at will, even after death. @@ -177,6 +204,8 @@ bodies = old_species.bodies /datum/species/jelly/slime/spec_life(mob/living/carbon/human/H) + if((HAS_TRAIT(H, TRAIT_NOMARROW))) + return if(H.blood_volume >= BLOOD_VOLUME_SLIME_SPLIT) if(prob(5)) to_chat(H, "You feel very bloated!") @@ -410,34 +439,6 @@ heatmod = 1 burnmod = 1 -/datum/species/jelly/roundstartslime/spec_death(gibbed, mob/living/carbon/human/H) - if(H) - stop_wagging_tail(H) - -/datum/species/jelly/roundstartslime/spec_stun(mob/living/carbon/human/H,amount) - if(H) - stop_wagging_tail(H) - . = ..() - -/datum/species/jelly/roundstartslime/can_wag_tail(mob/living/carbon/human/H) - return ("mam_tail" in mutant_bodyparts) || ("mam_waggingtail" in mutant_bodyparts) - -/datum/species/jelly/roundstartslime/is_wagging_tail(mob/living/carbon/human/H) - return ("mam_waggingtail" in mutant_bodyparts) - -/datum/species/jelly/roundstartslime/start_wagging_tail(mob/living/carbon/human/H) - if("mam_tail" in mutant_bodyparts) - mutant_bodyparts -= "mam_tail" - mutant_bodyparts |= "mam_waggingtail" - H.update_body() - -/datum/species/jelly/roundstartslime/stop_wagging_tail(mob/living/carbon/human/H) - if("mam_waggingtail" in mutant_bodyparts) - mutant_bodyparts -= "mam_waggingtail" - mutant_bodyparts |= "mam_tail" - H.update_body() - - /datum/action/innate/slime_change name = "Alter Form" check_flags = AB_CHECK_CONSCIOUS @@ -838,19 +839,16 @@ link_minds = new(src) link_minds.Grant(C) slimelink_owner = C - link_mob(C) + link_mob(C, TRUE) -/datum/species/jelly/stargazer/proc/link_mob(mob/living/M) - if(QDELETED(M) || M.stat == DEAD) +/datum/species/jelly/stargazer/proc/link_mob(mob/living/M, selflink = FALSE) + if(QDELETED(M) || (M in linked_mobs)) return FALSE - if(HAS_TRAIT(M, TRAIT_MINDSHIELD)) //mindshield implant, no dice - return FALSE - if(M.anti_magic_check(FALSE, FALSE, TRUE, 0)) - return FALSE - if(M in linked_mobs) + if(!selflink && (M.stat == DEAD || HAS_TRAIT(M, TRAIT_MINDSHIELD) || M.anti_magic_check(FALSE, FALSE, TRUE, 0))) return FALSE linked_mobs.Add(M) - to_chat(M, "You are now connected to [slimelink_owner.real_name]'s Slime Link.") + if(!selflink) + to_chat(M, "You are now connected to [slimelink_owner.real_name]'s Slime Link.") var/datum/action/innate/linked_speech/action = new(src) linked_actions.Add(action) action.Grant(M) diff --git a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm index c0973102d6..9317a51050 100644 --- a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm @@ -94,7 +94,7 @@ id = "ashlizard" limbs_id = "lizard" species_traits = list(MUTCOLORS,EYECOLOR,LIPS,DIGITIGRADE) - inherent_traits = list(TRAIT_NOGUNS) + inherent_traits = list(TRAIT_CHUNKYFINGERS) mutantlungs = /obj/item/organ/lungs/ashwalker burnmod = 0.9 brutemod = 0.9 diff --git a/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm b/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm index 90f63467a0..3247c96632 100644 --- a/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm @@ -39,7 +39,7 @@ blacklisted = TRUE no_equip = list(SLOT_WEAR_MASK, SLOT_WEAR_SUIT, SLOT_GLOVES, SLOT_SHOES, SLOT_W_UNIFORM, SLOT_S_STORE) species_traits = list(NOBLOOD,NO_UNDERWEAR,NO_DNA_COPY,NOTRANSSTING,NOEYES,NOGENITALS,NOAROUSAL) - inherent_traits = list(TRAIT_RESISTCOLD,TRAIT_NOBREATH,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_NOGUNS,TRAIT_RADIMMUNE,TRAIT_VIRUSIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER,TRAIT_NOHUNGER) + inherent_traits = list(TRAIT_RESISTCOLD,TRAIT_NOBREATH,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_CHUNKYFINGERS,TRAIT_RADIMMUNE,TRAIT_VIRUSIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER,TRAIT_NOHUNGER) mutanteyes = /obj/item/organ/eyes/night_vision/nightmare mutant_organs = list(/obj/item/organ/heart/nightmare) mutant_brain = /obj/item/organ/brain/nightmare diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm index 16a9f07ceb..0061c7ed70 100644 --- a/code/modules/mob/living/carbon/life.dm +++ b/code/modules/mob/living/carbon/life.dm @@ -659,6 +659,9 @@ GLOBAL_LIST_INIT(ballmer_windows_me_msg, list("Yo man, what if, we like, uh, put //used in human and monkey handle_environment() /mob/living/carbon/proc/natural_bodytemperature_stabilization() + if (HAS_TRAIT(src, TRAIT_COLDBLOODED)) + return 0 //Return 0 as your natural temperature. Species proc handle_environment() will adjust your temperature based on this. + var/body_temperature_difference = BODYTEMP_NORMAL - bodytemperature switch(bodytemperature) if(-INFINITY to BODYTEMP_COLD_DAMAGE_LIMIT) //Cold damage limit is 50 below the default, the temperature where you start to feel effects. diff --git a/code/modules/mob/living/death.dm b/code/modules/mob/living/death.dm index 0ff418d628..8345ef916d 100644 --- a/code/modules/mob/living/death.dm +++ b/code/modules/mob/living/death.dm @@ -62,12 +62,8 @@ unset_machine() timeofdeath = world.time tod = STATION_TIME_TIMESTAMP("hh:mm:ss") - var/turf/T = get_turf(src) for(var/obj/item/I in contents) I.on_mob_death(src, gibbed) - if(mind && mind.name && mind.active && !istype(T.loc, /area/ctf)) - var/rendered = "[mind.name] has died at [get_area_name(T)]." - deadchat_broadcast(rendered, follow_target = src, turf_target = T, message_type=DEADCHAT_DEATHRATTLE) if(mind) mind.store_memory("Time of death: [tod]", 0) GLOB.alive_mob_list -= src @@ -89,7 +85,12 @@ addtimer(CALLBACK(src, .proc/med_hud_set_status), (DEFIB_TIME_LIMIT * 10) + 1) stop_pulling() - SEND_SIGNAL(src, COMSIG_MOB_DEATH, gibbed) + var/signal = SEND_SIGNAL(src, COMSIG_MOB_DEATH, gibbed) + + var/turf/T = get_turf(src) + if(mind && mind.name && mind.active && !istype(T.loc, /area/ctf) && !(signal & COMPONENT_BLOCK_DEATH_BROADCAST)) + var/rendered = "[mind.name] has died at [get_area_name(T)]." + deadchat_broadcast(rendered, follow_target = src, turf_target = T, message_type=DEADCHAT_DEATHRATTLE) if (client) client.move_delay = initial(client.move_delay) diff --git a/code/modules/mob/living/emote.dm b/code/modules/mob/living/emote.dm index 6c1a2cfec9..041b367ebf 100644 --- a/code/modules/mob/living/emote.dm +++ b/code/modules/mob/living/emote.dm @@ -87,6 +87,12 @@ message_simple = S.deathmessage . = ..() message_simple = initial(message_simple) + if(. && user.deathsound) + if(isliving(user)) + var/mob/living/L = user + if(!L.can_speak_vocal() || L.oxyloss >= 50) + return //stop the sound if oxyloss too high/cant speak + playsound(user, user.deathsound, 200, TRUE, TRUE) if(. && isalienadult(user)) playsound(user.loc, 'sound/voice/hiss6.ogg', 80, 1, 1) diff --git a/code/modules/mob/living/silicon/ai/say.dm b/code/modules/mob/living/silicon/ai/say.dm index 112add367f..f757203237 100644 --- a/code/modules/mob/living/silicon/ai/say.dm +++ b/code/modules/mob/living/silicon/ai/say.dm @@ -49,7 +49,7 @@ else padloc = "(UNKNOWN)" src.log_talk(message, LOG_SAY, tag="HOLOPAD in [padloc]") - send_speech(message, 7, T, "robot", language) + send_speech(message, 7, T, "robot", message_language = language) to_chat(src, "Holopad transmitted, [real_name] \"[message]\"") else to_chat(src, "No holopad connected.") diff --git a/code/modules/mob/living/silicon/login.dm b/code/modules/mob/living/silicon/login.dm index 82c1435344..81f8fcbef1 100644 --- a/code/modules/mob/living/silicon/login.dm +++ b/code/modules/mob/living/silicon/login.dm @@ -4,4 +4,7 @@ var/datum/antagonist/rev/rev = mind.has_antag_datum(/datum/antagonist/rev) if(rev) rev.remove_revolutionary(TRUE) + var/datum/antagonist/bloodsucker/V = mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(V) + mind.remove_antag_datum(V) ..() diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm index 0484e39c1a..c5c2beb999 100644 --- a/code/modules/mob/living/silicon/robot/robot.dm +++ b/code/modules/mob/living/silicon/robot/robot.dm @@ -112,6 +112,8 @@ var/bellyup = 0 var/dogborg = FALSE + var/cansprint = 1 + /mob/living/silicon/robot/get_cell() return cell @@ -159,6 +161,7 @@ else if(!mmi || !mmi.brainmob) mmi = new (src) mmi.brain = new /obj/item/organ/brain(mmi) + mmi.brain.organ_flags |= ORGAN_FROZEN mmi.brain.name = "[real_name]'s brain" mmi.icon_state = "mmi_full" mmi.name = "Man-Machine Interface: [real_name]" @@ -233,7 +236,6 @@ var/list/modulelist = list("Standard" = /obj/item/robot_module/standard, \ "Engineering" = /obj/item/robot_module/engineering, \ "Medical" = /obj/item/robot_module/medical, \ - "Medihound" = /obj/item/robot_module/medihound, \ "Miner" = /obj/item/robot_module/miner, \ "Service" = /obj/item/robot_module/butler) if(!CONFIG_GET(flag/disable_peaceborg)) @@ -832,7 +834,7 @@ robot_suit.head.flash2.burn_out() robot_suit.head.flash2 = null robot_suit.head = null - robot_suit.updateicon() + robot_suit.update_icon() else new /obj/item/robot_suit(T) new /obj/item/bodypart/l_leg/robot(T) diff --git a/code/modules/mob/living/silicon/robot/robot_modules.dm b/code/modules/mob/living/silicon/robot/robot_modules.dm index 56011cb886..ccb879870a 100644 --- a/code/modules/mob/living/silicon/robot/robot_modules.dm +++ b/code/modules/mob/living/silicon/robot/robot_modules.dm @@ -22,7 +22,7 @@ var/moduleselect_icon = "nomod" - var/can_be_pushed = TRUE + var/can_be_pushed = FALSE var/magpulsing = FALSE var/clean_on_move = FALSE @@ -139,7 +139,7 @@ //Adds flavoursome dogborg items to dogborg variants without mechanical benefits /obj/item/robot_module/proc/dogborg_equip() - if(istype(src, /obj/item/robot_module/k9) || istype(src, /obj/item/robot_module/medihound)) + if(istype(src, /obj/item/robot_module/k9)) return //Bandaid fix to prevent stacking until I merge these two modules into their base types var/obj/item/I = new /obj/item/analyzer/nose/flavour(src) basic_modules += I @@ -322,19 +322,21 @@ /obj/item/stack/medical/gauze/cyborg, /obj/item/organ_storage, /obj/item/borg/lollipop, - /obj/item/sensor_device) + /obj/item/sensor_device, + /obj/item/twohanded/shockpaddles/cyborg) emag_modules = list(/obj/item/reagent_containers/borghypo/hacked) ratvar_modules = list( /obj/item/clockwork/slab/cyborg/medical, /obj/item/clockwork/weapon/ratvarian_spear) cyborg_base_icon = "medical" moduleselect_icon = "medical" - can_be_pushed = FALSE hat_offset = 3 /obj/item/robot_module/medical/be_transformed_to(obj/item/robot_module/old_module) var/mob/living/silicon/robot/R = loc - var/borg_icon = input(R, "Select an icon!", "Robot Icon", null) as null|anything in list("Default", "Heavy", "Sleek", "Marina", "Droid", "Eyebot") + var/borg_icon = input(R, "Select an icon!", "Robot Icon", null) as null|anything in list("Default", "Heavy", "Sleek", "Marina", "Droid", "Eyebot", "Medihound", "Medihound Dark", "Vale") + if(R.client && R.client.ckey in list("nezuli")) + borg_icon += "Alina" if(!borg_icon) return FALSE switch(borg_icon) @@ -356,59 +358,46 @@ if("Heavy") cyborg_base_icon = "heavymed" cyborg_icon_override = 'modular_citadel/icons/mob/robots.dmi' - return ..() - -/obj/item/robot_module/medihound - name = "MediHound" - basic_modules = list( - /obj/item/dogborg/jaws/small, - /obj/item/storage/bag/borgdelivery, - /obj/item/analyzer/nose, - /obj/item/soap/tongue, - /obj/item/extinguisher/mini, - /obj/item/healthanalyzer, - /obj/item/dogborg/sleeper/medihound, - /obj/item/roller/robo, - /obj/item/reagent_containers/borghypo, - /obj/item/twohanded/shockpaddles/cyborg/hound, - /obj/item/stack/medical/gauze/cyborg, - /obj/item/pinpointer/crew, - /obj/item/sensor_device) - emag_modules = list(/obj/item/dogborg/pounce) - ratvar_modules = list(/obj/item/clockwork/slab/cyborg/medical, - /obj/item/clockwork/weapon/ratvarian_spear) - cyborg_base_icon = "medihound" - moduleselect_icon = "medihound" - moduleselect_alternate_icon = 'modular_citadel/icons/ui/screen_cyborg.dmi' - can_be_pushed = FALSE - hat_offset = INFINITY - sleeper_overlay = "msleeper" - cyborg_icon_override = 'modular_citadel/icons/mob/widerobot.dmi' - has_snowflake_deadsprite = TRUE - dogborg = TRUE - cyborg_pixel_offset = -16 - -/obj/item/robot_module/medihound/be_transformed_to(obj/item/robot_module/old_module) - var/mob/living/silicon/robot/R = loc - var/list/medhoundmodels = list("Default", "Dark", "Vale") - if(R.client && R.client.ckey in list("nezuli")) - medhoundmodels += "Alina" - var/borg_icon = input(R, "Select an icon!", "Robot Icon", null) as null|anything in medhoundmodels - if(!borg_icon) - return FALSE - switch(borg_icon) - if("Default") + if("Medihound") cyborg_base_icon = "medihound" - if("Dark") + cyborg_icon_override = 'modular_citadel/icons/mob/widerobot.dmi' + moduleselect_icon = "medihound" + moduleselect_alternate_icon = 'modular_citadel/icons/ui/screen_cyborg.dmi' + has_snowflake_deadsprite = TRUE + dogborg = TRUE + cyborg_pixel_offset = -16 + hat_offset = INFINITY + if("Medihound Dark") cyborg_base_icon = "medihounddark" + cyborg_icon_override = 'modular_citadel/icons/mob/widerobot.dmi' sleeper_overlay = "mdsleeper" + moduleselect_icon = "medihound" + moduleselect_alternate_icon = 'modular_citadel/icons/ui/screen_cyborg.dmi' + has_snowflake_deadsprite = TRUE + dogborg = TRUE + cyborg_pixel_offset = -16 + hat_offset = INFINITY if("Vale") cyborg_base_icon = "valemed" + cyborg_icon_override = 'modular_citadel/icons/mob/widerobot.dmi' sleeper_overlay = "valemedsleeper" + moduleselect_icon = "medihound" + moduleselect_alternate_icon = 'modular_citadel/icons/ui/screen_cyborg.dmi' + has_snowflake_deadsprite = TRUE + dogborg = TRUE + cyborg_pixel_offset = -16 + hat_offset = INFINITY if("Alina") cyborg_base_icon = "alina-med" + cyborg_icon_override = 'modular_citadel/icons/mob/widerobot.dmi' special_light_key = "alina" sleeper_overlay = "alinasleeper" + moduleselect_icon = "medihound" + moduleselect_alternate_icon = 'modular_citadel/icons/ui/screen_cyborg.dmi' + has_snowflake_deadsprite = TRUE + dogborg = TRUE + cyborg_pixel_offset = -16 + hat_offset = INFINITY return ..() /obj/item/robot_module/engineering @@ -489,7 +478,6 @@ cyborg_icon_override = 'modular_citadel/icons/mob/robots.dmi' if("Pup Dozer") cyborg_base_icon = "pupdozer" - can_be_pushed = FALSE hat_offset = INFINITY cyborg_icon_override = 'modular_citadel/icons/mob/widerobot.dmi' has_snowflake_deadsprite = TRUE @@ -498,7 +486,6 @@ sleeper_overlay = "dozersleeper" if("Vale") cyborg_base_icon = "valeeng" - can_be_pushed = FALSE hat_offset = INFINITY cyborg_icon_override = 'modular_citadel/icons/mob/widerobot.dmi' has_snowflake_deadsprite = TRUE @@ -508,7 +495,6 @@ if("Alina") cyborg_base_icon = "alina-eng" special_light_key = "alina" - can_be_pushed = FALSE hat_offset = INFINITY cyborg_icon_override = 'modular_citadel/icons/mob/widerobot.dmi' has_snowflake_deadsprite = TRUE @@ -533,7 +519,6 @@ /obj/item/clockwork/weapon/ratvarian_spear) cyborg_base_icon = "sec" moduleselect_icon = "security" - can_be_pushed = FALSE hat_offset = 3 /obj/item/robot_module/security/do_transform_animation() @@ -589,7 +574,6 @@ cyborg_base_icon = "k9" moduleselect_icon = "k9" moduleselect_alternate_icon = 'modular_citadel/icons/ui/screen_cyborg.dmi' - can_be_pushed = FALSE hat_offset = INFINITY sleeper_overlay = "ksleeper" cyborg_icon_override = 'modular_citadel/icons/mob/widerobot.dmi' @@ -652,7 +636,6 @@ /obj/item/clockwork/weapon/ratvarian_spear) cyborg_base_icon = "peace" moduleselect_icon = "standard" - can_be_pushed = FALSE hat_offset = -2 /obj/item/robot_module/peacekeeper/do_transform_animation() @@ -947,7 +930,6 @@ /obj/item/clockwork/weapon/ratvarian_spear) cyborg_base_icon = "synd_sec" moduleselect_icon = "malf" - can_be_pushed = FALSE hat_offset = 3 /obj/item/robot_module/syndicate/rebuild_modules() @@ -987,7 +969,6 @@ /obj/item/clockwork/weapon/ratvarian_spear) cyborg_base_icon = "synd_medical" moduleselect_icon = "malf" - can_be_pushed = FALSE hat_offset = 3 /obj/item/robot_module/saboteur @@ -1026,7 +1007,6 @@ cyborg_base_icon = "synd_engi" moduleselect_icon = "malf" - can_be_pushed = FALSE magpulsing = TRUE hat_offset = -4 canDispose = TRUE diff --git a/code/modules/mob/living/simple_animal/astral.dm b/code/modules/mob/living/simple_animal/astral.dm index 2aafedb149..3d0c335989 100644 --- a/code/modules/mob/living/simple_animal/astral.dm +++ b/code/modules/mob/living/simple_animal/astral.dm @@ -27,6 +27,7 @@ unsuitable_atmos_damage = 0 minbodytemp = 0 maxbodytemp = 100000 + blood_volume = 0 /mob/living/simple_animal/astral/death() icon_state = "shade_dead" diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm index e61cfad068..7a653f4aa4 100644 --- a/code/modules/mob/living/simple_animal/bot/bot.dm +++ b/code/modules/mob/living/simple_animal/bot/bot.dm @@ -14,6 +14,7 @@ atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) maxbodytemp = INFINITY minbodytemp = 0 + blood_volume = 0 has_unlimited_silicon_privilege = 1 sentience_type = SENTIENCE_ARTIFICIAL status_flags = NONE //no default canpush diff --git a/code/modules/mob/living/simple_animal/constructs.dm b/code/modules/mob/living/simple_animal/constructs.dm index b7067f8904..be900aa4ef 100644 --- a/code/modules/mob/living/simple_animal/constructs.dm +++ b/code/modules/mob/living/simple_animal/constructs.dm @@ -33,6 +33,7 @@ initial_language_holder = /datum/language_holder/construct deathmessage = "collapses in a shattered heap." hud_type = /datum/hud/constructs + blood_volume = 0 var/list/construct_spells = list() var/playstyle_string = "You are a generic construct! Your job is to not exist, and you should probably adminhelp this." var/master = null @@ -459,4 +460,3 @@ hud_used.healths.icon_state = "[icon_state]_health5" else hud_used.healths.icon_state = "[icon_state]_health6" - diff --git a/code/modules/mob/living/simple_animal/friendly/cockroach.dm b/code/modules/mob/living/simple_animal/friendly/cockroach.dm index c85890df88..26d4691d83 100644 --- a/code/modules/mob/living/simple_animal/friendly/cockroach.dm +++ b/code/modules/mob/living/simple_animal/friendly/cockroach.dm @@ -3,6 +3,7 @@ desc = "This station is just crawling with bugs." icon_state = "cockroach" icon_dead = "cockroach" + blood_volume = 50 health = 1 maxHealth = 1 turns_per_move = 5 diff --git a/code/modules/mob/living/simple_animal/friendly/crab.dm b/code/modules/mob/living/simple_animal/friendly/crab.dm index d7d673ff8b..9c3e5b5def 100644 --- a/code/modules/mob/living/simple_animal/friendly/crab.dm +++ b/code/modules/mob/living/simple_animal/friendly/crab.dm @@ -8,6 +8,7 @@ speak_emote = list("clicks") emote_hear = list("clicks.") emote_see = list("clacks.") + blood_volume = 350 speak_chance = 1 turns_per_move = 5 butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab = 1) diff --git a/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm b/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm index 387973b979..7e0e43055f 100644 --- a/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm +++ b/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm @@ -50,6 +50,7 @@ dextrous_hud_type = /datum/hud/dextrous/drone lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE see_in_dark = 7 + blood_volume = 0 can_be_held = TRUE held_items = list(null, null) var/staticChoice = "static" diff --git a/code/modules/mob/living/simple_animal/friendly/mouse.dm b/code/modules/mob/living/simple_animal/friendly/mouse.dm index bf45c9cc73..5b962b2dd8 100644 --- a/code/modules/mob/living/simple_animal/friendly/mouse.dm +++ b/code/modules/mob/living/simple_animal/friendly/mouse.dm @@ -10,6 +10,7 @@ emote_see = list("runs in a circle.", "shakes.") speak_chance = 1 turns_per_move = 5 + blood_volume = 250 see_in_dark = 6 maxHealth = 5 health = 5 diff --git a/code/modules/mob/living/simple_animal/guardian/guardian.dm b/code/modules/mob/living/simple_animal/guardian/guardian.dm index e3ef14c784..1bd434b233 100644 --- a/code/modules/mob/living/simple_animal/guardian/guardian.dm +++ b/code/modules/mob/living/simple_animal/guardian/guardian.dm @@ -20,6 +20,7 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians icon_living = "magicOrange" icon_dead = "magicOrange" speed = 0 + blood_volume = 0 a_intent = INTENT_HARM stop_automated_movement = 1 movement_type = FLYING // Immunity to chasms and landmines, etc. diff --git a/code/modules/mob/living/simple_animal/guardian/types/explosive.dm b/code/modules/mob/living/simple_animal/guardian/types/explosive.dm index b1af34eb02..e9f767376a 100644 --- a/code/modules/mob/living/simple_animal/guardian/types/explosive.dm +++ b/code/modules/mob/living/simple_animal/guardian/types/explosive.dm @@ -29,6 +29,7 @@ /mob/living/simple_animal/hostile/guardian/bomb/AltClickOn(atom/movable/A) if(!istype(A)) + altclick_listed_turf(A) return if(loc == summoner) to_chat(src, "You must be manifested to create bombs!") diff --git a/code/modules/mob/living/simple_animal/guardian/types/support.dm b/code/modules/mob/living/simple_animal/guardian/types/support.dm index d31809e9aa..b9783ed116 100644 --- a/code/modules/mob/living/simple_animal/guardian/types/support.dm +++ b/code/modules/mob/living/simple_animal/guardian/types/support.dm @@ -105,8 +105,9 @@ /mob/living/simple_animal/hostile/guardian/healer/AltClickOn(atom/movable/A) if(!istype(A)) + altclick_listed_turf(A) return - if(src.loc == summoner) + if(loc == summoner) to_chat(src, "You must be manifested to warp a target!") return if(!beacon) diff --git a/code/modules/mob/living/simple_animal/hostile/faithless.dm b/code/modules/mob/living/simple_animal/hostile/faithless.dm index 4e7cb0ac70..bc766f7409 100644 --- a/code/modules/mob/living/simple_animal/hostile/faithless.dm +++ b/code/modules/mob/living/simple_animal/hostile/faithless.dm @@ -19,6 +19,7 @@ spacewalk = TRUE stat_attack = UNCONSCIOUS robust_searching = 1 + blood_volume = 0 harm_intent_damage = 10 obj_damage = 50 @@ -42,4 +43,4 @@ var/mob/living/carbon/C = target C.Knockdown(60) C.visible_message("\The [src] knocks down \the [C]!", \ - "\The [src] knocks you down!") \ No newline at end of file + "\The [src] knocks you down!") diff --git a/code/modules/mob/living/simple_animal/hostile/hivebot.dm b/code/modules/mob/living/simple_animal/hostile/hivebot.dm index ece5d7e24c..a9576a467b 100644 --- a/code/modules/mob/living/simple_animal/hostile/hivebot.dm +++ b/code/modules/mob/living/simple_animal/hostile/hivebot.dm @@ -28,6 +28,7 @@ gold_core_spawnable = HOSTILE_SPAWN del_on_death = 1 loot = list(/obj/effect/decal/cleanable/robot_debris) + blood_volume = 0 do_footstep = TRUE diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm index 0377efd16e..bd4f334476 100644 --- a/code/modules/mob/living/simple_animal/hostile/hostile.dm +++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm @@ -427,7 +427,7 @@ if(casingtype) var/obj/item/ammo_casing/casing = new casingtype(startloc) playsound(src, projectilesound, 100, 1) - casing.fire_casing(targeted_atom, src, null, null, null, ran_zone(), src) + casing.fire_casing(targeted_atom, src, null, null, null, ran_zone(), 0, src) else if(projectiletype) var/obj/item/projectile/P = new projectiletype(startloc) playsound(src, projectilesound, 100, 1) diff --git a/code/modules/mob/living/simple_animal/hostile/jungle/seedling.dm b/code/modules/mob/living/simple_animal/hostile/jungle/seedling.dm index 1a894734d8..38dd17a09b 100644 --- a/code/modules/mob/living/simple_animal/hostile/jungle/seedling.dm +++ b/code/modules/mob/living/simple_animal/hostile/jungle/seedling.dm @@ -30,6 +30,7 @@ robust_searching = TRUE stat_attack = UNCONSCIOUS anchored = TRUE + blood_volume = 0 var/combatant_state = SEEDLING_STATE_NEUTRAL var/obj/seedling_weakpoint/weak_point var/mob/living/beam_debuff_target diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm index 78dc050ee0..0e3cde5628 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm @@ -267,6 +267,7 @@ Difficulty: Medium /mob/living/simple_animal/hostile/megafauna/dragon/AltClickOn(atom/movable/A) if(!istype(A)) + altclick_listed_turf(A) return if(swoop_cooldown >= world.time) to_chat(src, "You need to wait 20 seconds between swoop attacks!") diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm index cca39cfea6..397d40925b 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm @@ -50,6 +50,7 @@ Difficulty: Normal armour_penetration = 75 melee_damage_lower = 15 melee_damage_upper = 20 + blood_volume = 0 speed = 1 move_to_delay = 11 ranged = 1 @@ -441,7 +442,7 @@ Difficulty: Normal /mob/living/simple_animal/hostile/megafauna/hierophant/AltClickOn(atom/A) //player control handler(don't give this to a player holy fuck) if(!istype(A) || get_dist(A, src) <= 2) - return + return altclick_listed_turf(A) blink(A) //Hierophant overlays diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/curse_blob.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/curse_blob.dm index 20916c9311..51919dad24 100644 --- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/curse_blob.dm +++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/curse_blob.dm @@ -22,6 +22,7 @@ environment_smash = ENVIRONMENT_SMASH_NONE sentience_type = SENTIENCE_BOSS layer = LARGE_MOB_LAYER + blood_volume = 0 var/doing_move_loop = FALSE var/mob/living/set_target var/timerid diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm new file mode 100644 index 0000000000..04a1b4a468 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm @@ -0,0 +1,366 @@ +#define TUMOR_INACTIVE 0 +#define TUMOR_ACTIVE 1 +#define TUMOR_PASSIVE 2 + +//Elite mining mobs +/mob/living/simple_animal/hostile/asteroid/elite + name = "elite" + desc = "An elite monster, found in one of the strange tumors on lavaland." + icon = 'icons/mob/lavaland/lavaland_elites.dmi' + faction = list("boss") + robust_searching = TRUE + ranged_ignores_vision = TRUE + ranged = TRUE + obj_damage = 5 + vision_range = 6 + aggro_vision_range = 18 + environment_smash = ENVIRONMENT_SMASH_NONE //This is to prevent elites smashing up the mining station, we'll make sure they can smash minerals fine below. + harm_intent_damage = 0 //Punching elites gets you nowhere + stat_attack = UNCONSCIOUS + layer = LARGE_MOB_LAYER + sentience_type = SENTIENCE_BOSS + hud_type = /datum/hud/lavaland_elite + var/chosen_attack = 1 + var/list/attack_action_types = list() + var/can_talk = FALSE + var/obj/loot_drop = null + +//Gives player-controlled variants the ability to swap attacks +/mob/living/simple_animal/hostile/asteroid/elite/Initialize(mapload) + . = ..() + for(var/action_type in attack_action_types) + var/datum/action/innate/elite_attack/attack_action = new action_type() + attack_action.Grant(src) + +//Prevents elites from attacking members of their faction (can't hurt themselves either) and lets them mine rock with an attack despite not being able to smash walls. +/mob/living/simple_animal/hostile/asteroid/elite/AttackingTarget() + if(istype(target, /mob/living/simple_animal/hostile)) + var/mob/living/simple_animal/hostile/M = target + if(faction_check_mob(M)) + return FALSE + if(istype(target, /obj/structure/elite_tumor)) + var/obj/structure/elite_tumor/T = target + if(T.mychild == src && T.activity == TUMOR_PASSIVE) + var/elite_remove = alert("Re-enter the tumor?", "Despawn yourself?", "Yes", "No") + if(elite_remove == "No" || !src || QDELETED(src)) + return + T.mychild = null + T.activity = TUMOR_INACTIVE + T.icon_state = "advanced_tumor" + qdel(src) + return FALSE + . = ..() + if(ismineralturf(target)) + var/turf/closed/mineral/M = target + M.gets_drilled() + +//Elites can't talk (normally)! +/mob/living/simple_animal/hostile/asteroid/elite/say(message, bubble_type, var/list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null) + if(can_talk) + . = ..() + return TRUE + return FALSE + +/*Basic setup for elite attacks, based on Whoneedspace's megafauna attack setup. +While using this makes the system rely on OnFire, it still gives options for timers not tied to OnFire, and it makes using attacks consistent accross the board for player-controlled elites.*/ + +/datum/action/innate/elite_attack + name = "Elite Attack" + icon_icon = 'icons/mob/actions/actions_elites.dmi' + button_icon_state = "" + background_icon_state = "bg_default" + var/mob/living/simple_animal/hostile/asteroid/elite/M + var/chosen_message + var/chosen_attack_num = 0 + +/datum/action/innate/elite_attack/Grant(mob/living/L) + if(istype(L, /mob/living/simple_animal/hostile/asteroid/elite)) + M = L + return ..() + return FALSE + +/datum/action/innate/elite_attack/Activate() + M.chosen_attack = chosen_attack_num + to_chat(M, chosen_message) + +/mob/living/simple_animal/hostile/asteroid/elite/updatehealth() + . = ..() + update_health_hud() + +/mob/living/simple_animal/hostile/asteroid/elite/update_health_hud() + if(hud_used) + var/severity = 0 + var/healthpercent = (health/maxHealth) * 100 + switch(healthpercent) + if(100 to INFINITY) + hud_used.healths.icon_state = "elite_health0" + if(80 to 100) + severity = 1 + if(60 to 80) + severity = 2 + if(40 to 60) + severity = 3 + if(20 to 40) + severity = 4 + if(10 to 20) + severity = 5 + if(1 to 20) + severity = 6 + else + severity = 7 + hud_used.healths.icon_state = "elite_health[severity]" + if(severity > 0) + overlay_fullscreen("brute", /obj/screen/fullscreen/brute, severity) + else + clear_fullscreen("brute") + +//The Pulsing Tumor, the actual "spawn-point" of elites, handles the spawning, arena, and procs for dealing with basic scenarios. + +/obj/structure/elite_tumor + name = "pulsing tumor" + desc = "An odd, pulsing tumor sticking out of the ground. You feel compelled to reach out and touch it..." + armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 100, "bio" = 100, "rad" = 100, "fire" = 100, "acid" = 100) + resistance_flags = INDESTRUCTIBLE + var/activity = TUMOR_INACTIVE + var/boosted = FALSE + var/times_won = 0 + var/mob/living/carbon/human/activator = null + var/mob/living/simple_animal/hostile/asteroid/elite/mychild = null + var/potentialspawns = list(/mob/living/simple_animal/hostile/asteroid/elite/broodmother, + /mob/living/simple_animal/hostile/asteroid/elite/pandora, + /mob/living/simple_animal/hostile/asteroid/elite/legionnaire, + /mob/living/simple_animal/hostile/asteroid/elite/herald) + icon = 'icons/obj/lavaland/tumor.dmi' + icon_state = "tumor" + pixel_x = -16 + light_color = LIGHT_COLOR_RED + light_range = 3 + anchored = TRUE + density = FALSE + var/obj/item/gps/internal = null + +/obj/item/gps/internal/elite + icon_state = null + gpstag = "Menacing Signal" + desc = "You're not quite sure how a signal can be menacing." + invisibility = 100 + +/obj/structure/elite_tumor/attack_hand(mob/user) + . = ..() + if(ishuman(user)) + switch(activity) + if(TUMOR_PASSIVE) + activity = TUMOR_ACTIVE + visible_message("[src] convulses as your arm enters its radius. Your instincts tell you to step back.") + activator = user + if(boosted) + mychild.playsound_local(get_turf(mychild), 'sound/effects/magic.ogg', 40, 0) + to_chat(mychild, "Someone has activated your tumor. You will be returned to fight shortly, get ready!") + addtimer(CALLBACK(src, .proc/return_elite), 30) + INVOKE_ASYNC(src, .proc/arena_checks) + if(TUMOR_INACTIVE) + activity = TUMOR_ACTIVE + var/mob/dead/observer/elitemind = null + visible_message("[src] begins to convulse. Your instincts tell you to step back.") + activator = user + if(!boosted) + addtimer(CALLBACK(src, .proc/spawn_elite), 30) + return + visible_message("Something within [src] stirs...") + var/list/candidates = pollCandidatesForMob("Do you want to play as a lavaland elite?", ROLE_SENTIENCE, null, ROLE_SENTIENCE, 50, src, POLL_IGNORE_SENTIENCE_POTION) + if(candidates.len) + audible_message("The stirring sounds increase in volume!") + elitemind = pick(candidates) + elitemind.playsound_local(get_turf(elitemind), 'sound/effects/magic.ogg', 40, 0) + to_chat(elitemind, "You have been chosen to play as a Lavaland Elite.\nIn a few seconds, you will be summoned on Lavaland as a monster to fight your activator, in a fight to the death.\nYour attacks can be switched using the buttons on the top left of the HUD, and used by clicking on targets or tiles similar to a gun.\nWhile the opponent might have an upper hand with powerful mining equipment and tools, you have great power normally limited by AI mobs.\nIf you want to win, you'll have to use your powers in creative ways to ensure the kill. It's suggested you try using them all as soon as possible.\nShould you win, you'll receive extra information regarding what to do after. Good luck!") + addtimer(CALLBACK(src, .proc/spawn_elite, elitemind), 100) + else + visible_message("The stirring stops, and nothing emerges. Perhaps try again later.") + activity = TUMOR_INACTIVE + activator = null + + +obj/structure/elite_tumor/proc/spawn_elite(var/mob/dead/observer/elitemind) + var/selectedspawn = pick(potentialspawns) + mychild = new selectedspawn(loc) + visible_message("[mychild] emerges from [src]!") + playsound(loc,'sound/effects/phasein.ogg', 200, 0, 50, TRUE, TRUE) + if(boosted) + mychild.key = elitemind.key + mychild.sentience_act() + icon_state = "tumor_popped" + INVOKE_ASYNC(src, .proc/arena_checks) + +obj/structure/elite_tumor/proc/return_elite() + mychild.forceMove(loc) + visible_message("[mychild] emerges from [src]!") + playsound(loc,'sound/effects/phasein.ogg', 200, 0, 50, TRUE, TRUE) + mychild.revive(full_heal = TRUE, admin_revive = TRUE) + if(boosted) + mychild.maxHealth = mychild.maxHealth * 2 + mychild.health = mychild.maxHealth + +/obj/structure/elite_tumor/Initialize(mapload) + . = ..() + internal = new/obj/item/gps/internal/elite(src) + START_PROCESSING(SSobj, src) + +/obj/structure/elite_tumor/Destroy() + STOP_PROCESSING(SSobj, src) + mychild = null + activator = null + return ..() + +/obj/structure/elite_tumor/process() + if(isturf(loc)) + for(var/mob/living/simple_animal/hostile/asteroid/elite/elitehere in loc) + if(elitehere == mychild && activity == TUMOR_PASSIVE) + mychild.adjustHealth(-mychild.maxHealth*0.05) + var/obj/effect/temp_visual/heal/H = new /obj/effect/temp_visual/heal(get_turf(mychild)) + H.color = "#FF0000" + +/obj/structure/elite_tumor/attackby(obj/item/I, mob/user, params) + . = ..() + if(istype(I, /obj/item/organ/regenerative_core) && activity == TUMOR_INACTIVE && !boosted) + var/obj/item/organ/regenerative_core/core = I + if(!core.preserved) + return + visible_message("As [user] drops the core into [src], [src] appears to swell.") + icon_state = "advanced_tumor" + boosted = TRUE + light_range = 6 + desc = "[desc] This one seems to glow with a strong intensity." + qdel(core) + return TRUE + +/obj/structure/elite_tumor/proc/arena_checks() + if(activity != TUMOR_ACTIVE || QDELETED(src)) + return + INVOKE_ASYNC(src, .proc/fighters_check) //Checks to see if our fighters died. + INVOKE_ASYNC(src, .proc/arena_trap) //Gets another arena trap queued up for when this one runs out. + INVOKE_ASYNC(src, .proc/border_check) //Checks to see if our fighters got out of the arena somehow. + addtimer(CALLBACK(src, .proc/arena_checks), 50) + +/obj/structure/elite_tumor/proc/fighters_check() + if(activator != null && activator.stat == DEAD || activity == TUMOR_ACTIVE && QDELETED(activator)) + onEliteWon() + if(mychild != null && mychild.stat == DEAD || activity == TUMOR_ACTIVE && QDELETED(mychild)) + onEliteLoss() + +/obj/structure/elite_tumor/proc/arena_trap() + var/turf/T = get_turf(src) + if(loc == null) + return + for(var/t in RANGE_TURFS(12, T)) + if(get_dist(t, T) == 12) + var/obj/effect/temp_visual/elite_tumor_wall/newwall + newwall = new /obj/effect/temp_visual/elite_tumor_wall(t, src) + newwall.activator = src.activator + newwall.ourelite = src.mychild + +/obj/structure/elite_tumor/proc/border_check() + if(activator != null && get_dist(src, activator) >= 12) + activator.forceMove(loc) + visible_message("[activator] suddenly reappears above [src]!") + playsound(loc,'sound/effects/phasein.ogg', 200, 0, 50, TRUE, TRUE) + if(mychild != null && get_dist(src, mychild) >= 12) + mychild.forceMove(loc) + visible_message("[mychild] suddenly reappears above [src]!") + playsound(loc,'sound/effects/phasein.ogg', 200, 0, 50, TRUE, TRUE) + +obj/structure/elite_tumor/proc/onEliteLoss() + playsound(loc,'sound/effects/tendril_destroyed.ogg', 200, 0, 50, TRUE, TRUE) + visible_message("[src] begins to convulse violently before beginning to dissipate.") + visible_message("As [src] closes, something is forced up from down below.") + var/obj/structure/closet/crate/necropolis/tendril/lootbox = new /obj/structure/closet/crate/necropolis/tendril(loc) + if(!boosted) + mychild = null + activator = null + qdel(src) + return + var/lootpick = rand(1, 2) + if(lootpick == 1 && mychild.loot_drop != null) + new mychild.loot_drop(lootbox) + else + new /obj/item/tumor_shard(lootbox) + mychild = null + activator = null + qdel(src) + +obj/structure/elite_tumor/proc/onEliteWon() + activity = TUMOR_PASSIVE + activator = null + mychild.revive(full_heal = TRUE, admin_revive = TRUE) + if(boosted) + times_won++ + mychild.maxHealth = mychild.maxHealth * 0.5 + mychild.health = mychild.maxHealth + if(times_won == 1) + mychild.playsound_local(get_turf(mychild), 'sound/effects/magic.ogg', 40, 0) + to_chat(mychild, "As the life in the activator's eyes fade, the forcefield around you dies out and you feel your power subside.\nDespite this inferno being your home, you feel as if you aren't welcome here anymore.\nWithout any guidance, your purpose is now for you to decide.") + to_chat(mychild, "Your max health has been halved, but can now heal by standing on your tumor. Note, it's your only way to heal.\nBear in mind, if anyone interacts with your tumor, you'll be resummoned here to carry out another fight. In such a case, you will regain your full max health.\nAlso, be weary of your fellow inhabitants, they likely won't be happy to see you!") + to_chat(mychild, "Note that you are a lavaland monster, and thus not allied to the station. You should not cooperate or act friendly with any station crew unless under extreme circumstances!") + +/obj/item/tumor_shard + name = "tumor shard" + desc = "A strange, sharp, crystal shard from an odd tumor on Lavaland. Stabbing the corpse of a lavaland elite with this will revive them, assuming their soul still lingers. Revived lavaland elites only have half their max health, but are completely loyal to their reviver." + icon = 'icons/obj/lavaland/artefacts.dmi' + icon_state = "crevice_shard" + lefthand_file = 'icons/mob/inhands/equipment/tools_lefthand.dmi' + righthand_file = 'icons/mob/inhands/equipment/tools_righthand.dmi' + item_state = "screwdriver_head" + throwforce = 5 + w_class = WEIGHT_CLASS_SMALL + throw_speed = 3 + throw_range = 5 + +/obj/item/tumor_shard/afterattack(atom/target, mob/user, proximity_flag) + . = ..() + if(istype(target, /mob/living/simple_animal/hostile/asteroid/elite) && proximity_flag) + var/mob/living/simple_animal/hostile/asteroid/elite/E = target + if(E.stat != DEAD || E.sentience_type != SENTIENCE_BOSS || !E.key) + user.visible_message("It appears [E] is unable to be revived right now. Perhaps try again later.") + return + E.faction = list("neutral") + E.revive(full_heal = TRUE, admin_revive = TRUE) + user.visible_message("[user] stabs [E] with [src], reviving it.") + E.playsound_local(get_turf(E), 'sound/effects/magic.ogg', 40, 0) + to_chat(E, "You have been revived by [user]. While you can't speak to them, you owe [user] a great debt. Assist [user.p_them()] in achieving [user.p_their()] goals, regardless of risk.Note that you now share the loyalties of [user]. You are expected not to intentionally sabotage their faction unless commanded to!") + E.maxHealth = E.maxHealth * 0.5 + E.health = E.maxHealth + E.desc = "[E.desc] However, this one appears appears less wild in nature, and calmer around people." + E.sentience_type = SENTIENCE_ORGANIC + qdel(src) + else + to_chat(user, "[src] only works on the corpse of a sentient lavaland elite.") + +/obj/effect/temp_visual/elite_tumor_wall + name = "magic wall" + icon = 'icons/turf/walls/hierophant_wall_temp.dmi' + icon_state = "wall" + duration = 50 + smooth = SMOOTH_TRUE + layer = BELOW_MOB_LAYER + var/mob/living/carbon/human/activator = null + var/mob/living/simple_animal/hostile/asteroid/elite/ourelite = null + color = rgb(255,0,0) + light_range = MINIMUM_USEFUL_LIGHT_RANGE + light_color = LIGHT_COLOR_RED + +/obj/effect/temp_visual/elite_tumor_wall/Initialize(mapload, new_caster) + . = ..() + queue_smooth_neighbors(src) + queue_smooth(src) + +/obj/effect/temp_visual/elite_tumor_wall/Destroy() + queue_smooth_neighbors(src) + activator = null + ourelite = null + return ..() + +/obj/effect/temp_visual/elite_tumor_wall/CanPass(atom/movable/mover, turf/target) + if(mover == ourelite || mover == activator) + return FALSE + else + return TRUE diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/goliath_broodmother.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/goliath_broodmother.dm new file mode 100644 index 0000000000..c18a342206 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/goliath_broodmother.dm @@ -0,0 +1,247 @@ +#define TENTACLE_PATCH 1 +#define SPAWN_CHILDREN 2 +#define RAGE 3 +#define CALL_CHILDREN 4 + +/** + * # Goliath Broodmother + * + * A stronger, faster variation of the goliath. Has the ability to spawn baby goliaths, which it can later detonate at will. + * When it's health is below half, tendrils will spawn randomly around it. When it is below a quarter of health, this effect is doubled. + * It's attacks are as follows: + * - Spawns a 3x3/plus shape of tentacles on the target location + * - Spawns 2 baby goliaths on its tile, up to a max of 8. Children blow up when they die. + * - The broodmother lets out a noise, and is able to move faster for 6.5 seconds. + * - Summons your children around you. + * The broodmother is a fight revolving around stage control, as the activator has to manage the baby goliaths and the broodmother herself, along with all the tendrils. + */ + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother + name = "goliath broodmother" + desc = "An example of sexual dimorphism, this female goliath looks much different than the males of her species. She is, however, just as dangerous, if not more." + gender = FEMALE + icon_state = "broodmother" + icon_living = "broodmother" + icon_aggro = "broodmother" + icon_dead = "egg_sac" + icon_gib = "syndicate_gib" + maxHealth = 800 + health = 800 + melee_damage_lower = 30 + melee_damage_upper = 30 + armour_penetration = 30 + attacktext = "beats down on" + /*attack_verb_continuous = "beats down on" + attack_verb_simple = "beat down on"*/ + attack_sound = 'sound/weapons/punch1.ogg' + throw_message = "does nothing to the rocky hide of the" + speed = 2 + move_to_delay = 5 + mob_biotypes = list(MOB_ORGANIC, MOB_BEAST) + mouse_opacity = MOUSE_OPACITY_ICON + deathmessage = "explodes into gore!" + loot_drop = /obj/item/crusher_trophy/broodmother_tongue + + attack_action_types = list(/datum/action/innate/elite_attack/tentacle_patch, + /datum/action/innate/elite_attack/spawn_children, + /datum/action/innate/elite_attack/rage, + /datum/action/innate/elite_attack/call_children) + + var/rand_tent = 0 + var/list/mob/living/simple_animal/hostile/asteroid/elite/broodmother_child/children_list = list() + +/datum/action/innate/elite_attack/tentacle_patch + name = "Tentacle Patch" + button_icon_state = "tentacle_patch" + chosen_message = "You are now attacking with a patch of tentacles." + chosen_attack_num = TENTACLE_PATCH + +/datum/action/innate/elite_attack/spawn_children + name = "Spawn Children" + button_icon_state = "spawn_children" + chosen_message = "You will spawn two children at your location to assist you in combat. You can have up to 8." + chosen_attack_num = SPAWN_CHILDREN + +/datum/action/innate/elite_attack/rage + name = "Rage" + button_icon_state = "rage" + chosen_message = "You will temporarily increase your movement speed." + chosen_attack_num = RAGE + +/datum/action/innate/elite_attack/call_children + name = "Call Children" + button_icon_state = "call_children" + chosen_message = "You will summon your children to your location." + chosen_attack_num = CALL_CHILDREN + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/OpenFire() + if(client) + switch(chosen_attack) + if(TENTACLE_PATCH) + tentacle_patch(target) + if(SPAWN_CHILDREN) + spawn_children() + if(RAGE) + rage() + if(CALL_CHILDREN) + call_children() + return + var/aiattack = rand(1,4) + switch(aiattack) + if(TENTACLE_PATCH) + tentacle_patch(target) + if(SPAWN_CHILDREN) + spawn_children() + if(RAGE) + rage() + if(CALL_CHILDREN) + call_children() + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/Life() + . = ..() + if(!.) //Checks if they are dead as a rock. + return + if(health < maxHealth * 0.5 && rand_tent < world.time) + rand_tent = world.time + 30 + var/tentacle_amount = 5 + if(health < maxHealth * 0.25) + tentacle_amount = 10 + var/tentacle_loc = spiral_range_turfs(5, get_turf(src)) + for(var/i in 1 to tentacle_amount) + var/turf/t = pick_n_take(tentacle_loc) + new /obj/effect/temp_visual/goliath_tentacle/broodmother(t, src) + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/proc/tentacle_patch(var/target) + ranged_cooldown = world.time + 15 + var/tturf = get_turf(target) + if(!isturf(tturf)) + return + visible_message("[src] digs its tentacles under [target]!") + new /obj/effect/temp_visual/goliath_tentacle/broodmother/patch(tturf, src) + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/proc/spawn_children(var/target) + ranged_cooldown = world.time + 40 + visible_message("The ground churns behind [src]!") + for(var/i in 1 to 2) + if(children_list.len >= 8) + return + var/mob/living/simple_animal/hostile/asteroid/elite/broodmother_child/newchild = new /mob/living/simple_animal/hostile/asteroid/elite/broodmother_child(loc) + newchild.GiveTarget(target) + newchild.faction = faction.Copy() + visible_message("[newchild] appears below [src]!") + newchild.mother = src + children_list += newchild + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/proc/rage() + ranged_cooldown = world.time + 70 + playsound(src,'sound/spookoween/insane_low_laugh.ogg', 200, 1) + visible_message("[src] starts picking up speed!") + color = "#FF0000" + set_varspeed(0) + move_to_delay = 3 + addtimer(CALLBACK(src, .proc/reset_rage), 65) + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/proc/reset_rage() + color = "#FFFFFF" + set_varspeed(2) + move_to_delay = 5 + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/proc/call_children() + ranged_cooldown = world.time + 60 + visible_message("The ground shakes near [src]!") + var/list/directions = GLOB.cardinals.Copy() + GLOB.diagonals.Copy() + for(var/mob/child in children_list) + var/spawndir = pick_n_take(directions) + var/turf/T = get_step(src, spawndir) + if(T) + child.forceMove(T) + playsound(src, 'sound/effects/bamf.ogg', 100, 1) + +//The goliath's children. Pretty weak, simple mobs which are able to put a single tentacle under their target when at range. +/mob/living/simple_animal/hostile/asteroid/elite/broodmother_child + name = "baby goliath" + desc = "A young goliath recently born from it's mother. While they hatch from eggs, said eggs are incubated in the mother until they are ready to be born." + icon = 'icons/mob/lavaland/lavaland_monsters.dmi' + icon_state = "goliath_baby" + icon_living = "goliath_baby" + icon_aggro = "goliath_baby" + icon_dead = "goliath_baby_dead" + icon_gib = "syndicate_gib" + maxHealth = 30 + health = 30 + melee_damage_lower = 5 + melee_damage_upper = 5 + attacktext = "bashes against" + /*attack_verb_continuous = "bashes against" + attack_verb_simple = "bash against"*/ + attack_sound = 'sound/weapons/punch1.ogg' + throw_message = "does nothing to the rocky hide of the" + speed = 2 + move_to_delay = 5 + mob_biotypes = list(MOB_ORGANIC, MOB_BEAST) + mouse_opacity = MOUSE_OPACITY_ICON + butcher_results = list() + guaranteed_butcher_results = list(/obj/item/stack/sheet/animalhide/goliath_hide = 1) + deathmessage = "falls to the ground." + status_flags = CANPUSH + var/mob/living/simple_animal/hostile/asteroid/elite/broodmother/mother = null + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother_child/OpenFire(target) + ranged_cooldown = world.time + 40 + var/tturf = get_turf(target) + if(!isturf(tturf)) + return + if(get_dist(src, target) <= 7)//Screen range check, so it can't attack people off-screen + visible_message("[src] digs one of its tentacles under [target]!") + new /obj/effect/temp_visual/goliath_tentacle/broodmother(tturf, src) + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother_child/death() + . = ..() + if(mother != null) + mother.children_list -= src + visible_message("[src] explodes!") + explosion(get_turf(loc),0,0,0,flame_range = 3, adminlog = FALSE) + gib() + +//Tentacles have less stun time compared to regular variant, to balance being able to use them much more often. Also, 10 more damage. +/obj/effect/temp_visual/goliath_tentacle/broodmother/trip() + var/latched = FALSE + for(var/mob/living/L in loc) + if((!QDELETED(spawner) && spawner.faction_check_mob(L)) || L.stat == DEAD) + continue + visible_message("[src] grabs hold of [L]!") + L.Stun(10) + L.adjustBruteLoss(rand(30,35)) + latched = TRUE + if(!latched) + retract() + else + deltimer(timerid) + timerid = addtimer(CALLBACK(src, .proc/retract), 10, TIMER_STOPPABLE) + +/obj/effect/temp_visual/goliath_tentacle/broodmother/patch/Initialize(mapload, new_spawner) + . = ..() + var/tentacle_locs = spiral_range_turfs(1, get_turf(src)) + for(var/T in tentacle_locs) + new /obj/effect/temp_visual/goliath_tentacle/broodmother(T, spawner) + var/list/directions = GLOB.cardinals.Copy() + for(var/i in directions) + var/turf/T = get_step(get_turf(src), i) + T = get_step(T, i) + new /obj/effect/temp_visual/goliath_tentacle/broodmother(T, spawner) + +// Broodmother's loot: Broodmother Tongue +/obj/item/crusher_trophy/broodmother_tongue + name = "broodmother tongue" + desc = "The tongue of a broodmother. If attached a certain way, makes for a suitable crusher trophy." + icon = 'icons/obj/lavaland/elite_trophies.dmi' + icon_state = "broodmother_tongue" + denied_type = /obj/item/crusher_trophy/broodmother_tongue + bonus_value = 10 + +/obj/item/crusher_trophy/broodmother_tongue/effect_desc() + return "mark detonation to have a [bonus_value]% chance to summon a patch of goliath tentacles at the target's location" + +/obj/item/crusher_trophy/broodmother_tongue/on_mark_detonation(mob/living/target, mob/living/user) + if(rand(1, 100) <= bonus_value && target.stat != DEAD) + new /obj/effect/temp_visual/goliath_tentacle/broodmother/patch(get_turf(target), user) diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/herald.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/herald.dm new file mode 100644 index 0000000000..f1e7494beb --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/herald.dm @@ -0,0 +1,275 @@ +#define HERALD_TRISHOT 1 +#define HERALD_DIRECTIONALSHOT 2 +#define HERALD_TELESHOT 3 +#define HERALD_MIRROR 4 + +/** + * # Herald + * + * A slow-moving projectile user with a few tricks up it's sleeve. Less unga-bunga than Colossus, with more cleverness in it's fighting style. + * As it's health gets lower, the amount of projectiles fired per-attack increases. + * It's attacks are as follows: + * - Fires three projectiles in a a given direction. + * - Fires a spread in every cardinal and diagonal direction at once, then does it again after a bit. + * - Shoots a single, golden bolt. Wherever it lands, the herald will be teleported to the location. + * - Spawns a mirror which reflects projectiles directly at the target. + * Herald is a more concentrated variation of the Colossus fight, having less projectiles overall, but more focused attacks. + */ + +/mob/living/simple_animal/hostile/asteroid/elite/herald + name = "herald" + desc = "A monstrous beast which fires deadly projectiles at threats and prey." + icon_state = "herald" + icon_living = "herald" + icon_aggro = "herald" + icon_dead = "herald_dying" + icon_gib = "syndicate_gib" + maxHealth = 800 + health = 800 + melee_damage_lower = 20 + melee_damage_upper = 20 + attacktext = "preaches to" + /*attack_verb_continuous = "preaches to" + attack_verb_simple = "preach to"*/ + attack_sound = 'sound/magic/clockwork/ratvar_attack.ogg' + throw_message = "doesn't affect the purity of" + speed = 4 + move_to_delay = 10 + mouse_opacity = MOUSE_OPACITY_ICON + deathsound = 'sound/magic/demon_dies.ogg' + deathmessage = "begins to shudder as it becomes transparent..." + loot_drop = /obj/item/clothing/neck/cloak/herald_cloak + + can_talk = 1 + + attack_action_types = list(/datum/action/innate/elite_attack/herald_trishot, + /datum/action/innate/elite_attack/herald_directionalshot, + /datum/action/innate/elite_attack/herald_teleshot, + /datum/action/innate/elite_attack/herald_mirror) + + var/mob/living/simple_animal/hostile/asteroid/elite/herald/mirror/my_mirror = null + var/is_mirror = FALSE + +/mob/living/simple_animal/hostile/asteroid/elite/herald/death() + . = ..() + if(!is_mirror) + addtimer(CALLBACK(src, .proc/become_ghost), 8) + if(my_mirror != null) + qdel(my_mirror) + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/become_ghost() + icon_state = "herald_ghost" + +/mob/living/simple_animal/hostile/asteroid/elite/herald/say(message, bubble_type, var/list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null) + . = ..() + playsound(get_turf(src), 'sound/magic/clockwork/invoke_general.ogg', 20, TRUE) + +/datum/action/innate/elite_attack/herald_trishot + name = "Triple Shot" + button_icon_state = "herald_trishot" + chosen_message = "You are now firing three shots in your chosen direction." + chosen_attack_num = HERALD_TRISHOT + +/datum/action/innate/elite_attack/herald_directionalshot + name = "Circular Shot" + button_icon_state = "herald_directionalshot" + chosen_message = "You are firing projectiles in all directions." + chosen_attack_num = HERALD_DIRECTIONALSHOT + +/datum/action/innate/elite_attack/herald_teleshot + name = "Teleport Shot" + button_icon_state = "herald_teleshot" + chosen_message = "You will now fire a shot which teleports you where it lands." + chosen_attack_num = HERALD_TELESHOT + +/datum/action/innate/elite_attack/herald_mirror + name = "Summon Mirror" + button_icon_state = "herald_mirror" + chosen_message = "You will spawn a mirror which duplicates your attacks." + chosen_attack_num = HERALD_MIRROR + +/mob/living/simple_animal/hostile/asteroid/elite/herald/OpenFire() + if(client) + switch(chosen_attack) + if(HERALD_TRISHOT) + herald_trishot(target) + if(my_mirror != null) + my_mirror.herald_trishot(target) + if(HERALD_DIRECTIONALSHOT) + herald_directionalshot() + if(my_mirror != null) + my_mirror.herald_directionalshot() + if(HERALD_TELESHOT) + herald_teleshot(target) + if(my_mirror != null) + my_mirror.herald_teleshot(target) + if(HERALD_MIRROR) + herald_mirror() + return + var/aiattack = rand(1,4) + switch(aiattack) + if(HERALD_TRISHOT) + herald_trishot(target) + if(my_mirror != null) + my_mirror.herald_trishot(target) + if(HERALD_DIRECTIONALSHOT) + herald_directionalshot() + if(my_mirror != null) + my_mirror.herald_directionalshot() + if(HERALD_TELESHOT) + herald_teleshot(target) + if(my_mirror != null) + my_mirror.herald_teleshot(target) + if(HERALD_MIRROR) + herald_mirror() + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/shoot_projectile(turf/marker, set_angle, var/is_teleshot) + var/turf/startloc = get_turf(src) + var/obj/item/projectile/herald/H = null + if(!is_teleshot) + H = new /obj/item/projectile/herald(startloc) + else + H = new /obj/item/projectile/herald/teleshot(startloc) + H.preparePixelProjectile(marker, startloc) + H.firer = src + if(target) + H.original = target + H.fire(set_angle) + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/herald_trishot(target) + ranged_cooldown = world.time + 30 + playsound(get_turf(src), 'sound/magic/clockwork/invoke_general.ogg', 20, TRUE) + var/target_turf = get_turf(target) + var/angle_to_target = Get_Angle(src, target_turf) + shoot_projectile(target_turf, angle_to_target, FALSE) + addtimer(CALLBACK(src, .proc/shoot_projectile, target_turf, angle_to_target, FALSE), 2) + addtimer(CALLBACK(src, .proc/shoot_projectile, target_turf, angle_to_target, FALSE), 4) + if(health < maxHealth * 0.5) + playsound(get_turf(src), 'sound/magic/clockwork/invoke_general.ogg', 20, TRUE) + addtimer(CALLBACK(src, .proc/shoot_projectile, target_turf, angle_to_target, FALSE), 10) + addtimer(CALLBACK(src, .proc/shoot_projectile, target_turf, angle_to_target, FALSE), 12) + addtimer(CALLBACK(src, .proc/shoot_projectile, target_turf, angle_to_target, FALSE), 14) + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/herald_circleshot() + var/static/list/directional_shot_angles = list(0, 45, 90, 135, 180, 225, 270, 315) + for(var/i in directional_shot_angles) + shoot_projectile(get_turf(src), i, FALSE) + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/unenrage() + if(stat == DEAD || is_mirror) + return + icon_state = "herald" + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/herald_directionalshot() + ranged_cooldown = world.time + 50 + if(!is_mirror) + icon_state = "herald_enraged" + playsound(get_turf(src), 'sound/magic/clockwork/invoke_general.ogg', 20, TRUE) + addtimer(CALLBACK(src, .proc/herald_circleshot), 5) + if(health < maxHealth * 0.5) + playsound(get_turf(src), 'sound/magic/clockwork/invoke_general.ogg', 20, TRUE) + addtimer(CALLBACK(src, .proc/herald_circleshot), 15) + addtimer(CALLBACK(src, .proc/unenrage), 20) + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/herald_teleshot(target) + ranged_cooldown = world.time + 30 + playsound(get_turf(src), 'sound/magic/clockwork/invoke_general.ogg', 20, TRUE) + var/target_turf = get_turf(target) + var/angle_to_target = Get_Angle(src, target_turf) + shoot_projectile(target_turf, angle_to_target, TRUE) + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/herald_mirror() + ranged_cooldown = world.time + 40 + playsound(get_turf(src), 'sound/magic/clockwork/invoke_general.ogg', 20, TRUE) + if(my_mirror != null) + qdel(my_mirror) + my_mirror = null + var/mob/living/simple_animal/hostile/asteroid/elite/herald/mirror/new_mirror = new /mob/living/simple_animal/hostile/asteroid/elite/herald/mirror(loc) + my_mirror = new_mirror + my_mirror.my_master = src + my_mirror.faction = faction.Copy() + +/mob/living/simple_animal/hostile/asteroid/elite/herald/mirror + name = "herald's mirror" + desc = "This fiendish work of magic copies the herald's attacks. Seems logical to smash it." + health = 60 + maxHealth = 60 + icon_state = "herald_mirror" + deathmessage = "shatters violently!" + deathsound = 'sound/effects/glassbr1.ogg' + movement_type = FLYING + del_on_death = TRUE + is_mirror = TRUE + var/mob/living/simple_animal/hostile/asteroid/elite/herald/my_master = null + +/mob/living/simple_animal/hostile/asteroid/elite/herald/mirror/Initialize() + ..() + toggle_ai(AI_OFF) + +/mob/living/simple_animal/hostile/asteroid/elite/herald/mirror/Destroy() + if(my_master != null) + my_master.my_mirror = null + . = ..() + +/obj/item/projectile/herald + name ="death bolt" + icon_state= "chronobolt" + damage = 15 + armour_penetration = 60 + speed = 2 + eyeblur = 0 + damage_type = BRUTE + pass_flags = PASSTABLE + +/obj/item/projectile/herald/teleshot + name ="golden bolt" + damage = 0 + color = rgb(255,255,102) + +/obj/item/projectile/herald/on_hit(atom/target, blocked = FALSE) + . = ..() + if(ismineralturf(target)) + var/turf/closed/mineral/M = target + M.gets_drilled() + return + else if(isliving(target)) + var/mob/living/L = target + var/mob/living/F = firer + if(F != null && istype(F, /mob/living/simple_animal/hostile/asteroid/elite) && F.faction_check_mob(L)) + L.heal_overall_damage(damage) + +/obj/item/projectile/herald/teleshot/on_hit(atom/target, blocked = FALSE) + . = ..() + firer.forceMove(get_turf(src)) + +//Herald's loot: Cloak of the Prophet + +/obj/item/clothing/neck/cloak/herald_cloak + name = "cloak of the prophet" + desc = "A cloak which protects you from the heresy of the world." + icon = 'icons/obj/lavaland/elite_trophies.dmi' + icon_state = "herald_cloak" + body_parts_covered = CHEST|GROIN|ARMS + hit_reaction_chance = 10 + +/obj/item/clothing/neck/cloak/herald_cloak/proc/reactionshot(mob/living/carbon/owner) + var/static/list/directional_shot_angles = list(0, 45, 90, 135, 180, 225, 270, 315) + for(var/i in directional_shot_angles) + shoot_projectile(get_turf(owner), i, owner) + +/obj/item/clothing/neck/cloak/herald_cloak/proc/shoot_projectile(turf/marker, set_angle, mob/living/carbon/owner) + var/turf/startloc = get_turf(owner) + var/obj/item/projectile/herald/H = null + H = new /obj/item/projectile/herald(startloc) + H.preparePixelProjectile(marker, startloc) + H.firer = owner + H.fire(set_angle) + +/obj/item/clothing/neck/cloak/herald_cloak/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK) + . = ..() + if(rand(1,100) > hit_reaction_chance) + return + owner.visible_message("[owner]'s [src] emits a loud noise as [owner] is struck!") + var/static/list/directional_shot_angles = list(0, 45, 90, 135, 180, 225, 270, 315) + playsound(get_turf(owner), 'sound/magic/clockwork/invoke_general.ogg', 20, TRUE) + addtimer(CALLBACK(src, .proc/reactionshot, owner), 10) diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm new file mode 100644 index 0000000000..1bc9ea1e4e --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm @@ -0,0 +1,303 @@ +#define LEGIONNAIRE_CHARGE 1 +#define HEAD_DETACH 2 +#define BONFIRE_TELEPORT 3 +#define SPEW_SMOKE 4 + +/** + * # Legionnaire + * + * A towering skeleton, embodying the power of Legion. + * As it's health gets lower, the head does more damage. + * It's attacks are as follows: + * - Charges at the target after a telegraph, throwing them across the arena should it connect. + * - Legionnaire's head detaches, attacking as it's own entity. Has abilities of it's own later into the fight. Once dead, regenerates after a brief period. If the skill is used while the head is off, it will be killed. + * - Leaves a pile of bones at your location. Upon using this skill again, you'll swap locations with the bone pile. + * - Spews a cloud of smoke from it's maw, wherever said maw is. + * A unique fight incorporating the head mechanic of legion into a whole new beast. Combatants will need to make sure the tag-team of head and body don't lure them into a deadly trap. + */ + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire + name = "legionnaire" + desc = "A towering skeleton, embodying the terrifying power of Legion." + icon_state = "legionnaire" + icon_living = "legionnaire" + icon_aggro = "legionnaire" + icon_dead = "legionnaire_dead" + icon_gib = "syndicate_gib" + maxHealth = 800 + health = 800 + melee_damage_lower = 30 + melee_damage_upper = 30 + attacktext = "slashes its arms at" + /*attack_verb_continuous = "slashes its arms at" + attack_verb_simple = "slash your arms at"*/ + attack_sound = 'sound/weapons/bladeslice.ogg' + throw_message = "doesn't affect the sturdiness of" + speed = 1 + move_to_delay = 3 + mouse_opacity = MOUSE_OPACITY_ICON + deathsound = 'sound/magic/curse.ogg' + deathmessage = "'s arms reach out before it falls apart onto the floor, lifeless." + loot_drop = /obj/item/crusher_trophy/legionnaire_spine + + attack_action_types = list(/datum/action/innate/elite_attack/legionnaire_charge, + /datum/action/innate/elite_attack/head_detach, + /datum/action/innate/elite_attack/bonfire_teleport, + /datum/action/innate/elite_attack/spew_smoke) + + var/mob/living/simple_animal/hostile/asteroid/elite/legionnairehead/myhead = null + var/obj/structure/legionnaire_bonfire/mypile = null + var/has_head = TRUE + +/datum/action/innate/elite_attack/legionnaire_charge + name = "Legionnaire Charge" + button_icon_state = "legionnaire_charge" + chosen_message = "You will attempt to grab your opponent and throw them." + chosen_attack_num = LEGIONNAIRE_CHARGE + +/datum/action/innate/elite_attack/head_detach + name = "Release Head" + button_icon_state = "head_detach" + chosen_message = "You will now detach your head or kill it if it is already released." + chosen_attack_num = HEAD_DETACH + +/datum/action/innate/elite_attack/bonfire_teleport + name = "Bonfire Teleport" + button_icon_state = "bonfire_teleport" + chosen_message = "You will leave a bonfire. Second use will let you swap positions with it indefintiely. Using this move on the same tile as your active bonfire removes it." + chosen_attack_num = BONFIRE_TELEPORT + +/datum/action/innate/elite_attack/spew_smoke + name = "Spew Smoke" + button_icon_state = "spew_smoke" + chosen_message = "Your head will spew smoke in an area, wherever it may be." + chosen_attack_num = SPEW_SMOKE + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/OpenFire() + if(client) + switch(chosen_attack) + if(LEGIONNAIRE_CHARGE) + legionnaire_charge(target) + if(HEAD_DETACH) + head_detach(target) + if(BONFIRE_TELEPORT) + bonfire_teleport() + if(SPEW_SMOKE) + spew_smoke() + return + var/aiattack = rand(1,4) + switch(aiattack) + if(LEGIONNAIRE_CHARGE) + legionnaire_charge(target) + if(HEAD_DETACH) + head_detach(target) + if(BONFIRE_TELEPORT) + bonfire_teleport() + if(SPEW_SMOKE) + spew_smoke() + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/proc/legionnaire_charge(target) + ranged_cooldown = world.time + 50 + var/dir_to_target = get_dir(get_turf(src), get_turf(target)) + var/turf/T = get_step(get_turf(src), dir_to_target) + for(var/i in 1 to 4) + new /obj/effect/temp_visual/dragon_swoop/legionnaire(T) + T = get_step(T, dir_to_target) + playsound(src,'sound/magic/demon_attack1.ogg', 200, 1) + visible_message("[src] prepares to charge!") + addtimer(CALLBACK(src, .proc/legionnaire_charge_2, dir_to_target, 0), 5) + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/proc/legionnaire_charge_2(var/move_dir, var/times_ran) + if(times_ran >= 4) + return + var/turf/T = get_step(get_turf(src), move_dir) + if(ismineralturf(T)) + var/turf/closed/mineral/M = T + M.gets_drilled() + if(T.density) + return + for(var/obj/structure/window/W in T.contents) + return + for(var/obj/machinery/door/D in T.contents) + return + forceMove(T) + playsound(src,'sound/effects/bang.ogg', 200, 1) + var/list/hit_things = list() + var/throwtarget = get_edge_target_turf(src, move_dir) + for(var/mob/living/L in T.contents - hit_things - src) + if(faction_check_mob(L)) + return + hit_things += L + visible_message("[src] attacks [L] with much force!") + to_chat(L, "[src] grabs you and throws you with much force!") + L.safe_throw_at(throwtarget, 10, 1, src) + //L.Paralyze(20) + L.Stun(20) //substituting this for the Paralyze from the line above, because we don't have tg paralysis stuff + L.adjustBruteLoss(50) + addtimer(CALLBACK(src, .proc/legionnaire_charge_2, move_dir, (times_ran + 1)), 2) + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/proc/head_detach(target) + ranged_cooldown = world.time + 10 + if(myhead != null) + myhead.adjustBruteLoss(600) + return + if(has_head) + has_head = FALSE + icon_state = "legionnaire_headless" + icon_living = "legionnaire_headless" + icon_aggro = "legionnaire_headless" + visible_message("[src]'s head flies off!") + var/mob/living/simple_animal/hostile/asteroid/elite/legionnairehead/newhead = new /mob/living/simple_animal/hostile/asteroid/elite/legionnairehead(loc) + newhead.flags_1 |= (flags_1 & ADMIN_SPAWNED_1) + newhead.GiveTarget(target) + newhead.faction = faction.Copy() + myhead = newhead + myhead.body = src + if(health < maxHealth * 0.25) + myhead.melee_damage_lower = 30 + myhead.melee_damage_upper = 30 + else if(health < maxHealth * 0.5) + myhead.melee_damage_lower = 20 + myhead.melee_damage_upper = 20 + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/proc/onHeadDeath() + myhead = null + addtimer(CALLBACK(src, .proc/regain_head), 50) + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/proc/regain_head() + has_head = TRUE + if(stat == DEAD) + return + icon_state = "legionnaire" + icon_living = "legionnaire" + icon_aggro = "legionnaire" + visible_message("The top of [src]'s spine leaks a black liquid, forming into a skull!") + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/proc/bonfire_teleport() + ranged_cooldown = world.time + 5 + if(mypile == null) + var/obj/structure/legionnaire_bonfire/newpile = new /obj/structure/legionnaire_bonfire(loc) + mypile = newpile + mypile.myowner = src + playsound(get_turf(src),'sound/items/fulext_deploy.wav', 200, 1) + visible_message("[src] summons a bonfire on [get_turf(src)]!") + return + else + var/turf/legionturf = get_turf(src) + var/turf/pileturf = get_turf(mypile) + if(legionturf == pileturf) + mypile.take_damage(100) + mypile = null + return + playsound(pileturf,'sound/items/fulext_deploy.wav', 200, 1) + playsound(legionturf,'sound/items/fulext_deploy.wav', 200, 1) + visible_message("[src] melts down into a burning pile of bones!") + forceMove(pileturf) + visible_message("[src] forms from the bonfire!") + mypile.forceMove(legionturf) + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/proc/spew_smoke() + ranged_cooldown = world.time + 60 + var/turf/T = null + if(myhead != null) + T = get_turf(myhead) + else + T = get_turf(src) + if(myhead != null) + myhead.visible_message("[myhead] spews smoke from its maw!") + else if(!has_head) + visible_message("[src] spews smoke from the tip of their spine!") + else + visible_message("[src] spews smoke from its maw!") + var/datum/effect_system/smoke_spread/smoke = new + smoke.set_up(2, T) + smoke.start() + +//The legionnaire's head. Basically the same as any legion head, but we have to tell our creator when we die so they can generate another head. +/mob/living/simple_animal/hostile/asteroid/elite/legionnairehead + name = "legionnaire head" + desc = "The legionnaire's head floating by itself. One shouldn't get too close, though once it sees you, you really don't have a choice." + icon_state = "legionnaire_head" + icon_living = "legionnaire_head" + icon_aggro = "legionnaire_head" + icon_dead = "legionnaire_dead" + icon_gib = "syndicate_gib" + maxHealth = 80 + health = 80 + melee_damage_lower = 10 + melee_damage_upper = 10 + attacktext = "bites at" + /*attack_verb_continuous = "bites at" + attack_verb_simple = "bite at"*/ + attack_sound = 'sound/effects/curse1.ogg' + throw_message = "simply misses" + speed = 0 + move_to_delay = 2 + del_on_death = 1 + deathmessage = "crumbles away!" + faction = list() + ranged = FALSE + var/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/body = null + +/mob/living/simple_animal/hostile/asteroid/elite/legionnairehead/death() + . = ..() + if(body) + body.onHeadDeath() + +//The legionnaire's bonfire, which can be swapped positions with. Also sets flammable living beings on fire when they walk over it. +/obj/structure/legionnaire_bonfire + name = "bone pile" + desc = "A pile of bones which seems to occasionally move a little. It's probably a good idea to smash them." + icon = 'icons/obj/lavaland/legionnaire_bonfire.dmi' + icon_state = "bonfire" + max_integrity = 100 + //move_resist = MOVE_FORCE_EXTREMELY_STRONG + anchored = TRUE + density = FALSE + light_range = 4 + light_color = LIGHT_COLOR_RED + var/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/myowner = null + + +/obj/structure/legionnaire_bonfire/Entered(atom/movable/mover, turf/target) + if(isliving(mover)) + var/mob/living/L = mover + L.adjust_fire_stacks(3) + L.IgniteMob() + . = ..() + +/obj/structure/legionnaire_bonfire/Destroy() + if(myowner != null) + myowner.mypile = null + . = ..() + +//The visual effect which appears in front of legionnaire when he goes to charge. +/obj/effect/temp_visual/dragon_swoop/legionnaire + duration = 10 + color = rgb(0,0,0) + +/obj/effect/temp_visual/dragon_swoop/legionnaire/Initialize() + . = ..() + transform *= 0.33 + +// Legionnaire's loot: Legionnaire Spine + +/obj/item/crusher_trophy/legionnaire_spine + name = "legionnaire spine" + desc = "The spine of a legionnaire. It almost feels like it's moving..." + icon = 'icons/obj/lavaland/elite_trophies.dmi' + icon_state = "legionnaire_spine" + denied_type = /obj/item/crusher_trophy/legionnaire_spine + bonus_value = 20 + +/obj/item/crusher_trophy/legionnaire_spine/effect_desc() + return "mark detonation to have a [bonus_value]% chance to summon a loyal legion skull" + +/obj/item/crusher_trophy/legionnaire_spine/on_mark_detonation(mob/living/target, mob/living/user) + if(!rand(1, 100) <= bonus_value || target.stat == DEAD) + return + var/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/A = new /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion(user.loc) + A.flags_1 |= (flags_1 & ADMIN_SPAWNED_1) + A.GiveTarget(target) + A.friends = user + A.faction = user.faction.Copy() diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/pandora.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/pandora.dm new file mode 100644 index 0000000000..540470d505 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/pandora.dm @@ -0,0 +1,193 @@ +#define SINGULAR_SHOT 1 +#define MAGIC_BOX 2 +#define PANDORA_TELEPORT 3 +#define AOE_SQUARES 4 + +/** + * # Pandora + * + * A box with a similar design to the Hierophant which trades large, single attacks for more frequent smaller ones. + * As it's health gets lower, the time between it's attacks decrease. + * It's attacks are as follows: + * - Fires hierophant blasts in a straight line. Can only fire in a straight line in 8 directions, being the diagonals and cardinals. + * - Creates a box of hierophant blasts around the target. If they try to run away to avoid it, they'll very likely get hit. + * - Teleports the pandora from one location to another, almost identical to Hierophant. + * - Spawns a 5x5 AOE at the location of choice, spreading out from the center. + * Pandora's fight mirrors Hierophant's closely, but has stark differences in attack effects. Instead of long-winded dodge times and long cooldowns, Pandora constantly attacks the opponent, but leaves itself open for attack. + */ + +/mob/living/simple_animal/hostile/asteroid/elite/pandora + name = "pandora" + desc = "A large magic box with similar power and design to the Hierophant. Once it opens, it's not easy to close it." + icon_state = "pandora" + icon_living = "pandora" + icon_aggro = "pandora" + icon_dead = "pandora_dead" + icon_gib = "syndicate_gib" + maxHealth = 800 + health = 800 + melee_damage_lower = 15 + melee_damage_upper = 15 + attacktext = "smashes into the side of" + /*attack_verb_continuous = "smashes into the side of" + attack_verb_simple = "smash into the side of"*/ + attack_sound = 'sound/weapons/sonic_jackhammer.ogg' + throw_message = "merely dinks off of the" + speed = 4 + move_to_delay = 10 + mouse_opacity = MOUSE_OPACITY_ICON + deathsound = 'sound/magic/repulse.ogg' + deathmessage = "'s lights flicker, before its top part falls down." + loot_drop = /obj/item/clothing/accessory/pandora_hope + + attack_action_types = list(/datum/action/innate/elite_attack/singular_shot, + /datum/action/innate/elite_attack/magic_box, + /datum/action/innate/elite_attack/pandora_teleport, + /datum/action/innate/elite_attack/aoe_squares) + + var/sing_shot_length = 8 + var/cooldown_time = 20 + +/datum/action/innate/elite_attack/singular_shot + name = "Singular Shot" + button_icon_state = "singular_shot" + chosen_message = "You are now creating a single linear magic square." + chosen_attack_num = SINGULAR_SHOT + +/datum/action/innate/elite_attack/magic_box + name = "Magic Box" + button_icon_state = "magic_box" + chosen_message = "You are now attacking with a box of magic squares." + chosen_attack_num = MAGIC_BOX + +/datum/action/innate/elite_attack/pandora_teleport + name = "Line Teleport" + button_icon_state = "pandora_teleport" + chosen_message = "You will now teleport to your target." + chosen_attack_num = PANDORA_TELEPORT + +/datum/action/innate/elite_attack/aoe_squares + name = "AOE Blast" + button_icon_state = "aoe_squares" + chosen_message = "Your attacks will spawn an AOE blast at your target location." + chosen_attack_num = AOE_SQUARES + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/OpenFire() + if(client) + switch(chosen_attack) + if(SINGULAR_SHOT) + singular_shot(target) + if(MAGIC_BOX) + magic_box(target) + if(PANDORA_TELEPORT) + pandora_teleport(target) + if(AOE_SQUARES) + aoe_squares(target) + return + var/aiattack = rand(1,4) + switch(aiattack) + if(SINGULAR_SHOT) + singular_shot(target) + if(MAGIC_BOX) + magic_box(target) + if(PANDORA_TELEPORT) + pandora_teleport(target) + if(AOE_SQUARES) + aoe_squares(target) + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/Life() + . = ..() + if(health >= maxHealth * 0.5) + cooldown_time = 20 + return + if(health < maxHealth * 0.5 && health > maxHealth * 0.25) + cooldown_time = 15 + return + else + cooldown_time = 10 + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/singular_shot(target) + ranged_cooldown = world.time + (cooldown_time * 0.5) + var/dir_to_target = get_dir(get_turf(src), get_turf(target)) + var/turf/T = get_step(get_turf(src), dir_to_target) + singular_shot_line(sing_shot_length, dir_to_target, T) + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/singular_shot_line(var/procsleft, var/angleused, var/turf/T) + if(procsleft <= 0) + return + new /obj/effect/temp_visual/hierophant/blast/pandora(T, src) + T = get_step(T, angleused) + procsleft = procsleft - 1 + addtimer(CALLBACK(src, .proc/singular_shot_line, procsleft, angleused, T), 2) + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/magic_box(target) + ranged_cooldown = world.time + cooldown_time + var/turf/T = get_turf(target) + for(var/t in spiral_range_turfs(3, T)) + if(get_dist(t, T) > 1) + new /obj/effect/temp_visual/hierophant/blast/pandora(t, src) + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/pandora_teleport(target) + ranged_cooldown = world.time + cooldown_time + var/turf/T = get_turf(target) + var/turf/source = get_turf(src) + new /obj/effect/temp_visual/hierophant/telegraph(T, src) + new /obj/effect/temp_visual/hierophant/telegraph(source, src) + playsound(source,'sound/machines/airlockopen.ogg', 200, 1) + addtimer(CALLBACK(src, .proc/pandora_teleport_2, T, source), 2) + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/pandora_teleport_2(var/turf/T, var/turf/source) + new /obj/effect/temp_visual/hierophant/telegraph/teleport(T, src) + new /obj/effect/temp_visual/hierophant/telegraph/teleport(source, src) + for(var/t in RANGE_TURFS(1, T)) + new /obj/effect/temp_visual/hierophant/blast/pandora(t, src) + for(var/t in RANGE_TURFS(1, source)) + new /obj/effect/temp_visual/hierophant/blast/pandora(t, src) + animate(src, alpha = 0, time = 2, easing = EASE_OUT) //fade out + visible_message("[src] fades out!") + density = FALSE + addtimer(CALLBACK(src, .proc/pandora_teleport_3, T), 2) + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/pandora_teleport_3(var/turf/T) + forceMove(T) + animate(src, alpha = 255, time = 2, easing = EASE_IN) //fade IN + density = TRUE + visible_message("[src] fades in!") + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/aoe_squares(target) + ranged_cooldown = world.time + cooldown_time + var/turf/T = get_turf(target) + new /obj/effect/temp_visual/hierophant/blast/pandora(T, src) + var/max_size = 2 + addtimer(CALLBACK(src, .proc/aoe_squares_2, T, 0, max_size), 2) + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/aoe_squares_2(var/turf/T, var/ring, var/max_size) + if(ring > max_size) + return + for(var/t in spiral_range_turfs(ring, T)) + if(get_dist(t, T) == ring) + new /obj/effect/temp_visual/hierophant/blast/pandora(t, src) + addtimer(CALLBACK(src, .proc/aoe_squares_2, T, (ring + 1), max_size), 2) + +//The specific version of hiero's squares pandora uses +/obj/effect/temp_visual/hierophant/blast/pandora + damage = 20 + monster_damage_boost = FALSE + +//Pandora's loot: Hope +/obj/item/clothing/accessory/pandora_hope + name = "Hope" + desc = "Found at the bottom of Pandora. After all the evil was released, this was the only thing left inside." + icon = 'icons/obj/lavaland/elite_trophies.dmi' + icon_state = "hope" + resistance_flags = FIRE_PROOF + +/obj/item/clothing/accessory/pandora_hope/on_uniform_equip(obj/item/clothing/under/U, user) + var/mob/living/L = user + if(L && L.mind) + SEND_SIGNAL(L, COMSIG_ADD_MOOD_EVENT, "hope_lavaland", /datum/mood_event/hope_lavaland) + +/obj/item/clothing/accessory/pandora_hope/on_uniform_dropped(obj/item/clothing/under/U, user) + var/mob/living/L = user + if(L && L.mind) + SEND_SIGNAL(L, COMSIG_CLEAR_MOOD_EVENT, "hope_lavaland") diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/bat.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/bat.dm index cc54ad3bef..4856ba2176 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/bat.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/bat.dm @@ -6,6 +6,7 @@ icon_dead = "bat_dead" icon_gib = "bat_dead" turns_per_move = 1 + blood_volume = 250 response_help = "brushes aside" response_disarm = "flails at" response_harm = "hits" diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/ghost.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/ghost.dm index 32e1c4d047..bfe8349192 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/ghost.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/ghost.dm @@ -31,6 +31,7 @@ movement_type = FLYING pressure_resistance = 300 gold_core_spawnable = NO_SPAWN //too spooky for science + blood_volume = 0 var/ghost_hair_style var/ghost_hair_color var/mutable_appearance/ghost_hair diff --git a/code/modules/mob/living/simple_animal/hostile/skeleton.dm b/code/modules/mob/living/simple_animal/hostile/skeleton.dm index d0ae01f443..48b69a1f5c 100644 --- a/code/modules/mob/living/simple_animal/hostile/skeleton.dm +++ b/code/modules/mob/living/simple_animal/hostile/skeleton.dm @@ -12,6 +12,7 @@ emote_see = list("rattles") a_intent = INTENT_HARM maxHealth = 40 + blood_volume = 0 health = 40 speed = 1 harm_intent_damage = 5 diff --git a/code/modules/mob/living/simple_animal/hostile/statue.dm b/code/modules/mob/living/simple_animal/hostile/statue.dm index 804989e71e..da423f3788 100644 --- a/code/modules/mob/living/simple_animal/hostile/statue.dm +++ b/code/modules/mob/living/simple_animal/hostile/statue.dm @@ -18,6 +18,7 @@ maxHealth = 50000 health = 50000 healable = 0 + blood_volume = 0 harm_intent_damage = 10 obj_damage = 100 diff --git a/code/modules/mob/living/simple_animal/hostile/stickman.dm b/code/modules/mob/living/simple_animal/hostile/stickman.dm index 7a86870aa1..fa5cb151ed 100644 --- a/code/modules/mob/living/simple_animal/hostile/stickman.dm +++ b/code/modules/mob/living/simple_animal/hostile/stickman.dm @@ -13,6 +13,7 @@ response_disarm = "shoves" response_harm = "hits" speed = 0 + blood_volume = 0 stat_attack = UNCONSCIOUS robust_searching = 1 environment_smash = ENVIRONMENT_SMASH_NONE diff --git a/code/modules/mob/living/simple_animal/shade.dm b/code/modules/mob/living/simple_animal/shade.dm index 3bd24c4df7..32ef52a6ae 100644 --- a/code/modules/mob/living/simple_animal/shade.dm +++ b/code/modules/mob/living/simple_animal/shade.dm @@ -31,6 +31,7 @@ loot = list(/obj/item/ectoplasm) del_on_death = TRUE initial_language_holder = /datum/language_holder/construct + blood_volume = 0 /mob/living/simple_animal/shade/death() deathmessage = "lets out a contented sigh as [p_their()] form unwinds." diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index 52e58aea4e..990fa008fe 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -4,6 +4,7 @@ health = 20 maxHealth = 20 gender = PLURAL //placeholder + blood_volume = 550 //How much blud it has for bloodsucking status_flags = CANPUSH diff --git a/code/modules/mob/living/simple_animal/slime/slime.dm b/code/modules/mob/living/simple_animal/slime/slime.dm index 185d717a00..d1e10ea693 100644 --- a/code/modules/mob/living/simple_animal/slime/slime.dm +++ b/code/modules/mob/living/simple_animal/slime/slime.dm @@ -26,6 +26,7 @@ health = 150 healable = 0 gender = NEUTER + blood_volume = 0 //Until someome reworks for them to have slime jelly see_in_dark = 8 diff --git a/code/modules/mob/living/status_procs.dm b/code/modules/mob/living/status_procs.dm index 0880f7f432..facc86da46 100644 --- a/code/modules/mob/living/status_procs.dm +++ b/code/modules/mob/living/status_procs.dm @@ -140,24 +140,28 @@ /////////////////////////////////// DISABILITIES //////////////////////////////////// -/mob/living/proc/add_quirk(quirk, spawn_effects) //separate proc due to the way these ones are handled - if(HAS_TRAIT(src, quirk)) +/mob/living/proc/add_quirk(quirktype, spawn_effects) //separate proc due to the way these ones are handled + if(has_quirk(quirktype)) return - if(!SSquirks || !SSquirks.quirks[quirk]) + var/datum/quirk/T = quirktype + var/qname = initial(T.name) + if(!SSquirks || !SSquirks.quirks[qname]) return - var/datum/quirk/T = SSquirks.quirks[quirk] - new T (src, spawn_effects) + new quirktype (src, spawn_effects) return TRUE -/mob/living/proc/remove_quirk(quirk) - var/datum/quirk/T = roundstart_quirks[quirk] - if(T) - qdel(T) - return TRUE - -/mob/living/proc/has_quirk(quirk) - return roundstart_quirks[quirk] +/mob/living/proc/remove_quirk(quirktype) + for(var/datum/quirk/Q in roundstart_quirks) + if(Q.type == quirktype) + qdel(Q) + return TRUE + return FALSE +/mob/living/proc/has_quirk(quirktype) + for(var/datum/quirk/Q in roundstart_quirks) + if(Q.type == quirktype) + return TRUE + return FALSE /////////////////////////////////// TRAIT PROCS //////////////////////////////////// /mob/living/proc/cure_blind(list/sources) diff --git a/code/modules/mob/living/ventcrawling.dm b/code/modules/mob/living/ventcrawling.dm index cacd1c7c56..7f8513bfd9 100644 --- a/code/modules/mob/living/ventcrawling.dm +++ b/code/modules/mob/living/ventcrawling.dm @@ -8,6 +8,7 @@ GLOBAL_LIST_INIT(ventcrawl_machinery, typecacheof(list( /mob/living/proc/handle_ventcrawl(atom/A) if(!ventcrawler || !Adjacent(A)) return + . = TRUE //return value to stop the client from being shown the turf contents stat tab on alt-click. if(stat) to_chat(src, "You must be conscious to do this!") return diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index d4528fb91f..a47ea9d5a0 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -465,9 +465,10 @@ mob/visible_message(message, self_message, blind_message, vision_distance = DEFA /mob/proc/transfer_ckey(mob/new_mob, send_signal = TRUE) if(!ckey || !new_mob) CRASH("transfer_ckey() called [ckey ? "" : "on a ckey-less mob[new_mob ? "" : " and "]"][new_mob ? "" : "without a valid mob target"]!") + SEND_SIGNAL(new_mob, COMSIG_MOB_PRE_PLAYER_CHANGE, new_mob, src) + new_mob.ckey = ckey if(send_signal) SEND_SIGNAL(src, COMSIG_MOB_KEY_CHANGE, new_mob, src) - new_mob.ckey = ckey return TRUE /mob/verb/cancel_camera() diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index 0cb886f11b..a9f4c94397 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -99,6 +99,12 @@ var/obj/control_object //Used by admins to possess objects. All mobs should have this var var/atom/movable/remote_control //Calls relaymove() to whatever it is + /** + * The sound made on death + * + * leave null for no sound. used for *deathgasp + */ + var/deathsound = null var/turf/listed_turf = null //the current turf being examined in the stat panel diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm index 2c8935c786..1a836c06cf 100644 --- a/code/modules/mob/mob_movement.dm +++ b/code/modules/mob/mob_movement.dm @@ -366,6 +366,9 @@ if(m_intent == MOVE_INTENT_RUN) m_intent = MOVE_INTENT_WALK else + if (HAS_TRAIT(src,TRAIT_NORUNNING)) // FULPSTATION 7/10/19 So you can't run during fortitude. + to_chat(src, "You find yourself unable to run.") + return FALSE m_intent = MOVE_INTENT_RUN if(hud_used && hud_used.static_inventory) for(var/obj/screen/mov_intent/selector in hud_used.static_inventory) @@ -401,4 +404,4 @@ return TRUE /mob/proc/canZMove(direction, turf/target) - return FALSE \ No newline at end of file + return FALSE diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm index 1b3501f78b..79c74de2eb 100644 --- a/code/modules/modular_computers/computers/item/computer.dm +++ b/code/modules/modular_computers/computers/item/computer.dm @@ -126,7 +126,7 @@ portable_drive.verb_pickup() /obj/item/modular_computer/AltClick(mob/user) - ..() + . = ..() if(issilicon(user)) return @@ -142,7 +142,7 @@ return if(ai_slot) ai_slot.try_eject(null, user) - + return TRUE // Gets IDs/access levels from card slot. Would be useful when/if PDAs would become modular PCs. /obj/item/modular_computer/GetAccess() diff --git a/code/modules/modular_computers/computers/item/laptop.dm b/code/modules/modular_computers/computers/item/laptop.dm index 4d4dee1b8c..ce8ab9659c 100644 --- a/code/modules/modular_computers/computers/item/laptop.dm +++ b/code/modules/modular_computers/computers/item/laptop.dm @@ -86,8 +86,8 @@ /obj/item/modular_computer/laptop/AltClick(mob/user) if(screen_on) // Close it. try_toggle_open(user) - else - return ..() + return TRUE + return ..() /obj/item/modular_computer/laptop/proc/toggle_open(mob/living/user=null) if(screen_on) diff --git a/code/modules/modular_computers/computers/machinery/modular_computer.dm b/code/modules/modular_computers/computers/machinery/modular_computer.dm index b3476e7046..a988003b67 100644 --- a/code/modules/modular_computers/computers/machinery/modular_computer.dm +++ b/code/modules/modular_computers/computers/machinery/modular_computer.dm @@ -94,8 +94,9 @@ cpu.eject_card() /obj/machinery/modular_computer/AltClick(mob/user) + . = ..() if(cpu) - cpu.AltClick(user) + return cpu.AltClick(user) //ATTACK HAND IGNORING PARENT RETURN VALUE // On-click handling. Turns on the computer if it's off and opens the GUI. diff --git a/code/modules/modular_computers/hardware/ai_slot.dm b/code/modules/modular_computers/hardware/ai_slot.dm index 47cbbff418..8428467a87 100644 --- a/code/modules/modular_computers/hardware/ai_slot.dm +++ b/code/modules/modular_computers/hardware/ai_slot.dm @@ -41,6 +41,13 @@ /obj/item/computer_hardware/ai_slot/try_eject(slot=0,mob/living/user = null,forced = 0) + if (get_dist(src,user) > 1) + if (iscarbon(user)) + var/mob/living/carbon/H = user + if (!(H.dna && H.dna.check_mutation(TK) && tkMaxRangeCheck(src,H))) + return FALSE + else + return FALSE if(!stored_card) to_chat(user, "There is no card in \the [src].") return FALSE diff --git a/code/modules/modular_computers/hardware/card_slot.dm b/code/modules/modular_computers/hardware/card_slot.dm index c68e1ad119..e4bc45dbc5 100644 --- a/code/modules/modular_computers/hardware/card_slot.dm +++ b/code/modules/modular_computers/hardware/card_slot.dm @@ -73,6 +73,13 @@ /obj/item/computer_hardware/card_slot/try_eject(slot=0, mob/living/user = null, forced = 0) + if (get_dist(src,user) > 1) + if (iscarbon(user)) + var/mob/living/carbon/H = user + if (!(H.dna && H.dna.check_mutation(TK) && tkMaxRangeCheck(src,H))) + return FALSE + else + return FALSE if(!stored_card && !stored_card2) to_chat(user, "There are no cards in \the [src].") return FALSE diff --git a/code/modules/paperwork/paperplane.dm b/code/modules/paperwork/paperplane.dm index f8d07862bf..2ca6ce2611 100644 --- a/code/modules/paperwork/paperplane.dm +++ b/code/modules/paperwork/paperplane.dm @@ -126,6 +126,7 @@ . += "Alt-click [src] to fold it into a paper plane." /obj/item/paper/AltClick(mob/living/carbon/user, obj/item/I) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user), NO_TK)) return to_chat(user, "You fold [src] into the shape of a plane!") @@ -138,3 +139,4 @@ I = new plane_type(user, src) user.put_in_hands(I) + return TRUE diff --git a/code/modules/photography/camera/camera.dm b/code/modules/photography/camera/camera.dm index 9d01eca0cf..96738e5021 100644 --- a/code/modules/photography/camera/camera.dm +++ b/code/modules/photography/camera/camera.dm @@ -46,13 +46,14 @@ . += "Alt-click to change its focusing, allowing you to set how big of an area it will capture." /obj/item/camera/AltClick(mob/user) + . = ..() if(!user.canUseTopic(src, BE_CLOSE)) return var/desired_x = input(user, "How high do you want the camera to shoot, between [picture_size_x_min] and [picture_size_x_max]?", "Zoom", picture_size_x) as num var/desired_y = input(user, "How wide do you want the camera to shoot, between [picture_size_y_min] and [picture_size_y_max]?", "Zoom", picture_size_y) as num picture_size_x = min(CLAMP(desired_x, picture_size_x_min, picture_size_x_max), CAMERA_PICTURE_SIZE_HARD_LIMIT) picture_size_y = min(CLAMP(desired_y, picture_size_y_min, picture_size_y_max), CAMERA_PICTURE_SIZE_HARD_LIMIT) - + return TRUE /obj/item/camera/attack(mob/living/carbon/human/M, mob/user) return diff --git a/code/modules/power/apc.dm b/code/modules/power/apc.dm index 5e3888a8c5..2652026a11 100644 --- a/code/modules/power/apc.dm +++ b/code/modules/power/apc.dm @@ -702,11 +702,11 @@ return ..() /obj/machinery/power/apc/AltClick(mob/user) - ..() + . = ..() if(!user.canUseTopic(src, !issilicon(user)) || !isturf(loc)) return - else - togglelock(user) + togglelock(user) + return TRUE /obj/machinery/power/apc/proc/togglelock(mob/living/user) if(obj_flags & EMAGGED) @@ -1113,9 +1113,9 @@ if(terminal && terminal.powernet) terminal.add_load(amount) -/obj/machinery/power/apc/avail() +/obj/machinery/power/apc/avail(amount) if(terminal) - return terminal.avail() + return terminal.avail(amount) else return 0 diff --git a/code/modules/power/cable.dm b/code/modules/power/cable.dm index a3dc6e7394..cc03976f79 100644 --- a/code/modules/power/cable.dm +++ b/code/modules/power/cable.dm @@ -217,9 +217,9 @@ By design, d1 is the smallest direction and d2 is the highest else return 0 -/obj/structure/cable/proc/avail() +/obj/structure/cable/proc/avail(amount) if(powernet) - return powernet.avail + return amount ? powernet.avail >= amount : powernet.avail else return 0 diff --git a/code/modules/power/power.dm b/code/modules/power/power.dm index 58a259de3a..d2d3d60066 100644 --- a/code/modules/power/power.dm +++ b/code/modules/power/power.dm @@ -46,9 +46,9 @@ else return 0 -/obj/machinery/power/proc/avail() +/obj/machinery/power/proc/avail(amount) if(powernet) - return powernet.avail + return amount ? powernet.avail >= amount : powernet.avail else return 0 diff --git a/code/modules/power/singularity/collector.dm b/code/modules/power/singularity/collector.dm index b1b0d2d718..054b91f273 100644 --- a/code/modules/power/singularity/collector.dm +++ b/code/modules/power/singularity/collector.dm @@ -110,7 +110,7 @@ if(!user.transferItemToLoc(W, src)) return loaded_tank = W - update_icons() + update_icon() else if(W.GetID()) if(allowed(user)) if(active) @@ -197,14 +197,14 @@ if(active) toggle_power() else - update_icons() + update_icon() /obj/machinery/power/rad_collector/rad_act(pulse_strength) . = ..() if(loaded_tank && active && pulse_strength > RAD_COLLECTOR_EFFICIENCY) stored_power += (pulse_strength-RAD_COLLECTOR_EFFICIENCY)*RAD_COLLECTOR_COEFFICIENT -/obj/machinery/power/rad_collector/proc/update_icons() +/obj/machinery/power/rad_collector/update_icon() cut_overlays() if(loaded_tank) add_overlay("ptank") @@ -222,7 +222,7 @@ else icon_state = "ca" flick("ca_deactive", src) - update_icons() + update_icon() return #undef RAD_COLLECTOR_EFFICIENCY diff --git a/code/modules/projectiles/guns/ballistic/shotgun.dm b/code/modules/projectiles/guns/ballistic/shotgun.dm index 1e1b518849..571525d8f0 100644 --- a/code/modules/projectiles/guns/ballistic/shotgun.dm +++ b/code/modules/projectiles/guns/ballistic/shotgun.dm @@ -225,10 +225,11 @@ spread = 2 /obj/item/gun/ballistic/shotgun/automatic/combat/compact/AltClick(mob/living/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user))) return toggle_stock(user) - . = ..() + return TRUE /obj/item/gun/ballistic/shotgun/automatic/combat/compact/examine(mob/user) . = ..() @@ -289,8 +290,10 @@ to_chat(user, "You switch to tube A.") /obj/item/gun/ballistic/shotgun/automatic/dual_tube/AltClick(mob/living/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user))) return pump() + return TRUE // DOUBLE BARRELED SHOTGUN and IMPROVISED SHOTGUN are in revolver.dm diff --git a/code/modules/projectiles/guns/misc/syringe_gun.dm b/code/modules/projectiles/guns/misc/syringe_gun.dm index 8a9d1c5b6b..d947e3155d 100644 --- a/code/modules/projectiles/guns/misc/syringe_gun.dm +++ b/code/modules/projectiles/guns/misc/syringe_gun.dm @@ -151,3 +151,17 @@ max_syringes = 1 desc = "[initial(desc)] It has a [B] strapped to it, but it doesn't seem to be doing anything." ..() + +/obj/item/gun/syringe/blowgun + name = "blowgun" + desc = "Fire syringes at a short distance." + icon_state = "blowgun" + item_state = "blowgun" + fire_sound = 'sound/items/syringeproj.ogg' + +/obj/item/gun/syringe/blowgun/process_fire(atom/target, mob/living/user, message = TRUE, params = null, zone_override = "", bonus_spread = 0) + visible_message("[user] starts aiming with a blowgun!") + if(do_after(user, 25, target = src)) + user.adjustStaminaLoss(20) + user.adjustOxyLoss(20) + ..() diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 88e766edfc..9141a6d299 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -180,7 +180,7 @@ else new /obj/effect/temp_visual/dir_setting/bloodsplatter(target_loca, splatter_dir, bloodtype_to_color()) - if(iscarbon(L)) + if(iscarbon(L) && !HAS_TRAIT(L, TRAIT_NOMARROW)) var/mob/living/carbon/C = L C.bleed(damage) else diff --git a/code/modules/projectiles/projectile/magic.dm b/code/modules/projectiles/projectile/magic.dm index d91c60367d..173d9721a9 100644 --- a/code/modules/projectiles/projectile/magic.dm +++ b/code/modules/projectiles/projectile/magic.dm @@ -507,4 +507,32 @@ return var/turf/T = get_turf(target) for(var/i=0, i<50, i+=10) - addtimer(CALLBACK(GLOBAL_PROC, .proc/explosion, T, -1, exp_heavy, exp_light, exp_flash, FALSE, FALSE, exp_fire), i) \ No newline at end of file + addtimer(CALLBACK(GLOBAL_PROC, .proc/explosion, T, -1, exp_heavy, exp_light, exp_flash, FALSE, FALSE, exp_fire), i) + +/obj/item/projectile/magic/nuclear + name = "\proper blazing manliness" + icon_state = "nuclear" + nodamage = TRUE + var/mob/living/victim = null + var/used = 0 + +/obj/item/projectile/magic/nuclear/on_hit(target) + if(used) + return + new/obj/effect/temp_visual/slugboom(get_turf(src)) + if(ismob(target)) + if(target == victim) + return + used = 1 + visible_message("[victim] slams into [target] with explosive force!") + explosion(src, 2, 3, 4, -1, TRUE, FALSE, 5) + else + used = 1 + victim.take_overall_damage(30,30) + victim.Knockdown(60) + explosion(src, -1, -1, -1, -1, FALSE, FALSE, 5) + +/obj/item/projectile/magic/nuclear/Destroy() + for(var/atom/movable/AM in contents) + AM.forceMove(get_turf(src)) + . = ..() \ No newline at end of file diff --git a/code/modules/reagents/chemistry/machinery/chem_dispenser.dm b/code/modules/reagents/chemistry/machinery/chem_dispenser.dm index 7e5cb21822..c6aa51deda 100644 --- a/code/modules/reagents/chemistry/machinery/chem_dispenser.dm +++ b/code/modules/reagents/chemistry/machinery/chem_dispenser.dm @@ -161,7 +161,7 @@ . = ..() if(A == beaker) beaker = null - cut_overlays() + update_icon() /obj/machinery/chem_dispenser/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, \ datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state) @@ -428,10 +428,11 @@ return final_list /obj/machinery/chem_dispenser/AltClick(mob/living/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK)) return replace_beaker(user) - return + return TRUE /obj/machinery/chem_dispenser/drinks/Initialize() . = ..() diff --git a/code/modules/reagents/chemistry/machinery/chem_heater.dm b/code/modules/reagents/chemistry/machinery/chem_heater.dm index eeb452dbb5..b4f14c69a7 100644 --- a/code/modules/reagents/chemistry/machinery/chem_heater.dm +++ b/code/modules/reagents/chemistry/machinery/chem_heater.dm @@ -29,10 +29,11 @@ icon_state = "mixer0b" /obj/machinery/chem_heater/AltClick(mob/living/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK)) return replace_beaker(user) - return + return TRUE /obj/machinery/chem_heater/proc/replace_beaker(mob/living/user, obj/item/reagent_containers/new_beaker) if(beaker) diff --git a/code/modules/reagents/chemistry/machinery/chem_master.dm b/code/modules/reagents/chemistry/machinery/chem_master.dm index f540ae850d..1ac62ba651 100644 --- a/code/modules/reagents/chemistry/machinery/chem_master.dm +++ b/code/modules/reagents/chemistry/machinery/chem_master.dm @@ -58,14 +58,14 @@ if(bottle) bottle.ex_act(severity, target) -/obj/machinery/chem_master/handle_atom_del(atom/A) - ..() +/obj/machinery/chem_master/Exited(atom/movable/A, atom/newloc) + . = ..() if(A == beaker) beaker = null - reagents.clear_reagents() update_icon() - else if(A == bottle) + if(A == bottle) bottle = null + update_icon() /obj/machinery/chem_master/update_icon() cut_overlays() @@ -103,6 +103,10 @@ updateUsrDialog() update_icon() else if(!condi && istype(I, /obj/item/storage/pill_bottle)) + . = TRUE // no afterattack + if(panel_open) + to_chat(user, "You can't use the [src.name] while its panel is opened!") + return if(!user.transferItemToLoc(I, src)) return replace_pillbottle(user, I) @@ -112,40 +116,40 @@ return ..() /obj/machinery/chem_master/AltClick(mob/living/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK)) return - replace_beaker(user) - return + if(beaker) + replace_beaker(user) + else if(bottle) + replace_pillbottle(user) + return TRUE /obj/machinery/chem_master/proc/replace_beaker(mob/living/user, obj/item/reagent_containers/new_beaker) if(beaker) - beaker.forceMove(drop_location()) + var/obj/item/reagent_containers/B = beaker + B.forceMove(drop_location()) if(user && Adjacent(user) && !issiliconoradminghost(user)) - user.put_in_hands(beaker) + user.put_in_hands(B) if(new_beaker) beaker = new_beaker - else - beaker = null update_icon() - return TRUE /obj/machinery/chem_master/proc/replace_pillbottle(mob/living/user, obj/item/storage/pill_bottle/new_bottle) if(bottle) - bottle.forceMove(drop_location()) + var/obj/item/storage/pill_bottle/B = bottle + B.forceMove(drop_location()) if(user && Adjacent(user) && !issiliconoradminghost(user)) - user.put_in_hands(beaker) + user.put_in_hands(B) else - adjust_item_drop_location(bottle) + adjust_item_drop_location(B) if(new_bottle) bottle = new_bottle - else - bottle = null - update_icon() - return TRUE /obj/machinery/chem_master/on_deconstruction() - replace_beaker(usr) - replace_pillbottle(usr) + var/atom/A = drop_location() + beaker.forceMove(A) + bottle.forceMove(A) return ..() /obj/machinery/chem_master/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, \ @@ -260,15 +264,16 @@ if(!name || !reagents.total_volume || !src || QDELETED(src) || !usr.canUseTopic(src, !issilicon(usr))) return var/obj/item/reagent_containers/pill/P - var/target_loc = bottle ? bottle : drop_location() + var/target_loc = drop_location() var/drop_threshold = INFINITY if(bottle) var/datum/component/storage/STRB = bottle.GetComponent(/datum/component/storage) if(STRB) drop_threshold = STRB.max_items - bottle.contents.len + target_loc = bottle for(var/i in 1 to amount) - if(i < drop_threshold) + if(i <= drop_threshold) P = new(target_loc) else P = new(drop_location()) diff --git a/code/modules/reagents/chemistry/machinery/pandemic.dm b/code/modules/reagents/chemistry/machinery/pandemic.dm index 05fa4d382a..6ae0a682d8 100644 --- a/code/modules/reagents/chemistry/machinery/pandemic.dm +++ b/code/modules/reagents/chemistry/machinery/pandemic.dm @@ -237,10 +237,11 @@ return ..() /obj/machinery/computer/pandemic/AltClick(mob/living/user) + . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK)) return replace_beaker(user) - return + return TRUE /obj/machinery/computer/pandemic/proc/replace_beaker(mob/living/user, obj/item/reagent_containers/new_beaker) if(beaker) diff --git a/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm b/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm index f96acf2d71..fd28ff9c47 100644 --- a/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm @@ -610,6 +610,8 @@ All effects don't start immediately, but rather get worse over time; the rate is value = 1.3 /datum/reagent/consumable/ethanol/bloody_mary/on_mob_life(mob/living/carbon/C) + if((HAS_TRAIT(C, TRAIT_NOMARROW))) + return if(C.blood_volume < (BLOOD_VOLUME_NORMAL*C.blood_ratio)) C.blood_volume = min((BLOOD_VOLUME_NORMAL*C.blood_ratio), C.blood_volume + 3) //Bloody Mary quickly restores blood loss. ..() diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm index 2a8ad86646..60c5cd6e71 100644 --- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm @@ -355,6 +355,8 @@ datum/reagent/medicine/styptic_powder/overdose_start(mob/living/M) value = 1 /datum/reagent/medicine/salglu_solution/on_mob_life(mob/living/carbon/M) + if((HAS_TRAIT(M, TRAIT_NOMARROW))) + return if(last_added) M.blood_volume -= last_added last_added = 0 diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm index bb65392057..512b8a21a1 100644 --- a/code/modules/reagents/chemistry/reagents/other_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm @@ -1,5 +1,5 @@ /datum/reagent/blood - data = list("donor"=null,"viruses"=null,"blood_DNA"=null, "bloodcolor" = BLOOD_COLOR_HUMAN, "blood_type"= null,"resistances"=null,"trace_chem"=null,"mind"=null,"ckey"=null,"gender"=null,"real_name"=null,"cloneable"=null,"factions"=null) + data = list("donor"=null,"viruses"=null,"blood_DNA"=null, "bloodcolor" = BLOOD_COLOR_HUMAN, "blood_type"= null,"resistances"=null,"trace_chem"=null,"mind"=null,"ckey"=null,"gender"=null,"real_name"=null,"cloneable"=null,"factions"=null,"quirks"=null) name = "Blood" id = "blood" value = 1 @@ -30,7 +30,7 @@ if(iscarbon(L)) var/mob/living/carbon/C = L var/blood_id = C.get_blood_id() - if((blood_id == "blood" || blood_id == "jellyblood") && (method == INJECT || (method == INGEST && C.dna && C.dna.species && (DRINKSBLOOD in C.dna.species.species_traits)))) + if((HAS_TRAIT(C, TRAIT_NOMARROW) || blood_id == "blood" || blood_id == "jellyblood") && (method == INJECT || (method == INGEST && C.dna && C.dna.species && (DRINKSBLOOD in C.dna.species.species_traits)))) C.blood_volume = min(C.blood_volume + round(reac_volume, 0.1), BLOOD_VOLUME_MAXIMUM * C.blood_ratio) // we don't care about bloodtype here, we're just refilling the mob @@ -38,6 +38,8 @@ L.add_blood_DNA(list(data["blood_DNA"] = data["blood_type"])) /datum/reagent/blood/on_mob_life(mob/living/carbon/C) //Because lethals are preferred over stamina. damnifino. + if((HAS_TRAIT(C, TRAIT_NOMARROW))) + return //We dont want vampires getting toxed from blood var/blood_id = C.get_blood_id() if((blood_id == "blood" || blood_id == "jellyblood")) if(!data || !(data["blood_type"] in get_safe_blood(C.dna.blood_type))) //we only care about bloodtype here because this is where the poisoning should be @@ -1116,6 +1118,8 @@ color = "#c2391d" /datum/reagent/iron/on_mob_life(mob/living/carbon/C) + if((HAS_TRAIT(C, TRAIT_NOMARROW))) + return if(C.blood_volume < (BLOOD_VOLUME_NORMAL*C.blood_ratio)) C.blood_volume += 0.01 //we'll have synthetics from medbay. ..() diff --git a/code/modules/reagents/reagent_containers/blood_pack.dm b/code/modules/reagents/reagent_containers/blood_pack.dm index fe35981bfe..61449fc153 100644 --- a/code/modules/reagents/reagent_containers/blood_pack.dm +++ b/code/modules/reagents/reagent_containers/blood_pack.dm @@ -107,8 +107,29 @@ else return ..() +/obj/item/reagent_containers/blood/attack(mob/M, mob/user, def_zone) + if(user.a_intent == INTENT_HELP && reagents.total_volume > 0) + if (user != M) + to_chat(user, "You force [M] to drink from the [src]") + user.visible_message("[user] forces [M] to drink from the [src].") + if(!do_mob(user, M, 50)) + return + else + if(!do_mob(user, M, 10)) + return + to_chat(user, "You take a sip from the [src].") + user.visible_message("[user] puts the [src] up to their mouth.") + if(reagents.total_volume <= 0) // Safety: In case you spam clicked the blood bag on yourself, and it is now empty (below will divide by zero) + return + var/gulp_size = 5 + var/fraction = min(gulp_size / reagents.total_volume, 1) + reagents.reaction(M, INGEST, fraction) //checkLiked(fraction, M) // Blood isn't food, sorry. + reagents.trans_to(M, gulp_size) + playsound(M.loc,'sound/items/drink.ogg', rand(10,50), 1) + ..() + /obj/item/reagent_containers/blood/bluespace name = "bluespace blood pack" desc = "Contains blood used for transfusion, this one has been made with bluespace technology to hold much more blood. Must be attached to an IV drip." icon_state = "bsbloodpack" - volume = 600 //its a blood bath! \ No newline at end of file + volume = 600 //its a blood bath! diff --git a/code/modules/reagents/reagent_containers/hypospray.dm b/code/modules/reagents/reagent_containers/hypospray.dm index 85cb544667..f1a4d1554e 100644 --- a/code/modules/reagents/reagent_containers/hypospray.dm +++ b/code/modules/reagents/reagent_containers/hypospray.dm @@ -338,8 +338,10 @@ return FALSE /obj/item/hypospray/mkii/AltClick(mob/user) + . = ..() if(vial) vial.attack_self(user) + return TRUE // Gunna allow this for now, still really don't approve - Pooj /obj/item/hypospray/mkii/emag_act(mob/user) diff --git a/code/modules/reagents/reagent_containers/rags.dm b/code/modules/reagents/reagent_containers/rags.dm index 414208d0bf..d09b18f244 100644 --- a/code/modules/reagents/reagent_containers/rags.dm +++ b/code/modules/reagents/reagent_containers/rags.dm @@ -104,6 +104,7 @@ msg += "'s liquids into \the [target]" reagents.trans_to(target, reagents.total_volume) to_chat(user, "[msg].") + return TRUE /obj/item/reagent_containers/rag/towel diff --git a/code/modules/recycling/conveyor2.dm b/code/modules/recycling/conveyor2.dm index 5875e821a3..7701e58616 100644 --- a/code/modules/recycling/conveyor2.dm +++ b/code/modules/recycling/conveyor2.dm @@ -135,6 +135,9 @@ GLOBAL_LIST_EMPTY(conveyors_by_id) addtimer(CALLBACK(src, .proc/convey, affecting), 1) /obj/machinery/conveyor/proc/convey(list/affecting) + var/turf/T = get_step(src, movedir) + if(length(T.contents) > 150) + return for(var/atom/movable/A in affecting) if((A.loc == loc) && A.has_gravity()) A.ConveyorMove(movedir) diff --git a/code/modules/research/designs/mechfabricator_designs.dm b/code/modules/research/designs/mechfabricator_designs.dm index e8c8bab09f..113a6262c9 100644 --- a/code/modules/research/designs/mechfabricator_designs.dm +++ b/code/modules/research/designs/mechfabricator_designs.dm @@ -696,15 +696,6 @@ construction_time = 120 category = list("Cyborg Upgrade Modules") -/datum/design/borg_upgrade_defibrillator - name = "Cyborg Upgrade (Defibrillator)" - id = "borg_upgrade_defibrillator" - build_type = MECHFAB - build_path = /obj/item/borg/upgrade/defib - materials = list(MAT_METAL=15000, MAT_GLASS=15000, MAT_SILVER=10000, MAT_GOLD=10000, MAT_TITANIUM=5000, MAT_DIAMOND=5000) - construction_time = 120 - category = list("Cyborg Upgrade Modules") - /datum/design/borg_upgrade_surgicalprocessor name = "Cyborg Upgrade (Surgical Processor)" id = "borg_upgrade_surgicalprocessor" diff --git a/code/modules/research/nanites/nanite_hijacker.dm b/code/modules/research/nanites/nanite_hijacker.dm index 88779df447..920c42b411 100644 --- a/code/modules/research/nanites/nanite_hijacker.dm +++ b/code/modules/research/nanites/nanite_hijacker.dm @@ -14,6 +14,7 @@ return if(disk) eject() + return TRUE /obj/item/nanite_hijacker/examine(mob/user) . = ..() diff --git a/code/modules/research/nanites/nanite_programs/healing.dm b/code/modules/research/nanites/nanite_programs/healing.dm index 50fb5efcb0..e2e1661ab7 100644 --- a/code/modules/research/nanites/nanite_programs/healing.dm +++ b/code/modules/research/nanites/nanite_programs/healing.dm @@ -98,7 +98,7 @@ /datum/nanite_program/blood_restoring/check_conditions() if(iscarbon(host_mob)) var/mob/living/carbon/C = host_mob - if(C.blood_volume >= (BLOOD_VOLUME_SAFE*C.blood_ratio)) + if(C.blood_volume >= (BLOOD_VOLUME_SAFE*C.blood_ratio) || (HAS_TRAIT(C, TRAIT_NOMARROW))) return FALSE else return FALSE diff --git a/code/modules/research/nanites/nanite_remote.dm b/code/modules/research/nanites/nanite_remote.dm index 3b242d28df..a7c8533521 100644 --- a/code/modules/research/nanites/nanite_remote.dm +++ b/code/modules/research/nanites/nanite_remote.dm @@ -35,6 +35,7 @@ update_icon() else to_chat(user, "Access denied.") + return TRUE /obj/item/nanite_remote/emag_act(mob/user) . = ..() diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm index c0aa4a3127..7ccdb76dd9 100644 --- a/code/modules/research/techweb/all_nodes.dm +++ b/code/modules/research/techweb/all_nodes.dm @@ -400,7 +400,7 @@ display_name = "Cyborg Upgrades: Medical" description = "Medical upgrades for cyborgs." prereq_ids = list("adv_biotech", "robotics") - design_ids = list("borg_upgrade_defibrillator", "borg_upgrade_advhealth", "borg_upgrade_piercinghypospray", "borg_upgrade_highstrengthsynthesiser", "borg_upgrade_expandedsynthesiser", "borg_upgrade_pinpointer", "borg_upgrade_surgicalprocessor") + design_ids = list("borg_upgrade_advhealth", "borg_upgrade_piercinghypospray", "borg_upgrade_highstrengthsynthesiser", "borg_upgrade_expandedsynthesiser", "borg_upgrade_pinpointer", "borg_upgrade_surgicalprocessor") research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2000) export_price = 5000 @@ -1043,14 +1043,21 @@ display_name = "Alien Technology" description = "Things used by the greys." prereq_ids = list("biotech","engineering") - boost_item_paths = list(/obj/item/gun/energy/alien, /obj/item/scalpel/alien, /obj/item/hemostat/alien, /obj/item/retractor/alien, /obj/item/circular_saw/alien, - /obj/item/cautery/alien, /obj/item/surgicaldrill/alien, /obj/item/screwdriver/abductor, /obj/item/wrench/abductor, /obj/item/crowbar/abductor, /obj/item/multitool/abductor, /obj/item/stock_parts/cell/infinite/abductor, - /obj/item/weldingtool/abductor, /obj/item/wirecutters/abductor, /obj/item/circuitboard/machine/abductor, /obj/item/abductor_baton, /obj/item/abductor, /obj/item/stack/sheet/mineral/abductor) research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 5000) export_price = 20000 hidden = TRUE design_ids = list("alienalloy") +/datum/techweb_node/alientech/New() + . = ..() + boost_item_paths = typesof(/obj/item/gun/energy/alien, /obj/item/scalpel/alien, /obj/item/hemostat/alien, + /obj/item/retractor/alien, /obj/item/circular_saw/alien, /obj/item/cautery/alien, + /obj/item/surgicaldrill/alien, /obj/item/screwdriver/abductor, /obj/item/wrench/abductor, + /obj/item/crowbar/abductor, /obj/item/multitool/abductor, + /obj/item/stock_parts/cell/infinite/abductor, /obj/item/weldingtool/abductor, + /obj/item/wirecutters/abductor, /obj/item/circuitboard/machine/abductor, + /obj/item/abductor_baton, /obj/item/abductor, /obj/item/stack/sheet/mineral/abductor) + /datum/techweb_node/alien_bio id = "alien_bio" display_name = "Alien Biological Tools" diff --git a/code/modules/ruins/spaceruin_code/hilbertshotel.dm b/code/modules/ruins/spaceruin_code/hilbertshotel.dm index cd641adb33..f64b5e4d01 100644 --- a/code/modules/ruins/spaceruin_code/hilbertshotel.dm +++ b/code/modules/ruins/spaceruin_code/hilbertshotel.dm @@ -284,6 +284,7 @@ GLOBAL_VAR_INIT(hhmysteryRoomNumber, 1337) var/datum/action/peepholeCancel/PHC = new user.overlay_fullscreen("remote_view", /obj/screen/fullscreen/impaired, 1) PHC.Grant(user) + return TRUE /turf/closed/indestructible/hoteldoor/check_eye(mob/user) if(get_dist(get_turf(src), get_turf(user)) >= 2) diff --git a/code/modules/spells/spell_types/godhand.dm b/code/modules/spells/spell_types/godhand.dm index a5f371ffc2..8108b121ae 100644 --- a/code/modules/spells/spell_types/godhand.dm +++ b/code/modules/spells/spell_types/godhand.dm @@ -182,3 +182,38 @@ if(charges <= 0) qdel(src) + +/obj/item/melee/touch_attack/nuclearfist + name = "\improper PURE MANLINESS" + desc = "SHOW THEM RAW POWER" + catchphrase = "I CAST FIST!" + on_use_sound = 'sound/weapons/nuclear_fist.ogg' + icon_state = "disintegrate" + item_state = "disintegrate" + +/obj/item/melee/touch_attack/nuclearfist/afterattack(atom/movable/target, mob/living/carbon/user, proximity) + if(!proximity || target == user || !ismob(target) || !iscarbon(user) || user.lying || user.handcuffed) //exploding after touching yourself would be bad + return + if(!user.can_speak_vocal()) + to_chat(user, "You can't get the words out!") + return + var/mob/M = target + var/atom/A = M.anti_magic_check() + if(A) + if(isitem(A)) + target.visible_message("[target]'s [A] glows brightly as it wards off the spell!") + user.visible_message("The feedback blows [user]'s arm off!","The spell bounces from [M]'s skin back into your arm!") + user.flash_act() + var/obj/item/bodypart/part = user.get_holding_bodypart_of_item(src) + if(part) + part.dismember() + return ..() + var/angle = dir2angle(get_dir(src, get_step_away(target, src))) + var/obj/item/projectile/magic/nuclear/P = new(get_turf(src)) + P.victim = target + target.forceMove(P) + P.setAngle(angle) + P.original = user + P.firer = user + P.fire() + return ..() \ No newline at end of file diff --git a/code/modules/spells/spell_types/touch_attacks.dm b/code/modules/spells/spell_types/touch_attacks.dm index 0ffe02cec2..794ed797ad 100644 --- a/code/modules/spells/spell_types/touch_attacks.dm +++ b/code/modules/spells/spell_types/touch_attacks.dm @@ -23,7 +23,7 @@ remove_hand(TRUE) to_chat(user, "You draw the power out of your hand.") return - + for(var/mob/living/carbon/C in targets) if(!attached_hand) if(ChargeHand(C)) @@ -71,3 +71,15 @@ action_icon_state = "statue" sound = 'sound/magic/fleshtostone.ogg' + +/obj/effect/proc_holder/spell/targeted/touch/nuclear_fist + name = "Nuclear Fist" + desc = "This spell channels raw manliness, allowing you punch your enemies across the galaxy, causing them to detonate violently if hitting any other living being midflight. Does not work while laying down." + hand_path = /obj/item/melee/touch_attack/nuclearfist + + school = "evocation" + charge_max = 200 + clothes_req = 0 + cooldown_min = 40 + + action_icon_state = "nuclearfist" \ No newline at end of file diff --git a/code/modules/surgery/organs/augments_chest.dm b/code/modules/surgery/organs/augments_chest.dm index 81627104e8..65172a1545 100644 --- a/code/modules/surgery/organs/augments_chest.dm +++ b/code/modules/surgery/organs/augments_chest.dm @@ -44,6 +44,9 @@ hunger_threshold = NUTRITION_LEVEL_HUNGRY poison_amount = 10 +#define MAX_HEAL_COOLDOWN 15 MINUTES +#define DEF_CONVALESCENCE_TIME 15 SECONDS + /obj/item/organ/cyberimp/chest/reviver name = "Reviver implant" desc = "This implant will attempt to revive and heal you if you lose consciousness. For the faint of heart!" @@ -51,43 +54,49 @@ implant_color = "#AD0000" slot = ORGAN_SLOT_HEART_AID var/revive_cost = 0 - var/reviving = 0 + var/reviving = FALSE var/cooldown = 0 + var/convalescence_time = 0 /obj/item/organ/cyberimp/chest/reviver/on_life() if(reviving) - if(owner.stat == UNCONSCIOUS) - addtimer(CALLBACK(src, .proc/heal), 30) + var/do_heal = world.time < convalescence_time + if(revive_cost >= MAX_HEAL_COOLDOWN) + do_heal = FALSE + else if(owner.stat && owner.stat != DEAD) + do_heal = TRUE + else if(!do_heal) + convalescence_time = world.time + DEF_CONVALESCENCE_TIME + if(do_heal) + addtimer(CALLBACK(src, .proc/heal), 3 SECONDS) else cooldown = revive_cost + world.time reviving = FALSE to_chat(owner, "Your reviver implant shuts down and starts recharging. It will be ready again in [DisplayTimeText(revive_cost)].") return - if(cooldown > world.time) - return - if(owner.stat != UNCONSCIOUS) - return - if(owner.suiciding) + if(cooldown > world.time || owner.stat == CONSCIOUS || owner.stat == DEAD || owner.suiciding) return revive_cost = 0 + convalescence_time = 0 reviving = TRUE to_chat(owner, "You feel a faint buzzing as your reviver implant starts patching your wounds...") /obj/item/organ/cyberimp/chest/reviver/proc/heal() if(owner.getOxyLoss()) owner.adjustOxyLoss(-5) - revive_cost += 5 + revive_cost += 0.5 SECONDS if(owner.getBruteLoss()) owner.adjustBruteLoss(-2) - revive_cost += 40 + revive_cost += 4 SECONDS if(owner.getFireLoss()) owner.adjustFireLoss(-2) - revive_cost += 40 + revive_cost += 4 SECONDS if(owner.getToxLoss()) owner.adjustToxLoss(-1) - revive_cost += 40 + revive_cost += 4 SECONDS + /obj/item/organ/cyberimp/chest/reviver/emp_act(severity) . = ..() @@ -95,25 +104,27 @@ return if(reviving) - revive_cost += 200 + revive_cost += 20 SECONDS else - cooldown += 200 + cooldown += 20 SECONDS if(ishuman(owner)) var/mob/living/carbon/human/H = owner if(H.stat != DEAD && prob(50 / severity) && H.can_heartattack()) H.set_heartattack(TRUE) to_chat(H, "You feel a horrible agony in your chest!") - addtimer(CALLBACK(src, .proc/undo_heart_attack), 600 / severity) + addtimer(CALLBACK(src, .proc/undo_heart_attack), 60 SECONDS / severity) /obj/item/organ/cyberimp/chest/reviver/proc/undo_heart_attack() var/mob/living/carbon/human/H = owner - if(!istype(H)) + if(!H || !istype(H)) return H.set_heartattack(FALSE) - if(H.stat == CONSCIOUS) + if(H.stat == CONSCIOUS || H.stat == SOFT_CRIT) to_chat(H, "You feel your heart beating again!") +#undef MAX_HEAL_COOLDOWN +#undef DEF_CONVALESCENCE_TIME /obj/item/organ/cyberimp/chest/thrusters name = "implantable thrusters set" diff --git a/code/modules/surgery/organs/heart.dm b/code/modules/surgery/organs/heart.dm index 212605669d..5b74b58cd0 100644 --- a/code/modules/surgery/organs/heart.dm +++ b/code/modules/surgery/organs/heart.dm @@ -55,6 +55,11 @@ update_icon() return 1 +/obj/item/organ/heart/proc/HeartStrengthMessage() + if(beating) + return "a healthy" + return "an unstable" + /obj/item/organ/heart/prepare_eat() var/obj/S = ..() S.icon_state = "[icon_base]-off" diff --git a/code/modules/surgery/organs/tongue.dm b/code/modules/surgery/organs/tongue.dm index 6b435f282f..0fbd8c8406 100644 --- a/code/modules/surgery/organs/tongue.dm +++ b/code/modules/surgery/organs/tongue.dm @@ -22,6 +22,7 @@ /datum/language/ratvar, /datum/language/aphasia, /datum/language/slime, + /datum/language/vampiric, )) healing_factor = STANDARD_ORGAN_HEALING*5 //Fast!! decay_factor = STANDARD_ORGAN_DECAY/2 diff --git a/code/modules/vehicles/ridden.dm b/code/modules/vehicles/ridden.dm index 27da0f6cea..13a139ef2e 100644 --- a/code/modules/vehicles/ridden.dm +++ b/code/modules/vehicles/ridden.dm @@ -49,6 +49,7 @@ return ..() /obj/vehicle/ridden/AltClick(mob/user) + . = ..() if(inserted_key && user.canUseTopic(src, BE_CLOSE, ismonkey(user))) if(!is_occupant(user)) to_chat(user, "You must be riding the [src] to remove [src]'s key!") @@ -57,7 +58,7 @@ inserted_key.forceMove(drop_location()) user.put_in_hands(inserted_key) inserted_key = null - return ..() + return TRUE /obj/vehicle/ridden/driver_move(mob/user, direction) if(key_type && !is_key(inserted_key)) diff --git a/code/modules/vehicles/scooter.dm b/code/modules/vehicles/scooter.dm index 8c21b050aa..0dd7ff32a8 100644 --- a/code/modules/vehicles/scooter.dm +++ b/code/modules/vehicles/scooter.dm @@ -94,6 +94,7 @@ qdel(src) /obj/vehicle/ridden/scooter/skateboard/AltClick(mob/user) + . = ..() var/datum/component/riding/R = src.GetComponent(/datum/component/riding) if (!adjusted_speed) R.vehicle_move_delay = 0 @@ -103,6 +104,7 @@ R.vehicle_move_delay = 1 to_chat(user, "You adjust the wheels on [src] to make it go slower.") adjusted_speed = FALSE + return TRUE //CONSTRUCTION /obj/item/scooter_frame diff --git a/code/modules/vending/clothesmate.dm b/code/modules/vending/clothesmate.dm index 2b86d9e415..626abd51ef 100644 --- a/code/modules/vending/clothesmate.dm +++ b/code/modules/vending/clothesmate.dm @@ -115,8 +115,18 @@ /obj/item/clothing/ears/headphones = 10, /obj/item/clothing/suit/apron/purple_bartender = 4, /obj/item/clothing/under/rank/bartender/purple = 4, - /obj/item/clothing/accessory/attrocious_pokadots = 8, - /obj/item/clothing/accessory/black_white_pokadots = 8) + /obj/item/clothing/under/christmas/christmasmaler = 3, + /obj/item/clothing/under/christmas/christmasmaleg = 3, + /obj/item/clothing/under/christmas/christmasfemaler = 3, + /obj/item/clothing/under/christmas/christmasfemaleg = 3, + /obj/item/clothing/suit/hooded/wintercoat/christmascoatr = 3, + /obj/item/clothing/suit/hooded/wintercoat/christmascoatg = 3, + /obj/item/clothing/suit/hooded/wintercoat/christmascoatrg = 3, + /obj/item/clothing/head/christmashat = 3, + /obj/item/clothing/head/christmashatg = 3, + /obj/item/clothing/shoes/winterboots/christmasbootsr = 3, + /obj/item/clothing/shoes/winterboots/christmasbootsg = 3, + /obj/item/clothing/shoes/winterboots/santaboots = 3) contraband = list(/obj/item/clothing/under/syndicate/tacticool = 3, /obj/item/clothing/under/syndicate/tacticool/skirt = 3, /obj/item/clothing/mask/balaclava = 3, @@ -126,8 +136,7 @@ /obj/item/clothing/suit/jacket/letterman_syndie = 5, /obj/item/clothing/under/jabroni = 2, /obj/item/clothing/suit/vapeshirt = 2, - /obj/item/clothing/under/geisha = 4, - /obj/item/clothing/accessory/syndi_pokadots = 4, + /obj/item/clothing/under/geisha = 4,, /obj/item/clothing/under/keyholesweater = 3) premium = list(/obj/item/clothing/under/suit_jacket/checkered = 4, /obj/item/clothing/head/mailman = 2, @@ -136,8 +145,7 @@ /obj/item/clothing/suit/jacket/leather/overcoat = 4, /obj/item/clothing/under/pants/mustangjeans = 3, /obj/item/clothing/neck/necklace/dope = 5, - /obj/item/clothing/suit/jacket/letterman_nanotrasen = 5, - /obj/item/clothing/accessory/nt_pokadots = 5) + /obj/item/clothing/suit/jacket/letterman_nanotrasen = 5) refill_canister = /obj/item/vending_refill/clothing /obj/item/vending_refill/clothing diff --git a/config/game_options.txt b/config/game_options.txt index a44c68226e..3b031bb5b9 100644 --- a/config/game_options.txt +++ b/config/game_options.txt @@ -92,6 +92,7 @@ PROBABILITY SECRET_EXTENDED 0 PROBABILITY DEVIL 0 PROBABILITY DEVIL_AGENTS 0 PROBABILITY CLOWNOPS 0 +PROBABILITY BLOODSUCKER 0 ## You probably want to keep sandbox off by default for secret and random. PROBABILITY SANDBOX 0 @@ -114,6 +115,7 @@ CONTINUOUS CLOCKWORK_CULT CONTINUOUS CHANGELING CONTINUOUS WIZARD #CONTINUOUS MONKEY +CONTINUOUS BLOODSUCKER ##Note: do not toggle continuous off for these modes, as they have no antagonists and would thus end immediately! diff --git a/dependencies.sh b/dependencies.sh index a75940088f..0f66dcab1a 100644 --- a/dependencies.sh +++ b/dependencies.sh @@ -18,3 +18,9 @@ export BSQL_VERSION=v1.4.0.0 #node version export NODE_VERSION=8 + +# PHP version +export PHP_VERSION=5.6 + +# SpacemanDMM git tag +export SPACEMAN_DMM_VERSION=suite-1.0 diff --git a/html/changelogs/AutoChangeLog-pr-10011.yml b/html/changelogs/AutoChangeLog-pr-10011.yml new file mode 100644 index 0000000000..70d569699f --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10011.yml @@ -0,0 +1,4 @@ +author: "Arturlang" +delete-after: True +changes: + - rscadd: "Adds Bloodsuckers, beware." diff --git a/html/changelogs/AutoChangeLog-pr-10026.yml b/html/changelogs/AutoChangeLog-pr-10026.yml new file mode 100644 index 0000000000..6e8062874b --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10026.yml @@ -0,0 +1,4 @@ +author: "Linzolle" +delete-after: True +changes: + - rscadd: "neck slice. harm intent someone's head while they are unconscious or in a neck grab to make them bleed uncontrollably." diff --git a/html/changelogs/AutoChangeLog-pr-10050.yml b/html/changelogs/AutoChangeLog-pr-10050.yml new file mode 100644 index 0000000000..c736ded339 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10050.yml @@ -0,0 +1,5 @@ +author: "Ghommie" +delete-after: True +changes: + - tweak: "You shouldn't be able to target objects you can't see (excluding darkness) with the ARCD and RLD" + - tweak: "The admin RCD is ranged too, just like the ARCD." diff --git a/html/changelogs/AutoChangeLog-pr-10072.yml b/html/changelogs/AutoChangeLog-pr-10072.yml new file mode 100644 index 0000000000..9a6f45e61e --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10072.yml @@ -0,0 +1,5 @@ +author: "Useroth" +delete-after: True +changes: + - rscadd: "bamboo which can be used to build punji sticks/ blowguns available as a sugarcane mutation or in exotic seed crate" + - tweak: "changed the sugar cane growth stages because fuck if I know why, but it was in the PR" diff --git a/html/changelogs/AutoChangeLog-pr-10076.yml b/html/changelogs/AutoChangeLog-pr-10076.yml new file mode 100644 index 0000000000..0e04ceddbc --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10076.yml @@ -0,0 +1,4 @@ +author: "Ghommie" +delete-after: True +changes: + - bugfix: "Fixed alien tech node not being unlockable with subtypes of the accepted items." diff --git a/html/changelogs/AutoChangeLog-pr-10085.yml b/html/changelogs/AutoChangeLog-pr-10085.yml new file mode 100644 index 0000000000..a82f052fed --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10085.yml @@ -0,0 +1,6 @@ +author: "Ghommie" +delete-after: True +changes: + - bugfix: "Fixes reviver implant having been a crapshot ever since soft-crit was introduced years ago." + - tweak: "Added a \"convalescence\" time (about 15 seconds) after the user is out of unconsciousbess/crit to ensure they are properly stabilized." + - tweak: "Added a 15 minutes hardcap for accumulated revive cooldown (equivalent to 150 points of brute or burn healed) above which the implant starts cooling down regardless of user's conditions." diff --git a/html/changelogs/AutoChangeLog-pr-10111.yml b/html/changelogs/AutoChangeLog-pr-10111.yml new file mode 100644 index 0000000000..0a80f01eef --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10111.yml @@ -0,0 +1,4 @@ +author: "kappa-sama" +delete-after: True +changes: + - bugfix: "you can now strip people while aggrograbbing or higher" diff --git a/html/changelogs/AutoChangeLog-pr-10115.yml b/html/changelogs/AutoChangeLog-pr-10115.yml new file mode 100644 index 0000000000..f7a60e4911 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10115.yml @@ -0,0 +1,5 @@ +author: "Useroth" +delete-after: True +changes: + - rscadd: "New lavaland ruin: Pulsating tumor" + - rscadd: "New class of lavaland mobs, a bit weaker than megafauna but still stronger than most of what you normally see" diff --git a/html/changelogs/AutoChangeLog-pr-10116.yml b/html/changelogs/AutoChangeLog-pr-10116.yml new file mode 100644 index 0000000000..70c1b4590d --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10116.yml @@ -0,0 +1,4 @@ +author: "Putnam" +delete-after: True +changes: + - bugfix: "acute hepatic pharmacokinesis now works if you already have relevant genitals" diff --git a/html/changelogs/AutoChangeLog-pr-10121.yml b/html/changelogs/AutoChangeLog-pr-10121.yml new file mode 100644 index 0000000000..38a91fcc01 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10121.yml @@ -0,0 +1,4 @@ +author: "kappa-sama" +delete-after: True +changes: + - rscadd: "plasmafist to wizard" diff --git a/html/changelogs/AutoChangeLog-pr-10122.yml b/html/changelogs/AutoChangeLog-pr-10122.yml new file mode 100644 index 0000000000..4d503dc2be --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10122.yml @@ -0,0 +1,4 @@ +author: "kevinz000" +delete-after: True +changes: + - rscdel: "conveyors can only stack items on tiles to 150 now." diff --git a/html/changelogs/AutoChangeLog-pr-10137.yml b/html/changelogs/AutoChangeLog-pr-10137.yml new file mode 100644 index 0000000000..63678d4425 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10137.yml @@ -0,0 +1,4 @@ +author: "KeRSedChaplain" +delete-after: True +changes: + - soundadd: "Extends the file \"deltakalaxon.ogg\" to a 38 second .ogg." diff --git a/html/changelogs/AutoChangeLog-pr-10140.yml b/html/changelogs/AutoChangeLog-pr-10140.yml new file mode 100644 index 0000000000..d5e33fa02d --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10140.yml @@ -0,0 +1,4 @@ +author: "Putnam3145" +delete-after: True +changes: + - bugfix: "Dynamic has a (totally unused for any relevant purpose) roundstart report now." diff --git a/html/changelogs/AutoChangeLog-pr-10143.yml b/html/changelogs/AutoChangeLog-pr-10143.yml new file mode 100644 index 0000000000..45dfe225c4 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10143.yml @@ -0,0 +1,4 @@ +author: "Putnam3145" +delete-after: True +changes: + - bugfix: "Dynamic from-ghost antags no longer double dip on threat refunds when the mode fails due to not enough applications." diff --git a/html/changelogs/AutoChangeLog-pr-10144.yml b/html/changelogs/AutoChangeLog-pr-10144.yml new file mode 100644 index 0000000000..6b175b149b --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10144.yml @@ -0,0 +1,4 @@ +author: "Linzolle" +delete-after: True +changes: + - bugfix: "officer's sabre now properly makes the unsheating and resheating noise" diff --git a/html/changelogs/AutoChangeLog-pr-10150.yml b/html/changelogs/AutoChangeLog-pr-10150.yml new file mode 100644 index 0000000000..d2a2b79ff0 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10150.yml @@ -0,0 +1,4 @@ +author: "Trilbyspaceclone" +delete-after: True +changes: + - bugfix: "Wrong icon names, missing dog fashion with telegram hat" diff --git a/html/changelogs/AutoChangeLog-pr-10153.yml b/html/changelogs/AutoChangeLog-pr-10153.yml new file mode 100644 index 0000000000..7196583de9 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10153.yml @@ -0,0 +1,4 @@ +author: "Linzolle" +delete-after: True +changes: + - bugfix: "fireman failure has a different message depending on the circumstance" diff --git a/html/changelogs/AutoChangeLog-pr-10155.yml b/html/changelogs/AutoChangeLog-pr-10155.yml new file mode 100644 index 0000000000..d82ea5ed43 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10155.yml @@ -0,0 +1,4 @@ +author: "Seris02" +delete-after: True +changes: + - bugfix: "distance checks" diff --git a/html/changelogs/AutoChangeLog-pr-10156.yml b/html/changelogs/AutoChangeLog-pr-10156.yml new file mode 100644 index 0000000000..ec08642b3f --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10156.yml @@ -0,0 +1,5 @@ +author: "Ghommie" +delete-after: True +changes: + - bugfix: "Fixed character setup preview bodyparts not displaying correctly most of times." + - bugfix: "Fixed character appearance preview displaying the mannequin in job attire instead of undergarments." diff --git a/html/changelogs/AutoChangeLog-pr-10157.yml b/html/changelogs/AutoChangeLog-pr-10157.yml new file mode 100644 index 0000000000..6ec495b7fc --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10157.yml @@ -0,0 +1,5 @@ +author: "ShizCalev" +delete-after: True +changes: + - bugfix: "Fixed floodlights not turning off properly when they're underpowered." + - bugfix: "Fixed emitters not changing icons properly when they're underpowered." diff --git a/html/changelogs/AutoChangeLog-pr-10161.yml b/html/changelogs/AutoChangeLog-pr-10161.yml new file mode 100644 index 0000000000..7f09609b99 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10161.yml @@ -0,0 +1,4 @@ +author: "Ghommie" +delete-after: True +changes: + - bugfix: "Fixed raven's shuttle computer not being of the emergency shuttle type." diff --git a/html/changelogs/AutoChangeLog-pr-10163.yml b/html/changelogs/AutoChangeLog-pr-10163.yml new file mode 100644 index 0000000000..2818019ad7 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10163.yml @@ -0,0 +1,4 @@ +author: "Ghommie" +delete-after: True +changes: + - tweak: "Blood bank generators can now be anchored and unanchored now." diff --git a/html/changelogs/AutoChangeLog-pr-10166.yml b/html/changelogs/AutoChangeLog-pr-10166.yml new file mode 100644 index 0000000000..ba919010af --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10166.yml @@ -0,0 +1,4 @@ +author: "nemvar" +delete-after: True +changes: + - bugfix: "The brains of roundstart borgs no longer decay." diff --git a/html/changelogs/AutoChangeLog-pr-10171.yml b/html/changelogs/AutoChangeLog-pr-10171.yml new file mode 100644 index 0000000000..0138d7a381 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10171.yml @@ -0,0 +1,4 @@ +author: "r4d6" +delete-after: True +changes: + - rscdel: "Removed a AM Shielding from the crate" diff --git a/html/changelogs/AutoChangeLog-pr-10172.yml b/html/changelogs/AutoChangeLog-pr-10172.yml new file mode 100644 index 0000000000..ce76e04d13 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10172.yml @@ -0,0 +1,11 @@ +author: "Mickyan, nemvar, RaveRadbury, AnturK, SpaceManiac" +delete-after: True +changes: + - bugfix: "Certain incompatible quirks can no longer be taken together." + - bugfix: "If an admin sends a ghost back to the lobby, they can now choose a different set of quirks." + - spellcheck: "the quirk menu went through some minor formatting changes." + - bugfix: "Podcloning now lets you keep your quirks." + - rscadd: "Quirks have flavor text in medical records." + - spellcheck: "All quirk medical records refer to \"Patient\", removing a few instances of \"Subject\"." + - tweak: "Quirks no longer apply to off-station roundstart antagonists." + - code_imp: "Mood quirks are now only processed by the quirk holders" diff --git a/html/changelogs/AutoChangeLog-pr-10180.yml b/html/changelogs/AutoChangeLog-pr-10180.yml new file mode 100644 index 0000000000..0a22910940 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10180.yml @@ -0,0 +1,5 @@ +author: "Ghommie" +delete-after: True +changes: + - bugfix: "Fixes a ghostchat eavesdropping exploit concerning VR." + - bugfix: "Fixes VR deaths being broadcasted in deadchat." diff --git a/html/changelogs/AutoChangeLog-pr-10181.yml b/html/changelogs/AutoChangeLog-pr-10181.yml new file mode 100644 index 0000000000..bb00ab1012 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10181.yml @@ -0,0 +1,4 @@ +author: "Ghommie" +delete-after: True +changes: + - bugfix: "Fixed a few pill bottle issues with the ChemMaster." diff --git a/html/changelogs/AutoChangeLog-pr-10195.yml b/html/changelogs/AutoChangeLog-pr-10195.yml new file mode 100644 index 0000000000..be8ed0ce76 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10195.yml @@ -0,0 +1,5 @@ +author: "Ghommie" +delete-after: True +changes: + - bugfix: "Fixes a few negative quirks not being properly removed when deleted." + - tweak: "Phobia and mute quirks are no longer cheesed by brain surgery grade healing or medicines." diff --git a/html/changelogs/AutoChangeLog-pr-10197.yml b/html/changelogs/AutoChangeLog-pr-10197.yml new file mode 100644 index 0000000000..05545c066c --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10197.yml @@ -0,0 +1,4 @@ +author: "Ghommie" +delete-after: True +changes: + - bugfix: "Fixed double-flavour (and bland custom) ice creams." diff --git a/html/changelogs/AutoChangeLog-pr-10198.yml b/html/changelogs/AutoChangeLog-pr-10198.yml new file mode 100644 index 0000000000..3414258d84 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10198.yml @@ -0,0 +1,4 @@ +author: "AnturK" +delete-after: True +changes: + - bugfix: "Fixed ranged syndicate mobs stormtrooper training." diff --git a/html/changelogs/AutoChangeLog-pr-10203.yml b/html/changelogs/AutoChangeLog-pr-10203.yml new file mode 100644 index 0000000000..7cb4df817b --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10203.yml @@ -0,0 +1,5 @@ +author: "DeltaFire15" +delete-after: True +changes: + - tweak: "changed mecha internals access for some special mechs." + - tweak: "no more mech maintenance access for engineers." diff --git a/html/changelogs/AutoChangeLog-pr-10216.yml b/html/changelogs/AutoChangeLog-pr-10216.yml new file mode 100644 index 0000000000..69daa62448 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10216.yml @@ -0,0 +1,8 @@ +author: "Nervere and subject217, Militaires, py01, nemvar" +delete-after: True +changes: + - balance: "The cook's CQC now only works when in the kitchen or the kitchen backroom." + - spellcheck: "corrected CQC help instructions" + - bugfix: "CQC and Sleeping Carp are properly logged." + - tweak: "CQC can passively grab targets when not on grab intent. Passive grabs do not count towards combos for CQC or Sleeping carp." + - code_imp: "Martial Art and NOGUN cleanup." diff --git a/html/changelogs/AutoChangeLog-pr-10225.yml b/html/changelogs/AutoChangeLog-pr-10225.yml new file mode 100644 index 0000000000..964cc68223 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10225.yml @@ -0,0 +1,5 @@ +author: "BlueWildrose" +delete-after: True +changes: + - bugfix: "Fixed stargazers being unable to link to themselves if mindshielded or if holding psionic shielding devices (tinfoil hats) when the species is set." + - bugfix: "Fixes non-roundstart slimes being unable to wag their tail." diff --git a/html/changelogs/AutoChangeLog-pr-10227.yml b/html/changelogs/AutoChangeLog-pr-10227.yml new file mode 100644 index 0000000000..5d9446d045 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10227.yml @@ -0,0 +1,4 @@ +author: "Putnam3145" +delete-after: True +changes: + - bugfix: "whoops broke quirks" diff --git a/html/changelogs/AutoChangeLog-pr-10228.yml b/html/changelogs/AutoChangeLog-pr-10228.yml new file mode 100644 index 0000000000..ccc386bbcb --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-10228.yml @@ -0,0 +1,4 @@ +author: "Putnam3145" +delete-after: True +changes: + - bugfix: "quirks work" diff --git a/html/changelogs/AutoChangeLog-pr-9736.yml b/html/changelogs/AutoChangeLog-pr-9736.yml new file mode 100644 index 0000000000..768474b8ef --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-9736.yml @@ -0,0 +1,4 @@ +author: "kevinz000" +delete-after: True +changes: + - code_imp: "Antag rep proc is now easier to read and supports returning a list." diff --git a/html/changelogs/AutoChangeLog-pr-9894.yml b/html/changelogs/AutoChangeLog-pr-9894.yml new file mode 100644 index 0000000000..6e4b5c09d0 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-9894.yml @@ -0,0 +1,5 @@ +author: "Putnam3145" +delete-after: True +changes: + - balance: "Bomb armor now acts like other armor types." + - balance: "Devastation-level explosions on armorless people no longer destroys everything in their bags." diff --git a/html/changelogs/AutoChangeLog-pr-9970.yml b/html/changelogs/AutoChangeLog-pr-9970.yml new file mode 100644 index 0000000000..ebeaa326e7 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-9970.yml @@ -0,0 +1,4 @@ +author: "Ghommie" +delete-after: True +changes: + - rscdel: "Removed literally atrocious polka dotted accessories. They were even more atrocious than the yellow horrible tie." diff --git a/html/changelogs/AutoChangeLog-pr-9971.yml b/html/changelogs/AutoChangeLog-pr-9971.yml new file mode 100644 index 0000000000..951b9dfc81 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-9971.yml @@ -0,0 +1,8 @@ +author: "Xantholne" +delete-after: True +changes: + - rscadd: "Santa Hats to Loadout and Clothesmate" + - rscadd: "Christmas Wintercoats to Loadout and Clothesmate" + - rscadd: "Christmas male and female uniforms to loadout and Clothesmate" + - rscadd: "Red, Green, and Traditional Santa boots to loadout and Clothesmate" + - rscadd: "Christmas Socks, Red candycane socks, Green candycane socks to sock selection" diff --git a/html/changelogs/AutoChangeLog-pr-9983.yml b/html/changelogs/AutoChangeLog-pr-9983.yml new file mode 100644 index 0000000000..b8cd4f0f5b --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-9983.yml @@ -0,0 +1,6 @@ +author: "Ghommie, Skogol" +delete-after: True +changes: + - refactor: "refactored altclick interaction to allow alt-click interactable objects to parent call without forcing the turf contents stat menu open." + - tweak: "Alt clicking will no longer show turf contents for items inside bags etc." + - tweak: "Alt clicking the source of your turf contents stat menu will now close said menu." diff --git a/icons/effects/96x96.dmi b/icons/effects/96x96.dmi index a9f20ae8a8..c0b5ef5b40 100644 Binary files a/icons/effects/96x96.dmi and b/icons/effects/96x96.dmi differ diff --git a/icons/misc/language.dmi b/icons/misc/language.dmi index dcd10a51b7..6f6f2546d5 100644 Binary files a/icons/misc/language.dmi and b/icons/misc/language.dmi differ diff --git a/icons/misc/mark_icons.dmi b/icons/misc/mark_icons.dmi new file mode 100644 index 0000000000..1d8472752b Binary files /dev/null and b/icons/misc/mark_icons.dmi differ diff --git a/icons/mob/accessories.dmi b/icons/mob/accessories.dmi index 33964645ee..cda7cca404 100644 Binary files a/icons/mob/accessories.dmi and b/icons/mob/accessories.dmi differ diff --git a/icons/mob/actions/actions_elites.dmi b/icons/mob/actions/actions_elites.dmi new file mode 100644 index 0000000000..335261b0f6 Binary files /dev/null and b/icons/mob/actions/actions_elites.dmi differ diff --git a/icons/mob/actions/actions_spells.dmi b/icons/mob/actions/actions_spells.dmi index 30927d3608..072bfc8fe3 100644 Binary files a/icons/mob/actions/actions_spells.dmi and b/icons/mob/actions/actions_spells.dmi differ diff --git a/icons/mob/actions/bloodsucker.dmi b/icons/mob/actions/bloodsucker.dmi new file mode 100644 index 0000000000..c08509dbbd Binary files /dev/null and b/icons/mob/actions/bloodsucker.dmi differ diff --git a/icons/mob/feet.dmi b/icons/mob/feet.dmi index c57a7cc112..2188952740 100644 Binary files a/icons/mob/feet.dmi and b/icons/mob/feet.dmi differ diff --git a/icons/mob/feet_digi.dmi b/icons/mob/feet_digi.dmi index a8e6c49423..e0a9cd462d 100644 Binary files a/icons/mob/feet_digi.dmi and b/icons/mob/feet_digi.dmi differ diff --git a/icons/mob/head.dmi b/icons/mob/head.dmi index d27e02a4f2..f5951e032c 100644 Binary files a/icons/mob/head.dmi and b/icons/mob/head.dmi differ diff --git a/icons/mob/hud.dmi b/icons/mob/hud.dmi index d2b67f4314..8ea155256c 100644 Binary files a/icons/mob/hud.dmi and b/icons/mob/hud.dmi differ diff --git a/icons/mob/inhands/weapons/guns_lefthand.dmi b/icons/mob/inhands/weapons/guns_lefthand.dmi index 8978d17237..ea185c5ad7 100644 Binary files a/icons/mob/inhands/weapons/guns_lefthand.dmi and b/icons/mob/inhands/weapons/guns_lefthand.dmi differ diff --git a/icons/mob/inhands/weapons/guns_righthand.dmi b/icons/mob/inhands/weapons/guns_righthand.dmi index 3f8a876d43..1d380e34bc 100644 Binary files a/icons/mob/inhands/weapons/guns_righthand.dmi and b/icons/mob/inhands/weapons/guns_righthand.dmi differ diff --git a/icons/mob/inhands/weapons/melee_lefthand.dmi b/icons/mob/inhands/weapons/melee_lefthand.dmi index 092881ee01..fc82db43ae 100644 Binary files a/icons/mob/inhands/weapons/melee_lefthand.dmi and b/icons/mob/inhands/weapons/melee_lefthand.dmi differ diff --git a/icons/mob/inhands/weapons/melee_righthand.dmi b/icons/mob/inhands/weapons/melee_righthand.dmi index ede430390c..a93a4e38d5 100644 Binary files a/icons/mob/inhands/weapons/melee_righthand.dmi and b/icons/mob/inhands/weapons/melee_righthand.dmi differ diff --git a/icons/mob/lavaland/lavaland_elites.dmi b/icons/mob/lavaland/lavaland_elites.dmi new file mode 100644 index 0000000000..69032735d9 Binary files /dev/null and b/icons/mob/lavaland/lavaland_elites.dmi differ diff --git a/icons/mob/neck.dmi b/icons/mob/neck.dmi index 5eb270d23f..de59a136d9 100644 Binary files a/icons/mob/neck.dmi and b/icons/mob/neck.dmi differ diff --git a/icons/mob/screen_elite.dmi b/icons/mob/screen_elite.dmi new file mode 100644 index 0000000000..f407fb79e4 Binary files /dev/null and b/icons/mob/screen_elite.dmi differ diff --git a/icons/mob/suit.dmi b/icons/mob/suit.dmi index 61168860d8..5b84dad999 100644 Binary files a/icons/mob/suit.dmi and b/icons/mob/suit.dmi differ diff --git a/icons/mob/underwear.dmi b/icons/mob/underwear.dmi index ee93b27f9f..b29570cdb7 100644 Binary files a/icons/mob/underwear.dmi and b/icons/mob/underwear.dmi differ diff --git a/icons/mob/uniform.dmi b/icons/mob/uniform.dmi index 1141c4b8c9..d7514c8688 100644 Binary files a/icons/mob/uniform.dmi and b/icons/mob/uniform.dmi differ diff --git a/icons/obj/clothing/accessories.dmi b/icons/obj/clothing/accessories.dmi index eb019bc44b..c62a88c829 100644 Binary files a/icons/obj/clothing/accessories.dmi and b/icons/obj/clothing/accessories.dmi differ diff --git a/icons/obj/clothing/hats.dmi b/icons/obj/clothing/hats.dmi index c6b261d9a2..b3cc31fc6d 100644 Binary files a/icons/obj/clothing/hats.dmi and b/icons/obj/clothing/hats.dmi differ diff --git a/icons/obj/clothing/shoes.dmi b/icons/obj/clothing/shoes.dmi index e3a9f6d84e..728d244b5f 100644 Binary files a/icons/obj/clothing/shoes.dmi and b/icons/obj/clothing/shoes.dmi differ diff --git a/icons/obj/clothing/uniforms.dmi b/icons/obj/clothing/uniforms.dmi index a980fd4177..058af32b1e 100644 Binary files a/icons/obj/clothing/uniforms.dmi and b/icons/obj/clothing/uniforms.dmi differ diff --git a/icons/obj/guns/projectile.dmi b/icons/obj/guns/projectile.dmi index 6d5365f7fa..24ec5797d4 100644 Binary files a/icons/obj/guns/projectile.dmi and b/icons/obj/guns/projectile.dmi differ diff --git a/icons/obj/hydroponics/equipment.dmi b/icons/obj/hydroponics/equipment.dmi index dd4d1e1f93..37adf54711 100644 Binary files a/icons/obj/hydroponics/equipment.dmi and b/icons/obj/hydroponics/equipment.dmi differ diff --git a/icons/obj/hydroponics/growing.dmi b/icons/obj/hydroponics/growing.dmi index 45e73c9281..469b1e1aff 100644 Binary files a/icons/obj/hydroponics/growing.dmi and b/icons/obj/hydroponics/growing.dmi differ diff --git a/icons/obj/hydroponics/harvest.dmi b/icons/obj/hydroponics/harvest.dmi index 9d4eefc3bb..a57719fb3a 100644 Binary files a/icons/obj/hydroponics/harvest.dmi and b/icons/obj/hydroponics/harvest.dmi differ diff --git a/icons/obj/hydroponics/seeds.dmi b/icons/obj/hydroponics/seeds.dmi index d8fcaa6258..8695d03b3c 100644 Binary files a/icons/obj/hydroponics/seeds.dmi and b/icons/obj/hydroponics/seeds.dmi differ diff --git a/icons/obj/items_and_weapons.dmi b/icons/obj/items_and_weapons.dmi index e6e0b940a8..e3540a782b 100644 Binary files a/icons/obj/items_and_weapons.dmi and b/icons/obj/items_and_weapons.dmi differ diff --git a/icons/obj/lavaland/artefacts.dmi b/icons/obj/lavaland/artefacts.dmi index 7f11ba29d4..7ae1ed5a0e 100644 Binary files a/icons/obj/lavaland/artefacts.dmi and b/icons/obj/lavaland/artefacts.dmi differ diff --git a/icons/obj/lavaland/elite_trophies.dmi b/icons/obj/lavaland/elite_trophies.dmi new file mode 100644 index 0000000000..d194c93853 Binary files /dev/null and b/icons/obj/lavaland/elite_trophies.dmi differ diff --git a/icons/obj/lavaland/legionnaire_bonfire.dmi b/icons/obj/lavaland/legionnaire_bonfire.dmi new file mode 100644 index 0000000000..aed00ed001 Binary files /dev/null and b/icons/obj/lavaland/legionnaire_bonfire.dmi differ diff --git a/icons/obj/lavaland/tumor.dmi b/icons/obj/lavaland/tumor.dmi new file mode 100644 index 0000000000..a41224c823 Binary files /dev/null and b/icons/obj/lavaland/tumor.dmi differ diff --git a/icons/obj/library.dmi b/icons/obj/library.dmi index 20e0f5f73c..45181b7c8d 100644 Binary files a/icons/obj/library.dmi and b/icons/obj/library.dmi differ diff --git a/icons/obj/projectiles.dmi b/icons/obj/projectiles.dmi index d1c625c18e..a434994846 100644 Binary files a/icons/obj/projectiles.dmi and b/icons/obj/projectiles.dmi differ diff --git a/icons/obj/stack_objects.dmi b/icons/obj/stack_objects.dmi index 7cb212384a..a80dc92b9a 100644 Binary files a/icons/obj/stack_objects.dmi and b/icons/obj/stack_objects.dmi differ diff --git a/icons/obj/stake.dmi b/icons/obj/stake.dmi new file mode 100644 index 0000000000..dfc1dc08bf Binary files /dev/null and b/icons/obj/stake.dmi differ diff --git a/icons/obj/vamp_obj.dmi b/icons/obj/vamp_obj.dmi new file mode 100644 index 0000000000..89718773dd Binary files /dev/null and b/icons/obj/vamp_obj.dmi differ diff --git a/modular_citadel/code/modules/arousal/organs/breasts.dm b/modular_citadel/code/modules/arousal/organs/breasts.dm index a82d02703d..f4ba95d830 100644 --- a/modular_citadel/code/modules/arousal/organs/breasts.dm +++ b/modular_citadel/code/modules/arousal/organs/breasts.dm @@ -119,7 +119,7 @@ shape = D.features["breasts_shape"] fluid_id = D.features["breasts_fluid"] if(!D.features["breasts_producing"]) - DISABLE_BITFIELD(genital_flags, GENITAL_FUID_PRODUCTION) + DISABLE_BITFIELD(genital_flags, GENITAL_FUID_PRODUCTION|CAN_CLIMAX_WITH|CAN_MASTURBATE_WITH) if(!isnum(size)) cached_size = breast_values[size] else diff --git a/modular_citadel/code/modules/arousal/toys/dildos.dm b/modular_citadel/code/modules/arousal/toys/dildos.dm index 22ed7cdf2f..964c9964ad 100644 --- a/modular_citadel/code/modules/arousal/toys/dildos.dm +++ b/modular_citadel/code/modules/arousal/toys/dildos.dm @@ -37,15 +37,11 @@ name = "[sizeword][dildo_shape] [can_customize ? "custom " : ""][dildo_type]" /obj/item/dildo/AltClick(mob/living/user) - if(QDELETED(src)) - return - if(!isliving(user)) - return - if(isAI(user)) - return - if(user.stat > 0)//unconscious or dead + . = ..() + if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK)) return customize(user) + return TRUE /obj/item/dildo/proc/customize(mob/living/user) if(!can_customize) diff --git a/modular_citadel/code/modules/client/loadout/head.dm b/modular_citadel/code/modules/client/loadout/head.dm index 2d65f093bb..3e7eb39823 100644 --- a/modular_citadel/code/modules/client/loadout/head.dm +++ b/modular_citadel/code/modules/client/loadout/head.dm @@ -91,4 +91,14 @@ category = SLOT_HEAD path = /obj/item/clothing/head/caphat/formal/fedcover/sec restricted_desc = "Engineering, Security, and Cargo" - restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Warden","Detective","Security Officer","Head of Security","Cargo Technician", "Shaft Miner", "Quartermaster") \ No newline at end of file + restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Warden","Detective","Security Officer","Head of Security","Cargo Technician", "Shaft Miner", "Quartermaster") + +/datum/gear/santahatr + name = "Red Santa Hat" + category = SLOT_HEAD + path = /obj/item/clothing/head/christmashat + +/datum/gear/santahatg + name = "Green Santa Hat" + category = SLOT_HEAD + path = /obj/item/clothing/head/christmashatg \ No newline at end of file diff --git a/modular_citadel/code/modules/client/loadout/shoes.dm b/modular_citadel/code/modules/client/loadout/shoes.dm index 6688529c73..7b3bee6638 100644 --- a/modular_citadel/code/modules/client/loadout/shoes.dm +++ b/modular_citadel/code/modules/client/loadout/shoes.dm @@ -56,4 +56,19 @@ /datum/gear/bluecuffs name = "Blue leg wraps" category = SLOT_SHOES - path= /obj/item/clothing/shoes/wraps/blue \ No newline at end of file + path= /obj/item/clothing/shoes/wraps/blue + +/datum/gear/christmasbootsr + name = "Red Christmas Boots" + category = SLOT_SHOES + path= /obj/item/clothing/shoes/winterboots/christmasbootsr + +/datum/gear/christmasbootsg + name = "Green Christmas Boots" + category = SLOT_SHOES + path= /obj/item/clothing/shoes/winterboots/christmasbootsg + +/datum/gear/santaboots + name = "Santa Boots" + category = SLOT_SHOES + path= /obj/item/clothing/shoes/winterboots/santaboots \ No newline at end of file diff --git a/modular_citadel/code/modules/client/loadout/suit.dm b/modular_citadel/code/modules/client/loadout/suit.dm index 72387b622a..b8f3bd1cc1 100644 --- a/modular_citadel/code/modules/client/loadout/suit.dm +++ b/modular_citadel/code/modules/client/loadout/suit.dm @@ -204,3 +204,18 @@ path = /obj/item/clothing/suit/storage/fluff/modernfedcoat/eng restricted_desc = "Engineering and Cargo" restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Cargo Technician", "Shaft Miner", "Quartermaster") + +/datum/gear/christmascoatr + name = "Red Christmas Coat" + category = SLOT_WEAR_SUIT + path = /obj/item/clothing/suit/hooded/wintercoat/christmascoatr + +/datum/gear/christmascoatg + name = "Green Christmas Coat" + category = SLOT_WEAR_SUIT + path = /obj/item/clothing/suit/hooded/wintercoat/christmascoatg + +/datum/gear/christmascoatrg + name = "Red and Green Christmas Coat" + category = SLOT_WEAR_SUIT + path = /obj/item/clothing/suit/hooded/wintercoat/christmascoatrg \ No newline at end of file diff --git a/modular_citadel/code/modules/client/loadout/uniform.dm b/modular_citadel/code/modules/client/loadout/uniform.dm index 232e37d921..72709069eb 100644 --- a/modular_citadel/code/modules/client/loadout/uniform.dm +++ b/modular_citadel/code/modules/client/loadout/uniform.dm @@ -330,6 +330,27 @@ category = SLOT_W_UNIFORM path = /obj/item/clothing/under/gear_harness +//Christmas +/datum/gear/christmasmaler + name = "Red Masculine Christmas Suit" + category = SLOT_W_UNIFORM + path = /obj/item/clothing/under/christmas/christmasmaler + +/datum/gear/christmasmaleg + name = "Green Masculine Christmas Suit" + category = SLOT_W_UNIFORM + path = /obj/item/clothing/under/christmas/christmasmaleg + +/datum/gear/christmasfemaler + name = "Red Feminine Christmas Suit" + category = SLOT_W_UNIFORM + path = /obj/item/clothing/under/christmas/christmasfemaler + +/datum/gear/christmasfemaleg + name = "Green Feminine Christmas Suit" + category = SLOT_W_UNIFORM + path = /obj/item/clothing/under/christmas/christmasfemaleg + /datum/gear/pinkstripper name = "Pink stripper outfit" category = SLOT_W_UNIFORM diff --git a/modular_citadel/code/modules/clothing/clothing.dm b/modular_citadel/code/modules/clothing/clothing.dm index b23e805f92..843b7a84c9 100644 --- a/modular_citadel/code/modules/clothing/clothing.dm +++ b/modular_citadel/code/modules/clothing/clothing.dm @@ -56,7 +56,7 @@ add_overlay(tertiary_overlay) /obj/item/clothing/AltClick(mob/living/user) - ..() + . = ..() if(hasprimary | hassecondary | hastertiary) var/choice = input(user,"polychromic thread options", "Clothing Recolor") as null|anything in list("[hasprimary ? "Primary Color" : ""]", "[hassecondary ? "Secondary Color" : ""]", "[hastertiary ? "Tertiary Color" : ""]") //generates a list depending on the enabled overlays switch(choice) //Lets the list's options actually lead to something @@ -78,6 +78,7 @@ tertiary_color = sanitize_hexcolor(tertiary_color_input, desired_format=6, include_crunch=1) update_icon() user.regenerate_icons() + return TRUE /obj/item/clothing/examine(mob/user) . = ..() diff --git a/modular_citadel/code/modules/mob/living/silicon/robot/dogborg archive.dm b/modular_citadel/code/modules/mob/living/silicon/robot/dogborg archive.dm index 8b5c0b0af0..368abc6335 100644 --- a/modular_citadel/code/modules/mob/living/silicon/robot/dogborg archive.dm +++ b/modular_citadel/code/modules/mob/living/silicon/robot/dogborg archive.dm @@ -57,20 +57,4 @@ modules += new /obj/item/healthanalyzer(src) modules += new /obj/item/analyzer/nose(src) emag = new /obj/item/dogborg/pounce(src) - fix_modules() - -/obj/item/robot_module/medihound - name = "MediHound module" - -/obj/item/robot_module/medihound/New() - ..() - modules += new /obj/item/dogborg/jaws/small(src) - modules += new /obj/item/storage/bag/borgdelivery(src) - modules += new /obj/item/analyzer/nose(src) - modules += new /obj/item/soap/tongue(src) - modules += new /obj/item/healthanalyzer(src) - modules += new /obj/item/dogborg/sleeper(src) - modules += new /obj/item/twohanded/shockpaddles/hound(src) - modules += new /obj/item/sensor_device(src) - emag = new /obj/item/dogborg/pounce(src) fix_modules() \ No newline at end of file diff --git a/modular_citadel/code/modules/mob/living/silicon/robot/dogborg_equipment.dm b/modular_citadel/code/modules/mob/living/silicon/robot/dogborg_equipment.dm index 17def26f1d..a1466f58d4 100644 --- a/modular_citadel/code/modules/mob/living/silicon/robot/dogborg_equipment.dm +++ b/modular_citadel/code/modules/mob/living/silicon/robot/dogborg_equipment.dm @@ -162,9 +162,6 @@ SLEEPER CODE IS IN game/objects/items/devices/dogborg_sleeper.dm ! to_chat(user, "[GLOB.meta_gas_names[id]]: [round(gas_concentration*100, 0.01)] %") to_chat(user, "Temperature: [round(environment.temperature-T0C)] °C") -/obj/item/analyzer/nose/AltClick(mob/user) //Barometer output for measuring when the next storm happens - . = ..() - /obj/item/analyzer/nose/afterattack(atom/target, mob/user, proximity) . = ..() if(!proximity) @@ -356,15 +353,6 @@ SLEEPER CODE IS IN game/objects/items/devices/dogborg_sleeper.dm ! icon = 'icons/mob/dogborg.dmi' icon_state= "kibble" -//Defibs - -/obj/item/twohanded/shockpaddles/cyborg/hound - name = "Paws of Life" - desc = "MediHound specific shock paws." - icon = 'icons/mob/dogborg.dmi' - icon_state = "defibpaddles0" - item_state = "defibpaddles0" - // Pounce stuff for K-9 /obj/item/dogborg/pounce diff --git a/modular_citadel/code/modules/mob/living/silicon/robot/robot_movement.dm b/modular_citadel/code/modules/mob/living/silicon/robot/robot_movement.dm index 28327cde0e..59630186d1 100644 --- a/modular_citadel/code/modules/mob/living/silicon/robot/robot_movement.dm +++ b/modular_citadel/code/modules/mob/living/silicon/robot/robot_movement.dm @@ -14,7 +14,7 @@ . += speed /mob/living/silicon/robot/proc/togglesprint(shutdown = FALSE) //Basically a copypaste of the proc from /mob/living/carbon/human - if(!shutdown && (!cell || cell.charge < 25)) + if(!shutdown && (!cell || cell.charge < 25) || !cansprint) return FALSE sprinting = shutdown ? FALSE : !sprinting if(!resting && canmove) diff --git a/modular_citadel/code/modules/projectiles/guns/ballistic/rifles.dm b/modular_citadel/code/modules/projectiles/guns/ballistic/rifles.dm index e40ccfe6ea..024669757a 100644 --- a/modular_citadel/code/modules/projectiles/guns/ballistic/rifles.dm +++ b/modular_citadel/code/modules/projectiles/guns/ballistic/rifles.dm @@ -207,8 +207,10 @@ var/mob/M = loc M.update_inv_hands() /obj/item/gun/ballistic/automatic/AM4B/AltClick(mob/living/user) + . = ..() if(!in_range(src, user)) //Basic checks to prevent abuse return + . = TRUE if(user.incapacitated() || !istype(user)) to_chat(user, "You can't do that right now!") return @@ -217,6 +219,7 @@ if(body_color_input) body_color = sanitize_hexcolor(body_color_input, desired_format=6, include_crunch=1) update_icon() + /obj/item/gun/ballistic/automatic/AM4B/examine(mob/user) . = ..() . += "Alt-click to recolor it." diff --git a/modular_citadel/code/modules/projectiles/guns/energy/energy_gun.dm b/modular_citadel/code/modules/projectiles/guns/energy/energy_gun.dm index 5fc75fa414..89e086687a 100644 --- a/modular_citadel/code/modules/projectiles/guns/energy/energy_gun.dm +++ b/modular_citadel/code/modules/projectiles/guns/energy/energy_gun.dm @@ -30,8 +30,10 @@ obj/item/gun/energy/e_gun/cx/update_icon() M.update_inv_hands() obj/item/gun/energy/e_gun/cx/AltClick(mob/living/user) + . = ..() if(!in_range(src, user)) //Basic checks to prevent abuse return + . = TRUE if(user.incapacitated() || !istype(user)) to_chat(user, "You can't do that right now!") return diff --git a/modular_citadel/code/modules/projectiles/guns/pumpenergy.dm b/modular_citadel/code/modules/projectiles/guns/pumpenergy.dm index 86404c4fe4..735c0890af 100644 --- a/modular_citadel/code/modules/projectiles/guns/pumpenergy.dm +++ b/modular_citadel/code/modules/projectiles/guns/pumpenergy.dm @@ -83,15 +83,17 @@ return 1 /obj/item/gun/energy/pumpaction/AltClick(mob/living/user) //for changing firing modes since attackself is already used for pumping + . = ..() if(!in_range(src, user)) //Basic checks to prevent abuse return - if(user.incapacitated() || !istype(user)) - to_chat(user, "You can't do that right now!") - return if(ammo_type.len > 1) - select_fire(user) - update_icon() + if(user.incapacitated() || !istype(user)) + to_chat(user, "You can't do that right now!") + else + select_fire(user) + update_icon() + return TRUE /obj/item/gun/energy/pumpaction/examine(mob/user) //so people don't ask HOW TO CHANGE FIRING MODE . = ..() diff --git a/modular_citadel/code/modules/reagents/chemistry/reagents/enlargement.dm b/modular_citadel/code/modules/reagents/chemistry/reagents/enlargement.dm index ee90f985c6..7d358b4149 100644 --- a/modular_citadel/code/modules/reagents/chemistry/reagents/enlargement.dm +++ b/modular_citadel/code/modules/reagents/chemistry/reagents/enlargement.dm @@ -56,18 +56,17 @@ var/mob/living/carbon/human/H = M var/obj/item/organ/genital/breasts/B = M.getorganslot(ORGAN_SLOT_BREASTS) + //If they have Acute hepatic pharmacokinesis, then route processing though liver. + if(HAS_TRAIT(H, TRAIT_PHARMA) || !H.canbearoused) + var/obj/item/organ/liver/L = H.getorganslot(ORGAN_SLOT_LIVER) + if(L) + L.swelling += 0.05 + else + H.adjustToxLoss(1) + return..() + //otherwise proceed as normal if(!B) //If they don't have breasts, give them breasts. - //If they have Acute hepatic pharmacokinesis, then route processing though liver. - if(HAS_TRAIT(H, TRAIT_PHARMA) || !H.canbearoused) - var/obj/item/organ/liver/L = H.getorganslot(ORGAN_SLOT_LIVER) - if(L) - L.swelling += 0.05 - else - H.adjustToxLoss(1) - return..() - - //otherwise proceed as normal B = new if(H.dna.species.use_skintones && H.dna.features["genitals_use_skintone"]) B.color = skintone2hex(H.skin_tone) @@ -222,18 +221,17 @@ return ..() var/mob/living/carbon/human/H = M var/obj/item/organ/genital/penis/P = H.getorganslot(ORGAN_SLOT_PENIS) + //If they have Acute hepatic pharmacokinesis, then route processing though liver. + if(HAS_TRAIT(H, TRAIT_PHARMA) || !H.canbearoused) + var/obj/item/organ/liver/L = H.getorganslot(ORGAN_SLOT_LIVER) + if(L) + L.swelling += 0.05 + else + H.adjustToxLoss(1) + return ..() + //otherwise proceed as normal if(!P)//They do have a preponderance for escapism, or so I've heard. - //If they have Acute hepatic pharmacokinesis, then route processing though liver. - if(HAS_TRAIT(H, TRAIT_PHARMA) || !H.canbearoused) - var/obj/item/organ/liver/L = H.getorganslot(ORGAN_SLOT_LIVER) - if(L) - L.swelling += 0.05 - else - H.adjustToxLoss(1) - return ..() - - //otherwise proceed as normal P = new P.length = 1 to_chat(H, "Your groin feels warm, as you feel a newly forming bulge down below.") diff --git a/sound/bloodsucker/BloodsuckerAlert.ogg b/sound/bloodsucker/BloodsuckerAlert.ogg new file mode 100644 index 0000000000..686f15bb71 Binary files /dev/null and b/sound/bloodsucker/BloodsuckerAlert.ogg differ diff --git a/sound/bloodsucker/coffin_close.ogg b/sound/bloodsucker/coffin_close.ogg new file mode 100644 index 0000000000..600809f782 Binary files /dev/null and b/sound/bloodsucker/coffin_close.ogg differ diff --git a/sound/bloodsucker/coffin_open.ogg b/sound/bloodsucker/coffin_open.ogg new file mode 100644 index 0000000000..38ca5c1f87 Binary files /dev/null and b/sound/bloodsucker/coffin_open.ogg differ diff --git a/sound/effects/neovgre_exploding.ogg b/sound/effects/neovgre_exploding.ogg new file mode 100644 index 0000000000..6ca2db05c3 Binary files /dev/null and b/sound/effects/neovgre_exploding.ogg differ diff --git a/sound/magic/curse.ogg b/sound/magic/curse.ogg new file mode 100644 index 0000000000..bda610416d Binary files /dev/null and b/sound/magic/curse.ogg differ diff --git a/sound/misc/deltakalaxon.ogg b/sound/misc/deltakalaxon.ogg index cc489526dd..7a9522c1ff 100644 Binary files a/sound/misc/deltakalaxon.ogg and b/sound/misc/deltakalaxon.ogg differ diff --git a/sound/weapons/nuclear_fist.ogg b/sound/weapons/nuclear_fist.ogg new file mode 100644 index 0000000000..2b2682dc33 Binary files /dev/null and b/sound/weapons/nuclear_fist.ogg differ diff --git a/tgstation.dme b/tgstation.dme index 4c7e29739f..e13584b7e3 100755 --- a/tgstation.dme +++ b/tgstation.dme @@ -201,6 +201,7 @@ #include "code\_onclick\hud\hud.dm" #include "code\_onclick\hud\hud_cit.dm" #include "code\_onclick\hud\human.dm" +#include "code\_onclick\hud\lavaland_elite.dm" #include "code\_onclick\hud\monkey.dm" #include "code\_onclick\hud\movable_screen_objects.dm" #include "code\_onclick\hud\parallax.dm" @@ -555,6 +556,8 @@ #include "code\game\gamemodes\game_mode.dm" #include "code\game\gamemodes\objective.dm" #include "code\game\gamemodes\objective_items.dm" +#include "code\game\gamemodes\bloodsucker\bloodsucker.dm" +#include "code\game\gamemodes\bloodsucker\hunter.dm" #include "code\game\gamemodes\brother\traitor_bro.dm" #include "code\game\gamemodes\changeling\changeling.dm" #include "code\game\gamemodes\changeling\traitor_chan.dm" @@ -1214,6 +1217,33 @@ #include "code\modules\antagonists\blob\blob\blobs\resource.dm" #include "code\modules\antagonists\blob\blob\blobs\shield.dm" #include "code\modules\antagonists\blood_contract\blood_contract.dm" +#include "code\modules\antagonists\bloodsucker\bloodsucker_flaws.dm" +#include "code\modules\antagonists\bloodsucker\bloodsucker_integration.dm" +#include "code\modules\antagonists\bloodsucker\bloodsucker_life.dm" +#include "code\modules\antagonists\bloodsucker\bloodsucker_objectives.dm" +#include "code\modules\antagonists\bloodsucker\bloodsucker_powers.dm" +#include "code\modules\antagonists\bloodsucker\bloodsucker_sunlight.dm" +#include "code\modules\antagonists\bloodsucker\bloodsucker_ui.dm" +#include "code\modules\antagonists\bloodsucker\datum_bloodsucker.dm" +#include "code\modules\antagonists\bloodsucker\datum_hunter.dm" +#include "code\modules\antagonists\bloodsucker\datum_vassal.dm" +#include "code\modules\antagonists\bloodsucker\items\bloodsucker_organs.dm" +#include "code\modules\antagonists\bloodsucker\items\bloodsucker_stake.dm" +#include "code\modules\antagonists\bloodsucker\objects\bloodsucker_coffin.dm" +#include "code\modules\antagonists\bloodsucker\objects\bloodsucker_crypt.dm" +#include "code\modules\antagonists\bloodsucker\objects\bloodsucker_lair.dm" +#include "code\modules\antagonists\bloodsucker\powers\bs_brawn.dm" +#include "code\modules\antagonists\bloodsucker\powers\bs_cloak.dm" +#include "code\modules\antagonists\bloodsucker\powers\bs_feed.dm" +#include "code\modules\antagonists\bloodsucker\powers\bs_fortitude.dm" +#include "code\modules\antagonists\bloodsucker\powers\bs_gohome.dm" +#include "code\modules\antagonists\bloodsucker\powers\bs_haste.dm" +#include "code\modules\antagonists\bloodsucker\powers\bs_lunge.dm" +#include "code\modules\antagonists\bloodsucker\powers\bs_masquerade.dm" +#include "code\modules\antagonists\bloodsucker\powers\bs_mesmerize.dm" +#include "code\modules\antagonists\bloodsucker\powers\bs_trespass.dm" +#include "code\modules\antagonists\bloodsucker\powers\bs_veil.dm" +#include "code\modules\antagonists\bloodsucker\powers\v_recuperate.dm" #include "code\modules\antagonists\brainwashing\brainwashing.dm" #include "code\modules\antagonists\brother\brother.dm" #include "code\modules\antagonists\changeling\cellular_emporium.dm" @@ -1920,6 +1950,7 @@ #include "code\modules\language\ratvarian.dm" #include "code\modules\language\slime.dm" #include "code\modules\language\swarmer.dm" +#include "code\modules\language\vampiric.dm" #include "code\modules\language\xenocommon.dm" #include "code\modules\library\lib_codex_gigas.dm" #include "code\modules\library\lib_items.dm" @@ -2309,6 +2340,11 @@ #include "code\modules\mob\living\simple_animal\hostile\mining_mobs\hivelord.dm" #include "code\modules\mob\living\simple_animal\hostile\mining_mobs\mining_mobs.dm" #include "code\modules\mob\living\simple_animal\hostile\mining_mobs\necropolis_tendril.dm" +#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\elites\elite.dm" +#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\elites\goliath_broodmother.dm" +#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\elites\herald.dm" +#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\elites\legionnaire.dm" +#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\elites\pandora.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\bat.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\clown.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\frog.dm" diff --git a/tools/json_verifier.py b/tools/json_verifier.py index 04c8670705..dbba811205 100644 --- a/tools/json_verifier.py +++ b/tools/json_verifier.py @@ -4,16 +4,17 @@ import json if len(sys.argv) <= 1: exit(1) -files = filter(len, sys.argv[1].split('\n')) -msg = [] -for file in files: +status = 0 + +for file in sys.argv[1:]: with open(file, encoding="ISO-8859-1") as f: try: json.load(f) except ValueError as exception: - msg.append("JSON synxtax error on file: {}".format(file)) - msg.append(str(exception)) -if msg: - print("\n".join(msg)) - exit(1) -exit(0) + print("JSON error in {}".format(file)) + print(exception) + status = 1 + else: + print("Valid {}".format(file)) + +exit(status) diff --git a/tools/travis/before_build_tools.sh b/tools/travis/before_build_tools.sh deleted file mode 100755 index 2329de82b2..0000000000 --- a/tools/travis/before_build_tools.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -e - -if [ "$BUILD_TOOLS" = true ]; then - cd tgui && source ~/.nvm/nvm.sh && npm install && cd .. -fi; - diff --git a/tools/travis/build_bsql.sh b/tools/travis/build_bsql.sh new file mode 100755 index 0000000000..e2b281efc6 --- /dev/null +++ b/tools/travis/build_bsql.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +source dependencies.sh + +mkdir -p BSQL +cd BSQL +git init +git remote add origin https://github.com/tgstation/BSQL +git fetch --depth 1 origin $BSQL_VERSION +git checkout FETCH_HEAD + +mkdir -p artifacts +cd artifacts +export CXX=g++-7 +# The -D will be unnecessary past BSQL v1.4.0.0 +cmake .. -DMARIA_LIBRARY=/usr/lib/i386-linux-gnu/libmariadb.so +make + +mkdir -p ~/.byond/bin +ln -s $PWD/src/BSQL/libBSQL.so ../../libBSQL.so diff --git a/tools/travis/build_byond.sh b/tools/travis/build_byond.sh deleted file mode 100755 index 3a7e00df29..0000000000 --- a/tools/travis/build_byond.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/bin/bash - -#nb: must be bash to support shopt globstar -set -e -shopt -s globstar - -if [ "$BUILD_TOOLS" = false ]; then - if grep -E '^\".+\" = \(.+\)' _maps/**/*.dmm; then - echo "Non-TGM formatted map detected. Please convert it using Map Merger!" - exit 1 - fi; - if grep -P '^\ttag = \"icon' _maps/**/*.dmm; then - echo "tag vars from icon state generation detected in maps, please remove them." - exit 1 - fi; - if grep 'step_[xy]' _maps/**/*.dmm; then - echo "step_x/step_y variables detected in maps, please remove them." - exit 1 - fi; - if grep 'pixel_[xy] = 0' _maps/**/*.dmm; then - echo "pixel_x/pixel_y = 0 variables detected in maps, please review to ensure they are not dirty varedits." - fi; - if grep -P '\td[1-2] =' _maps/**/*.dmm; then - echo "d1/d2 cable variables detected in maps, please remove them." - exit 1 - fi; - if grep '^/area/.+[\{]' _maps/**/*.dmm; then - echo "Vareditted /area path use detected in maps, please replace with proper paths." - exit 1 - fi; - if grep '\W\/turf\s*[,\){]' _maps/**/*.dmm; then - echo "base /turf path use detected in maps, please replace with proper paths." - exit 1 - fi; - if grep '^/*var/' code/**/*.dm; then - echo "Unmanaged global var use detected in code, please use the helpers." - exit 1 - fi; - if grep -i 'centcomm' code/**/*.dm; then - echo "Misspelling(s) of CENTCOM detected in code, please remove the extra M(s)." - exit 1 - fi; - if grep -i 'centcomm' _maps/**/*.dmm; then - echo "Misspelling(s) of CENTCOM detected in maps, please remove the extra M(s)." - exit 1 - fi; - - source $HOME/BYOND/byond/bin/byondsetup - if [ "$BUILD_TESTING" = true ]; then - tools/travis/dm.sh -DTRAVISBUILDING -DTRAVISTESTING -DALL_MAPS tgstation.dme - else - tools/travis/dm.sh -DTRAVISBUILDING tgstation.dme - - tools/deploy.sh travis_test - mkdir travis_test/config - - #test config - cp tools/travis/travis_config.txt travis_test/config/config.txt - - # get libmariadb, cache it so limmex doesn't get angery - if [ -f $HOME/libmariadb ]; then - #travis likes to interpret the cache command as it being a file for some reason - rm $HOME/libmariadb - fi - mkdir -p $HOME/libmariadb - if [ ! -f $HOME/libmariadb/libmariadb.so ]; then - wget http://www.byond.com/download/db/mariadb_client-2.0.0-linux.tgz - tar -xvf mariadb_client-2.0.0-linux.tgz - mv mariadb_client-2.0.0-linux/libmariadb.so $HOME/libmariadb/libmariadb.so - rm -rf mariadb_client-2.0.0-linux.tgz mariadb_client-2.0.0-linux - fi - - cd travis_test - ln -s $HOME/libmariadb/libmariadb.so libmariadb.so - DreamDaemon tgstation.dmb -close -trusted -verbose -params "test-run&log-directory=travis" - cd .. - cat travis_test/data/logs/travis/clean_run.lk - - fi; -fi; diff --git a/tools/travis/build_dependencies.sh b/tools/travis/build_dependencies.sh deleted file mode 100755 index 2c24bdbaaf..0000000000 --- a/tools/travis/build_dependencies.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -set -e - -source dependencies.sh - -#ensure the Dockerfile version matches the dependencies.sh version -line=$(head -n 1 Dockerfile) -if [[ $line != *"$BYOND_MAJOR.$BYOND_MINOR"* ]]; then - echo "Dockerfile BYOND version in FROM command does not match dependencies.sh (Or it's not on line 1)!" - exit 1 -fi - -if [ $BUILD_TOOLS = false ] && [ $BUILD_TESTING = false ]; then - curl https://sh.rustup.rs -sSf | sh -s -- -y --default-host i686-unknown-linux-gnu - source ~/.profile - - mkdir rust-g - cd rust-g - git init - git remote add origin https://github.com/tgstation/rust-g - git fetch --depth 1 origin $RUST_G_VERSION - git checkout FETCH_HEAD - cargo build --release - cmp target/rust_g.dm ../code/__DEFINES/rust_g.dm - - mkdir -p ~/.byond/bin - ln -s $PWD/target/release/librust_g.so ~/.byond/bin/rust_g - - mkdir -p ../BSQL/artifacts - cd ../BSQL - git init - git remote add origin https://github.com/tgstation/BSQL - git fetch --depth 1 origin $BSQL_VERSION - git checkout FETCH_HEAD - - if [ -f "$HOME/MariaDB/libmariadb.so.2" ] && [ -f "$HOME/MariaDB/libmariadb.so" ] && [ -d "$HOME/MariaDB/include" ]; - then - echo "Using cached MariaDB library." - else - echo "Setting up MariaDB." - rm -rf "$HOME/MariaDB" - mkdir -p "$HOME/MariaDB" - mkdir -p "$HOME/MariaDB/include" - cp /usr/lib/i386-linux-gnu/libmariadb.so.2 $HOME/MariaDB/ - ln -s $HOME/MariaDB/libmariadb.so.2 $HOME/MariaDB/libmariadb.so - cp -r /usr/include/mariadb $HOME/MariaDB/include/ - fi - - cd artifacts - export CXX=g++-7 - cmake .. -DMARIA_INCLUDE_DIR=$HOME/MariaDB/include - make - mv src/BSQL/libBSQL.so ../../ -fi diff --git a/tools/travis/build_tgui.sh b/tools/travis/build_tgui.sh new file mode 100755 index 0000000000..0141f3e117 --- /dev/null +++ b/tools/travis/build_tgui.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -euo pipefail + +cd tgui +source ~/.nvm/nvm.sh +npm ci +node node_modules/gulp/bin/gulp.js --min diff --git a/tools/travis/build_tools.sh b/tools/travis/build_tools.sh deleted file mode 100755 index eaccd64c8c..0000000000 --- a/tools/travis/build_tools.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -#must also be bash for the md5sum commands -set -e - -if [ "$BUILD_TOOLS" = true ]; -then - md5sum -c - <<< "49bc6b1b9ed56c83cceb6674bd97cb34 *html/changelogs/example.yml"; - (cd tgui && source ~/.nvm/nvm.sh && npm ci && node node_modules/gulp/bin/gulp.js --min) - phpenv global 5.6 - php -l tools/WebhookProcessor/github_webhook_processor.php; - php -l tools/TGUICompiler.php; - echo "Checking for JSON errors"; - find . -name "*.json" -not -path "./tgui/node_modules/*" | xargs -0 python3 ./tools/json_verifier.py; - python3 tools/ss13_genchangelog.py html/changelog.html html/changelogs; -fi; diff --git a/tools/travis/check_changelogs.sh b/tools/travis/check_changelogs.sh new file mode 100755 index 0000000000..e33c0e6df3 --- /dev/null +++ b/tools/travis/check_changelogs.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euo pipefail + +md5sum -c - <<< "49bc6b1b9ed56c83cceb6674bd97cb34 *html/changelogs/example.yml" +python3 tools/ss13_genchangelog.py html/changelog.html html/changelogs diff --git a/tools/travis/check_grep.sh b/tools/travis/check_grep.sh new file mode 100755 index 0000000000..2117b570eb --- /dev/null +++ b/tools/travis/check_grep.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -euo pipefail + +#nb: must be bash to support shopt globstar +shopt -s globstar + +st=0 + +if grep -El '^\".+\" = \(.+\)' _maps/**/*.dmm; then + echo "Non-TGM formatted map detected. Please convert it using Map Merger!" + st=1 +fi; +if grep -P '^\ttag = \"icon' _maps/**/*.dmm; then + echo "tag vars from icon state generation detected in maps, please remove them." + st=1 +fi; +if grep 'step_[xy]' _maps/**/*.dmm; then + echo "step_x/step_y variables detected in maps, please remove them." + st=1 +fi; +if grep 'pixel_[xy] = 0' _maps/**/*.dmm; then + echo "pixel_x/pixel_y = 0 variables detected in maps, please review to ensure they are not dirty varedits." +fi; +if grep -P '\td[1-2] =' _maps/**/*.dmm; then + echo "d1/d2 cable variables detected in maps, please remove them." + st=1 +fi; +if grep '^/area/.+[\{]' _maps/**/*.dmm; then + echo "Vareditted /area path use detected in maps, please replace with proper paths." + st=1 +fi; +if grep '\W\/turf\s*[,\){]' _maps/**/*.dmm; then + echo "base /turf path use detected in maps, please replace with proper paths." + st=1 +fi; +if grep '^/*var/' code/**/*.dm; then + echo "Unmanaged global var use detected in code, please use the helpers." + st=1 +fi; +if grep -i 'centcomm' code/**/*.dm; then + echo "Misspelling(s) of CENTCOM detected in code, please remove the extra M(s)." + st=1 +fi; +if grep -i 'centcomm' _maps/**/*.dmm; then + echo "Misspelling(s) of CENTCOM detected in maps, please remove the extra M(s)." + st=1 +fi; + +exit $st diff --git a/tools/travis/install_build_tools.sh b/tools/travis/install_build_tools.sh index 0ea2a605ab..224687f088 100755 --- a/tools/travis/install_build_tools.sh +++ b/tools/travis/install_build_tools.sh @@ -1,11 +1,12 @@ #!/bin/bash -set -e +set -euo pipefail source dependencies.sh -if [ "$BUILD_TOOLS" = true ]; then - source ~/.nvm/nvm.sh - nvm install $NODE_VERSION - pip3 install --user PyYaml - pip3 install --user beautifulsoup4 -fi; +source ~/.nvm/nvm.sh +nvm install $NODE_VERSION + +pip3 install --user PyYaml +pip3 install --user beautifulsoup4 + +phpenv global $PHP_VERSION diff --git a/tools/travis/before_build_byond.sh b/tools/travis/install_byond.sh similarity index 67% rename from tools/travis/before_build_byond.sh rename to tools/travis/install_byond.sh index 55afb07ffe..4a688755d3 100755 --- a/tools/travis/before_build_byond.sh +++ b/tools/travis/install_byond.sh @@ -1,15 +1,5 @@ #!/bin/bash -set -e - -#If this is the build tools step, we do not bother to install/build byond -if [ "$BUILD_TOOLS" = true ]; then - exit 0 -fi - -echo "Combining maps for building" -if [ $BUILD_TESTING = true ]; then - python tools/travis/template_dm_generator.py -fi +set -euo pipefail source dependencies.sh diff --git a/tools/travis/install_dreamchecker.sh b/tools/travis/install_dreamchecker.sh new file mode 100755 index 0000000000..4423e5aafb --- /dev/null +++ b/tools/travis/install_dreamchecker.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -euo pipefail + +source dependencies.sh + +wget -O ~/dreamchecker "https://github.com/SpaceManiac/SpacemanDMM/releases/download/$SPACEMAN_DMM_VERSION/dreamchecker" +chmod +x ~/dreamchecker +~/dreamchecker --version diff --git a/tools/travis/install_libmariadb.sh b/tools/travis/install_libmariadb.sh new file mode 100755 index 0000000000..d0ce4adc14 --- /dev/null +++ b/tools/travis/install_libmariadb.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euo pipefail + +# get libmariadb, cache it so limmex doesn't get angery +if [ -f $HOME/libmariadb ]; then + #travis likes to interpret the cache command as it being a file for some reason + rm $HOME/libmariadb +fi +mkdir -p $HOME/libmariadb +if [ ! -f $HOME/libmariadb/libmariadb.so ]; then + wget http://www.byond.com/download/db/mariadb_client-2.0.0-linux.tgz + tar -xvf mariadb_client-2.0.0-linux.tgz + mv mariadb_client-2.0.0-linux/libmariadb.so $HOME/libmariadb/libmariadb.so + rm -rf mariadb_client-2.0.0-linux.tgz mariadb_client-2.0.0-linux +fi diff --git a/tools/travis/install_rust_g.sh b/tools/travis/install_rust_g.sh new file mode 100755 index 0000000000..227034af66 --- /dev/null +++ b/tools/travis/install_rust_g.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +source dependencies.sh + +mkdir -p ~/.byond/bin +wget -O ~/.byond/bin/rust_g "https://github.com/tgstation/rust-g/releases/download/$RUST_G_VERSION/librust_g.so" +chmod +x ~/.byond/bin/rust_g diff --git a/tools/travis/run_server.sh b/tools/travis/run_server.sh new file mode 100755 index 0000000000..975e8f6884 --- /dev/null +++ b/tools/travis/run_server.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail + +tools/deploy.sh travis_test +mkdir travis_test/config + +#test config +cp tools/travis/travis_config.txt travis_test/config/config.txt + +cd travis_test +ln -s $HOME/libmariadb/libmariadb.so libmariadb.so +DreamDaemon tgstation.dmb -close -trusted -verbose -params "test-run&log-directory=travis" +cd .. +cat travis_test/data/logs/travis/clean_run.lk diff --git a/tools/travis/template_dm_generator.py b/tools/travis/template_dm_generator.py index 89e1d2647d..970fba7db4 100755 --- a/tools/travis/template_dm_generator.py +++ b/tools/travis/template_dm_generator.py @@ -1,7 +1,6 @@ #!/usr/bin/env python import os -import os.path import sys folders = ["_maps/RandomRuins", "_maps/RandomZLevels", "_maps/shuttles",