diff --git a/_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_gluttony.dmm b/_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_gluttony.dmm index e449be961cd..c6636911f72 100644 --- a/_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_gluttony.dmm +++ b/_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_gluttony.dmm @@ -16,7 +16,7 @@ /turf/simulated/floor/plating/lava/smooth, /area/ruin/powered/gluttony) "f" = ( -/obj/item/reagent_containers/syringe/gluttony, +/obj/item/dnainjector/eatmut, /turf/simulated/floor/plasteel/freezer, /area/ruin/powered/gluttony) "g" = ( diff --git a/code/datums/spell.dm b/code/datums/spell.dm index 00c807687cd..21527e1dc7f 100644 --- a/code/datums/spell.dm +++ b/code/datums/spell.dm @@ -225,7 +225,7 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) charge_counter = charge_max /obj/effect/proc_holder/spell/proc/perform(list/targets, recharge = 1, mob/user = usr, make_attack_logs = TRUE) //if recharge is started is important for the trigger spells - before_cast(targets) + before_cast(targets, user) invocation() if(user && user.ckey && make_attack_logs) add_attack_logs(user, targets, "cast the spell [name]", ATKLOG_ALL) @@ -244,7 +244,7 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) if(action) action.UpdateButtonIcon() -/obj/effect/proc_holder/spell/proc/before_cast(list/targets) +/obj/effect/proc_holder/spell/proc/before_cast(list/targets, mob/user) if(vampire_ability) if(!before_cast_vampire(targets)) return diff --git a/code/datums/spells/area_teleport.dm b/code/datums/spells/area_teleport.dm index 0fdce753476..dfb40218816 100644 --- a/code/datums/spells/area_teleport.dm +++ b/code/datums/spells/area_teleport.dm @@ -10,7 +10,7 @@ var/sound2 = 'sound/weapons/zapbang.ogg' /obj/effect/proc_holder/spell/targeted/area_teleport/perform(list/targets, recharge = 1, mob/living/user = usr) - var/thearea = before_cast(targets) + var/thearea = before_cast(targets, user) if(!thearea || !cast_check(FALSE, FALSE, user)) revert_cast() return @@ -21,7 +21,7 @@ cast(targets,thearea) after_cast(targets) -/obj/effect/proc_holder/spell/targeted/area_teleport/before_cast(list/targets) +/obj/effect/proc_holder/spell/targeted/area_teleport/before_cast(list/targets, mob/user) var/A = null if(!randomise_selection) @@ -35,7 +35,7 @@ var/area/thearea = SSmapping.teleportlocs[A] if(thearea.tele_proof && !istype(thearea, /area/wizard_station)) - to_chat(usr, "A mysterious force disrupts your arcane spell matrix, and you remain where you are.") + to_chat(user, "A mysterious force disrupts your arcane spell matrix, and you remain where you are.") return return thearea diff --git a/code/datums/spells/mimic.dm b/code/datums/spells/mimic.dm new file mode 100644 index 00000000000..4f7ba9f6951 --- /dev/null +++ b/code/datums/spells/mimic.dm @@ -0,0 +1,203 @@ +/obj/effect/proc_holder/spell/targeted/click/mimic + name = "Mimic" + desc = "Learn a new form to mimic or become one of your known forms" + clothes_req = FALSE + charge_max = 3 SECONDS + include_user = TRUE // To change forms + action_icon_state = "genetic_morph" + allowed_type = /atom/movable + auto_target_single = FALSE + click_radius = -1 + selection_activated_message = "Click on a target to remember it's form. Click on yourself to change form." + create_logs = FALSE + action_icon_state = "morph_mimic" + /// Which form is currently selected + var/datum/mimic_form/selected_form + /// Which forms the user can become + var/list/available_forms = list() + /// How many forms the user can remember + var/max_forms = 5 + /// Which index will be overriden next when the user wants to remember another form + var/next_override_index = 1 + /// If a message is shown when somebody examines the user from close range + var/perfect_disguise = FALSE + + var/static/list/black_listed_form_types = list(/obj/screen, /obj/singularity, /obj/effect, /mob/living/simple_animal/hostile/megafauna, /atom/movable/lighting_object, /obj/machinery/dna_vault, + /obj/machinery/power/bluespace_tap, /obj/structure/sign/barsign, /obj/machinery/atmospherics/unary/cryo_cell) + +/obj/effect/proc_holder/spell/targeted/click/mimic/valid_target(atom/target, user) + if(is_type_in_list(target, black_listed_form_types)) + return FALSE + if(istype(target, /atom/movable)) + var/atom/movable/AM = target + if(AM.bound_height > world.icon_size || AM.bound_width > world.icon_size) + return FALSE // No multitile structures + if(user != target && istype(target, /mob/living/simple_animal/hostile/morph)) + return FALSE + return ..() + +/obj/effect/proc_holder/spell/targeted/click/mimic/cast(list/targets, mob/user) + var/atom/movable/A = targets[1] + if(A == user) + INVOKE_ASYNC(src, .proc/pick_form, user) + return + + INVOKE_ASYNC(src, .proc/remember_form, A, user) + +/obj/effect/proc_holder/spell/targeted/click/mimic/proc/remember_form(atom/movable/A, mob/user) + if(A.name in available_forms) + to_chat(user, "[A] is already an available form.") + revert_cast(user) + return + if(length(available_forms) >= max_forms) + to_chat(user, "You start to forget the form of [available_forms[next_override_index]] to learn a new one.") + + to_chat(user, "You start remembering the form of [A].") + if(!do_after(user, 2 SECONDS, FALSE, user)) + to_chat(user, "You lose focus.") + return + + // Forget the old form if needed + if(length(available_forms) >= max_forms) + qdel(available_forms[available_forms[next_override_index]]) // Delete the value using the key + available_forms[next_override_index++] = A.name + // Reset if needed + if(next_override_index > max_forms) + next_override_index = 1 + + available_forms[A.name] = new /datum/mimic_form(A, user) + to_chat(user, "You learn the form of [A].") + +/obj/effect/proc_holder/spell/targeted/click/mimic/proc/pick_form(mob/user) + if(!length(available_forms) && !selected_form) + to_chat(user, "No available forms. Learn more forms by using this spell on other objects first.") + revert_cast(user) + return + + var/list/forms = list() + if(selected_form) + forms += "Original Form" + + forms += available_forms.Copy() + var/what = input(user, "Which form do you want to become?", "Mimic") as null|anything in forms + if(!what) + to_chat(user, "You decide against changing forms.") + revert_cast(user) + return + + if(what == "Original Form") + restore_form(user) + return + to_chat(user, "You start becoming [what].") + if(!do_after(user, 2 SECONDS, FALSE, user)) + to_chat(user, "You lose focus.") + return + take_form(available_forms[what], user) + +/obj/effect/proc_holder/spell/targeted/click/mimic/proc/take_form(datum/mimic_form/form, mob/user) + var/old_name = "[user]" + if(ishuman(user)) + // Not fully finished yet + var/mob/living/carbon/human/H = user + H.name_override = form.name + else + user.appearance = form.appearance + user.transform = initial(user.transform) + user.pixel_y = initial(user.pixel_y) + user.pixel_x = initial(user.pixel_x) + user.layer = MOB_LAYER // Avoids weirdness when mimicing something below the vent layer + + playsound(user, "bonebreak", 75, TRUE) + show_change_form_message(user, old_name, "[user]") + user.create_log(MISC_LOG, "Mimicked into [user]") + + if(!selected_form) + RegisterSignal(user, COMSIG_PARENT_EXAMINE, .proc/examine_override) + RegisterSignal(user, COMSIG_MOB_DEATH, .proc/on_death) + + selected_form = form + +/obj/effect/proc_holder/spell/targeted/click/mimic/proc/show_change_form_message(mob/user, old_name, new_name) + user.visible_message("[old_name] contorts and slowly becomes [new_name]!", "You take form of [new_name].", "You hear loud cracking noises!") + +/obj/effect/proc_holder/spell/targeted/click/mimic/proc/restore_form(mob/user, show_message = TRUE) + selected_form = null + var/old_name = "[user]" + + user.cut_overlays() + user.icon = initial(user.icon) + user.icon_state = initial(user.icon_state) + if(ishuman(user)) + var/mob/living/carbon/human/H = user + H.name_override = null + H.regenerate_icons() + else + user.name = initial(user.name) + user.desc = initial(user.desc) + user.color = initial(user.color) + + playsound(user, "bonebreak", 150, TRUE) + if(show_message) + show_restore_form_message(user, old_name, "[user]") + + UnregisterSignal(user, list(COMSIG_PARENT_EXAMINE, COMSIG_MOB_DEATH)) + +/obj/effect/proc_holder/spell/targeted/click/mimic/proc/show_restore_form_message(mob/user, old_name, new_name) + user.visible_message("[old_name] shakes and contorts and quickly becomes [new_name]!", "You take return to your normal self.", "You hear loud cracking noises!") + +/obj/effect/proc_holder/spell/targeted/click/mimic/proc/examine_override(datum/source, mob/user, list/examine_list) + examine_list.Cut() + examine_list += selected_form.examine_text + if(!perfect_disguise && get_dist(user, source) <= 3) + examine_list += "It doesn't look quite right..." + +/obj/effect/proc_holder/spell/targeted/click/mimic/proc/on_death(mob/user, gibbed) + if(!gibbed) + restore_form(user, FALSE) + show_death_message(user) + +/obj/effect/proc_holder/spell/targeted/click/mimic/proc/show_death_message(mob/user) + user.visible_message("[user] shakes and contorts as [user.p_they()] die[user.p_s()], returning to [user.p_their()] true form!", "Your disguise fails as your life forces drain away.", "You hear loud cracking noises followed by a thud!") + + +/datum/mimic_form + /// How does the form look like? + var/appearance + /// What is the examine text paired with this form + var/examine_text + /// What the name of the form is + var/name + +/datum/mimic_form/New(atom/movable/form, mob/user) + appearance = form.appearance + examine_text = form.examine(user) + name = form.name + + +/obj/effect/proc_holder/spell/targeted/click/mimic/morph + action_background_icon_state = "bg_morph" + +/obj/effect/proc_holder/spell/targeted/click/mimic/morph/valid_target(atom/target, user) + if(target != user && istype(target, /mob/living/simple_animal/hostile/morph)) + return FALSE + return ..() + +/obj/effect/proc_holder/spell/targeted/click/mimic/morph/take_form(datum/mimic_form/form, mob/living/simple_animal/hostile/morph/user) + ..() + user.assume() + +/obj/effect/proc_holder/spell/targeted/click/mimic/morph/restore_form(mob/living/simple_animal/hostile/morph/user, show_message = TRUE) + ..() + user.restore() + +/obj/effect/proc_holder/spell/targeted/click/mimic/morph/show_change_form_message(mob/user, old_name, new_name) + user.visible_message("[old_name] suddenly twists and changes shape, becoming a copy of [new_name]!", \ + "You twist your body and assume the form of [new_name].") + +/obj/effect/proc_holder/spell/targeted/click/mimic/morph/show_restore_form_message(mob/user, old_name, new_name) + user.visible_message("[old_name] suddenly collapses in on itself, dissolving into a pile of green flesh!", \ + "You reform to your normal body.") + +/obj/effect/proc_holder/spell/targeted/click/mimic/morph/show_death_message(mob/user) + user.visible_message("[user] twists and dissolves into a pile of green flesh!", \ + "Your skin ruptures! Your flesh breaks apart! No disguise can ward off de--") diff --git a/code/game/gamemodes/miniantags/morph/morph.dm b/code/game/gamemodes/miniantags/morph/morph.dm index 6e3627f2684..5bf147c2b75 100644 --- a/code/game/gamemodes/miniantags/morph/morph.dm +++ b/code/game/gamemodes/miniantags/morph/morph.dm @@ -1,4 +1,5 @@ -#define MORPH_COOLDOWN 50 +#define MORPHED_SPEED 2.5 +#define ITEM_EAT_COST 5 /mob/living/simple_animal/hostile/morph name = "morph" @@ -10,11 +11,12 @@ icon_state = "morph" icon_living = "morph" icon_dead = "morph_dead" - speed = 2 + speed = 1.5 a_intent = INTENT_HARM stop_automated_movement = 1 status_flags = CANPUSH pass_flags = PASSTABLE + move_resist = MOVE_FORCE_STRONG // Fat being ventcrawler = 2 atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) @@ -24,8 +26,8 @@ health = 150 environment_smash = 1 obj_damage = 50 - melee_damage_lower = 20 - melee_damage_upper = 20 + melee_damage_lower = 15 + melee_damage_upper = 15 see_in_dark = 8 lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE vision_range = 1 // Only attack when target is close @@ -34,18 +36,40 @@ attack_sound = 'sound/effects/blobattack.ogg' butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab = 2) - var/morphed = 0 - var/atom/movable/form = null - var/morph_time = 0 + /// If the morph is disguised or not + var/morphed = FALSE + /// If the morph is ready to perform an ambush + var/ambush_prepared = FALSE + /// How much damage a successful ambush attack does + var/ambush_damage = 25 + /// How much weaken a successful ambush attack applies + var/ambush_weaken = 3 + /// The spell the morph uses to morph + var/obj/effect/proc_holder/spell/targeted/click/mimic/morph/mimic_spell + /// The ambush action used by the morph + var/obj/effect/proc_holder/spell/morph/ambush/ambush_spell + /// The spell the morph uses to pass through airlocks + var/obj/effect/proc_holder/spell/targeted/click/pass_airlock/pass_airlock_spell - var/list/examine_text_list + /// How much the morph has gathered in terms of food. Used to reproduce and such + var/gathered_food = 20 // Start with a bit to use abilities - var/playstyle_string = "You are a morph.
As an abomination created primarily with changeling cells, \ - you may take the form of anything nearby by shift-clicking it. This process will alert any nearby \ - observers, and can only be performed once every five seconds.
While morphed, you move faster, but do \ - less damage. In addition, anyone within three tiles will note an uncanny wrongness if examining you. \ - You can restore yourself to your original form while morphed by shift-clicking yourself.
\ - Finally, you can attack any item or dead creature to consume it - creatures will restore 1/3 of your max health.
" +/mob/living/simple_animal/hostile/morph/Initialize(mapload) + . = ..() + mimic_spell = new + AddSpell(mimic_spell) + ambush_spell = new + AddSpell(ambush_spell) + AddSpell(new /obj/effect/proc_holder/spell/morph/reproduce) + AddSpell(new /obj/effect/proc_holder/spell/morph/open_vent) + pass_airlock_spell = new + AddSpell(pass_airlock_spell) + +/mob/living/simple_animal/hostile/morph/Stat(Name, Value) + ..() + if(statpanel("Status")) + stat(null, "Food Stored: [gathered_food]") + return TRUE /mob/living/simple_animal/hostile/morph/wizard name = "magical morph" @@ -57,78 +81,117 @@ AddSpell(new /obj/effect/proc_holder/spell/targeted/smoke) AddSpell(new /obj/effect/proc_holder/spell/targeted/forcewall) -/mob/living/simple_animal/hostile/morph/examine(mob/user) - if(morphed) - . = examine_text_list.Copy() - if(get_dist(user, src) <= 3) - . += "It doesn't look quite right..." - else - . = ..() -/mob/living/simple_animal/hostile/morph/proc/allowed(atom/movable/A) // make it into property/proc ? not sure if worth it - if(istype(A,/obj/screen)) - return 0 - if(istype(A,/obj/singularity)) - return 0 - if(istype(A,/mob/living/simple_animal/hostile/morph)) - return 0 - return 1 +/mob/living/simple_animal/hostile/morph/proc/try_eat(atom/movable/A) + var/food_value = calc_food_gained(A) + if(food_value + gathered_food < 0) + to_chat(src, "You can't force yourself to eat more disgusting items. Eat some living things first.") + return + var/eat_self_message + if(food_value < 0) + eat_self_message = "You start eating [A]... disgusting...." + else + eat_self_message = "You start eating [A]." + visible_message("[src] starts eating [target]!", eat_self_message, "You hear loud crunching!") + if(do_after(src, 3 SECONDS, target = A)) + if(food_value + gathered_food < 0) + to_chat(src, "You can't force yourself to eat more disgusting items. Eat some living things first.") + return + eat(A) /mob/living/simple_animal/hostile/morph/proc/eat(atom/movable/A) if(A && A.loc != src) visible_message("[src] swallows [A] whole!") + + var/mob/living/carbon/human/H = A + if(istype(H) && H.w_uniform && istype(H.w_uniform, /obj/item/clothing/under)) + var/obj/item/clothing/under/U = H.w_uniform + U.turn_sensors_off() + + A.extinguish_light() A.forceMove(src) - return 1 - return 0 + var/food_value = calc_food_gained(A) + add_food(food_value) + if(food_value > 0) + adjustHealth(-food_value) + add_attack_logs(src, A, "morph ate") + return TRUE + return FALSE -/mob/living/simple_animal/hostile/morph/ShiftClickOn(atom/movable/A) - if(morph_time <= world.time && !stat) - if(A == src) - restore() - return - if(istype(A) && allowed(A)) - assume(A) - else - to_chat(src, "Your chameleon skin is still repairing itself!") - ..() +/mob/living/simple_animal/hostile/morph/proc/calc_food_gained(mob/living/L) + if(!istype(L)) + return -ITEM_EAT_COST // Anything other than a tasty mob will make me sad ;( + var/gained_food = max(5, 10 * L.mob_size) // Tiny things are worth less + if(ishuman(L) && !ismonkeybasic(L)) + gained_food += 10 // Humans are extra tasty -/mob/living/simple_animal/hostile/morph/proc/assume(atom/movable/target) - morphed = 1 - form = target - visible_message("[src] suddenly twists and changes shape, becoming a copy of [target]!", \ - "You twist your body and assume the form of [target].") + return gained_food - appearance = target.appearance - transform = initial(transform) - pixel_y = initial(pixel_y) - pixel_x = initial(pixel_x) - //Morphed is weaker +/mob/living/simple_animal/hostile/morph/proc/use_food(amount) + if(amount > gathered_food) + return FALSE + add_food(-amount) + return TRUE + +/** + * Adds the given amount of food to the gathered food and updates the actions. + * Does not include a check to see if it goes below 0 or not + */ +/mob/living/simple_animal/hostile/morph/proc/add_food(amount) + gathered_food += amount + for(var/obj/effect/proc_holder/spell/morph/MS in mind.spell_list) + MS.updateButtonIcon() + + +/mob/living/simple_animal/hostile/morph/proc/assume() + morphed = TRUE + + //Morph is weaker initially when disguised melee_damage_lower = 5 melee_damage_upper = 5 - speed = 0 - examine_text_list = form.examine(src) - morph_time = world.time + MORPH_COOLDOWN - return + speed = MORPHED_SPEED + ambush_spell.updateButtonIcon() + pass_airlock_spell.updateButtonIcon() + move_resist = MOVE_FORCE_DEFAULT // They become more fragile and easier to move /mob/living/simple_animal/hostile/morph/proc/restore() if(!morphed) return - morphed = 0 - form = null - examine_text_list = null // Free that memory - visible_message("[src] suddenly collapses in on itself, dissolving into a pile of green flesh!", \ - "You reform to your normal body.") - name = initial(name) - icon = initial(icon) - icon_state = initial(icon_state) - overlays.Cut() + morphed = FALSE //Baseline stats melee_damage_lower = initial(melee_damage_lower) melee_damage_upper = initial(melee_damage_upper) speed = initial(speed) + if(ambush_prepared) + to_chat(src, "The ambush potential has faded as you take your true form.") + failed_ambush() + pass_airlock_spell.updateButtonIcon() + move_resist = MOVE_FORCE_STRONG // Return to their fatness + + +/mob/living/simple_animal/hostile/morph/proc/prepare_ambush() + ambush_prepared = TRUE + to_chat(src, "You are ready to ambush any unsuspected target. Your next attack will hurt a lot more and weaken the target! Moving will break your focus. Standing still will perfect your disguise.") + apply_status_effect(/datum/status_effect/morph_ambush) + RegisterSignal(src, COMSIG_MOVABLE_MOVED, .proc/on_move) + +/mob/living/simple_animal/hostile/morph/proc/failed_ambush() + ambush_prepared = FALSE + ambush_spell.updateButtonIcon() + mimic_spell.perfect_disguise = FALSE // Reset the perfect disguise + remove_status_effect(/datum/status_effect/morph_ambush) + UnregisterSignal(src, COMSIG_MOVABLE_MOVED) + +/mob/living/simple_animal/hostile/morph/proc/perfect_ambush() + mimic_spell.perfect_disguise = TRUE // Reset the perfect disguise + to_chat(src, "You've perfected your disguise. Making you indistinguishable from the real form!") + + +/mob/living/simple_animal/hostile/morph/proc/on_move() + failed_ambush() + to_chat(src, "You moved out of your ambush spot!") - morph_time = world.time + MORPH_COOLDOWN /mob/living/simple_animal/hostile/morph/death(gibbed) . = ..() @@ -140,40 +203,122 @@ // Only execute the below if we successfully died if(!.) return FALSE - if(morphed) - visible_message("[src] twists and dissolves into a pile of green flesh!", \ - "Your skin ruptures! Your flesh breaks apart! No disguise can ward off de--") - restore() -/mob/living/simple_animal/hostile/morph/Aggro() // automated only - ..() - restore() +/mob/living/simple_animal/hostile/morph/attack_hand(mob/living/carbon/human/M) + if(ambush_prepared) + to_chat(M, "[src] feels a bit different from normal... it feels more.. SLIMEY?!") + ambush_attack(M, TRUE) + else + return ..() + +#define MORPH_ATTACKED if((. = ..()) && morphed) mimic_spell.restore_form(src) + +/mob/living/simple_animal/hostile/morph/attackby(obj/item/O, mob/living/user) + if(user.a_intent == INTENT_HELP && ambush_prepared) + to_chat(user, "You try to use [O] on [src]... it seems different than no-") + ambush_attack(user, TRUE) + return TRUE + MORPH_ATTACKED + +/mob/living/simple_animal/hostile/morph/attack_animal(mob/living/simple_animal/M) + if(M.a_intent == INTENT_HELP && ambush_prepared) + to_chat(M, "You nuzzle [src]. And [src] nuzzles back!") + ambush_attack(M, TRUE) + return TRUE + MORPH_ATTACKED + +/mob/living/simple_animal/hostile/morph/attack_hulk(mob/living/carbon/human/user, does_attack_animation) // Me SMASH + MORPH_ATTACKED + +/mob/living/simple_animal/hostile/morph/attack_larva(mob/living/carbon/alien/larva/L) + MORPH_ATTACKED + +/mob/living/simple_animal/hostile/morph/attack_alien(mob/living/carbon/alien/humanoid/M) + MORPH_ATTACKED + +/mob/living/simple_animal/hostile/morph/attack_tk(mob/user) + MORPH_ATTACKED + +/mob/living/simple_animal/hostile/morph/attack_slime(mob/living/simple_animal/slime/M) + MORPH_ATTACKED + +#undef MORPH_ATTACKED + +/mob/living/simple_animal/hostile/morph/proc/ambush_attack(mob/living/L, touched) + ambush_prepared = FALSE + var/total_weaken = ambush_weaken + var/total_damage = ambush_damage + if(touched) // Touching a morph while he's ready to kill you is a bad idea + total_weaken *= 2 + total_damage *= 2 + + L.Weaken(total_weaken) + L.apply_damage(total_damage, BRUTE) + add_attack_logs(src, L, "morph ambush attacked") + do_attack_animation(L, ATTACK_EFFECT_BITE) + visible_message("[src] suddenly leaps towards [L]!", "You strike [L] when [L.p_they()] least expected it!", "You hear a horrible crunch!") + + mimic_spell.restore_form(src) /mob/living/simple_animal/hostile/morph/LoseAggro() vision_range = initial(vision_range) /mob/living/simple_animal/hostile/morph/AIShouldSleep(list/possible_targets) . = ..() - if(.) + if(. && !morphed) var/list/things = list() for(var/atom/movable/A in view(src)) - if(allowed(A)) + if(mimic_spell.valid_target(A, src)) things += A var/atom/movable/T = pick(things) - assume(T) + mimic_spell.take_form(new /datum/mimic_form(T, src), src) + prepare_ambush() // They cheat okay /mob/living/simple_animal/hostile/morph/AttackingTarget() if(isliving(target)) // Eat Corpses to regen health var/mob/living/L = target if(L.stat == DEAD) - if(do_after(src, 30, target = L)) - if(eat(L)) - adjustHealth(-50) - return + try_eat(L) + return TRUE + if(ambush_prepared) + ambush_attack(L) + return TRUE // No double attack else if(istype(target,/obj/item)) // Eat items just to be annoying var/obj/item/I = target if(!I.anchored) - if(do_after(src, 20, target = I)) - eat(I) - return - return ..() + try_eat(I) + return TRUE + . = ..() + if(. && morphed) + mimic_spell.restore_form(src) + + +/mob/living/simple_animal/hostile/morph/proc/make_morph_antag(give_default_objectives = TRUE) + mind.assigned_role = SPECIAL_ROLE_MORPH + mind.special_role = SPECIAL_ROLE_MORPH + SSticker.mode.traitors |= mind + to_chat(src, "You are a morph.
") + to_chat(src, "You hunger for living beings and desire to procreate. Achieve this goal by ambushing unsuspecting pray using your abilities.") + to_chat(src, "As an abomination created primarily with changeling cells you may take the form of anything nearby by using your Mimic ability.") + to_chat(src, "The transformation will not go unnoticed for bystanding observers.") + to_chat(src, "While morphed, you move slower and do less damage. In addition, anyone within three tiles will note an uncanny wrongness if examining you.") + to_chat(src, "From this form you can however Prepare an Ambush using your ability.") + to_chat(src, "This will allow you to deal a lot of damage the first hit. And if they touch you then even more.") + to_chat(src, "Finally, you can attack any item or dead creature to consume it - creatures will restore 1/3 of your max health and will add to your stored food while eating items will reduce your stored food.") + to_chat(src, "For more information, check the wiki page: ([GLOB.configuration.url.wiki_url]/index.php/Morph)") + SEND_SOUND(src, sound('sound/magic/mutate.ogg')) + if(give_default_objectives) + var/datum/objective/eat = new /datum/objective + eat.owner = mind + eat.explanation_text = "Eat as many living beings as possible to still the hunger within you." + eat.completed = TRUE + mind.objectives += eat + var/datum/objective/procreate = new /datum/objective + procreate.owner = mind + procreate.explanation_text = "Split yourself in as many other [name]'s as possible!" + procreate.completed = TRUE + mind.objectives += procreate + mind.announce_objectives() + +#undef MORPHED_SPEED +#undef ITEM_EAT_COST diff --git a/code/game/gamemodes/miniantags/morph/morph_event.dm b/code/game/gamemodes/miniantags/morph/morph_event.dm index ad42365947b..eb5857f49a2 100644 --- a/code/game/gamemodes/miniantags/morph/morph_event.dm +++ b/code/game/gamemodes/miniantags/morph/morph_event.dm @@ -22,12 +22,7 @@ return var/mob/living/simple_animal/hostile/morph/S = new /mob/living/simple_animal/hostile/morph(pick(GLOB.xeno_spawn)) player_mind.transfer_to(S) - player_mind.assigned_role = SPECIAL_ROLE_MORPH - player_mind.special_role = SPECIAL_ROLE_MORPH - SSticker.mode.traitors |= player_mind - to_chat(S, S.playstyle_string) - to_chat(S, "For more information, check the wiki page: ([GLOB.configuration.url.wiki_url]/index.php/Morph)") - SEND_SOUND(S, sound('sound/magic/mutate.ogg')) + S.make_morph_antag() message_admins("[key_of_morph] has been made into morph by an event.") log_game("[key_of_morph] was spawned as a morph by an event.") diff --git a/code/game/gamemodes/miniantags/morph/spells/ambush.dm b/code/game/gamemodes/miniantags/morph/spells/ambush.dm new file mode 100644 index 00000000000..1fc68648704 --- /dev/null +++ b/code/game/gamemodes/miniantags/morph/spells/ambush.dm @@ -0,0 +1,60 @@ +#define MORPH_AMBUSH_PERFECTION_TIME 15 SECONDS + +/obj/effect/proc_holder/spell/morph/ambush + name = "Prepare Ambush" + desc = "Prepare an ambush. Dealing significantly more damage on the first hit and you will weaken the target. Only works while morphed. If the target tries to use you with their hands then you will do even more damage. \ + Keeping still for another 10 seconds will perfect your disguise." + action_icon_state = "morph_ambush" + charge_max = 8 SECONDS + +/obj/effect/proc_holder/spell/morph/ambush/cast_check(charge_check = TRUE, start_recharge = TRUE, mob/living/simple_animal/hostile/morph/user = usr) + if(!istype(user)) + return ..() // Message is in there + if(!user.morphed) + to_chat(user, "You can only prepare an ambush if you're disguised!") + return FALSE + if(user.ambush_prepared) + to_chat(user, "You are already prepared!") + return FALSE + return ..() + +/obj/effect/proc_holder/spell/morph/ambush/can_cast(mob/living/simple_animal/hostile/morph/user, charge_check, show_message) + if(!istype(user) || !user.morphed || user.ambush_prepared) + return FALSE + return ..() + +/obj/effect/proc_holder/spell/morph/ambush/choose_targets(mob/user) + perform(list(user), TRUE, user, FALSE) + +/obj/effect/proc_holder/spell/morph/ambush/cast(list/targets, mob/living/simple_animal/hostile/morph/user) + to_chat(user, "You start preparing an ambush.") + if(!do_after(user, 6 SECONDS, FALSE, user, TRUE, list(CALLBACK(src, .proc/prepare_check, user)), FALSE)) + if(!user.morphed) + to_chat(user, "You need to stay morphed to prepare the ambush!") + return + to_chat(user, "You need to stay still to prepare the ambush!") + return + user.prepare_ambush() + +/obj/effect/proc_holder/spell/morph/ambush/proc/prepare_check(mob/living/simple_animal/hostile/morph/user) + return !user.morphed + +/datum/status_effect/morph_ambush + id = "morph_ambush" + duration = -1 + tick_interval = MORPH_AMBUSH_PERFECTION_TIME + alert_type = /obj/screen/alert/status_effect/morph_ambush + +/datum/status_effect/morph_ambush/tick() + STOP_PROCESSING(SSfastprocess, src) + var/mob/living/simple_animal/hostile/morph/M = owner + M.perfect_ambush() + linked_alert.name = "Perfect Ambush!" + linked_alert.desc = "You have prepared an ambush! Your disguise is flawless!" + +/obj/screen/alert/status_effect/morph_ambush + name = "Ambush!" + desc = "You have prepared an ambush!" + icon_state = "morph_ambush" + +#undef MORPH_AMBUSH_PERFECTION_TIME diff --git a/code/game/gamemodes/miniantags/morph/spells/morph_spell.dm b/code/game/gamemodes/miniantags/morph/spells/morph_spell.dm new file mode 100644 index 00000000000..08d2944df2b --- /dev/null +++ b/code/game/gamemodes/miniantags/morph/spells/morph_spell.dm @@ -0,0 +1,36 @@ +/obj/effect/proc_holder/spell/morph + action_background_icon_state = "bg_morph" + clothes_req = FALSE + /// How much food it costs the morph to use this + var/hunger_cost = 0 + +/obj/effect/proc_holder/spell/morph/Initialize(mapload) + . = ..() + if(hunger_cost) + name = "[name] ([hunger_cost])" + +/obj/effect/proc_holder/spell/morph/cast_check(charge_check = TRUE, start_recharge = TRUE, mob/living/simple_animal/hostile/morph/user = usr) + if(!istype(user)) + to_chat(user, "You should not be able to use this abilty! Report this as a bug on github please.") + stack_trace() + log_debug("[user] has the spell [src] while he is not a morph") + return FALSE + if(user.gathered_food < hunger_cost) + to_chat(user, "You require at least [hunger_cost] stored food to use this ability!") + return FALSE + return ..() + +/obj/effect/proc_holder/spell/morph/can_cast(mob/living/simple_animal/hostile/morph/user, charge_check, show_message) + if(!istype(user) || user.gathered_food < hunger_cost) + return FALSE + return ..() + +/obj/effect/proc_holder/spell/morph/before_cast(list/targets, mob/living/simple_animal/hostile/morph/user) + user.use_food(hunger_cost) + if(hunger_cost) + to_chat(user, "You have [user.gathered_food] left to use.") + +/obj/effect/proc_holder/spell/morph/revert_cast(mob/living/simple_animal/hostile/morph/user) + user.add_food(hunger_cost) + to_chat(user, "You have [user.gathered_food] left to use.") + ..() diff --git a/code/game/gamemodes/miniantags/morph/spells/open_vent.dm b/code/game/gamemodes/miniantags/morph/spells/open_vent.dm new file mode 100644 index 00000000000..e2409635d1d --- /dev/null +++ b/code/game/gamemodes/miniantags/morph/spells/open_vent.dm @@ -0,0 +1,46 @@ +/obj/effect/proc_holder/spell/morph/open_vent + name = "Open Vents" + desc = "Spit out acidic puke on nearby vents or scrubbers. Will take a little while for the acid to take effect. Not usable from inside a vent." + action_icon_state = "acid_vent" + charge_max = 10 SECONDS + hunger_cost = 10 + +/obj/effect/proc_holder/spell/morph/open_vent/choose_targets(mob/user) + var/list/targets = list() + for(var/obj/machinery/atmospherics/unary/U in view(user, 1)) + if(istype(U, /obj/machinery/atmospherics/unary/vent_scrubber)) + var/obj/machinery/atmospherics/unary/vent_scrubber/S = U + if(S.welded) + targets += S + else if(istype(U, /obj/machinery/atmospherics/unary/vent_pump)) + var/obj/machinery/atmospherics/unary/vent_scrubber/V = U + if(V.welded) + targets += V + + perform(targets, TRUE, user) + +/obj/effect/proc_holder/spell/morph/open_vent/cast(list/targets, mob/user) + if(!length(targets)) + to_chat(user, "No nearby welded vents found!") + revert_cast(user) + return + to_chat(user, "You begin regurgitating up some acidic puke!") + if(!do_after(user, 2 SECONDS, FALSE, user)) + to_chat(user, "You swallow the acid again.") + revert_cast(user) + return + for(var/thing in targets) + var/obj/machinery/atmospherics/unary/U = thing + U.add_overlay(GLOB.acid_overlay, TRUE) + addtimer(CALLBACK(src, .proc/unweld_vent, U), 2 SECONDS) + playsound(U, 'sound/items/welder.ogg', 100, TRUE) + +/obj/effect/proc_holder/spell/morph/open_vent/proc/unweld_vent(obj/machinery/atmospherics/unary/U) + if(istype(U, /obj/machinery/atmospherics/unary/vent_scrubber)) + var/obj/machinery/atmospherics/unary/vent_scrubber/S = U + S.welded = FALSE + else if(istype(U, /obj/machinery/atmospherics/unary/vent_pump)) + var/obj/machinery/atmospherics/unary/vent_scrubber/V = U + V.welded = FALSE + U.update_icon() + U.cut_overlay(GLOB.acid_overlay, TRUE) diff --git a/code/game/gamemodes/miniantags/morph/spells/pass_airlock.dm b/code/game/gamemodes/miniantags/morph/spells/pass_airlock.dm new file mode 100644 index 00000000000..8334fd4cab9 --- /dev/null +++ b/code/game/gamemodes/miniantags/morph/spells/pass_airlock.dm @@ -0,0 +1,52 @@ +// TODO refactor when spell code is component based instead of OO based +/obj/effect/proc_holder/spell/targeted/click/pass_airlock + name = "Pass Airlock" + desc = "Reform yourself so you can fit through a non bolted airlock. Takes a while to do and can only be used in a non disguised form." + action_background_icon_state = "bg_morph" + action_icon_state = "morph_airlock" + clothes_req = FALSE + charge_max = 10 SECONDS + range = 1 + allowed_type = /obj/machinery/door/airlock + selection_activated_message = "Click on an airlock to try pass it." + click_radius = -1 + +/obj/effect/proc_holder/spell/targeted/click/pass_airlock/cast_check(charge_check = TRUE, start_recharge = TRUE, mob/living/simple_animal/hostile/morph/user = usr) + if(!istype(user)) + to_chat(user, "You should not be able to use this abilty! Report this as a bug on github please.") + stack_trace() + log_debug("[user] has the spell [src] while he is not a morph") + return FALSE + if(user.morphed) + to_chat(user, "You can only pass through airlocks in your true form!") + return FALSE + return ..() + +/obj/effect/proc_holder/spell/targeted/click/pass_airlock/can_cast(mob/living/simple_animal/hostile/morph/user, charge_check, show_message) + if(!istype(user) || user.morphed) + return FALSE + return ..() + +/obj/effect/proc_holder/spell/targeted/click/pass_airlock/cast(list/targets, mob/living/simple_animal/hostile/morph/user) + var/obj/machinery/door/airlock/A = targets[1] + if(A.locked) + to_chat(user, "[A] is bolted shut! You're unable to create a crack to pass through!") + revert_cast(user) + return + user.visible_message("[user] starts pushing itself against [A]!", "You try to pry [A] open enough to get through.") + if(!do_after(user, 6 SECONDS, FALSE, user, TRUE, list(CALLBACK(src, .proc/pass_check, user, A)), FALSE)) + if(user.morphed) + to_chat(user, "You need to stay in your true form to pass through [A]!") + else if(A.locked) + to_chat(user, "[A] is bolted shut! You're unable to create a crack to pass through!") + else + to_chat(user, "You need to stay still to pass through [A]!") + revert_cast(user) + return + + user.visible_message("[user] briefly opens [A] slightly and passes through!", "You slide through the open crack in [A].") + user.forceMove(A.loc) // Move into the turf of the airlock + + +/obj/effect/proc_holder/spell/targeted/click/pass_airlock/proc/pass_check(mob/living/simple_animal/hostile/morph/user, obj/machinery/door/airlock/A) + return user.morphed || A.locked diff --git a/code/game/gamemodes/miniantags/morph/spells/reproduce.dm b/code/game/gamemodes/miniantags/morph/spells/reproduce.dm new file mode 100644 index 00000000000..cd2b18af69b --- /dev/null +++ b/code/game/gamemodes/miniantags/morph/spells/reproduce.dm @@ -0,0 +1,42 @@ +/obj/effect/proc_holder/spell/morph/reproduce + name = "Reproduce" + desc = "Split yourself in half making a new morph. Can only be used while on a floor. Makes you temporarily unable to vent crawl." + hunger_cost = 150 // 5 humans + charge_max = 30 SECONDS + action_icon_state = "morph_reproduce" + + +/obj/effect/proc_holder/spell/morph/reproduce/choose_targets(mob/user) + perform(list(user), TRUE, user, FALSE) + +/obj/effect/proc_holder/spell/morph/reproduce/cast_check(charge_check = TRUE, start_recharge = TRUE, mob/living/simple_animal/hostile/morph/user = usr) + if(!isturf(user.loc)) + to_chat(user, "You can only split while on flooring!") + return FALSE + return ..() + +/obj/effect/proc_holder/spell/morph/reproduce/can_cast(mob/living/simple_animal/hostile/morph/user, charge_check, show_message) + . = ..() + if(!.) + return + if(!isturf(user.loc)) + return FALSE + +/obj/effect/proc_holder/spell/morph/reproduce/cast(list/targets, mob/living/simple_animal/hostile/morph/user) + to_chat(user, "You prepare to split in two, making you unable to vent crawl!") + user.ventcrawler = FALSE // Temporarily disable it + var/list/candidates = SSghost_spawns.poll_candidates("Do you want to play as a morph?", ROLE_MORPH, TRUE, poll_time = 10 SECONDS, source = /mob/living/simple_animal/hostile/morph) + if(!length(candidates)) + to_chat(user, "Your body refuses to split at the moment. Try again later.") + revert_cast(user) + user.ventcrawler = initial(user.ventcrawler) // re enable the crawling + return + var/mob/C = pick(candidates) + + var/mob/living/simple_animal/hostile/morph/new_morph = new /mob/living/simple_animal/hostile/morph(get_turf(user)) + var/datum/mind/player_mind = new /datum/mind(C.key) + player_mind.active = TRUE + player_mind.transfer_to(new_morph) + new_morph.make_morph_antag() + user.create_log(MISC_LOG, "Made a new morph using [src]", new_morph) + user.ventcrawler = initial(user.ventcrawler) // re enable the crawling diff --git a/code/game/objects/items/tools/wirecutters.dm b/code/game/objects/items/tools/wirecutters.dm index bf8ccce706a..da47218ffc1 100644 --- a/code/game/objects/items/tools/wirecutters.dm +++ b/code/game/objects/items/tools/wirecutters.dm @@ -38,7 +38,7 @@ C.update_handcuffed() return else - ..() + return ..() /obj/item/wirecutters/suicide_act(mob/user) user.visible_message("[user] is cutting at [user.p_their()] arteries with [src]! It looks like [user.p_theyre()] trying to commit suicide!") diff --git a/code/game/objects/items/weapons/dna_injector.dm b/code/game/objects/items/weapons/dna_injector.dm index 02b53d2ff56..9c46b2656b5 100644 --- a/code/game/objects/items/weapons/dna_injector.dm +++ b/code/game/objects/items/weapons/dna_injector.dm @@ -395,6 +395,28 @@ block = GLOB.smallsizeblock ..() +/obj/item/dnainjector/eatmut + name = "DNA-Injector (Matter Eater)" + desc = "Gives you an appetite for anything." + datatype = DNA2_BUF_SE + value = 0xFFF + forcedmutation = TRUE + +/obj/item/dnainjector/eatmut/Initialize() + block = GLOB.eatblock + return ..() + +/obj/item/dnainjector/antieat + name = "DNA-Injector (Anti-Matter Eater)" + desc = "Makes you regain your normal appetite." + datatype = DNA2_BUF_SE + value = 0x001 + forcedmutation = TRUE + +/obj/item/dnainjector/antieat/Initialize() + block = GLOB.eatblock + return ..() + ///////////////////////////////////// /obj/item/dnainjector/antiglasses name = "DNA-Injector (Anti-Glasses)" diff --git a/code/modules/antagonists/_common/antag_spawner.dm b/code/modules/antagonists/_common/antag_spawner.dm index 07bcf113cb4..052b3302b3d 100644 --- a/code/modules/antagonists/_common/antag_spawner.dm +++ b/code/modules/antagonists/_common/antag_spawner.dm @@ -269,10 +269,7 @@ /obj/item/antag_spawner/morph/spawn_antag(client/C, turf/T, type = "", mob/user) var/mob/living/simple_animal/hostile/morph/wizard/M = new /mob/living/simple_animal/hostile/morph/wizard(pick(GLOB.xeno_spawn)) M.key = C.key - M.mind.assigned_role = SPECIAL_ROLE_MORPH - M.mind.special_role = SPECIAL_ROLE_MORPH - to_chat(M, M.playstyle_string) - SSticker.mode.traitors += M.mind + M.make_morph_antag(FALSE) var/datum/objective/assassinate/KillDaWiz = new /datum/objective/assassinate KillDaWiz.owner = M.mind KillDaWiz.target = user.mind @@ -285,4 +282,3 @@ M.mind.objectives += KillDaCrew to_chat(M, "Objective #[1]: [KillDaWiz.explanation_text]") to_chat(M, "Objective #[2]: [KillDaCrew.explanation_text]") - SEND_SOUND(M, sound('sound/magic/mutate.ogg')) diff --git a/code/modules/clothing/clothing.dm b/code/modules/clothing/clothing.dm index ea74a9778ca..cebd8a2b267 100644 --- a/code/modules/clothing/clothing.dm +++ b/code/modules/clothing/clothing.dm @@ -285,6 +285,15 @@ BLIND // can't see anything else return ..() +/** + * Tries to turn the sensors off. Returns TRUE if it succeeds + */ +/obj/item/clothing/under/proc/turn_sensors_off() + if(has_sensor != 1) + return FALSE + sensor_mode = SUIT_SENSOR_OFF + return TRUE + /obj/item/clothing/under/proc/set_sensors(mob/user as mob) var/mob/M = user if(istype(M, /mob/dead/)) return diff --git a/icons/mob/actions/actions.dmi b/icons/mob/actions/actions.dmi index 8e3c33d4752..19fc60a03c4 100644 Binary files a/icons/mob/actions/actions.dmi and b/icons/mob/actions/actions.dmi differ diff --git a/icons/mob/animal.dmi b/icons/mob/animal.dmi index a882e1d50b5..cd31834e7c4 100644 Binary files a/icons/mob/animal.dmi and b/icons/mob/animal.dmi differ diff --git a/icons/mob/screen_alert.dmi b/icons/mob/screen_alert.dmi index 3354c42a5b7..38fc4ffaf89 100644 Binary files a/icons/mob/screen_alert.dmi and b/icons/mob/screen_alert.dmi differ diff --git a/paradise.dme b/paradise.dme index 5e94a56943d..a29b6fc9851 100644 --- a/paradise.dme +++ b/paradise.dme @@ -433,6 +433,7 @@ #include "code\datums\spells\magnet.dm" #include "code\datums\spells\mime.dm" #include "code\datums\spells\mime_malaise.dm" +#include "code\datums\spells\mimic.dm" #include "code\datums\spells\mind_transfer.dm" #include "code\datums\spells\night_vision.dm" #include "code\datums\spells\projectile.dm" @@ -591,6 +592,11 @@ #include "code\game\gamemodes\miniantags\guardian\types\standard.dm" #include "code\game\gamemodes\miniantags\morph\morph.dm" #include "code\game\gamemodes\miniantags\morph\morph_event.dm" +#include "code\game\gamemodes\miniantags\morph\spells\ambush.dm" +#include "code\game\gamemodes\miniantags\morph\spells\morph_spell.dm" +#include "code\game\gamemodes\miniantags\morph\spells\open_vent.dm" +#include "code\game\gamemodes\miniantags\morph\spells\pass_airlock.dm" +#include "code\game\gamemodes\miniantags\morph\spells\reproduce.dm" #include "code\game\gamemodes\miniantags\revenant\revenant.dm" #include "code\game\gamemodes\miniantags\revenant\revenant_abilities.dm" #include "code\game\gamemodes\miniantags\revenant\revenant_spawn_event.dm"