diff --git a/code/datums/components/species/shadekin/powers/phase_shift.dm b/code/datums/components/species/shadekin/powers/phase_shift.dm new file mode 100644 index 00000000000..37ef14ccbeb --- /dev/null +++ b/code/datums/components/species/shadekin/powers/phase_shift.dm @@ -0,0 +1,275 @@ +///////////////////// +/// PHASE SHIFT /// +///////////////////// +//Visual effect for phase in/out +/obj/effect/temp_visual/shadekin + randomdir = FALSE + duration = 5 + icon = 'icons/mob/vore_shadekin.dmi' + +/obj/effect/temp_visual/shadekin/phase_in + icon_state = "tp_in" + +/obj/effect/temp_visual/shadekin/phase_out + icon_state = "tp_out" + +/datum/power/shadekin/phase_shift + name = "Phase Shift (100)" + desc = "Shift yourself out of alignment with realspace to travel quickly to different areas." + verbpath = /mob/living/proc/phase_shift + ability_icon_state = "phase_shift" + +/mob/living/proc/phase_shift() + set name = "Phase Shift (100)" + set desc = "Shift yourself out of alignment with realspace to travel quickly to different areas." + set category = "Abilities.Shadekin" + + var/datum/component/shadekin/SK = get_shadekin_component() + if(!SK) + return FALSE + if(SK.special_considerations()) + return FALSE + if(stat) + to_chat(src, span_warning("Can't use that ability in your state!")) + return FALSE + var/area/A = get_area(src) + if(!check_rights_for(client, R_HOLDER) && A.flag_check(AREA_BLOCK_PHASE_SHIFT)) + to_chat(src, span_warning("You can't do that here!")) + return + + var/ability_cost = 100 + + var/darkness = 1 + var/turf/T = get_turf(src) + if(!T) + to_chat(src,span_warning("You can't use that here!")) + return FALSE + + if(SK.doing_phase) + return FALSE + + var/brightness = T.get_lumcount() //Brightness in 0.0 to 1.0 + darkness = 1-brightness //Invert + + var/watcher = 0 + for(var/mob/living/thing in orange(7, src)) //Fun fact, doing two typed loops is faster than doing one untyped loop. Check it with Tracy! + if(istype(thing, /mob/living/carbon/human)) + var/mob/living/carbon/human/watchers = thing + if(watchers in oviewers(7,src)) + var/datum/component/shadekin/watcher_SK = watchers.get_shadekin_component() + if(!watcher_SK && !(watchers.stat) && !isbelly(watchers.loc) && !istype(watchers.loc, /obj/item/holder)) // And they are alive and not being held by someone... + watcher++ //They are watching us! + if(istype(thing, /mob/living/silicon/robot)) + var/mob/living/silicon/robot/watchers = thing + var/datum/component/shadekin/watcher_SK = watchers.get_shadekin_component() //you never know, man. + if(watchers in oviewers(7,src)) + if(!watcher_SK && !watchers.stat && !isbelly(watchers.loc)) + watcher++ //The robot is watching us! + if(SK.camera_counts_as_watcher) + for(var/obj/machinery/camera/watchers in orange(7, src)) + if(watchers.can_use()) + if(src in watchers.can_see()) + watcher++ //The camera is watching us! + + ability_cost = CLAMP(ability_cost/(0.01+darkness*2),50, 80)//This allows for 1 watcher in full light + if(watcher>0) + ability_cost = ability_cost + ( 15 * watcher ) + /* + if(!(SK.in_phase)) + to_chat(world, "[src] attempted to shift with [watcher] visible Carbons with a cost of [ability_cost] in a darkness level of [darkness]") + */ + + if(SK.doing_phase) + to_chat(src, span_warning("You are already trying to phase!")) + return FALSE + else if(SK.shadekin_get_energy() < ability_cost && !(SK.in_phase)) + to_chat(src, span_warning("Not enough energy for that ability!")) + return FALSE + + if(!(SK.in_phase)) + SK.shadekin_adjust_energy(-ability_cost) + playsound(src, SK.phase_noise, 75, 1) + + if(!T.CanPass(src,T) || loc != T) + to_chat(src,span_warning("You can't use that here!")) + return FALSE + + //Shifting in + if(SK.in_phase) + phase_in(T, SK) + //Shifting out + else + phase_out(T, SK) + +/mob/living/proc/phase_in(var/turf/T, var/datum/component/shadekin/SK) + if(SK.in_phase) + + // pre-change + if(!isturf(T)) //Sanity + return + forceMove(T) + var/original_canmove = canmove + SetStunned(0) + SetWeakened(0) + if(buckled) + buckled.unbuckle_mob() + if(pulledby) + pulledby.stop_pulling() + stop_pulling() + + // change + canmove = FALSE + SK.in_phase = FALSE + SK.doing_phase = TRUE + throwpass = FALSE + name = get_visible_name() + for(var/obj/belly/B as anything in vore_organs) + B.escapable = initial(B.escapable) + + //cut_overlays() + invisibility = initial(invisibility) + see_invisible = initial(see_invisible) + incorporeal_move = initial(incorporeal_move) + density = initial(density) + can_pull_size = initial(can_pull_size) + can_pull_mobs = initial(can_pull_mobs) + hovering = initial(hovering) + update_icon() + + //Cosmetics mostly + var/obj/effect/temp_visual/shadekin/phase_in/phaseanim = new SK.phase_in_anim(src.loc) + phaseanim.pixel_y = (src.size_multiplier - 1) * 16 // Pixel shift for the animation placement + phaseanim.adjust_scale(src.size_multiplier, src.size_multiplier) + phaseanim.dir = dir + alpha = 0 + automatic_custom_emote(VISIBLE_MESSAGE,"phases in!") + + addtimer(CALLBACK(src, PROC_REF(shadekin_complete_phase_in), original_canmove, SK), SK.phase_time, TIMER_DELETE_ME) + + +/mob/living/proc/shadekin_complete_phase_in(var/original_canmove, var/datum/component/shadekin/SK) + canmove = original_canmove + alpha = initial(alpha) + remove_modifiers_of_type(/datum/modifier/shadekin_phase_vision) + remove_modifiers_of_type(/datum/modifier/phased_out) + + //Potential phase-in vore + + if(can_be_drop_pred || can_be_drop_prey) //Toggleable in vore panel + var/list/potentials = living_mobs(0) + var/mob/living/our_prey + if(potentials.len) + var/mob/living/target = pick(potentials) + if(can_be_drop_pred && istype(target) && target.devourable && target.can_be_drop_prey && target.phase_vore && vore_selected && phase_vore) + target.forceMove(vore_selected) + to_chat(target, span_vwarning("\The [src] phases in around you, [vore_selected.vore_verb]ing you into their [vore_selected.get_belly_name()]!")) + to_chat(src, span_vwarning("You phase around [target], [vore_selected.vore_verb]ing them into your [vore_selected.get_belly_name()]!")) + our_prey = target + else if(can_be_drop_prey && istype(target) && devourable && target.can_be_drop_pred && target.phase_vore && target.vore_selected && phase_vore) + our_prey = src + forceMove(target.vore_selected) + to_chat(target, span_vwarning("\The [src] phases into you, [target.vore_selected.vore_verb]ing them into your [target.vore_selected.get_belly_name()]!")) + to_chat(src, span_vwarning("You phase into [target], having them [target.vore_selected.vore_verb] you into their [target.vore_selected.get_belly_name()]!")) + if(our_prey) + for(var/obj/item/flashlight/held_lights in our_prey.contents) + if(istype(held_lights,/obj/item/flashlight/glowstick) ||istype(held_lights,/obj/item/flashlight/flare) ) //No affecting glowsticks or flares...As funny as that is + continue + held_lights.on = 0 + held_lights.update_brightness() + + SK.doing_phase = FALSE + if(SK.flicker_time < 5 || SK.flicker_distance < 5 || SK.flicker_break_chance < 5) + Stun(SK.calculate_stun()) + if(!SK.flicker_time) + return //Early return. No time, no flickering. + //Affect nearby lights + for(var/obj/machinery/light/L in range(SK.flicker_distance, src)) + if(prob(SK.flicker_break_chance)) + addtimer(CALLBACK(L, TYPE_PROC_REF(/obj/machinery/light, broken)), rand(5,25), TIMER_DELETE_ME) + else + if(SK.flicker_color) + L.flicker(SK.flicker_time, SK.flicker_color) + else + L.flicker(SK.flicker_time) + for(var/obj/item/flashlight/flashlights in range(SK.flicker_distance, src)) //Find any flashlights near us and make them flicker too! + if(istype(flashlights,/obj/item/flashlight/glowstick) ||istype(flashlights,/obj/item/flashlight/flare)) //No affecting glowsticks or flares...As funny as that is + continue + flashlights.flicker(SK.flicker_time, SK.flicker_color, TRUE) + for(var/mob/living/creatures in range(SK.flicker_distance, src)) + if(isbelly(creatures.loc)) //don't flicker anyone that gets nomphed. + continue + for(var/obj/item/flashlight/held_lights in creatures.contents) + if(istype(held_lights,/obj/item/flashlight/glowstick) ||istype(held_lights,/obj/item/flashlight/flare) ) //No affecting glowsticks or flares...As funny as that is + continue + held_lights.flicker(SK.flicker_time, SK.flicker_color, TRUE) + +/mob/living/proc/phase_out(var/turf/T) + var/datum/component/shadekin/SK = get_shadekin_component() + if(!(SK.in_phase)) + // pre-change + forceMove(T) + var/original_canmove = canmove + SetStunned(0) + SetWeakened(0) + if(buckled) + buckled.unbuckle_mob() + if(pulledby) + pulledby.stop_pulling() + stop_pulling() + if(SK.normal_phase && SK.drop_items_on_phase) + drop_both_hands() + if(back) + unEquip(back) + + can_pull_size = 0 + can_pull_mobs = MOB_PULL_NONE + hovering = TRUE + canmove = FALSE + + // change + SK.in_phase = TRUE + SK.doing_phase = TRUE + throwpass = TRUE + automatic_custom_emote(VISIBLE_MESSAGE,"phases out!") + + if(real_name) //If we a real name, perfect, let's just set our name to our newfound visible name. + name = get_visible_name() + else //If we don't, let's put our real_name as our initial name. + real_name = initial(name) + name = get_visible_name() + + for(var/obj/belly/B as anything in vore_organs) + B.escapable = FALSE + + var/obj/effect/temp_visual/shadekin/phase_out/phaseanim = new SK.phase_out_anim(src.loc) + phaseanim.pixel_y = (src.size_multiplier - 1) * 16 // Pixel shift for the animation placement + phaseanim.adjust_scale(src.size_multiplier, src.size_multiplier) + phaseanim.dir = dir + alpha = 0 + add_modifier(/datum/modifier/shadekin_phase_vision) + if(SK.normal_phase) + add_modifier(/datum/modifier/phased_out) + addtimer(CALLBACK(src, PROC_REF(complete_phase_out), original_canmove, SK), SK.phase_time, TIMER_DELETE_ME) + + +/mob/living/proc/complete_phase_out(original_canmove, var/datum/component/shadekin/SK) + invisibility = INVISIBILITY_SHADEKIN + see_invisible = INVISIBILITY_SHADEKIN + see_invisible_default = INVISIBILITY_SHADEKIN // Allow seeing phased entities while phased. + update_icon() + alpha = 127 + + canmove = original_canmove + incorporeal_move = TRUE + density = FALSE + SK.doing_phase = FALSE + +/datum/modifier/shadekin_phase_vision + name = "Shadekin Phase Vision" + vision_flags = SEE_THRU + +/datum/modifier/phased_out + name = "Phased Out" + desc = "You are currently phased out of realspace, and cannot interact with it." + hidden = TRUE + //Stops you from using guns. See /obj/item/gun/proc/special_check(var/mob/user) diff --git a/code/datums/components/species/shadekin/shadekin.dm b/code/datums/components/species/shadekin/shadekin.dm new file mode 100644 index 00000000000..b1020607589 --- /dev/null +++ b/code/datums/components/species/shadekin/shadekin.dm @@ -0,0 +1,297 @@ +//See comp_helpers.dm for helper procs. +/datum/component/shadekin + VAR_PRIVATE/mob/living/owner + dupe_mode = COMPONENT_DUPE_UNIQUE + + //Energy Vars + ///How much energy we have RIGHT NOW + var/dark_energy = 100 + ///How much energy we can have + var/max_dark_energy = 100 + ///Always be at our max_dark_energy + var/dark_energy_infinite = FALSE + ///How much energy we generate in the dark + var/energy_dark = 0.75 + ///How much energy we generate in the light + var/energy_light = 0.25 + ///If we care about eye color when it comes to factoring in energy + var/eye_color_influences_energy = TRUE + ///Energy gain from nutrition + var/nutrition_conversion_scaling = 0.5 + ///If we convert nutrition to energy + var/nutrition_energy_conversion = FALSE + + //Phase Vars + ///Are we currently in a phase transition? + var/doing_phase = FALSE + ///Are we currently phased? + var/in_phase = FALSE + ///Chance to break lights on phase-in + var/flicker_break_chance = 0 + ///Color that lights will flicker to on phase-in. Off by default. + var/flicker_color + ///Time that lights will flicker on phase-in. Default is 10 times. + var/flicker_time = 10 + ///Range that we flicker lights. Default is 10. + var/flicker_distance = 10 + ///If we can get the 'phase debuff' applied to us. (No using guns, dropping things in hands, etc). + var/normal_phase = TRUE + ///If we drop items on phase. + var/drop_items_on_phase = FALSE + ///If cameras count as watchers for us + var/camera_counts_as_watcher = FALSE + ///Phase in animation + var/obj/effect/temp_visual/phase_in_anim = /obj/effect/temp_visual/shadekin/phase_in + ///Phase out animation + var/obj/effect/temp_visual/phase_out_anim = /obj/effect/temp_visual/shadekin/phase_out + //How long does it take to complete + var/phase_time = 0.5 SECONDS + //Phase sound + var/phase_noise = 'sound/effects/stealthoff.ogg' + + //Dark Respite Vars (Unused on Virgo) + ///If we are in dark respite or not + var/in_dark_respite = FALSE + var/manual_respite = FALSE + var/respite_activating = FALSE + ///If we return to The Dark upon death or not. + var/no_retreat = FALSE + + //Dark Tunneling Vars (Unused on Virgo) + ///If we have already made a dark tunnel + var/created_dark_tunnel = FALSE + + //Dark Maw Vars (Unused on Virgo) + ///Our current active dark maws + var/list/active_dark_maws = list() + + //Ability Vars + ///The innate abilities we start with + var/list/shadekin_abilities = list(/datum/power/shadekin/phase_shift, + /datum/power/shadekin/regenerate_other, + /datum/power/shadekin/create_shade) + ///Datum holder. Largely ignore this. + var/list/shadekin_ability_datums = list() + + //Misc Vars + ///Eyecolor + var/eye_color = BLUE_EYES + ///For downstream. Enables some extra verbs. Causes things to drop in hand when you phase. + var/extended_kin = FALSE + +/datum/component/shadekin/phase_only + shadekin_abilities = list(/datum/power/shadekin/phase_shift) + +/datum/component/shadekin/full + shadekin_abilities = list(/datum/power/shadekin/phase_shift, + /datum/power/shadekin/regenerate_other, + /datum/power/shadekin/create_shade, + /datum/power/shadekin/dark_maw, + /datum/power/shadekin/dark_respite, + /datum/power/shadekin/dark_tunneling) + extended_kin = TRUE + drop_items_on_phase = TRUE + camera_counts_as_watcher = TRUE + +/datum/component/shadekin/full/rakshasa + flicker_time = 0 //Rakshasa don't flicker lights when they phase in. + dark_energy_infinite = TRUE + normal_phase = FALSE + +/datum/component/shadekin/Initialize() + //normal component bs + if(!isliving(parent) || issilicon(parent)) + return COMPONENT_INCOMPATIBLE + owner = parent + if(ishuman(owner)) + RegisterSignal(owner, COMSIG_SHADEKIN_COMPONENT, PROC_REF(handle_comp)) //Happens every species tick. + else + RegisterSignal(owner, COMSIG_LIVING_LIFE, PROC_REF(handle_comp)) //Happens every life tick (mobs) + + //generates powers and then adds them + build_and_add_abilities() + + handle_comp() //First hit is free! + + //decides what 'eye color' we are and how much energy we should get + set_shadekin_eyecolor() //Gets what eye color we are. + set_eye_energy() //Sets the energy values based on our eye color. + + //Misc stuff we need to do + add_verb(owner, /mob/living/proc/shadekin_control_panel) + +/datum/component/shadekin/Destroy(force) + if(ishuman(owner)) + UnregisterSignal(owner, COMSIG_SHADEKIN_COMPONENT) + else + UnregisterSignal(owner, COMSIG_LIVING_LIFE) + remove_verb(owner, /mob/living/proc/shadekin_control_panel) + for(var/datum/power in shadekin_ability_datums) + qdel(power) + for(var/obj/effect/abstract/dark_maw/dm as anything in active_dark_maws) //if the component gets destroyed so does your precious maws + if(!QDELETED(dm)) + qdel(dm) + if(owner.shadekin_display) + owner.shadekin_display.invisibility = INVISIBILITY_ABSTRACT //hide it + replace_shadekin_master() + active_dark_maws.Cut() + shadekin_abilities.Cut() + shadekin_ability_datums.Cut() + owner = null + . = ..() + +/datum/component/shadekin/proc/recalc_values() + set_shadekin_eyecolor() //Gets what eye color we are. + set_eye_energy() //Sets the energy values based on our eye color. + +///Handles the component running. +/datum/component/shadekin/proc/handle_comp() + SIGNAL_HANDLER + if(QDELETED(parent)) + return + if(owner.stat == DEAD) //dead, don't process. + return + handle_shade() + +///Handles the shadekin's energy gain and loss. +/datum/component/shadekin/proc/handle_shade() + //Shifted kin don't gain/lose energy (and save time if we're at the cap) + var/darkness = 1 + var/dark_gains = 0 + + var/suit = owner.get_equipped_item(slot_wear_suit) + if(istype(suit, /obj/item/clothing/suit/space)) + if(dark_energy) + to_chat(owner, span_warning("You feel your energy waning and your powers being blocked from the heavy equipment you're wearing!")) + dark_energy = 0 + return + + var/turf/T = get_turf(owner) + if(!T) + dark_gains = 0 + return + + var/brightness = T.get_lumcount() //Brightness in 0.0 to 1.0 + darkness = 1-brightness //Invert + var/is_dark = (darkness >= 0.5) + + if(in_phase) + dark_gains = 0 + else + //Heal (very) slowly in good darkness + if(is_dark) + //The below sends a DB query...This needs to be fixed before this can be enabled as we're now dealing with signal handlers. + //Reenable once that mess is taken care of. + owner.adjustFireLoss((-0.10)*darkness) + owner.adjustBruteLoss((-0.10)*darkness) + owner.adjustToxLoss((-0.10)*darkness) + //energy_dark and energy_light are set by the shadekin eye traits. + //These are balanced around their playstyles and 2 planned new aggressive abilities + dark_gains = energy_dark + else + dark_gains = energy_light + + handle_nutrition_conversion(dark_gains) + + shadekin_adjust_energy(dark_gains) + + //Update huds + update_shadekin_hud() + +/datum/component/shadekin/proc/calculate_stun() + var/stun_time = 3 + if(flicker_time > 0) + stun_time -= min(flicker_time / 5, 1) + if(flicker_distance > 0) + stun_time -= min(flicker_distance / 5, 1) + if(flicker_break_chance > 0) + stun_time -= min(flicker_break_chance / 5, 1) + return stun_time + +///Sees if the savefile we have selected in CHARACTER SETUP is the same as our ACTIVE CHARACTER savefile. +/datum/component/shadekin/proc/correct_savefile_selected() + if(owner.client.prefs.default_slot == owner.mind.loaded_from_slot) + return TRUE + return FALSE + +/datum/component/shadekin/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ShadekinConfig", "Shadekin Config") + ui.open() + +/datum/component/shadekin/tgui_data(mob/user) + var/data = list( + "stun_time" = calculate_stun(), + "flicker_time" = flicker_time, + "flicker_color" = flicker_color, + "flicker_break_chance" = flicker_break_chance, + "flicker_distance" = flicker_distance, + "no_retreat" = no_retreat, + "nutrition_energy_conversion" = nutrition_energy_conversion, + "extended_kin" = extended_kin, + "savefile_selected" = correct_savefile_selected() + ) + + return data + +/datum/component/shadekin/tgui_close(mob/user) + SScharacter_setup.queue_preferences_save(user?.client?.prefs) + . = ..() + +/datum/component/shadekin/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + if(..()) + return TRUE + + switch(action) + if("adjust_time") + var/new_time = text2num(params["val"]) + new_time = CLAMP(new_time, 2, 20) + if(!isnum(new_time)) + return FALSE + flicker_time = new_time + ui.user.write_preference_directly(/datum/preference/numeric/living/flicker_time, new_time, WRITE_PREF_MANUAL, save_to_played_slot = TRUE) + return TRUE + if("adjust_color") + var/set_new_color = tgui_color_picker(ui.user, "Select a color you wish the lights to flicker as (Default is #E0EFF0)", "Color Selector", flicker_color) + if(!set_new_color) + return FALSE + flicker_color = set_new_color + ui.user.write_preference_directly(/datum/preference/color/living/flicker_color, set_new_color, WRITE_PREF_MANUAL, save_to_played_slot = TRUE) + return TRUE + if("adjust_break") + var/new_break_chance = text2num(params["val"]) + new_break_chance = CLAMP(new_break_chance, 0, 25) + if(!isnum(new_break_chance)) + return FALSE + flicker_break_chance = new_break_chance + ui.user.write_preference_directly(/datum/preference/numeric/living/flicker_break_chance, new_break_chance, WRITE_PREF_MANUAL, save_to_played_slot = TRUE) + return TRUE + if("adjust_distance") + var/new_distance = text2num(params["val"]) + new_distance = CLAMP(new_distance, 4, 10) + if(!isnum(new_distance)) + return FALSE + flicker_distance = new_distance + ui.user.write_preference_directly(/datum/preference/numeric/living/flicker_distance, new_distance, WRITE_PREF_MANUAL, save_to_played_slot = TRUE) + return TRUE + if("toggle_retreat") + var/new_retreat = !no_retreat + no_retreat = !no_retreat + ui.user.write_preference_directly(/datum/preference/toggle/living/dark_retreat_toggle, new_retreat, WRITE_PREF_MANUAL, save_to_played_slot = TRUE) + if("toggle_nutrition") + var/new_retreat = !nutrition_energy_conversion + nutrition_energy_conversion = !nutrition_energy_conversion + ui.user.write_preference_directly(/datum/preference/toggle/living/shadekin_nutrition_conversion, new_retreat, WRITE_PREF_MANUAL, save_to_played_slot = TRUE) + +/mob/living/proc/shadekin_control_panel() + set name = "Shadekin Control Panel" + set desc = "Allows you to adjust the settings of various shadekin settings!" + set category = "Abilities.Shadekin" + + var/datum/component/shadekin/SK = get_shadekin_component() + if(!SK) + to_chat(src, span_warning("Only a shadekin can use that!")) + return FALSE + + SK.tgui_interact(src)