From c3a6058fc4d659d7dbca9737f15d87b125e664eb Mon Sep 17 00:00:00 2001 From: nikothedude <59709059+nikothedude@users.noreply.github.com> Date: Sun, 30 Jun 2024 03:25:45 -0400 Subject: [PATCH] [SEMI-MODULAR] [NOVA PORT] Ports the serpentine taur "rework" from nova with a few tweaks (#28492) [SEMI-MODULAR] Taur rework Pt. 1: Serpentine taurs (#1953) * lets just push * agh * last commiut of the night * some more * taking a break * ugh * waaghhh * better * uhhhh * arfgggg * cleanup * womp * some more * a lil tweak * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa * forgot this was nullable * atomize * dwa' * why isnt it publishing * womp * i am stupid * DOUBLE DEFINTIION * suffix, to avoid confusion * Apply suggestions from code review * manual review * Apply suggestions from code review * manual review, save for one thing ill do in a min * this is as much as i understand * rename package, will do the same in the other pr soon enough * some tweaks * Apply suggestions from code review * reviews * Apply suggestions from code review * dirs! * this was way easier than i thought itd be * i missed these * how did i miss this? * you get this, but not hardened soles * yargh * ok they convinced me * wahhhh * hytthg * fml * WHY IS THIS HERE * le typo * inversion * it works now * saNity * a b it of work * Apply suggestions from code review * womp --------- Co-authored-by: GoldenAlpharex <58045821+GoldenAlpharex@users.noreply.github.com> Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com> --- .../mob/living/carbon/human/_species.dm | 2 +- .../sprite_accessories/taur_types.dm | 4 + .../modules/surgery/organs/taur_body.dm | 3 + .../code/additionalemotes/turf_emote.dm | 8 +- .../emotes/code/additionalemotes/turf_list.dm | 27 +- .../modules/taur_mechanics/code/constrict.dm | 650 ++++++++++++++++++ .../modules/taur_mechanics/code/mod.dm | 18 + .../taur_mechanics/code/preferences.dm | 20 + .../taur_mechanics/code/serpentine_taur.dm | 47 ++ .../modules/taur_mechanics/code/species.dm | 3 + .../taur_mechanics/sprites/ability.dmi | Bin 0 -> 592 bytes .../modules/taur_mechanics/sprites/tail.dmi | Bin 0 -> 1276 bytes tgstation.dme | 5 + .../skyrat/species_features.tsx | 7 + 14 files changed, 761 insertions(+), 33 deletions(-) create mode 100644 modular_skyrat/modules/taur_mechanics/code/constrict.dm create mode 100644 modular_skyrat/modules/taur_mechanics/code/mod.dm create mode 100644 modular_skyrat/modules/taur_mechanics/code/preferences.dm create mode 100644 modular_skyrat/modules/taur_mechanics/code/serpentine_taur.dm create mode 100644 modular_skyrat/modules/taur_mechanics/code/species.dm create mode 100644 modular_skyrat/modules/taur_mechanics/sprites/ability.dmi create mode 100644 modular_skyrat/modules/taur_mechanics/sprites/tail.dmi diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm index 2419557c5e1..b3839d41fe4 100644 --- a/code/modules/mob/living/carbon/human/_species.dm +++ b/code/modules/mob/living/carbon/human/_species.dm @@ -841,7 +841,7 @@ GLOBAL_LIST_EMPTY(features_by_species) H.adjustBruteLoss(0.5 * seconds_per_tick) /datum/species/proc/can_equip(obj/item/I, slot, disable_warning, mob/living/carbon/human/H, bypass_equip_delay_self = FALSE, ignore_equipped = FALSE, indirect_action = FALSE) - if(no_equip_flags & slot) + if(no_equip_flags & slot && !(I.is_mod_shell_component() && (modsuit_slot_exceptions & slot))) // SKYRAT EDIT ADDITION - ORIGINAL: if(no_equip_flags & slot) if(!I.species_exception || !is_type_in_list(src, I.species_exception)) return FALSE diff --git a/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/taur_types.dm b/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/taur_types.dm index 270bf1f0d96..069eaf6f698 100644 --- a/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/taur_types.dm +++ b/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/taur_types.dm @@ -32,6 +32,10 @@ var/alt_taur_mode = NONE /datum/sprite_accessory/taur/is_hidden(mob/living/carbon/human/target) + var/obj/item/organ/external/taur_body/taur_body = target.get_organ_slot(ORGAN_SLOT_EXTERNAL_TAUR) + if (taur_body?.hide_self) + return TRUE + var/obj/item/clothing/suit/worn_suit = target.wear_suit if (istype(worn_suit)) if((worn_suit.flags_inv & HIDETAIL) && !worn_suit.gets_cropped_on_taurs) diff --git a/modular_skyrat/modules/customization/modules/surgery/organs/taur_body.dm b/modular_skyrat/modules/customization/modules/surgery/organs/taur_body.dm index 6ef47016ad0..5a3d01fdef7 100644 --- a/modular_skyrat/modules/customization/modules/surgery/organs/taur_body.dm +++ b/modular_skyrat/modules/customization/modules/surgery/organs/taur_body.dm @@ -20,6 +20,9 @@ /// The mob's old left leg. Used if the person switches to this organ and then back, so they don't just, have no legs anymore. Can be null. var/obj/item/bodypart/leg/right/old_left_leg = null + /// If true, our sprite accessory will not render. + var/hide_self + /obj/item/organ/external/taur_body/horselike /obj/item/organ/external/taur_body/horselike/synth diff --git a/modular_skyrat/modules/emotes/code/additionalemotes/turf_emote.dm b/modular_skyrat/modules/emotes/code/additionalemotes/turf_emote.dm index 07c7ddc7b3f..5c3fc8426db 100644 --- a/modular_skyrat/modules/emotes/code/additionalemotes/turf_emote.dm +++ b/modular_skyrat/modules/emotes/code/additionalemotes/turf_emote.dm @@ -65,7 +65,6 @@ var/taur_mode = human_user.get_taur_mode() if(taur_mode & STYLE_TAUR_SNAKE) user.allowed_turfs -= list("pawprint", "hoofprint", "clawprint") - user.allowed_turfs += "constrict" //clothing var/obj/item/shoes = user.get_item_by_slot(ITEM_SLOT_FEET) @@ -113,16 +112,13 @@ user.owned_turf.color = human_user.dna.features["mcolor"] - var/list/body_part = list("tails", "constrict") + var/list/body_part = list("tails") if(current_turf in body_part) //These turfs can be a body part and need color/size applied var/key = null var/list/tail_emotes = list("tails") if(current_turf in tail_emotes) key = "tail" - var/list/taur_emotes = list("constrict") - if(current_turf in taur_emotes) - key = "taur" //coloring var/list/finished_list = list() @@ -175,8 +171,6 @@ return FALSE if(isspaceturf(get_turf(user))) return FALSE - if(user.buckled) - return FALSE else return TRUE diff --git a/modular_skyrat/modules/emotes/code/additionalemotes/turf_list.dm b/modular_skyrat/modules/emotes/code/additionalemotes/turf_list.dm index 606f0fc45d6..e753c3d3401 100644 --- a/modular_skyrat/modules/emotes/code/additionalemotes/turf_list.dm +++ b/modular_skyrat/modules/emotes/code/additionalemotes/turf_list.dm @@ -108,34 +108,11 @@ icon = 'modular_skyrat/master_files/icons/effects/turf_effects_64.dmi' icon_state = "tails" pixel_x = -16 //correcting the offset for 64 - var/mutable_appearance/overlay = mutable_appearance( - 'modular_skyrat/master_files/icons/effects/turf_effects_64.dmi', - "tails_top", - EXTRA_ABOVE_MOB_LAYER, - src, - ABOVE_GAME_PLANE, - ) + var/mutable_appearance/overlay = mutable_appearance('modular_skyrat/master_files/icons/effects/turf_effects_64.dmi', "tails_top", EXTRA_ABOVE_MOB_LAYER, src) overlay.appearance_flags = TILE_BOUND|PIXEL_SCALE|KEEP_TOGETHER src.add_overlay(overlay) playsound(get_turf(src), 'sound/weapons/thudswoosh.ogg', 25, TRUE) - if("constrict") - name = "tail" - desc = "It's a scaly tail." - icon = 'modular_skyrat/master_files/icons/effects/turf_effects_64.dmi' - icon_state = "naga" - pixel_x = -16 - var/mutable_appearance/overlay = mutable_appearance( - 'modular_skyrat/master_files/icons/effects/turf_effects_64.dmi', - "naga_top", - EXTRA_ABOVE_MOB_LAYER, - src, - ABOVE_GAME_PLANE, - ) - overlay.appearance_flags = TILE_BOUND|PIXEL_SCALE|KEEP_TOGETHER - src.add_overlay(overlay) - playsound(get_turf(src), 'modular_skyrat/modules/emotes/sound/emotes/hiss.ogg', 25, TRUE) - //prints if("pawprint") name = "pawprint" @@ -190,7 +167,7 @@ return /obj/structure/mark_turf/proc/turf_check(mob/living/user) //This gets called when a player leaves their turf - var/list/no_trail = list("tail", "constrict") + var/list/no_trail = list("tail") var/list/long_trail = list("pawprint", "hoofprint", "clawprint", "footprint", "shoeprint") if(user.owned_turf.name in no_trail) diff --git a/modular_skyrat/modules/taur_mechanics/code/constrict.dm b/modular_skyrat/modules/taur_mechanics/code/constrict.dm new file mode 100644 index 00000000000..21ddf3baf19 --- /dev/null +++ b/modular_skyrat/modules/taur_mechanics/code/constrict.dm @@ -0,0 +1,650 @@ +/// When a mob is constricted, its pixel_x will be modified by this. Reverted on unconstriction. Modified by sprite scaling. +#define CONSTRICT_BASE_PIXEL_SHIFT 12 +/// The base chance a mob has to escape from a constriction. +#define CONSTRICT_ESCAPE_CHANCE 25 + +/datum/action/innate/constrict + name = "Constrict" + desc = "Left click to coil/uncoil your powerful tail around something, right click to begin crushing." + check_flags = AB_CHECK_LYING|AB_CHECK_CONSCIOUS|AB_CHECK_INCAPACITATED|AB_CHECK_PHASED + + button_icon = 'modular_skyrat/modules/taur_mechanics/sprites/ability.dmi' + button_icon_state = "constrict" + + ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_pickturf.dmi' + + click_action = TRUE + + /// The tail we use to constrict mobs with. Nullable, if inactive. + var/obj/structure/serpentine_tail/tail + /// The base time it takes for us to constrict a mob. + var/base_coil_delay = 3.25 SECONDS + +/datum/action/innate/constrict/Destroy() + qdel(tail) // we already listen for COMSIG_QDELETING on our tail, so it already sets it to null via the signal + return ..() + +/datum/action/innate/constrict/Trigger(trigger_flags) + if(!..()) + return FALSE + + if (trigger_flags & TRIGGER_SECONDARY_ACTION) + unset_ranged_ability(owner) + if (isnull(tail)) + owner.balloon_alert(owner, "coil tail first!") + return FALSE + tail.toggle_crushing() + return FALSE + return TRUE + +/datum/action/innate/constrict/do_ability(mob/living/caller, atom/clicked_on) + if (!isliving(clicked_on)) + if (tail) + qdel(tail) + return TRUE + + create_tail() + return TRUE + + var/mob/living/living_target = clicked_on + + if (living_target == caller) + return TRUE + + if (!can_coil_target(living_target)) + return TRUE + + caller.balloon_alert_to_viewers("starts coiling tail") + caller.visible_message(span_warning("[caller] starts coiling [caller.p_their()] tail around [living_target]..."), span_notice("You start coiling your tail around [living_target]..."), ignored_mobs = list(living_target)) + to_chat(living_target, span_userdanger("[caller] starts coiling [caller.p_their()] tail around you!")) + + owner.changeNext_move(base_coil_delay) // prevent interaction during this + unset_ranged_ability(owner) // because we sleep + var/result = do_after(caller, base_coil_delay, living_target, IGNORE_HELD_ITEM, extra_checks = CALLBACK(src, PROC_REF(can_coil_target), living_target)) + owner.changeNext_move(-base_coil_delay) + if (!result) + return TRUE + + do_constriction(living_target) + return TRUE + +/// Actually constricts the mob, by setting constricted to this mob and spawning a tail if needed. +/datum/action/innate/constrict/proc/do_constriction(mob/living/living_target) + owner.visible_message(span_boldwarning("[owner] coils [owner.p_their()] tail around [living_target]!"), span_notice("You coil your tail around [living_target]!"), ignored_mobs = list(living_target)) + to_chat(living_target, span_userdanger("[owner] coils [owner.p_their()] tail around you!")) + create_tail() + tail.set_constricted(living_target) + return TRUE + +/// Returns TRUE if the target can be constricted, FALSE otherwise. If silent is TRUE, sends no feedback messages. +/datum/action/innate/constrict/proc/can_coil_target(mob/living/target, silent = FALSE) + if (!owner.Adjacent(target)) + if (!silent) + owner.balloon_alert(owner, "too far!") + return FALSE + + if (target.buckled) + if (!silent) + owner.balloon_alert(owner, "unbuckle [target.p_them()] first!") + return FALSE + + if (owner.buckled) + if (!silent) + owner.balloon_alert(owner, "unbuckle yourself first!") + return FALSE + + return TRUE + +/// If we have no tail, sets our tail to a new tail instance. +/datum/action/innate/constrict/proc/create_tail() + RETURN_TYPE(/obj/structure/serpentine_tail) + + if (isnull(tail)) + set_tail(new /obj/structure/serpentine_tail(owner.loc, owner)) + return tail + +/// Setter proc for tail. Handles signals. +/datum/action/innate/constrict/proc/set_tail(obj/structure/serpentine_tail/new_tail) + if (tail) + UnregisterSignal(tail, COMSIG_QDELETING) + + tail = new_tail + + if (tail) + RegisterSignal(tail, COMSIG_QDELETING, PROC_REF(tail_qdeleting)) + + return tail + +/// Signal handler for COMSIG_QDELETING. Sets our tail to null. +/datum/action/innate/constrict/proc/tail_qdeleting(datum/signal_source) + SIGNAL_HANDLER + + set_tail(null) + +/obj/structure/serpentine_tail + name = "serpentine tail" + desc = "A scaley tail, currently coiled." + + icon = 'modular_skyrat/modules/taur_mechanics/sprites/tail.dmi' + icon_state = "naga" + pixel_x = -16 + + can_buckle = TRUE + buckle_lying = FALSE + layer = ABOVE_OBJ_LAYER + anchored = TRUE + density = FALSE + max_integrity = 60 + + /// The overlay for our coiled tail. + var/mutable_appearance/tail_overlay + + /// The mob we are originating from. + var/mob/living/carbon/human/owner + + /// The mob we are currently constricting, usually coincides with what we have buckled to us. Nullable. + var/mob/living/constricted + /// The pixel shift we have applied to constricted. Is null if constricted is null. Used to unapply it. + var/applied_x_shift + + /// If we're currently allowing constricted to be grabbed. Only briefly true, during set_constricted. + var/allowing_grab_on_constricted = FALSE + + /// Are we currently crushing constricted? + var/currently_crushing = FALSE + /// The amount of brute damage we will do per second to constricted if we are crushing. + var/brute_per_second = 2 + /// How likely are we, per second, to cause a blunt wound on constricted if we are crushing? + var/chance_to_cause_wound = 5 + + /// Chance, per second, of us causing losebreath for a constricted target we are crushing. + var/chance_to_losebreath = 55 + /// Assuming we proc chance_to_losebreath, this is the losebreath we will inflict on our target. + var/losebreath_to_cause = 2 // above 1 to allow potential suffocation + + /// If we try to do crush damage and total below 5 (the minimum wounding amount), we store it here for next time. + var/stored_damage = 0 + + /// Used for escaping the tail, separate from grab cooldowns. + COOLDOWN_DECLARE(escape_cooldown) + +/obj/structure/serpentine_tail/New(loc, mob/living/carbon/human/new_owner) + if (isnull(new_owner)) + qdel(src) // requires an owner, not stack tracing because it fails tests + return FALSE + + set_owner(new_owner) + + return ..() + +/obj/structure/serpentine_tail/Initialize(mapload) + . = ..() + + add_tail_overlay() + sync_sprite() + +/// If we have no tail overlay, creates a new one and sets it up. +/obj/structure/serpentine_tail/proc/add_tail_overlay() + RETURN_TYPE(/mutable_appearance) + + if (tail_overlay) + return tail_overlay // we already have it + + tail_overlay = mutable_appearance('modular_skyrat/modules/taur_mechanics/sprites/tail.dmi', "naga_top", ABOVE_MOB_LAYER + 0.01, src) + tail_overlay.appearance_flags = TILE_BOUND|PIXEL_SCALE|KEEP_TOGETHER + tail_overlay.setDir(owner.dir) + add_overlay(tail_overlay) + + return tail_overlay + +/obj/structure/serpentine_tail/Destroy() + INVOKE_ASYNC(src, PROC_REF(set_constricted), null) // safe - the only time it can potentially sleep is if we dont pass in null + var/mob/living/carbon/human/old_owner = owner + set_owner(null) + + old_owner?.update_mutant_bodyparts() + + tail_overlay = null + return ..() + +/// Since slimepeople are transparent, we have to match their alpha. What our alpha is set to when our owner is a slime. +#define SERPENTINE_TAIL_SLIME_ALPHA 130 + +/// Syncs our colors, size, sprite, etc. with owner. +/obj/structure/serpentine_tail/proc/sync_sprite() + //coloring + var/list/finished_list = list() + var/list/color_list = owner.dna.species.mutant_bodyparts["taur"][MUTANT_INDEX_COLOR_LIST] //identify color + var/datum/sprite_accessory/sprite_type = SSaccessories.sprite_accessories["taur"][owner.dna.species.mutant_bodyparts["taur"][MUTANT_INDEX_NAME]] //identify type + + switch(sprite_type.color_src) + if(USE_MATRIXED_COLORS) + finished_list += rgb2num("[color_list[1]]00") + finished_list += rgb2num("[color_list[2]]00") + finished_list += rgb2num("[color_list[3]]00") + if(USE_ONE_COLOR) + var/padded_string = "[color_list[1]]00" + finished_list += rgb2num(padded_string) + finished_list += rgb2num(padded_string) + finished_list += rgb2num(padded_string) + + finished_list += list(0, 0, 0, 255) + for(var/index in 1 to finished_list.len) + finished_list[index] /= 255 + + color = finished_list + if(isroundstartslime(owner) || isslimeperson(owner) || isjellyperson(owner)) // slimes are translucent + alpha = SERPENTINE_TAIL_SLIME_ALPHA + + var/scale_multiplier = get_scale_change_mult() + // If our owner is big and we scale, we need to move to the side so we remain aligned with them. + var/translate = ((scale_multiplier-1) * 32)/2 + transform = transform.Scale(scale_multiplier) + transform = transform.Translate(0, translate) // re-align ourselves + appearance_flags = PIXEL_SCALE + + sync_direction() + +#undef SERPENTINE_TAIL_SLIME_ALPHA + +/// Syncs our dir with our owner. Fails to sync if we have a constricted mob and we move in a way that would spin them around. +/obj/structure/serpentine_tail/proc/sync_direction() + SIGNAL_HANDLER + + if (dir == owner.dir) + return + + if (constricted) + /// Assoc list of stringified dir to dir. Controls what dir we can switch to from any given dir. + var/static/list/switchable_dirs = list( + "[NORTH]" = EAST, + "[EAST]" = NORTH, + "[WEST]" = SOUTH, + "[SOUTH]" = WEST + ) + // prevents snakes from spinning their victims around + var/switchable_dir = switchable_dirs["[dir]"] + if (switchable_dir != owner.dir) + return + + dir = owner.dir + tail_overlay.setDir(owner.dir) + +/// Returns the scale, compared to default, our owner has. +/obj/structure/serpentine_tail/proc/get_scale_change_mult() + return owner.dna.features["body_size"] / BODY_SIZE_NORMAL + +/// Returns the x shift for anything we constrict. +/obj/structure/serpentine_tail/proc/get_constriction_pixel_x_shift() + var/static/list/dirs_to_shift = list( // stringified since dirs are numbers + "[SOUTH]" = CONSTRICT_BASE_PIXEL_SHIFT, + "[WEST]" = CONSTRICT_BASE_PIXEL_SHIFT, + "[NORTH]" = -CONSTRICT_BASE_PIXEL_SHIFT, + "[EAST]" = -CONSTRICT_BASE_PIXEL_SHIFT, + ) + + return dirs_to_shift["[owner.dir]"] + +/obj/structure/serpentine_tail/process(seconds_per_tick) + stored_damage += (brute_per_second * seconds_per_tick) + if (stored_damage < WOUND_MINIMUM_DAMAGE) + return + var/losebreath = 0 + if (SPT_PROB(chance_to_losebreath, seconds_per_tick)) + losebreath += losebreath_to_cause + squeeze_constricted(stored_damage, losebreath, SPT_PROB(chance_to_cause_wound, seconds_per_tick)) + stored_damage = 0 + +/// The minimum wound bonus caused by a forced wound in squeeze_constricted. +#define CONSTRICTED_FORCE_WOUND_BONUS_MIN 40 +/// The maximum wound bonus caused by a forced wound in squeeze_constricted. +#define CONSTRICTED_FORCE_WOUND_BONUS_MAX 70 + +/** + * Attempts to squeeze constricted with ourselves, dealing blunt brute damage to them based on the damage arg. + * + * Arguments: + * * damage: Float - The numerical damage to apply, with MELEE armor flag and BLUNT wounding type. + * * force_wound: Boolean - If we should force a wound to be applied to constricted. + * + * Returns: + * * FALSE if we aborted trying to inflict damage, TRUE otherwise. + */ +/obj/structure/serpentine_tail/proc/squeeze_constricted(damage, losebreath_to_use, force_wound = FALSE) + if (!constricted) + return FALSE + + constricted.losebreath += losebreath_to_use + + if (damage <= 0) + return FALSE + + var/armor = constricted.run_armor_check(attack_flag = MELEE) + var/wound_bonus = 0 + if (force_wound) + wound_bonus += rand(CONSTRICTED_FORCE_WOUND_BONUS_MIN, CONSTRICTED_FORCE_WOUND_BONUS_MAX) + var/def_zone = null + if (iscarbon(constricted)) + var/mob/living/carbon/carbon_target = constricted + def_zone = pick(carbon_target.bodyparts) + constricted.apply_damage(stored_damage, BRUTE, def_zone = def_zone, blocked = armor, wound_bonus = wound_bonus) + owner.visible_message(span_warning("[owner] squeezes [constricted] with [owner.p_their()] tail!"), span_danger("You squeeze [constricted] with your tail!"), ignored_mobs = list(constricted)) + to_chat(constricted, span_warning("[owner] squeezes you with [owner.p_their()] tail!")) + return TRUE + +#undef CONSTRICTED_FORCE_WOUND_BONUS_MIN +#undef CONSTRICTED_FORCE_WOUND_BONUS_MAX + + +/// The damage dealt to a serpentine tail's owner apon its destruction. +#define SERPENTINE_TAIL_DESTRUCTION_OWNER_BRUTE_DAMAGE 30 +/// The chance that the damage dealt to a destroyed tail's owner goes to the right leg over the left leg. +#define SERPENTINE_TAIL_DESTRUCTION_R_LEG_CHANCE 50 + +/obj/structure/serpentine_tail/atom_destruction(damage_flag) + /// Assoc list of [damage_flag -> damage_type], e.g. ACID = BURN. + var/static/list/damage_flags_to_types = list( + ACID = BURN, + LASER = BURN, + ENERGY = BURN, + BIO = BURN, + FIRE = BURN, + CONSUME = BRUTE, + MELEE = BRUTE, + BULLET = BRUTE, + BOMB = BRUTE, + ) + + var/damage_type = damage_flags_to_types[damage_flag] + if (!damage_type) + damage_type = BRUTE + + var/obj/item/bodypart/def_zone = owner.get_bodypart(BODY_ZONE_L_LEG) + if (!def_zone || rand(SERPENTINE_TAIL_DESTRUCTION_R_LEG_CHANCE)) + def_zone = owner.get_bodypart(BODY_ZONE_R_LEG) + if (!def_zone) + def_zone = owner.get_bodypart(BODY_ZONE_CHEST) + + to_chat(owner, span_userdanger("You recall your tail as a sharp pain shoots through it!")) + owner.apply_damage(SERPENTINE_TAIL_DESTRUCTION_OWNER_BRUTE_DAMAGE, damage_type, def_zone) + + return ..() + +#undef SERPENTINE_TAIL_DESTRUCTION_OWNER_BRUTE_DAMAGE +#undef SERPENTINE_TAIL_DESTRUCTION_R_LEG_CHANCE + +/// Setter proc for constricted. Handles signals, pixel shifting, status effects, etc. +/obj/structure/serpentine_tail/proc/set_constricted(mob/living/target) + if (constricted == target) + return + + if (currently_crushing && !target) + stop_crushing() + + if (constricted) + unregister_constricted() + unapply_pixel_shift() + constricted.remove_status_effect(/datum/status_effect/constricted) + + constricted = target + + if (!constricted) + return + + register_constricted() + apply_pixel_shift() + constricted.apply_status_effect(/datum/status_effect/constricted) + + restrain_constricted() + +/// Applies our pixel shift to our constricted. Do not call if we have already applied our pixel shift. +/obj/structure/serpentine_tail/proc/apply_pixel_shift() + if (!constricted) + return FALSE + + if (!isnull(applied_x_shift)) // this isnt a proc to call willy nilly so this is fine + stack_trace("apply_pixel_shift called with a non-null applied_x_shift! value: [applied_x_shift]") + return FALSE + + var/pixel_shift = get_constriction_pixel_x_shift() + var/scale_change_mult = get_scale_change_mult() + var/final_shift = (pixel_shift * scale_change_mult) + + constricted.pixel_x += final_shift + applied_x_shift = final_shift + + return TRUE + +/// Unapplies our pixel shift to our constricted. Do not call if we have not applied our pixel shift. +/obj/structure/serpentine_tail/proc/unapply_pixel_shift() + if (!constricted) + return FALSE + + if (isnull(applied_x_shift)) + stack_trace("unapply_pixel_shift called with a null applied_x_shift!") + return FALSE + + constricted.pixel_x -= applied_x_shift + applied_x_shift = null + + return TRUE + +/// Registers signals to constricted. +/obj/structure/serpentine_tail/proc/register_constricted() + RegisterSignal(constricted, COMSIG_MOVABLE_MOVED, PROC_REF(constricted_moved)) + RegisterSignal(constricted, COMSIG_ATOM_EXAMINE, PROC_REF(constricted_examined)) + RegisterSignal(constricted, COMSIG_LIVING_TRY_PULL, PROC_REF(constricted_tried_pull)) + RegisterSignal(constricted, COMSIG_QDELETING, PROC_REF(constricted_qdeleting)) + +/// Unregisters signals to constricted. +/obj/structure/serpentine_tail/proc/unregister_constricted() + UnregisterSignal(constricted, list(COMSIG_MOVABLE_MOVED, COMSIG_ATOM_EXAMINE, COMSIG_LIVING_TRY_PULL, COMSIG_QDELETING)) + +/// Buckles constricted to ourselves, and migrates the current grab constricted may have on them. +/obj/structure/serpentine_tail/proc/restrain_constricted() + if (!constricted) + return FALSE + + if (constricted.buckled == src) // already restrained + return FALSE + + var/old_grab_state = owner.grab_state + constricted.forceMove(get_turf(src)) + buckle_mob(constricted) + + if (old_grab_state < GRAB_AGGRESSIVE) + return FALSE + + allowing_grab_on_constricted = TRUE + owner.grab(constricted) // can potentially sleep + owner.setGrabState(old_grab_state) + allowing_grab_on_constricted = FALSE + + return FALSE + +/// Toggle proc for crushing. See stop_crushing and start_crushing. +/obj/structure/serpentine_tail/proc/toggle_crushing() + if (!constricted) + owner.balloon_alert(owner, "not constricting anything!") + return FALSE + + if (currently_crushing) + stop_crushing() + else + start_crushing() + + return TRUE + +/// Setter proc for currently_crushing that handles processing and warnings. +/obj/structure/serpentine_tail/proc/start_crushing() + if (currently_crushing) + return FALSE + + currently_crushing = TRUE + START_PROCESSING(SSobj, src) + + owner.balloon_alert_to_viewers("starts crushing") + owner.visible_message(span_boldwarning("[owner] starts crushing [constricted] with [owner.p_their()] tail!"), span_warning("You start crushing [constricted] with your tail!"), ignored_mobs = list(constricted)) + to_chat(constricted, span_userdanger("[owner] starts crushing you with [owner.p_their()] tail!")) + return TRUE + +/// Setter proc for currently_crushing that handles processing and warnings. +/obj/structure/serpentine_tail/proc/stop_crushing() + if (!currently_crushing) + return FALSE + + owner.balloon_alert_to_viewers("stops crushing") + owner.visible_message(span_warning("[owner] stops crushing [constricted] with [owner.p_their()] tail."), span_notice("You stop crushing [constricted] with your tail."), ignored_mobs = list(constricted)) + to_chat(constricted, span_boldwarning("[owner] stops crushing you with [owner.p_their()] tail.")) + + currently_crushing = FALSE + STOP_PROCESSING(SSobj, src) + stored_damage = 0 + return TRUE + +/// Setter proc for owner that handles signals, bodyparts, etc. +/obj/structure/serpentine_tail/proc/set_owner(mob/living/carbon/human/new_owner) + if (owner) + UnregisterSignal(owner, list(COMSIG_MOVABLE_MOVED, COMSIG_LIVING_GRAB, COMSIG_LIVING_TRY_PULL, COMSIG_LIVING_SET_BODY_POSITION, COMSIG_ATOM_POST_DIR_CHANGE)) + + if (owner) + var/obj/item/organ/external/taur_body/taur_body = owner.get_organ_slot(ORGAN_SLOT_EXTERNAL_TAUR) + taur_body?.hide_self = FALSE + + owner = new_owner + + if (owner) + var/obj/item/organ/external/taur_body/taur_body = owner.get_organ_slot(ORGAN_SLOT_EXTERNAL_TAUR) + taur_body?.hide_self = TRUE + + RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(owner_moved)) + RegisterSignal(owner, COMSIG_LIVING_GRAB, PROC_REF(owner_tried_grab)) + RegisterSignal(owner, COMSIG_LIVING_TRY_PULL, PROC_REF(owner_tried_pull)) + RegisterSignal(owner, COMSIG_LIVING_SET_BODY_POSITION, PROC_REF(owner_body_position_changed)) + RegisterSignal(owner, COMSIG_ATOM_POST_DIR_CHANGE, PROC_REF(sync_direction)) + owner?.update_mutant_bodyparts() + +/// The time it takes for a constricted thing to do a break-out attempt. +#define SERPENTINE_TAIL_UNBUCKLE_TIME 0.5 SECONDS // arbitrary + +/obj/structure/serpentine_tail/is_buckle_possible(mob/living/target, force, check_loc) + if (target == owner) + return FALSE + + return ..() + +/obj/structure/serpentine_tail/user_unbuckle_mob(mob/living/buckled_mob, mob/user) + if (!constricted || (user != constricted)) // anyone can easily free them except themselves + return ..() + + if (!COOLDOWN_FINISHED(src, escape_cooldown)) + to_chat(user, span_warning("You're still recovering from your last escape attempt!")) // prevent escape spam + return FALSE + + var/escape_chance = CONSTRICT_ESCAPE_CHANCE + if (HAS_TRAIT(user, TRAIT_SLIPPERY)) + escape_chance += AKULA_GRAB_RESIST_BONUS + + if (!prob(escape_chance)) + user.visible_message(span_warning("[user] squirms as they fail to escape from [owner]'s tail!"), span_warning("You squirm as you fail to escape from [owner]'s tail!"), ignored_mobs = owner) + to_chat(owner, span_warning("[user] squirms as they fail to escape from the grip of your tail!")) + COOLDOWN_START(src, escape_cooldown, SERPENTINE_TAIL_UNBUCKLE_TIME) + return FALSE + + user.visible_message(span_warning("[user] breaks free from [owner]'s tail!"), span_warning("You break free from [owner]'s tail!"), ignored_mobs = owner) + to_chat(owner, span_boldwarning("[user] breaks free from the grip of your tail!")) + return ..() + +#undef SERPENTINE_TAIL_UNBUCKLE_TIME + +/obj/structure/serpentine_tail/post_unbuckle_mob(mob/living/unbuckled_mob) + . = ..() + + if (unbuckled_mob == constricted) + set_constricted(null) + +/obj/structure/serpentine_tail/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0) + switch(damage_type) + if(BRUTE) + if(damage_amount) + playsound(loc, 'sound/weapons/bladeslice.ogg', 100, TRUE) + else + playsound(src, 'sound/weapons/tap.ogg', 50, TRUE) + if(BURN) + if(!damage_amount) + return + + playsound(loc, 'sound/items/welder.ogg', 100, TRUE) + + +/// Signal proc for when owner moves. Qdels src. +/obj/structure/serpentine_tail/proc/owner_moved(datum/signal_source, atom/old_loc, dir, forced, list/old_locs) + SIGNAL_HANDLER + + qdel(src) + +/// Signal proc for if our owner changes body positions. Qdels src if they lie down. +/obj/structure/serpentine_tail/proc/owner_body_position_changed(datum/signal_source, new_position, old_position) + SIGNAL_HANDLER + + if (new_position == LYING_DOWN) + qdel(src) + +/// Signal proc for if constricted moves. If the new loc isnt our loc, we stop constricting them. Used for teleportation escapes. +/obj/structure/serpentine_tail/proc/constricted_moved(datum/signal_source, atom/old_loc, dir, forced, list/old_locs) + SIGNAL_HANDLER + + if (constricted.loc != loc) + INVOKE_ASYNC(src, PROC_REF(set_constricted), null) + +/// Signal proc for constricted being examined. Appends a string warning the viewer of them being crushed. +/obj/structure/serpentine_tail/proc/constricted_examined(datum/signal_source, mob/user, list/examine_text) + SIGNAL_HANDLER + + if (currently_crushing) + examine_text += span_boldwarning("[owner] is crushing [constricted.p_them()] with [owner.p_their()] tail!") + +/// Signal proc for constricted qdeleting. Sets constricted to null. +/obj/structure/serpentine_tail/proc/constricted_qdeleting(datum/signal_source) + SIGNAL_HANDLER + + INVOKE_ASYNC(src, PROC_REF(set_constricted), null) + +/// Signal proc for owner pulling someone. Forbids them from pulling constricted. +/obj/structure/serpentine_tail/proc/owner_tried_pull(datum/signal_source, atom/movable/thing, force) + SIGNAL_HANDLER + + if (!allowing_grab_on_constricted && thing == constricted) + owner.balloon_alert(owner, "can't grab constricted!") + return COMPONENT_CANCEL_ATTACK_CHAIN + +/// Signal proc for owner grabbing someone, separate from pulling. Forbids them from upgrading grabs on constricted. +/obj/structure/serpentine_tail/proc/owner_tried_grab(datum/signal_source, mob/living/grabbing) + SIGNAL_HANDLER + + if (!allowing_grab_on_constricted && grabbing == constricted) + owner.balloon_alert(owner, "can't grab constricted!") + return COMPONENT_CANCEL_ATTACK_CHAIN + +/// Signal proc that prevents constricted from grabbing owner. +/obj/structure/serpentine_tail/proc/constricted_tried_pull(datum/signal_source, atom/movable/thing, force) + SIGNAL_HANDLER + + if (thing == owner) + constricted.balloon_alert(constricted, "can't grab constrictor!") + return COMPONENT_CANCEL_ATTACK_CHAIN + +/datum/status_effect/constricted + id = "constricted_tail" + + alert_type = /atom/movable/screen/alert/status_effect/constricted + +/atom/movable/screen/alert/status_effect/constricted + name = "Constricted" + desc = "You're being constricted by a giant tail! You can resist, attack the tail, or attack the constrictor to escape!" + + icon = 'modular_skyrat/modules/taur_mechanics/sprites/ability.dmi' + icon_state = "constrict" + +#undef CONSTRICT_BASE_PIXEL_SHIFT +#undef CONSTRICT_ESCAPE_CHANCE diff --git a/modular_skyrat/modules/taur_mechanics/code/mod.dm b/modular_skyrat/modules/taur_mechanics/code/mod.dm new file mode 100644 index 00000000000..7db71246037 --- /dev/null +++ b/modular_skyrat/modules/taur_mechanics/code/mod.dm @@ -0,0 +1,18 @@ +/// Returns TRUE if this is a mod control module, a mod core, or a piece of mod clothing. FALSE otherwise. +/obj/item/proc/is_mod_shell_component() + return FALSE + +/obj/item/mod/is_mod_shell_component() + return TRUE + +/obj/item/clothing/shoes/mod/is_mod_shell_component() + return TRUE + +/obj/item/clothing/gloves/mod/is_mod_shell_component() + return TRUE + +/obj/item/clothing/suit/mod/is_mod_shell_component() + return TRUE + +/obj/item/clothing/head/mod/is_mod_shell_component() + return TRUE diff --git a/modular_skyrat/modules/taur_mechanics/code/preferences.dm b/modular_skyrat/modules/taur_mechanics/code/preferences.dm new file mode 100644 index 00000000000..35e9eba4a52 --- /dev/null +++ b/modular_skyrat/modules/taur_mechanics/code/preferences.dm @@ -0,0 +1,20 @@ +/datum/preference/toggle/naga_soles + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "naga_sole" + savefile_identifier = PREFERENCE_CHARACTER + priority = PREFERENCE_PRIORITY_BODYPARTS + 0.1 + default_value = FALSE + +/datum/preference/toggle/naga_soles/is_accessible(datum/preferences/preferences) + if (!..()) + return FALSE + + var/datum/sprite_accessory/taur_mode = SSaccessories.sprite_accessories["taur"][preferences.read_preference(/datum/preference/choiced/mutant_choice/taur)] + return (istype(taur_mode, /datum/sprite_accessory/taur/naga) || istype(taur_mode, /datum/sprite_accessory/taur/synthliz/synthnaga)) + +/datum/preference/toggle/naga_soles/apply_to_human(mob/living/carbon/human/target, value, datum/preferences/preferences) + if (value) + return + var/obj/item/organ/external/taur_body/serpentine/snake_body = target.get_organ_slot(ORGAN_SLOT_EXTERNAL_TAUR) + if (istype(snake_body)) + snake_body.add_hardened_soles(target) diff --git a/modular_skyrat/modules/taur_mechanics/code/serpentine_taur.dm b/modular_skyrat/modules/taur_mechanics/code/serpentine_taur.dm new file mode 100644 index 00000000000..c5579362c44 --- /dev/null +++ b/modular_skyrat/modules/taur_mechanics/code/serpentine_taur.dm @@ -0,0 +1,47 @@ +/obj/item/organ/external/taur_body/serpentine + /// The constrict ability we have given our owner. Nullable, if we have no owner. + var/datum/action/innate/constrict/constrict_ability + + /// Did our owner have their feet blocked before we ran on_mob_insert? Used for determining if we should unblock their feet slots on removal. + var/owner_blocked_feet_before_insert + +/obj/item/organ/external/taur_body/serpentine/Destroy() + QDEL_NULL(constrict_ability) // handled in remove, but lets be safe + return ..() + +/obj/item/organ/external/taur_body/serpentine/on_mob_insert(mob/living/carbon/organ_owner, special, movement_flags) + . = ..() + + constrict_ability = new /datum/action/innate/constrict(organ_owner) + constrict_ability.Grant(organ_owner) + + owner_blocked_feet_before_insert = (organ_owner.dna.species.no_equip_flags & ITEM_SLOT_FEET) + organ_owner.dna.species.no_equip_flags |= ITEM_SLOT_FEET + organ_owner.dna.species.modsuit_slot_exceptions |= ITEM_SLOT_FEET + + var/obj/item/clothing/shoes/shoe = organ_owner.get_item_by_slot(ITEM_SLOT_FEET) + if (shoe && !HAS_TRAIT(shoe, TRAIT_NODROP)) + shoe.forceMove(get_turf(organ_owner)) + + var/use_hardened_soles = FALSE + var/datum/preferences/prefs = organ_owner.client?.prefs + if (prefs) + use_hardened_soles = !(prefs.read_preference(/datum/preference/toggle/naga_soles)) + + if (use_hardened_soles) + add_hardened_soles(organ_owner) + +/// Adds TRAIT_HARD_SOLES to our owner. +/obj/item/organ/external/taur_body/serpentine/proc/add_hardened_soles(mob/living/carbon/organ_owner = owner) + ADD_TRAIT(organ_owner, TRAIT_HARD_SOLES, ORGAN_TRAIT) + +/obj/item/organ/external/taur_body/serpentine/on_mob_remove(mob/living/carbon/organ_owner, special, movement_flags) + . = ..() + + QDEL_NULL(constrict_ability) + if (!owner_blocked_feet_before_insert) + organ_owner.dna.species.no_equip_flags &= ~ITEM_SLOT_FEET + owner_blocked_feet_before_insert = FALSE + organ_owner.dna.species.modsuit_slot_exceptions &= ~ITEM_SLOT_FEET + + REMOVE_TRAIT(organ_owner, TRAIT_HARD_SOLES, ORGAN_TRAIT) diff --git a/modular_skyrat/modules/taur_mechanics/code/species.dm b/modular_skyrat/modules/taur_mechanics/code/species.dm new file mode 100644 index 00000000000..915baa64725 --- /dev/null +++ b/modular_skyrat/modules/taur_mechanics/code/species.dm @@ -0,0 +1,3 @@ +/datum/species + /// A bitfield of exceptions, relating to no_equip_flags. If a mod item is equipped into a blocked slot, and that slot is in this bitfield, it will not be skipped. + var/modsuit_slot_exceptions diff --git a/modular_skyrat/modules/taur_mechanics/sprites/ability.dmi b/modular_skyrat/modules/taur_mechanics/sprites/ability.dmi new file mode 100644 index 0000000000000000000000000000000000000000..64b1e25d8ab1a3a6f1a8cb8552f84b2e423cfbeb GIT binary patch literal 592 zcmV-W0V=-0C=2JR&a84_w-Y6@%7{?OD!tS%+FJ>RWQ*r;NmRLOex6#a*U0*I5Sc+ z(=$pSoZ^zil2jm5DLFr{xTGjExkQPJGbOXA7$|1Q#hF%=n41b=D=WDAxqyuZ0KrEa z zAuJi3JHGOKIlxAKPqHLp%o`_R`ZZH*zBrq__+E2V7Vw#CI1Iz4 ztg5Ptp0!khKvo1|VYdNr5*h^5m+LUFvnIFf|G)+^4WNgRQxJHlKYSCLwmz$mMX}E1?r)zng@7u4| zFM1JvaP)p)o{CF}SQaMpOcov$LLR*AZSNN%ySc}OM@Z@qz$ytjIS?f$WHI0!hyY?o zMKLEKya#!R0tbj^az8ZU1@3Vi$Ok-14GK^bBpA{Vc3n5kLKGkm0=cwLQUEuiULYD8 zLf9#xBqAz6BhLZ$p?gpf8v(D9#R#Q$a9wpm%?Jkgp|1Ce(8@0$`8A}QpeLaITR%dX zaqzN><-sliDfN9#DuICNq9EcaCiM--7670Z&}Z5g_7VqV{UhS%T#x_x e(D?Dj5&QvX3dX?cVO*mC0000R1`-f5GV-(A_<`3kc!e#XCQKvLm;0_Tcm(o z77-5w$q*_@5wU^?BsMBo2n8g-h?OFfa*T>XOkonzFHYagzTJKE=Iy^N3J(h+S$tsu z03;4OAOh>CvCQ!pFHKe~W6eK2gcoQ4LpeYfz;^&W0PUE59bgFH7b6*WH9!eKfzdR9 zDGNryXsreVHe^A+`{hM!K++?^xWH78C1Fci4)0KU!11)i)DwdA6Di4Q zLm7T416|FI53Jw?L2xqZlHMttJjAz0*pgc|zo}_fd-k)#A6~kUV~t1u($d=O@LY#- z{>*00Adr}cv+f3Gdj8){> z%gxZQw?)=H8H$Q8hL5p@Ovgwa%Tf`$1tnX_lgvaPc5<#(FSR_glKH)E7PZ~U?utCm zUSd#z#qJ-y#r6<;P*$R|%>|av9@E?{l`2Rtt1Bi9WScWz(b4!E-oCip;HPoB=0j|N zKJ`%Kc)6jkFR36*yU|4U!keOaqn6#jL+6!{Jhxo9EGLYlP;KO$Uxiwm$;P)Q_cFS! zc+`7Yul1|$`Dgt4D-roEbtkSSgPwaB3AzTB_W2{it>u(L;qYC3=F1rKloLcxyRihi zVtHXEY3y%Z;rf5^RR_nXZ`!MBjc}c(aX-2#-E=tALHaIp^WIH9X?a3=_ku#B@6_R> z??e+3NDDYD63xnlF%xoOb)d_&x1%hrDXFgC?c6tcm7$F}$c{6nGRO9M?y+vA8*^ht z8c-9XAY~R*ze3Xo;u_|(Gc#v34$)nmGrT3rnsqpsi5Csid}0RL4zLnt!HeW4^z24$ zouqPgULNIj1N{K4BgVIx_GEF{wDeT1k1ZskLjIiHgpcbRc~6thUt>Y`Mp|9P!wJr< z-RqH=u149rWz#J3&uf)WeMDznEU@ zb)}qdvWKwQ>-#(2ib2)6ZmXKx+JTQ#6`km*h22jjr%RGjtIjkb4_KqUjmLUV3eMLY z7n?y9?)S+k()Jp8ABAO#O5JbJS>!0P4uAIWE?u@;!!n_uq1;7XdQ#H-3A2Tf_UifK zpIScj%NTP6!7JhWmjtz~z)<&`yq8ZOA-k_MVTAj0j$ntdHTaU%A`Lzl6f4Nrz}Le@ zW{`xEfWNLHw=OqfgoMu%I-o~`kA>+K#1&HYV9Vrw!myL3U85h@J*rY^n-+=P(tMPI z#e_kG9=3rbA6e*Tq = { component: FeatureTriBoolInput, }; +export const naga_sole: FeatureToggle = { + name: 'Taur (Naga) disable hardened soles', + description: + 'If using a serpentine taur body, determines if you are immune to caltrops and a few other effects of being barefoot.', + component: CheckboxInput, +}; + export const xenodorsal_toggle: FeatureToggle = { name: 'Xenodorsal', component: CheckboxInput,