diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm index 5d9ce528c65..9cd8f80af18 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm @@ -152,6 +152,16 @@ ///from /atom/movable/screen/alert/give/proc/handle_transfer(): (taker, item) #define COMSIG_CARBON_ITEM_GIVEN "carbon_item_given" +/// Saiyan survived a near-death encounter +#define COMSIG_SAIYAN_SURVIVOR "miracle_zenkai_power" +/// Saiyan just lost their tail +#define COMSIG_SAIYAN_TAIL_REMOVED "saiyan_tail_removed" + +/// Signal sent when you use the kaioken technique (power_multiplier) +#define COMSIG_KAIKOEN_APPLIED "kaioken_applied" +/// Signal sent when you lose the kaioken power (power_multiplier) +#define COMSIG_KAIOKEN_REMOVED "kaioken_removed" + /// Sent from /mob/living/carbon/human/handle_blood(): (seconds_per_tick, times_fired) #define COMSIG_HUMAN_ON_HANDLE_BLOOD "human_on_handle_blood" /// Return to prevent all default blood handling diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm index bca8c9002e2..64c4577fc5b 100644 --- a/code/__DEFINES/mobs.dm +++ b/code/__DEFINES/mobs.dm @@ -133,6 +133,7 @@ #define SPECIES_MUSHROOM "mush" #define SPECIES_PLASMAMAN "plasmaman" #define SPECIES_PODPERSON "pod" +#define SPECIES_SAIYAN "saiyan" #define SPECIES_SHADOW "shadow" #define SPECIES_SKELETON "skeleton" #define SPECIES_SNAIL "snail" diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index b96fe2025b3..829bdf0d6b4 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -89,6 +89,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// "Magic" trait that blocks the mob from moving or interacting with anything. Used for transient stuff like mob transformations or incorporality in special cases. /// Will block movement, `Life()` (!!!), and other stuff based on the mob. #define TRAIT_NO_TRANSFORM "block_transformations" +/// I've been turned into something else +#define TRAIT_SHAPESHIFTED "shapeshifted" /// Tracks whether we're gonna be a baby alien's mummy. #define TRAIT_XENO_HOST "xeno_host" /// This parrot is currently perched @@ -862,6 +864,12 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_CONTRABAND_BLOCKER "contraband_blocker" /// For edible items that cannot be composted inside hydro trays #define TRAIT_UNCOMPOSTABLE "uncompostable" +/// limbs with this trait can be empowered by a saiyan heart +#define TRAIT_SAIYAN_STRENGTH "saiyan_strength" +/// Allows you to detect power levels +#define TRAIT_MARTIAL_VISION "martial_vision" +/// Are you the legendary super saiyan? If so, you are allowed to become blonde. +#define TRAIT_POWER_HAIR "thirty_dollar_haircut" //quirk traits #define TRAIT_ALCOHOL_TOLERANCE "alcohol_tolerance" diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index f3988082b16..82029b82033 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -330,6 +330,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_MAGICALLY_PHASED" = TRAIT_MAGICALLY_PHASED, "TRAIT_MARTIAL_ARTS_IMMUNE" = TRAIT_MARTIAL_ARTS_IMMUNE, "TRAIT_MANSUS_TOUCHED" = TRAIT_MANSUS_TOUCHED, + "TRAIT_MARTIAL_VISION" = TRAIT_MARTIAL_VISION, "TRAIT_MEDIBOTCOMINGTHROUGH" = TRAIT_MEDIBOTCOMINGTHROUGH, "TRAIT_MEDICAL_HUD" = TRAIT_MEDICAL_HUD, "TRAIT_MESON_VISION" = TRAIT_MESON_VISION, @@ -430,6 +431,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_PLANT_SAFE" = TRAIT_PLANT_SAFE, "TRAIT_PLASMA_LOVER_METABOLISM" = TRAIT_PLASMA_LOVER_METABOLISM, "TRAIT_POSTERBOY" = TRAIT_POSTERBOY, + "TRAIT_POWER_HAIR" = TRAIT_POWER_HAIR, "TRAIT_PRESENT_VISION" = TRAIT_PRESENT_VISION, "TRAIT_PRESERVE_UI_WITHOUT_CLIENT" = TRAIT_PRESERVE_UI_WITHOUT_CLIENT, "TRAIT_PREVENT_IMPLANT_AUTO_EXPLOSION" = TRAIT_PREVENT_IMPLANT_AUTO_EXPLOSION, @@ -465,10 +467,12 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_ROD_SUPLEX" = TRAIT_ROD_SUPLEX, "TRAIT_ROUGHRIDER" = TRAIT_ROUGHRIDER, "TRAIT_SABRAGE_PRO" = TRAIT_SABRAGE_PRO, + "TRAIT_SAIYAN_STRENGTH" = TRAIT_SAIYAN_STRENGTH, "TRAIT_SECURITY_HUD" = TRAIT_SECURITY_HUD, "TRAIT_SEE_WORN_COLOURS" = TRAIT_SEE_WORN_COLOURS, "TRAIT_SELF_AWARE" = TRAIT_SELF_AWARE, "TRAIT_SETTLER" = TRAIT_SETTLER, + "TRAIT_SHAPESHIFTED" = TRAIT_SHAPESHIFTED, "TRAIT_SHAVED" = TRAIT_SHAVED, "TRAIT_SHIFTY_EYES" = TRAIT_SHIFTY_EYES, "TRAIT_SHOCKIMMUNE" = TRAIT_SHOCKIMMUNE, diff --git a/code/controllers/subsystem/sprite_accessories.dm b/code/controllers/subsystem/sprite_accessories.dm index 2d121daa7a0..5dcd6057908 100644 --- a/code/controllers/subsystem/sprite_accessories.dm +++ b/code/controllers/subsystem/sprite_accessories.dm @@ -46,6 +46,7 @@ SUBSYSTEM_DEF(accessories) // just 'accessories' for brevity var/list/tails_list_felinid var/list/tails_list_lizard var/list/tails_list_monkey + var/list/tails_list_saiyan var/list/tails_list_fish var/list/ears_list var/list/wings_list @@ -56,6 +57,7 @@ SUBSYSTEM_DEF(accessories) // just 'accessories' for brevity var/list/caps_list var/list/pod_hair_list + /datum/controller/subsystem/accessories/PreInit() // this stuff NEEDS to be set up before GLOB for preferences and stuff to work so this must go here. sorry setup_lists() init_hair_gradients() @@ -91,6 +93,7 @@ SUBSYSTEM_DEF(accessories) // just 'accessories' for brevity tails_list_felinid = init_sprite_accessory_subtypes(/datum/sprite_accessory/tails/felinid, add_blank = TRUE)[DEFAULT_SPRITE_LIST] tails_list_lizard = init_sprite_accessory_subtypes(/datum/sprite_accessory/tails/lizard)[DEFAULT_SPRITE_LIST] tails_list_monkey = init_sprite_accessory_subtypes(/datum/sprite_accessory/tails/monkey)[DEFAULT_SPRITE_LIST] + tails_list_saiyan = init_sprite_accessory_subtypes(/datum/sprite_accessory/tails/saiyan, add_blank = FALSE)[DEFAULT_SPRITE_LIST] //tails fo fish organ infusions, not for prefs. tails_list_fish = init_sprite_accessory_subtypes(/datum/sprite_accessory/tails/fish)[DEFAULT_SPRITE_LIST] snouts_list = init_sprite_accessory_subtypes(/datum/sprite_accessory/snouts)[DEFAULT_SPRITE_LIST] @@ -107,6 +110,8 @@ SUBSYSTEM_DEF(accessories) // just 'accessories' for brevity moth_markings_list = init_sprite_accessory_subtypes(/datum/sprite_accessory/moth_markings, add_blank = TRUE)[DEFAULT_SPRITE_LIST] pod_hair_list = init_sprite_accessory_subtypes(/datum/sprite_accessory/pod_hair)[DEFAULT_SPRITE_LIST] + + /// This proc just initializes all /datum/sprite_accessory/hair_gradient into an list indexed by gradient-style name /datum/controller/subsystem/accessories/proc/init_hair_gradients() hair_gradients_list = list() diff --git a/code/datums/sprite_accessories.dm b/code/datums/sprite_accessories.dm index 50409d51126..73e84ade292 100644 --- a/code/datums/sprite_accessories.dm +++ b/code/datums/sprite_accessories.dm @@ -1811,6 +1811,14 @@ icon_state = "default" color_src = FALSE +/datum/sprite_accessory/tails/saiyan + icon = 'icons/mob/human/species/monkey/monkey_tail.dmi' + color_src = FALSE + +/datum/sprite_accessory/tails/saiyan/standard + name = "Saiyan" + icon_state = "monkey" + /datum/sprite_accessory/pod_hair icon = 'icons/mob/human/species/podperson_hair.dmi' em_block = TRUE diff --git a/code/game/atom/atom_examine.dm b/code/game/atom/atom_examine.dm index 2151e3927b9..968b66f4498 100644 --- a/code/game/atom/atom_examine.dm +++ b/code/game/atom/atom_examine.dm @@ -165,3 +165,8 @@ /// force_real_name will always return real_name and add (as face_name/id_name) if it doesn't match their appearance /atom/proc/get_visible_name(add_id_name, force_real_name) return name + +/mob/living/examine(mob/user) + . = ..() + if(HAS_TRAIT(user, TRAIT_MARTIAL_VISION)) + . += report_power_level() diff --git a/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm index 997447aaa04..9d27c22b3a9 100644 --- a/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm +++ b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm @@ -197,4 +197,67 @@ . = ..() qdel(GetComponent(/datum/component/amputating_limbs)) +/// A terrifyingly powerful ape from space +/mob/living/basic/gorilla/saiyan + name = "Saiyan Great Ape" + desc = "A large and destructive ape-like creature, capable of surviving the depths of space and discharging energy beams." + unsuitable_atmos_damage = 0 + unsuitable_cold_damage = 0 + basic_mob_flags = DEL_ON_DEATH + ai_controller = null + +/mob/living/basic/gorilla/saiyan/Initialize(mapload) + . = ..() + add_traits(list(TRAIT_MARTIAL_VISION, TRAIT_SPACEWALK), INNATE_TRAIT) + AddComponent(\ + /datum/component/ranged_attacks,\ + projectile_type = /obj/projectile/beam/emitter/hitscan,\ + projectile_sound = 'sound/items/weapons/emitter.ogg',\ + cooldown_time = 0.5 SECONDS, \ + ) + RegisterSignal(src, COMSIG_ATOM_AFTER_ATTACKEDBY, PROC_REF(check_tail_sever)) + update_appearance(UPDATE_ICON) + +/mob/living/basic/gorilla/saiyan/update_icon_state() + . = ..() + if (stat == DEAD) + return + icon_state = "great_ape" + +/mob/living/basic/gorilla/saiyan/death(gibbed) + var/mob/living/corpse = get_internal_saiyan() + corpse.death() + corpse.setBruteLoss(corpse.maxHealth, TRUE, TRUE) + return ..() + +/// Cut off his tail! It's the only way! +/mob/living/basic/gorilla/saiyan/proc/check_tail_sever(mob/living/target, obj/item/weapon, mob/attacker, proximity_flag, click_parameters) + SIGNAL_HANDLER + if (!proximity_flag || weapon.force < 5 || weapon.get_sharpness() != SHARP_EDGED) + return + if (!prob(3)) + return + target.visible_message(span_warning("[src]'s tail falls to the ground, severed completely!")) + INVOKE_ASYNC(target, TYPE_PROC_REF(/mob, emote), "scream") + + var/mob/living/carbon/saiyan = get_internal_saiyan() + if (istype(saiyan)) + var/obj/item/organ/tail/saiyan_tail = saiyan.get_organ_slot(ORGAN_SLOT_EXTERNAL_TAIL) + saiyan_tail.Remove(saiyan) + saiyan_tail.forceMove(saiyan.loc) + + remove_status_effect(/datum/status_effect/shapechange_mob) + qdel(src) + +/// Find our normal body or return a fake saiyan +/mob/living/basic/gorilla/saiyan/proc/get_internal_saiyan() + var/datum/status_effect/shapechange_mob/shapechange_status = has_status_effect(/datum/status_effect/shapechange_mob) + if (!isnull(shapechange_status)) + return shapechange_status.caster_mob + + var/mob/saiyan = new /mob/living/carbon/human/species/saiyan(loc) + saiyan.name = name + saiyan.real_name = name + return saiyan + #undef GORILLA_HANDS_LAYER diff --git a/code/modules/mob/living/basic/lavaland/brimdemon/brimbeam.dm b/code/modules/mob/living/basic/lavaland/brimdemon/brimbeam.dm index 5900289cae5..dcb07daa828 100644 --- a/code/modules/mob/living/basic/lavaland/brimdemon/brimbeam.dm +++ b/code/modules/mob/living/basic/lavaland/brimdemon/brimbeam.dm @@ -9,6 +9,8 @@ click_to_activate = TRUE cooldown_time = 5 SECONDS melee_cooldown_time = 0 + /// How long to go on cooldown when interrupted? + var/fail_cooldown = 5 SECONDS /// How far does our beam go? var/beam_range = 10 /// How long does our beam last? @@ -19,6 +21,8 @@ var/static/image/direction_overlay = image('icons/mob/simple/lavaland/lavaland_monsters.dmi', "brimdemon_telegraph_dir") /// A list of all the beam parts. var/list/beam_parts = list() + /// If this isn't a child of /obj/effect/brimbeam it will likely runtime + var/created_type = /obj/effect/brimbeam /datum/action/cooldown/mob_cooldown/brimbeam/Destroy() extinguish_laser() @@ -35,13 +39,12 @@ var/fully_charged = do_after(owner, delay = charge_duration, target = owner) owner.cut_overlay(direction_overlay) if (!fully_charged) - StartCooldown() + StartCooldown(fail_cooldown) return TRUE if (!fire_laser()) - var/static/list/fail_emotes = list("coughs.", "wheezes.", "belches out a puff of black smoke.") - owner.manual_emote(pick(fail_emotes)) - StartCooldown() + on_fail() + StartCooldown(fail_cooldown) return TRUE do_after(owner, delay = beam_duration, target = owner, hidden = TRUE) @@ -49,6 +52,11 @@ StartCooldown() return TRUE +/// Emote if fired directly into a wall +/datum/action/cooldown/mob_cooldown/brimbeam/proc/on_fail() + var/static/list/fail_emotes = list("coughs.", "wheezes.", "belches out a puff of black smoke.") + owner.manual_emote(pick(fail_emotes)) + /// Create a laser in the direction we are facing /datum/action/cooldown/mob_cooldown/brimbeam/proc/fire_laser() owner.visible_message(span_danger("[owner] fires a brimbeam!")) @@ -66,20 +74,20 @@ break if(blocked) break - var/obj/effect/brimbeam/new_brimbeam = new(affected_turf) + var/obj/effect/brimbeam/new_brimbeam = new created_type(affected_turf) new_brimbeam.dir = owner.dir beam_parts += new_brimbeam new_brimbeam.assign_creator(owner) for(var/mob/living/hit_mob in affected_turf.contents) hit_mob.apply_damage(damage = 25, damagetype = BURN) - to_chat(hit_mob, span_userdanger("You're blasted by [owner]'s brimbeam!")) + to_chat(hit_mob, span_userdanger("You're blasted by [owner]'s beam!")) RegisterSignal(new_brimbeam, COMSIG_QDELETING, PROC_REF(extinguish_laser)) // In case idk a singularity eats it or something if(!length(beam_parts)) return FALSE var/atom/last_brimbeam = beam_parts[length(beam_parts)] - last_brimbeam.icon_state = "brimbeam_end" + last_brimbeam.icon_state = "[last_brimbeam.base_icon_state]_end" var/atom/first_brimbeam = beam_parts[1] - first_brimbeam.icon_state = "brimbeam_start" + first_brimbeam.icon_state = "[last_brimbeam.base_icon_state]_start" return TRUE /// Get rid of our laser when we are done with it @@ -96,6 +104,7 @@ name = "brimbeam" icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi' icon_state = "brimbeam_mid" + base_icon_state = "brimbeam" layer = ABOVE_MOB_LAYER plane = ABOVE_GAME_PLANE mouse_opacity = MOUSE_OPACITY_TRANSPARENT diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm index 7d0ad8f191a..4df6ebbc3bd 100644 --- a/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm +++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm @@ -21,12 +21,18 @@ var/image/current_overlay /// Timer until we go to the next stage var/stage_timer + /// Text to print upon use + var/report_started = "'s eye glows ominously!" + /// What do we report blinded people? + var/blinded_source = "piercing gaze" + /// Should we stun ourselves after? + var/stop_self = TRUE /datum/action/cooldown/mob_cooldown/watcher_gaze/Activate(mob/living/target) show_indicator_overlay("eye_open") stage_timer = addtimer(CALLBACK(src, PROC_REF(show_indicator_overlay), "eye_pulse"), animation_time, TIMER_STOPPABLE) StartCooldown(360 SECONDS, 360 SECONDS) - owner.visible_message(span_warning("[owner]'s eye glows ominously!")) + owner.visible_message(span_warning("[owner] [report_started]")) if (do_after(owner, delay = wait_delay, target = owner)) trigger_effect() else @@ -64,6 +70,8 @@ ) flick_overlay_global(flashed_overlay, show_to = GLOB.clients, duration = animation_time) stage_timer = addtimer(CALLBACK(src, PROC_REF(hide_eye)), animation_time, TIMER_STOPPABLE) + if (!stop_self) + return var/mob/living/living_owner = owner living_owner.Stun(1.5 SECONDS, ignore_canstun = TRUE) @@ -72,7 +80,7 @@ if (!viewer.flash_act(intensity = 4, affect_silicon = TRUE, visual = TRUE, length = 3 SECONDS)) return FALSE viewer.set_confusion_if_lower(12 SECONDS) - to_chat(viewer, span_warning("You are blinded by [owner]'s piercing gaze!")) + to_chat(viewer, span_warning("You are blinded by [owner]'s [blinded_source]!")) return TRUE /// Animate our effect out diff --git a/code/modules/mob/living/brain/brain_item.dm b/code/modules/mob/living/brain/brain_item.dm index 9daa88a51a3..d47894d807d 100644 --- a/code/modules/mob/living/brain/brain_item.dm +++ b/code/modules/mob/living/brain/brain_item.dm @@ -462,7 +462,6 @@ /obj/item/organ/brain/lustrous/on_mob_insert(mob/living/carbon/organ_owner, special, movement_flags) . = ..() organ_owner.gain_trauma(/datum/brain_trauma/special/bluespace_prophet, TRAUMA_RESILIENCE_ABSOLUTE) - organ_owner.AddElement(/datum/element/tenacious) /obj/item/organ/brain/felinid //A bit smaller than average brain_size = 0.8 diff --git a/code/modules/mob/living/brain/brain_saiyan.dm b/code/modules/mob/living/brain/brain_saiyan.dm new file mode 100644 index 00000000000..76d7b21b28f --- /dev/null +++ b/code/modules/mob/living/brain/brain_saiyan.dm @@ -0,0 +1,497 @@ +#define GOKU_FILTER "goku_filter" + +/// The Saiyan brain contains knowledge of powerful martial arts +/obj/item/organ/brain/saiyan + name = "saiyan brain" + desc = "The brain of a mighty saiyan warrior. Guess they don't work out at the library..." + brain_size = 0.5 + /// What buttons did we give out + var/list/granted_abilities = list() + /// Saiyans gain one of these cool karate moves at random + var/static/list/saiyan_skills = list( + /datum/action/cooldown/mob_cooldown/brimbeam/kamehameha, + /datum/action/cooldown/mob_cooldown/kaioken, + /datum/action/cooldown/mob_cooldown/super_saiyan, + /datum/action/cooldown/mob_cooldown/watcher_gaze/solar_flare, + /datum/action/cooldown/mob_cooldown/ultra_instinct, + ) + +/obj/item/organ/brain/saiyan/on_mob_insert(mob/living/carbon/organ_owner, special, movement_flags) + . = ..() + var/datum/action/cooldown/mob_cooldown/ki_blast/blast = new(organ_owner) + blast.Grant(organ_owner) + granted_abilities += blast + + var/datum/action/cooldown/mob_cooldown/saiyan_flight/flight = new(organ_owner) + flight.Grant(organ_owner) + granted_abilities += flight + + var/random_skill_path = pick(saiyan_skills) + var/datum/action/random_skill = new random_skill_path(organ_owner) + random_skill.Grant(organ_owner) + granted_abilities += random_skill + +/obj/item/organ/brain/saiyan/on_mob_remove(mob/living/carbon/organ_owner, special) + . = ..() + QDEL_LIST(granted_abilities) + +/// Shoot power from your hands, wow +/datum/action/cooldown/mob_cooldown/ki_blast + name = "Ki Blast" + desc = "Channel your ki into your hands and out into the world as rapid projectiles. Drains your fighting spirit." + button_icon = 'icons/obj/weapons/guns/projectiles.dmi' + button_icon_state = "pulse1" + background_icon_state = "bg_demon" + click_to_activate = FALSE + check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_INCAPACITATED|AB_CHECK_HANDS_BLOCKED + shared_cooldown = NONE + melee_cooldown_time = 0 SECONDS + /// Extra damage to do + var/damage_modifier = 1 + +/datum/action/cooldown/mob_cooldown/ki_blast/Activate(atom/target) + var/mob/living/mob_caster = target + if (!istype(mob_caster)) + return FALSE + var/obj/item/gun/ki_blast/ki_gun = new(mob_caster.loc) + ki_gun.projectile_damage_multiplier = damage_modifier + if (!mob_caster.put_in_hands(ki_gun, del_on_fail = TRUE)) + mob_caster.balloon_alert(mob_caster, "no free hands!") + return TRUE + +/obj/item/gun/ki_blast + name = "concentrated ki" + desc = "The power of your lifeforce converted into a deadly weapon. Fire it at someone." + fire_sound = 'sound/effects/magic/wand_teleport.ogg' + icon = 'icons/obj/weapons/guns/projectiles.dmi' + icon_state = "pulse1" + inhand_icon_state = "arcane_barrage" + base_icon_state = "arcane_barrage" + lefthand_file = 'icons/mob/inhands/weapons/guns_lefthand.dmi' + righthand_file = 'icons/mob/inhands/weapons/guns_righthand.dmi' + slot_flags = null + item_flags = NEEDS_PERMIT | DROPDEL | ABSTRACT | NOBLUDGEON + flags_1 = NONE + trigger_guard = TRIGGER_GUARD_ALLOW_ALL + +/obj/item/gun/ki_blast/Initialize(mapload) + . = ..() + AddComponent(/datum/component/automatic_fire, 0.15 SECONDS) + chambered = new /obj/item/ammo_casing/ki(src) + +/obj/item/gun/ki_blast/process_fire(atom/target, mob/living/user, message, params, zone_override, bonus_spread) + . = ..() + if (!.) + return FALSE + user.apply_damage(3, STAMINA) + return TRUE + +/obj/item/gun/ki_blast/handle_chamber(empty_chamber, from_firing, chamber_next_round) + chambered.newshot() + +/obj/item/ammo_casing/ki + slot_flags = null + projectile_type = /obj/projectile/ki + firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/blue + +/obj/projectile/ki + name = "ki blast" + icon_state = "pulse1_bl" + damage = 3 + damage_type = BRUTE + hitsound = 'sound/items/weapons/sear_disabler.ogg' + hitsound_wall = 'sound/items/weapons/sear_disabler.ogg' + light_system = OVERLAY_LIGHT + light_range = 1 + light_power = 1.4 + light_color = LIGHT_COLOR_CYAN + +/// Saiyans can fly +/datum/action/cooldown/mob_cooldown/saiyan_flight + name = "Flight" + button_icon = 'icons/mob/actions/actions_items.dmi' + button_icon_state = "flight" + background_icon_state = "bg_demon" + desc = "Focus your energy and lift into the air, or alternately stop doing that if you are doing it already." + check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_IMMOBILE|AB_CHECK_INCAPACITATED|AB_CHECK_LYING + click_to_activate = FALSE + cooldown_time = 3 SECONDS + shared_cooldown = NONE + melee_cooldown_time = 0 SECONDS + +/datum/action/cooldown/mob_cooldown/saiyan_flight/Activate(atom/target) + var/mob/living/mob_caster = target + if (!istype(mob_caster)) + return FALSE + + StartCooldown() + if(!HAS_TRAIT_FROM(mob_caster, TRAIT_MOVE_FLYING, REF(src))) + mob_caster.balloon_alert(mob_caster, "flying") + ADD_TRAIT(mob_caster, TRAIT_MOVE_FLYING, REF(src)) + passtable_on(mob_caster, REF(src)) + return TRUE + + mob_caster.balloon_alert(mob_caster, "landed") + REMOVE_TRAIT(mob_caster, TRAIT_MOVE_FLYING, REF(src)) + passtable_off(mob_caster, REF(src)) + return TRUE + + +/// Charge up a big beam +/datum/action/cooldown/mob_cooldown/brimbeam/kamehameha + name = "Kamehameha" + desc = "The signature technique of the turtle school, a devastating charged beam attack!" + button_icon = 'icons/effects/saiyan_effects.dmi' + button_icon_state = "kamehameha_start" + created_type = /obj/effect/brimbeam/kamehameha + charge_duration = 5 SECONDS + beam_duration = 12 SECONDS + cooldown_time = 90 SECONDS + shared_cooldown = NONE + melee_cooldown_time = 0 SECONDS + /// Things we still need to say, before it's too late + var/speech_timers = list() + +/datum/action/cooldown/mob_cooldown/brimbeam/kamehameha/Activate(mob/living/target) + owner.add_filter(GOKU_FILTER, 2, list("type" = "outline", "color" = COLOR_CYAN, "alpha" = 0, "size" = 1)) + var/filter = owner.get_filter(GOKU_FILTER) + animate(filter, alpha = 200, time = 0.5 SECONDS, loop = -1) + animate(alpha = 0, time = 0.5 SECONDS) + owner.say("Ka...") + var/queued_speech = list("...me...", "...ha...", "...me...") + var/speech_interval = charge_duration/4 + var/current_interval = speech_interval + while(length(queued_speech)) + var/timer = addtimer(CALLBACK(owner, TYPE_PROC_REF(/atom/movable, say), pop(queued_speech)), current_interval, TIMER_STOPPABLE | TIMER_DELETE_ME) + current_interval += speech_interval + speech_timers += timer + playsound(owner, 'sound/items/modsuit/loader_charge.ogg', 75, TRUE) + + var/mob/living/living_owner = owner + var/lightbulb = istype(living_owner) ? living_owner.mob_light(2, 1, LIGHT_COLOR_CYAN) : null + + . = ..() + + QDEL_NULL(lightbulb) + for (var/timer as anything in speech_timers) + deltimer(timer) + speech_timers = list() + animate(filter) + owner.remove_filter(GOKU_FILTER) + +/datum/action/cooldown/mob_cooldown/brimbeam/kamehameha/fire_laser() + . = ..() + if (.) + owner.say("...HA!!!!!") + +/datum/action/cooldown/mob_cooldown/brimbeam/kamehameha/on_fail() + owner.visible_message(span_notice("...and launches it straight into a wall, wasting their energy.")) + +/// It's blue now! +/obj/effect/brimbeam/kamehameha + name = "kamehameha" + light_color = LIGHT_COLOR_CYAN + icon = 'icons/effects/saiyan_effects.dmi' + icon_state = "kamehameha" + base_icon_state = "kamehameha" + +/// Blinds people +/datum/action/cooldown/mob_cooldown/watcher_gaze/solar_flare + name = "Solar Flare" + desc = "A surprising move of the Crane school, creating a blinding flash that can overpower even shaded glasses. Useful on opponents regardless of power level." + wait_delay = 1 SECONDS + report_started = "holds their hands to their forehead!" + blinded_source = "flash of light!" + stop_self = FALSE + +/datum/action/cooldown/mob_cooldown/watcher_gaze/solar_flare/trigger_effect() + . = ..() + owner.say("Solar flare!!") + +/// Makes you stronger and stacks too. But watch out! +/datum/action/cooldown/mob_cooldown/kaioken + name = "Kaio-ken Technique" + desc = "A technique taught by the powerful Kais of Otherworld, allows the user to multiply their ki at great personal risk. The effects can be stacked multiplicatively to greatly increase fighting strength, however overuse may cause immediate disintegration." + button_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "tele" + background_icon_state = "bg_demon" + cooldown_time = 3 SECONDS + shared_cooldown = NONE + melee_cooldown_time = NONE + click_to_activate = FALSE + +// This is basically handled entirely by the status effect +/datum/action/cooldown/mob_cooldown/kaioken/Activate(mob/living/target) + target.apply_status_effect(/datum/status_effect/stacking/kaioken, 1) + StartCooldown() + return TRUE + +/datum/status_effect/stacking/kaioken + id = "kaioken" + stacks = 0 + max_stacks = INFINITY // but good luck + consumed_on_threshold = FALSE + alert_type = null + status_type = STATUS_EFFECT_REFRESH // Allows us to add one stack at a time by just applying the effect + duration = 10 SECONDS + stack_decay = 0 + /// How much strength to add every time? + var/power_multiplier = 3 + /// Percentage chance to die instantly, will be multiplied by current stacks + var/death_chance = 5 + /// Light holder + var/lightbulb + /// What colour was our hair? + var/previous_hair_colour + +/datum/status_effect/stacking/kaioken/on_apply() + . = ..() + owner.add_filter(GOKU_FILTER, 2, list("type" = "outline", "color" = COLOR_SOFT_RED, "alpha" = 0, "size" = 1)) + var/filter = owner.get_filter(GOKU_FILTER) + animate(filter, alpha = 200, time = 0.5 SECONDS, loop = -1) + animate(alpha = 0, time = 0.5 SECONDS) + lightbulb = owner.mob_light(2, 1, LIGHT_COLOR_BUBBLEGUM) + + ADD_TRAIT(owner, TRAIT_POWER_HAIR, "[STATUS_EFFECT_TRAIT]_[id]") + if (ishuman(owner)) + var/mob/living/carbon/human/human_owner = owner + previous_hair_colour = human_owner.hair_color + human_owner.set_haircolor(COLOR_SOFT_RED, update = TRUE) + +/datum/status_effect/stacking/kaioken/on_remove() + QDEL_NULL(lightbulb) + var/filter = owner.get_filter(GOKU_FILTER) + animate(filter) + owner.remove_filter(GOKU_FILTER) + owner.saiyan_boost(-power_multiplier * stacks) + + REMOVE_TRAIT(owner, TRAIT_POWER_HAIR, "[STATUS_EFFECT_TRAIT]_[id]") + if (ishuman(owner)) + var/mob/living/carbon/human/human_owner = owner + human_owner.set_haircolor(previous_hair_colour, update = TRUE) + return ..() + +/datum/status_effect/stacking/kaioken/refresh(effect, stacks_to_add) + . = ..() + add_stacks(stacks_to_add) + +/datum/status_effect/stacking/kaioken/add_stacks(stacks_added) + if (stacks_added == 0) + return + . = ..() + if (stacks == 0) + return + if (prob((stacks - 1) * death_chance)) + owner.say("Kaio-AARGH!!") + owner.visible_message(span_boldwarning("[owner] vanishes in an intense flash of light!")) + owner.ghostize(can_reenter_corpse = FALSE) + owner.dust() + return + owner.saiyan_boost(power_multiplier) + if (stacks == 1) + owner.say("Kaio-ken!") + return + var/exclamations = "" + for (var/i in 1 to stacks) + exclamations += "!" + owner.say("Kaio-ken... times [convert_integer_to_words(stacks)][exclamations]") + +/// Achieve the legend +/datum/action/cooldown/mob_cooldown/super_saiyan + name = "Power Up" + desc = "Concentrate your energy, surpass your limits, and go even further beyond!" + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "sacredflame" + background_icon_state = "bg_demon" + cooldown_time = 4 MINUTES + cooldown_rounding = 0 + shared_cooldown = NONE + melee_cooldown_time = NONE + click_to_activate = FALSE + /// How long does it take to assume your next form? + var/charge_time = 30 SECONDS + /// Storage for our scream timer + var/yell_timer + +/datum/action/cooldown/mob_cooldown/super_saiyan/Activate(mob/living/target) + StartCooldown(360 SECONDS) + + target.add_filter(GOKU_FILTER, 2, list("type" = "outline", "color" = COLOR_GOLD, "alpha" = 0, "size" = 1)) + var/filter = target.get_filter(GOKU_FILTER) + animate(filter, alpha = 200, time = 0.5 SECONDS, loop = -1) + animate(alpha = 0, time = 0.5 SECONDS) + yell() + + var/lightbulb = target.mob_light(3, 1, LIGHT_COLOR_BRIGHT_YELLOW) + + owner.balloon_alert(owner, "charging...") + var/succeeded = do_after(target, delay = charge_time, target = target) + + deltimer(yell_timer) + animate(filter) + target.remove_filter(GOKU_FILTER) + QDEL_NULL(lightbulb) + + if (succeeded) + charge_time = max(6 SECONDS, charge_time - 2 SECONDS) + target.apply_status_effect(/datum/status_effect/super_saiyan) + StartCooldown() + return TRUE + + StartCooldown(10 SECONDS) + return TRUE + +/// Aaaaaaa Aaaaaaaa aaaaaa AAAAAAAa a AaAAAAAA aAAAAAAAAAAAAAAAAAAAAAAA!!!! +/datum/action/cooldown/mob_cooldown/super_saiyan/proc/yell() + owner.emote("scream") + yell_timer = addtimer(CALLBACK(src, PROC_REF(yell)), rand(1 SECONDS, 3 SECONDS), TIMER_DELETE_ME | TIMER_STOPPABLE) + +/datum/status_effect/super_saiyan + id = "super_saiyan" + alert_type = null + duration = 45 SECONDS + /// How much strength do we gain? + var/power_multiplier = 8 + /// What colour was our hair? + var/previous_hair_colour + /// Light holder + var/atom/lightbulb + +/datum/status_effect/super_saiyan/on_apply() + . = ..() + to_chat(owner, span_notice("Your power surges!")) + + new /obj/effect/temp_visual/explosion/fast(get_turf(owner)) + ADD_TRAIT(owner, TRAIT_POWER_HAIR, "[STATUS_EFFECT_TRAIT]_[id]") + + if (ishuman(owner)) + var/mob/living/carbon/human/human_owner = owner + previous_hair_colour = human_owner.hair_color + human_owner.set_haircolor(COLOR_GOLD, update = TRUE) + + owner.add_filter(GOKU_FILTER, 2, list("type" = "outline", "color" = COLOR_GOLD, "alpha" = 0, "size" = 2.5)) + var/filter = owner.get_filter(GOKU_FILTER) + animate(filter, alpha = 200, time = 0.25 SECONDS, loop = -1) + animate(alpha = 0, time = 0.25 SECONDS) + owner.saiyan_boost(multiplier = power_multiplier) + + lightbulb = owner.mob_light(5, 1, COLOR_GOLD) + + playsound(owner, 'sound/effects/magic/charge.ogg', vol = 80) + + var/list/destroy_turfs = circle_range_turfs(center = owner, radius = 2) + for (var/turf/check_turf as anything in destroy_turfs) + if (!isfloorturf(check_turf) || isindestructiblefloor(check_turf)) + continue + if (prob(75)) + continue + check_turf.break_tile() + + var/transform_area = get_area(owner) + for(var/mob/living/player as anything in GLOB.alive_player_list) + if (player == owner || !HAS_TRAIT(player, TRAIT_MARTIAL_VISION)) + continue + to_chat(player, span_warning("You sense an incredible power level coming from the direction of the [transform_area]!")) + +/datum/status_effect/super_saiyan/on_remove() + . = ..() + QDEL_NULL(lightbulb) + + REMOVE_TRAIT(owner, TRAIT_POWER_HAIR, "[STATUS_EFFECT_TRAIT]_[id]") + + var/filter = owner.get_filter(GOKU_FILTER) + animate(filter) + owner.remove_filter(GOKU_FILTER) + owner.saiyan_boost(multiplier = -power_multiplier) + + if (ishuman(owner)) + var/mob/living/carbon/human/human_owner = owner + human_owner.set_haircolor(previous_hair_colour, update = TRUE) + +/// Dodge without thinking +/datum/action/cooldown/mob_cooldown/ultra_instinct + name = "Ultra Instinct" + desc = "Clear your mind and instinctually avoid incoming blows, until you take action yourself." + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "chuuni" + background_icon_state = "bg_demon" + cooldown_time = 90 SECONDS + shared_cooldown = NONE + melee_cooldown_time = NONE + click_to_activate = FALSE + +// This is basically handled entirely by the status effect +/datum/action/cooldown/mob_cooldown/ultra_instinct/Activate(mob/living/target) + target.apply_status_effect(/datum/status_effect/ultra_instinct) + StartCooldown() + return TRUE + +/datum/status_effect/ultra_instinct + id = "ultra_instinct" + alert_type = null + duration = 90 SECONDS + /// Light holder + var/atom/lightbulb + /// What colour was our hair? + var/previous_hair_colour + +/datum/status_effect/ultra_instinct/on_apply() + . = ..() + owner.add_filter(GOKU_FILTER, 2, list("type" = "outline", "color" = COLOR_CYAN, "alpha" = 0, "size" = 2)) + var/filter = owner.get_filter(GOKU_FILTER) + animate(filter, alpha = 200, time = 0.5 SECONDS, loop = -1) + animate(alpha = 0, time = 0.5 SECONDS) + lightbulb = owner.mob_light(2, 1, LIGHT_COLOR_CYAN) + + ADD_TRAIT(owner, TRAIT_POWER_HAIR, "[STATUS_EFFECT_TRAIT]_[id]") + if (ishuman(owner)) + var/mob/living/carbon/human/human_owner = owner + previous_hair_colour = human_owner.hair_color + human_owner.set_haircolor(COLOR_CYAN, update = TRUE) + + RegisterSignals(owner, list(COMSIG_MOB_ATTACK_HAND, COMSIG_MOB_FIRED_GUN, COMSIG_LIVING_GRAB, COMSIG_MOB_ITEM_ATTACK, COMSIG_MOB_THROW), PROC_REF(took_action)) + RegisterSignal(owner, COMSIG_LIVING_CHECK_BLOCK, PROC_REF(on_hit)) + +/datum/status_effect/ultra_instinct/on_remove() + QDEL_NULL(lightbulb) + var/filter = owner.get_filter(GOKU_FILTER) + animate(filter) + owner.remove_filter(GOKU_FILTER) + + REMOVE_TRAIT(owner, TRAIT_POWER_HAIR, "[STATUS_EFFECT_TRAIT]_[id]") + if (ishuman(owner)) + var/mob/living/carbon/human/human_owner = owner + human_owner.set_haircolor(previous_hair_colour, update = TRUE) + + UnregisterSignal(owner, list( + COMSIG_LIVING_CHECK_BLOCK, + COMSIG_LIVING_GRAB, + COMSIG_MOB_ATTACK_HAND, + COMSIG_MOB_FIRED_GUN, + COMSIG_MOB_ITEM_ATTACK, + COMSIG_MOB_THROW + )) + return ..() + +/// Called when we do something +/datum/status_effect/ultra_instinct/proc/took_action() + SIGNAL_HANDLER + qdel(src) + +/// Called when something hits us +/datum/status_effect/ultra_instinct/proc/on_hit(mob/living/source) + SIGNAL_HANDLER + source.balloon_alert_to_viewers("dodged") + var/turf/current = get_turf(source) + var/list/valid_turfs = list() + for (var/turf/open/check_turf in orange(source, 1)) + if (check_turf.is_blocked_turf(exclude_mobs = FALSE, source_atom = source)) + continue + valid_turfs += check_turf + if (length(valid_turfs)) + var/turf/land_turf = pick(valid_turfs) + source.Move(land_turf, get_dir(current, land_turf)) + new /obj/effect/temp_visual/jet_plume(current) + return SUCCESSFUL_BLOCK + +#undef GOKU_FILTER diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index 533da926326..a0d66859ced 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -1469,6 +1469,40 @@ return FALSE return unwagged.stop_wag(src) +/mob/living/carbon/calculate_power_level() + var/min_damage = 0 + var/max_damage = 0 + var/part_count = 0 + for (var/obj/item/bodypart/part as anything in bodyparts) + if (part.unarmed_damage_high <= 0) + continue + min_damage += part.unarmed_damage_low + max_damage += part.unarmed_damage_high + part_count++ + + var/damage = ((min_damage / part_count) + (max_damage / part_count)) / 2 + return maxHealth * max(damage, 0.1) + +#define DAMAGE_BOOST 2 +#define DEFENCE_BOOST 0.05 + +/mob/living/carbon/saiyan_boost(multiplier) + . = ..() + var/added_damage = DAMAGE_BOOST * multiplier + var/added_defence = DEFENCE_BOOST * multiplier + + for (var/obj/item/bodypart/part as anything in bodyparts) + if (!HAS_TRAIT(part, TRAIT_SAIYAN_STRENGTH)) + continue + part.unarmed_damage_high += added_damage + part.unarmed_damage_low += added_damage + part.unarmed_effectiveness += added_damage // This is maybe stronger than increasing the damage tbqh + part.brute_modifier = max(0, part.brute_modifier - added_defence) + part.burn_modifier = max(0, part.burn_modifier - added_defence) + +#undef DAMAGE_BOOST +#undef DEFENCE_BOOST + /mob/living/carbon/itch(obj/item/bodypart/target_part = null, damage = 0.5, can_scratch = TRUE, silent = FALSE) if (isnull(target_part)) target_part = get_bodypart(get_random_valid_zone(even_weights = TRUE)) diff --git a/code/modules/mob/living/carbon/examine.dm b/code/modules/mob/living/carbon/examine.dm index faff5411a7c..d33d65ac6ef 100644 --- a/code/modules/mob/living/carbon/examine.dm +++ b/code/modules/mob/living/carbon/examine.dm @@ -298,6 +298,9 @@ ADD_NEWLINE_IF_NECESSARY(.) . += "Quirks: [get_quirk_string(FALSE, CAT_QUIRK_ALL)]" + if(HAS_TRAIT(user, TRAIT_MARTIAL_VISION)) + . += "[t_His] power level is [report_power_level()]." + SEND_SIGNAL(src, COMSIG_ATOM_EXAMINE, user, .) if(length(.)) .[1] = "" + .[1] diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index ac34dbf994b..b8296f2cffa 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -1179,6 +1179,9 @@ /mob/living/carbon/human/species/pod race = /datum/species/pod +/mob/living/carbon/human/species/saiyan + race = /datum/species/saiyan + /mob/living/carbon/human/species/shadow race = /datum/species/shadow diff --git a/code/modules/mob/living/carbon/human/species_types/saiyan.dm b/code/modules/mob/living/carbon/human/species_types/saiyan.dm new file mode 100644 index 00000000000..1d81758fafa --- /dev/null +++ b/code/modules/mob/living/carbon/human/species_types/saiyan.dm @@ -0,0 +1,190 @@ +#define SAIYAN_TAIL_MOOD "saiyan_humiliated" + +/datum/species/saiyan + name = "\improper Saiyan" + id = SPECIES_SAIYAN + mutanteyes = /obj/item/organ/eyes/saiyan + mutantbrain = /obj/item/organ/brain/saiyan + mutantheart = /obj/item/organ/heart/saiyan + mutantstomach = /obj/item/organ/stomach/saiyan + payday_modifier = 2.0 + inherent_traits = list( + TRAIT_CATLIKE_GRACE, + TRAIT_CHUNKYFINGERS, + TRAIT_USES_SKINTONES, + ) + changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_PRIDE | MIRROR_MAGIC | RACE_SWAP | ERT_SPAWN | SLIME_EXTRACT + + species_language_holder = /datum/language_holder/human_basic + + bodypart_overrides = list( + BODY_ZONE_HEAD = /obj/item/bodypart/head/saiyan, + BODY_ZONE_CHEST = /obj/item/bodypart/chest/saiyan, + BODY_ZONE_L_ARM = /obj/item/bodypart/arm/left/saiyan, + BODY_ZONE_R_ARM = /obj/item/bodypart/arm/right/saiyan, + BODY_ZONE_L_LEG = /obj/item/bodypart/leg/left/saiyan, + BODY_ZONE_R_LEG = /obj/item/bodypart/leg/right/saiyan, + ) + mutant_organs = list( + /obj/item/organ/tail/monkey/saiyan = "Saiyan", + ) + +/datum/species/saiyan/prepare_human_for_preview(mob/living/carbon/human/human) + human.set_haircolor("#292929", update = FALSE) + human.set_hairstyle("Spiky 2", update = TRUE) + +/datum/species/saiyan/check_roundstart_eligible() + return TRUE + +/datum/species/saiyan/get_scream_sound(mob/living/carbon/human/human) + if(human.physique == MALE) + if(prob(1)) + return 'sound/mobs/humanoids/human/scream/wilhelm_scream.ogg' + return pick( + 'sound/mobs/humanoids/human/scream/malescream_1.ogg', + 'sound/mobs/humanoids/human/scream/malescream_2.ogg', + 'sound/mobs/humanoids/human/scream/malescream_3.ogg', + 'sound/mobs/humanoids/human/scream/malescream_4.ogg', + 'sound/mobs/humanoids/human/scream/malescream_5.ogg', + 'sound/mobs/humanoids/human/scream/malescream_6.ogg', + ) + + return pick( + 'sound/mobs/humanoids/human/scream/femalescream_1.ogg', + 'sound/mobs/humanoids/human/scream/femalescream_2.ogg', + 'sound/mobs/humanoids/human/scream/femalescream_3.ogg', + 'sound/mobs/humanoids/human/scream/femalescream_4.ogg', + 'sound/mobs/humanoids/human/scream/femalescream_5.ogg', + ) + +/datum/species/saiyan/on_species_gain(mob/living/carbon/human/human_who_gained_species, datum/species/old_species, pref_load, regenerate_icons) + . = ..() + RegisterSignal(human_who_gained_species, COMSIG_SAIYAN_SURVIVOR, PROC_REF(on_survived_boost)) + RegisterSignal(human_who_gained_species, COMSIG_CARBON_GAIN_ORGAN, PROC_REF(on_tail_gained)) + RegisterSignal(human_who_gained_species, COMSIG_CARBON_LOSE_ORGAN, PROC_REF(on_tail_removed)) + RegisterSignal(human_who_gained_species, COMSIG_ATOM_AFTER_ATTACKEDBY, PROC_REF(check_tail_sever)) + +/datum/species/saiyan/on_species_loss(mob/living/carbon/human/C, datum/species/new_species, pref_load) + . = ..() + UnregisterSignal(C, list( + COMSIG_ATOM_AFTER_ATTACKEDBY, + COMSIG_CARBON_GAIN_ORGAN, + COMSIG_CARBON_LOSE_ORGAN, + COMSIG_SAIYAN_SURVIVOR, + )) + +/// If you take sharp damage someone might sever your tail +/datum/species/saiyan/proc/check_tail_sever(mob/living/carbon/target, obj/item/weapon, mob/attacker, proximity_flag, click_parameters) + SIGNAL_HANDLER + if (!proximity_flag || weapon.force < 5 || weapon.get_sharpness() != SHARP_EDGED) + return + if (attacker.zone_selected != BODY_ZONE_PRECISE_GROIN && attacker.zone_selected != BODY_ZONE_CHEST) + return + var/obj/item/organ/tail/saiyan_tail = target.get_organ_slot(ORGAN_SLOT_EXTERNAL_TAIL) + if (isnull(saiyan_tail) || !prob(3)) + return + target.visible_message(span_warning("[target]'s tail falls to the ground, severed completely!")) + INVOKE_ASYNC(target, TYPE_PROC_REF(/mob, emote), "scream") + saiyan_tail.Remove(target) + saiyan_tail.forceMove(target.loc) + +/// Called when we survive near-death +/datum/species/saiyan/proc/on_survived_boost(mob/living/saiyan) + SIGNAL_HANDLER + to_chat(saiyan, span_notice("Your near-death experience grants you more strength!")) + saiyan.saiyan_boost() + +/// When your tail is cut you get weaker +/datum/species/saiyan/proc/on_tail_gained(mob/living/vegeta, obj/item/organ/tail) + SIGNAL_HANDLER + if (!istype(tail, /obj/item/organ/tail/monkey/saiyan)) + return + if (!vegeta.mob_mood.has_mood_of_category(SAIYAN_TAIL_MOOD)) + return + to_chat(vegeta, span_notice("As your tail returns, your strength returns too.")) + vegeta.saiyan_boost(multiplier = 5) + vegeta.clear_mood_event(SAIYAN_TAIL_MOOD) + +/// If your tail is restored you return to original strength +/datum/species/saiyan/proc/on_tail_removed(mob/living/vegeta, obj/item/organ/tail) + SIGNAL_HANDLER + if (!istype(tail, /obj/item/organ/tail/monkey/saiyan)) + return + to_chat(vegeta, span_boldwarning("No! Your tail!!")) + vegeta.saiyan_boost(multiplier = -5) + vegeta.add_mood_event(SAIYAN_TAIL_MOOD, /datum/mood_event/saiyan_humiliated) + vegeta.Paralyze(10 SECONDS) + vegeta.adjust_confusion(1 MINUTES) + +/datum/species/saiyan/get_physical_attributes() + return "While they appear superficially similar to humans, Saiyans are universally specimens of toned and perfect health with \ + the honed physique of warriors. They can be distinguished from inferior Human stock by their simian tails, and expressive haircuts." + +/datum/species/saiyan/get_species_description() + return "Martially-inclined space warriors who live for battle and carnage. Have a tendency to lose it when exposed to moonlight." + +/datum/species/saiyan/get_species_lore() + return list( + "Saiyans were once native to the planet Vegeta, which they shared with another species that they annihilated utterly. \ + Saiyans are natural warriors with an instinctive understanding of martial arts and love of violence, \ + their predominant reputation in the galaxy is as conquerors who clear planets of life before selling them to the highest bidder.", + ) + +/datum/species/saiyan/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "fist-raised", + SPECIES_PERK_NAME = "Strong", + SPECIES_PERK_DESC = "Saiyans build muscle quickly and easily, and have a natural understanding of fighting unarmed.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "bolt", + SPECIES_PERK_NAME = "Ki Mastery", + SPECIES_PERK_DESC = "Mastery of martial arts grants Saiyans many useful abilities such as the ability to fire Ki Blasts, and flight.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "first-aid", + SPECIES_PERK_NAME = "Fighting Spirit", + SPECIES_PERK_DESC = "A Saiyan who recovers from grievous injury (but not death) often becomes more powerful.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "moon", + SPECIES_PERK_NAME = "Going Ape", + SPECIES_PERK_DESC = "Saiyans uncontrollably revert into the form of powerful giant apes when exposed to moonlight.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "warning", + SPECIES_PERK_NAME = "Achilles' Tail", + SPECIES_PERK_DESC = "Saiyans are significantly weakened if their tail is harmed or removed.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "bowl-rice", + SPECIES_PERK_NAME = "Warrior's Appetite", + SPECIES_PERK_DESC = "Maintaining fighting fitness sure makes you awfully hungry.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "paw", + SPECIES_PERK_NAME = "Melee Fixation", + SPECIES_PERK_DESC = "Saiyans mostly disavow the use of projectile weaponry on the grounds of honour, although some say it's simply that their big hands mean that they're not very good at using it.", + ), + ) + + return to_add + +/datum/movespeed_modifier/saiyan_speed + variable = TRUE + +/datum/mood_event/saiyan_humiliated + description = "Someone removed your Saiyan birthright... such an insult must not be tolerated." + mood_change = -4 + +#undef SAIYAN_TAIL_MOOD diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index fa32d5eff6f..585925fc3e5 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -2971,6 +2971,40 @@ GLOBAL_LIST_EMPTY(fire_appearances) counted_money += counted_credit return round(physical_cash_total) +/// Returns the approximate power level of this creature +/mob/living/proc/calculate_power_level() + return maxHealth * max((melee_damage_lower + melee_damage_upper) / 2, 0.1) + +/// Formats the above as a string +/mob/living/proc/report_power_level() + var/power_level = floor(calculate_power_level()) + if (power_level >= 9000) + return span_boldwarning("[p_Their()] power level is... What?! [power_level]?!?!") + return span_notice("[p_Their()] power level is [power_level].") + +#define DAMAGE_BOOST 2 +#define DAMAGE_MULT 0.1 +#define SPEED_BOOST 0.1 +#define HEALTH_BOOST 5 + +/// Generally get stronger, as a saiyan would (or weaker if we pass a negative multiplier) +/mob/living/proc/saiyan_boost(multiplier = 1) + maxHealth += HEALTH_BOOST * multiplier // Fuck knows if this actually does anything + var/datum/action/cooldown/mob_cooldown/ki_blast/blast = locate() in actions + if (!isnull(blast)) + blast.damage_modifier += DAMAGE_MULT * multiplier + boost_movespeed -= SPEED_BOOST * multiplier + add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/saiyan_speed, TRUE, boost_movespeed) + + var/added_damage = DAMAGE_BOOST * multiplier + melee_damage_lower += added_damage + melee_damage_upper += added_damage + +#undef DAMAGE_BOOST +#undef DAMAGE_MULT +#undef SPEED_BOOST +#undef HEALTH_BOOST + /// Returns an arbitrary number which very roughly correlates with how buff you look /mob/living/proc/calculate_fitness() var/athletics_level = mind?.get_skill_level(/datum/skill/athletics) || 1 diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index 53d8b1fae9c..31b28a4b1a3 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -233,5 +233,8 @@ /// What our current gravity state is. Used to avoid duplicate animates and such var/gravity_state = null + /// How much fasterer are we as a result of martial arts superpowers? + var/boost_movespeed = 0 + /// How long it takes to return to 0 stam var/stamina_regen_time = 10 SECONDS diff --git a/code/modules/spells/spell_types/shapeshift/_shape_status.dm b/code/modules/spells/spell_types/shapeshift/_shape_status.dm index a42cd97542c..ff5f6a55024 100644 --- a/code/modules/spells/spell_types/shapeshift/_shape_status.dm +++ b/code/modules/spells/spell_types/shapeshift/_shape_status.dm @@ -30,6 +30,7 @@ return ..() /datum/status_effect/shapechange_mob/on_apply() + ADD_TRAIT(caster_mob, TRAIT_SHAPESHIFTED, REF(src)) caster_mob.mind?.transfer_to(owner) caster_mob.forceMove(owner) ADD_TRAIT(caster_mob, TRAIT_NO_TRANSFORM, REF(src)) @@ -87,6 +88,7 @@ UnregisterSignal(caster_mob, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH)) REMOVE_TRAIT(caster_mob, TRAIT_NO_TRANSFORM, REF(src)) + REMOVE_TRAIT(caster_mob, TRAIT_SHAPESHIFTED, REF(src)) caster_mob.remove_status_effect(/datum/status_effect/grouped/stasis, STASIS_SHAPECHANGE_EFFECT) var/atom/former_loc = owner.loc diff --git a/code/modules/surgery/bodyparts/species_parts/saiyan_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/saiyan_bodyparts.dm new file mode 100644 index 00000000000..56186d46788 --- /dev/null +++ b/code/modules/surgery/bodyparts/species_parts/saiyan_bodyparts.dm @@ -0,0 +1,68 @@ +/obj/item/bodypart/head/saiyan + unarmed_damage_low = 5 + unarmed_damage_high = 7 + unarmed_effectiveness = 15 + /// All TRUE saiyans have this colour hair + var/saiyan_hair_colour = "#292929" + +/obj/item/bodypart/head/saiyan/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_SAIYAN_STRENGTH, INNATE_TRAIT) + +/obj/item/bodypart/head/saiyan/update_hair_and_lips(dropping_limb, is_creating) + . = ..() + + if(!owner) + return + + if (HAS_TRAIT(owner, TRAIT_POWER_HAIR)) + return + // Sorry, you are not legendary enough to dye your hair + var/mob/living/carbon/human/human_head_owner = owner + human_head_owner?.hair_color = saiyan_hair_colour + hair_color = saiyan_hair_colour + +/obj/item/bodypart/chest/saiyan + unarmed_damage_low = 5 // Good luck actually dealing damage with your chest + unarmed_damage_high = 7 + unarmed_effectiveness = 15 + +/obj/item/bodypart/chest/saiyan/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_SAIYAN_STRENGTH, INNATE_TRAIT) + +/obj/item/bodypart/arm/left/saiyan + unarmed_damage_low = 8 + unarmed_damage_high = 12 + unarmed_effectiveness = 15 + +/obj/item/bodypart/arm/left/saiyan/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_SAIYAN_STRENGTH, INNATE_TRAIT) + +/obj/item/bodypart/arm/right/saiyan + unarmed_damage_low = 8 + unarmed_damage_high = 12 + unarmed_effectiveness = 15 + +/obj/item/bodypart/arm/right/saiyan/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_SAIYAN_STRENGTH, INNATE_TRAIT) + +/obj/item/bodypart/leg/left/saiyan + unarmed_damage_low = 12 + unarmed_damage_high = 18 + unarmed_effectiveness = 15 + +/obj/item/bodypart/leg/left/saiyan/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_SAIYAN_STRENGTH, INNATE_TRAIT) + +/obj/item/bodypart/leg/right/saiyan + unarmed_damage_low = 12 + unarmed_damage_high = 18 + unarmed_effectiveness = 15 + +/obj/item/bodypart/leg/right/saiyan/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_SAIYAN_STRENGTH, INNATE_TRAIT) diff --git a/code/modules/surgery/organs/external/tails.dm b/code/modules/surgery/organs/external/tails.dm index e6c382387a2..2b053abc07c 100644 --- a/code/modules/surgery/organs/external/tails.dm +++ b/code/modules/surgery/organs/external/tails.dm @@ -187,6 +187,85 @@ /datum/bodypart_overlay/mutant/tail/monkey/get_global_feature_list() return SSaccessories.tails_list_monkey +/obj/item/organ/tail/monkey/saiyan + name = "saiyan tail" + desc = "The severed tail of a mighty Saiyan warrior, the ultimate humiliation." + bodypart_overlay = /datum/bodypart_overlay/mutant/tail/monkey/saiyan + +/obj/item/organ/tail/monkey/saiyan/on_mob_insert(mob/living/carbon/organ_owner, special, movement_flags) + . = ..() + var/static/list/loc_connections = list( + COMSIG_MOVABLE_MOVED = PROC_REF(on_moved), + ) + AddComponent(/datum/component/connect_containers, src, loc_connections) + +/obj/item/organ/tail/monkey/saiyan/on_mob_remove(mob/living/carbon/organ_owner, special) + . = ..() + qdel(GetComponent(/datum/component/connect_containers)) + +/obj/item/organ/tail/monkey/saiyan/get_butt_sprite() + return BUTT_SPRITE_CAT // how don't we have a monkey one... + +/// When we move check if we are exposed to space +/obj/item/organ/tail/monkey/saiyan/proc/on_moved() + SIGNAL_HANDLER + if (isnull(owner)) + return + if (is_space_exposed_turf(get_turf(src))) + go_ape(owner) + else + escape_ape(owner) + +/// Check if the passed turf can see space +/obj/item/organ/tail/monkey/saiyan/proc/is_space_exposed_turf(turf/turf_to_check) + if (isnull(turf_to_check)) + return FALSE + var/area/area_to_check = get_area(turf_to_check) + if (isspaceturf(turf_to_check) || area_to_check.outdoors) + return TRUE + var/turf/turf_above = GET_TURF_ABOVE(turf_to_check) + if (!isnull(turf_above) && istransparentturf(turf_above)) + return is_space_exposed_turf(turf_above) + if (!istransparentturf(turf_to_check)) + return FALSE + while (!isnull(turf_to_check) && istransparentturf(turf_to_check)) + turf_to_check = GET_TURF_BELOW(turf_to_check) + return isnull(turf_to_check) || isspaceturf(turf_to_check) + +/// Start being an ape +/obj/item/organ/tail/monkey/saiyan/proc/go_ape() + if (HAS_TRAIT(owner, TRAIT_SHAPESHIFTED) || owner.stat == DEAD) + return + owner.visible_message(span_warning("[owner] transforms into a huge, ape-like creature!")) + var/mob/living/basic/gorilla/saiyan/monkie = new(owner.loc) + monkie.dir = owner.dir + monkie.faction = owner.faction.Copy() + monkie.name = owner.real_name + monkie.real_name = owner.real_name + monkie.apply_status_effect(/datum/status_effect/shapechange_mob, owner) + monkie.saiyan_boost(owner.boost_movespeed / -0.1) + RegisterSignal(monkie, COMSIG_LIVING_DEATH, PROC_REF(ape_died)) + +/obj/item/organ/tail/monkey/saiyan/proc/ape_died() + SIGNAL_HANDLER + owner.death() + +/// Stop being an ape +/obj/item/organ/tail/monkey/saiyan/proc/escape_ape() + if (!HAS_TRAIT(owner, TRAIT_SHAPESHIFTED)) + return + var/mob/living/ape_form = owner.loc + if (!istype(ape_form)) + return // Uh oh + owner.visible_message(span_warning("[owner] returns to [owner.p_their()] normal form.")) + ape_form.remove_status_effect(/datum/status_effect/shapechange_mob) + +/datum/bodypart_overlay/mutant/tail/monkey/saiyan + feature_key = "tail_saiyan" + +/datum/bodypart_overlay/mutant/tail/monkey/saiyan/get_global_feature_list() + return SSaccessories.tails_list_saiyan + /obj/item/organ/tail/lizard name = "lizard tail" desc = "A severed lizard tail. Somewhere, no doubt, a lizard hater is very pleased with themselves." diff --git a/code/modules/surgery/organs/internal/eyes/_eyes.dm b/code/modules/surgery/organs/internal/eyes/_eyes.dm index daf0b0b060c..c530e2f91a4 100644 --- a/code/modules/surgery/organs/internal/eyes/_eyes.dm +++ b/code/modules/surgery/organs/internal/eyes/_eyes.dm @@ -820,6 +820,18 @@ eye_icon_state = "motheyes" icon_state = "eyeballs-cybermoth" +/obj/item/organ/eyes/saiyan + name = "saiyan eyes" + desc = "The incisive eyes of a warrior. Special cells allow the detection of power levels." + +/obj/item/organ/eyes/saiyan/on_mob_insert(mob/living/carbon/organ_owner, special, movement_flags) + . = ..() + ADD_TRAIT(organ_owner, TRAIT_MARTIAL_VISION, ORGAN_TRAIT) + +/obj/item/organ/eyes/saiyan/on_mob_remove(mob/living/carbon/organ_owner, special) + . = ..() + REMOVE_TRAIT(organ_owner, TRAIT_MARTIAL_VISION, ORGAN_TRAIT) + /obj/item/organ/eyes/snail name = "snail eyes" desc = "These eyes seem to have a large range, but might be cumbersome with glasses." diff --git a/code/modules/surgery/organs/internal/heart/heart_saiyan.dm b/code/modules/surgery/organs/internal/heart/heart_saiyan.dm new file mode 100644 index 00000000000..3b30a24b2f1 --- /dev/null +++ b/code/modules/surgery/organs/internal/heart/heart_saiyan.dm @@ -0,0 +1,48 @@ +/// A warrior's heart which gains experience from fighting (and losing) +/obj/item/organ/heart/saiyan + maxHealth = STANDARD_ORGAN_THRESHOLD*0.5 // Vulnerable to heart disease + +/obj/item/organ/heart/saiyan/on_mob_insert(mob/living/carbon/organ_owner, special, movement_flags) + . = ..() + RegisterSignal(organ_owner, COMSIG_MOB_STATCHANGE, PROC_REF(on_stat_changed)) + +/obj/item/organ/heart/saiyan/on_mob_remove(mob/living/carbon/organ_owner, special) + . = ..() + UnregisterSignal(organ_owner, COMSIG_MOB_STATCHANGE) + +/// When we enter crit, prepare for a zenkai boost +/obj/item/organ/heart/saiyan/proc/on_stat_changed(mob/living/source, new_stat) + SIGNAL_HANDLER + if (new_stat != HARD_CRIT) + return + source.apply_status_effect(/datum/status_effect/saiyan_survivor_tracker) + +/// Removes itself if you die, buffs your saiyan limbs if you do not +/datum/status_effect/saiyan_survivor_tracker + id = "saiyan_survivor_tracker" + alert_type = null + +/datum/status_effect/saiyan_survivor_tracker/on_apply() + . = ..() + if (!.) + return FALSE + + RegisterSignal(owner, COMSIG_MOB_STATCHANGE, PROC_REF(on_stat_changed)) + RegisterSignal(owner, COMSIG_LIVING_DEATH, PROC_REF(on_died)) + return TRUE + +/datum/status_effect/saiyan_survivor_tracker/on_remove() + . = ..() + UnregisterSignal(owner, list(COMSIG_MOB_STATCHANGE, COMSIG_LIVING_DEATH)) + +/// Upgrade all of your limb stats if you recovered, wow +/datum/status_effect/saiyan_survivor_tracker/proc/on_stat_changed(mob/living/source, new_stat) + SIGNAL_HANDLER + if (new_stat != CONSCIOUS || !iscarbon(source)) + return + SEND_SIGNAL(source, COMSIG_SAIYAN_SURVIVOR) + qdel(src) + +/datum/status_effect/saiyan_survivor_tracker/proc/on_died(mob/living/source) + SIGNAL_HANDLER + qdel(src) diff --git a/code/modules/surgery/organs/internal/stomach/_stomach.dm b/code/modules/surgery/organs/internal/stomach/_stomach.dm index 40d3265684d..ab7d50b5c8b 100644 --- a/code/modules/surgery/organs/internal/stomach/_stomach.dm +++ b/code/modules/surgery/organs/internal/stomach/_stomach.dm @@ -325,4 +325,9 @@ . = ..() AddElement(/datum/element/dangerous_organ_removal, /*surgical = */ TRUE) +/obj/item/organ/stomach/saiyan + disgust_metabolism = 2 + hunger_modifier = 3 + desc = "The Saiyan stomach can handle a wide range of foods, but burns it fast to power their energetic lifestyle." + #undef STOMACH_METABOLISM_CONSTANT diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_saiyan.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_saiyan.png new file mode 100644 index 00000000000..73570bb23b6 Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_saiyan.png differ diff --git a/icons/effects/saiyan_effects.dmi b/icons/effects/saiyan_effects.dmi new file mode 100644 index 00000000000..e8f2a81f5f4 Binary files /dev/null and b/icons/effects/saiyan_effects.dmi differ diff --git a/icons/mob/human/species/monkey/monkey_tail.dmi b/icons/mob/human/species/monkey/monkey_tail.dmi index ffebf714f9a..5d21803de42 100644 Binary files a/icons/mob/human/species/monkey/monkey_tail.dmi and b/icons/mob/human/species/monkey/monkey_tail.dmi differ diff --git a/icons/mob/simple/gorilla.dmi b/icons/mob/simple/gorilla.dmi index 77d03fa606d..aa43c01fb5d 100644 Binary files a/icons/mob/simple/gorilla.dmi and b/icons/mob/simple/gorilla.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 4f1e579be74..5c6348b8252 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -5182,6 +5182,7 @@ #include "code\modules\mob\living\brain\brain.dm" #include "code\modules\mob\living\brain\brain_cybernetic.dm" #include "code\modules\mob\living\brain\brain_item.dm" +#include "code\modules\mob\living\brain\brain_saiyan.dm" #include "code\modules\mob\living\brain\brain_say.dm" #include "code\modules\mob\living\brain\death.dm" #include "code\modules\mob\living\brain\emote.dm" @@ -5276,6 +5277,7 @@ #include "code\modules\mob\living\carbon\human\species_types\mushpeople.dm" #include "code\modules\mob\living\carbon\human\species_types\plasmamen.dm" #include "code\modules\mob\living\carbon\human\species_types\podpeople.dm" +#include "code\modules\mob\living\carbon\human\species_types\saiyan.dm" #include "code\modules\mob\living\carbon\human\species_types\shadowpeople.dm" #include "code\modules\mob\living\carbon\human\species_types\skeletons.dm" #include "code\modules\mob\living\carbon\human\species_types\snail.dm" @@ -6145,6 +6147,7 @@ #include "code\modules\surgery\bodyparts\species_parts\misc_bodyparts.dm" #include "code\modules\surgery\bodyparts\species_parts\moth_bodyparts.dm" #include "code\modules\surgery\bodyparts\species_parts\plasmaman_bodyparts.dm" +#include "code\modules\surgery\bodyparts\species_parts\saiyan_bodyparts.dm" #include "code\modules\surgery\organs\_organ.dm" #include "code\modules\surgery\organs\autosurgeon.dm" #include "code\modules\surgery\organs\helpers.dm" @@ -6167,6 +6170,7 @@ #include "code\modules\surgery\organs\internal\heart\_heart.dm" #include "code\modules\surgery\organs\internal\heart\heart_anomalock.dm" #include "code\modules\surgery\organs\internal\heart\heart_ethereal.dm" +#include "code\modules\surgery\organs\internal\heart\heart_saiyan.dm" #include "code\modules\surgery\organs\internal\liver\_liver.dm" #include "code\modules\surgery\organs\internal\liver\liver_golem.dm" #include "code\modules\surgery\organs\internal\liver\liver_plasmaman.dm"