Merge remote-tracking branch 'upstream/master' into embed-changes
This commit is contained in:
@@ -176,9 +176,6 @@
|
||||
SSticker.queue_delay = 4
|
||||
qdel(src)
|
||||
|
||||
if(!ready && href_list["preference"])
|
||||
if(client)
|
||||
client.prefs.process_link(src, href_list)
|
||||
else if(!href_list["late_join"])
|
||||
new_player_panel()
|
||||
|
||||
@@ -582,6 +579,12 @@
|
||||
qdel(src)
|
||||
|
||||
/mob/dead/new_player/proc/ViewManifest()
|
||||
if(!client)
|
||||
return
|
||||
if(world.time < client.crew_manifest_delay)
|
||||
return
|
||||
client.crew_manifest_delay = world.time + (1 SECONDS)
|
||||
|
||||
var/dat = "<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'></head><body>"
|
||||
dat += "<h4>Crew Manifest</h4>"
|
||||
dat += GLOB.data_core.get_manifest(OOC = 1)
|
||||
|
||||
@@ -717,6 +717,12 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
|
||||
set name = "View Crew Manifest"
|
||||
set category = "Ghost"
|
||||
|
||||
if(!client)
|
||||
return
|
||||
if(world.time < client.crew_manifest_delay)
|
||||
return
|
||||
client.crew_manifest_delay = world.time + (1 SECONDS)
|
||||
|
||||
var/dat
|
||||
dat += "<h4>Crew Manifest</h4>"
|
||||
dat += GLOB.data_core.get_manifest()
|
||||
|
||||
@@ -333,6 +333,7 @@
|
||||
I.moveToNullspace()
|
||||
else
|
||||
I.forceMove(newloc)
|
||||
on_item_dropped(I)
|
||||
if(I.dropped(src) == ITEM_RELOCATED_BY_DROPPED)
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
@@ -77,20 +77,18 @@
|
||||
return TRUE
|
||||
|
||||
/mob/living/carbon/alien/humanoid/get_standard_pixel_y_offset(lying = 0)
|
||||
. = ..()
|
||||
if(leaping)
|
||||
return -32
|
||||
else if(custom_pixel_y_offset)
|
||||
return custom_pixel_y_offset
|
||||
else
|
||||
return initial(pixel_y)
|
||||
. -= 32
|
||||
if(custom_pixel_y_offset)
|
||||
. += custom_pixel_y_offset
|
||||
|
||||
/mob/living/carbon/alien/humanoid/get_standard_pixel_x_offset(lying = 0)
|
||||
. = ..()
|
||||
if(leaping)
|
||||
return -32
|
||||
else if(custom_pixel_x_offset)
|
||||
return custom_pixel_x_offset
|
||||
else
|
||||
return initial(pixel_x)
|
||||
. -= 32
|
||||
if(custom_pixel_x_offset)
|
||||
. += custom_pixel_x_offset
|
||||
|
||||
/mob/living/carbon/alien/humanoid/get_permeability_protection(list/target_zones)
|
||||
return 0.8
|
||||
|
||||
@@ -440,10 +440,9 @@
|
||||
return
|
||||
|
||||
/mob/living/carbon/get_standard_pixel_y_offset(lying = 0)
|
||||
. = ..()
|
||||
if(lying)
|
||||
return -6
|
||||
else
|
||||
return initial(pixel_y)
|
||||
. -= 6
|
||||
|
||||
/mob/living/carbon/proc/accident(obj/item/I)
|
||||
if(!I || (I.item_flags & ABSTRACT) || HAS_TRAIT(I, TRAIT_NODROP))
|
||||
|
||||
@@ -76,11 +76,13 @@
|
||||
visible_message("<span class='danger'>[I] embeds itself in [src]'s [L.name]!</span>","<span class='userdanger'>[I] embeds itself in your [L.name]!</span>")
|
||||
SEND_SIGNAL(src, COMSIG_ADD_MOOD_EVENT, "embedded", /datum/mood_event/embedded)
|
||||
|
||||
/mob/living/carbon/attacked_by(obj/item/I, mob/living/user)
|
||||
var/totitemdamage = pre_attacked_by(I, user)
|
||||
/mob/living/carbon/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1)
|
||||
var/totitemdamage = pre_attacked_by(I, user) * damage_multiplier
|
||||
var/impacting_zone = (user == src)? check_zone(user.zone_selected) : ran_zone(user.zone_selected)
|
||||
if((user != src) && (mob_run_block(I, totitemdamage, "the [I]", ATTACK_TYPE_MELEE, I.armour_penetration, user, impacting_zone, null) & BLOCK_SUCCESS))
|
||||
var/list/block_return = list()
|
||||
if((user != src) && (mob_run_block(I, totitemdamage, "the [I]", ((attackchain_flags & ATTACKCHAIN_PARRY_COUNTERATTACK)? ATTACK_TYPE_PARRY_COUNTERATTACK : NONE) | ATTACK_TYPE_MELEE, I.armour_penetration, user, impacting_zone, block_return) & BLOCK_SUCCESS))
|
||||
return FALSE
|
||||
totitemdamage = block_calculate_resultant_damage(totitemdamage, block_return)
|
||||
var/obj/item/bodypart/affecting = get_bodypart(impacting_zone)
|
||||
if(!affecting) //missing limb? we select the first bodypart (you can never have zero, because of chest)
|
||||
affecting = bodyparts[1]
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
if(istype(J) && (movement_dir || J.stabilizers) && J.allow_thrust(0.01, src))
|
||||
return 1
|
||||
|
||||
/mob/living/carbon/Move(NewLoc, direct)
|
||||
/mob/living/carbon/Moved()
|
||||
. = ..()
|
||||
if(. && (movement_type & FLOATING)) //floating is easy
|
||||
if(HAS_TRAIT(src, TRAIT_NOHUNGER))
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/mob/living/carbon/human/get_blocking_items()
|
||||
. = ..()
|
||||
if(wear_suit)
|
||||
. |= wear_suit
|
||||
if(!.[wear_suit])
|
||||
.[wear_suit] = wear_suit.block_priority
|
||||
if(w_uniform)
|
||||
. |= w_uniform
|
||||
if(!.[w_uniform])
|
||||
.[w_uniform] = w_uniform.block_priority
|
||||
if(wear_neck)
|
||||
. |= wear_neck
|
||||
if(!.[wear_neck])
|
||||
.[wear_neck] = wear_neck.block_priority
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
..()
|
||||
|
||||
|
||||
/mob/living/carbon/human/attacked_by(obj/item/I, mob/living/user)
|
||||
/mob/living/carbon/human/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1)
|
||||
if(!I || !user)
|
||||
return 0
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
SSblackbox.record_feedback("tally", "zone_targeted", 1, target_area)
|
||||
|
||||
// the attacked_by code varies among species
|
||||
return dna.species.spec_attacked_by(I, user, affecting, a_intent, src)
|
||||
return dna.species.spec_attacked_by(I, user, affecting, a_intent, src, attackchain_flags, damage_multiplier)
|
||||
|
||||
/mob/living/carbon/human/attack_hulk(mob/living/carbon/human/user, does_attack_animation = FALSE)
|
||||
if(user.a_intent == INTENT_HARM)
|
||||
@@ -209,7 +209,7 @@
|
||||
/mob/living/carbon/human/attack_animal(mob/living/simple_animal/M)
|
||||
. = ..()
|
||||
if(.)
|
||||
var/damage = rand(M.melee_damage_lower, M.melee_damage_upper)
|
||||
var/damage = .
|
||||
var/dam_zone = dismembering_strike(M, pick(BODY_ZONE_CHEST, BODY_ZONE_PRECISE_L_HAND, BODY_ZONE_PRECISE_R_HAND, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG))
|
||||
if(!dam_zone) //Dismemberment successful
|
||||
return TRUE
|
||||
|
||||
@@ -1700,12 +1700,14 @@ GLOBAL_LIST_EMPTY(roundstart_race_names)
|
||||
if("disarm")
|
||||
disarm(M, H, attacker_style)
|
||||
|
||||
/datum/species/proc/spec_attacked_by(obj/item/I, mob/living/user, obj/item/bodypart/affecting, intent, mob/living/carbon/human/H)
|
||||
var/totitemdamage = H.pre_attacked_by(I, user)
|
||||
/datum/species/proc/spec_attacked_by(obj/item/I, mob/living/user, obj/item/bodypart/affecting, intent, mob/living/carbon/human/H, attackchain_flags = NONE, damage_multiplier = 1)
|
||||
var/totitemdamage = H.pre_attacked_by(I, user) * damage_multiplier
|
||||
// Allows you to put in item-specific reactions based on species
|
||||
if(user != H)
|
||||
if(H.mob_run_block(I, totitemdamage, "the [I.name]", ATTACK_TYPE_MELEE, I.armour_penetration, user, affecting.body_zone, null) & BLOCK_SUCCESS)
|
||||
var/list/block_return = list()
|
||||
if(H.mob_run_block(I, totitemdamage, "the [I.name]", ((attackchain_flags & ATTACKCHAIN_PARRY_COUNTERATTACK)? ATTACK_TYPE_PARRY_COUNTERATTACK : NONE) | ATTACK_TYPE_MELEE, I.armour_penetration, user, affecting.body_zone, block_return) & BLOCK_SUCCESS)
|
||||
return 0
|
||||
totitemdamage = block_calculate_resultant_damage(totitemdamage, block_return)
|
||||
if(H.check_martial_melee_block())
|
||||
H.visible_message("<span class='warning'>[H] blocks [I]!</span>")
|
||||
return 0
|
||||
@@ -1720,6 +1722,7 @@ GLOBAL_LIST_EMPTY(roundstart_race_names)
|
||||
var/armor_block = H.run_armor_check(affecting, "melee", "<span class='notice'>Your armor has protected your [hit_area].</span>", "<span class='notice'>Your armor has softened a hit to your [hit_area].</span>",I.armour_penetration)
|
||||
armor_block = min(90,armor_block) //cap damage reduction at 90%
|
||||
var/Iforce = I.force //to avoid runtimes on the forcesay checks at the bottom. Some items might delete themselves if you drop them. (stunning yourself, ninja swords)
|
||||
|
||||
var/weakness = H.check_weakness(I, user)
|
||||
apply_damage(totitemdamage * weakness, I.damtype, def_zone, armor_block, H) //CIT CHANGE - replaces I.force with totitemdamage
|
||||
|
||||
|
||||
@@ -459,8 +459,18 @@
|
||||
|
||||
/datum/action/innate/slime_change/proc/change_form()
|
||||
var/mob/living/carbon/human/H = owner
|
||||
var/select_alteration = input(owner, "Select what part of your form to alter", "Form Alteration", "cancel") in list("Hair Style", "Genitals", "Tail", "Snout", "Markings", "Ears", "Taur body", "Penis", "Vagina", "Penis Length", "Breast Size", "Breast Shape", "Cancel")
|
||||
if(select_alteration == "Hair Style")
|
||||
var/select_alteration = input(owner, "Select what part of your form to alter", "Form Alteration", "cancel") in list("Body Color","Hair Style", "Genitals", "Tail", "Snout", "Markings", "Ears", "Taur body", "Penis", "Vagina", "Penis Length", "Breast Size", "Breast Shape", "Cancel")
|
||||
|
||||
if(select_alteration == "Body Color")
|
||||
var/new_color = input(owner, "Choose your skin color:", "Race change","#"+H.dna.features["mcolor"]) as color|null
|
||||
if(new_color)
|
||||
var/temp_hsv = RGBtoHSV(new_color)
|
||||
if(ReadHSV(temp_hsv)[3] >= ReadHSV("#7F7F7F")[3]) // mutantcolors must be bright
|
||||
H.dna.features["mcolor"] = sanitize_hexcolor(new_color)
|
||||
H.update_body()
|
||||
else
|
||||
to_chat(H, "<span class='notice'>Invalid color. Your color is not bright enough.</span>")
|
||||
else if(select_alteration == "Hair Style")
|
||||
if(H.gender == MALE)
|
||||
var/new_style = input(owner, "Select a facial hair style", "Hair Alterations") as null|anything in GLOB.facial_hair_styles_list
|
||||
if(new_style)
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
/mob/living/carbon/monkey/attack_animal(mob/living/simple_animal/M)
|
||||
. = ..()
|
||||
if(.)
|
||||
var/damage = rand(M.melee_damage_lower, M.melee_damage_upper)
|
||||
var/damage = .
|
||||
var/dam_zone = dismembering_strike(M, pick(BODY_ZONE_CHEST, BODY_ZONE_PRECISE_L_HAND, BODY_ZONE_PRECISE_R_HAND, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG))
|
||||
if(!dam_zone) //Dismemberment successful
|
||||
return TRUE
|
||||
|
||||
@@ -78,8 +78,6 @@
|
||||
apply_damage(brain, BRAIN, def_zone, blocked)
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
/mob/living/proc/apply_effect(effect = 0,effecttype = EFFECT_STUN, blocked = FALSE, knockdown_stamoverride, knockdown_stammax)
|
||||
var/hit_percent = (100-blocked)/100
|
||||
if(!effect || (hit_percent <= 0))
|
||||
@@ -108,7 +106,7 @@
|
||||
return 1
|
||||
|
||||
|
||||
/mob/living/proc/apply_effects(stun = 0, knockdown = 0, unconscious = 0, irradiate = 0, slur = 0, stutter = 0, eyeblur = 0, drowsy = 0, blocked = FALSE, stamina = 0, jitter = 0, kd_stamoverride, kd_stammax)
|
||||
/mob/living/proc/apply_effects(stun = 0, knockdown = 0, unconscious = 0, irradiate = 0, slur = 0, stutter = 0, eyeblur = 0, drowsy = 0, blocked = 0, stamina = 0, jitter = 0, kd_stamoverride, kd_stammax)
|
||||
if(blocked >= 100)
|
||||
return BULLET_ACT_BLOCK
|
||||
if(stun)
|
||||
|
||||
@@ -64,6 +64,8 @@
|
||||
|
||||
handle_gravity()
|
||||
|
||||
handle_block_parry(seconds)
|
||||
|
||||
if(machine)
|
||||
machine.check_eye(src)
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
med_hud_set_status()
|
||||
|
||||
/mob/living/Destroy()
|
||||
end_parry_sequence()
|
||||
stop_active_blocking()
|
||||
if(LAZYLEN(status_effects))
|
||||
for(var/s in status_effects)
|
||||
var/datum/status_effect/S = s
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
// Active directional block system. Shared code is in [living_blocking_parrying.dm]
|
||||
/mob/living/proc/stop_active_blocking(was_forced = FALSE)
|
||||
if(!(combat_flags & (COMBAT_FLAG_ACTIVE_BLOCK_STARTING | COMBAT_FLAG_ACTIVE_BLOCKING)))
|
||||
return FALSE
|
||||
var/obj/item/I = active_block_item
|
||||
combat_flags &= ~(COMBAT_FLAG_ACTIVE_BLOCKING | COMBAT_FLAG_ACTIVE_BLOCK_STARTING)
|
||||
active_block_effect_end()
|
||||
active_block_item = null
|
||||
REMOVE_TRAIT(src, TRAIT_MOBILITY_NOUSE, ACTIVE_BLOCK_TRAIT)
|
||||
REMOVE_TRAIT(src, TRAIT_SPRINT_LOCKED, ACTIVE_BLOCK_TRAIT)
|
||||
remove_movespeed_modifier(/datum/movespeed_modifier/active_block)
|
||||
var/datum/block_parry_data/data = I.get_block_parry_data()
|
||||
if(timeToNextMove() < data.block_end_click_cd_add)
|
||||
changeNext_move(data.block_end_click_cd_add)
|
||||
return TRUE
|
||||
|
||||
/mob/living/proc/start_active_blocking(obj/item/I)
|
||||
if(combat_flags & (COMBAT_FLAG_ACTIVE_BLOCK_STARTING | COMBAT_FLAG_ACTIVE_BLOCKING))
|
||||
return FALSE
|
||||
if(!(I in held_items))
|
||||
return FALSE
|
||||
var/datum/block_parry_data/data = I.get_block_parry_data()
|
||||
if(!istype(data)) //Typecheck because if an admin/coder screws up varediting or something we do not want someone being broken forever, the CRASH logs feedback so we know what happened.
|
||||
CRASH("start_active_blocking called with an item with no valid data: [I] --> [I.block_parry_data]!")
|
||||
combat_flags |= COMBAT_FLAG_ACTIVE_BLOCKING
|
||||
active_block_item = I
|
||||
if(data.block_lock_attacking)
|
||||
ADD_TRAIT(src, TRAIT_MOBILITY_NOUSE, ACTIVE_BLOCK_TRAIT) //probably should be something else at some point
|
||||
if(data.block_lock_sprinting)
|
||||
ADD_TRAIT(src, TRAIT_SPRINT_LOCKED, ACTIVE_BLOCK_TRAIT)
|
||||
add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/active_block, multiplicative_slowdown = data.block_slowdown)
|
||||
active_block_effect_start()
|
||||
return TRUE
|
||||
|
||||
/// Visual effect setup for starting a directional block
|
||||
/mob/living/proc/active_block_effect_start()
|
||||
visible_message("<span class='warning'>[src] raises their [active_block_item], dropping into a defensive stance!</span>")
|
||||
animate(src, pixel_x = get_standard_pixel_x_offset(), pixel_y = get_standard_pixel_y_offset(), time = 2.5, FALSE, SINE_EASING | EASE_OUT)
|
||||
|
||||
/// Visual effect cleanup for starting a directional block
|
||||
/mob/living/proc/active_block_effect_end()
|
||||
visible_message("<span class='warning'>[src] lowers their [active_block_item].</span>")
|
||||
animate(src, pixel_x = get_standard_pixel_x_offset(), pixel_y = get_standard_pixel_y_offset(), time = 2.5, FALSE, SINE_EASING | EASE_IN)
|
||||
|
||||
/mob/living/proc/continue_starting_active_block()
|
||||
if(SEND_SIGNAL(src, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_INACTIVE))
|
||||
return DO_AFTER_STOP
|
||||
return (combat_flags & COMBAT_FLAG_ACTIVE_BLOCK_STARTING)? DO_AFTER_CONTINUE : DO_AFTER_STOP
|
||||
|
||||
/mob/living/get_standard_pixel_x_offset()
|
||||
. = ..()
|
||||
if(combat_flags & (COMBAT_FLAG_ACTIVE_BLOCK_STARTING | COMBAT_FLAG_ACTIVE_BLOCKING))
|
||||
if(dir & EAST)
|
||||
. += 8
|
||||
if(dir & WEST)
|
||||
. -= 8
|
||||
|
||||
/mob/living/get_standard_pixel_y_offset()
|
||||
. = ..()
|
||||
if(combat_flags & (COMBAT_FLAG_ACTIVE_BLOCK_STARTING | COMBAT_FLAG_ACTIVE_BLOCKING))
|
||||
if(dir & NORTH)
|
||||
. += 8
|
||||
if(dir & SOUTH)
|
||||
. -= 8
|
||||
|
||||
/**
|
||||
* Proc called by keybindings to toggle active blocking.
|
||||
*/
|
||||
/mob/living/proc/keybind_toggle_active_blocking()
|
||||
if(combat_flags & (COMBAT_FLAG_ACTIVE_BLOCK_STARTING | COMBAT_FLAG_ACTIVE_BLOCKING))
|
||||
return keybind_stop_active_blocking()
|
||||
else
|
||||
return keybind_start_active_blocking()
|
||||
|
||||
/**
|
||||
* Proc called by keybindings to start active blocking.
|
||||
*/
|
||||
/mob/living/proc/keybind_start_active_blocking()
|
||||
if(combat_flags & (COMBAT_FLAG_ACTIVE_BLOCK_STARTING | COMBAT_FLAG_ACTIVE_BLOCKING))
|
||||
return FALSE
|
||||
if(!(combat_flags & COMBAT_FLAG_BLOCK_CAPABLE))
|
||||
to_chat(src, "<span class='warning'>You're not something that can actively block.</span>")
|
||||
return FALSE
|
||||
// QOL: Instead of trying to just block with held item, grab first available item.
|
||||
var/obj/item/I = find_active_block_item()
|
||||
if(!I)
|
||||
to_chat(src, "<span class='warning'>You can't block with your bare hands!</span>")
|
||||
return
|
||||
if(!I.can_active_block())
|
||||
to_chat(src, "<span class='warning'>[I] is either not capable of being used to actively block, or is not currently in a state that can! (Try wielding it if it's twohanded, for example.)</span>")
|
||||
return
|
||||
// QOL: Attempt to toggle on combat mode if it isn't already
|
||||
SEND_SIGNAL(src, COMSIG_ENABLE_COMBAT_MODE)
|
||||
if(!SEND_SIGNAL(src, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_ACTIVE))
|
||||
to_chat(src, "<span class='warning'>You must be in combat mode to actively block!</span>")
|
||||
return FALSE
|
||||
var/datum/block_parry_data/data = I.get_block_parry_data()
|
||||
var/delay = data.block_start_delay
|
||||
combat_flags |= COMBAT_FLAG_ACTIVE_BLOCK_STARTING
|
||||
animate(src, pixel_x = get_standard_pixel_x_offset(), pixel_y = get_standard_pixel_y_offset(), time = delay, FALSE, SINE_EASING | EASE_IN)
|
||||
if(!do_after_advanced(src, delay, src, DO_AFTER_REQUIRES_USER_ON_TURF|DO_AFTER_NO_COEFFICIENT, CALLBACK(src, .proc/continue_starting_active_block), MOBILITY_USE, null, null, I))
|
||||
to_chat(src, "<span class='warning'>You fail to raise [I].</span>")
|
||||
combat_flags &= ~(COMBAT_FLAG_ACTIVE_BLOCK_STARTING)
|
||||
animate(src, pixel_x = get_standard_pixel_x_offset(), pixel_y = get_standard_pixel_y_offset(), time = 2.5, FALSE, SINE_EASING | EASE_IN, ANIMATION_END_NOW)
|
||||
return
|
||||
combat_flags &= ~(COMBAT_FLAG_ACTIVE_BLOCK_STARTING)
|
||||
start_active_blocking(I)
|
||||
|
||||
/**
|
||||
* Gets the first item we can that can block, but if that fails, default to active held item.COMSIG_ENABLE_COMBAT_MODE
|
||||
*/
|
||||
/mob/living/proc/find_active_block_item()
|
||||
var/obj/item/held = get_active_held_item()
|
||||
if(!held?.can_active_block())
|
||||
for(var/obj/item/I in held_items - held)
|
||||
if(I.can_active_block())
|
||||
return I
|
||||
return held
|
||||
|
||||
/**
|
||||
* Proc called by keybindings to stop active blocking.
|
||||
*/
|
||||
/mob/living/proc/keybind_stop_active_blocking()
|
||||
combat_flags &= ~(COMBAT_FLAG_ACTIVE_BLOCK_STARTING)
|
||||
if(combat_flags & COMBAT_FLAG_ACTIVE_BLOCKING)
|
||||
stop_active_blocking(FALSE)
|
||||
return TRUE
|
||||
|
||||
/**
|
||||
* Returns if we can actively block.
|
||||
*/
|
||||
/obj/item/proc/can_active_block()
|
||||
return block_parry_data && (item_flags & ITEM_CAN_BLOCK)
|
||||
|
||||
/**
|
||||
* Calculates FINAL ATTACK DAMAGE after mitigation
|
||||
*/
|
||||
/obj/item/proc/active_block_calculate_final_damage(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return)
|
||||
var/datum/block_parry_data/data = get_block_parry_data()
|
||||
var/absorption = data.attack_type_list_scan(data.block_damage_absorption_override, attack_type)
|
||||
var/efficiency = data.attack_type_list_scan(data.block_damage_multiplier_override, attack_type)
|
||||
var/limit = data.attack_type_list_scan(data.block_damage_limit_override, attack_type)
|
||||
// must use isnulls to handle 0's.
|
||||
if(isnull(absorption))
|
||||
absorption = data.block_damage_absorption
|
||||
if(isnull(efficiency))
|
||||
efficiency = data.block_damage_multiplier
|
||||
if(isnull(limit))
|
||||
limit = data.block_damage_limit
|
||||
// now we calculate damage to reduce.
|
||||
var/final_damage = 0
|
||||
// apply limit
|
||||
if(damage > limit) //clamp and apply overrun
|
||||
final_damage += (damage - limit)
|
||||
damage = limit
|
||||
// apply absorption
|
||||
damage -= min(absorption, damage) //this way if damage is less than absorption it 0's properly.
|
||||
// apply multiplier to remaining
|
||||
final_damage += (damage * efficiency)
|
||||
return final_damage
|
||||
|
||||
/// Amount of stamina from damage blocked. Note that the damage argument is damage_blocked.
|
||||
/obj/item/proc/active_block_stamina_cost(mob/living/owner, atom/object, damage_blocked, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return)
|
||||
var/datum/block_parry_data/data = get_block_parry_data()
|
||||
var/efficiency = data.attack_type_list_scan(data.block_stamina_efficiency_override, attack_type)
|
||||
if(isnull(efficiency))
|
||||
efficiency = data.block_stamina_efficiency
|
||||
var/multiplier = 1
|
||||
if(!CHECK_MOBILITY(owner, MOBILITY_STAND))
|
||||
multiplier = data.attack_type_list_scan(data.block_resting_stamina_penalty_multiplier_override, attack_type)
|
||||
if(isnull(multiplier))
|
||||
multiplier = data.block_resting_stamina_penalty_multiplier
|
||||
return (damage_blocked / efficiency) * multiplier
|
||||
|
||||
/// Apply the stamina damage to our user, notice how damage argument is stamina_amount.
|
||||
/obj/item/proc/active_block_do_stamina_damage(mob/living/owner, atom/object, stamina_amount, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return)
|
||||
var/datum/block_parry_data/data = get_block_parry_data()
|
||||
if(iscarbon(owner))
|
||||
var/mob/living/carbon/C = owner
|
||||
var/held_index = C.get_held_index_of_item(src)
|
||||
var/obj/item/bodypart/BP = C.hand_bodyparts[held_index]
|
||||
if(!BP?.body_zone)
|
||||
return C.adjustStaminaLossBuffered(stamina_amount) //nah
|
||||
var/zone = BP.body_zone
|
||||
var/stamina_to_zone = data.block_stamina_limb_ratio * stamina_amount
|
||||
var/stamina_to_chest = stamina_amount - stamina_to_zone
|
||||
var/stamina_buffered = stamina_to_chest * data.block_stamina_buffer_ratio
|
||||
stamina_to_chest -= stamina_buffered
|
||||
C.apply_damage(stamina_to_zone, STAMINA, zone)
|
||||
C.apply_damage(stamina_to_chest, STAMINA, BODY_ZONE_CHEST)
|
||||
C.adjustStaminaLossBuffered(stamina_buffered)
|
||||
else
|
||||
owner.adjustStaminaLossBuffered(stamina_amount)
|
||||
|
||||
/obj/item/proc/on_active_block(mob/living/owner, atom/object, damage, damage_blocked, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return, override_direction)
|
||||
return
|
||||
|
||||
/obj/item/proc/active_block(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return, override_direction)
|
||||
if(!can_active_block())
|
||||
return BLOCK_NONE
|
||||
var/datum/block_parry_data/data = get_block_parry_data()
|
||||
if(attack_type && !(attack_type & data.can_block_attack_types))
|
||||
return BLOCK_NONE
|
||||
var/incoming_direction
|
||||
if(isnull(override_direction))
|
||||
if(istype(object, /obj/item/projectile))
|
||||
var/obj/item/projectile/P = object
|
||||
incoming_direction = angle2dir(P.Angle)
|
||||
else
|
||||
incoming_direction = get_dir(get_turf(attacker) || get_turf(object), src)
|
||||
if(!CHECK_MOBILITY(owner, MOBILITY_STAND) && !(data.block_resting_attack_types_anydir & attack_type) && (!(data.block_resting_attack_types_directional & attack_type) || !can_block_direction(owner.dir, incoming_direction)))
|
||||
return BLOCK_NONE
|
||||
else if(!can_block_direction(owner.dir, incoming_direction))
|
||||
return BLOCK_NONE
|
||||
block_return[BLOCK_RETURN_ACTIVE_BLOCK] = TRUE
|
||||
var/final_damage = active_block_calculate_final_damage(owner, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return)
|
||||
var/damage_blocked = damage - final_damage
|
||||
var/stamina_cost = active_block_stamina_cost(owner, object, damage_blocked, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return)
|
||||
active_block_do_stamina_damage(owner, object, stamina_cost, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return)
|
||||
block_return[BLOCK_RETURN_ACTIVE_BLOCK_DAMAGE_MITIGATED] = damage - final_damage
|
||||
block_return[BLOCK_RETURN_SET_DAMAGE_TO] = final_damage
|
||||
. = BLOCK_SHOULD_CHANGE_DAMAGE
|
||||
if((final_damage <= 0) || (damage <= 0))
|
||||
. |= BLOCK_SUCCESS //full block
|
||||
owner.visible_message("<span class='warning'>[owner] blocks \the [attack_text] with [src]!</span>")
|
||||
else
|
||||
owner.visible_message("<span class='warning'>[owner] dampens \the [attack_text] with [src]!</span>")
|
||||
block_return[BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE] = data.block_projectile_mitigation
|
||||
if(length(data.block_sounds))
|
||||
playsound(loc, pickweight(data.block_sounds), 75, TRUE)
|
||||
on_active_block(owner, object, damage, damage_blocked, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return, override_direction)
|
||||
|
||||
/obj/item/proc/check_active_block(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return)
|
||||
if(!can_active_block())
|
||||
return
|
||||
var/incoming_direction = get_dir(get_turf(attacker) || get_turf(object), src)
|
||||
if(!can_block_direction(owner.dir, incoming_direction))
|
||||
return
|
||||
block_return[BLOCK_RETURN_ACTIVE_BLOCK] = TRUE
|
||||
block_return[BLOCK_RETURN_ACTIVE_BLOCK_DAMAGE_MITIGATED] = damage - active_block_calculate_final_damage(owner, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return)
|
||||
|
||||
/**
|
||||
* Gets the block direction bitflags of what we can block.
|
||||
*/
|
||||
/obj/item/proc/blockable_directions()
|
||||
var/datum/block_parry_data/data = get_block_parry_data()
|
||||
return data.can_block_directions
|
||||
|
||||
/**
|
||||
* Checks if we can block from a specific direction from our direction.
|
||||
*
|
||||
* @params
|
||||
* * our_dir - our direction.
|
||||
* * their_dir - their direction. Must be a single direction, or NONE for an attack from the same tile. This is incoming direction.
|
||||
*/
|
||||
/obj/item/proc/can_block_direction(our_dir, their_dir)
|
||||
their_dir = turn(their_dir, 180)
|
||||
if(our_dir != NORTH)
|
||||
var/turn_angle = dir2angle(our_dir)
|
||||
// dir2angle(), ss13 proc is clockwise so dir2angle(EAST) == 90
|
||||
// turn(), byond proc is counterclockwise so turn(NORTH, 90) == WEST
|
||||
their_dir = turn(their_dir, turn_angle)
|
||||
return (DIR2BLOCKDIR(their_dir) & blockable_directions())
|
||||
|
||||
/**
|
||||
* can_block_direction but for "compound" directions to check all of them and return the number of directions that were blocked.
|
||||
*
|
||||
* @params
|
||||
* * our_dir - our direction.
|
||||
* * their_dirs - list of their directions as we cannot use bitfields here.
|
||||
*/
|
||||
/obj/item/proc/can_block_directions_multiple(our_dir, list/their_dirs)
|
||||
. = FALSE
|
||||
for(var/i in their_dirs)
|
||||
. |= can_block_direction(our_dir, i)
|
||||
@@ -0,0 +1,332 @@
|
||||
// Active parry system goes in here.
|
||||
/**
|
||||
* Determines if we can actively parry.
|
||||
*/
|
||||
/obj/item/proc/can_active_parry()
|
||||
return block_parry_data && (item_flags & ITEM_CAN_PARRY)
|
||||
|
||||
/**
|
||||
* Called from keybindings.
|
||||
*/
|
||||
/mob/living/proc/keybind_parry()
|
||||
initiate_parry_sequence()
|
||||
|
||||
/**
|
||||
* Initiates a parrying sequence.
|
||||
*/
|
||||
/mob/living/proc/initiate_parry_sequence()
|
||||
if(parrying)
|
||||
return // already parrying
|
||||
if(!(combat_flags & COMBAT_FLAG_PARRY_CAPABLE))
|
||||
to_chat(src, "<span class='warning'>You are not something that can parry attacks.</span>")
|
||||
return
|
||||
// Prioritize item, then martial art, then unarmed.
|
||||
// yanderedev else if time
|
||||
var/obj/item/using_item = get_active_held_item()
|
||||
var/datum/block_parry_data/data
|
||||
var/method
|
||||
if(using_item?.can_active_parry())
|
||||
data = using_item.block_parry_data
|
||||
method = ITEM_PARRY
|
||||
else if(mind?.martial_art?.can_martial_parry)
|
||||
data = mind.martial_art.block_parry_data
|
||||
method = MARTIAL_PARRY
|
||||
else if(combat_flags & COMBAT_FLAG_UNARMED_PARRY)
|
||||
data = block_parry_data
|
||||
method = UNARMED_PARRY
|
||||
else
|
||||
// QOL: If none of the above work, try to find another item.
|
||||
var/obj/item/backup = find_backup_parry_item()
|
||||
if(!backup)
|
||||
to_chat(src, "<span class='warning'>You have nothing to parry with!</span>")
|
||||
return FALSE
|
||||
data = backup.block_parry_data
|
||||
using_item = backup
|
||||
method = ITEM_PARRY
|
||||
//QOL: Try to enable combat mode if it isn't already
|
||||
SEND_SIGNAL(src, COMSIG_ENABLE_COMBAT_MODE)
|
||||
if(!SEND_SIGNAL(src, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_ACTIVE))
|
||||
to_chat(src, "<span class='warning'>You must be in combat mode to parry!</span>")
|
||||
return FALSE
|
||||
data = return_block_parry_datum(data)
|
||||
var/full_parry_duration = data.parry_time_windup + data.parry_time_active + data.parry_time_spindown
|
||||
// no system in place to "fallback" if out of the 3 the top priority one can't parry due to constraints but something else can.
|
||||
// can always implement it later, whatever.
|
||||
if((data.parry_respect_clickdelay && (next_move > world.time)) || ((parry_end_time_last + data.parry_cooldown) > world.time))
|
||||
to_chat(src, "<span class='warning'>You are not ready to parry (again)!</span>")
|
||||
return
|
||||
// Point of no return, make sure everything is set.
|
||||
parrying = method
|
||||
if(method == ITEM_PARRY)
|
||||
active_parry_item = using_item
|
||||
adjustStaminaLossBuffered(data.parry_stamina_cost)
|
||||
parry_start_time = world.time
|
||||
successful_parries = list()
|
||||
addtimer(CALLBACK(src, .proc/end_parry_sequence), full_parry_duration)
|
||||
if(data.parry_flags & PARRY_LOCK_ATTACKING)
|
||||
ADD_TRAIT(src, TRAIT_MOBILITY_NOUSE, ACTIVE_PARRY_TRAIT)
|
||||
if(data.parry_flags & PARRY_LOCK_SPRINTING)
|
||||
ADD_TRAIT(src, TRAIT_SPRINT_LOCKED, ACTIVE_PARRY_TRAIT)
|
||||
handle_parry_starting_effects(data)
|
||||
return TRUE
|
||||
|
||||
/**
|
||||
* Tries to find a backup parry item.
|
||||
* Does not look at active held item.
|
||||
*/
|
||||
/mob/living/proc/find_backup_parry_item()
|
||||
for(var/obj/item/I in held_items - get_active_held_item())
|
||||
if(I.can_active_parry())
|
||||
return I
|
||||
|
||||
/**
|
||||
* Called via timer when the parry sequence ends.
|
||||
*/
|
||||
/mob/living/proc/end_parry_sequence()
|
||||
if(!parrying)
|
||||
return
|
||||
REMOVE_TRAIT(src, TRAIT_MOBILITY_NOUSE, ACTIVE_PARRY_TRAIT)
|
||||
REMOVE_TRAIT(src, TRAIT_SPRINT_LOCKED, ACTIVE_PARRY_TRAIT)
|
||||
if(parry_visual_effect)
|
||||
QDEL_NULL(parry_visual_effect)
|
||||
var/datum/block_parry_data/data = get_parry_data()
|
||||
var/list/effect_text = list()
|
||||
var/successful = FALSE
|
||||
for(var/efficiency in successful_parries)
|
||||
if(efficiency >= data.parry_efficiency_considered_successful)
|
||||
successful = TRUE
|
||||
break
|
||||
if(!successful) // didn't parry anything successfully
|
||||
if(data.parry_failed_stagger_duration)
|
||||
Stagger(data.parry_failed_stagger_duration)
|
||||
effect_text += "staggering themselves"
|
||||
if(data.parry_failed_clickcd_duration)
|
||||
changeNext_move(data.parry_failed_clickcd_duration)
|
||||
effect_text += "throwing themselves off balance"
|
||||
handle_parry_ending_effects(data, effect_text)
|
||||
parrying = NOT_PARRYING
|
||||
parry_start_time = 0
|
||||
parry_end_time_last = world.time
|
||||
successful_parries = null
|
||||
|
||||
/**
|
||||
* Handles starting effects for parrying.
|
||||
*/
|
||||
/mob/living/proc/handle_parry_starting_effects(datum/block_parry_data/data)
|
||||
playsound(src, data.parry_start_sound, 75, 1)
|
||||
parry_visual_effect = new /obj/effect/abstract/parry/main(null, TRUE, src, data.parry_effect_icon_state, data.parry_time_windup_visual_override || data.parry_time_windup, data.parry_time_active_visual_override || data.parry_time_active, data.parry_time_spindown_visual_override || data.parry_time_spindown)
|
||||
switch(parrying)
|
||||
if(ITEM_PARRY)
|
||||
visible_message("<span class='warning'>[src] swings [active_parry_item]!</span>")
|
||||
else
|
||||
visible_message("<span class='warning'>[src] rushes forwards!</span>")
|
||||
|
||||
/**
|
||||
* Handles ending effects for parrying.
|
||||
*/
|
||||
/mob/living/proc/handle_parry_ending_effects(datum/block_parry_data/data, list/failed_effect_text)
|
||||
if(length(successful_parries))
|
||||
return
|
||||
visible_message("<span class='warning'>[src] fails to connect their parry[failed_effect_text? ", [english_list(failed_effect_text)]" : ""]!")
|
||||
|
||||
/**
|
||||
* Gets this item's datum/block_parry_data
|
||||
*/
|
||||
/obj/item/proc/get_block_parry_data()
|
||||
return return_block_parry_datum(block_parry_data)
|
||||
|
||||
//Stubs.
|
||||
|
||||
/**
|
||||
* Called when an attack is parried using this, whether or not the parry was successful.
|
||||
*/
|
||||
/obj/item/proc/on_active_parry(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return, parry_efficiency, parry_time)
|
||||
|
||||
/**
|
||||
* Called when an attack is parried innately, whether or not the parry was successful.
|
||||
*/
|
||||
/mob/living/proc/on_active_parry(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return, parry_efficiency, parry_time)
|
||||
|
||||
/**
|
||||
* Called when an attack is parried using this, whether or not the parry was successful.
|
||||
*/
|
||||
/datum/martial_art/proc/on_active_parry(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return, parry_efficiency, parry_time)
|
||||
|
||||
/**
|
||||
* Called when an attack is parried and block_parra_data indicates to use a proc to handle counterattack.
|
||||
*/
|
||||
/obj/item/proc/active_parry_reflex_counter(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list, parry_efficiency, list/effect_text)
|
||||
|
||||
/**
|
||||
* Called when an attack is parried and block_parra_data indicates to use a proc to handle counterattack.
|
||||
*/
|
||||
/mob/living/proc/active_parry_reflex_counter(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list, parry_efficiency, list/effect_text)
|
||||
|
||||
/**
|
||||
* Called when an attack is parried and block_parra_data indicates to use a proc to handle counterattack.
|
||||
*/
|
||||
/datum/martial_art/proc/active_parry_reflex_counter(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list, parry_efficiency, list/effect_text)
|
||||
|
||||
/**
|
||||
* Gets the stage of our parry sequence we're currently in.
|
||||
*/
|
||||
/mob/living/proc/get_parry_stage()
|
||||
if(!parrying)
|
||||
return NOT_PARRYING
|
||||
var/datum/block_parry_data/data = get_parry_data()
|
||||
var/windup_end = data.parry_time_windup
|
||||
var/active_end = windup_end + data.parry_time_active
|
||||
var/spindown_end = active_end + data.parry_time_spindown
|
||||
var/current_time = get_parry_time()
|
||||
// Not a switch statement because byond switch statements don't support floats at time of writing with "to" keyword.
|
||||
if(current_time < 0)
|
||||
return NOT_PARRYING
|
||||
else if(current_time < windup_end)
|
||||
return PARRY_WINDUP
|
||||
else if(current_time <= active_end) // this uses <= on purpose, give a slight bit of advantage because time is rounded to world.tick_lag
|
||||
return PARRY_ACTIVE
|
||||
else if(current_time <= spindown_end)
|
||||
return PARRY_SPINDOWN
|
||||
else
|
||||
return NOT_PARRYING
|
||||
|
||||
/**
|
||||
* Gets the current decisecond "frame" of an active parry.
|
||||
*/
|
||||
/mob/living/proc/get_parry_time()
|
||||
return world.time - parry_start_time
|
||||
|
||||
/// same return values as normal blocking, called with absolute highest priority in the block "chain".
|
||||
/mob/living/proc/run_parry(atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list = list())
|
||||
var/stage = get_parry_stage()
|
||||
if(stage != PARRY_ACTIVE)
|
||||
return BLOCK_NONE
|
||||
var/datum/block_parry_data/data = get_parry_data()
|
||||
if(attack_type && (!(attack_type & data.parry_attack_types) || (attack_type & ATTACK_TYPE_PARRY_COUNTERATTACK))) // if this attack is from a parry do not parry it lest we infinite loop.
|
||||
return BLOCK_NONE
|
||||
var/efficiency = data.get_parry_efficiency(attack_type, get_parry_time())
|
||||
switch(parrying)
|
||||
if(ITEM_PARRY)
|
||||
if(!active_parry_item.can_active_parry())
|
||||
return BLOCK_NONE
|
||||
. = active_parry_item.on_active_parry(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency, get_parry_time())
|
||||
if(UNARMED_PARRY)
|
||||
. = on_active_parry(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency, get_parry_time())
|
||||
if(MARTIAL_PARRY)
|
||||
. = mind.martial_art.on_active_parry(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency, get_parry_time())
|
||||
if(!isnull(return_list[BLOCK_RETURN_OVERRIDE_PARRY_EFFICIENCY])) // one of our procs overrode
|
||||
efficiency = return_list[BLOCK_RETURN_OVERRIDE_PARRY_EFFICIENCY]
|
||||
if(efficiency <= 0) // Do not allow automatically handled/standardized parries that increase damage for now.
|
||||
return
|
||||
. |= BLOCK_SHOULD_PARTIAL_MITIGATE
|
||||
if(isnull(return_list[BLOCK_RETURN_MITIGATION_PERCENT])) // if one of the on_active_parry procs overrode. We don't have to worry about interference since parries are the first thing checked in the [do_run_block()] sequence.
|
||||
return_list[BLOCK_RETURN_MITIGATION_PERCENT] = clamp(efficiency, 0, 100) // do not allow > 100% or < 0% for now.
|
||||
if((return_list[BLOCK_RETURN_MITIGATION_PERCENT] >= 100) || (damage <= 0))
|
||||
. |= BLOCK_SUCCESS
|
||||
var/list/effect_text
|
||||
if(efficiency >= data.parry_efficiency_to_counterattack)
|
||||
run_parry_countereffects(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency)
|
||||
if(data.parry_flags & PARRY_DEFAULT_HANDLE_FEEDBACK)
|
||||
handle_parry_feedback(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency, effect_text)
|
||||
successful_parries += efficiency
|
||||
if(length(successful_parries) >= data.parry_max_attacks)
|
||||
end_parry_sequence()
|
||||
|
||||
/mob/living/proc/handle_parry_feedback(atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list = list(), parry_efficiency, list/effect_text)
|
||||
var/datum/block_parry_data/data = get_parry_data()
|
||||
if(data.parry_sounds)
|
||||
playsound(src, pick(data.parry_sounds), 75)
|
||||
visible_message("<span class='danger'>[src] parries \the [attack_text][length(effect_text)? ", [english_list(effect_text)] [attacker]" : ""]!</span>")
|
||||
|
||||
/// Run counterattack if any
|
||||
/mob/living/proc/run_parry_countereffects(atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list = list(), parry_efficiency)
|
||||
if(!isliving(attacker))
|
||||
return
|
||||
var/mob/living/L = attacker
|
||||
var/datum/block_parry_data/data = get_parry_data()
|
||||
var/list/effect_text = list()
|
||||
// Always proc so items can override behavior easily
|
||||
switch(parrying)
|
||||
if(ITEM_PARRY)
|
||||
active_parry_item.active_parry_reflex_counter(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, parry_efficiency, effect_text)
|
||||
if(UNARMED_PARRY)
|
||||
active_parry_reflex_counter(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, parry_efficiency, effect_text)
|
||||
if(MARTIAL_PARRY)
|
||||
mind.martial_art.active_parry_reflex_counter(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, parry_efficiency, effect_text)
|
||||
if(Adjacent(attacker) || data.parry_data[PARRY_COUNTERATTACK_IGNORE_ADJACENCY])
|
||||
if(data.parry_data[PARRY_COUNTERATTACK_MELEE_ATTACK_CHAIN])
|
||||
switch(parrying)
|
||||
if(ITEM_PARRY)
|
||||
active_parry_item.melee_attack_chain(src, attacker, null, ATTACKCHAIN_PARRY_COUNTERATTACK, data.parry_data[PARRY_COUNTERATTACK_MELEE_ATTACK_CHAIN])
|
||||
effect_text += "reflexively counterattacking with [active_parry_item]"
|
||||
if(UNARMED_PARRY) // WARNING: If you are using these two, the attackchain parry counterattack flags and damage multipliers are unimplemented. Be careful with how you handle this.
|
||||
UnarmedAttack(attacker)
|
||||
effect_text += "reflexively counterattacking in the process"
|
||||
if(MARTIAL_PARRY) // Not well implemeneted, recommend custom implementation using the martial art datums.
|
||||
UnarmedAttack(attacker)
|
||||
effect_text += "reflexively maneuvering to retaliate"
|
||||
if(data.parry_data[PARRY_DISARM_ATTACKER])
|
||||
L.drop_all_held_items()
|
||||
effect_text += "disarming"
|
||||
if(data.parry_data[PARRY_KNOCKDOWN_ATTACKER])
|
||||
L.DefaultCombatKnockdown(data.parry_data[PARRY_KNOCKDOWN_ATTACKER])
|
||||
effect_text += "knocking them to the ground"
|
||||
if(data.parry_data[PARRY_STAGGER_ATTACKER])
|
||||
L.Stagger(data.parry_data[PARRY_STAGGER_ATTACKER])
|
||||
effect_text += "staggering"
|
||||
if(data.parry_data[PARRY_DAZE_ATTACKER])
|
||||
L.Daze(data.parry_data[PARRY_DAZE_ATTACKER])
|
||||
effect_text += "dazing"
|
||||
return effect_text
|
||||
|
||||
/// Gets the datum/block_parry_data we're going to use to parry.
|
||||
/mob/living/proc/get_parry_data()
|
||||
if(parrying == ITEM_PARRY)
|
||||
return active_parry_item.get_block_parry_data()
|
||||
else if(parrying == UNARMED_PARRY)
|
||||
return return_block_parry_datum(block_parry_data)
|
||||
else if(parrying == MARTIAL_PARRY)
|
||||
return return_block_parry_datum(mind.martial_art.block_parry_data)
|
||||
|
||||
/// Effects
|
||||
/obj/effect/abstract/parry
|
||||
icon = 'icons/effects/block_parry.dmi'
|
||||
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
|
||||
layer = FLOAT_LAYER
|
||||
plane = FLOAT_PLANE
|
||||
vis_flags = VIS_INHERIT_LAYER|VIS_INHERIT_PLANE
|
||||
/// The person we're on
|
||||
var/mob/living/owner
|
||||
|
||||
/obj/effect/abstract/parry/main
|
||||
name = null
|
||||
|
||||
/obj/effect/abstract/parry/main/Initialize(mapload, autorun, mob/living/owner, set_icon_state, windup, active, spindown)
|
||||
. = ..()
|
||||
icon_state = set_icon_state
|
||||
if(owner)
|
||||
attach_to(owner)
|
||||
if(autorun)
|
||||
INVOKE_ASYNC(src, .proc/run_animation, windup, active, spindown)
|
||||
|
||||
/obj/effect/abstract/parry/main/Destroy()
|
||||
detach_from(owner)
|
||||
return ..()
|
||||
|
||||
/obj/effect/abstract/parry/main/proc/attach_to(mob/living/attaching)
|
||||
if(owner)
|
||||
detach_from(owner)
|
||||
owner = attaching
|
||||
owner.vis_contents += src
|
||||
|
||||
/obj/effect/abstract/parry/main/proc/detach_from(mob/living/detaching)
|
||||
if(detaching == owner)
|
||||
owner = null
|
||||
detaching.vis_contents -= src
|
||||
|
||||
/obj/effect/abstract/parry/main/proc/run_animation(windup_time = 2, active_time = 5, spindown_time = 3)
|
||||
var/matrix/current = transform
|
||||
transform = matrix(0.1, 0, 0, 0, 0.1, 0)
|
||||
animate(src, transform = current, time = windup_time)
|
||||
sleep(active_time)
|
||||
animate(src, alpha = 0, spindown_time)
|
||||
@@ -1,31 +1,6 @@
|
||||
// This file has a weird name, but it's for anything related to the checks for shields, blocking, dodging,
|
||||
// and similar "stop this attack before it actually impacts the target" as opposed to "defend once it has hit".
|
||||
|
||||
/*
|
||||
/// You can find the mob_check_block() and mob_run_block() macros in __DEFINES/combat.dm
|
||||
|
||||
/// Bitflags for check_block() and run_block(). Meant to be combined. You can be hit and still reflect, for example, if you do not use BLOCK_SUCCESS.
|
||||
/// Attack was not blocked
|
||||
#define BLOCK_NONE NONE
|
||||
/// Attack was blocked, do not do damage. THIS FLAG MUST BE THERE FOR DAMAGE/EFFECT PREVENTION!
|
||||
#define BLOCK_SUCCESS (1<<1)
|
||||
|
||||
/// The below are for "metadata" on "how" the attack was blocked.
|
||||
|
||||
/// Attack was and should be reflected (NOTE: the SHOULD here is important, as it says "the thing blocking isn't handling the reflecting for you so do it yourself"!)
|
||||
#define BLOCK_SHOULD_REFLECT (1<<2)
|
||||
/// Attack was manually redirected (including reflected) by any means by the defender. For when YOU are handling the reflection, rather than the thing hitting you. (see sleeping carp)
|
||||
#define BLOCK_REDIRECTED (1<<3)
|
||||
/// Attack was blocked by something like a shield.
|
||||
#define BLOCK_PHYSICAL_EXTERNAL (1<<4)
|
||||
/// Attack was blocked by something worn on you.
|
||||
#define BLOCK_PHYSICAL_INTERNAL (1<<5)
|
||||
/// Attack should pass through. Like SHOULD_REFLECT but for.. well, passing through harmlessly.
|
||||
#define BLOCK_SHOULD_PASSTHROUGH (1<<6)
|
||||
/// Attack outright missed because the target dodged. Should usually be combined with SHOULD_PASSTHROUGH or something (see martial arts)
|
||||
#define BLOCK_TARGET_DODGED (1<<7)
|
||||
*/
|
||||
|
||||
/** The actual proc for block checks. DO NOT USE THIS DIRECTLY UNLESS YOU HAVE VERY GOOD REASON TO. To reduce copypaste for differences between handling for real attacks and virtual checks.
|
||||
* Automatically checks all held items for /obj/item/proc/run_block() with the same parameters.
|
||||
* @params
|
||||
@@ -39,21 +14,31 @@
|
||||
* attacker - Set to the mob attacking IF KNOWN. Do not expect this to always be set!
|
||||
* def_zone - The zone this'll impact.
|
||||
* return_list - If something wants to grab things from what items/whatever put into list/block_return on obj/item/run_block and the comsig, pass in a list so you can grab anything put in it after block runs.
|
||||
* attack_direction - Direction of the attack. It is highly recommended to put this in, as the automatic guesswork that's done otherwise is quite inaccurate at times.
|
||||
*/
|
||||
/mob/living/proc/do_run_block(real_attack = TRUE, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list = list())
|
||||
/mob/living/proc/do_run_block(real_attack = TRUE, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list = list(), attack_direction)
|
||||
if(real_attack)
|
||||
. = run_parry(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list) //Parry - Highest priority!
|
||||
if((. & BLOCK_SUCCESS) && !(. & BLOCK_CONTINUE_CHAIN))
|
||||
return
|
||||
// Component signal block runs have highest priority.. for now.
|
||||
. = SEND_SIGNAL(src, COMSIG_LIVING_RUN_BLOCK, real_attack, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list)
|
||||
. = SEND_SIGNAL(src, COMSIG_LIVING_RUN_BLOCK, real_attack, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, attack_direction)
|
||||
if((. & BLOCK_SUCCESS) && !(. & BLOCK_CONTINUE_CHAIN))
|
||||
return_list[BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE] = 100
|
||||
return
|
||||
var/list/obj/item/tocheck = get_blocking_items()
|
||||
sortTim(tocheck, /proc/cmp_item_block_priority_asc)
|
||||
sortTim(tocheck, /proc/cmp_numeric_dsc, TRUE)
|
||||
// i don't like this
|
||||
var/block_chance_modifier = round(damage / -3)
|
||||
if(real_attack)
|
||||
for(var/obj/item/I in tocheck)
|
||||
// i don't like this too
|
||||
var/final_block_chance = I.block_chance - (clamp((armour_penetration-I.armour_penetration)/2,0,100)) + block_chance_modifier //So armour piercing blades can still be parried by other blades, for example
|
||||
var/results = I.run_block(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, return_list)
|
||||
var/results
|
||||
if(I == active_block_item)
|
||||
results = I.active_block(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, return_list, attack_direction)
|
||||
else
|
||||
results = I.run_block(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, return_list)
|
||||
. |= results
|
||||
if((results & BLOCK_SUCCESS) && !(results & BLOCK_CONTINUE_CHAIN))
|
||||
break
|
||||
@@ -61,17 +46,29 @@
|
||||
for(var/obj/item/I in tocheck)
|
||||
// i don't like this too
|
||||
var/final_block_chance = I.block_chance - (clamp((armour_penetration-I.armour_penetration)/2,0,100)) + block_chance_modifier //So armour piercing blades can still be parried by other blades, for example
|
||||
I.check_block(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, return_list)
|
||||
if(I == active_block_item) //block is long termed enough we give a damn. parry, not so much.
|
||||
I.check_active_block(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, return_list, attack_direction)
|
||||
else
|
||||
I.check_block(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, return_list)
|
||||
if(. & BLOCK_SUCCESS)
|
||||
return_list[BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE] = 100
|
||||
else if(isnull(return_list[BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE]))
|
||||
return_list[BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE] = return_list[BLOCK_RETURN_MITIGATION_PERCENT]
|
||||
|
||||
/// Gets an unsortedlist of objects to run block checks on.
|
||||
/// Gets an unsortedlist of objects to run block checks on. List must have associative values for priorities!
|
||||
/mob/living/proc/get_blocking_items()
|
||||
. = list()
|
||||
if(active_block_item)
|
||||
var/datum/block_parry_data/data = active_block_item.get_block_parry_data()
|
||||
.[active_block_item] = data.block_active_priority
|
||||
SEND_SIGNAL(src, COMSIG_LIVING_GET_BLOCKING_ITEMS, .)
|
||||
for(var/obj/item/I in held_items)
|
||||
// this is a bad check but i am not removing it until a better catchall is made
|
||||
if(istype(I, /obj/item/clothing))
|
||||
continue
|
||||
. |= I
|
||||
if(.[I]) //don't override block/parry.
|
||||
continue
|
||||
.[I] = I.block_priority
|
||||
|
||||
/obj/item
|
||||
/// The 0% to 100% chance for the default implementation of random block rolls.
|
||||
@@ -95,3 +92,15 @@
|
||||
SEND_SIGNAL(src, COMSIG_ITEM_CHECK_BLOCK, owner, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return)
|
||||
var/existing = block_return[BLOCK_RETURN_NORMAL_BLOCK_CHANCE]
|
||||
block_return[BLOCK_RETURN_NORMAL_BLOCK_CHANCE] = max(existing || 0, final_block_chance)
|
||||
|
||||
// HELPER PROCS
|
||||
|
||||
/**
|
||||
* Considers a block return_list and calculates damage to use from that.
|
||||
*/
|
||||
/proc/block_calculate_resultant_damage(damage, list/block_return)
|
||||
if(!isnull(block_return[BLOCK_RETURN_SET_DAMAGE_TO])) // higher priority
|
||||
return block_return[BLOCK_RETURN_SET_DAMAGE_TO]
|
||||
else if(!isnull(block_return[BLOCK_RETURN_MITIGATION_PERCENT]))
|
||||
return damage * ((100 - block_return[BLOCK_RETURN_MITIGATION_PERCENT]) * 0.01)
|
||||
return damage
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
// yell at me later for file naming
|
||||
// This file contains stuff relating to the new directional blocking and parry system.
|
||||
GLOBAL_LIST_EMPTY(block_parry_data)
|
||||
|
||||
/proc/return_block_parry_datum(datum/block_parry_data/type_id_datum)
|
||||
if(istype(type_id_datum))
|
||||
return type_id_datum
|
||||
if(ispath(type_id_datum))
|
||||
. = GLOB.block_parry_data["[type_id_datum]"]
|
||||
if(!.)
|
||||
. = GLOB.block_parry_data["[type_id_datum]"] = new type_id_datum
|
||||
else //text id
|
||||
return GLOB.block_parry_data["[type_id_datum]"]
|
||||
|
||||
/proc/set_block_parry_datum(id, datum/block_parry_data/data)
|
||||
if(ispath(id))
|
||||
CRASH("Path-fetching of block parry data is only to grab static data, do not attempt to modify global caches of paths. Use string IDs.")
|
||||
GLOB.block_parry_data["[id]"] = data
|
||||
|
||||
/// Carries data like list data that would be a waste of memory if we initialized the list on every /item as we can cache datums easier.
|
||||
/datum/block_parry_data
|
||||
/////////// BLOCKING ////////////
|
||||
|
||||
/// NOTE: FOR ATTACK_TYPE_DEFINE, you MUST wrap it in "[DEFINE_HERE]"! The defines are bitflags, and therefore, NUMBERS!
|
||||
|
||||
/// See defines. Point of reference is someone facing north.
|
||||
var/can_block_directions = BLOCK_DIR_NORTH | BLOCK_DIR_NORTHEAST | BLOCK_DIR_NORTHWEST
|
||||
/// Attacks we can block
|
||||
var/can_block_attack_types = ALL
|
||||
/// Our slowdown added while blocking
|
||||
var/block_slowdown = 1
|
||||
/// Clickdelay added to user after block ends
|
||||
var/block_end_click_cd_add = 0
|
||||
/// Disallow attacking during block
|
||||
var/block_lock_attacking = TRUE
|
||||
/// Disallow sprinting during block
|
||||
var/block_lock_sprinting = FALSE
|
||||
/// The priority we get in [mob/do_run_block()] while we're being used to parry.
|
||||
var/block_active_priority = BLOCK_PRIORITY_ACTIVE_BLOCK
|
||||
/// Windup before we have our blocking active.
|
||||
var/block_start_delay = 5
|
||||
|
||||
/// Amount of "free" damage blocking absorbs
|
||||
var/block_damage_absorption = 10
|
||||
/// Override absorption, list("[ATTACK_TYPE_DEFINE]" = absorption), see [block_damage_absorption]
|
||||
var/list/block_damage_absorption_override
|
||||
|
||||
/// Ratio of damage to allow through above absorption and below limit. Multiplied by damage to determine how much to let through. Lower is better.
|
||||
var/block_damage_multiplier = 0.5
|
||||
/// Override damage overrun efficiency, list("[ATTACK_TYPE_DEFINE]" = absorption), see [block_damage_efficiency]
|
||||
var/list/block_damage_multiplier_override
|
||||
|
||||
/// Upper bound of damage block, anything above this will go right through.
|
||||
var/block_damage_limit = 80
|
||||
/// Override upper bound of damage block, list("[ATTACK_TYPE_DEFINE]" = absorption), see [block_damage_limit]
|
||||
var/list/block_damage_limit_override
|
||||
|
||||
/// The blocked variable of on_hit() on projectiles is impacted by this. Higher is better, 0 to 100, percentage.
|
||||
var/block_projectile_mitigation = 50
|
||||
|
||||
/*
|
||||
* NOTE: Overrides for attack types for most the block_stamina variables were removed,
|
||||
* because at the time of writing nothing needed to use it. Add them if you need it,
|
||||
* it should be pretty easy, just copy [active_block_damage_mitigation]
|
||||
* for how to override with list.
|
||||
*/
|
||||
|
||||
/// Default damage-to-stamina coefficient, higher is better. This is based on amount of damage BLOCKED, not initial damage, to prevent damage from "double dipping".
|
||||
var/block_stamina_efficiency = 2
|
||||
/// Override damage-to-stamina coefficient, see [block_efficiency], this should be list("[ATTACK_TYPE_DEFINE]" = coefficient_number)
|
||||
var/list/block_stamina_efficiency_override
|
||||
/// Ratio of stamina incurred by blocking that goes to the arm holding the object instead of the chest. Has no effect if this is not held in hand.
|
||||
var/block_stamina_limb_ratio = 0.5
|
||||
/// Ratio of stamina incurred by chest (so after [block_stamina_limb_ratio] runs) that is buffered.
|
||||
var/block_stamina_buffer_ratio = 1
|
||||
|
||||
/// Stamina dealt directly via adjustStaminaLossBuffered() per SECOND of block.
|
||||
var/block_stamina_cost_per_second = 1.5
|
||||
|
||||
/// Bitfield for attack types that we can block while down. This will work in any direction.
|
||||
var/block_resting_attack_types_anydir = ATTACK_TYPE_MELEE | ATTACK_TYPE_UNARMED | ATTACK_TYPE_TACKLE
|
||||
/// Bitfield for attack types that we can block while down but only in our normal directions.
|
||||
var/block_resting_attack_types_directional = ATTACK_TYPE_PROJECTILE | ATTACK_TYPE_THROWN
|
||||
/// Multiplier to stamina damage taken for attacks blocked while downed.
|
||||
var/block_resting_stamina_penalty_multiplier = 1.5
|
||||
/// Override list for multiplier to stamina damage taken for attacks blocked while down. list("[ATTACK_TYPE_DEFINE]" = multiplier_number)
|
||||
var/list/block_resting_stamina_penalty_multiplier_override
|
||||
|
||||
/// Sounds for blocking
|
||||
var/list/block_sounds = list('sound/block_parry/block_metal1.ogg' = 1, 'sound/block_parry/block_metal1.ogg' = 1)
|
||||
|
||||
/////////// PARRYING ////////////
|
||||
/// Prioriry for [mob/do_run_block()] while we're being used to parry.
|
||||
// None - Parry is always highest priority!
|
||||
/// Parry doesn't work if you aren't able to otherwise attack due to clickdelay
|
||||
var/parry_respect_clickdelay = TRUE
|
||||
/// Parry stamina cost
|
||||
var/parry_stamina_cost = 5
|
||||
/// Attack types we can block
|
||||
var/parry_attack_types = ALL
|
||||
/// Parry flags
|
||||
var/parry_flags = PARRY_DEFAULT_HANDLE_FEEDBACK | PARRY_LOCK_ATTACKING
|
||||
|
||||
/// Parry windup duration in deciseconds. 0 to this is windup, afterwards is main stage.
|
||||
var/parry_time_windup = 2
|
||||
/// Parry spindown duration in deciseconds. main stage end to this is the spindown stage, afterwards the parry fully ends.
|
||||
var/parry_time_spindown = 3
|
||||
/// Main parry window in deciseconds. This is between [parry_time_windup] and [parry_time_spindown]
|
||||
var/parry_time_active = 5
|
||||
// Visual overrides
|
||||
/// If set, overrides visual duration of windup
|
||||
var/parry_time_windup_visual_override
|
||||
/// If set, overrides visual duration of active period
|
||||
var/parry_time_active_visual_override
|
||||
/// If set, overrides visual duration of spindown
|
||||
var/parry_time_spindown_visual_override
|
||||
/// Perfect parry window in deciseconds from the start of the main window. 3 with main 5 = perfect on third decisecond of main window.
|
||||
var/parry_time_perfect = 2.5
|
||||
/// Time on both sides of perfect parry that still counts as part of the perfect window.
|
||||
var/parry_time_perfect_leeway = 1
|
||||
/// [parry_time_perfect_leeway] override for attack types, list("[ATTACK_TYPE_DEFINE]" = deciseconds)
|
||||
var/list/parry_time_perfect_leeway_override
|
||||
/// Parry "efficiency" falloff in percent per decisecond once perfect window is over.
|
||||
var/parry_imperfect_falloff_percent = 20
|
||||
/// [parry_imperfect_falloff_percent] override for attack types, list("[ATTACK_TYPE_DEFINE]" = deciseconds)
|
||||
var/list/parry_imperfect_falloff_percent_override
|
||||
/// Efficiency in percent on perfect parry.
|
||||
var/parry_efficiency_perfect = 120
|
||||
/// Parry effect data.
|
||||
var/list/parry_data = list(
|
||||
PARRY_COUNTERATTACK_MELEE_ATTACK_CHAIN = 1
|
||||
)
|
||||
/// Efficiency must be at least this to be considered successful
|
||||
var/parry_efficiency_considered_successful = 0.1
|
||||
/// Efficiency must be at least this to run automatic counterattack
|
||||
var/parry_efficiency_to_counterattack = 0.1
|
||||
/// Maximum attacks to parry successfully or unsuccessfully (but not efficiency < 0) during active period, hitting this immediately ends the sequence.
|
||||
var/parry_max_attacks = INFINITY
|
||||
/// Visual icon state override for parrying
|
||||
var/parry_effect_icon_state = "parry_bm_hold"
|
||||
/// Parrying cooldown, separate of clickdelay. It must be this much deciseconds since their last parry for them to parry with this object.
|
||||
var/parry_cooldown = 0
|
||||
/// Parry start sound
|
||||
var/parry_start_sound = 'sound/block_parry/sfx-parry.ogg'
|
||||
/// Sounds for parrying
|
||||
var/list/parry_sounds = list('sound/block_parry/block_metal1.ogg' = 1, 'sound/block_parry/block_metal1.ogg' = 1)
|
||||
/// Stagger duration post-parry if you fail to parry an attack
|
||||
var/parry_failed_stagger_duration = 3.5 SECONDS
|
||||
/// Clickdelay duration post-parry if you fail to parry an attack
|
||||
var/parry_failed_clickcd_duration = 2 SECONDS
|
||||
|
||||
/**
|
||||
* Quirky proc to get average of flags in list that are in attack_type because why is attack_type a flag.
|
||||
*/
|
||||
/datum/block_parry_data/proc/attack_type_list_scan(list/L, attack_type)
|
||||
var/total = 0
|
||||
var/div = 0
|
||||
for(var/flagtext in L)
|
||||
if(attack_type & text2num(flagtext))
|
||||
total += L[flagtext]
|
||||
div++
|
||||
// if none, return null.
|
||||
if(!div)
|
||||
return
|
||||
return total/div //groan
|
||||
|
||||
|
||||
/**
|
||||
* Gets the percentage efficiency of our parry.
|
||||
*
|
||||
* Returns a percentage in normal 0 to 100 scale, but not clamped to just 0 to 100.
|
||||
* This is a proc to allow for overriding.
|
||||
* @params
|
||||
* * attack_type - int, bitfield of the attack type(s)
|
||||
* * parry_time - deciseconds since start of the parry.
|
||||
*/
|
||||
/datum/block_parry_data/proc/get_parry_efficiency(attack_type, parry_time)
|
||||
var/difference = abs(parry_time - (parry_time_perfect + parry_time_windup))
|
||||
var/leeway = attack_type_list_scan(parry_time_perfect_leeway_override, attack_type)
|
||||
if(isnull(leeway))
|
||||
leeway = parry_time_perfect_leeway
|
||||
difference -= leeway
|
||||
. = parry_efficiency_perfect
|
||||
if(difference <= 0)
|
||||
return
|
||||
var/falloff = attack_type_list_scan(parry_imperfect_falloff_percent_override, attack_type)
|
||||
if(isnull(falloff))
|
||||
falloff = parry_imperfect_falloff_percent
|
||||
. -= falloff * difference
|
||||
|
||||
#define RENDER_VARIABLE_SIMPLE(varname, desc) dat += "<tr><th>[#varname]<br><i>[desc]</i></th><th>[varname]</th></tr>"
|
||||
#define RENDER_OVERRIDE_LIST(varname, desc) \
|
||||
dat += "<tr><th>[#varname]<br><i>[desc]</i></th><th>"; \
|
||||
var/list/assembled__##varname = list(); \
|
||||
for(var/textbit in varname){ \
|
||||
assembled__##varname += "[GLOB.attack_type_names[textbit]] = [varname[textbit]]"; \
|
||||
} \
|
||||
dat += "[english_list(assembled__##varname)]</th>";
|
||||
#define RENDER_ATTACK_TYPES(varname, desc) dat += "<tr><th>[#varname]<br><i>[desc]</i></th><th>"; \
|
||||
var/list/assembled__##varname = list(); \
|
||||
for(var/bit in bitfield2list(varname)){ \
|
||||
var/name = GLOB.attack_type_names[num2text(bit)]; \
|
||||
if(name){ \
|
||||
assembled__##varname += "[name]"; \
|
||||
} \
|
||||
} \
|
||||
dat += "[english_list(assembled__##varname)]</th>";
|
||||
#define RENDER_BLOCK_DIRECTIONS(varname, desc) \
|
||||
dat += "<tr><th>[#varname]<br><i>[desc]</i></th><th>"; \
|
||||
var/list/assembled__##varname = list(); \
|
||||
for(var/bit in bitfield2list(varname)){ \
|
||||
var/name = GLOB.block_direction_names[num2text(bit)]; \
|
||||
if(name){ \
|
||||
assembled__##varname += "[name]"; \
|
||||
} \
|
||||
} \
|
||||
dat += "[english_list(assembled__##varname)]</th>";
|
||||
|
||||
/datum/block_parry_data/Topic(href, href_list)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
if(href_list["render"])
|
||||
var/datum/browser/B = new(usr, REF(src), href_list["name"], 800, 1000)
|
||||
B.set_content(render_html_readout(href_list["block"], href_list["parry"]))
|
||||
B.open()
|
||||
|
||||
/**
|
||||
* Generates a HTML render of this datum for self-documentation
|
||||
* Maybe make this tgui-next someday haha god this is ugly as sin.
|
||||
* Does NOT include the popout or title or anything. Just the variables and explanations..
|
||||
*/
|
||||
/datum/block_parry_data/proc/render_html_readout(block_data = FALSE, parry_data = FALSE)
|
||||
var/list/dat = list()
|
||||
if(block_data)
|
||||
dat += "<div class='statusDisplay'><h3>Block Stats</h3><table style='width:100%'><tr><th>Name/Description</th><th>Value</th></tr>"
|
||||
RENDER_BLOCK_DIRECTIONS(can_block_directions, "Which directions this can block in.")
|
||||
RENDER_ATTACK_TYPES(can_block_attack_types, "The kinds of attacks this can block.")
|
||||
RENDER_VARIABLE_SIMPLE(block_slowdown, "How much slowdown is applied to the user while blocking. Lower is better.")
|
||||
RENDER_VARIABLE_SIMPLE(block_end_click_cd_add, "How much click delay in deciseconds is applied to the user when blocking ends. Lower is better.")
|
||||
RENDER_VARIABLE_SIMPLE(block_lock_attacking, "Whether or not (1 or 0) the user is locked from atacking and/or item usage while blocking.")
|
||||
RENDER_VARIABLE_SIMPLE(block_active_priority, "The priority of this item in the block sequence. This will probably mean nothing to you unless you are a coder.")
|
||||
RENDER_VARIABLE_SIMPLE(block_start_delay, "The amount of time in deciseconds it takes to start a block with this item. Lower is better.")
|
||||
RENDER_VARIABLE_SIMPLE(block_damage_absorption, "The amount of damage that is absorbed by default. Higher is better.")
|
||||
RENDER_OVERRIDE_LIST(block_damage_absorption_override, "Overrides for the above for each attack type")
|
||||
RENDER_VARIABLE_SIMPLE(block_damage_multiplier, "Damage between absorption and limit is multiplied by this. Lower is better.")
|
||||
RENDER_OVERRIDE_LIST(block_damage_multiplier_override, "Overrides for the above for each attack type")
|
||||
RENDER_VARIABLE_SIMPLE(block_damage_limit, "Damage above this passes right through and is not impacted. Higher is better.")
|
||||
RENDER_OVERRIDE_LIST(block_damage_limit_override, "Overrides for the above for each attack type.")
|
||||
RENDER_VARIABLE_SIMPLE(block_stamina_efficiency, "Coefficient for stamina damage dealt to user by damage blocked. Higher is better.")
|
||||
RENDER_OVERRIDE_LIST(block_stamina_efficiency_override, "Overrides for the above for each attack type.")
|
||||
RENDER_VARIABLE_SIMPLE(block_stamina_limb_ratio, "The ratio of stamina that is applied to the limb holding this object (if applicable) rather than whole body/chest.")
|
||||
RENDER_VARIABLE_SIMPLE(block_stamina_buffer_ratio, "The ratio of stamina incurred by chest/whole body that is buffered rather than direct (buffer = your stamina buffer, direct = direct stamina damage like from a disabler.)")
|
||||
RENDER_VARIABLE_SIMPLE(block_stamina_cost_per_second, "The buffered stamina damage the user incurs per second of block. Lower is better.")
|
||||
RENDER_ATTACK_TYPES(block_resting_attack_types_anydir, "The kinds of attacks you can block while resting/otherwise knocked to the floor from any direction. can_block_attack_types takes precedence.")
|
||||
RENDER_ATTACK_TYPES(block_resting_attack_types_directional, "The kinds of attacks you can block wihle resting/otherwise knocked to the floor that are directional only. can_block_attack_types takes precedence.")
|
||||
RENDER_VARIABLE_SIMPLE(block_resting_stamina_penalty_multiplier, "Multiplier to stamina damage incurred from blocking while downed. Lower is better.")
|
||||
RENDER_OVERRIDE_LIST(block_resting_stamina_penalty_multiplier, "Overrides for the above for each attack type.")
|
||||
dat += "</div></table>"
|
||||
if(parry_data)
|
||||
dat += "<div class='statusDisplay'><h3>Parry Stats</h3><table style='width:100%'><tr><th>Name/Description</th><th>Value</th></tr>"
|
||||
RENDER_VARIABLE_SIMPLE(parry_respect_clickdelay, "Whether or not (1 or 0) you can only parry if your attack cooldown isn't in effect.")
|
||||
RENDER_VARIABLE_SIMPLE(parry_stamina_cost, "Buffered stamina damage incurred by you for parrying with this.")
|
||||
RENDER_ATTACK_TYPES(parry_attack_types, "Attack types you can parry.")
|
||||
// parry_flags
|
||||
dat += ""
|
||||
RENDER_VARIABLE_SIMPLE(parry_time_windup, "Deciseconds of parry windup.")
|
||||
RENDER_VARIABLE_SIMPLE(parry_time_spindown, "Deciseconds of parry spindown.")
|
||||
RENDER_VARIABLE_SIMPLE(parry_time_active, "Deciseconds of active parry window - This is the ONLY time your parry is active.")
|
||||
RENDER_VARIABLE_SIMPLE(parry_time_windup_visual_override, "Visual effect length override")
|
||||
RENDER_VARIABLE_SIMPLE(parry_time_spindown_visual_override, "Visual effect length override")
|
||||
RENDER_VARIABLE_SIMPLE(parry_time_active_visual_override, "Visual effect length override")
|
||||
RENDER_VARIABLE_SIMPLE(parry_time_perfect, "Deciseconds <b>into the active window</b> considered the 'center' of the perfect period.")
|
||||
RENDER_VARIABLE_SIMPLE(parry_time_perfect_leeway, "Leeway on both sides of the perfect period's center still considered perfect.")
|
||||
RENDER_OVERRIDE_LIST(parry_time_perfect_leeway_override, "Override for the above for each attack type")
|
||||
RENDER_VARIABLE_SIMPLE(parry_imperfect_falloff_percent, "Linear falloff in percent per decisecond for attacks parried outside of perfect window.")
|
||||
RENDER_OVERRIDE_LIST(parry_imperfect_falloff_percent_override, "Override for the above for each attack type")
|
||||
RENDER_VARIABLE_SIMPLE(parry_efficiency_perfect, "Efficiency in percentage a parry in the perfect window is considered.")
|
||||
// parry_data
|
||||
dat += ""
|
||||
RENDER_VARIABLE_SIMPLE(parry_efficiency_considered_successful, "Minimum parry efficiency to be considered a successful parry.")
|
||||
RENDER_VARIABLE_SIMPLE(parry_efficiency_to_counterattack, "Minimum parry efficiency to trigger counterattack effects.")
|
||||
RENDER_VARIABLE_SIMPLE(parry_max_attacks, "Max attacks parried per parry cycle.")
|
||||
RENDER_VARIABLE_SIMPLE(parry_effect_icon_state, "Parry effect image name")
|
||||
RENDER_VARIABLE_SIMPLE(parry_cooldown, "Deciseconds it has to be since the last time a parry sequence <b>ended</b> for you before you can parry again.")
|
||||
RENDER_VARIABLE_SIMPLE(parry_failed_stagger_duration, "Deciseconds you are staggered for at the of the parry sequence if you do not successfully parry anything.")
|
||||
RENDER_VARIABLE_SIMPLE(parry_failed_clickcd_duration, "Deciseconds you are put on attack cooldown at the end of the parry sequence if you do not successfully parry anything.")
|
||||
dat += "</div></table>"
|
||||
return dat.Join("")
|
||||
#undef RENDER_VARIABLE_SIMPLE
|
||||
#undef RENDER_OVERRIDE_LIST
|
||||
#undef RENDER_ATTACK_TYPES
|
||||
#undef RENDER_BLOCK_DIRECTIONS
|
||||
|
||||
// MOB PROCS
|
||||
|
||||
/**
|
||||
* Called every life tick to handle blocking/parrying effects.
|
||||
*/
|
||||
/mob/living/proc/handle_block_parry(seconds = 1)
|
||||
if(combat_flags & COMBAT_FLAG_ACTIVE_BLOCKING)
|
||||
var/datum/block_parry_data/data = return_block_parry_datum(active_block_item.block_parry_data)
|
||||
adjustStaminaLossBuffered(data.block_stamina_cost_per_second * seconds)
|
||||
|
||||
/mob/living/on_item_dropped(obj/item/I)
|
||||
if(I == active_block_item)
|
||||
stop_active_blocking()
|
||||
if(I == active_parry_item)
|
||||
end_parry_sequence()
|
||||
return ..()
|
||||
@@ -1,12 +1,7 @@
|
||||
|
||||
/mob/living/proc/run_armor_check(def_zone = null, attack_flag = "melee", absorb_text = null, soften_text = null, armour_penetration, penetrated_text, silent=FALSE)
|
||||
/mob/living/proc/run_armor_check(def_zone = null, attack_flag = "melee", absorb_text = "Your armor absorbs the blow!", soften_text = "Your armor softens the blow!", armour_penetration, penetrated_text = "Your armor was penetrated!")
|
||||
var/armor = getarmor(def_zone, attack_flag)
|
||||
|
||||
if(armor <= 0)
|
||||
return armor
|
||||
if(silent)
|
||||
return max(0, armor - armour_penetration)
|
||||
|
||||
//the if "armor" check is because this is used for everything on /living, including humans
|
||||
if(armor && armour_penetration)
|
||||
armor = max(0, armor - armour_penetration)
|
||||
@@ -15,7 +10,7 @@
|
||||
else if(armor >= 100)
|
||||
if(absorb_text)
|
||||
to_chat(src, "<span class='danger'>[absorb_text]</span>")
|
||||
else
|
||||
else if(armor > 0)
|
||||
if(soften_text)
|
||||
to_chat(src, "<span class='danger'>[soften_text]</span>")
|
||||
return armor
|
||||
@@ -71,22 +66,30 @@
|
||||
CRASH("Invalid rediretion mode [redirection_mode]")
|
||||
|
||||
/mob/living/bullet_act(obj/item/projectile/P, def_zone)
|
||||
var/totaldamage = P.damage
|
||||
var/final_percent = 0
|
||||
if(P.original != src || P.firer != src) //try to block or reflect the bullet, can't do so when shooting oneself
|
||||
var/list/returnlist = list()
|
||||
var/returned = mob_run_block(P, P.damage, "the [P.name]", ATTACK_TYPE_PROJECTILE, P.armour_penetration, P.firer, def_zone, returnlist)
|
||||
final_percent = returnlist[BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE]
|
||||
if(returned & BLOCK_SHOULD_REDIRECT)
|
||||
handle_projectile_attack_redirection(P, returnlist[BLOCK_RETURN_REDIRECT_METHOD])
|
||||
if(returned & BLOCK_REDIRECTED)
|
||||
return BULLET_ACT_FORCE_PIERCE
|
||||
if(returned & BLOCK_SUCCESS)
|
||||
P.on_hit(src, 100, def_zone)
|
||||
P.on_hit(src, final_percent, def_zone)
|
||||
return BULLET_ACT_BLOCK
|
||||
totaldamage = block_calculate_resultant_damage(totaldamage, returnlist)
|
||||
var/armor = run_armor_check(def_zone, P.flag, null, null, P.armour_penetration, null)
|
||||
if(!P.nodamage)
|
||||
apply_damage(P.damage, P.damage_type, def_zone, armor)
|
||||
apply_damage(totaldamage, P.damage_type, def_zone, armor)
|
||||
if(P.dismemberment)
|
||||
check_projectile_dismemberment(P, def_zone)
|
||||
return P.on_hit(src, armor) ? BULLET_ACT_HIT : BULLET_ACT_BLOCK
|
||||
var/missing = 100 - final_percent
|
||||
var/armor_ratio = armor * 0.01
|
||||
if(missing > 0)
|
||||
final_percent += missing * armor_ratio
|
||||
return P.on_hit(src, final_percent, def_zone) ? BULLET_ACT_HIT : BULLET_ACT_BLOCK
|
||||
|
||||
/mob/living/proc/check_projectile_dismemberment(obj/item/projectile/P, def_zone)
|
||||
return 0
|
||||
@@ -116,11 +119,14 @@
|
||||
I = AM
|
||||
throwpower = I.throwforce
|
||||
var/impacting_zone = ran_zone(BODY_ZONE_CHEST, 65)//Hits a random part of the body, geared towards the chest
|
||||
if(mob_run_block(AM, throwpower, "\the [AM.name]", ATTACK_TYPE_THROWN, 0, throwingdatum?.thrower, impacting_zone, null) & BLOCK_SUCCESS)
|
||||
var/list/block_return = list()
|
||||
var/total_damage = I.throwforce
|
||||
if(mob_run_block(AM, throwpower, "\the [AM.name]", ATTACK_TYPE_THROWN, 0, throwingdatum?.thrower, impacting_zone, block_return) & BLOCK_SUCCESS)
|
||||
hitpush = FALSE
|
||||
skipcatch = TRUE
|
||||
blocked = TRUE
|
||||
else if(I && I.throw_speed >= EMBED_THROWSPEED_THRESHOLD && can_embed(I, src) && prob(I.embedding["embed_chance"]) && !HAS_TRAIT(src, TRAIT_PIERCEIMMUNE) && (!HAS_TRAIT(src, TRAIT_AUTO_CATCH_ITEM) || incapacitated() || get_active_held_item()))
|
||||
total_damage = block_calculate_resultant_damage(total_damage, block_return)
|
||||
else if(I && I.throw_speed >= EMBED_THROWSPEED_THRESHOLD && can_embed(I, src) && prob(I.embedding.embed_chance) && !HAS_TRAIT(src, TRAIT_PIERCEIMMUNE) && (!HAS_TRAIT(src, TRAIT_AUTO_CATCH_ITEM) || incapacitated() || get_active_held_item()))
|
||||
embed_item(I)
|
||||
hitpush = FALSE
|
||||
skipcatch = TRUE //can't catch the now embedded item
|
||||
@@ -129,12 +135,9 @@
|
||||
return TRUE
|
||||
var/dtype = BRUTE
|
||||
var/volume = I.get_volume_by_throwforce_and_or_w_class()
|
||||
var/nosell_hit = SEND_SIGNAL(I, COMSIG_MOVABLE_IMPACT_ZONE, src, impacting_zone, throwingdatum) // TODO: find a better way to handle hitpush and skipcatch for humans
|
||||
if(nosell_hit)
|
||||
skipcatch = TRUE
|
||||
hitpush = FALSE
|
||||
|
||||
SEND_SIGNAL(I, COMSIG_MOVABLE_IMPACT_ZONE, src, impacting_zone)
|
||||
dtype = I.damtype
|
||||
|
||||
if (I.throwforce > 0) //If the weapon's throwforce is greater than zero...
|
||||
if (I.throwhitsound) //...and throwhitsound is defined...
|
||||
playsound(loc, I.throwhitsound, volume, 1, -1) //...play the weapon's throwhitsound.
|
||||
@@ -151,7 +154,7 @@
|
||||
visible_message("<span class='danger'>[src] has been hit by [I].</span>", \
|
||||
"<span class='userdanger'>You have been hit by [I].</span>")
|
||||
var/armor = run_armor_check(impacting_zone, "melee", "Your armor has protected your [parse_zone(impacting_zone)].", "Your armor has softened hit to your [parse_zone(impacting_zone)].",I.armour_penetration)
|
||||
apply_damage(I.throwforce, dtype, impacting_zone, armor)
|
||||
apply_damage(total_damage, dtype, impacting_zone, armor)
|
||||
if(I.thrownby)
|
||||
log_combat(I.thrownby, src, "threw and hit", I)
|
||||
else
|
||||
@@ -321,8 +324,10 @@
|
||||
var/damage = rand(5, 35)
|
||||
if(M.is_adult)
|
||||
damage = rand(20, 40)
|
||||
if(mob_run_block(M, damage, "the [M.name]", ATTACK_TYPE_MELEE, null, M, check_zone(M.zone_selected), null) & BLOCK_SUCCESS)
|
||||
var/list/block_return = list()
|
||||
if(mob_run_block(M, damage, "the [M.name]", ATTACK_TYPE_MELEE, null, M, check_zone(M.zone_selected), block_return) & BLOCK_SUCCESS)
|
||||
return FALSE
|
||||
damage = block_calculate_resultant_damage(damage, block_return)
|
||||
|
||||
if (stat != DEAD)
|
||||
log_combat(M, src, "attacked")
|
||||
@@ -338,13 +343,16 @@
|
||||
M.visible_message("<span class='notice'>\The [M] [M.friendly_verb_continuous] [src]!</span>",
|
||||
"<span class='notice'>You [M.friendly_verb_simple] [src]!</span>", target = src,
|
||||
target_message = "<span class='notice'>\The [M] [M.friendly_verb_continuous] you!</span>")
|
||||
return FALSE
|
||||
return 0
|
||||
else
|
||||
if(HAS_TRAIT(M, TRAIT_PACIFISM))
|
||||
to_chat(M, "<span class='notice'>You don't want to hurt anyone!</span>")
|
||||
return FALSE
|
||||
if(mob_run_block(M, rand(M.melee_damage_lower, M.melee_damage_upper), "the [M.name]", ATTACK_TYPE_MELEE, M.armour_penetration, M, check_zone(M.zone_selected), null) & BLOCK_SUCCESS)
|
||||
return FALSE
|
||||
var/damage = rand(M.melee_damage_lower, M.melee_damage_upper)
|
||||
var/list/return_list = list()
|
||||
if(mob_run_block(M, damage, "the [M.name]", ATTACK_TYPE_MELEE, M.armour_penetration, M, check_zone(M.zone_selected), return_list) & BLOCK_SUCCESS)
|
||||
return 0
|
||||
damage = block_calculate_resultant_damage(damage, return_list)
|
||||
if(M.attack_sound)
|
||||
playsound(loc, M.attack_sound, 50, 1, 1)
|
||||
M.do_attack_animation(src)
|
||||
@@ -352,7 +360,7 @@
|
||||
"<span class='userdanger'>\The [M] [M.attack_verb_continuous] you!</span>", null, COMBAT_MESSAGE_RANGE, null,
|
||||
M, "<span class='danger'>You [M.attack_verb_simple] [src]!</span>")
|
||||
log_combat(M, src, "attacked")
|
||||
return TRUE
|
||||
return damage
|
||||
|
||||
/mob/living/attack_paw(mob/living/carbon/monkey/M)
|
||||
if (M.a_intent == INTENT_HARM)
|
||||
|
||||
@@ -29,6 +29,26 @@
|
||||
|
||||
var/mobility_flags = MOBILITY_FLAGS_DEFAULT
|
||||
|
||||
// Combat - Blocking/Parrying system
|
||||
/// Our block_parry_data for unarmed blocks/parries. Currently only used for parrying, as unarmed block isn't implemented yet. YOU MUST RUN [get_block_parry_data(this)] INSTEAD OF DIRECTLY ACCESSING!
|
||||
var/datum/block_parry_data/block_parry_data = /datum/block_parry_data // defaults to *something* because [combat_flags] dictates whether or not we can unarmed block/parry.
|
||||
// Blocking
|
||||
/// The item the user is actively blocking with if any.
|
||||
var/obj/item/active_block_item
|
||||
// Parrying
|
||||
/// Whether or not the user is in the middle of an active parry. Set to [UNARMED_PARRY], [ITEM_PARRY], [MARTIAL_PARRY] if parrying.
|
||||
var/parrying = FALSE
|
||||
/// The itme the user is currently parrying with, if any.
|
||||
var/obj/item/active_parry_item
|
||||
/// world.time of parry action start
|
||||
var/parry_start_time = 0
|
||||
/// Current parry effect.
|
||||
var/obj/effect/abstract/parry/parry_visual_effect
|
||||
/// world.time of last parry end
|
||||
var/parry_end_time_last = 0
|
||||
/// Successful parries within the current parry cycle. It's a list of efficiency percentages.
|
||||
var/list/successful_parries
|
||||
|
||||
var/confused = 0 //Makes the mob move in random directions.
|
||||
|
||||
var/hallucination = 0 //Directly affects how long a mob will hallucinate for
|
||||
|
||||
@@ -3,10 +3,21 @@
|
||||
update_turf_movespeed(loc)
|
||||
//Hide typing indicator if we move.
|
||||
clear_typing_indicator()
|
||||
if(is_shifted)
|
||||
is_shifted = FALSE
|
||||
pixel_x = get_standard_pixel_x_offset(lying)
|
||||
pixel_y = get_standard_pixel_y_offset(lying)
|
||||
update_pixel_shifting(TRUE)
|
||||
|
||||
/mob/living/setDir(newdir, ismousemovement)
|
||||
. = ..()
|
||||
if(ismousemovement)
|
||||
update_pixel_shifting()
|
||||
|
||||
/mob/living/proc/update_pixel_shifting(moved = FALSE)
|
||||
if(combat_flags & COMBAT_FLAG_ACTIVE_BLOCKING)
|
||||
animate(src, pixel_x = get_standard_pixel_x_offset(), pixel_y = get_standard_pixel_y_offset(), time = 2.5, flags = ANIMATION_END_NOW)
|
||||
else if(moved)
|
||||
if(is_shifted)
|
||||
is_shifted = FALSE
|
||||
pixel_x = get_standard_pixel_x_offset(lying)
|
||||
pixel_y = get_standard_pixel_y_offset(lying)
|
||||
|
||||
/mob/living/CanPass(atom/movable/mover, turf/target)
|
||||
if((mover.pass_flags & PASSMOB))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/mob/living/silicon/ai/attacked_by(obj/item/I, mob/living/user, def_zone)
|
||||
/mob/living/silicon/ai/attacked_by(obj/item/I, mob/living/user, def_zone, attackchain_flags = NONE, damage_multiplier = 1)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return FALSE
|
||||
|
||||
@@ -359,7 +359,8 @@
|
||||
"Sleek" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "sleekmed"),
|
||||
"Marina" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "marinamed"),
|
||||
"Eyebot" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "eyebotmed"),
|
||||
"Heavy" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "heavymed")
|
||||
"Heavy" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "heavymed"),
|
||||
"Zoomba" = image(icon = 'icons/mob/robots.dmi', icon_state = "zoomba_med")
|
||||
)
|
||||
var/list/L = list("Medihound" = "medihound", "Medihound Dark" = "medihounddark", "Vale" = "valemed")
|
||||
for(var/a in L)
|
||||
@@ -375,6 +376,8 @@
|
||||
switch(med_borg_icon)
|
||||
if("Default")
|
||||
cyborg_base_icon = "medical"
|
||||
if("Zoomba")
|
||||
cyborg_base_icon = "zoomba_med"
|
||||
if("Droid")
|
||||
cyborg_base_icon = "medical"
|
||||
cyborg_icon_override = 'modular_citadel/icons/mob/robots.dmi'
|
||||
@@ -476,7 +479,8 @@
|
||||
"Can" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "caneng"),
|
||||
"Marina" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "marinaeng"),
|
||||
"Spider" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "spidereng"),
|
||||
"Heavy" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "heavyeng")
|
||||
"Heavy" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "heavyeng"),
|
||||
"Zoomba" = image(icon = 'icons/mob/robots.dmi', icon_state = "zoomba_engi")
|
||||
)
|
||||
var/list/L = list("Pup Dozer" = "pupdozer", "Vale" = "valeeng")
|
||||
for(var/a in L)
|
||||
@@ -492,6 +496,8 @@
|
||||
switch(engi_borg_icon)
|
||||
if("Default")
|
||||
cyborg_base_icon = "engineer"
|
||||
if("Zoomba")
|
||||
cyborg_base_icon = "zoomba_engi"
|
||||
if("Default - Treads")
|
||||
cyborg_base_icon = "engi-tread"
|
||||
special_light_key = "engineer"
|
||||
@@ -572,7 +578,8 @@
|
||||
"Can" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "cansec"),
|
||||
"Marina" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "marinasec"),
|
||||
"Spider" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "spidersec"),
|
||||
"Heavy" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "heavysec")
|
||||
"Heavy" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "heavysec"),
|
||||
"Zoomba" = image(icon = 'icons/mob/robots.dmi', icon_state = "zoomba_sec")
|
||||
)
|
||||
var/list/L = list("K9" = "k9", "Vale" = "valesec", "K9 Dark" = "k9dark")
|
||||
for(var/a in L)
|
||||
@@ -588,6 +595,8 @@
|
||||
switch(sec_borg_icon)
|
||||
if("Default")
|
||||
cyborg_base_icon = "sec"
|
||||
if("Zoomba")
|
||||
cyborg_base_icon = "zoomba_sec"
|
||||
if("Default - Treads")
|
||||
cyborg_base_icon = "sec-tread"
|
||||
special_light_key = "sec"
|
||||
@@ -827,6 +836,7 @@
|
||||
"(Janitor) Sleek" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "sleekjan"),
|
||||
"(Janitor) Can" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "canjan"),
|
||||
"(Janitor) Heavy" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "heavyjan"),
|
||||
"Zoomba" = image(icon = 'icons/mob/robots.dmi', icon_state = "zoomba_jani")
|
||||
)
|
||||
var/list/L = list("(Service) DarkK9" = "k50", "(Service) Vale" = "valeserv", "(Service) ValeDark" = "valeservdark",
|
||||
"(Janitor) Scrubpuppy" = "scrubpup")
|
||||
@@ -841,6 +851,8 @@
|
||||
service_icons = sortList(service_icons)
|
||||
var/service_robot_icon = show_radial_menu(R, R , service_icons, custom_check = CALLBACK(src, .proc/check_menu, R), radius = 42, require_near = TRUE)
|
||||
switch(service_robot_icon)
|
||||
if("Zoomba")
|
||||
cyborg_base_icon = "zoomba_jani"
|
||||
if("(Service) Waitress")
|
||||
cyborg_base_icon = "service_f"
|
||||
special_light_key = "service"
|
||||
@@ -944,6 +956,7 @@
|
||||
"Marina" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "marinamin"),
|
||||
"Can" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "canmin"),
|
||||
"Heavy" = image(icon = 'modular_citadel/icons/mob/robots.dmi', icon_state = "heavymin"),
|
||||
"Zoomba" = image(icon = 'icons/mob/robots.dmi', icon_state = "zoomba_miner")
|
||||
)
|
||||
var/list/L = list("Blade" = "blade", "Vale" = "valemine")
|
||||
for(var/a in L)
|
||||
@@ -987,6 +1000,8 @@
|
||||
cyborg_icon_override = 'modular_citadel/icons/mob/widerobot.dmi'
|
||||
sleeper_overlay = "valeminesleeper"
|
||||
dogborg = TRUE
|
||||
if("Zoomba")
|
||||
cyborg_base_icon = "zoomba_miner"
|
||||
else
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
@@ -396,6 +396,12 @@
|
||||
return aicamera.selectpicture(user)
|
||||
|
||||
/mob/living/silicon/proc/ai_roster()
|
||||
if(!client)
|
||||
return
|
||||
if(world.time < client.crew_manifest_delay)
|
||||
return
|
||||
client.crew_manifest_delay = world.time + (1 SECONDS)
|
||||
|
||||
var/dat = "<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Crew Roster</title></head><body><b>Crew Roster:</b><br><br>"
|
||||
|
||||
dat += GLOB.data_core.get_manifest()
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
/mob/living/silicon/attack_animal(mob/living/simple_animal/M)
|
||||
. = ..()
|
||||
if(.)
|
||||
var/damage = rand(M.melee_damage_lower, M.melee_damage_upper)
|
||||
var/damage = .
|
||||
if(prob(damage))
|
||||
for(var/mob/living/N in buckled_mobs)
|
||||
N.DefaultCombatKnockdown(20)
|
||||
@@ -120,6 +120,7 @@
|
||||
flash_act(affect_silicon = 1)
|
||||
|
||||
/mob/living/silicon/bullet_act(obj/item/projectile/P, def_zone)
|
||||
var/totaldamage = P.damage
|
||||
if(P.original != src || P.firer != src) //try to block or reflect the bullet, can't do so when shooting oneself
|
||||
var/list/returnlist = list()
|
||||
var/returned = mob_run_block(P, P.damage, "the [P.name]", ATTACK_TYPE_PROJECTILE, P.armour_penetration, P.firer, def_zone, returnlist)
|
||||
@@ -128,22 +129,18 @@
|
||||
if(returned & BLOCK_REDIRECTED)
|
||||
return BULLET_ACT_FORCE_PIERCE
|
||||
if(returned & BLOCK_SUCCESS)
|
||||
P.on_hit(src, 100, def_zone)
|
||||
P.on_hit(src, returnlist[BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE], def_zone)
|
||||
return BULLET_ACT_BLOCK
|
||||
totaldamage = block_calculate_resultant_damage(totaldamage, returnlist)
|
||||
if((P.damage_type == BRUTE || P.damage_type == BURN))
|
||||
adjustBruteLoss(P.damage)
|
||||
if(prob(P.damage*1.5))
|
||||
for(var/mob/living/M in buckled_mobs)
|
||||
M.visible_message("<span class='boldwarning'>[M] is knocked off of [src]!</span>",
|
||||
"<span class='boldwarning'>You are knocked off of [src]!</span>")
|
||||
unbuckle_mob(M)
|
||||
M.DefaultCombatKnockdown(40)
|
||||
if(P.stun || P.knockdown)
|
||||
adjustBruteLoss(totaldamage)
|
||||
if((P.damage >= 10) || P.stun || P.knockdown || (P.stamina >= 20))
|
||||
for(var/mob/living/M in buckled_mobs)
|
||||
unbuckle_mob(M)
|
||||
M.visible_message("<span class='boldwarning'>[M] is knocked off of [src] by the [P]!</span>",
|
||||
"<span class='boldwarning'>You are knocked off of [src] by the [P]!</span>")
|
||||
P.on_hit(src)
|
||||
unbuckle_mob(M)
|
||||
M.DefaultCombatKnockdown(40)
|
||||
P.on_hit(src, 0, def_zone)
|
||||
return BULLET_ACT_HIT
|
||||
|
||||
/mob/living/silicon/flash_act(intensity = 1, override_blindness_check = 0, affect_silicon = 0, visual = 0, type = /obj/screen/fullscreen/flash/static)
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
/mob/living/simple_animal/attack_animal(mob/living/simple_animal/M)
|
||||
. = ..()
|
||||
if(.)
|
||||
var/damage = rand(M.melee_damage_lower, M.melee_damage_upper)
|
||||
var/damage = .
|
||||
return attack_threshold_check(damage, M.melee_damage_type)
|
||||
|
||||
/mob/living/simple_animal/attack_slime(mob/living/simple_animal/slime/M)
|
||||
|
||||
@@ -98,6 +98,10 @@
|
||||
|
||||
hud_possible = list(DIAG_STAT_HUD, DIAG_BOT_HUD, DIAG_HUD, DIAG_PATH_HUD = HUD_LIST_LIST) //Diagnostic HUD views
|
||||
|
||||
var/commissioned = FALSE // Will other (noncommissioned) bots salute this bot?
|
||||
var/can_salute = TRUE
|
||||
var/salute_delay = 60 SECONDS
|
||||
|
||||
/mob/living/simple_animal/bot/proc/get_mode()
|
||||
if(client) //Player bots do not have modes, thus the override. Also an easy way for PDA users/AI to know when a bot is a player.
|
||||
if(paicard)
|
||||
@@ -251,6 +255,14 @@
|
||||
if(!on || client)
|
||||
return
|
||||
|
||||
if(!commissioned && can_salute)
|
||||
for(var/mob/living/simple_animal/bot/B in get_hearers_in_view(5, get_turf(src)))
|
||||
if(B.commissioned)
|
||||
visible_message("<b>[src]</b> performs an elaborate salute for [B]!")
|
||||
can_salute = FALSE
|
||||
addtimer(VARSET_CALLBACK(src, can_salute, TRUE), salute_delay)
|
||||
break
|
||||
|
||||
switch(mode) //High-priority overrides are processed first. Bots can do nothing else while under direct command.
|
||||
if(BOT_RESPONDING) //Called by the AI.
|
||||
call_mode()
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
model = "Cleanbot"
|
||||
bot_core_type = /obj/machinery/bot_core/cleanbot
|
||||
window_id = "autoclean"
|
||||
window_name = "Automatic Station Cleaner v1.2"
|
||||
window_name = "Automatic Station Cleaner v1.3"
|
||||
pass_flags = PASSMOB
|
||||
path_image_color = "#993299"
|
||||
weather_immunities = list("lava","ash")
|
||||
@@ -36,8 +36,62 @@
|
||||
var/next_dest
|
||||
var/next_dest_loc
|
||||
|
||||
var/obj/item/weapon
|
||||
var/weapon_orig_force = 0
|
||||
var/chosen_name
|
||||
|
||||
var/list/stolen_valor
|
||||
|
||||
var/static/list/officers = list("Captain", "Head of Personnel", "Head of Security")
|
||||
var/static/list/command = list("Captain" = "Cpt.","Head of Personnel" = "Lt.")
|
||||
var/static/list/security = list("Head of Security" = "Maj.", "Warden" = "Sgt.", "Detective" = "Det.", "Security Officer" = "Officer")
|
||||
var/static/list/engineering = list("Chief Engineer" = "Chief Engineer", "Station Engineer" = "Engineer", "Atmospherics Technician" = "Technician")
|
||||
var/static/list/medical = list("Chief Medical Officer" = "C.M.O.", "Medical Doctor" = "M.D.", "Chemist" = "Pharm.D.")
|
||||
var/static/list/research = list("Research Director" = "Ph.D.", "Roboticist" = "M.S.", "Scientist" = "B.S.")
|
||||
var/static/list/legal = list("Lawyer" = "Esq.")
|
||||
|
||||
var/list/prefixes
|
||||
var/list/suffixes
|
||||
|
||||
/mob/living/simple_animal/bot/cleanbot/proc/deputize(obj/item/W, mob/user)
|
||||
if(in_range(src, user))
|
||||
to_chat(user, "<span class='notice'>You attach \the [W] to \the [src].</span>")
|
||||
user.transferItemToLoc(W, src)
|
||||
weapon = W
|
||||
weapon_orig_force = weapon.force
|
||||
if(!emagged)
|
||||
weapon.force = weapon.force / 2
|
||||
add_overlay(image(icon=weapon.lefthand_file,icon_state=weapon.item_state))
|
||||
|
||||
/mob/living/simple_animal/bot/cleanbot/proc/update_titles()
|
||||
var/working_title = ""
|
||||
|
||||
for(var/pref in prefixes)
|
||||
for(var/title in pref)
|
||||
if(title in stolen_valor)
|
||||
working_title += pref[title] + " "
|
||||
if(title in officers)
|
||||
commissioned = TRUE
|
||||
break
|
||||
|
||||
working_title += chosen_name
|
||||
|
||||
for(var/suf in suffixes)
|
||||
for(var/title in suf)
|
||||
if(title in stolen_valor)
|
||||
working_title += " " + suf[title]
|
||||
break
|
||||
|
||||
name = working_title
|
||||
|
||||
/mob/living/simple_animal/bot/cleanbot/examine(mob/user)
|
||||
. = ..()
|
||||
if(weapon)
|
||||
. += " <span class='warning'>Is that \a [weapon] taped to it...?</span>"
|
||||
|
||||
/mob/living/simple_animal/bot/cleanbot/Initialize()
|
||||
. = ..()
|
||||
chosen_name = name
|
||||
get_targets()
|
||||
icon_state = "cleanbot[on]"
|
||||
|
||||
@@ -45,6 +99,18 @@
|
||||
access_card.access += J.get_access()
|
||||
prev_access = access_card.access
|
||||
|
||||
stolen_valor = list()
|
||||
|
||||
prefixes = list(command, security, engineering)
|
||||
suffixes = list(research, medical, legal)
|
||||
|
||||
/mob/living/simple_animal/bot/cleanbot/Destroy()
|
||||
if(weapon)
|
||||
var/atom/Tsec = drop_location()
|
||||
weapon.force = weapon_orig_force
|
||||
drop_part(weapon, Tsec)
|
||||
return ..()
|
||||
|
||||
/mob/living/simple_animal/bot/cleanbot/turn_on()
|
||||
..()
|
||||
icon_state = "cleanbot[on]"
|
||||
@@ -57,6 +123,8 @@
|
||||
|
||||
/mob/living/simple_animal/bot/cleanbot/bot_reset()
|
||||
..()
|
||||
if(weapon && (emagged == 2))
|
||||
weapon.force = weapon_orig_force
|
||||
ignore_list = list() //Allows the bot to clean targets it previously ignored due to being unreachable.
|
||||
target = null
|
||||
oldloc = null
|
||||
@@ -66,6 +134,22 @@
|
||||
text_dehack = "[name]'s software has been reset!"
|
||||
text_dehack_fail = "[name] does not seem to respond to your repair code!"
|
||||
|
||||
/mob/living/simple_animal/bot/cleanbot/Crossed(atom/movable/AM)
|
||||
. = ..()
|
||||
|
||||
zone_selected = pick(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)
|
||||
if(weapon && has_gravity() && ismob(AM))
|
||||
var/mob/living/carbon/C = AM
|
||||
if(!istype(C))
|
||||
return
|
||||
|
||||
if(!(C.job in stolen_valor))
|
||||
stolen_valor += C.job
|
||||
update_titles()
|
||||
|
||||
weapon.attack(C, src)
|
||||
C.Knockdown(20)
|
||||
|
||||
/mob/living/simple_animal/bot/cleanbot/attackby(obj/item/W, mob/user, params)
|
||||
if(istype(W, /obj/item/card/id)||istype(W, /obj/item/pda))
|
||||
if(bot_core.allowed(user) && !open && !emagged)
|
||||
@@ -79,6 +163,11 @@
|
||||
else
|
||||
to_chat(user, "<span class='notice'>The [src] doesn't seem to respect your authority.</span>")
|
||||
|
||||
else if(istype(W, /obj/item/kitchen/knife) && user.a_intent != INTENT_HARM)
|
||||
to_chat(user, "<span class='notice'>You start attaching \the [W] to \the [src]...</span>")
|
||||
if(do_after(user, 25, target = src))
|
||||
deputize(W, user)
|
||||
|
||||
else if(istype(W, /obj/item/mop/advanced))
|
||||
if(bot_core.allowed(user) && open && !CHECK_BITFIELD(upgrades,UPGRADE_CLEANER_ADVANCED_MOP))
|
||||
to_chat(user, "<span class='notice'>You replace \the [src] old mop with a new better one!</span>")
|
||||
@@ -87,10 +176,10 @@
|
||||
window_name = "Automatic Station Cleaner v2.1 BETA" //New!
|
||||
qdel(W)
|
||||
if(!open)
|
||||
to_chat(user, "<span class='notice'>The [src] access pannle is not open!</span>")
|
||||
to_chat(user, "<span class='notice'>The [src] access panel is not open!</span>")
|
||||
return
|
||||
if(!bot_core.allowed(user))
|
||||
to_chat(user, "<span class='notice'>The [src] access pannel locked off to you!</span>")
|
||||
to_chat(user, "<span class='notice'>The [src] access panel locked off to you!</span>")
|
||||
return
|
||||
else
|
||||
to_chat(user, "<span class='notice'>The [src] already has this mop!</span>")
|
||||
@@ -116,6 +205,8 @@
|
||||
/mob/living/simple_animal/bot/cleanbot/emag_act(mob/user)
|
||||
. = ..()
|
||||
if(emagged == 2)
|
||||
if(weapon)
|
||||
weapon.force = weapon_orig_force
|
||||
if(user)
|
||||
to_chat(user, "<span class='danger'>[src] buzzes and beeps.</span>")
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
speak_emote = list("chitters")
|
||||
density = FALSE
|
||||
ventcrawler = VENTCRAWLER_ALWAYS
|
||||
mob_size = MOB_SIZE_TINY
|
||||
gold_core_spawnable = FRIENDLY_SPAWN
|
||||
verb_say = "chitters"
|
||||
verb_ask = "chitters inquisitively"
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
friendly_verb_continuous = "pinches"
|
||||
friendly_verb_simple = "pinch"
|
||||
ventcrawler = VENTCRAWLER_ALWAYS
|
||||
mob_size = MOB_SIZE_TINY
|
||||
var/obj/item/inventory_head
|
||||
var/obj/item/inventory_mask
|
||||
gold_core_spawnable = FRIENDLY_SPAWN
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
response_disarm_simple = "gently push aside"
|
||||
response_harm_continuous = "kicks"
|
||||
response_harm_simple = "kick"
|
||||
mob_size = MOB_SIZE_SMALL
|
||||
mob_biotypes = MOB_ORGANIC|MOB_BEAST
|
||||
gold_core_spawnable = FRIENDLY_SPAWN
|
||||
melee_damage_lower = 18
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
Move(get_step(src,chosen_dir))
|
||||
face_atom(target) //Looks better if they keep looking at you when dodging
|
||||
|
||||
/mob/living/simple_animal/hostile/attacked_by(obj/item/I, mob/living/user)
|
||||
/mob/living/simple_animal/hostile/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1)
|
||||
if(stat == CONSCIOUS && !target && AIStatus != AI_OFF && !client && user)
|
||||
FindTarget(list(user), 1)
|
||||
return ..()
|
||||
|
||||
@@ -103,7 +103,7 @@ IGNORE_PROC_IF_NOT_TARGET(attack_slime)
|
||||
return BULLET_ACT_FORCE_PIERCE
|
||||
return ..()
|
||||
|
||||
/mob/living/simple_animal/hostile/asteroid/curseblob/attacked_by(obj/item/I, mob/living/L)
|
||||
/mob/living/simple_animal/hostile/asteroid/curseblob/attacked_by(obj/item/I, mob/living/L, attackchain_flags = NONE, damage_multiplier = 1)
|
||||
if(L != set_target)
|
||||
L.changeNext_move(I.click_delay) //pre_attacked_by not called
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
icon = 'icons/mob/slimes.dmi'
|
||||
icon_state = "grey baby slime"
|
||||
pass_flags = PASSTABLE
|
||||
mob_size = MOB_SIZE_SMALL
|
||||
ventcrawler = VENTCRAWLER_ALWAYS
|
||||
gender = NEUTER
|
||||
var/is_adult = 0
|
||||
|
||||
@@ -1086,3 +1086,10 @@ GLOBAL_VAR_INIT(exploit_warn_spam_prevention, 0)
|
||||
for(var/obj/item/I in held_items)
|
||||
if(I.item_flags & SLOWS_WHILE_IN_HAND)
|
||||
. += I.slowdown
|
||||
|
||||
/**
|
||||
* Mostly called by doUnEquip()
|
||||
* Like item dropped() on mob side.
|
||||
*/
|
||||
/mob/proc/on_item_dropped(obj/item/I)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user