diff --git a/code/__DEFINES/mob_hud.dm b/code/__DEFINES/mob_hud.dm index c43e723ccd41..ff00f1df4703 100644 --- a/code/__DEFINES/mob_hud.dm +++ b/code/__DEFINES/mob_hud.dm @@ -29,6 +29,7 @@ #define XENO_EXECUTE "29" // Execute thershold, vampire #define NEW_PLAYER_HUD "30" //Makes it easy to see new players. #define SPYCAM_HUD "31" //Remote control spy cameras. +#define XENO_HOSTILE_TAG_SPREAD "32" // dancer prae yellow 'tag' //data HUD (medhud, sechud) defines #define MOB_HUD_SECURITY_BASIC 1 diff --git a/code/__DEFINES/xeno.dm b/code/__DEFINES/xeno.dm index ee8962ace73d..1bc925bb22fc 100644 --- a/code/__DEFINES/xeno.dm +++ b/code/__DEFINES/xeno.dm @@ -744,6 +744,9 @@ #define XENO_VISION_LEVEL_HIGH_NVG "Three Quarters Night Vision" #define XENO_VISION_LEVEL_FULL_NVG "Full Night Vision" +// dancer defines +#define DANCER_DODGE_TIME 7 SECONDS + // drone fruits diff --git a/code/datums/effects/xeno_strains/dancer_tag.dm b/code/datums/effects/xeno_strains/dancer_tag.dm index 68a00fd4e5a5..6aeb13998745 100644 --- a/code/datums/effects/xeno_strains/dancer_tag.dm +++ b/code/datums/effects/xeno_strains/dancer_tag.dm @@ -3,19 +3,24 @@ duration = null flags = DEL_ON_DEATH | INF_DURATION + var/spread = FALSE + var/mob/living/carbon/xenomorph/source_xeno -/datum/effects/dancer_tag/New(atom/A, mob/from = null, last_dmg_source = null, zone = "chest", ttl = 35) - . = ..(A, from, last_dmg_source, zone) +/datum/effects/dancer_tag/New(atom/target_atom, mob/from = null, last_dmg_source = null, zone = "chest", ttl = 35) + . = ..(target_atom, from, last_dmg_source, zone) addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), src), ttl) - if (ishuman(A)) - var/mob/living/carbon/human/H = A - H.update_xeno_hostile_hud() + if(istype(from, /mob/living/carbon/xenomorph)) + source_xeno = from + if(ishuman(target_atom)) + var/mob/living/carbon/human/target_human = target_atom + target_human.update_xeno_hostile_hud() -/datum/effects/dancer_tag/validate_atom(mob/living/carbon/H) - if (!isxeno_human(H) || H.stat == DEAD) + +/datum/effects/dancer_tag/validate_atom(mob/living/carbon/target_human) + if(!isxeno_human(target_human) || target_human.stat == DEAD) return FALSE return ..() @@ -24,18 +29,32 @@ . = ..() // Also checks for null atoms - if (!istype(affected_atom, /mob/living/carbon/human)) + if(!istype(affected_atom, /mob/living/carbon/human)) return - var/mob/living/carbon/human/H = affected_atom - H.update_xeno_hostile_hud() + var/mob/living/carbon/human/target_human = affected_atom + target_human.update_xeno_hostile_hud() /datum/effects/dancer_tag/Destroy() - if (!ishuman(affected_atom)) - return ..() - - var/mob/living/carbon/human/H = affected_atom - addtimer(CALLBACK(H, TYPE_PROC_REF(/mob/living/carbon/human, update_xeno_hostile_hud)), 3) + if(ishuman(affected_atom)) + var/mob/living/carbon/human/target_human = affected_atom + target_human.update_xeno_hostile_hud() + if(spread) + to_chat(target_human, SPAN_XENODANGER("You calm down and get back to your senses.")) + to_chat(source_xeno, SPAN_XENODANGER("Our surging instincts fade away, we no longer feel compelled to hunt them.")) + spread = FALSE return ..() + +/datum/effects/dancer_tag/normal + effect_name = "dancer tag normal" + +/datum/effects/dancer_tag/spread + effect_name = "dancer tag spread" + +/datum/effects/dancer_tag/spread/New(atom/target_atom, mob/from = null) + . = ..(target_atom, from, null, "chest", 7 SECONDS) + + to_chat(target_atom, SPAN_XENOHIGHDANGER("You feel fear washing down your spine... you could be next!")) + spread = TRUE diff --git a/code/datums/mob_hud.dm b/code/datums/mob_hud.dm index 739b08268217..41e23d14b590 100644 --- a/code/datums/mob_hud.dm +++ b/code/datums/mob_hud.dm @@ -204,7 +204,7 @@ GLOBAL_LIST_INIT_TYPED(huds, /datum/mob_hud, flatten_numeric_alist(alist( /datum/mob_hud/xeno/xeno_hive_tutorial /datum/mob_hud/xeno_hostile - hud_icons = list(XENO_HOSTILE_ACID, XENO_HOSTILE_SLOW, XENO_HOSTILE_TAG, XENO_HOSTILE_FREEZE) + hud_icons = list(XENO_HOSTILE_ACID, XENO_HOSTILE_SLOW, XENO_HOSTILE_TAG, XENO_HOSTILE_TAG_SPREAD, XENO_HOSTILE_FREEZE) /datum/mob_hud/execute_hud hud_icons = list(XENO_EXECUTE) @@ -903,16 +903,19 @@ GLOBAL_DATUM_INIT(hud_icon_hudfocus, /image, image('icons/mob/hud/human_status.d var/image/acid_holder = hud_list[XENO_HOSTILE_ACID] var/image/slow_holder = hud_list[XENO_HOSTILE_SLOW] var/image/tag_holder = hud_list[XENO_HOSTILE_TAG] + var/image/tag_spread_holder = hud_list[XENO_HOSTILE_TAG_SPREAD] var/image/freeze_holder = hud_list[XENO_HOSTILE_FREEZE] acid_holder.icon_state = "hudblank" slow_holder.icon_state = "hudblank" tag_holder.icon_state = "hudblank" + tag_spread_holder.icon_state = "hudblank" freeze_holder.icon_state = "hudblank" acid_holder.overlays.Cut() slow_holder.overlays.Cut() tag_holder.overlays.Cut() + tag_spread_holder.overlays.Cut() freeze_holder.overlays.Cut() var/acid_found = FALSE @@ -936,14 +939,23 @@ GLOBAL_DATUM_INIT(hud_icon_hudfocus, /image, image('icons/mob/hud/human_status.d slow_holder.overlays += image('icons/mob/hud/hud.dmi', "xeno_slow") var/tag_found = FALSE - for (var/datum/effects/dancer_tag/DT in effects_list) - if (!QDELETED(DT)) + for(var/datum/effects/dancer_tag/normal/normal_tag in effects_list) + if(!QDELETED(normal_tag)) tag_found = TRUE break - if (tag_found) + if(tag_found) tag_holder.overlays += image('icons/mob/hud/hud.dmi', src, "prae_tag") + var/spread_tag_found = FALSE + for(var/datum/effects/dancer_tag/spread/spread_tag in effects_list) + if(!QDELETED(spread_tag)) + spread_tag_found = TRUE + break + + if(spread_tag_found) + tag_spread_holder.overlays += image('icons/mob/hud/hud.dmi', src, "prae_tag_yellow") + var/freeze_found = HAS_TRAIT(src, TRAIT_IMMOBILIZED) && body_position == STANDING_UP && !buckled // Eligible targets are unable to move but can stand and aren't buckled (eg nested) - This is to convey that they are temporarily unable to move if (freeze_found) freeze_holder.overlays += image('icons/mob/hud/hud.dmi', src, "xeno_freeze") diff --git a/code/modules/client/preferences_toggles.dm b/code/modules/client/preferences_toggles.dm index a427c8508f6c..9d744e6a2c31 100644 --- a/code/modules/client/preferences_toggles.dm +++ b/code/modules/client/preferences_toggles.dm @@ -771,6 +771,7 @@ CLIENT_VERB(toggle_minimap_ceiling_protection) "Security HUD" = MOB_HUD_SECURITY_ADVANCED, "Squad HUD" = MOB_HUD_FACTION_OBSERVER, "Xeno Status HUD" = MOB_HUD_XENO_STATUS, + "Xeno Effects HUD" = MOB_HUD_XENO_HOSTILE, "Hunter HUD" = MOB_HUD_HUNTER, "Faction UPP HUD" = MOB_HUD_FACTION_UPP, "Faction Wey-Yu HUD" = MOB_HUD_FACTION_WY, diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index 194429b145d8..c04b6517a057 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -407,6 +407,9 @@ if("Xeno Status HUD") the_hud = GLOB.huds[MOB_HUD_XENO_STATUS] the_hud.add_hud_to(src, src) + if("Xeno Effects HUD") + the_hud = GLOB.huds[MOB_HUD_XENO_HOSTILE] + the_hud.add_hud_to(src, src) if("Hunter HUD") the_hud = GLOB.huds[MOB_HUD_HUNTER] the_hud.add_hud_to(src, src) diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm index 4e09975e2418..bc776bb526b4 100644 --- a/code/modules/mob/living/carbon/human/human_defines.dm +++ b/code/modules/mob/living/carbon/human/human_defines.dm @@ -142,7 +142,7 @@ var/last_chew = 0 //taken from human.dm - hud_possible = list(HEALTH_HUD, STATUS_HUD, STATUS_HUD_OOC, STATUS_HUD_XENO_INFECTION, STATUS_HUD_XENO_CULTIST, ID_HUD, WANTED_HUD, ORDER_HUD, XENO_HOSTILE_ACID, XENO_HOSTILE_SLOW, XENO_HOSTILE_TAG, XENO_HOSTILE_FREEZE, XENO_EXECUTE, HUNTER_CLAN, HUNTER_HUD, FACTION_HUD, HOLOCARD_HUD, NEW_PLAYER_HUD) + hud_possible = list(HEALTH_HUD, STATUS_HUD, STATUS_HUD_OOC, STATUS_HUD_XENO_INFECTION, STATUS_HUD_XENO_CULTIST, ID_HUD, WANTED_HUD, ORDER_HUD, XENO_HOSTILE_ACID, XENO_HOSTILE_SLOW, XENO_HOSTILE_TAG, XENO_HOSTILE_TAG_SPREAD, XENO_HOSTILE_FREEZE, XENO_EXECUTE, HUNTER_CLAN, HUNTER_HUD, FACTION_HUD, HOLOCARD_HUD, NEW_PLAYER_HUD) var/embedded_flag //To check if we've need to roll for damage on movement while an item is imbedded in us. var/allow_gun_usage = TRUE var/melee_allowed = TRUE @@ -182,6 +182,9 @@ // Xenomorph that is hauling us if we are hauled var/mob/living/carbon/xenomorph/hauling_xeno + /// Timer to prevent spreading yellow dancer tags from same person. + var/last_target_spread_time = 0 + // Haul resist cooldown var/next_haul_resist diff --git a/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm b/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm index eeac2255cbc8..20d6b24244bf 100644 --- a/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm +++ b/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm @@ -45,7 +45,7 @@ see_in_dark = 12 recovery_constant = 1.5 see_invisible = SEE_INVISIBLE_LIVING - hud_possible = list(HEALTH_HUD_XENO, PLASMA_HUD, SPECIAL_HUD, PHEROMONE_HUD, QUEEN_OVERWATCH_HUD, ARMOR_HUD_XENO, XENO_STATUS_HUD, XENO_BANISHED_HUD, XENO_HOSTILE_ACID, XENO_HOSTILE_SLOW, XENO_HOSTILE_TAG, XENO_HOSTILE_FREEZE, HUNTER_HUD, NEW_PLAYER_HUD) + hud_possible = list(HEALTH_HUD_XENO, PLASMA_HUD, SPECIAL_HUD, PHEROMONE_HUD, QUEEN_OVERWATCH_HUD, ARMOR_HUD_XENO, XENO_STATUS_HUD, XENO_BANISHED_HUD, XENO_HOSTILE_ACID, XENO_HOSTILE_SLOW, XENO_HOSTILE_TAG, XENO_HOSTILE_TAG_SPREAD, XENO_HOSTILE_FREEZE, HUNTER_HUD, NEW_PLAYER_HUD) unacidable = TRUE rebounds = TRUE faction = FACTION_XENOMORPH @@ -69,6 +69,13 @@ var/static/list/walking_state_cache = list() var/has_walking_icon_state = FALSE + /// Timer for dodge_threshold + var/last_projectile_time = 0 + /// Counts how many bullets hit xeno before dodge_threshold occurs. + var/projectiles_counted = 0 + /// Guaranteed bullet dodge every X bullet shoot (don't work when you are laying down or UNCONSCIOUS) + var/dodge_threshold = 0 + ////////////////////////////////////////////////////////////////// // // Core Stats diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/praetorian/praetorian_abilities.dm b/code/modules/mob/living/carbon/xenomorph/abilities/praetorian/praetorian_abilities.dm index 911cada561e3..5991ac7fde8b 100644 --- a/code/modules/mob/living/carbon/xenomorph/abilities/praetorian/praetorian_abilities.dm +++ b/code/modules/mob/living/carbon/xenomorph/abilities/praetorian/praetorian_abilities.dm @@ -167,6 +167,12 @@ ////////// Dancer Abilities +/datum/action/xeno_action/activable/tail_stab/harpoon_tail + name = "Tail Lance" + action_icon_state = "tail_harpoon" + action_type = XENO_ACTION_CLICK + ability_primacy = XENO_TAIL_STAB + /datum/action/xeno_action/activable/prae_impale name = "Impale" action_icon_state = "prae_impale" @@ -175,8 +181,9 @@ action_type = XENO_ACTION_CLICK xeno_cooldown = 13 SECONDS plasma_cost = 80 + var/range = 2 - var/impale_click_miss_cooldown = 1.5 SECONDS + var/impale_click_miss_cooldown = 0.7 SECONDS /datum/action/xeno_action/onclick/prae_dodge name = "Dodge" @@ -184,12 +191,12 @@ macro_path = /datum/action/xeno_action/verb/verb_prae_dodge ability_primacy = XENO_PRIMARY_ACTION_2 action_type = XENO_ACTION_CLICK - plasma_cost = 200 - xeno_cooldown = 19 SECONDS // Config - var/duration = 70 + var/duration = DANCER_DODGE_TIME + var/dodge_timer = TIMER_ID_NULL var/speed_buff_amount = 0.5 + var/afterimage_interval = 1 DECISECONDS /datum/action/xeno_action/activable/prae_tail_trip name = "Tail Trip" @@ -200,7 +207,7 @@ xeno_cooldown = 13 SECONDS plasma_cost = 30 - var/tail_click_miss_cooldown = 1.5 SECONDS + var/tail_click_miss_cooldown = 0.7 SECONDS // Config var/range = 2 diff --git a/code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/dancer.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/dancer.dm index 75e9e4ecdf82..616418396fad 100644 --- a/code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/dancer.dm +++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/dancer.dm @@ -1,11 +1,12 @@ /datum/xeno_strain/dancer // My name is Cuban Pete, I'm the King of the Rumba Beat name = PRAETORIAN_DANCER - description = "You lose all of your acid-based abilities and a small amount of your armor in exchange for increased movement speed, evasion, and unparalleled agility that gives you an ability to move even more quickly, dodge bullets, and phase through enemies and allies alike. By slashing enemies, you temporarily increase your movement speed and you also you apply a tag that changes how your two new tail abilities function. By tagging enemies, you will make Impale hit twice instead of once and make Tail Trip knock enemies down instead of stunning them." + description = "You lose all acid-based abilities and a small amount of your armor in exchange for increased movement speed, evasion, and unparalleled agility. This strain excels at rapid repositioning, bullet dodging, and phasing effortlessly through enemies and allies alike. Slashing enemies applies a red tag, altering how your tail abilities function. Tagged enemies cause Impale to strike twice and transform Tail Trip into a powerful knockdown instead of a brief stun. Your new Tail Stab adapts to your intent. When used in Disarm mode, it becomes a Blunt, armor-piercing strike. When enemies are brought close to death, yellow tags will spread to nearby foes. Slashing yellow-tagged enemies reduces the cooldown of your tail abilities, and using a tail trip or impale ability on a yellow-tagged target grants no cooldown penalty." flavor_description = "A performance fit for a Queen, this one will become my instrument of death." icon_state_prefix = "Dancer" actions_to_remove = list( + /datum/action/xeno_action/activable/tail_stab, /datum/action/xeno_action/activable/xeno_spit, /datum/action/xeno_action/activable/pounce/base_prae_dash, /datum/action/xeno_action/activable/prae_acid_ball, @@ -14,6 +15,7 @@ /datum/action/xeno_action/activable/xeno_spit/praetorian, ) actions_to_add = list( + /datum/action/xeno_action/activable/tail_stab/harpoon_tail, /datum/action/xeno_action/activable/prae_impale, /datum/action/xeno_action/onclick/prae_dodge, /datum/action/xeno_action/activable/prae_tail_trip, @@ -24,77 +26,241 @@ /datum/xeno_strain/dancer/apply_strain(mob/living/carbon/xenomorph/praetorian/prae) prae.armor_modifier -= XENO_ARMOR_MOD_VERY_SMALL prae.speed_modifier += XENO_SPEED_FASTMOD_TIER_5 + prae.regeneration_multiplier = XENO_REGEN_MULTIPLIER_TIER_7 prae.plasma_types = list(PLASMA_CATECHOLAMINE) prae.claw_type = CLAW_TYPE_SHARP + prae.dodge_threshold = 6 + prae.received_phero_caps["recovery"] = 3 //need to be limited, regens too fast with high strength phermones. prae.recalculate_everything() +#define DANCER_YELLOW_TAG_SPREAD_DURATION 7 SECONDS +#define DANCER_YELLOW_TAG_SPREAD_CD 20 SECONDS +#define DANCER_YELLOW_TAG_SPREAD_DIST 5 +#define DANCER_TAG_SPREAD_COUNT 5 + /datum/behavior_delegate/praetorian_dancer name = "Praetorian Dancer Behavior Delegate" - // State + /// How much time is left on timer. (used for status) + var/time_left = null + + /// Check for slashed target that had yellow tag + var/spread_slash_triggered = FALSE + + /// How many targets got yellow tag. + var/spread_count = 0 + + /// How much damage Harpoon Tail on DISARM mode do. (pierces armor) + var/blunt_damage = 8 + /// Harpoon Tail mode, used only to display in status. + var/tail_mode = null + + /// Is Dodge ability active? var/dodge_activated = FALSE + /// Used to countdown DANCER_DODGE_TIME. + var/dodge_start_time = -1 + /// How much refund we want to get back? 1.0 is 1s used to 1s cooldown, 2.0 is 1s used 2s cooldown. + var/refund_multiplier = 2.0 + /// Used in calculation, finalized number will be displayed as cooldown. + var/recharge_time = null + /// Cooldown after activation to prevent accidental double click. + var/safe_click_cooldown = 0 + + /// Timer to prevent dancer from spreading yellow tags. + var/last_dancer_spread_time = 0 + +/datum/behavior_delegate/praetorian_dancer/append_to_stat() + . = list() + . += "Guaranteed Dodge every [bound_xeno.dodge_threshold] bullet\s." + . += "Yellow Tag Spread Delay: 5 seconds." + intent_detection() + . += "Tail Lance Intent: [tail_mode]" + if(tail_mode == "Blunt") + . += "Damage: [blunt_damage] AP" + . += "Cooldown: 3 seconds." + if(dodge_start_time != -1) + time_left = (DANCER_DODGE_TIME - (world.time - dodge_start_time)) / 10 + . += "Dodge Remaining: [time_left] second\s." + return -/datum/behavior_delegate/praetorian_dancer/melee_attack_additional_effects_target(mob/living/carbon/target_carbon) - if (!isxeno_human(target_carbon)) +/datum/behavior_delegate/praetorian_dancer/melee_attack_additional_effects_self() + ..() + + if(!spread_slash_triggered) return - if (target_carbon.stat) + spread_slash_triggered = FALSE + + var/datum/action/xeno_action/activable/prae_impale/impale_action = get_action(bound_xeno, /datum/action/xeno_action/activable/prae_impale) + if(!impale_action.action_cooldown_check()) + impale_action.apply_cooldown_override() + + var/datum/action/xeno_action/activable/prae_tail_trip/tail_trip_action = get_action(bound_xeno, /datum/action/xeno_action/activable/prae_tail_trip) + if(!tail_trip_action.action_cooldown_check()) + tail_trip_action.apply_cooldown_override() + +/datum/behavior_delegate/praetorian_dancer/melee_attack_additional_effects_target(mob/living/carbon/target_carbon) + if(!isxeno_human(target_carbon)) return // Clean up all tags to 'refresh' our TTL - for (var/datum/effects/dancer_tag/target_tag in target_carbon.effects_list) + for(var/datum/effects/dancer_tag/normal/target_tag in target_carbon.effects_list) qdel(target_tag) - new /datum/effects/dancer_tag(target_carbon, bound_xeno, , , 35) + new /datum/effects/dancer_tag/normal(target_carbon, bound_xeno, , , 35) if(ishuman(target_carbon)) var/mob/living/carbon/human/target_human = target_carbon target_human.update_xeno_hostile_hud() + var/consumed_spread = FALSE + for(var/datum/effects/dancer_tag/spread/spread_tag in target_carbon.effects_list) + qdel(spread_tag) + consumed_spread = TRUE + break + + if(consumed_spread) + spread_slash_triggered = TRUE + + if(target_carbon.health <= 0) + try_spread_tags_from(target_carbon) + +/datum/behavior_delegate/praetorian_dancer/proc/try_spread_tags_from(mob/living/carbon/human/source) + if(!ishuman(source)) + return + + var/turf/origin = get_turf(source) + if(!origin) + return + + if(world.time < last_dancer_spread_time + DANCER_YELLOW_TAG_SPREAD_DURATION) + return + + if(world.time < source.last_target_spread_time + DANCER_YELLOW_TAG_SPREAD_CD) + return + source.last_target_spread_time = world.time + + var/spread_count = 0 + + for(var/mob/living/carbon/human/human_target in view(DANCER_YELLOW_TAG_SPREAD_DIST)) + if(human_target == source) + continue + if(human_target.stat == DEAD || human_target.stat == UNCONSCIOUS) + continue + if(locate(/datum/effects/dancer_tag) in human_target.effects_list) + continue + + new /datum/effects/dancer_tag/spread(human_target, bound_xeno) + human_target.update_xeno_hostile_hud() + spread_count++ + + if(spread_count >= DANCER_TAG_SPREAD_COUNT) + break + + if(spread_count) + last_dancer_spread_time = world.time + + if(spread_count >= 0) + to_chat(bound_xeno, SPAN_XENOHIGHDANGER("Fear spreads among the prey, their weakness fuels your instincts to strike them down!")) + +/datum/behavior_delegate/praetorian_dancer/proc/intent_detection() + if(bound_xeno && bound_xeno.a_intent == INTENT_DISARM) + tail_mode = "Blunt" + else + tail_mode = "Normal" + +/datum/action/xeno_action/activable/tail_stab/harpoon_tail/ability_act(mob/living/carbon/xenomorph/xeno, mob/living/carbon/target, obj/limb/limb) + if(!istype(xeno) || !istype(target)) + return + + var/datum/behavior_delegate/praetorian_dancer/behavior = xeno.behavior_delegate + if(!istype(behavior)) + return + + if(xeno.a_intent == INTENT_DISARM) + target.last_damage_data = create_cause_data(initial(xeno.caste_type), xeno) + + xeno.visible_message( + SPAN_XENOWARNING("[xeno] smash [target] with flat side of its tail!"), + SPAN_XENOWARNING("We smash [target] with flat side of our tail!") + ) + xeno.animation_attack_on(target) + xeno.flick_attack_overlay(target, "slam") + + if(xeno.behavior_delegate) + xeno.behavior_delegate.melee_attack_additional_effects_target(target) + + playsound(target, "punch", 25, TRUE) + target.apply_damage(behavior.blunt_damage, BRUTE, "chest") + apply_cooldown(cooldown_modifier = 0.3) + update_button_icon() + return target + + return ..() + /datum/action/xeno_action/activable/prae_impale/use_ability(atom/target_atom) var/mob/living/carbon/xenomorph/dancer_user = owner - if (!action_cooldown_check()) + if(!action_cooldown_check()) return - if (!dancer_user.check_state()) + if(!dancer_user.check_state()) return - if (!ismob(target_atom)) + if(!ismob(target_atom)) apply_cooldown_override(impale_click_miss_cooldown) update_button_icon() return - if (!isxeno_human(target_atom) || dancer_user.can_not_harm(target_atom)) + if(!isxeno_human(target_atom) || dancer_user.can_not_harm(target_atom)) to_chat(dancer_user, SPAN_XENODANGER("We must target a hostile!")) return - if (!dancer_user.Adjacent(target_atom)) - to_chat(dancer_user, SPAN_XENODANGER("We must be adjacent to [target_atom]!")) - return - var/mob/living/carbon/target_carbon = target_atom - if (target_carbon.stat == DEAD) + if(target_carbon.stat == DEAD) to_chat(dancer_user, SPAN_XENOWARNING("[target_atom] is dead, why would we want to attack it?")) return - if (!check_and_use_plasma_owner()) + var/dist = get_dist(dancer_user, target_carbon) + + if(dist > range) + to_chat(dancer_user, SPAN_WARNING("[target_carbon] is too far away!")) + return + + if(dist > 1) + var/turf/target_turf = get_step(dancer_user, get_dir(dancer_user, target_carbon)) + if(target_turf.density) + to_chat(dancer_user, SPAN_WARNING("We can't attack through [target_turf]!")) + return + else + for(var/atom/atom_in_turf in target_turf) + if(atom_in_turf.density && !atom_in_turf.throwpass && !istype(atom_in_turf, /obj/structure/barricade) && !istype(atom_in_turf, /mob/living)) + to_chat(dancer_user, SPAN_WARNING("We can't attack through [atom_in_turf]!")) + return + + if(!check_and_use_plasma_owner()) return apply_cooldown() var/buffed = FALSE - for (var/datum/effects/dancer_tag/dancer_tag_effect in target_carbon.effects_list) + for(var/datum/effects/dancer_tag/spread/tag_spread in target_carbon.effects_list) + buffed = TRUE + qdel(tag_spread) + apply_cooldown_override() + break + + for(var/datum/effects/dancer_tag/normal/dancer_tag_effect in target_carbon.effects_list) buffed = TRUE qdel(dancer_tag_effect) break if(ishuman(target_carbon)) - var/mob/living/carbon/human/Hu = target_carbon - Hu.update_xeno_hostile_hud() + var/mob/living/carbon/human/human_target = target_carbon + human_target.update_xeno_hostile_hud() - // Hmm todayvisible_message(SPAN_DANGER("\The [dancer_user] violently slices [target_atom] with its tail[buffed?" twice":""]!"), + // Hmm today visible_message(SPAN_DANGER("\The [dancer_user] violently slices [target_atom] with its tail[buffed?" twice":""]!"), dancer_user.face_atom(target_atom) var/damage = get_xeno_damage_slash(target_carbon, rand(dancer_user.melee_damage_lower, dancer_user.melee_damage_upper)) @@ -102,98 +268,247 @@ dancer_user.visible_message(SPAN_DANGER("\The [dancer_user] violently slices [target_atom] with its tail[buffed?" twice":""]!"), SPAN_DANGER("We slice [target_atom] with our tail[buffed?" twice":""]!")) + var/list/attack_data = list( + "attacker" = dancer_user, + "target" = target_carbon, + "damage" = damage + ) + impale_strike(attack_data) + if(buffed) - // Do two attacks instead of one - dancer_user.animation_attack_on(target_atom) - dancer_user.flick_attack_overlay(target_atom, "tail") dancer_user.emote("roar") // Feedback for the player that we got the magic double impale + addtimer(CALLBACK(src, PROC_REF(impale_strike), attack_data), 4 DECISECONDS) - target_carbon.apply_armoured_damage(damage, ARMOR_MELEE, BRUTE, "chest", 10) - playsound(target_carbon, 'sound/weapons/alien_tail_attack.ogg', 30, TRUE) + return ..() - // Reroll damage - damage = get_xeno_damage_slash(target_carbon, rand(dancer_user.melee_damage_lower, dancer_user.melee_damage_upper)) - sleep(4) // Short sleep so the animation and sounds will be distinct, but this creates some strange effects if the prae runs away. not entirely happy with this, but I think its benefits outweigh its drawbacks +/datum/action/xeno_action/activable/prae_impale/proc/impale_strike(list/attack_data) + var/mob/living/carbon/xenomorph/attacker = attack_data["attacker"] + var/mob/living/carbon/target = attack_data["target"] + var/damage = attack_data["damage"] - dancer_user.animation_attack_on(target_atom) - dancer_user.flick_attack_overlay(target_atom, "tail") + if(!attacker || !target || target.stat == DEAD || QDELETED(attacker) || QDELETED(target)) + return - target_carbon.last_damage_data = create_cause_data(initial(dancer_user.caste_type), dancer_user) - target_carbon.apply_armoured_damage(damage, ARMOR_MELEE, BRUTE, "chest", 10) - playsound(target_carbon, 'sound/weapons/alien_tail_attack.ogg', 30, TRUE) - return ..() + attacker.animation_attack_on(target) + attacker.flick_attack_overlay(target, "tail") + + target.last_damage_data = create_cause_data(initial(attacker.caste_type), attacker) + target.apply_armoured_damage(damage, ARMOR_MELEE, BRUTE, "chest", 10) + playsound(target, 'sound/weapons/alien_tail_attack.ogg', 30, TRUE) /datum/action/xeno_action/onclick/prae_dodge/use_ability(atom/target) var/mob/living/carbon/xenomorph/dodge_user = owner + if(!istype(dodge_user) || !dodge_user.check_state()) + return - if (!action_cooldown_check()) + var/datum/behavior_delegate/praetorian_dancer/behavior = dodge_user.behavior_delegate + if(!istype(behavior)) return - if (!istype(dodge_user) || !dodge_user.check_state()) + if(behavior.dodge_activated) + remove_effects() return - if (!check_and_use_plasma_owner()) + if(!action_cooldown_check()) return - var/datum/behavior_delegate/praetorian_dancer/behavior = dodge_user.behavior_delegate - if (!istype(behavior)) + if(!check_and_use_plasma_owner(200)) return behavior.dodge_activated = TRUE + behavior.dodge_start_time = world.time + behavior.safe_click_cooldown = world.time + 1 SECONDS button.icon_state = "template_active" - to_chat(dodge_user, SPAN_XENOHIGHDANGER("We can now dodge through mobs!")) dodge_user.speed_modifier -= speed_buff_amount + dodge_user.dodge_threshold -= 3 dodge_user.add_temp_pass_flags(PASS_MOB_THRU) dodge_user.recalculate_speed() + dodge_user.balloon_alert(dodge_user, "we start our evasive stance!", text_color = "#7d32bb", delay = 1 SECONDS) - addtimer(CALLBACK(src, PROC_REF(remove_effects)), duration) + INVOKE_ASYNC(src, PROC_REF(create_afterimage_sequence), dodge_user, duration) + + if(dodge_timer != TIMER_ID_NULL) + deltimer(dodge_timer) + + dodge_timer = addtimer(CALLBACK(src, PROC_REF(remove_effects)), duration, TIMER_STOPPABLE) - apply_cooldown() return ..() /datum/action/xeno_action/onclick/prae_dodge/proc/remove_effects() var/mob/living/carbon/xenomorph/dodge_remove = owner - - if (!istype(dodge_remove)) + if(!istype(dodge_remove)) return var/datum/behavior_delegate/praetorian_dancer/behavior = dodge_remove.behavior_delegate - if (!istype(behavior)) + if(!istype(behavior)) + return + + if(!behavior.dodge_activated) + return + + if(world.time < behavior.safe_click_cooldown) + to_chat(dodge_remove, SPAN_XENOWARNING("We need a moment before breaking our evasive stance!")) + return + + behavior.dodge_activated = FALSE + button.icon_state = "template_xeno" + dodge_remove.speed_modifier += speed_buff_amount + dodge_remove.dodge_threshold += 3 + dodge_remove.remove_temp_pass_flags(PASS_MOB_THRU) + dodge_remove.recalculate_speed() + dodge_remove.reset_position_to_initial() + dodge_remove.balloon_alert(dodge_remove, "our evasive stance fades", text_color = "#bb5d32", delay = 1 SECONDS) + + if(dodge_timer != TIMER_ID_NULL) + deltimer(dodge_timer) + dodge_timer = TIMER_ID_NULL + + if(behavior.dodge_start_time > 0) + var/used_ratio = round((world.time - behavior.dodge_start_time) / duration, 0.1) + behavior.recharge_time = max(DANCER_DODGE_TIME * used_ratio * behavior.refund_multiplier, 5 SECONDS) + + behavior.dodge_start_time = -1 + apply_cooldown_override(behavior.recharge_time) + +/datum/action/xeno_action/onclick/prae_dodge/proc/create_afterimage_sequence(mob/living/carbon/xenomorph/dodge_user, duration) + if(!dodge_user || !dodge_user.loc) return - if (behavior.dodge_activated) - behavior.dodge_activated = FALSE - button.icon_state = "template_xeno" - dodge_remove.speed_modifier += speed_buff_amount - dodge_remove.remove_temp_pass_flags(PASS_MOB_THRU) - dodge_remove.recalculate_speed() - to_chat(dodge_remove, SPAN_XENOHIGHDANGER("We can no longer dodge through mobs!")) + var/afterimage_count = round(duration / afterimage_interval) + + var/datum/afterimage_state/state = new + state.owner = dodge_user + state.remaining = afterimage_count + state.last_turf = get_turf(dodge_user.loc) + + addtimer(CALLBACK(src, PROC_REF(process_afterimage_tick), state), afterimage_interval) + +/datum/action/xeno_action/onclick/prae_dodge/proc/process_afterimage_tick(datum/afterimage_state/state) + if(!state || !state.owner || !state.owner.loc) + return + + var/mob/living/carbon/xenomorph/dodge_user = state.owner + var/datum/behavior_delegate/praetorian_dancer/behavior = dodge_user.behavior_delegate + if(!istype(behavior) || !behavior.dodge_activated) + return + var/turf/current_position = get_turf(dodge_user.loc) + + if(current_position && current_position != state.last_turf) + var/random_offset_x = rand(-4, 4) + var/random_offset_y = rand(-4, 4) + + dodge_user.reset_position_to_initial() + dodge_user.apply_offset(random_offset_x, random_offset_y) + + create_afterimage(dodge_user, random_offset_x, random_offset_y) + state.last_turf = current_position + + state.remaining-- + + if(state.remaining > 0) + addtimer(CALLBACK(src, PROC_REF(process_afterimage_tick), state), afterimage_interval) + else + addtimer(CALLBACK(dodge_user, TYPE_PROC_REF(/atom/movable, reset_position_to_initial)), 2 DECISECONDS) + +/datum/action/xeno_action/onclick/prae_dodge/proc/create_afterimage(mob/living/carbon/xenomorph/dodge_user, random_offset_x, random_offset_y) + if(!dodge_user || !dodge_user.loc) + return + + var/turf/afterimage_location = get_turf(dodge_user.loc) + if(!afterimage_location) + return + + var/directional_offset_x = 0 + var/directional_offset_y = 0 + + switch(dodge_user.dir) + if(NORTH) + directional_offset_y = -16 + if(SOUTH) + directional_offset_y = 16 + if(EAST) + directional_offset_x = -16 + if(WEST) + directional_offset_x = 16 + + var/obj/effect/overlay/afterimage = new /obj/effect/overlay/afterimage(afterimage_location) + afterimage.icon = dodge_user.icon + afterimage.icon_state = dodge_user.icon_state + afterimage.color = dodge_user.color + afterimage.layer = dodge_user.layer + afterimage.dir = dodge_user.dir + afterimage.alpha = 200 + afterimage.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + afterimage.pixel_x = dodge_user.pixel_x + directional_offset_x + afterimage.pixel_y = dodge_user.pixel_y + directional_offset_y + + addtimer(CALLBACK(afterimage, TYPE_PROC_REF(/obj/effect/overlay/afterimage, fade_out_afterimage))) + +/obj/effect/overlay/afterimage/proc/fade_out_afterimage() + if(!src) + return + + fade_step = 1 + addtimer(CALLBACK(src, PROC_REF(handle_fade_tick)), fade_delay) + +/obj/effect/overlay/afterimage/proc/handle_fade_tick() + if(!src) + return + + alpha = round(200 * (1 - (fade_step / fade_max_steps))) + + if(fade_step >= fade_max_steps) + qdel(src) + else + fade_step++ + addtimer(CALLBACK(src, PROC_REF(handle_fade_tick)), fade_delay) + +/atom/movable/proc/reset_position_to_initial() + pixel_x = initial(pixel_x) + pixel_y = initial(pixel_y) + +/atom/movable/proc/apply_offset(dir_x, dir_y) + pixel_x += dir_x + pixel_y += dir_y + +/datum/afterimage_state + var/mob/living/carbon/xenomorph/owner + var/remaining + var/turf/last_turf + +/obj/effect/overlay/afterimage + name = "Dancer Afterimage" + icon = 'icons/mob/xenos/castes/tier_3/praetorian.dmi' + layer = MOB_LAYER + var/fade_step = 0 + var/fade_max_steps = 3 + var/fade_delay = 1 DECISECONDS /datum/action/xeno_action/activable/prae_tail_trip/use_ability(atom/target_atom) var/mob/living/carbon/xenomorph/dancer_user = owner - if (!action_cooldown_check()) + if(!action_cooldown_check()) return - if (!istype(dancer_user) || !dancer_user.check_state()) + if(!istype(dancer_user) || !dancer_user.check_state()) return - if (!ismob(target_atom)) + if(!ismob(target_atom)) apply_cooldown_override(tail_click_miss_cooldown) update_button_icon() return - if (!isxeno_human(target_atom) || dancer_user.can_not_harm(target_atom)) + if(!isxeno_human(target_atom) || dancer_user.can_not_harm(target_atom)) to_chat(dancer_user, SPAN_XENODANGER("We must target a hostile!")) return var/mob/living/carbon/target_carbon = target_atom - if (target_carbon.stat == DEAD) + if(target_carbon.stat == DEAD) to_chat(dancer_user, SPAN_XENOWARNING("[target_atom] is dead, why would we want to attack it?")) return - if (!check_and_use_plasma_owner()) + if(!check_and_use_plasma_owner()) return @@ -203,42 +518,46 @@ var/dist = get_dist(dancer_user, target_carbon) - if (dist > range) + if(dist > range) to_chat(dancer_user, SPAN_WARNING("[target_carbon] is too far away!")) return - if (dist > 1) - var/turf/targetTurf = get_step(dancer_user, get_dir(dancer_user, target_carbon)) - if (targetTurf.density) - to_chat(dancer_user, SPAN_WARNING("We can't attack through [targetTurf]!")) + if(dist > 1) + var/turf/target_turf = get_step(dancer_user, get_dir(dancer_user, target_carbon)) + if(target_turf.density) + to_chat(dancer_user, SPAN_WARNING("We can't attack through [target_turf]!")) return else - for (var/atom/atom_in_turf in targetTurf) - if (atom_in_turf.density && !atom_in_turf.throwpass && !istype(atom_in_turf, /obj/structure/barricade) && !istype(atom_in_turf, /mob/living)) + for(var/atom/atom_in_turf in target_turf) + if(atom_in_turf.density && !atom_in_turf.throwpass && !istype(atom_in_turf, /obj/structure/barricade) && !istype(atom_in_turf, /mob/living)) to_chat(dancer_user, SPAN_WARNING("We can't attack through [atom_in_turf]!")) return - - // Hmm today I will kill a marine while looking away from them dancer_user.face_atom(target_carbon) dancer_user.flick_attack_overlay(target_carbon, "disarm") var/buffed = FALSE - var/datum/effects/dancer_tag/dancer_tag_effect = locate() in target_carbon.effects_list + var/datum/effects/dancer_tag/normal/dancer_tag_effect = locate() in target_carbon.effects_list + var/datum/effects/dancer_tag/spread/tag_spread = locate() in target_carbon.effects_list - if (dancer_tag_effect) + if(tag_spread) + buffed = TRUE + qdel(tag_spread) + apply_cooldown_override() + + if(dancer_tag_effect) buffed = TRUE qdel(dancer_tag_effect) - if (!buffed) + if(!buffed) new /datum/effects/xeno_slow(target_carbon, dancer_user, null, null, get_xeno_stun_duration(target_carbon, slow_duration)) var/stun_duration = stun_duration_default var/daze_duration = 0 - if (buffed) + if(buffed) stun_duration = stun_duration_buffed daze_duration = daze_duration_buffed @@ -260,7 +579,7 @@ dancer_user.spin_circle() dancer_user.emote("tail") to_chat(target_carbon, SPAN_XENOHIGHDANGER("You are swept off your feet by [dancer_user]!")) - if (daze_duration > 0) + if(daze_duration > 0) target_carbon.apply_effect(daze_duration, DAZE) playsound(dancer_user, 'sound/effects/hit_kick.ogg', 75, 1) diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index ea4cc7281074..a1a2bda585dd 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -892,21 +892,43 @@ else return FALSE -/mob/living/carbon/xenomorph/get_projectile_hit_chance(obj/projectile/P) +/mob/living/carbon/xenomorph/get_projectile_hit_chance(obj/projectile/bullet) . = ..() if(.) - var/ammo_flags = P.ammo.flags_ammo_behavior | P.projectile_override_flags - if(SEND_SIGNAL(P, COMSIG_BULLET_CHECK_MOB_SKIPPING, src) & COMPONENT_SKIP_MOB\ - || P.runtime_iff_group && get_target_lock(P.runtime_iff_group)) + var/ammo_flags = bullet.ammo.flags_ammo_behavior | bullet.projectile_override_flags + if(SEND_SIGNAL(bullet, COMSIG_BULLET_CHECK_MOB_SKIPPING, src) & COMPONENT_SKIP_MOB\ + || bullet.runtime_iff_group && get_target_lock(bullet.runtime_iff_group)) return FALSE if(ammo_flags & AMMO_SKIPS_ALIENS) - var/mob/living/carbon/xenomorph/X = P.firer - if(!istype(X)) + var/mob/living/carbon/xenomorph/xeno = bullet.firer + if(!istype(xeno)) return FALSE - if(X.hivenumber == hivenumber) + if(xeno.hivenumber == hivenumber) return FALSE + if(dodge_threshold > 0) + if(body_position == LYING_DOWN || stat == UNCONSCIOUS) + projectiles_counted = 0 + last_projectile_time = 0 + else if(!(ammo_flags & (AMMO_SNIPER | AMMO_ROCKET))) + if(last_projectile_time && world.time - last_projectile_time >= 6 SECONDS) + projectiles_counted = 0 + + projectiles_counted++ + last_projectile_time = world.time + + if(projectiles_counted >= dodge_threshold) + projectiles_counted = 0 + last_projectile_time = 0 + + xeno_jitter(5 DECISECONDS) + if(bullet.ammo.sound_miss) + playsound_client(client, bullet.ammo.sound_miss, get_turf(src), 75, TRUE) + visible_message(SPAN_AVOIDHARM("The [src] darts aside, evading [bullet]!"), + SPAN_AVOIDHARM("You react fast, and [bullet] narrowly misses you!"), null, 4, CHAT_TYPE_TAKING_HIT) + return FALSE + if(mob_size == MOB_SIZE_SMALL) . -= 10 else if(mob_size >= MOB_SIZE_BIG) diff --git a/icons/mob/hud/actions_xeno.dmi b/icons/mob/hud/actions_xeno.dmi index e2427acb5472..4409d45a1470 100644 Binary files a/icons/mob/hud/actions_xeno.dmi and b/icons/mob/hud/actions_xeno.dmi differ diff --git a/icons/mob/hud/hud.dmi b/icons/mob/hud/hud.dmi index b04912d17a23..4a91d96e84b4 100644 Binary files a/icons/mob/hud/hud.dmi and b/icons/mob/hud/hud.dmi differ