diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index 623165024ee..3c92c3c4cdb 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -1463,4 +1463,10 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai ///Trait given to heretic summons, making them immune to heretic spells #define TRAIT_HERETIC_SUMMON "heretic_summon" +///trait given to mobs that are difficult to tame through mounting +#define TRAIT_MOB_DIFFICULT_TO_MOUNT "difficult_to_mount" + +///trait given to mobs that are easy to tame through mounting +#define TRAIT_MOB_EASY_TO_MOUNT "easy_to_mount" + // END TRAIT DEFINES diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index c28b2dce752..84ebeb2d298 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -357,6 +357,8 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_FAKE_SOULLESS" = TRAIT_FAKE_SOULLESS, "TRAIT_MOB_BREEDER" = TRAIT_MOB_BREEDER, "TRAIT_MOB_CAN_DIG" = TRAIT_MOB_CAN_DIG, + "TRAIT_MOB_DIFFICULT_TO_MOUNT" = TRAIT_MOB_DIFFICULT_TO_MOUNT, + "TRAIT_MOB_EASY_TO_MOUNT" = TRAIT_MOB_EASY_TO_MOUNT, "TRAIT_MOB_EATER" = TRAIT_MOB_EATER, "TRAIT_MOB_ELEVATED" = TRAIT_MOB_ELEVATED, "TRAIT_MOB_HATCHED" = TRAIT_MOB_HATCHED, diff --git a/code/datums/components/riding/riding_minigame.dm b/code/datums/components/riding/riding_minigame.dm new file mode 100644 index 00000000000..4aa2311d352 --- /dev/null +++ b/code/datums/components/riding/riding_minigame.dm @@ -0,0 +1,264 @@ +#define STARTING_ARROW_POSITION -50 +#define ENDING_ARROW_POSITION 20 +#define VERTICAL_ARROW_HEIGHT 13 +#define HORIZONTAL_ARROW_HEIGHT 9 +#define FADE_AWAY_TIME 0.1 SECONDS +#define PING_GRACE 0.25 SECONDS + +//a simple minigame players must win to mount and tame a mob +/datum/riding_minigame + ///our host mob + var/datum/weakref/host + ///our current rider + var/datum/weakref/mounter + ///the total amount of tries the rider gets + var/maximum_attempts = 25 + ///maximum number of failures before we fail + var/current_attempts = 0 + ///required number of successes + var/required_successes = 11 + ///what failures are we on + var/current_failures = 0 + ///we win these + var/current_succeeded = 0 + ///holder of our heart counter + var/image/heart_counter + ///list of our hearts + var/list/hearts_list = list() + ///holder of our minigame! + var/image/minigame_holder + ///cached directional icons of our host + var/list/cached_arrows = list() + ///speed of our arrow + var/arrow_speed = 45 + ///time to linger + var/linger_time = 0.3 SECONDS + ///multiplier for easy settings + var/easy_difficulty_multiplier = 1.1 + ///multiplier for hard settings + var/hard_difficulty_multiplier = 0.9 + ///cooldown when we fail + COOLDOWN_DECLARE(failure_cooldown) + +/datum/riding_minigame/New(mob/living/ridden, mob/living/rider) + . = ..() + host = WEAKREF(ridden) + mounter = WEAKREF(rider) + set_difficulty(ridden, rider) + RegisterSignal(rider, COMSIG_MOB_UNBUCKLED, PROC_REF(lose_minigame)) + RegisterSignal(ridden, COMSIG_MOVABLE_ATTEMPTED_MOVE, PROC_REF(on_ridden_moved)) + minigame_holder = image(icon='icons/effects/effects.dmi', loc=rider,icon_state="nothing", layer = 0, pixel_x = 32, pixel_y = 0) + heart_counter = image(icon='icons/effects/effects.dmi', loc=rider,icon_state="nothing", layer = 0, pixel_x = 0, pixel_y = -32) + SET_PLANE_EXPLICIT(minigame_holder, ABOVE_HUD_PLANE, rider) + SET_PLANE_EXPLICIT(heart_counter, ABOVE_HUD_PLANE, rider) + generate_heart_counter() + generate_visuals() + rider.client.images |= list(minigame_holder, heart_counter) + START_PROCESSING(SSprocessing, src) + +/datum/riding_minigame/proc/set_difficulty(mob/living/ridden, mob/living/rider) + if(HAS_TRAIT(rider, TRAIT_BEAST_EMPATHY) || HAS_TRAIT(ridden, TRAIT_MOB_EASY_TO_MOUNT)) + linger_time *= easy_difficulty_multiplier + if(HAS_TRAIT(ridden, TRAIT_MOB_DIFFICULT_TO_MOUNT)) + linger_time *= hard_difficulty_multiplier + +/datum/riding_minigame/proc/generate_visuals() + var/static/list/void_arrow_order = list( + "north", + "west", + "south", + "east", + ) + var/x_offset = 0 + for(var/direction in void_arrow_order) + var/obj/effect/overlay/vis/ride_minigame/new_arrow = new + new_arrow.icon = 'icons/effects/riding_minigame.dmi' + new_arrow.icon_state = "blank_arrow" + new_arrow.setDir(text2dir(direction)) + new_arrow.pixel_x = x_offset + new_arrow.pixel_y = ENDING_ARROW_POSITION + new_arrow.layer = ABOVE_ALL_MOB_LAYER + minigame_holder.vis_contents += new_arrow + cached_arrows[direction] = list("visual_object" = new_arrow, "is_active" = null) + x_offset += 16 + +/datum/riding_minigame/proc/generate_heart_counter() + var/x_offset = -32 + for(var/i in 1 to required_successes) + var/obj/effect/overlay/vis/ride_minigame/heart = new + heart.icon = 'icons/effects/effects.dmi' + heart.icon_state = "empty_heart" + heart.pixel_x = x_offset + x_offset += 8 + hearts_list += heart + heart_counter.vis_contents += heart + +/datum/riding_minigame/process() + if(current_attempts >= maximum_attempts) + lose_minigame() + return + if(prob(30)) //we shake and move uncontrollably! + var/mob/living/living_host = host.resolve() + living_host.Shake(pixelshiftx = 1, pixelshifty = 0, duration = 0.75 SECONDS) + living_host.spin(spintime = 0.75 SECONDS, speed = 1) + + generate_arrow() + +/datum/riding_minigame/proc/generate_arrow() + if(current_attempts >= maximum_attempts) + return + var/static/list/possible_arrows = list( + "north" = 0, + "west" = 16, + "south" = 32, + "east" = 48, + ) + + current_attempts++ + var/picked_arrow = pick(possible_arrows) + var/obj/effect/overlay/vis/ride_minigame/new_arrow = new + new_arrow.icon = 'icons/effects/riding_minigame.dmi' + new_arrow.icon_state = "[picked_arrow]_arrow" + new_arrow.alpha = 0 + new_arrow.layer = ABOVE_ALL_MOB_LAYER + 0.1 + new_arrow.pixel_x = possible_arrows[picked_arrow] + new_arrow.pixel_y = STARTING_ARROW_POSITION + minigame_holder.vis_contents += new_arrow + + animate(new_arrow, alpha = 255, time = 0.15 SECONDS) + animate(new_arrow, pixel_y = ENDING_ARROW_POSITION, time = ((ENDING_ARROW_POSITION - STARTING_ARROW_POSITION) / arrow_speed) SECONDS) + + addtimer(CALLBACK(src, PROC_REF(add_active_arrow), new_arrow, picked_arrow), ((ENDING_ARROW_POSITION - (STARTING_ARROW_POSITION + get_arrow_height(picked_arrow))) / arrow_speed) SECONDS) + +/datum/riding_minigame/proc/get_arrow_height(text_direction) + var/direction = text2dir(text_direction) + return NSCOMPONENT(direction) ? VERTICAL_ARROW_HEIGHT : HORIZONTAL_ARROW_HEIGHT + +/datum/riding_minigame/proc/add_active_arrow(atom/arrow, direction) + if(QDELETED(arrow)) + return + + if(!COOLDOWN_FINISHED(src, failure_cooldown)) + remove_active_arrow(arrow, direction) + return + + var/mob/living/rider = mounter?.resolve() + if(isnull(rider)) + return + var/client/rider_client = rider.client + if(isnull(rider_client)) + return + cached_arrows[direction]["is_active"] = arrow + RegisterSignal(arrow, COMSIG_QDELETING, PROC_REF(on_arrow_delete)) + var/distance_to_travel = get_arrow_height(direction) * 2 + var/time_of_grace = (distance_to_travel / arrow_speed) SECONDS + linger_time + PING_GRACE - FADE_AWAY_TIME + addtimer(CALLBACK(src, PROC_REF(remove_active_arrow), arrow, direction), time_of_grace) + +/datum/riding_minigame/proc/remove_active_arrow(atom/arrow, direction) + if(QDELETED(arrow)) + return + animate(arrow, alpha = 0, time = FADE_AWAY_TIME) + QDEL_IN(arrow, FADE_AWAY_TIME) + +/datum/riding_minigame/proc/on_arrow_delete(datum/source) + SIGNAL_HANDLER + for(var/arrow in cached_arrows) + var/list/arrow_details = cached_arrows[arrow] + if(arrow_details["is_active"] != source) + continue + arrow_details["is_active"] = null + +/datum/riding_minigame/proc/on_ridden_moved(atom/movable/source, atom/new_loc, direction) + SIGNAL_HANDLER + . = NONE + if(new_loc.z != source.z || !COOLDOWN_FINISHED(src, failure_cooldown)) + return + var/list/arrow_data = cached_arrows[dir2text(direction)] + var/atom/existing_arrow = arrow_data["is_active"] + + if(!QDELETED(existing_arrow)) + qdel(existing_arrow) + var/atom/arrow_object = arrow_data["visual_object"] + flick("blank_arrow_win", arrow_object) + increment_counter() + return + + for(var/arrow_direction in cached_arrows) + var/obj/my_arrow = cached_arrows[arrow_direction]["visual_object"] + if(isnull(my_arrow)) + continue + var/atom/clickable_arrow = cached_arrows[arrow_direction]["is_active"] + if(!QDELETED(clickable_arrow)) + qdel(clickable_arrow) + my_arrow.layer = ABOVE_ALL_MOB_LAYER + 0.2 + flick("blank_arrow_lose", my_arrow) + my_arrow.Shake(duration = 2 SECONDS) + addtimer(VARSET_CALLBACK(my_arrow, layer, ABOVE_ALL_MOB_LAYER), 2 SECONDS) + + increment_failure() + +/datum/riding_minigame/proc/increment_counter() + current_succeeded++ + var/obj/new_heart = hearts_list[current_succeeded] + new_heart.icon_state = "full_heart" + new_heart.transform = new_heart.transform.Scale(2 ,2) + animate(new_heart, transform = matrix(), time = 0.3 SECONDS) + if(current_succeeded >= required_successes) + win_minigame() + +/datum/riding_minigame/proc/increment_failure() + current_failures++ + COOLDOWN_START(src, failure_cooldown, 2 SECONDS) + if(current_failures > (maximum_attempts - required_successes)) + lose_minigame() + +/datum/riding_minigame/proc/lose_minigame() + SIGNAL_HANDLER + var/mob/living/living_host = host?.resolve() + var/mob/living/living_rider = mounter?.resolve() + if(isnull(living_host) || isnull(living_rider)) + qdel(src) + return + if(LAZYFIND(living_host.buckled_mobs, living_rider)) + UnregisterSignal(living_rider, COMSIG_MOB_UNBUCKLED) //we're about to knock them down! + living_host.spin(spintime = 2 SECONDS, speed = 1) + living_rider.Knockdown(4 SECONDS) + living_host.unbuckle_mob(living_rider) + living_host.balloon_alert(living_rider, "knocks you down!") + qdel(src) + +/datum/riding_minigame/proc/win_minigame() + var/mob/living/living_host = host?.resolve() + var/mob/living/living_rider = mounter?.resolve() + if(isnull(living_host) || isnull(living_rider)) + qdel(src) + return + living_host.befriend(living_rider) + living_host.balloon_alert(living_rider, "calms down...") + qdel(src) + +/datum/riding_minigame/Destroy() + STOP_PROCESSING(SSprocessing, src) + + var/mob/living/living_mounter = mounter?.resolve() + if(living_mounter) + living_mounter.client.images -= minigame_holder + living_mounter.client.images -= heart_counter + + mounter = null + host = null + hearts_list = null + cached_arrows = null + minigame_holder = null + heart_counter = null + return ..() + +/obj/effect/overlay/vis/ride_minigame + vis_flags = VIS_INHERIT_PLANE + +#undef STARTING_ARROW_POSITION +#undef ENDING_ARROW_POSITION +#undef VERTICAL_ARROW_HEIGHT +#undef HORIZONTAL_ARROW_HEIGHT +#undef FADE_AWAY_TIME +#undef PING_GRACE diff --git a/code/datums/components/riding/riding_mob.dm b/code/datums/components/riding/riding_mob.dm index d4d9b4ba99c..46fc7b7cf9b 100644 --- a/code/datums/components/riding/riding_mob.dm +++ b/code/datums/components/riding/riding_mob.dm @@ -672,217 +672,3 @@ /datum/component/riding/creature/raptor/fast vehicle_move_delay = 1.5 - -//a simple minigame players must win to mount and tame a mob -/datum/riding_minigame - ///our host mob - var/datum/weakref/host - ///our current rider - var/datum/weakref/mounter - ///the total amount of tries the rider gets - var/maximum_attempts = 25 - ///maximum number of failures before we fail - var/current_attempts = 0 - ///required number of successes - var/required_successes = 11 - ///what failures are we on - var/current_failures = 0 - ///we win these - var/current_succeeded = 0 - ///holder of our heart counter - var/image/heart_counter - ///list of our hearts - var/list/hearts_list = list() - ///holder of our minigame! - var/image/minigame_holder - ///cached directional icons of our host - var/list/cached_arrows = list() - ///cooldown when we fail - COOLDOWN_DECLARE(failure_cooldown) - -/datum/riding_minigame/New(mob/living/ridden, mob/living/rider) - . = ..() - host = WEAKREF(ridden) - mounter = WEAKREF(rider) - RegisterSignal(rider, COMSIG_MOB_UNBUCKLED, PROC_REF(lose_minigame)) - RegisterSignal(ridden, COMSIG_MOVABLE_ATTEMPTED_MOVE, PROC_REF(on_ridden_moved)) - minigame_holder = image(icon='icons/effects/effects.dmi', loc=rider,icon_state="nothing", layer = 0, pixel_x = 32, pixel_y = 0) - heart_counter = image(icon='icons/effects/effects.dmi', loc=rider,icon_state="nothing", layer = 0, pixel_x = 0, pixel_y = -32) - SET_PLANE_EXPLICIT(minigame_holder, ABOVE_HUD_PLANE, rider) - SET_PLANE_EXPLICIT(heart_counter, ABOVE_HUD_PLANE, rider) - generate_heart_counter() - generate_visuals() - rider.client.images |= list(minigame_holder, heart_counter) - START_PROCESSING(SSprocessing, src) - -/datum/riding_minigame/proc/generate_visuals() - var/static/list/void_arrow_angles = list( - "north" = -90, - "west" = 180, - "south" = 90, - "east" = 0, - ) - var/x_offset = 0 - for(var/direction in void_arrow_angles) - var/obj/effect/overlay/vis/ride_minigame/new_arrow = new - new_arrow.icon = 'icons/effects/riding_minigame.dmi' - new_arrow.icon_state = "blank_arrow" - new_arrow.transform = new_arrow.transform.Turn(void_arrow_angles[direction]) - new_arrow.pixel_x = x_offset - new_arrow.layer = ABOVE_ALL_MOB_LAYER - minigame_holder.vis_contents += new_arrow - cached_arrows[direction] = list("visual_object" = new_arrow, "is_active" = null) - x_offset += 16 - -/datum/riding_minigame/proc/generate_heart_counter() - var/x_offset = -32 - for(var/i in 1 to required_successes) - var/obj/effect/overlay/vis/ride_minigame/heart = new - heart.icon = 'icons/effects/effects.dmi' - heart.icon_state = "empty_heart" - heart.pixel_x = x_offset - x_offset += 8 - hearts_list += heart - heart_counter.vis_contents += heart - -/datum/riding_minigame/process() - if(current_attempts >= maximum_attempts) - lose_minigame() - return - if(prob(30)) //we shake and move uncontrollably! - var/mob/living/living_host = host.resolve() - living_host.Shake(pixelshiftx = 1, pixelshifty = 0, duration = 0.75 SECONDS) - living_host.spin(spintime = 0.75 SECONDS, speed = 1) - - - - for(var/index in 0 to 1) - addtimer(CALLBACK(src, PROC_REF(generate_arrow)), 0.3 SECONDS * index) - -/datum/riding_minigame/proc/generate_arrow() - if(current_attempts >= maximum_attempts) - return - var/static/list/possible_arrows = list( - "north" = 0, - "west" = 16, - "south" = 32, - "east" = 48, - ) - current_attempts++ - var/picked_arrow = pick(possible_arrows) - var/obj/effect/overlay/vis/ride_minigame/new_arrow = new - new_arrow.icon = 'icons/effects/riding_minigame.dmi' - new_arrow.icon_state = "[picked_arrow]_arrow" - new_arrow.alpha = 0 - new_arrow.layer = ABOVE_ALL_MOB_LAYER + 0.1 - new_arrow.pixel_x = possible_arrows[picked_arrow] - new_arrow.pixel_y = -50 - minigame_holder.vis_contents += new_arrow - animate(new_arrow, alpha = 255, time = 0.15 SECONDS) - animate(new_arrow, pixel_y = 20, time = 1.4 SECONDS) - addtimer(CALLBACK(src, PROC_REF(add_active_arrow), new_arrow, picked_arrow), 0.9 SECONDS) - -/datum/riding_minigame/proc/add_active_arrow(atom/arrow, direction) - if(QDELETED(arrow)) - return - cached_arrows[direction]["is_active"] = arrow - RegisterSignal(arrow, COMSIG_QDELETING, PROC_REF(on_arrow_delete)) - addtimer(CALLBACK(src, PROC_REF(remove_active_arrow), arrow, direction), 0.2 SECONDS) - -/datum/riding_minigame/proc/remove_active_arrow(atom/arrow, direction) - if(QDELETED(arrow)) - return - animate(arrow, alpha = 0, time = 0.1 SECONDS) - QDEL_IN(arrow, 0.1 SECONDS) - -/datum/riding_minigame/proc/on_arrow_delete(datum/source) - SIGNAL_HANDLER - for(var/arrow in cached_arrows) - var/list/arrow_details = cached_arrows[arrow] - if(arrow_details["is_active"] != source) - continue - arrow_details["is_active"] = null - -/datum/riding_minigame/proc/on_ridden_moved(atom/movable/source, atom/new_loc, direction) - SIGNAL_HANDLER - . = NONE - if(new_loc.z != source.z || !COOLDOWN_FINISHED(src, failure_cooldown)) - return - var/list/arrow_data = cached_arrows[dir2text(direction)] - var/atom/existing_arrow = arrow_data["is_active"] - - if(!QDELETED(existing_arrow)) - qdel(existing_arrow) - var/atom/arrow_object = arrow_data["visual_object"] - flick("blank_arrow_win", arrow_object) - increment_counter() - return - - for(var/arrow_direction in cached_arrows) - var/obj/my_arrow = cached_arrows[arrow_direction]["visual_object"] - if(isnull(my_arrow)) - continue - flick("blank_arrow_lose", my_arrow) - my_arrow.Shake(duration = 2 SECONDS) - - increment_failure() - -/datum/riding_minigame/proc/increment_counter() - current_succeeded++ - var/obj/new_heart = hearts_list[current_succeeded] - new_heart.icon_state = "full_heart" - new_heart.transform = new_heart.transform.Scale(2 ,2) - animate(new_heart, transform = matrix(), time = 0.3 SECONDS) - if(current_succeeded >= required_successes) - win_minigame() - -/datum/riding_minigame/proc/increment_failure() - current_failures++ - COOLDOWN_START(src, failure_cooldown, 2 SECONDS) - if(current_failures > (maximum_attempts - required_successes)) - lose_minigame() - -/datum/riding_minigame/proc/lose_minigame() - SIGNAL_HANDLER - var/mob/living/living_host = host?.resolve() - var/mob/living/living_rider = mounter?.resolve() - if(isnull(living_host) || isnull(living_rider)) - qdel(src) - return - if(LAZYFIND(living_host.buckled_mobs, living_rider)) - UnregisterSignal(living_rider, COMSIG_MOB_UNBUCKLED) //we're about to knock them down! - living_host.spin(spintime = 2 SECONDS, speed = 1) - living_rider.Knockdown(4 SECONDS) - living_host.unbuckle_mob(living_rider) - living_host.balloon_alert(living_rider, "knocks you down!") - qdel(src) - -/datum/riding_minigame/proc/win_minigame() - var/mob/living/living_host = host?.resolve() - var/mob/living/living_rider = mounter?.resolve() - if(isnull(living_host) || isnull(living_rider)) - qdel(src) - return - living_host.befriend(living_rider) - living_host.balloon_alert(living_rider, "calms down...") - qdel(src) - -/datum/riding_minigame/Destroy() - STOP_PROCESSING(SSprocessing, src) - - var/mob/living/living_mounter = mounter?.resolve() - if(living_mounter) - living_mounter.client.images -= minigame_holder - living_mounter.client.images -= heart_counter - - mounter = null - host = null - hearts_list = null - cached_arrows = null - minigame_holder = null - heart_counter = null - return ..() - -/obj/effect/overlay/vis/ride_minigame - vis_flags = VIS_INHERIT_DIR | VIS_INHERIT_PLANE - diff --git a/code/modules/mob/living/basic/lavaland/raptor/raptor_ai_controller.dm b/code/modules/mob/living/basic/lavaland/raptor/raptor_ai_controller.dm index 4241eb4295e..a21f0b47e01 100644 --- a/code/modules/mob/living/basic/lavaland/raptor/raptor_ai_controller.dm +++ b/code/modules/mob/living/basic/lavaland/raptor/raptor_ai_controller.dm @@ -31,6 +31,21 @@ /datum/ai_planning_subtree/find_and_hunt_target/play_with_owner/raptor, ) +/datum/ai_controller/basic_controller/raptor/TryPossessPawn(atom/new_pawn) + . = ..() + if(. & AI_CONTROLLER_INCOMPATIBLE) + return + RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_SET(BB_RAPTOR_COWARD), PROC_REF(on_cowardly_set)) + RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(BB_RAPTOR_COWARD), PROC_REF(on_cowardly_clear)) + +/datum/ai_controller/basic_controller/raptor/proc/on_cowardly_set(datum/source) + SIGNAL_HANDLER + ADD_TRAIT(pawn, TRAIT_MOB_DIFFICULT_TO_MOUNT, REF(src)) + +/datum/ai_controller/basic_controller/raptor/proc/on_cowardly_clear(datum/source) + SIGNAL_HANDLER + REMOVE_TRAIT(pawn, TRAIT_MOB_DIFFICULT_TO_MOUNT, REF(src)) + /datum/ai_controller/basic_controller/raptor/on_mob_eat() . = ..() clear_blackboard_key(BB_RAPTOR_TROUGH_TARGET) diff --git a/icons/effects/riding_minigame.dmi b/icons/effects/riding_minigame.dmi index 55c2b1fef73..fada0ca9642 100644 Binary files a/icons/effects/riding_minigame.dmi and b/icons/effects/riding_minigame.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 6ea73b3ca63..df22038a2de 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1369,6 +1369,7 @@ #include "code\datums\components\plumbing\simple_components.dm" #include "code\datums\components\plumbing\splitter.dm" #include "code\datums\components\riding\riding.dm" +#include "code\datums\components\riding\riding_minigame.dm" #include "code\datums\components\riding\riding_mob.dm" #include "code\datums\components\riding\riding_vehicle.dm" #include "code\datums\components\style\style.dm"