diff --git a/code/_macros_vr.dm b/code/_macros_vr.dm index 2eecbe69ed..314689541e 100644 --- a/code/_macros_vr.dm +++ b/code/_macros_vr.dm @@ -1 +1,2 @@ #define isbelly(A) istype(A, /obj/belly) +#define iscapturecrystal(A) istype(A, /obj/item/capture_crystal) diff --git a/code/game/objects/items/weapons/capture_crystal.dm b/code/game/objects/items/weapons/capture_crystal.dm index 51bd40f362..c027c9712f 100644 --- a/code/game/objects/items/weapons/capture_crystal.dm +++ b/code/game/objects/items/weapons/capture_crystal.dm @@ -1,110 +1,351 @@ /obj/item/capture_crystal - name = "curious crystal" + name = "Curious Crystal" desc = "A silent, unassuming crystal in what appears to be some kind of steel housing." icon = 'icons/obj/capture_crystal_vr.dmi' icon_state = "inactive" drop_sound = 'sound/items/drop/ring.ogg' pickup_sound = 'sound/items/pickup/ring.ogg' - var/active = FALSE - var/owner - var/mob/living/bound_mob - var/stored_mob = FALSE - var/spawn_mob_type - var/activate_cooldown = 30 SECONDS - var/last_activate + throwforce = 0 + force = 0 + action_button_name = "Command" + + var/active = FALSE //Is it set up? + var/mob/living/owner //Reference to the owner + var/mob/living/bound_mob //Reference to our bound mob + var/spawn_mob_type //The kind of mob an inactive crystal will try to spawn when activated + var/activate_cooldown = 30 SECONDS //How long do we wait between unleashing and recalling + var/last_activate //Automatically set by things that try to move the bound mob or capture things + var/empty_icon = "empty" + var/full_icon = "full" /obj/item/capture_crystal/Initialize() + . = ..() + update_icon() + +//Let's make sure we clean up our references and things if the crystal goes away (such as when it's digested) +/obj/item/capture_crystal/Destroy() + . = ..() + if(bound_mob) + if(bound_mob in contents) + unleash() + to_chat(bound_mob, "You feel like yourself again. You are no longer under the influince of \the [src]'s command.") + UnregisterSignal(bound_mob, COMSIG_PARENT_QDELETING) + bound_mob = null + if(owner) + UnregisterSignal(owner, COMSIG_PARENT_QDELETING) + owner = null + +/obj/item/capture_crystal/examine(user) + . = ..() + if(user == owner) + if(bound_mob) + . += "[bound_mob]'s crystal" + if(isanimal(bound_mob)) + . += "[bound_mob.health / bound_mob.maxHealth * 100]%" + if(bound_mob.ooc_notes) + . += "OOC Notes: \[View\]" + . += "\[Mechanical Vore Preferences\]" + +//Command! This lets the owner toggle hostile on AI controlled mobs, or send a silent command message to your bound mob, wherever they may be. +/obj/item/capture_crystal/ui_action_click() + if(ismob(loc)) + var/mob/living/M = src.loc + if(M != owner) + to_chat(M, "\The [src] emits an unpleasant tone... It does not respond to your command.") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + return + if(!bound_mob) + to_chat(M, "\The [src] emits an unpleasant tone... There is nothing to command.") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + return + if(isanimal(bound_mob) && !bound_mob.client) + if(!isnull(bound_mob.get_AI_stance())) + var/datum/ai_holder/AI = bound_mob.ai_holder + AI.hostile = !AI.hostile + if(!AI.hostile) + AI.set_stance(STANCE_IDLE) + to_chat(M, span("notice", "\The [bound_mob] is now [AI.hostile ? "hostile" : "passive"].")) + return + if(bound_mob.client) + var/transmit_msg + transmit_msg = sanitizeSafe(input(usr, "What is your command?", "Command", null) as text, MAX_NAME_LEN) + if(isnull(transmit_msg)) + to_chat(M, "You decided against it.") + return + to_chat(bound_mob, "\The [owner] commands, '[transmit_msg]'") + to_chat(M, "Your command has been transmitted, '[transmit_msg]'") + else + to_chat(M, "\The [src] emits an unpleasant tone... \The [bound_mob] is unresponsive.") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + else + return + +//Lets the owner get AI controlled bound mobs to follow them, or tells player controlled mobs to follow them. +/obj/item/capture_crystal/verb/follow_owner() + set name = "Toggle Follow" + set category = "Object" + set src in usr + if(ismob(loc)) + var/mob/living/M = src.loc + if(M != owner) + to_chat(M, "\The [src] emits an unpleasant tone... It does not respond to your command.") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + return + if(bound_mob.stat != CONSCIOUS) + to_chat(M, "\The [src] emits an unpleasant tone... \The [bound_mob] is not able to hear your command.") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + return + if(bound_mob.client) + to_chat(bound_mob, "\The [owner] wishes for you to follow them.") + return + if(bound_mob in contents) + var/datum/ai_holder/AI = bound_mob.ai_holder + if(AI.leader) + to_chat(M, "\The [src] chimes~ \The [bound_mob] stopped following [AI.leader].") + AI.lose_follow(AI.leader) + else + AI.set_follow(M) + to_chat(M, "\The [src] chimes~ \The [bound_mob] started following following [AI.leader].") + if(!(bound_mob in view(M))) + to_chat(M, "\The [src] emits an unpleasant tone... \The [bound_mob] is not able to hear your command.") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + return + var/datum/ai_holder/AI = bound_mob.ai_holder + if(AI.leader) + to_chat(M, "\The [src] chimes~ \The [bound_mob] stopped following [AI.leader].") + AI.lose_follow(AI.leader) + else + AI.set_follow(M) + to_chat(M, "\The [src] chimes~ \The [bound_mob] started following following [AI.leader].") + + +/obj/item/capture_crystal/update_icon() . = ..() if(spawn_mob_type) icon_state = "full" - -/obj/item/capture_crystal/attack_self(mob/living/user) - if(world.time < last_activate + activate_cooldown) - to_chat(user, "\The [src] emits an unpleasant tone... It is not ready yet.") return - last_activate = world.time - if(user == bound_mob) + if(!bound_mob) + icon_state = "inactive" + else if(bound_mob in contents) + icon_state = full_icon + else + icon_state = empty_icon + if(!cooldown_check()) + icon_state = "[icon_state]-busy" + +/obj/item/capture_crystal/proc/cooldown_check() + if(world.time < last_activate + activate_cooldown) + return FALSE + else return TRUE + +/obj/item/capture_crystal/attack(mob/living/M, mob/living/user) + if(bound_mob) + if(!bound_mob.devourable) //Don't eat if prefs are bad + return + if(user.zone_sel.selecting == "mouth") //Click while targetting the mouth and you eat/feed the stored mob to whoever you clicked on + if(bound_mob in contents) + user.visible_message("\The [user] moves \the [src] to [M]'s [M.vore_selected]...") + M.perform_the_nom(M, bound_mob, M, M.vore_selected) + return + else if(M == user) //You don't have a mob, you ponder the orb instead of trying to capture yourself + user.visible_message("\The [user] ponders \the [src]...", "You ponder \the [src]...") + return + else //Try to capture someone without throwing + user.visible_message("\The [user] taps \the [M] with \the [src].") + activate(user, M) + +//Tries to unleash or recall your stored mob +/obj/item/capture_crystal/attack_self(mob/living/user) + if(!cooldown_check()) + to_chat(user, "\The [src] emits an unpleasant tone... It is not ready yet.") + playsound(src, 'sound/effects/capture-crystal-problem.ogg', 75, 1, -1) + return + if(user == bound_mob) //You can't recall yourself to_chat(user, "\The [src] emits an unpleasant tone... It does not activate for you.") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) return if(!active) activate(user) return - if(bound_mob && stored_mob) - unleash(user) - else if(bound_mob) - recall(user) - else - to_chat(user, "\The [src] clicks unsatisfyingly.") - active = FALSE - icon_state = "inactive" - owner = null + determine_action(user) +//Make it so the crystal knows if its mob references get deleted to make sure things get cleaned up /obj/item/capture_crystal/proc/knowyoursignals(mob/living/M, mob/living/U) - RegisterSignal(M, COMSIG_PARENT_QDELETING, .proc/mob_was_deleted) - RegisterSignal(U, COMSIG_PARENT_QDELETING, .proc/owner_was_deleted) + RegisterSignal(M, COMSIG_PARENT_QDELETING, .proc/mob_was_deleted, TRUE) + RegisterSignal(U, COMSIG_PARENT_QDELETING, .proc/owner_was_deleted, TRUE) +//The basic capture command does most of the registration work. /obj/item/capture_crystal/proc/capture(mob/living/M, mob/living/U) + if(!M.capture_crystal) + to_chat(U, "This creature is not suitable for capture.") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + return knowyoursignals(M, U) owner = U if(!bound_mob) bound_mob = M + +//Determines the capture chance! So you can't capture AI mobs if they're perfectly healthy and all that +/obj/item/capture_crystal/proc/capture_chance(mob/living/M) + var/capture_chance = ((1 - (M.health / M.maxHealth)) * 100) + if(M.stat == UNCONSCIOUS) + capture_chance += 10 + else if(M.stat == CONSCIOUS) + capture_chance -= 25 + else + capture_chance = 0 + if(capture_chance <= 0) + capture_chance = 0 + last_activate = world.time + return capture_chance +//Handles checking relevent bans, preferences, and asking the player if they want to be caught +/obj/item/capture_crystal/proc/capture_player(mob/living/M, mob/living/U) + if(jobban_isbanned(M, "GhostRoles")) + to_chat(U, "This creature is not suitable for capture.") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + return + if(!M.capture_crystal) + to_chat(U, "This creature is not suitable for capture.") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + return + if(tgui_alert(M, "Would you like to be caught by in [src] by [U]? You will be bound to their will.", "Become Caught",list("No","Yes")) == "Yes") + if(tgui_alert(M, "Are you really sure? The only way to undo this is to OOC escape while you're in the crystal.", "Become Caught", list("No","Yes")) == "Yes") + log_admin("[key_name(M)] has agreed to become caught by [key_name(U)].") + capture(M, U) + recall(U) + +//The clean up procs! /obj/item/capture_crystal/proc/mob_was_deleted() UnregisterSignal(bound_mob, COMSIG_PARENT_QDELETING) bound_mob = null owner = null active = FALSE - icon_state = "inactive" + update_icon() /obj/item/capture_crystal/proc/owner_was_deleted() UnregisterSignal(owner, COMSIG_PARENT_QDELETING) owner = null active = FALSE - icon_state = "inactive" + update_icon() -/obj/item/capture_crystal/proc/activate(mob/living/user) - if(spawn_mob_type && !bound_mob) - bound_mob = new spawn_mob_type(src) +//If the crystal hasn't been set up, it does this +/obj/item/capture_crystal/proc/activate(mob/living/user, target) + if(!cooldown_check()) //Are we ready to do things yet? + to_chat(thrower, "\The [src] clicks unsatisfyingly... It is not ready yet.") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + return + if(spawn_mob_type && !bound_mob) //We don't already have a mob, but we know what kind of mob we want + bound_mob = new spawn_mob_type(src) //Well let's spawn it then! bound_mob.faction = user.faction - stored_mob = TRUE spawn_mob_type = null capture(bound_mob, user) - if(bound_mob) + if(bound_mob) //We have a mob! Let's finish setting up. user.visible_message("\The [src] clicks, and then emits a small chime.", "\The [src] grows warm in your hand, something inside is awake.") active = TRUE - unleash(user) - else - to_chat(user, "\The [src] clicks unsatisfyingly.") + if(!owner) //Do we have an owner? It's pretty unlikely that this would ever happen! But it happens, let's claim the crystal. + owner = user + unleash(user, target) + return + if(isliving(target)) //So we don't have a mob, let's try to claim one! Is the target a mob? + var/mob/living/M = target + if(M.stat == DEAD) //Is it dead? We can't influence dead things. + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + to_chat(user, "\The [src] clicks unsatisfyingly... \The [M] is not in a state to be captured.") + return + if(M.client) //Is it player controlled? + capture_player(M, user) //We have to do things a little differently if so. + return + if(!isanimal(M)) //So it's not player controlled, but it's also not a simplemob? + to_chat(user, "This creature is not suitable for capture.") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + return + var/mob/living/simple_mob/S = M + if(!S.ai_holder) //We don't really want to capture simplemobs that don't have an AI + to_chat(user, "This creature is not suitable for capture.") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + return + if(prob(capture_chance(S))) //OKAY! So we have an NPC simplemob with an AI, let's calculate its capture chance! It varies based on the mob's condition. + capture(S, user) //We did it! Woo! We capture it! + user.visible_message("\The [src] clicks, and then emits a small chime.", "Alright! \The [S] was caught!") + recall(user) + active = TRUE + update_icon() + spawn(activate_cooldown) + update_icon() + else //Shoot, it didn't work and now it's mad!!! + S.ai_holder.go_wake() + S.ai_holder.target = user + S.ai_holder.track_target_position() + S.ai_holder.set_stance(STANCE_FIGHT) + visible_message("\The [src] bonks into \the [S], angering it!") + update_icon() + spawn(activate_cooldown) + update_icon() + return + //The target is not a mob, so let's not do anything. + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + to_chat(user, "\The [src] clicks unsatisfyingly.") -/obj/item/capture_crystal/proc/recall(mob/living/user) - if(bound_mob in view(user)) - //flick_overlay_view(coolanimation, bound_mob.loc, 11, FALSE) +//We're using the crystal, but what will it do? +/obj/item/capture_crystal/proc/determine_action(mob/living/U) + if(!cooldown_check()) //Are we ready yet? + to_chat(thrower, "\The [src] clicks unsatisfyingly... It is not ready yet.") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + return //No + if(bound_mob in contents) //Do we have our mob? + unleash(U) //Yes, let's let it out! + else if (bound_mob) //Do we HAVE a mob? + recall(U) //Yes, let's try to put it back in the crystal + else //No we don't have a mob, let's reset the crystal. + to_chat(U, "\The [src] clicks unsatisfyingly.") + active = FALSE + update_icon() + owner = null + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + +//Let's try to call our mob back! +/obj/item/capture_crystal/proc/recall(mob/living/user, atom/target) + if(bound_mob in view(user)) //We can only recall it if we can see it var/turf/turfmemory = get_turf(bound_mob) if(isanimal(bound_mob)) var/mob/living/simple_mob/M = bound_mob - M.ai_holder.go_sleep() + M.ai_holder.go_sleep() //AI doesn't need to think when it's in the crystal bound_mob.forceMove(src) - icon_state = "full" - stored_mob = TRUE + last_activate = world.time bound_mob.visible_message("\The [user]'s [src] flashes, disappearing [bound_mob] in an instant!!!", "\The [src] pulls you back into confinement in a flash of light!!!") animate_action(turfmemory) + playsound(src, 'sound/effects/capture-crystal-in.ogg', 75, 1, -1) + update_icon() + spawn(activate_cooldown) //We wait for the cooldown to go by + update_icon() else to_chat(user, "\The [src] clicks and emits a small, unpleasant tone. \The [bound_mob] cannot be recalled.") +//Let's let our mob out! /obj/item/capture_crystal/proc/unleash(mob/living/user, atom/target) - if(!target) + if(!user && !target) //We got thrown but we're not sure who did it, let's go to where the crystal is + bound_mob.forceMove(src.drop_location()) + return + if(!target) //We know who wants to let us out, but they didn't say where, so let's drop us on them bound_mob.forceMove(user.drop_location()) - else - bound_mob.forceMove(target) - icon_state = "empty" - stored_mob = FALSE - //flick_overlay_view(coolanimation, bound_mob, 11, FALSE) + else //We got thrown! Let's go where we got thrown + bound_mob.forceMove(target.drop_location()) + last_activate = world.time if(isanimal(bound_mob)) var/mob/living/simple_mob/M = bound_mob - M.ai_holder.go_wake() - bound_mob.visible_message("\The [user]'s [src] flashes, \the [bound_mob] appears in an instant!!!", "The world around you rematerialize as you are unleashed from the [src] next to \the [user].") + M.ai_holder.go_wake() //Okay it's time to do work, let's wake up! + if(!M.client && !M.ghostjoin) + M.ghostjoin = 1 //Players playing mobs is fun! + bound_mob.faction = owner.faction //Let's make sure we aren't hostile to our owner or their friends + bound_mob.visible_message("\The [user]'s [src] flashes, \the [bound_mob] appears in an instant!!!", "The world around you rematerialize as you are unleashed from the [src] next to \the [user]. You feel a strong compulsion to enact \the [owner]'s will.") animate_action(get_turf(bound_mob)) + playsound(src, 'sound/effects/capture-crystal-out.ogg', 75, 1, -1) + update_icon() + spawn(activate_cooldown) + update_icon() +//Let's make a flashy sparkle when someone appears or disappears! /obj/item/capture_crystal/proc/animate_action(atom/thing) var/image/coolanimation = image('icons/obj/capture_crystal_vr.dmi', null, "animation") coolanimation.plane = PLANE_LIGHTING_ABOVE @@ -112,20 +353,367 @@ sleep(11) thing.overlays -= coolanimation +//IF the crystal somehow ends up in a tummy and digesting with a bound mob who doesn't want to be eaten, let's move them to the ground /obj/item/capture_crystal/digest_act(var/atom/movable/item_storage = null) - if(bound_mob && bound_mob.digestable) - return TRUE - else - return FALSE + if(bound_mob in contents && !bound_mob.devourable) + bound_mob.forceMove(src.drop_location()) + return ..() +//We got thrown! Let's figure out what to do /obj/item/capture_crystal/throw_at(atom/target, range, speed, mob/thrower, spin = TRUE, datum/callback/callback) . = ..() - if(!stored_mob || !bound_mob) + if(target == bound_mob && thrower != bound_mob) //We got thrown at our bound mob (and weren't thrown by the bound mob) let's ignore the cooldown and just put them back in + recall(thrower) return - sleep(10) - unleash(thrower, target) + if(!cooldown_check()) //OTHERWISE let's obey the cooldown + to_chat(thrower, "\The [src] emits an soft tone... It is not ready yet.") + playsound(src, 'sound/effects/capture-crystal-problem.ogg', 75, 1, -1) + return + if(!active) //The ball isn't set up, let's try to set it up. + if(isliving(target)) //We're hitting a mob, let's try to capture it. + sleep(10) + activate(thrower, target) + return + sleep(10) + activate(thrower, src) + return + if(!bound_mob) //We hit something else, and we don't have a mob, so we can't really do anything! + to_chat(thrower, "\The [src] clicks unpleasantly...") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) + return + if(bound_mob in contents) //We have our mob! Let's try to let it out. + sleep(10) + unleash(thrower, src) + update_icon() + else //Our mob isn't here, we can't do anything. + to_chat(thrower, "\The [src] clicks unpleasantly...") + playsound(src, 'sound/effects/capture-crystal-negative.ogg', 75, 1, -1) -/obj/item/capture_crystal/woof + +/obj/item/capture_crystal/cass spawn_mob_type = /mob/living/simple_mob/vore/woof/cass /obj/item/capture_crystal/adg spawn_mob_type = /mob/living/simple_mob/mechanical/mecha/combat/gygax/dark/advanced +/obj/item/capture_crystal/bigdragon + spawn_mob_type = /mob/living/simple_mob/vore/bigdragon +/obj/item/capture_crystal/bigdragon/friendly + spawn_mob_type = /mob/living/simple_mob/vore/bigdragon/friendly + +/obj/item/capture_crystal/teppi + spawn_mob_type = /mob/living/simple_mob/vore/alienanimals/teppi +/obj/item/capture_crystal/broodmother + spawn_mob_type = /mob/living/simple_mob/animal/giant_spider/broodmother + +/obj/item/capture_crystal/random + var/static/list/possible_mob_types = list( + list(/mob/living/simple_mob/animal/goat), + list( + /mob/living/simple_mob/animal/passive/bird, + /mob/living/simple_mob/animal/passive/bird/azure_tit, + /mob/living/simple_mob/animal/passive/bird/black_bird, + /mob/living/simple_mob/animal/passive/bird/european_robin, + /mob/living/simple_mob/animal/passive/bird/goldcrest, + /mob/living/simple_mob/animal/passive/bird/ringneck_dove, + /mob/living/simple_mob/animal/passive/bird/parrot, + /mob/living/simple_mob/animal/passive/bird/parrot/black_headed_caique, + /mob/living/simple_mob/animal/passive/bird/parrot/budgerigar, + /mob/living/simple_mob/animal/passive/bird/parrot/budgerigar/blue, + /mob/living/simple_mob/animal/passive/bird/parrot/budgerigar/bluegreen, + /mob/living/simple_mob/animal/passive/bird/parrot/cockatiel, + /mob/living/simple_mob/animal/passive/bird/parrot/cockatiel/grey, + /mob/living/simple_mob/animal/passive/bird/parrot/cockatiel/white, + /mob/living/simple_mob/animal/passive/bird/parrot/cockatiel/yellowish, + /mob/living/simple_mob/animal/passive/bird/parrot/eclectus, + /mob/living/simple_mob/animal/passive/bird/parrot/grey_parrot, + /mob/living/simple_mob/animal/passive/bird/parrot/kea, + /mob/living/simple_mob/animal/passive/bird/parrot/pink_cockatoo, + /mob/living/simple_mob/animal/passive/bird/parrot/sulphur_cockatoo, + /mob/living/simple_mob/animal/passive/bird/parrot/white_caique, + /mob/living/simple_mob/animal/passive/bird/parrot/white_cockatoo + ), + list( + /mob/living/simple_mob/animal/passive/cat, + /mob/living/simple_mob/animal/passive/cat/black + ), + list(/mob/living/simple_mob/animal/passive/chick), + list(/mob/living/simple_mob/animal/passive/cow), + list(/mob/living/simple_mob/animal/passive/dog/brittany), + list(/mob/living/simple_mob/animal/passive/dog/corgi), + list(/mob/living/simple_mob/animal/passive/dog/tamaskan), + list(/mob/living/simple_mob/animal/passive/fox), + list(/mob/living/simple_mob/animal/passive/hare), + list(/mob/living/simple_mob/animal/passive/lizard), + list(/mob/living/simple_mob/animal/passive/mouse), + list(/mob/living/simple_mob/animal/passive/mouse/jerboa), + list(/mob/living/simple_mob/animal/passive/opossum), + list(/mob/living/simple_mob/animal/passive/pillbug), + list(/mob/living/simple_mob/animal/passive/snake), + list(/mob/living/simple_mob/animal/passive/tindalos), + list(/mob/living/simple_mob/animal/passive/yithian), + list( + /mob/living/simple_mob/animal/wolf, + /mob/living/simple_mob/animal/wolf/direwolf + ), + list(/mob/living/simple_mob/vore/rabbit), + list(/mob/living/simple_mob/vore/redpanda), + list(/mob/living/simple_mob/vore/woof), + list(/mob/living/simple_mob/vore/fennec), + list(/mob/living/simple_mob/vore/fennix), + list(/mob/living/simple_mob/vore/hippo), + list(/mob/living/simple_mob/vore/horse), + list(/mob/living/simple_mob/vore/bee), + list( + /mob/living/simple_mob/animal/space/bear, + /mob/living/simple_mob/animal/space/bear/brown + ), + list( + /mob/living/simple_mob/otie/feral, + /mob/living/simple_mob/otie/feral/chubby, + /mob/living/simple_mob/otie/red, + /mob/living/simple_mob/otie/red/chubby + ), + list(/mob/living/simple_mob/animal/sif/diyaab), + list(/mob/living/simple_mob/animal/sif/duck), + list(/mob/living/simple_mob/animal/sif/frostfly), + list( + /mob/living/simple_mob/animal/sif/glitterfly =50, + /mob/living/simple_mob/animal/sif/glitterfly/rare = 1 + ), + list( + /mob/living/simple_mob/animal/sif/kururak = 10, + /mob/living/simple_mob/animal/sif/kururak/leader = 1, + /mob/living/simple_mob/animal/sif/kururak/hibernate = 2, + ), + list( + /mob/living/simple_mob/animal/sif/sakimm = 10, + /mob/living/simple_mob/animal/sif/sakimm/intelligent = 1 + ), + list(/mob/living/simple_mob/animal/sif/savik) = 5, + list( + /mob/living/simple_mob/animal/sif/shantak = 10, + /mob/living/simple_mob/animal/sif/shantak/leader = 1 + ), + list(/mob/living/simple_mob/animal/sif/siffet), + list(/mob/living/simple_mob/animal/sif/tymisian), + list( + /mob/living/simple_mob/animal/giant_spider/nurse = 10, + /mob/living/simple_mob/animal/giant_spider/electric = 5, + /mob/living/simple_mob/animal/giant_spider/frost = 5, + /mob/living/simple_mob/animal/giant_spider/hunter = 10, + /mob/living/simple_mob/animal/giant_spider/ion = 5, + /mob/living/simple_mob/animal/giant_spider/lurker = 10, + /mob/living/simple_mob/animal/giant_spider/pepper = 10, + /mob/living/simple_mob/animal/giant_spider/phorogenic = 10, + /mob/living/simple_mob/animal/giant_spider/thermic = 5, + /mob/living/simple_mob/animal/giant_spider/tunneler = 10, + /mob/living/simple_mob/animal/giant_spider/webslinger = 5 + ), + list( + /mob/living/simple_mob/animal/wolf = 10, + /mob/living/simple_mob/animal/wolf/direwolf = 1 + ), + list(/mob/living/simple_mob/creature/strong), + list(/mob/living/simple_mob/faithless/strong), + list(/mob/living/simple_mob/animal/goat), + list( + /mob/living/simple_mob/animal/sif/shantak/leader = 1, + /mob/living/simple_mob/animal/sif/shantak = 10), + list(/mob/living/simple_mob/animal/sif/savik,), + list(/mob/living/simple_mob/animal/sif/hooligan_crab), + list( + /mob/living/simple_mob/animal/space/alien = 50, + /mob/living/simple_mob/animal/space/alien/drone = 40, + /mob/living/simple_mob/animal/space/alien/sentinel = 25, + /mob/living/simple_mob/animal/space/alien/sentinel/praetorian = 15, + /mob/living/simple_mob/animal/space/alien/queen = 10, + /mob/living/simple_mob/animal/space/alien/queen/empress = 5, + /mob/living/simple_mob/animal/space/alien/queen/empress/mother = 1 + ), + list(/mob/living/simple_mob/animal/space/bats/cult/strong), + list( + /mob/living/simple_mob/animal/space/bear, + /mob/living/simple_mob/animal/space/bear/brown + ), + list( + /mob/living/simple_mob/animal/space/carp = 50, + /mob/living/simple_mob/animal/space/carp/large = 10, + /mob/living/simple_mob/animal/space/carp/large/huge = 5 + ), + list(/mob/living/simple_mob/animal/space/goose), + list(/mob/living/simple_mob/animal/space/jelly), + list(/mob/living/simple_mob/animal/space/tree), + list( + /mob/living/simple_mob/vore/aggressive/corrupthound = 10, + /mob/living/simple_mob/vore/aggressive/corrupthound/prettyboi = 1, + ), + list(/mob/living/simple_mob/vore/aggressive/deathclaw), + list(/mob/living/simple_mob/vore/aggressive/dino), + list(/mob/living/simple_mob/vore/aggressive/dragon), + list(/mob/living/simple_mob/vore/aggressive/dragon/virgo3b), + list(/mob/living/simple_mob/vore/aggressive/frog), + list(/mob/living/simple_mob/vore/aggressive/giant_snake), + list(/mob/living/simple_mob/vore/aggressive/mimic), + list(/mob/living/simple_mob/vore/aggressive/panther), + list(/mob/living/simple_mob/vore/aggressive/rat), + list(/mob/living/simple_mob/vore/bee), + list( + /mob/living/simple_mob/vore/sect_drone = 10, + /mob/living/simple_mob/vore/sect_queen = 1 + ), + list(/mob/living/simple_mob/vore/solargrub), + list( + /mob/living/simple_mob/vore/oregrub = 5, + /mob/living/simple_mob/vore/oregrub/lava = 1 + ), + list(/mob/living/simple_mob/vore/catgirl), + list(/mob/living/simple_mob/vore/wolfgirl), + list( + /mob/living/simple_mob/vore/lamia, + /mob/living/simple_mob/vore/lamia/albino, + /mob/living/simple_mob/vore/lamia/albino/bra, + /mob/living/simple_mob/vore/lamia/albino/shirt, + /mob/living/simple_mob/vore/lamia/bra, + /mob/living/simple_mob/vore/lamia/cobra, + /mob/living/simple_mob/vore/lamia/cobra/bra, + /mob/living/simple_mob/vore/lamia/cobra/shirt, + /mob/living/simple_mob/vore/lamia/copper, + /mob/living/simple_mob/vore/lamia/copper/bra, + /mob/living/simple_mob/vore/lamia/copper/shirt, + /mob/living/simple_mob/vore/lamia/green, + /mob/living/simple_mob/vore/lamia/green/bra, + /mob/living/simple_mob/vore/lamia/green/shirt, + /mob/living/simple_mob/vore/lamia/zebra, + /mob/living/simple_mob/vore/lamia/zebra/bra, + /mob/living/simple_mob/vore/lamia/zebra/shirt + ), + list( + /mob/living/simple_mob/humanoid/merc = 100, + /mob/living/simple_mob/humanoid/merc/melee/sword = 50, + /mob/living/simple_mob/humanoid/merc/ranged = 25, + /mob/living/simple_mob/humanoid/merc/ranged/grenadier = 1, + /mob/living/simple_mob/humanoid/merc/ranged/ionrifle = 10, + /mob/living/simple_mob/humanoid/merc/ranged/laser = 5, + /mob/living/simple_mob/humanoid/merc/ranged/rifle = 5, + /mob/living/simple_mob/humanoid/merc/ranged/smg = 5, + /mob/living/simple_mob/humanoid/merc/ranged/sniper = 1, + /mob/living/simple_mob/humanoid/merc/ranged/space = 10, + /mob/living/simple_mob/humanoid/merc/ranged/technician = 5 + ), + list( + /mob/living/simple_mob/humanoid/pirate = 3, + /mob/living/simple_mob/humanoid/pirate/ranged = 1 + ), + list(/mob/living/simple_mob/mechanical/combat_drone), + list(/mob/living/simple_mob/mechanical/corrupt_maint_drone), + list( + /mob/living/simple_mob/mechanical/hivebot = 100, + /mob/living/simple_mob/mechanical/hivebot/ranged_damage = 20, + /mob/living/simple_mob/mechanical/hivebot/ranged_damage/backline = 10, + /mob/living/simple_mob/mechanical/hivebot/ranged_damage/basic = 20, + /mob/living/simple_mob/mechanical/hivebot/ranged_damage/dot = 5, + /mob/living/simple_mob/mechanical/hivebot/ranged_damage/ion = 20, + /mob/living/simple_mob/mechanical/hivebot/ranged_damage/laser = 10, + /mob/living/simple_mob/mechanical/hivebot/ranged_damage/rapid = 2, + /mob/living/simple_mob/mechanical/hivebot/ranged_damage/siege = 1, + /mob/living/simple_mob/mechanical/hivebot/ranged_damage/siege/emp = 5, + /mob/living/simple_mob/mechanical/hivebot/ranged_damage/siege/fragmentation = 1, + /mob/living/simple_mob/mechanical/hivebot/ranged_damage/siege/radiation = 1, + /mob/living/simple_mob/mechanical/hivebot/ranged_damage/strong = 3, + /mob/living/simple_mob/mechanical/hivebot/ranged_damage/strong/guard = 3, + /mob/living/simple_mob/mechanical/hivebot/support = 8, + /mob/living/simple_mob/mechanical/hivebot/support/commander = 5, + /mob/living/simple_mob/mechanical/hivebot/support/commander/autofollow = 10, + /mob/living/simple_mob/mechanical/hivebot/swarm = 20, + /mob/living/simple_mob/mechanical/hivebot/tank = 20, + /mob/living/simple_mob/mechanical/hivebot/tank/armored = 20, + /mob/living/simple_mob/mechanical/hivebot/tank/armored/anti_bullet = 20, + /mob/living/simple_mob/mechanical/hivebot/tank/armored/anti_laser = 20, + /mob/living/simple_mob/mechanical/hivebot/tank/armored/anti_melee = 20, + /mob/living/simple_mob/mechanical/hivebot/tank/meatshield = 20 + ), + list(/mob/living/simple_mob/mechanical/infectionbot), + list(/mob/living/simple_mob/mechanical/mining_drone), + list(/mob/living/simple_mob/mechanical/technomancer_golem), + list( + /mob/living/simple_mob/mechanical/viscerator, + /mob/living/simple_mob/mechanical/viscerator/piercing + ), + list(/mob/living/simple_mob/mechanical/wahlem), + list(/mob/living/simple_mob/animal/passive/fox/syndicate), + list(/mob/living/simple_mob/animal/passive/fox), + list(/mob/living/simple_mob/animal/wolf/direwolf), + list(/mob/living/simple_mob/animal/space/jelly), + list( + /mob/living/simple_mob/otie/feral, + /mob/living/simple_mob/otie/feral/chubby, + /mob/living/simple_mob/otie/red, + /mob/living/simple_mob/otie/red/chubby + ), + list( + /mob/living/simple_mob/shadekin/blue/ai = 100, + /mob/living/simple_mob/shadekin/green/ai = 50, + /mob/living/simple_mob/shadekin/orange/ai = 20, + /mob/living/simple_mob/shadekin/purple/ai = 60, + /mob/living/simple_mob/shadekin/red/ai = 40, + /mob/living/simple_mob/shadekin/yellow/ai = 1 + ), + list( + /mob/living/simple_mob/vore/aggressive/corrupthound, + /mob/living/simple_mob/vore/aggressive/corrupthound/prettyboi + ), + list(/mob/living/simple_mob/vore/aggressive/deathclaw), + list(/mob/living/simple_mob/vore/aggressive/dino), + list(/mob/living/simple_mob/vore/aggressive/dragon), + list(/mob/living/simple_mob/vore/aggressive/dragon/virgo3b), + list(/mob/living/simple_mob/vore/aggressive/frog), + list(/mob/living/simple_mob/vore/aggressive/giant_snake), + list(/mob/living/simple_mob/vore/aggressive/mimic), + list(/mob/living/simple_mob/vore/aggressive/panther), + list(/mob/living/simple_mob/vore/aggressive/rat), + list(/mob/living/simple_mob/vore/bee), + list(/mob/living/simple_mob/vore/catgirl), + list(/mob/living/simple_mob/vore/cookiegirl), + list(/mob/living/simple_mob/vore/fennec), + list(/mob/living/simple_mob/vore/fennix), + list(/mob/living/simple_mob/vore/hippo), + list(/mob/living/simple_mob/vore/horse), + list(/mob/living/simple_mob/vore/oregrub), + list(/mob/living/simple_mob/vore/rabbit), + list( + /mob/living/simple_mob/vore/redpanda = 50, + /mob/living/simple_mob/vore/redpanda/fae = 1 + ), + list( + /mob/living/simple_mob/vore/sect_drone = 10, + /mob/living/simple_mob/vore/sect_queen = 1 + ), + list(/mob/living/simple_mob/vore/solargrub), + list(/mob/living/simple_mob/vore/woof), + list(/mob/living/simple_mob/vore/alienanimals/teppi), + list(/mob/living/simple_mob/vore/alienanimals/space_ghost), + list(/mob/living/simple_mob/vore/alienanimals/catslug), + list(/mob/living/simple_mob/vore/alienanimals/space_jellyfish), + list(/mob/living/simple_mob/vore/alienanimals/startreader), + list(/mob/living/simple_mob/vore/bigdragon), + list( + /mob/living/simple_mob/vore/leopardmander = 50, + /mob/living/simple_mob/vore/leopardmander/blue = 10, + /mob/living/simple_mob/vore/leopardmander/exotic = 1 + ), + list(/mob/living/simple_mob/vore/sheep), + list( + /mob/living/simple_mob/vore/greatwolf, + /mob/living/simple_mob/vore/greatwolf/black, + /mob/living/simple_mob/vore/greatwolf/grey + ), + list(/mob/living/simple_mob/vore/weretiger) + ) + +/obj/item/capture_crystal/random/Initialize() + var/subchoice = pickweight(possible_mob_types) //Some of the lists have nested lists, so let's pick one of them + var/choice = pickweight(subchoice) //And then we'll pick something from whatever's left + spawn_mob_type = choice //Now when someone uses this, we'll spawn whatever we picked! + return ..() + +/mob/living + var/capture_crystal = TRUE //If TRUE, the mob is capturable. Otherwise it isn't. \ No newline at end of file diff --git a/code/modules/client/preference_setup/vore/09_misc.dm b/code/modules/client/preference_setup/vore/09_misc.dm index 60a207bcc8..fff3bb6af4 100644 --- a/code/modules/client/preference_setup/vore/09_misc.dm +++ b/code/modules/client/preference_setup/vore/09_misc.dm @@ -8,6 +8,7 @@ S["directory_erptag"] >> pref.directory_erptag S["directory_ad"] >> pref.directory_ad S["sensorpref"] >> pref.sensorpref + S["capture_crystal"] >> pref.capture_crystal /datum/category_item/player_setup_item/vore/misc/save_character(var/savefile/S) S["show_in_directory"] << pref.show_in_directory @@ -15,17 +16,21 @@ S["directory_erptag"] << pref.directory_erptag S["directory_ad"] << pref.directory_ad S["sensorpref"] << pref.sensorpref + S["capture_crystal"] << pref.capture_crystal + /datum/category_item/player_setup_item/vore/misc/copy_to_mob(var/mob/living/carbon/human/character) if(pref.sensorpref > 5 || pref.sensorpref < 1) pref.sensorpref = 5 character.sensorpref = pref.sensorpref + character.capture_crystal = pref.capture_crystal /datum/category_item/player_setup_item/vore/misc/sanitize_character() pref.show_in_directory = sanitize_integer(pref.show_in_directory, 0, 1, initial(pref.show_in_directory)) pref.directory_tag = sanitize_inlist(pref.directory_tag, GLOB.char_directory_tags, initial(pref.directory_tag)) pref.directory_erptag = sanitize_inlist(pref.directory_erptag, GLOB.char_directory_erptags, initial(pref.directory_erptag)) pref.sensorpref = sanitize_integer(pref.sensorpref, 1, sensorpreflist.len, initial(pref.sensorpref)) + pref.capture_crystal = sanitize_integer(pref.capture_crystal, 0, 1, initial(pref.capture_crystal)) /datum/category_item/player_setup_item/vore/misc/content(var/mob/user) . += "
" @@ -34,6 +39,7 @@ . += "Character Directory ERP Tag: [pref.directory_erptag]
" . += "Character Directory Advertisement: Set Directory Ad
" . += "Suit Sensors Preference: [sensorpreflist[pref.sensorpref]]
" + . += "Capture Crystal Preference [pref.capture_crystal ? "Yes" : "No"]
" /datum/category_item/player_setup_item/vore/misc/OnTopic(var/href, var/list/href_list, var/mob/user) if(href_list["toggle_show_in_directory"]) @@ -60,4 +66,8 @@ if (!isnull(new_sensorpref) && CanUseTopic(user)) pref.sensorpref = sensorpreflist.Find(new_sensorpref) return TOPIC_REFRESH + else if(href_list["toggle_capture_crystal"]) + pref.capture_crystal = pref.capture_crystal ? 0 : 1; + return TOPIC_REFRESH + return ..(); diff --git a/code/modules/client/preferences_vr.dm b/code/modules/client/preferences_vr.dm index 6bba9fe31e..9aebb70bfc 100644 --- a/code/modules/client/preferences_vr.dm +++ b/code/modules/client/preferences_vr.dm @@ -4,6 +4,7 @@ var/directory_erptag = "Unset" //ditto, but for non-vore scenes var/directory_ad = "" //Advertisement stuff to show in character directory. var/sensorpref = 5 //Set character's suit sensor level + var/capture_crystal = 1 //Whether or not someone is able to be caught with capture crystals var/job_talon_high = 0 var/job_talon_med = 0 @@ -72,7 +73,7 @@ feedback_add_details("admin_verb","TEmoteNoise") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/verb/toggle_ghost_quiets() - set name = "Toggle Whisper/Subtle Vis" + set name = "Toggle Ghost Privacy" set category = "Preferences" set desc = "Toggle ghosts viewing your subtles/whispers." @@ -85,3 +86,22 @@ SScharacter_setup.queue_preferences_save(prefs) feedback_add_details("admin_verb","TWhisubtleVis") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + +/client/verb/toggle_capture_crystal() + set name = "Toggle Catchable" + set category = "Preferences" + set desc = "Toggle being catchable with capture crystals." + + var/mob/living/L = mob + + if(prefs.capture_crystal) + to_chat(src, "You are no longer catchable.") + prefs.capture_crystal = 0 + else + to_chat(src, "You are now catchable.") + prefs.capture_crystal = 1 + if(L) + L.capture_crystal = prefs.capture_crystal + SScharacter_setup.queue_preferences_save(prefs) + + feedback_add_details("admin_verb","TCaptureCrystal") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! \ No newline at end of file diff --git a/code/modules/vore/eating/living_vr.dm b/code/modules/vore/eating/living_vr.dm index 593b344da7..d6fdb91eb2 100644 --- a/code/modules/vore/eating/living_vr.dm +++ b/code/modules/vore/eating/living_vr.dm @@ -432,6 +432,14 @@ var/obj/effect/overlay/aiholo/holo = loc holo.drop_prey() //Easiest way log_and_message_admins("[key_name(src)] used the OOC escape button to get out of [key_name(holo.master)] (AI HOLO) ([holo ? "JMP" : "null"])") + + //You're in a capture crystal! ((It's not vore but close enough!)) + else if(iscapturecrystal(loc)) + var/obj/item/capture_crystal/crystal = loc + crystal.unleash() + crystal.bound_mob = null + crystal.bound_mob = capture_crystal = 0 + log_and_message_admins("[key_name(src)] used the OOC escape button to get out of [crystal] owned by [crystal.owner]. [ADMIN_FLW(src)]") //Don't appear to be in a vore situation else @@ -638,7 +646,11 @@ if(S.holding) to_chat(src, "There's something inside!") return - + if(iscapturecrystal(I)) + var/obj/item/capture_crystal/C = I + if(!C.bound_mob.devourable) + to_chat(src, "That doesn't seem like a good idea. (\The [C.bound_mob]'s prefs don't allow it.)") + return drop_item() I.forceMove(vore_selected) updateVRPanel() @@ -692,6 +704,13 @@ else if (istype(I,/obj/item/clothing/accessory/collar)) visible_message("[src] demonstrates their voracious capabilities by swallowing [I] whole!") to_chat(src, "You can taste the submissiveness in the wearer of [I]!") + else if(iscapturecrystal(I)) + var/obj/item/capture_crystal/C = I + if(C.bound_mob && C.bound_mob in C.contents) + if(isbelly(C.loc)) + var/obj/belly/B = C.loc + to_chat(C.bound_mob, "Outside of your crystal, you can see; [B.desc]") + to_chat(src, "You can taste the the power of command.") else to_chat(src, "You can taste the flavor of garbage. Delicious.") return diff --git a/icons/obj/capture_crystal_vr.dmi b/icons/obj/capture_crystal_vr.dmi index 511a6c94d8..7cd9208b2b 100644 Binary files a/icons/obj/capture_crystal_vr.dmi and b/icons/obj/capture_crystal_vr.dmi differ diff --git a/sound/effects/capture-crystal-in.ogg b/sound/effects/capture-crystal-in.ogg new file mode 100644 index 0000000000..b00d7be9c9 Binary files /dev/null and b/sound/effects/capture-crystal-in.ogg differ diff --git a/sound/effects/capture-crystal-negative.ogg b/sound/effects/capture-crystal-negative.ogg new file mode 100644 index 0000000000..a0d6de3395 Binary files /dev/null and b/sound/effects/capture-crystal-negative.ogg differ diff --git a/sound/effects/capture-crystal-out.ogg b/sound/effects/capture-crystal-out.ogg new file mode 100644 index 0000000000..363ad85c56 Binary files /dev/null and b/sound/effects/capture-crystal-out.ogg differ diff --git a/sound/effects/capture-crystal-problem.ogg b/sound/effects/capture-crystal-problem.ogg new file mode 100644 index 0000000000..3c0a7b343b Binary files /dev/null and b/sound/effects/capture-crystal-problem.ogg differ