diff --git a/code/game/objects/items/glassjar.dm b/code/game/objects/items/glassjar.dm index 2e73e89e67c..ce5bfbe91e6 100644 --- a/code/game/objects/items/glassjar.dm +++ b/code/game/objects/items/glassjar.dm @@ -6,7 +6,7 @@ w_class = ITEMSIZE_SMALL matter = list("glass" = 200) flags = NOBLUDGEON - var/list/accept_mobs = list(/mob/living/simple_mob/animal/passive/lizard, /mob/living/simple_mob/animal/passive/mouse) + var/list/accept_mobs = list(/mob/living/simple_mob/animal/passive/lizard, /mob/living/simple_mob/animal/passive/mouse, /mob/living/simple_mob/animal/sif/leech, /mob/living/simple_mob/animal/sif/frostfly, /mob/living/simple_mob/animal/sif/glitterfly) var/contains = 0 // 0 = nothing, 1 = money, 2 = animal, 3 = spiderling /obj/item/glass_jar/New() @@ -96,6 +96,12 @@ for(var/mob/M in src) var/image/victim = image(M.icon, M.icon_state) victim.pixel_y = 6 + victim.color = M.color + if(M.plane == PLANE_LIGHTING_ABOVE) // This will only show up on the ground sprite, due to the HuD being over it, so we need both images. + var/image/victim_glow = image(M.icon, M.icon_state) + victim_glow.pixel_y = 6 + victim_glow.color = M.color + underlays += victim_glow underlays += victim name = "glass jar with [M]" desc = "A small jar with [M] inside." diff --git a/code/modules/ai/ai_holder_targeting.dm b/code/modules/ai/ai_holder_targeting.dm index b13d4d68440..dfe5c85b63b 100644 --- a/code/modules/ai/ai_holder_targeting.dm +++ b/code/modules/ai/ai_holder_targeting.dm @@ -24,7 +24,7 @@ // Step 1, find out what we can see. /datum/ai_holder/proc/list_targets() - . = hearers(vision_range, holder) - src // Remove ourselves to prevent suicidal decisions. + . = hearers(vision_range, holder) - holder // Remove ourselves to prevent suicidal decisions. ~ SRC is the ai_holder. var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha)) diff --git a/code/modules/mob/_modifiers/modifiers_misc.dm b/code/modules/mob/_modifiers/modifiers_misc.dm index 64be119d7c4..7d1ab792fff 100644 --- a/code/modules/mob/_modifiers/modifiers_misc.dm +++ b/code/modules/mob/_modifiers/modifiers_misc.dm @@ -269,6 +269,20 @@ the artifact triggers the rage. return FALSE return TRUE +/datum/modifier/poisoned/paralysis + desc = "You have poison inside of you. It will cause harm over a long span of time if not cured, and may cause temporary paralysis." + on_created_text = "You feel incredibly weak..." + damage_per_tick = 0.75 + +/datum/modifier/poisoned/paralysis/tick() + ..() + if(prob(5)) + holder.Paralyse(3) + +/datum/modifier/poisoned/paralysis/on_applied() + ..() + holder.Paralyse(4) + // Pulse modifier. /datum/modifier/false_pulse name = "false pulse" @@ -278,4 +292,4 @@ the artifact triggers the rage. on_expired_text = "You feel.. different." stacks = MODIFIER_STACK_EXTEND - pulse_set_level = PULSE_NORM + pulse_set_level = PULSE_NORM \ No newline at end of file diff --git a/code/modules/mob/holder.dm b/code/modules/mob/holder.dm index 584dc51ad5a..25a6ed122ab 100644 --- a/code/modules/mob/holder.dm +++ b/code/modules/mob/holder.dm @@ -104,6 +104,10 @@ var/list/holder_mob_icon_cache = list() /obj/item/weapon/holder/borer origin_tech = list(TECH_BIO = 6) +/obj/item/weapon/holder/leech + color = "#003366" + origin_tech = list(TECH_BIO = 5, TECH_PHORON = 2) + /obj/item/weapon/holder/fish attack_verb = list("fished", "disrespected", "smacked", "smackereled") hitsound = 'sound/effects/slime_squish.ogg' diff --git a/code/modules/mob/living/simple_mob/simple_mob.dm b/code/modules/mob/living/simple_mob/simple_mob.dm index 0cd734ec066..c70094d5792 100644 --- a/code/modules/mob/living/simple_mob/simple_mob.dm +++ b/code/modules/mob/living/simple_mob/simple_mob.dm @@ -47,6 +47,7 @@ var/movement_sound = null // If set, will play this sound when it moves on its own will. var/turn_sound = null // If set, plays the sound when the mob's dir changes in most cases. var/movement_shake_radius = 0 // If set, moving will shake the camera of all living mobs within this radius slightly. + var/aquatic_movement = 0 // If set, the mob will move through fluids with no hinderance. //Mob interaction var/response_help = "tries to help" // If clicked on help intent @@ -230,8 +231,11 @@ // Turf related slowdown var/turf/T = get_turf(src) - if(T && T.movement_cost && !hovering) // Flying mobs ignore turf-based slowdown. - tally += T.movement_cost + if(T && T.movement_cost && !hovering) // Flying mobs ignore turf-based slowdown. Aquatic mobs ignore water slowdown, and can gain bonus speed in it. + if(istype(T,/turf/simulated/floor/water) && aquatic_movement) + tally -= aquatic_movement - 1 + else + tally += T.movement_cost if(purge)//Purged creatures will move more slowly. The more time before their purge stops, the slower they'll move. if(tally <= 0) diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/sif/duck.dm b/code/modules/mob/living/simple_mob/subtypes/animal/sif/duck.dm new file mode 100644 index 00000000000..0aa74f9e6b7 --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/animal/sif/duck.dm @@ -0,0 +1,58 @@ +// Crystal-feather "ducks" are rather weak, but will become aggressive if you have food. + +/datum/category_item/catalogue/fauna/crystalduck + name = "Sivian Fauna - Crystal-Feather Duck" + desc = "Classification: S Anatidae vitriae \ +

\ + Small, asocial omnivores with glistening, razor-thin feathers valued for their use as a reflective glitter by poachers. \ + The Crystal-Feather Duck commonly forms at most a familial group of four, a male, two females, and a single 'chosen' young. Primarily detrivorous browsers, \ + supplementing their diet with animals living below thinner shore ice-sheets. \ + Family units have been observed to form gangs and scavenge from Sivian domeciles and \ + various food transports during stressful months. \ + It is advised to seal and hide any form of food near even lone individuals, as they will become \ + increasingly aggressive." + value = CATALOGUER_REWARD_EASY + +/mob/living/simple_mob/animal/sif/duck + name = "crystal-feather duck" + desc = "A glittering flightless bird." + tt_desc = "S Anatidae vitriae" + catalogue_data = list(/datum/category_item/catalogue/fauna/crystalduck) + + faction = "duck" + + icon_state = "duck" + icon_living = "duck" + icon_dead = "duck_dead" + icon = 'icons/mob/animal.dmi' + has_eye_glow = TRUE + + maxHealth = 50 + health = 50 + + movement_cooldown = 0 + + melee_damage_lower = 2 + melee_damage_upper = 10 + base_attack_cooldown = 1 SECOND + attack_edge = 1 // Razor-edged wings, and 'claws' made for digging through ice. + attacktext = list("nipped", "bit", "cut", "clawed") + + say_list_type = /datum/say_list/duck + ai_holder_type = /datum/ai_holder/simple_mob/retaliate/cooperative + +/datum/say_list/duck + speak = list("Wack!", "Wock?", "Wack.") + emote_see = list("ruffles its wings","looks around", "preens itself") + emote_hear = list("quacks", "giggles") + +/mob/living/simple_mob/animal/sif/duck/IIsAlly(mob/living/L) + . = ..() + + var/has_food = FALSE + for(var/obj/item/I in L.get_contents()) // Do they have food? + if(istype(I, /obj/item/weapon/reagent_containers/food)) + has_food = TRUE + break + if(has_food) // Yes? Gimme the food. + return FALSE diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/sif/frostfly.dm b/code/modules/mob/living/simple_mob/subtypes/animal/sif/frostfly.dm new file mode 100644 index 00000000000..5711868d157 --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/animal/sif/frostfly.dm @@ -0,0 +1,169 @@ +// Frostflies are large, flightless insects with glittering wings, used as a means of deploying their gaseous self-defense mechanism. +/datum/category_item/catalogue/fauna/frostfly + name = "Sivian Fauna - Frostfly" + desc = "Classification: S Carabidae glacios \ +

\ + A large, flightless insectoid with bioluminescent wings. \ + Frostflies utilize their vestigial wings as a method of dispersing a chemical that produces a rapid \ + endothermic reaction on contact with the air, resulting in the flash-freezing of nearby materials. \ +
\ + Carnivorous in nature, they use their cryogenic compounds to trap smaller prey, or frighten predators. \ + Individuals are known to slalom when facing other creatures, dispersing clouds of gas, and spitting \ + condensed globs of the compound. These masses of mucous and ice seem to be intended to impede movement. \ +
\ + Travelers are advised to avoid frostfly swarms whenever possible, as they will become aggressive \ + to anything other than Diyaabs, which they seem to have formed a tangential symbiosis with." + value = CATALOGUER_REWARD_MEDIUM + +/mob/living/simple_mob/animal/sif/frostfly + name = "frostfly" + desc = "A large insect with glittering wings." + tt_desc = "S Carabidae glacios" + catalogue_data = list(/datum/category_item/catalogue/fauna/frostfly) + + faction = "diyaab" + + icon_state = "firefly" + icon_living = "firefly" + icon_dead = "firefly_dead" + icon_rest = "firefly_dead" + icon = 'icons/mob/animal.dmi' + has_eye_glow = TRUE + + maxHealth = 65 + health = 65 + + pass_flags = PASSTABLE + + var/energy = 100 + var/max_energy = 100 + + movement_cooldown = 0.5 + + melee_damage_lower = 5 + melee_damage_upper = 10 + base_attack_cooldown = 1.5 SECONDS + attacktext = list("nipped", "bit", "pinched") + + projectiletype = /obj/item/projectile/energy/blob/freezing + + special_attack_cooldown = 5 SECONDS + special_attack_min_range = 0 + special_attack_max_range = 4 + + armor = list( + "melee" = 20, + "bullet" = 10, + "laser" = 5, + "energy" = 0, + "bomb" = 10, + "bio" = 100, + "rad" = 100 + ) + + // The frostfly's body is incredibly cold at all times, natural resistance to things trying to burn it. + armor_soak = list( + "melee" = 0, + "bullet" = 0, + "laser" = 15, + "energy" = 0, + "bomb" = 0, + "bio" = 0, + "rad" = 0 + ) + + var/datum/effect/effect/system/smoke_spread/frost/smoke_special + + say_list_type = /datum/say_list/frostfly + ai_holder_type = /datum/ai_holder/simple_mob/ranged/kiting/threatening/frostfly + +/mob/living/simple_mob/animal/sif/frostfly/get_cold_protection() + return 1 // It literally produces a cryogenic mist inside itself. Cold doesn't bother it. + +/mob/living/simple_mob/animal/sif/frostfly/Initialize() + ..() + smoke_special = new + verbs += /mob/living/proc/ventcrawl + verbs += /mob/living/proc/hide + +/datum/say_list/frostfly + speak = list("Zzzz.", "Kss.", "Zzt?") + emote_see = list("flutters its wings","looks around", "rubs its mandibles") + emote_hear = list("chitters", "clicks", "chirps") + + say_understood = list("Ssst.") + say_cannot = list("Zzrt.") + say_maybe_target = list("Ki?") + say_got_target = list("Ksst!") + say_threaten = list("Kszsz.","Kszzt...","Kzzi!") + say_stand_down = list("Sss.","Zt.","! clicks.") + say_escalate = list("Rszt!") + + threaten_sound = 'sound/effects/refill.ogg' + stand_down_sound = 'sound/effects/sparks5.ogg' + +/mob/living/simple_mob/animal/sif/frostfly/handle_special() + ..() + + if(energy < max_energy) + energy++ + +/mob/living/simple_mob/animal/sif/frostfly/Stat() + ..() + if(client.statpanel == "Status") + statpanel("Status") + if(emergency_shuttle) + var/eta_status = emergency_shuttle.get_status_panel_eta() + if(eta_status) + stat(null, eta_status) + stat("Energy", energy) + +/mob/living/simple_mob/animal/sif/frostfly/should_special_attack(atom/A) + if(energy >= 20) + return TRUE + return FALSE + +/mob/living/simple_mob/animal/sif/frostfly/do_special_attack(atom/A) + . = TRUE + switch(a_intent) + if(I_DISARM) + if(energy < 20) + return FALSE + + energy -= 20 + + if(smoke_special) + smoke_special.set_up(7,0,src) + smoke_special.start() + return TRUE + + return FALSE + +/datum/ai_holder/simple_mob/ranged/kiting/threatening/frostfly + can_flee = TRUE + dying_threshold = 0.5 + flee_when_outmatched = TRUE + run_if_this_close = 3 + +/datum/ai_holder/simple_mob/ranged/kiting/threatening/frostfly/special_flee_check() + var/mob/living/simple_mob/animal/sif/frostfly/F = holder + if(F.energy < F.max_energy * 0.2) + return TRUE + return FALSE + +/datum/ai_holder/simple_mob/ranged/kiting/threatening/frostfly/pre_special_attack(atom/A) + if(isliving(A)) + holder.a_intent = I_DISARM + else + holder.a_intent = I_HURT + +/datum/ai_holder/simple_mob/ranged/kiting/threatening/frostfly/post_ranged_attack(atom/A) + var/mob/living/simple_mob/animal/sif/frostfly/F = holder + if(istype(A,/mob/living)) + var/new_dir = turn(F.dir, -90) + if(prob(50)) + new_dir = turn(F.dir, 90) + holder.IMove(get_step(holder, new_dir)) + holder.face_atom(A) + + F.energy = max(0, F.energy - 1) // The AI will eventually flee. diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/sif/glitterfly.dm b/code/modules/mob/living/simple_mob/subtypes/animal/sif/glitterfly.dm new file mode 100644 index 00000000000..e455f221684 --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/animal/sif/glitterfly.dm @@ -0,0 +1,102 @@ +// Glitterflies! Little butterfly-like creatures that float around and be cute. + +/datum/category_item/catalogue/fauna/glitterfly + name = "Sivian Fauna - Glitterfly" + desc = "Classification: S Lepidoptera adamas \ +

\ + An herbivorous insectoid beloved for its glittering wings. \ +
\ + The creature has four wings mirrored laterally, which reflect \ + the ambient light to provide a measure of camouflage." + value = CATALOGUER_REWARD_TRIVIAL + +/datum/category_item/catalogue/fauna/glitterfly_rare + name = "Sivian Fauna - Adela's Glitterfly" + desc = "Classification: S Lepidoptera adamas \ +

\ + This mutation of the glitterfly is incredibly rare, one of the few and first known individuals\ + known to exist with it is owned by little-known Sivian ecologist Adela Svingelm, and kept within its own \ + climate-controlled habitat." + value = CATALOGUER_REWARD_HARD + +/mob/living/simple_mob/animal/sif/glitterfly + name = "glitterfly" + desc = "A shiny butterfly!" + tt_desc = "S Lepidoptera adamas" + catalogue_data = list(/datum/category_item/catalogue/fauna/glitterfly) + + faction = "neutral" + + icon_state = "butterfly" + icon_living = "butterfly" + icon_dead = "butterfly_dead" + icon = 'icons/mob/animal.dmi' + + maxHealth = 10 + health = 10 + + movement_cooldown = -1 + hovering = TRUE + + melee_damage_lower = 1 + melee_damage_upper = 2 + attack_armor_pen = 80 + attack_sharp = TRUE + + density = FALSE // Non-dense, so things can walk through their groups unhindered. + + pass_flags = PASSTABLE + + attacktext = list("bit", "buffeted", "slashed") + + say_list_type = /datum/say_list/glitterfly + ai_holder_type = /datum/ai_holder/simple_mob/melee/evasive/glitterfly + +/mob/living/simple_mob/animal/sif/glitterfly/Initialize() + ..() + var/colorlist = list(rgb(rand(100,255), rand(100,255), rand(100,255)) = 10, rgb(rand(5,100), rand(5,100), rand(5,100)) = 2, "#222222" = 1) + color = pickweight(colorlist) + + default_pixel_y = rand(5,12) + pixel_y = default_pixel_y + +/mob/living/simple_mob/animal/sif/glitterfly/rare + name = "sparkling glitterfly" + desc = "An incredibly shiny butterfly!" + catalogue_data = list(/datum/category_item/catalogue/fauna/glitterfly, /datum/category_item/catalogue/fauna/glitterfly_rare) + maxHealth = 30 + health = 30 + + movement_cooldown = -2 + + melee_damage_upper = 5 + + plane = PLANE_LIGHTING_ABOVE + +/mob/living/simple_mob/animal/sif/glitterfly/rare/Initialize() + ..() + +/datum/say_list/glitterfly + speak = list("Pi..","Po...", "Pa...") + emote_see = list("vibrates","flutters", "twirls") + emote_hear = list("pips", "clicks", "chirps") + +/datum/ai_holder/simple_mob/melee/evasive/glitterfly + hostile = FALSE + can_flee = TRUE + flee_when_outmatched = TRUE + outmatched_threshold = 100 + max_home_distance = 5 + +/datum/ai_holder/simple_mob/melee/evasive/glitterfly/handle_special_strategical() + if(prob(1)) + var/friendly_animal_corpse = FALSE + for(var/mob/living/simple_mob/animal/A in view(vision_range,holder)) + if(holder.IIsAlly(A) && A.stat == DEAD) + friendly_animal_corpse = TRUE + break + + if(friendly_animal_corpse) + hostile = TRUE + return + hostile = initial(hostile) diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/sif/kururak.dm b/code/modules/mob/living/simple_mob/subtypes/animal/sif/kururak.dm new file mode 100644 index 00000000000..22c2d020829 --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/animal/sif/kururak.dm @@ -0,0 +1,404 @@ +// Kururaks, large pack-hunting felinids that reside in coastal regions. Less slowdown in water, speed on rocky turf. + +/datum/category_item/catalogue/fauna/kururak + name = "Sivian Fauna - Kururak" + desc = "Classification: S Felidae fluctursora \ +

\ + An uncommon sight to many Sivian residents, these creatures are hypercarnivores, with\ + their diets almost exclusively consisting of other fauna. This is achieved in the frozen \ + environments of Sif via the means of fishing, even going to the lengths of evolving a \ + third, dense lung only inflated for long dives. \ +
\ + One of the most distinguishing features of these animals are their four tails, capped in \ + reflective 'mirrors'. These mirrors are used for tricking fish and other prey into ambushes,\ + or distract would-be rivals. \ +
\ + Kururak packs are incredibly dangerous if faced alone, and should only be approached if prepared \ + for a fight." + value = CATALOGUER_REWARD_HARD + +/mob/living/simple_mob/animal/sif/kururak + name = "kururak" + desc = "A large animal with sleek fur." + tt_desc = "S Felidae fluctursora" + catalogue_data = list(/datum/category_item/catalogue/fauna/kururak) + + faction = "kururak" + + icon_state = "bigcat" + icon_living = "bigcat" + icon_dead = "bigcat_dead" + icon_rest = "bigcat_rest" + icon = 'icons/mob/64x64.dmi' + + default_pixel_x = -16 + pixel_x = -16 + + maxHealth = 200 + health = 200 + + universal_understand = 1 + + movement_cooldown = 1 + + melee_damage_lower = 15 + melee_damage_upper = 25 + attack_armor_pen = 40 + base_attack_cooldown = 2 SECONDS + attacktext = list("gouged", "bit", "cut", "clawed", "whipped") + + armor = list( + "melee" = 30, + "bullet" = 15, + "laser" = 5, + "energy" = 0, + "bomb" = 10, + "bio" = 100, + "rad" = 100 + ) + + armor_soak = list( + "melee" = 5, + "bullet" = 5, + "laser" = 5, + "energy" = 0, + "bomb" = 0, + "bio" = 0, + "rad" = 0 + ) + + say_list_type = /datum/say_list/kururak + ai_holder_type = /datum/ai_holder/simple_mob/intentional/kururak + + special_attack_min_range = 0 + special_attack_max_range = 4 + special_attack_cooldown = 30 SECONDS + + // Players have 2 seperate cooldowns for these, while the AI must choose one. Both respect special_attack_cooldown + var/last_strike_time = 0 + var/last_flash_time = 0 + + var/instinct // The points used by Kururaks to decide Who Is The Boss + var/obey_pack_rule = TRUE // Decides if the Kururak will automatically assign itself to follow the one with the highest instinct. + +/datum/say_list/kururak + speak = list("Kurr?","|R|rrh..", "Ksss...") + emote_see = list("scratches its ear","flutters its tails", "flicks an ear", "shakes out its hair") + emote_hear = list("chirps", "clicks", "grumbles", "chitters") + +/mob/living/simple_mob/animal/sif/kururak/leader // Going to be the starting leader. Has some base buffs to make it more likely to stay the leader. + maxHealth = 250 + health = 250 + instinct = 50 + +/mob/living/simple_mob/animal/sif/kururak/Initialize() + ..() + if(!instinct) + if(prob(20)) + instinct = rand(6, 10) + return + instinct = rand(0, 5) + +/mob/living/simple_mob/animal/sif/kururak/IIsAlly(mob/living/L) + . = ..() + if(!.) + if(issilicon(L)) // Metal things are usually reflective, or in general aggrivating. + return FALSE + if(ishuman(L)) // Might be metal, but they're humanoid shaped. + var/mob/living/carbon/human/H = L + if(H.get_active_hand()) + var/obj/item/I = H.get_active_hand() + if(I.force >= 1.20 * melee_damage_upper) + return TRUE + else if(istype(L, /mob/living/simple_mob)) + var/mob/living/simple_mob/S = L + if(S.melee_damage_upper > 1.20 * melee_damage_upper) + return TRUE + +/mob/living/simple_mob/animal/sif/kururak/handle_special() + ..() + if(client) + pack_gauge() + +/mob/living/simple_mob/animal/sif/kururak/apply_melee_effects(atom/A) // Only gains instinct. + instinct += rand(1, 2) + return + +/mob/living/simple_mob/animal/sif/kururak/should_special_attack(atom/A) + return has_modifier_of_type(/datum/modifier/ace) + +/mob/living/simple_mob/animal/sif/kururak/do_special_attack(atom/A) + . = TRUE + switch(a_intent) + if(I_DISARM) // Ranged mob flash, will also confuse borgs rather than stun. + tail_flash(A) + if(I_GRAB) // Armor-ignoring hit, causes agonizing wounds. + set_AI_busy(TRUE) + rending_strike(A) + set_AI_busy(FALSE) + +/mob/living/simple_mob/animal/sif/kururak/verb/do_flash() + set category = "Abilities" + set name = "Tail Blind" + set desc = "Disorient a creature within range." + + if(world.time < last_flash_time + special_attack_cooldown) + to_chat(src, span("warning", "You do not have the focus to do this so soon..")) + return + + last_flash_time = world.time + tail_flash() + +/mob/living/simple_mob/animal/sif/kururak/proc/tail_flash(atom/A) + set waitfor = FALSE + + if(stat) + to_chat(src, span("warning","You cannot move your tails in this state..")) + return + + if(!A && src.client) + var/list/choices = list() + for(var/mob/living/carbon/C in view(1,src)) + if(src.Adjacent(C)) + choices += C + + for(var/obj/mecha/M in view(1,src)) + if(src.Adjacent(M)) + choices += M + + if(!choices.len) + choices["radial"] = get_turf(src) + + A = input(src,"What do we wish to flash?") in null|choices + + + visible_message(span("alien","\The [src] flares its tails!")) + if(isliving(A)) + var/mob/living/L = A + if(iscarbon(L)) + var/mob/living/carbon/C = L + if(C.stat != DEAD) + var/safety = C.eyecheck() + if(safety <= 0) + var/flash_strength = 5 + if(ishuman(C)) + var/mob/living/carbon/human/H = C + flash_strength *= H.species.flash_mod + if(flash_strength > 0) + to_chat(H, span("alien","You are disoriented by \the [src]!")) + H.Confuse(flash_strength + 5) + H.Blind(flash_strength) + H.eye_blurry = max(H.eye_blurry, flash_strength + 5) + H.flash_eyes() + H.adjustHalLoss(flash_strength / 5) + H.apply_damage(flash_strength * H.species.flash_burn/5, BURN, BP_HEAD, 0, 0, "Photon burns") + + else if(issilicon(L)) + if(isrobot(L)) + var/flashfail = FALSE + var/mob/living/silicon/robot/R = L + if(R.has_active_type(/obj/item/borg/combat/shield)) + var/obj/item/borg/combat/shield/shield = locate() in R + if(shield) + if(shield.active) + shield.adjust_flash_count(R, 1) + flashfail = TRUE + if(!flashfail) + to_chat(R, span("alien","Your optics are scrambled by \the [src]!")) + R.Confuse(10) + R.flash_eyes() + + else + L.Confuse(10) + L.flash_eyes() + + else + for(var/mob/living/carbon/C in oviewers(special_attack_max_range, null)) + var/safety = C.eyecheck() + if(!safety) + if(!C.blinded) + C.flash_eyes() + for(var/mob/living/silicon/robot/R in oviewers(special_attack_max_range, null)) + if(R.has_active_type(/obj/item/borg/combat/shield)) + var/obj/item/borg/combat/shield/shield = locate() in R + if(shield) + if(shield.active) + continue + R.flash_eyes() + +/mob/living/simple_mob/animal/sif/kururak/verb/do_strike() + set category = "Abilities" + set name = "Rending Strike" + set desc = "Strike viciously at an entity within range." + + if(world.time < last_strike_time + special_attack_cooldown) + to_chat(src, span("warning", "Your claws cannot take that much stress in so short a time..")) + return + + last_strike_time = world.time + rending_strike() + +/mob/living/simple_mob/animal/sif/kururak/proc/rending_strike(atom/A) + if(stat) + to_chat(src, span("warning","You cannot strike in this state..")) + return + + if(!A && src.client) + var/list/choices = list() + for(var/mob/living/carbon/C in view(1,src)) + if(src.Adjacent(C)) + choices += C + + for(var/obj/mecha/M in view(1,src)) + if(src.Adjacent(M)) + choices += M + + if(!choices.len) + to_chat(src, span("warning","There are no viable targets within range...")) + return + + A = input(src,"What do we wish to strike?") in null|choices + + if(!A || !src) return + + if(!(src.Adjacent(A))) return + + var/damage_to_apply = rand(melee_damage_lower, melee_damage_upper) + 10 + if(isliving(A)) + visible_message(span("danger","\The [src] rakes its claws across [A].")) + var/mob/living/L = A + if(ishuman(L)) + var/mob/living/carbon/human/H = L + H.apply_damage(damage_to_apply, BRUTE, BP_TORSO, 0, 0, "Animal claws") + + else + L.adjustBruteLoss(damage_to_apply) + + L.add_modifier(/datum/modifier/grievous_wounds, 60 SECONDS) + + else if(istype(A, /obj/mecha)) + visible_message(span("danger","\The [src] rakes its claws against \the [A].")) + var/obj/mecha/M = A + M.take_damage(damage_to_apply) + if(prob(3) && do_after(src, 5)) + visible_message(span("critical","\The [src]'s strike ripped \the [M]'s access hatch open, allowing it to drag [M.occupant] out!")) + M.go_out() + + else + A.attack_generic(src, damage_to_apply, "rakes its claws against") // Well it's not a mob, and it's not a mech. + +/mob/living/simple_mob/animal/sif/kururak/verb/rally_pack() // Mostly for telling other players to follow you. AI Kururaks will auto-follow, if set to. + set name = "Rally Pack" + set desc = "Tries to command your fellow pack members to follow you." + set category = "Abilities" + + if(has_modifier_of_type(/datum/modifier/ace)) + for(var/mob/living/simple_mob/animal/sif/kururak/K in hearers(7, src)) + if(K == src) + continue + if(!K.ai_holder) + continue + if(K.faction != src.faction) + continue + var/datum/ai_holder/AI = K.ai_holder + to_chat(K, span("notice","The pack leader wishes for you to follow them.")) + AI.set_follow(src) + +/mob/living/simple_mob/animal/sif/kururak/proc/detect_instinct() // Will return the Kururak within 10 tiles that has the highest instinct. + var/mob/living/simple_mob/animal/sif/kururak/A + + var/pack_count = 0 + + for(var/mob/living/simple_mob/animal/sif/kururak/K in hearers(10, src)) + if(K == src) + continue + if(K.stat != DEAD) + pack_count++ + if(K.instinct > src.instinct) + A = K + + if(!A && pack_count) + A = src + + return A + +/mob/living/simple_mob/animal/sif/kururak/proc/pack_gauge() // Check incase we have a client. + var/mob/living/simple_mob/animal/sif/kururak/highest_instinct = detect_instinct() + if(highest_instinct == src) + add_modifier(/datum/modifier/ace, 60 SECONDS) + else + remove_modifiers_of_type(/datum/modifier/ace) + +/datum/ai_holder/simple_mob/intentional/kururak + hostile = FALSE + retaliate = TRUE + cooperative = TRUE + can_flee = TRUE + flee_when_dying = TRUE + +/datum/ai_holder/simple_mob/intentional/kururak/handle_special_strategical() + follow_distance = rand(initial(follow_distance), initial(follow_distance) + 2) + var/mob/living/simple_mob/animal/sif/kururak/K = holder + + if(istype(K)) + var/mob/living/simple_mob/animal/sif/kururak/highest_instinct = K.detect_instinct() + if(highest_instinct == K) + K.add_modifier(/datum/modifier/ace, 60 SECONDS) + else + K.remove_modifiers_of_type(/datum/modifier/ace) + + if(holder.has_modifier_of_type(/datum/modifier/ace)) + if(leader && istype(leader, /mob/living/simple_mob/animal/sif/kururak)) // Kururaks will not follow another kururak if they're the pack leader. + lose_follow() + + else if(highest_instinct) + set_follow(highest_instinct) + + if(holder.has_modifier_of_type(/datum/modifier/ace)) + hostile = TRUE + else + hostile = initial(hostile) + +/datum/ai_holder/simple_mob/intentional/kururak/pre_special_attack(atom/A) + holder.a_intent = I_HURT + if(isliving(A)) + var/mob/living/L = A + if(holder.Adjacent(L)) + holder.a_intent = I_GRAB + + if(iscarbon(L)) + var/mob/living/carbon/C = L + if(!C.eyecheck()) + if(holder.a_intent != I_GRAB) + holder.a_intent = I_DISARM + + if(issilicon(L) && holder.a_intent != I_GRAB) + holder.a_intent = I_DISARM + + else if(istype(A, /obj/mecha)) + holder.a_intent = I_GRAB + +/datum/ai_holder/simple_mob/intentional/kururak/post_melee_attack() + if(holder.has_modifier_of_type(/datum/modifier/ace)) + request_help() + +// Kururak Ace modifier, given to the one with the highest Instinct. + +/datum/modifier/ace + name = "Ace" + desc = "You are universally superior, in terms of physical prowess." + on_created_text = "You feel superior." + on_expired_text = "You feel your superiority lessen..." + stacks = MODIFIER_STACK_EXTEND + + mob_overlay_state = "ace" + + max_health_flat = 25 + max_health_percent = 1.2 + disable_duration_percent = 0.8 + incoming_damage_percent = 0.7 + incoming_healing_percent = 1.5 + outgoing_melee_damage_percent = 1.5 + evasion = 20 + bleeding_rate_percent = 0.7 + attack_speed_percent = 0.8 diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/sif/leech.dm b/code/modules/mob/living/simple_mob/subtypes/animal/sif/leech.dm new file mode 100644 index 00000000000..c59e292a4db --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/animal/sif/leech.dm @@ -0,0 +1,503 @@ +// Small creatures that will embed themselves in unsuspecting victim's bodies, drink their blood, and/or eat their organs. Steals some things from borers. + +/datum/category_item/catalogue/fauna/iceleech + name = "Sivian Fauna - River Leech" + desc = "Classification: S Hirudinea phorus \ +

\ + An incredibly dangerous species of worm phorogenically mutated from a Sivian river leech, \ + believed to have resulted from corporate mining in the Ullran Expanse; these accusations are \ + unfounded, however speculation remains.\ +
\ + The creatures' heads hold four long prehensile tendrils surrounding a central beak, which are \ + used as locomotive and grappling appendages. Each is capped in a hollow tooth, capable of pumping \ + chemicals into an unsuspecting host. \ +
\ + The rear half of the creature is entirely musculature, capped with a sharp, arrowhead-shaped fin." + value = CATALOGUER_REWARD_MEDIUM + +/mob/living/simple_mob/animal/sif/leech + name = "river leech" + desc = "What appears to be an oversized leech." + tt_desc = "S Hirudinea phorus" + catalogue_data = list(/datum/category_item/catalogue/fauna/iceleech) + + faction = "leech" + + icon_state = "leech" + item_state = "brainslug" + icon_living = "leech" + icon_dead = "leech_dead" + icon = 'icons/mob/animal.dmi' + + density = FALSE // Non-dense, so things can pass over them. + + status_flags = CANPUSH + pass_flags = PASSTABLE + + maxHealth = 100 + health = 100 + + universal_understand = 1 + + special_attack_min_range = 0 + special_attack_max_range = 1 + + var/obj/item/organ/external/host_bodypart // Where in the body we are infesting. + var/docile = FALSE + var/chemicals = 0 + var/max_chemicals = 400 + var/list/bodypart_targets = list(BP_L_LEG,BP_R_LEG,BP_L_ARM,BP_R_ARM,BP_TORSO,BP_GROIN,BP_HEAD) + var/infest_target = BP_TORSO // The currently chosen bodypart to infest. + var/mob/living/carbon/host // Our humble host. + var/list/produceable_chemicals = list("inaprovaline","anti_toxin","alkysine","bicaridine","tramadol","kelotane","leporazine","iron","phoron","condensedcapsaicin_v","frostoil") + var/randomized_reagent = "iron" // The reagent chosen at random to be produced, if there's no one piloting the worm. + var/passive_reagent = "paracetamol" // Reagent passively produced by the leech. Should usually be a painkiller. + + var/feeding_delay = 30 SECONDS // How long do we have to wait to bite our host's organs? + var/last_feeding = 0 + + intent = I_HELP + + holder_type = /obj/item/weapon/holder/leech + + movement_cooldown = 0 + aquatic_movement = -2 + + melee_damage_lower = 1 + melee_damage_upper = 5 + attack_armor_pen = 15 + attack_sharp = TRUE + attacktext = list("nipped", "bit", "pinched") + + armor = list( + "melee" = 10, + "bullet" = 15, + "laser" = -10, + "energy" = 0, + "bomb" = 10, + "bio" = 100, + "rad" = 100 + ) + + armor_soak = list( + "melee" = 5, + "bullet" = 5, + "laser" = 0, + "energy" = 0, + "bomb" = 0, + "bio" = 0, + "rad" = 0 + ) + + say_list_type = /datum/say_list/leech + ai_holder_type = /datum/ai_holder/simple_mob/intentional/leech + +/mob/living/simple_mob/animal/sif/leech/IIsAlly(mob/living/L) + . = ..() + + var/mob/living/carbon/human/H = L + if(!istype(H)) + return . + + if(istype(L.buckled, /obj/vehicle) || L.hovering) // Ignore people hovering or on boats. + return TRUE + + if(!.) + var/has_organ = FALSE + var/obj/item/organ/internal/O = H.get_active_hand() + if(istype(O) && O.robotic < ORGAN_ROBOT && !(O.status & ORGAN_DEAD)) + has_organ = TRUE + return has_organ + +/datum/say_list/leech + speak = list("...", "Sss..", ". . .","Gss..") + emote_see = list("vibrates","looks around", "stares", "extends a proboscis") + emote_hear = list("chitters", "clicks", "gurgles") + +/mob/living/simple_mob/animal/sif/leech/Initialize() + ..() + + verbs += /mob/living/proc/ventcrawl + verbs += /mob/living/proc/hide + +/mob/living/simple_mob/animal/sif/leech/Stat() + ..() + if(client.statpanel == "Status") + statpanel("Status") + if(emergency_shuttle) + var/eta_status = emergency_shuttle.get_status_panel_eta() + if(eta_status) + stat(null, eta_status) + stat("Chemicals", chemicals) + +/mob/living/simple_mob/animal/sif/leech/do_special_attack(atom/A) + . = TRUE + if(istype(A, /mob/living/carbon)) + switch(a_intent) + if(I_DISARM) // Poison + set_AI_busy(TRUE) + poison_inject(src, A) + set_AI_busy(FALSE) + if(I_GRAB) // Infesting! + set_AI_busy(TRUE) + do_infest(src, A) + set_AI_busy(FALSE) + +/mob/living/simple_mob/animal/sif/leech/handle_special() + if(prob(5)) + randomized_reagent = pick(produceable_chemicals) + + var/turf/T = get_turf(src) + if(istype(T, /turf/simulated/floor/water) && src.loc == T && !stat) // Are we sitting in water, and alive? + alpha = max(5, alpha - 10) + if(chemicals + 1 < max_chemicals / 3) + chemicals++ + else + alpha = min(255, alpha + 20) + + if(!client && !host) + infest_target = pick(bodypart_targets) + + if(host && !stat && !host.stat) + if(ai_holder) + ai_holder.hostile = FALSE + ai_holder.lose_target() + alpha = 5 + if(host.reagents.has_reagent("cordradaxon") && !docile) // Overwhelms the leech with food. + var/message = "We feel the rush of cardiac pluripotent cells in your host's blood, lulling us into docility." + to_chat(src, span("warning", message)) + docile = TRUE + if(chemicals + 5 <= max_chemicals) + chemicals += 5 + + else if(docile) + var/message = "We shake off our lethargy as the pluripotent cell count declines in our host's blood." + to_chat(src, span("notice", message)) + docile = FALSE + + if(!host.reagents.has_reagent(passive_reagent)) + host.reagents.add_reagent(passive_reagent, 5) + chemicals -= 3 + + if(!docile && ishuman(host) && chemicals < max_chemicals) + var/mob/living/carbon/human/H = host + H.vessel.remove_reagent("blood", 1) + if(!H.reagents.has_reagent("inaprovaline")) + H.reagents.add_reagent("inaprovaline", 1) + chemicals += 2 + + if(!client && !docile) // Automatic 'AI' to manage damage levels. + if(host.getBruteLoss() >= 30 && chemicals > 50) + host.reagents.add_reagent("bicaridine", 5) + chemicals -= 30 + + if(host.getToxLoss() >= 30 && chemicals > 50) + var/randomchem = pickweight("tramadol" = 7, "anti_toxin" = 15, "frostoil" = 3) + host.reagents.add_reagent(randomchem, 5) + chemicals -= 50 + + if(host.getFireLoss() >= 30 && chemicals > 50) + host.reagents.add_reagent("kelotane", 5) + host.reagents.add_reagent("leporazine", 2) + chemicals -= 50 + + if(host.getOxyLoss() >= 30 && chemicals > 50) + host.reagents.add_reagent("iron", 10) + chemicals -= 40 + + if(host.getBrainLoss() >= 10 && chemicals > 100) + host.reagents.add_reagent("alkysine", 5) + host.reagents.add_reagent("tramadol", 3) + chemicals -= 100 + + if(prob(30) && chemicals > 50) + inject_meds(randomized_reagent) + + var/heartless_mod = 0 + if(ishuman(host)) // Species without hearts mean the worm gets hungry faster, if AI controlled. + var/mob/living/carbon/human/H = host + if(!H.species.has_organ[O_HEART]) + heartless_mod = 1 + + if(prob(15 + (20 * heartless_mod))) + feed_on_organ() + else + if(ai_holder) + ai_holder.hostile = initial(ai_holder.hostile) + + if(host && host.stat == DEAD && istype(get_turf(host), /turf/simulated/floor/water)) + leave_host() + +/mob/living/simple_mob/animal/sif/leech/verb/infest() + set category = "Abilities" + set name = "Infest" + set desc = "Infest a suitable humanoid host." + + if(docile) + to_chat(src, span("alium","We are too tired to do this...")) + return + + do_infest(usr) + +/mob/living/simple_mob/animal/sif/leech/proc/do_infest(var/mob/living/user, var/mob/living/target = null) + if(host) + to_chat(user, span("alien", "We are already within a host.")) + return + + if(stat) + to_chat(user, span("warning","We cannot infest a target in your current state.")) + return + + var/mob/living/carbon/M = target + + if(!M && src.client) + var/list/choices = list() + for(var/mob/living/carbon/C in view(1,src)) + if(src.Adjacent(C)) + choices += C + + if(!choices.len) + to_chat(user, span("warning","There are no viable hosts within range...")) + return + + M = input(src,"Who do we wish to infest?") in null|choices + + if(!M || !src) return + + if(!(src.Adjacent(M))) return + + if(!istype(M) || M.isSynthetic()) + to_chat(user, "\The [M] cannot be infested.") + return + + if(istype(M,/mob/living/carbon/human)) + var/mob/living/carbon/human/H = M + + var/obj/item/organ/external/E = H.organs_by_name[infest_target] + if(!E || E.is_stump() || E.robotic >= ORGAN_ROBOT) + to_chat(src,"\The [H] does not have an infestable [infest_target]!") + + var/list/covering_clothing = E.get_covering_clothing() + for(var/obj/item/clothing/C in covering_clothing) + if(C.armor["melee"] >= 20 + attack_armor_pen) + to_chat(user, span("notice","We cannot get through that host's protective gear.")) + return + + if(!do_after(src,2)) + to_chat(user, span("notice", "As [M] moves away, we are dislodged and fall to the ground.")) + return + + if(!M || !src) + return + + if(src.stat) + to_chat(user, span("warning","We cannot infest a target in your current state.")) + return + + if(M in view(1, src)) + to_chat(user,span("alien", "We burrow into [M]'s flesh.")) + if(!M.stat) + to_chat(M, span("critical", "You feel a sharp pain as something digs into your flesh!")) + + src.host = M + src.forceMove(M) + if(ai_holder) + ai_holder.hostile = FALSE + ai_holder.lose_target() + + if(istype(M,/mob/living/carbon/human)) + var/mob/living/carbon/human/H = M + host_bodypart = H.get_organ(infest_target) + host_bodypart.implants |= src + + return + else + to_chat(user, span("notice","They are no longer in range.")) + return + +/mob/living/simple_mob/animal/sif/leech/verb/uninfest() + set category = "Abilities" + set name = "Uninfest" + set desc = "Leave your current host." + + if(docile) + to_chat(src, span("alium","We are too tired to do this...")) + return + + leave_host() + +/mob/living/simple_mob/animal/sif/leech/proc/leave_host() + if(!host) + return + + if(ai_holder) + ai_holder.hostile = initial(ai_holder.hostile) + ai_holder.lose_target() + + host_bodypart.implants -= src + host_bodypart = null + + forceMove(get_turf(host)) + + reset_view(null) + + host = null + +/mob/living/simple_mob/animal/sif/leech/verb/inject_victim() + set category = "Abilities" + set name = "Incapacitate Potential Host" + set desc = "Inject an organic host with an incredibly painful mixture of chemicals." + + if(docile) + to_chat(src, span("alium","We are too tired to do this...")) + return + + var/mob/living/carbon/M + if(src.client) + var/list/choices = list() + for(var/mob/living/carbon/C in view(1,src)) + if(src.Adjacent(C)) + choices += C + + if(!choices.len) + to_chat(src, span("warning","There are no viable hosts within range...")) + return + + M = input(src,"Who do we wish to inject?") in null|choices + + if(!M || stat) + return + + poison_inject(usr, M) + +/mob/living/simple_mob/animal/sif/leech/proc/poison_inject(var/mob/living/user, var/mob/living/carbon/L) + if(!L || !Adjacent(L) || stat) + return + + var/mob/living/carbon/human/H = L + + if(!istype(H) || H.isSynthetic()) + to_chat(user, span("warning","You cannot inject this target...")) + + var/obj/item/organ/external/E = H.organs_by_name[infest_target] + if(!E || E.is_stump() || E.robotic >= ORGAN_ROBOT) + to_chat(src,"\The [H] does not have an infestable [infest_target]!") + + var/list/covering_clothing = E.get_covering_clothing() + for(var/obj/item/clothing/C in covering_clothing) + if(C.armor["melee"] >= 40 + attack_armor_pen) + to_chat(user, span("notice","You cannot get through that host's protective gear.")) + return + + H.add_modifier(/datum/modifier/poisoned/paralysis, 15 SECONDS) + +/mob/living/simple_mob/animal/sif/leech/verb/medicate_host() + set category = "Abilities" + set name = "Produce Chemicals (50)" + set desc = "Inject your host with possibly beneficial chemicals, to keep the blood flowing." + + if(docile) + to_chat(src, span("alium","We are too tired to do this...")) + return + + if(!host || chemicals <= 50) + to_chat(usr, span("alien","We cannot produce any chemicals right now.")) + return + + if(host) + var/chem = input("Select a chemical to produce.", "Chemicals") as null|anything in produceable_chemicals + inject_meds(chem) + +/mob/living/simple_mob/animal/sif/leech/proc/inject_meds(var/chem) + if(host) + chemicals = max(1, chemicals - 50) + host.reagents.add_reagent(chem, 5) + to_chat(src, span("alien","We injected \the [host] with five units of [chem].")) + +/mob/living/simple_mob/animal/sif/leech/verb/feed_on_organ() + set category = "Abilities" + set name = "Feed on Organ" + set desc = "Extend probosci to feed on a piece of your host's organs." + + if(docile) + to_chat(src, span("alium","We are too tired to do this...")) + return + + if(host && world.time >= last_feeding + feeding_delay) + var/list/host_internal_organs = host.internal_organs + + for(var/obj/item/organ/internal/O in host_internal_organs) // Remove organs with maximum damage. + if(O.damage >= O.max_damage) + host_internal_organs -= O + + var/target + if(client) + target = input("Select an organ to feed on.", "Organs") as null|anything in host_internal_organs + if(!target) + to_chat(src, span("alien","We decide not to feed.")) + return + + if(!target) + target = pick(host_internal_organs) + + if(target) + bite_organ(target) + + else + to_chat(src, span("warning","We cannot feed now.")) + +/mob/living/simple_mob/animal/sif/leech/proc/bite_organ(var/obj/item/organ/internal/O) + last_feeding = world.time + + if(O) + to_chat(src, span("alien","We feed on [O].")) + O.take_damage(2,silent=prob(10)) + chemicals = min(max_chemicals, chemicals + 60) + host.add_modifier(/datum/modifier/grievous_wounds, 60 SECONDS) + adjustBruteLoss(rand(-10,-60)) + adjustFireLoss(rand(-10,-60)) + +/datum/ai_holder/simple_mob/intentional/leech + hostile = TRUE + retaliate = TRUE + vision_range = 3 + mauling = TRUE + returns_home = TRUE + can_flee = TRUE + home_low_priority = TRUE // If we've got a target, we're going for them. + max_home_distance = 1 // Low to ensure the creature doesn't leave the water unless it has a host. + +/datum/ai_holder/simple_mob/intentional/leech/handle_special_strategical() + var/mob/living/simple_mob/animal/sif/leech/SL = holder + if(!SL.host && !istype(get_turf(SL), /turf/simulated/floor/water)) + var/list/nearby_water = list() + for(var/turf/simulated/floor/water/W in view(holder, 10)) + nearby_water |= W + if(nearby_water && nearby_water.len) + var/turf/T = pick(nearby_water) + if(T && can_attack(T)) + home_turf = T + +/datum/ai_holder/simple_mob/intentional/leech/special_flee_check() + var/mob/living/simple_mob/animal/sif/leech/SL = holder + + if(!SL.host && !istype(get_turf(SL), /turf/simulated/floor/water)) + return TRUE + +/datum/ai_holder/simple_mob/intentional/leech/pre_special_attack(atom/A) + if(isliving(A)) + var/mob/living/L = A + if(ishuman(L) && !L.isSynthetic()) + if(L.incapacitated() || (L.stat && L.stat != DEAD) || L.resting || L.paralysis) + holder.a_intent = I_GRAB // Infesting time. + else + holder.a_intent = I_DISARM // They're standing up! Try to drop or stun them. + else + holder.a_intent = I_HURT // Otherwise, bite. + + else if(istype(A, /obj/item)) + var/obj/item/I = A + if(istype(I, /obj/item/weapon/reagent_containers/food/snacks)) + holder.a_intent = I_HURT + else + holder.a_intent = I_HURT diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/sif/racoon.dm b/code/modules/mob/living/simple_mob/subtypes/animal/sif/racoon.dm new file mode 100644 index 00000000000..b2e913bfb24 --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/animal/sif/racoon.dm @@ -0,0 +1,355 @@ +// Sakimm are small scavengers with an adoration for shiny things. They won't attack you for them, but you will be their friend holding something like a coin. + +/datum/category_item/catalogue/fauna/sakimm + name = "Sivian Fauna - Sakimm" + desc = "Classification: S Procyon cogitae \ +

\ + Small, social omnivores known to collect objects within their dens. \ + The Sakimm form colonies that have been known to grow up to a hundred individuals. Primarily carnivorous hunters, \ + they often supplement their diets with nuts, roots, and other fruits. \ + Individuals are known to steal food and reflective objects from unsuspecting Sivian residents. \ + It is advised to keep any valuable items within dull wraps when venturing near the den of a Sakimm." + value = CATALOGUER_REWARD_EASY + +/mob/living/simple_mob/animal/sif/sakimm + name = "sakimm" + desc = "What appears to be an oversized rodent with hands." + tt_desc = "S Procyon cogitae" + catalogue_data = list(/datum/category_item/catalogue/fauna/sakimm) + + faction = "sakimm" + + icon_state = "raccoon" + icon_living = "raccoon" + icon_dead = "raccoon_dead" + icon_rest = "raccoon_dead" + icon = 'icons/mob/animal.dmi' + + maxHealth = 50 + health = 50 + has_hands = TRUE + humanoid_hands = TRUE + + pass_flags = PASSTABLE + + universal_understand = 1 + + movement_cooldown = 0 + + melee_damage_lower = 5 + melee_damage_upper = 15 + base_attack_cooldown = 1 SECOND + attacktext = list("nipped", "bit", "cut", "clawed") + + armor = list( + "melee" = 15, + "bullet" = 5, + "laser" = 5, + "energy" = 0, + "bomb" = 10, + "bio" = 100, + "rad" = 100 + ) + + armor_soak = list( + "melee" = 2, + "bullet" = 2, + "laser" = 0, + "energy" = 0, + "bomb" = 0, + "bio" = 0, + "rad" = 0 + ) + + say_list_type = /datum/say_list/sakimm + ai_holder_type = /datum/ai_holder/simple_mob/retaliate/cooperative/sakimm + + var/obj/item/clothing/head/hat = null // The hat the Sakimm may be wearing. + var/list/friend_loot_list = list(/obj/item/weapon/coin) // What will make this animal non-hostile if held? + var/randomize_size = TRUE + +/mob/living/simple_mob/animal/sif/sakimm/verb/remove_hat() + set name = "Remove Hat" + set desc = "Remove the animal's hat. You monster." + set category = "Abilities" + set src in view(1) + + drop_hat(usr) + +/mob/living/simple_mob/animal/sif/sakimm/proc/drop_hat(var/mob/user) + if(hat) + hat.forceMove(get_turf(user)) + hat = null + update_icon() + if(user == src) + to_chat(user, "You removed your hat.") + return + to_chat(user, "You removed \the [src]'s hat. You monster.") + else + if(user == src) + to_chat(user, "You are not wearing a hat!") + return + to_chat(user, "\The [src] is not wearing a hat!") + +/mob/living/simple_mob/animal/sif/sakimm/verb/give_hat() + set name = "Give Hat" + set desc = "Give the animal a hat. You hero." + set category = "Abilities" + set src in view(1) + + take_hat(usr) + +/mob/living/simple_mob/animal/sif/sakimm/proc/take_hat(var/mob/user) + if(hat) + if(user == src) + to_chat(user, "You already have a hat!") + return + to_chat(user, "\The [src] already has a hat!") + else + if(user == src) + if(istype(get_active_hand(), /obj/item/clothing/head)) + hat = get_active_hand() + drop_from_inventory(hat, src) + hat.forceMove(src) + to_chat(user, "You put on the hat.") + update_icon() + return + else if(ishuman(user)) + var/mob/living/carbon/human/H = user + + if(istype(H.get_active_hand(), /obj/item/clothing/head) && !get_active_hand()) + var/obj/item/clothing/head/newhat = H.get_active_hand() + H.drop_from_inventory(newhat, get_turf(src)) + if(!stat) + intent = I_HELP + newhat.attack_hand(src) + else if(src.get_active_hand()) + to_chat(user, "\The [src] seems busy with \the [get_active_hand()] already!") + + else + to_chat(user, "You aren't holding a hat...") + +/datum/say_list/sakimm + speak = list("Shurr.", "|R|rr?", "Hss.") + emote_see = list("sniffs","looks around", "rubs its hands") + emote_hear = list("chitters", "clicks") + +/mob/living/simple_mob/animal/sif/sakimm/Destroy() + if(hat) + drop_hat(src) + ..() + +/mob/living/simple_mob/animal/sif/sakimm/update_icon() + overlays.Cut() + ..() + if(hat) + var/hat_state = hat.item_state ? hat.item_state : hat.icon_state + var/image/I = image('icons/mob/head.dmi', src, hat_state) + I.pixel_y = -15 // Sakimm are tiny! + I.appearance_flags = RESET_COLOR + add_overlay(I) + +/mob/living/simple_mob/animal/sif/sakimm/Initialize() + . = ..() + + verbs += /mob/living/proc/ventcrawl + verbs += /mob/living/proc/hide + + if(randomize_size) + adjust_scale(rand(8, 11) / 10) + +/mob/living/simple_mob/animal/sif/sakimm/IIsAlly(mob/living/L) + . = ..() + + var/mob/living/carbon/human/H = L + if(!istype(H)) + return . + + if(!.) + var/has_loot = FALSE + var/obj/item/I = H.get_active_hand() + if(I) + for(var/item_type in friend_loot_list) + if(istype(I, item_type)) + has_loot = TRUE + break + return has_loot + +/datum/ai_holder/simple_mob/retaliate/cooperative/sakimm/handle_special_strategical() // Just needs to take hats. + var/mob/living/simple_mob/animal/sif/sakimm/S = holder + + if(holder.get_active_hand() && istype(holder.get_active_hand(), /obj/item/clothing/head) && !S.hat) + var/obj/item/I = holder.get_active_hand() + S.take_hat(S) + holder.visible_message("\The [holder] wears \the [I]") + +/mob/living/simple_mob/animal/sif/sakimm/intelligent + desc = "What appears to be an oversized rodent with hands. This one has a curious look in its eyes." + ai_holder_type = /datum/ai_holder/simple_mob/intentional/sakimm + randomize_size = FALSE // Most likely to have a hat. + +/datum/ai_holder/simple_mob/intentional/sakimm + hostile = FALSE + retaliate = TRUE + vision_range = 10 + can_flee = TRUE + flee_when_dying = TRUE + + var/greed = 0 // The probability we will try to steal something. Increases over time if we are not holding something, or wearing a hat. + var/list/steal_loot_list = list(/obj/item/weapon/coin, /obj/item/weapon/gun, /obj/item/weapon/fossil, /obj/item/stack/material, /obj/item/weapon/material, /obj/item/weapon/reagent_containers/food/snacks, /obj/item/clothing/head, /obj/item/weapon/reagent_containers/glass, /obj/item/device/flashlight, /obj/item/stack/medical, /obj/item/seeds, /obj/item/weapon/spacecash) + var/hoard_items = TRUE + var/hoard_distance = 1 // How far an item can be from the Sakimm's home turf to be counted inside its 'hoard'. + var/original_home_distance = null + var/search_delay = 2 SECONDS // How often can we look for item targets? + var/last_search = 0 + +/datum/ai_holder/simple_mob/intentional/sakimm/New() + ..() + original_home_distance = max_home_distance + +/datum/ai_holder/simple_mob/intentional/sakimm/post_melee_attack(atom/A) + if(istype(A, /obj/item) && !holder.get_active_hand() && holder.Adjacent(A)) + var/obj/item/I = A + I.attack_hand(holder) + lose_target() + if(istype(A,/mob/living) && holder.Adjacent(A)) // Not the dumbest tool in the shed. If we're fighting, we're gonna dance around them. + holder.IMove(get_step(holder, pick(alldirs))) + holder.face_atom(A) + request_help() // And we're going to call friends, too. + +/datum/ai_holder/simple_mob/intentional/sakimm/list_targets() + . = hearers(vision_range, holder) - holder + + var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha)) + + for(var/HM in typecache_filter_list(range(vision_range, holder), hostile_machines)) + if(can_see(holder, HM, vision_range)) + . += HM + + if(holder.get_active_hand()) // We don't want item targets if we have an item! + return . + + if(world.time <= last_search + search_delay) // Don't spam searching for item targets, since they can be in areas with a -lot- of items. + return . + + for(var/obj/item/I in view(holder, vision_range)) + last_search = world.time + if(!hoard_items || get_dist(I, home_turf) <= 1) + continue + for(var/itemtype in steal_loot_list) + if(istype(I, itemtype)) + if(!I.anchored) + . += I + break + + . -= holder.contents + +/datum/ai_holder/simple_mob/intentional/sakimm/find_target(var/list/possible_targets, var/has_targets_list = FALSE) + var/can_pick_mobs = TRUE + + if(!hostile) + can_pick_mobs = FALSE + + . = list() + if(!has_targets_list) + possible_targets = list_targets() + + for(var/possible_target in possible_targets) + var/atom/A = possible_target + if(found(A)) + . = list(A) + break + if(istype(A, /mob/living) && !can_pick_mobs) + continue + if(can_attack(A)) // Can we attack it? + . += A + continue + + for(var/obj/item/I in .) + last_search = world.time + if(!hoard_items || get_dist(I, home_turf) <= 1) + . -= I + + var/new_target = pick_target(.) + give_target(new_target) + return new_target + +/datum/ai_holder/simple_mob/intentional/sakimm/pre_melee_attack(atom/A) + if(isliving(A)) + var/mob/living/L = A + if(holder.get_active_hand()) // Are we holding something? If so, drop it, we have a new target to kill, and we shouldn't use their weapons. + holder.drop_from_inventory(holder.get_active_hand(), get_turf(holder)) + + if(ishuman(L)) + if(L.incapacitated(INCAPACITATION_DISABLED)) // Is our target on the ground? If so, let's scratch! + if(prob(30)) // Unless we decide to cut and run, and take their stuff while we're at it. + var/turf/T = get_turf(L) + var/obj/item/IT = locate() in T.contents + if(IT) + lose_target() + give_target(IT) + return + holder.a_intent = I_HURT + else + holder.a_intent = I_DISARM // Otherwise, try to disarm them! + else + holder.a_intent = I_HURT // We can't disarm you, so we're going to hurt you. + + else if(istype(A, /obj/item)) + var/obj/item/I = A + if(istype(I, /obj/item/weapon/reagent_containers/food/snacks)) // If we can't pick it up, or it's edible, go to harm. + holder.a_intent = I_HURT + else + holder.a_intent = I_HELP + + else + holder.a_intent = I_HURT + +/datum/ai_holder/simple_mob/intentional/sakimm/should_go_home() + if((!returns_home && !holder.get_active_hand()) || !home_turf) // If we have an item, we want to go home. + return FALSE + if(get_dist(holder, home_turf) > max_home_distance) + if(!home_low_priority) + return TRUE + else if(!leader && !target) + return TRUE + return FALSE + +/datum/ai_holder/simple_mob/intentional/sakimm/handle_special_tactic() + var/mob/living/simple_mob/animal/sif/sakimm/S = holder + if(S.hat) + hoard_items = FALSE + else + hoard_items = TRUE + +/datum/ai_holder/simple_mob/intentional/sakimm/handle_special_strategical() + var/mob/living/simple_mob/animal/sif/sakimm/S = holder + var/carrying_item = FALSE + + if(holder.get_active_hand()) // Do we have loot? + if(istype(holder) && istype(holder.get_active_hand(), /obj/item/clothing/head) && !S.hat) + var/obj/item/I = holder.get_active_hand() + S.take_hat(S) + holder.visible_message("\The [holder] wears \the [I]") + carrying_item = TRUE + + if(istype(holder) && S.hat) // Do we have a hat? Hats are loot. + carrying_item = TRUE + + if(!carrying_item) // Not carrying or wearing anything? We want to carry something more. + greed++ + greed = min(95, greed) + else + greed = 0 + if(!target && prob(5 + greed) && !holder.get_active_hand()) + find_target() + if(holder.get_active_hand() && hoard_items) + lose_target() + max_home_distance = 1 + if(get_dist(holder, home_turf) <= max_home_distance) + holder.drop_from_inventory(holder.get_active_hand(), get_turf(holder)) + if(!holder.get_active_hand()) + max_home_distance = original_home_distance + +/datum/ai_holder/simple_mob/intentional/sakimm/special_flee_check() + return holder.get_active_hand() diff --git a/code/modules/projectiles/projectile/blob.dm b/code/modules/projectiles/projectile/blob.dm index 926c78a0a03..7ebc5cdfad7 100644 --- a/code/modules/projectiles/projectile/blob.dm +++ b/code/modules/projectiles/projectile/blob.dm @@ -26,13 +26,14 @@ ..() /obj/item/projectile/energy/blob/on_impact(var/atom/A) - var/turf/location = get_turf(src) - var/datum/effect/effect/system/smoke_spread/chem/S = new /datum/effect/effect/system/smoke_spread/chem - S.attach(location) - S.set_up(reagents, splatter_volume, 0, location) - playsound(location, 'sound/effects/slime_squish.ogg', 30, 1, -3) - spawn(0) - S.start() + if(splatter) + var/turf/location = get_turf(src) + var/datum/effect/effect/system/smoke_spread/chem/S = new /datum/effect/effect/system/smoke_spread/chem + S.attach(location) + S.set_up(reagents, splatter_volume, 0, location) + playsound(location, 'sound/effects/slime_squish.ogg', 30, 1, -3) + spawn(0) + S.start() ..() /obj/item/projectile/energy/blob/proc/ready_chemicals() @@ -61,3 +62,11 @@ splatter = TRUE flammability = 0.25 my_chems = list("fuel", "mold") + +/obj/item/projectile/energy/blob/freezing + my_chems = list("frostoil") + modifier_type_to_apply = /datum/modifier/chilled + modifier_duration = 1 MINUTE + +/obj/item/projectile/energy/blob/freezing/splattering + splatter = TRUE diff --git a/html/changelogs/Mechoid - SifAnimalExpansion1.yml b/html/changelogs/Mechoid - SifAnimalExpansion1.yml new file mode 100644 index 00000000000..b8e67e02b2e --- /dev/null +++ b/html/changelogs/Mechoid - SifAnimalExpansion1.yml @@ -0,0 +1,38 @@ +################################ +# Example Changelog File +# +# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb. +# +# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.) +# When it is, any changes listed below will disappear. +# +# Valid Prefixes: +# bugfix +# wip (For works in progress) +# tweak +# soundadd +# sounddel +# rscadd (general adding of nice things) +# rscdel (general deleting of nice things) +# imageadd +# imagedel +# maptweak +# spellcheck (typo fixes) +# experiment +################################# + +# Your name. +author: Mechoid + +# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again. +delete-after: True + +# Any changes you've made. See valid prefix list above. +# INDENT WITH TWO SPACES. NOT TABS. SPACES. +# SCREW THIS UP AND IT WON'T WORK. +# Also, all entries are changed into a single [] after a master changelog generation. Just remove the brackets when you add new entries. +# Please surround your changes in double quotes ("), as certain characters otherwise screws up compiling. The quotes will not show up in the changelog. +changes: + - rscadd: "The Shimm, Frostfly, Glitterfly, River Leech, Crystal-Feathered Duck, and Kururak are now able to be used on the map and PoIs." + - tweak: "Glass jars can now hold glitterflies, river leeches, and frostflies." + - tweak: "Glass jars now respect mobs that exist over the lighting plane, adding a special overlay visible when on the ground." diff --git a/icons/mob/64x64.dmi b/icons/mob/64x64.dmi index a829ee2c6fd..9c0c5b61e31 100644 Binary files a/icons/mob/64x64.dmi and b/icons/mob/64x64.dmi differ diff --git a/icons/mob/animal.dmi b/icons/mob/animal.dmi index 2f22e0ddaaa..1f7c06a0371 100644 Binary files a/icons/mob/animal.dmi and b/icons/mob/animal.dmi differ diff --git a/icons/mob/modifier_effects.dmi b/icons/mob/modifier_effects.dmi index a28dc6a5205..99a4f6e4ed0 100644 Binary files a/icons/mob/modifier_effects.dmi and b/icons/mob/modifier_effects.dmi differ diff --git a/vorestation.dme b/vorestation.dme index 977e49d5799..43f1593a20c 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -2358,7 +2358,13 @@ #include "code\modules\mob\living\simple_mob\subtypes\animal\pets\parrot.dm" #include "code\modules\mob\living\simple_mob\subtypes\animal\sif\diyaab.dm" #include "code\modules\mob\living\simple_mob\subtypes\animal\sif\fluffy_vr.dm" +#include "code\modules\mob\living\simple_mob\subtypes\animal\sif\duck.dm" +#include "code\modules\mob\living\simple_mob\subtypes\animal\sif\frostfly.dm" +#include "code\modules\mob\living\simple_mob\subtypes\animal\sif\glitterfly.dm" #include "code\modules\mob\living\simple_mob\subtypes\animal\sif\hooligan_crab.dm" +#include "code\modules\mob\living\simple_mob\subtypes\animal\sif\kururak.dm" +#include "code\modules\mob\living\simple_mob\subtypes\animal\sif\leech.dm" +#include "code\modules\mob\living\simple_mob\subtypes\animal\sif\racoon.dm" #include "code\modules\mob\living\simple_mob\subtypes\animal\sif\savik.dm" #include "code\modules\mob\living\simple_mob\subtypes\animal\sif\shantak.dm" #include "code\modules\mob\living\simple_mob\subtypes\animal\sif\sif.dm"