diff --git a/code/__DEFINES/ai.dm b/code/__DEFINES/ai.dm index db0a2f31547..eee06e8df8a 100644 --- a/code/__DEFINES/ai.dm +++ b/code/__DEFINES/ai.dm @@ -56,6 +56,21 @@ ///Amount of successful hits in a row this item has had #define BB_HAUNTED_THROW_ATTEMPT_COUNT "BB_haunted_throw_attempt_count" +///Cursed item controller defines + +//defines +///how far a cursed item will still try to chase a target +#define CURSED_VIEW_RANGE 7 +#define CURSED_ITEM_TELEPORT_CHANCE 4 +//blackboards + +///Actual mob the item is haunting at the moment +#define BB_CURSE_TARGET "BB_haunt_target" +///Where the item wants to land on +#define BB_TARGET_SLOT "BB_target_slot" +///Amount of successful hits in a row this item has had +#define BB_CURSED_THROW_ATTEMPT_COUNT "BB_cursed_throw_attempt_count" + ///Vending machine AI controller blackboard keys #define BB_VENDING_CURRENT_TARGET "BB_vending_current_target" #define BB_VENDING_TILT_COOLDOWN "BB_vending_tilt_cooldown" diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index 156dce1bb37..487ec775164 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -744,7 +744,10 @@ #define COMSIG_ITEM_EQUIPPED "item_equip" ///called on [/obj/item] before unequip from base of [mob/proc/doUnEquip]: (force, atom/newloc, no_move, invdrop, silent) #define COMSIG_ITEM_PRE_UNEQUIP "item_pre_unequip" + ///only the pre unequip can be cancelled #define COMPONENT_ITEM_BLOCK_UNEQUIP (1<<0) +///called on [/obj/item] AFTER unequip from base of [mob/proc/doUnEquip]: (force, atom/newloc, no_move, invdrop, silent) +#define COMSIG_ITEM_POST_UNEQUIP "item_post_unequip" ///from base of obj/item/on_grind(): ()) #define COMSIG_ITEM_ON_GRIND "on_grind" ///from base of obj/item/on_juice(): () diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 9c6996e8eb7..4de8b8587c7 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -272,7 +272,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// nobody can use martial arts on this mob #define TRAIT_MARTIAL_ARTS_IMMUNE "martial_arts_immune" /// You've been cursed with a living duffelbag, and can't have more added -#define TRAIT_DUFFEL_CURSED "duffel_cursed" +#define TRAIT_DUFFEL_CURSE_PROOF "duffel_cursed" /// Revenants draining you only get a very small benefit. #define TRAIT_WEAK_SOUL "weak_soul" /// Prevents mob from riding mobs when buckled onto something @@ -484,7 +484,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define CHANGELING_TRAIT "changeling" #define CULT_TRAIT "cult" /// The item is magically cursed -#define CURSED_ITEM_TRAIT "cursed-item" +#define CURSED_ITEM_TRAIT(item_type) "cursed_item_[item_type]" #define ABSTRACT_ITEM_TRAIT "abstract-item" #define STATUS_EFFECT_TRAIT "status-effect" #define CLOTHING_TRAIT "clothing" diff --git a/code/datums/ai/_item_behaviors.dm b/code/datums/ai/_item_behaviors.dm index e51c7f392c5..99c2d903f5f 100644 --- a/code/datums/ai/_item_behaviors.dm +++ b/code/datums/ai/_item_behaviors.dm @@ -41,6 +41,10 @@ /datum/ai_behavior/item_move_close_and_attack/finish_action(datum/ai_controller/controller, succeeded) . = ..() + reset_blackboard(controller, succeeded) + + +/datum/ai_behavior/item_move_close_and_attack/proc/reset_blackboard(datum/ai_controller/controller, succeeded) controller.blackboard[throw_count_key] = 0 controller.blackboard[target_key] = null diff --git a/code/datums/ai/cursed/cursed_behaviors.dm b/code/datums/ai/cursed/cursed_behaviors.dm new file mode 100644 index 00000000000..bab31f7b5c2 --- /dev/null +++ b/code/datums/ai/cursed/cursed_behaviors.dm @@ -0,0 +1,17 @@ + +/datum/ai_behavior/find_and_set/cursed + //optional, don't use if you're changing search_tactic() + locate_path = /mob/living/carbon + bb_key_to_set = BB_CURSE_TARGET + +/datum/ai_behavior/item_move_close_and_attack/cursed + throw_count_key = BB_CURSED_THROW_ATTEMPT_COUNT + target_key = BB_CURSE_TARGET + attack_sound = 'sound/items/haunted/ghostitemattack.ogg' + max_attempts = 4 + +/datum/ai_behavior/item_move_close_and_attack/cursed/reset_blackboard(datum/ai_controller/controller, succeeded) + var/atom/throw_target = controller.blackboard[target_key] + //dropping our target from the blackboard if they are no longer a valid target after the attack behavior + if(get_dist(throw_target, controller.pawn) > CURSED_VIEW_RANGE) + controller.blackboard[target_key] = null diff --git a/code/datums/ai/cursed/cursed_controller.dm b/code/datums/ai/cursed/cursed_controller.dm new file mode 100644 index 00000000000..468dc32d650 --- /dev/null +++ b/code/datums/ai/cursed/cursed_controller.dm @@ -0,0 +1,95 @@ +/** + * # cursed item ai! + * + * Haunted AI tries to not be interacted with, and will attack people who do. + * Cursed AI instead tries to be interacted with, and will attempt to equip itself onto people. + * Added by /datum/element/cursed, and as such will try to remove this element and go dormant when it finds a victim to curse + */ +/datum/ai_controller/cursed + movement_delay = 0.4 SECONDS + blackboard = list( + BB_CURSE_TARGET, + BB_TARGET_SLOT, + BB_CURSED_THROW_ATTEMPT_COUNT + ) + +/datum/ai_controller/cursed/TryPossessPawn(atom/new_pawn) + if(!isitem(new_pawn)) + return AI_CONTROLLER_INCOMPATIBLE + RegisterSignal(new_pawn, COMSIG_MOVABLE_IMPACT, .proc/on_throw_hit) + RegisterSignal(new_pawn, COMSIG_ITEM_EQUIPPED, .proc/on_equip) + return ..() //Run parent at end + +/datum/ai_controller/cursed/UnpossessPawn() + UnregisterSignal(pawn, list(COMSIG_MOVABLE_IMPACT, COMSIG_ITEM_EQUIPPED)) + return ..() //Run parent at end + +/datum/ai_controller/cursed/SelectBehaviors(delta_time) + current_behaviors = list() + var/obj/item/item_pawn = pawn + + + //make sure we have a target + var/mob/living/carbon/curse_target = blackboard[BB_CURSE_TARGET] + if(!curse_target) + current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/find_and_set/cursed) + return + //make sure attack is valid + if(get_dist(curse_target, item_pawn) > CURSED_VIEW_RANGE) + blackboard[BB_CURSE_TARGET] = null + return + current_movement_target = curse_target + current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/item_move_close_and_attack/cursed) + +/datum/ai_controller/cursed/PerformIdleBehavior(delta_time) + var/obj/item/item_pawn = pawn + if(ismob(item_pawn.loc)) //Being held. dont teleport + return + if(DT_PROB(CURSED_ITEM_TELEPORT_CHANCE, delta_time)) + playsound(item_pawn.loc, 'sound/items/haunted/ghostitemattack.ogg', 100, TRUE) + do_teleport(pawn, get_turf(pawn), 4, channel = TELEPORT_CHANNEL_MAGIC) + +///signal called by the pawn hitting something after a throw +/datum/ai_controller/cursed/proc/on_throw_hit(datum/source, atom/hit_atom, datum/thrownthing/throwingdatum) + SIGNAL_HANDLER + if(!iscarbon(hit_atom)) + return + //equipcode has sleeps all over it. + INVOKE_ASYNC(src, .proc/try_equipping_to_target_slot, hit_atom) + +///signal called by picking up the pawn, will try to equip to where it should actually be and start the curse +/datum/ai_controller/cursed/proc/on_equip(datum/source, mob/equipper, slot) + SIGNAL_HANDLER + INVOKE_ASYNC(src, .proc/try_equipping_to_target_slot, equipper, slot) + +/** + * curse of hunger component; for very hungry items. + * + * called when someone grabs the cursed item or the cursed item impacts with a mob it's throwing itself at + * arguments: + * * curse_victim: whomever we're attaching this to + * * slot_already_in: the slot the item is already in before this was called, possibly null but at least in hands if picked up + */ +/datum/ai_controller/cursed/proc/try_equipping_to_target_slot(mob/living/carbon/curse_victim, slot_already_in) + var/obj/item/item_pawn = pawn + var/attempted_slot = blackboard[BB_TARGET_SLOT] + if(slot_already_in && attempted_slot == slot_already_in) //thanks for making it easy + what_a_horrible_night_to_have_a_curse() + return + if(attempted_slot == ITEM_SLOT_HANDS) //hands needs some different checks + curse_victim.drop_all_held_items() + if(curse_victim.put_in_hands(item_pawn, del_on_fail = FALSE)) + to_chat(curse_victim, "[item_pawn] leaps into your hands!") + what_a_horrible_night_to_have_a_curse() + return + var/obj/item/blocking = curse_victim.get_item_by_slot(attempted_slot) + if(!curse_victim.dropItemToGround(blocking, silent = TRUE)) + return //cannot equip to this person so whatever just keep whacking them until they die or fugg off + curse_victim.equip_to_slot_if_possible(item_pawn, attempted_slot, qdel_on_fail = FALSE, disable_warning = FALSE) + to_chat(curse_victim, "[item_pawn] equips [item_pawn.p_them()]self onto you!") + what_a_horrible_night_to_have_a_curse() + +///proc called when the cursed object successfully attaches itself to someone, removing the cursed element and by extension the ai itself +/datum/ai_controller/cursed/proc/what_a_horrible_night_to_have_a_curse() + var/obj/item/item_pawn = pawn + item_pawn.RemoveElement(/datum/element/cursed) diff --git a/code/datums/ai/generic_actions.dm b/code/datums/ai/generic_actions.dm index 038ca326059..35f59781bf2 100644 --- a/code/datums/ai/generic_actions.dm +++ b/code/datums/ai/generic_actions.dm @@ -156,3 +156,28 @@ if(QDELETED(target) || prob(10)) // Even if we don't finish it all we can randomly decide to be done finish_action(controller, TRUE) + +/**find and set + * Finds an item near themselves, sets a blackboard key as it. Very useful for ais that need to use machines or something. + * if you want to do something more complicated than find a single atom, change the search_tactic() proc + * cool tip: search_tactic() can set lists + */ +/datum/ai_behavior/find_and_set + action_cooldown = 5 SECONDS + ///search range in how many tiles around the pawn to look for the path + var/search_range = 7 + //optional, don't use if you're changing search_tactic() + var/locate_path + var/bb_key_to_set + +/datum/ai_behavior/find_and_set/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/find_this_thing = search_tactic(controller) + if(find_this_thing) + controller.blackboard[bb_key_to_set] = find_this_thing + finish_action(controller, TRUE) + else + finish_action(controller, FALSE) + +/datum/ai_behavior/find_and_set/proc/search_tactic(datum/ai_controller/controller) + return locate(locate_path) in oview(search_range, controller.pawn) diff --git a/code/datums/components/curse_of_hunger.dm b/code/datums/components/curse_of_hunger.dm new file mode 100644 index 00000000000..a754f5fa61c --- /dev/null +++ b/code/datums/components/curse_of_hunger.dm @@ -0,0 +1,156 @@ +///inital food tolerances, two dishes +#define FULL_HEALTH 2 +///the point where you can notice the item is hungry on examine. +#define HUNGER_THRESHOLD_WARNING 25 +///the point where the item has a chance to eat something on every tick. possibly you! +#define HUNGER_THRESHOLD_TRY_EATING 50 + +/** + * curse of hunger component; for very hungry items. + * + * Used as a rpgloot suffix and wizard spell! + */ +/datum/component/curse_of_hunger + ///whether to add dropdel to the item with curse of hunger, used for temporary curses like the wizard duffelbags + var/add_dropdel + ///items given the curse of hunger will not seek out someone else to latch onto until they are dropped for the first time. + var/awakened = FALSE + ///counts time passed since it ate food + var/hunger = 0 + ///how many times it needs to be fed poisoned food for it to drop off of you + var/poison_food_tolerance = FULL_HEALTH + +/datum/component/curse_of_hunger/Initialize(add_dropdel = FALSE) + . = ..() + if(!isitem(parent)) + return COMPONENT_INCOMPATIBLE + src.add_dropdel = add_dropdel + +/datum/component/curse_of_hunger/RegisterWithParent() + . = ..() + var/obj/item/cursed_item = parent + RegisterSignal(cursed_item, COMSIG_PARENT_EXAMINE, .proc/on_examine) + //checking slot_equipment_priority is the better way to decide if it should be an equip-curse (alternative being if it has slot_flags) + //because it needs to know where to equip to (and stuff like buckets and cones can be on_pickup curses despite having slots to equip to) + if(cursed_item.slot_equipment_priority) + RegisterSignal(cursed_item, COMSIG_ITEM_EQUIPPED, .proc/on_equip) + else + RegisterSignal(cursed_item, COMSIG_ITEM_PICKUP, .proc/on_pickup) + +/datum/component/curse_of_hunger/UnregisterFromParent() + . = ..() + UnregisterSignal(parent, list(COMSIG_PARENT_EXAMINE, COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_POST_UNEQUIP, COMSIG_ITEM_PICKUP, COMSIG_ITEM_DROPPED)) + +///signal called on parent being examined +/datum/component/curse_of_hunger/proc/on_examine(datum/source, mob/user, list/examine_list) + SIGNAL_HANDLER + if(!awakened) + return //we should not reveal we are cursed until equipped + if(poison_food_tolerance != FULL_HEALTH) + examine_list += "[parent] looks sick from something it ate." + if(hunger > HUNGER_THRESHOLD_WARNING) + examine_list += "[parent] hungers for something to eat..." + +///signal called from equipping parent +/datum/component/curse_of_hunger/proc/on_equip(datum/source, mob/equipper, slot) + SIGNAL_HANDLER + var/obj/item/at_least_item = parent + if(!(at_least_item.slot_flags & slot)) + return + the_curse_begins(equipper) + +///signal called from a successful unequip of parent +/datum/component/curse_of_hunger/proc/on_unequip(mob/living/unequipper, force, atom/newloc, no_move, invdrop, silent) + SIGNAL_HANDLER + the_curse_ends(unequipper) + +///signal called from picking up parent +/datum/component/curse_of_hunger/proc/on_pickup(datum/source, mob/grabber) + SIGNAL_HANDLER + the_curse_begins(grabber) + +///signal called from dropping parent +/datum/component/curse_of_hunger/proc/on_drop(datum/source, mob/dropper) + SIGNAL_HANDLER + the_curse_ends(dropper) + +/datum/component/curse_of_hunger/proc/the_curse_begins(mob/cursed) + var/obj/item/cursed_item = parent + awakened = TRUE + START_PROCESSING(SSobj, src) + ADD_TRAIT(cursed_item, TRAIT_NODROP, CURSED_ITEM_TRAIT(cursed_item.type)) + ADD_TRAIT(cursed, TRAIT_CLUMSY, CURSED_ITEM_TRAIT(cursed_item.type)) + ADD_TRAIT(cursed, TRAIT_PACIFISM, CURSED_ITEM_TRAIT(cursed_item.type)) + if(add_dropdel) + cursed_item.item_flags |= DROPDEL + return + if(cursed_item.slot_equipment_priority) + RegisterSignal(cursed_item, COMSIG_ITEM_POST_UNEQUIP, .proc/on_unequip) + else + RegisterSignal(cursed_item, COMSIG_ITEM_DROPPED, .proc/on_drop) + +/datum/component/curse_of_hunger/proc/the_curse_ends(mob/uncursed) + var/obj/item/at_least_item = parent + STOP_PROCESSING(SSobj, src) + REMOVE_TRAIT(parent, TRAIT_NODROP, CURSED_ITEM_TRAIT(parent.type)) + REMOVE_TRAIT(uncursed, TRAIT_CLUMSY, CURSED_ITEM_TRAIT(at_least_item.type)) + REMOVE_TRAIT(uncursed, TRAIT_PACIFISM, CURSED_ITEM_TRAIT(at_least_item.type)) + //remove either one of the signals that could have called this proc + UnregisterSignal(parent, list(COMSIG_ITEM_POST_UNEQUIP, COMSIG_ITEM_DROPPED)) + + var/turf/vomit_turf = get_turf(at_least_item) + playsound(vomit_turf, 'sound/effects/splat.ogg', 50, TRUE) + new /obj/effect/decal/cleanable/vomit(vomit_turf) + + if(!add_dropdel) //gives a head start for the person to get away from the cursed item before it begins hunting again! + addtimer(CALLBACK(src, .proc/seek_new_target), 10 SECONDS) + +///proc called after a timer to awaken the AI in the cursed item if it doesn't have a target already. +/datum/component/curse_of_hunger/proc/seek_new_target() + var/obj/item/cursed_item = parent + if(iscarbon(cursed_item.loc)) + return + else if(!isturf(cursed_item.loc)) + cursed_item.forceMove(get_turf(cursed_item)) + //only taking the most reasonable slot is fine since it unequips what is there to equip itself. + cursed_item.AddElement(/datum/element/cursed, cursed_item.slot_equipment_priority[1]) + cursed_item.visible_message("[cursed_item] begins to move on [cursed_item.p_their()] own...") + +/datum/component/curse_of_hunger/process(delta_time) + var/obj/item/cursed_item = parent + var/mob/living/carbon/cursed = cursed_item.loc + ///check hp + if(!poison_food_tolerance) + cursed.dropItemToGround(cursed_item, TRUE) + return + hunger++ + if((hunger <= HUNGER_THRESHOLD_TRY_EATING) && prob(20)) + return + //check hungry enough to eat something! + for(var/obj/item/food in cursed.contents) + if(!IS_EDIBLE(food)) + continue + food.forceMove(cursed.loc) + playsound(cursed_item, 'sound/items/eatfood.ogg', 20, TRUE) + ///poisoned food damages it + if(istype(food, /obj/item/food/badrecipe)) + to_chat(cursed, "[cursed_item] eats your [food] to sate [cursed_item.p_their()] hunger, and looks [pick("queasy", "sick", "iffy", "unwell")] afterwards!") + poison_food_tolerance-- + else + to_chat(cursed, "[cursed_item] eats your [food] to sate [cursed_item.p_their()] hunger.") + cursed.temporarilyRemoveItemFromInventory(food, force = TRUE) + qdel(food) + hunger = 0 + return + ///no food found: it bites you and regains some poison food tolerance + playsound(cursed_item, 'sound/items/eatfood.ogg', 20, TRUE) + to_chat(cursed, "[cursed_item] bites you to sate [cursed_item.p_their()] hunger!") + var/affecting = cursed.get_bodypart(BODY_ZONE_CHEST) + cursed.apply_damage(60, BRUTE, affecting) + hunger = 0 + poison_food_tolerance = min(poison_food_tolerance + 1, FULL_HEALTH) + +/datum/component/curse_of_hunger/proc/test() + var/obj/item/cursed_item = parent + var/mob/living/carbon/cursed = cursed_item.loc + cursed.dropItemToGround(cursed_item, TRUE) diff --git a/code/datums/components/fantasy/suffixes.dm b/code/datums/components/fantasy/suffixes.dm index 43726d7e2ff..df823b99fee 100644 --- a/code/datums/components/fantasy/suffixes.dm +++ b/code/datums/components/fantasy/suffixes.dm @@ -173,5 +173,30 @@ /datum/fantasy_affix/fool/apply(datum/component/fantasy/comp, newName) . = ..() var/obj/item/master = comp.parent - comp.appliedComponents += master.AddComponent(/datum/component/squeak, list('sound/items/bikehorn.ogg'=1), 50, falloff_exponent = 20) + comp.appliedComponents += master.AddComponent(/datum/component/squeak, list('sound/items/bikehorn.ogg' = 1), 50, falloff_exponent = 20) return "[newName] of the fool" + +/datum/fantasy_affix/curse_of_hunger + name = "curse of hunger" + placement = AFFIX_SUFFIX + alignment = AFFIX_EVIL + +/datum/fantasy_affix/curse_of_hunger/validate(obj/item/attached) + //curse of hunger that attaches onto food has the ability to eat itself. it's hilarious. + if(!IS_EDIBLE(attached)) + return TRUE + return TRUE + +/datum/fantasy_affix/curse_of_hunger/apply(datum/component/fantasy/comp, newName) + . = ..() + var/obj/item/master = comp.parent + var/filter_color = "#8a0c0ca1" //clarified args + var/new_name = pick(", eternally hungry", " of the glutton", " cursed with hunger", ", consumer of all", " of the feast") + master.AddElement(/datum/element/curse_announcement, "[master] is cursed with the curse of hunger!", filter_color, new_name, comp) + var/add_dropdel = FALSE //clarified boolean + comp.appliedComponents += master.AddComponent(/datum/component/curse_of_hunger, add_dropdel) + return newName //no spoilers! + +/datum/fantasy_affix/curse_of_hunger/remove(datum/component/fantasy/comp) + var/obj/item/master = comp.parent + master.RemoveElement(/datum/element/curse_announcement) //just in case diff --git a/code/datums/elements/curse_announcement.dm b/code/datums/elements/curse_announcement.dm new file mode 100644 index 00000000000..35b71cf5556 --- /dev/null +++ b/code/datums/elements/curse_announcement.dm @@ -0,0 +1,63 @@ +/** + * # curse announcement element! + * + * Bespoke element that sends a harrowing message when you first pick up an item, applying a spooky color outline and renaming the item. + * For most items, it will announce when picked up. If the item can be equipped, though, it will only announce when the item is worn. + * Possible improvements for the future: add an option to allow the cursed affix to be a prefix. right now only coded for suffixes + */ +/datum/element/curse_announcement + element_flags = ELEMENT_BESPOKE|ELEMENT_DETACH + id_arg_index = 2 + ///message sent on announce + var/announcement_message + ///color of the outline filter on announce + var/filter_color + ///new name given to the item on announce + var/new_name + ///optional fantasy component, used in building the name if provided + var/datum/component/fantasy/fantasy_component + +/datum/element/curse_announcement/Attach(datum/target, announcement_message, filter_color, new_name, datum/component/fantasy/fantasy_component) + . = ..() + if(!isitem(target)) + return ELEMENT_INCOMPATIBLE + var/obj/item/cursed_item = target + src.announcement_message = announcement_message + src.filter_color = filter_color + src.new_name = new_name + src.fantasy_component = fantasy_component + if(cursed_item.slot_equipment_priority) //if it can equip somewhere, only go active when it is actually done + RegisterSignal(cursed_item, COMSIG_ITEM_EQUIPPED, .proc/on_equipped) + else + RegisterSignal(cursed_item, COMSIG_ITEM_PICKUP, .proc/on_pickup) + +/datum/element/curse_announcement/Detach(datum/target) + . = ..() + UnregisterSignal(target, list(COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_PICKUP)) + +/datum/element/curse_announcement/proc/on_equipped(obj/item/cursed_item, mob/equipper, slot) + SIGNAL_HANDLER + if(cursed_item.slot_flags & slot) + announce(cursed_item, equipper) + +/datum/element/curse_announcement/proc/on_pickup(obj/item/cursed_item, mob/grabber) + SIGNAL_HANDLER + announce(cursed_item, grabber) + +/datum/element/curse_announcement/proc/announce(obj/item/cursed_item, mob/cursed) + //this is from rpgloot, remove the quality suffix to format the name correctly + var/quality_suffix_text + if(fantasy_component) + quality_suffix_text = " [fantasy_component.quality]" + cursed_item.name = replacetext(cursed_item.name, quality_suffix_text,"") + + //modifications to the item so it looks cursed + to_chat(cursed, "[announcement_message]") + cursed_item.add_filter("cursed_item", 9, list("type" = "outline", "color" = filter_color, "size" = 1)) + cursed_item.name = "[cursed_item][new_name]" + + //this is from rpgloot, readd the quality suffix + if(quality_suffix_text) + cursed_item.name += quality_suffix_text + cursed_item.RemoveElement(/datum/element/curse_announcement) + diff --git a/code/datums/elements/cursed.dm b/code/datums/elements/cursed.dm new file mode 100644 index 00000000000..ddb6cae5747 --- /dev/null +++ b/code/datums/elements/cursed.dm @@ -0,0 +1,25 @@ +/** + * # cursed element! + * + *Attaching this element to something will make it float, and get a special ai controller! + */ +/datum/element/cursed + element_flags = ELEMENT_DETACH + +/datum/element/cursed/Attach(datum/target, slot) + . = ..() + if(!isitem(target)) + return COMPONENT_INCOMPATIBLE + var/obj/item/master = target + var/datum/ai_controller/ai = new /datum/ai_controller/cursed(master) + ai.blackboard[BB_TARGET_SLOT] = slot + master.ai_controller = ai + master.AddElement(/datum/element/movetype_handler) + ADD_TRAIT(master, TRAIT_MOVE_FLYING, ELEMENT_TRAIT) + +/datum/element/cursed/Detach(datum/source) + . = ..() + var/obj/item/master = source + QDEL_NULL(master.ai_controller) + REMOVE_TRAIT(master, TRAIT_MOVE_FLYING, ELEMENT_TRAIT) + master.RemoveElement(/datum/element/movetype_handler) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 371bcdcf00f..509094fbe8b 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -583,7 +583,7 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb * Arguments: * * user is mob that equipped it * * slot uses the slot_X defines found in setup.dm for items that can be placed in multiple slots - * * Initial is used to indicate whether or not this is the initial equipment (job datums etc) or just a player doing it + * * initial is used to indicate whether or not this is the initial equipment (job datums etc) or just a player doing it */ /obj/item/proc/equipped(mob/user, slot, initial = FALSE) SHOULD_CALL_PARENT(TRUE) diff --git a/code/game/objects/items/storage/backpack.dm b/code/game/objects/items/storage/backpack.dm index 18845017510..fb07f0c6513 100644 --- a/code/game/objects/items/storage/backpack.dm +++ b/code/game/objects/items/storage/backpack.dm @@ -372,62 +372,10 @@ ///counts time passed since it ate food var/hunger = 0 -/obj/item/storage/backpack/duffelbag/cursed/examine(mob/user) +/obj/item/storage/backpack/duffelbag/cursed/Initialize() . = ..() - - if(hunger > 25) - . += "The bag is growling for food..." - -/obj/item/storage/backpack/duffelbag/cursed/equipped(mob/living/carbon/human/user, slot) - . = ..() - START_PROCESSING(SSobj,src) - ADD_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT) - ADD_TRAIT(user, TRAIT_CLUMSY, CURSED_ITEM_TRAIT) - ADD_TRAIT(user, TRAIT_PACIFISM, CURSED_ITEM_TRAIT) - ADD_TRAIT(user, TRAIT_DUFFEL_CURSED, CURSED_ITEM_TRAIT) - -/obj/item/storage/backpack/duffelbag/cursed/dropped(mob/living/carbon/human/user) - REMOVE_TRAIT(user, TRAIT_DUFFEL_CURSED, CURSED_ITEM_TRAIT) - REMOVE_TRAIT(user, TRAIT_CLUMSY, CURSED_ITEM_TRAIT) - REMOVE_TRAIT(user, TRAIT_PACIFISM, CURSED_ITEM_TRAIT) - STOP_PROCESSING(SSobj,src) - - var/turf/T = get_turf(user) - playsound(T, 'sound/effects/splat.ogg', 50, TRUE) - new /obj/effect/decal/cleanable/vomit(T) - - . = ..() - -/obj/item/storage/backpack/duffelbag/cursed/process() - - var/mob/living/carbon/user = loc - ///check hp - if(obj_integrity == 0) - user.dropItemToGround(src, TRUE) - hunger++ - ///check hunger - if((hunger > 50) && prob(20)) - for(var/obj/item/I in contents) - if(IS_EDIBLE(I)) - var/obj/item/food/hunger_breaks = I //If you fed them poundland microwave meals, it probably would kill them - hunger_breaks.forceMove(user.loc) - playsound(src, 'sound/items/eatfood.ogg', 20, TRUE) - ///poisoned food damages it - if(istype(hunger_breaks, /obj/item/food/badrecipe)) - to_chat(user, "The [name] grumbles!") - obj_integrity -= 50 - else - to_chat(user, "The [name] eats your [hunger_breaks]!") - QDEL_NULL(hunger_breaks) - hunger = 0 - return - ///no food found: it bites you and loses some hp - var/affecting = user.get_bodypart(BODY_ZONE_CHEST) - user.apply_damage(60, BRUTE, affecting) - hunger = initial(hunger) - playsound(src, 'sound/items/eatfood.ogg', 20, TRUE) - to_chat(user, "The [name] eats your back!") - obj_integrity -= 25 + var/add_dropdel = TRUE //clarified boolean + AddComponent(/datum/component/curse_of_hunger, add_dropdel) /obj/item/storage/backpack/duffelbag/captain name = "captain's duffel bag" diff --git a/code/game/objects/items/weaponry.dm b/code/game/objects/items/weaponry.dm index 3eff8542e44..394b6144f58 100644 --- a/code/game/objects/items/weaponry.dm +++ b/code/game/objects/items/weaponry.dm @@ -278,7 +278,7 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301 /obj/item/katana/cursed/Initialize() . = ..() - ADD_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT) + ADD_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT(type)) /obj/item/wirerod name = "wired rod" diff --git a/code/modules/antagonists/fugitive/fugitive_outfits.dm b/code/modules/antagonists/fugitive/fugitive_outfits.dm index a893d2b22f2..286ee2f37de 100644 --- a/code/modules/antagonists/fugitive/fugitive_outfits.dm +++ b/code/modules/antagonists/fugitive/fugitive_outfits.dm @@ -26,29 +26,28 @@ ears = /obj/item/radio/headset glasses = /obj/item/clothing/glasses/regular/circle -/datum/outfit/waldo/post_equip(mob/living/carbon/human/H, visualsOnly=FALSE) +/datum/outfit/waldo/post_equip(mob/living/carbon/human/equipped_on, visualsOnly=FALSE) if(visualsOnly) return - H.fully_replace_character_name(null,"Waldo") - H.eye_color = "000" - H.gender = MALE - H.skin_tone = "caucasian3" - H.hairstyle = "Business Hair 3" - H.facial_hairstyle = "Shaved" - H.hair_color = "000" - H.facial_hair_color = H.hair_color - H.update_body() - if(H.mind) - H.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/knock(null)) + equipped_on.fully_replace_character_name(null,"Waldo") + equipped_on.eye_color = "000" + equipped_on.gender = MALE + equipped_on.skin_tone = "caucasian3" + equipped_on.hairstyle = "Business Hair 3" + equipped_on.facial_hairstyle = "Shaved" + equipped_on.hair_color = "000" + equipped_on.facial_hair_color = equipped_on.hair_color + equipped_on.update_body() + if(equipped_on.mind) + equipped_on.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/knock(null)) var/list/no_drops = list() - no_drops += H.get_item_by_slot(ITEM_SLOT_FEET) - no_drops += H.get_item_by_slot(ITEM_SLOT_ICLOTHING) - no_drops += H.get_item_by_slot(ITEM_SLOT_OCLOTHING) - no_drops += H.get_item_by_slot(ITEM_SLOT_HEAD) - no_drops += H.get_item_by_slot(ITEM_SLOT_EYES) - for(var/i in no_drops) - var/obj/item/I = i - ADD_TRAIT(I, TRAIT_NODROP, CURSED_ITEM_TRAIT) + no_drops += equipped_on.get_item_by_slot(ITEM_SLOT_FEET) + no_drops += equipped_on.get_item_by_slot(ITEM_SLOT_ICLOTHING) + no_drops += equipped_on.get_item_by_slot(ITEM_SLOT_OCLOTHING) + no_drops += equipped_on.get_item_by_slot(ITEM_SLOT_HEAD) + no_drops += equipped_on.get_item_by_slot(ITEM_SLOT_EYES) + for(var/obj/item/trait_needed as anything in no_drops) + ADD_TRAIT(trait_needed, TRAIT_NODROP, CURSED_ITEM_TRAIT(trait_needed.type)) /datum/outfit/synthetic name = "Factory Error Synth" diff --git a/code/modules/events/wizard/curseditems.dm b/code/modules/events/wizard/curseditems.dm index 1eba76744e5..872fc5a5b1f 100644 --- a/code/modules/events/wizard/curseditems.dm +++ b/code/modules/events/wizard/curseditems.dm @@ -51,7 +51,7 @@ var/obj/item/I = new J //dumb but required because of byond throwing a fit anytime new gets too close to a list H.dropItemToGround(H.get_item_by_slot(i), TRUE) H.equip_to_slot_or_del(I, i) - ADD_TRAIT(I, TRAIT_NODROP, CURSED_ITEM_TRAIT) + ADD_TRAIT(I, TRAIT_NODROP, CURSED_ITEM_TRAIT(I)) I.item_flags |= DROPDEL I.name = "cursed " + I.name diff --git a/code/modules/mining/lavaland/necropolis_chests.dm b/code/modules/mining/lavaland/necropolis_chests.dm index fa1358a658c..2b920d14c2d 100644 --- a/code/modules/mining/lavaland/necropolis_chests.dm +++ b/code/modules/mining/lavaland/necropolis_chests.dm @@ -201,7 +201,7 @@ /obj/item/rod_of_asclepius/proc/activated() item_flags = DROPDEL - ADD_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT) + ADD_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT(type)) desc = "A short wooden rod with a mystical snake inseparably gripping itself and the rod to your forearm. It flows with a healing energy that disperses amongst yourself and those around you. " icon_state = "asclepius_active" activated = TRUE diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm index 5a73a27533a..773808dc93a 100644 --- a/code/modules/mob/inventory.dm +++ b/code/modules/mob/inventory.dm @@ -326,6 +326,7 @@ else I.forceMove(newloc) I.dropped(src, silent) + SEND_SIGNAL(I, COMSIG_ITEM_POST_UNEQUIP, force, newloc, no_move, invdrop, silent) return TRUE /** diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm index 067dc2e9240..45968629d12 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm @@ -331,12 +331,12 @@ Difficulty: Extremely Hard /obj/item/clothing/shoes/winterboots/ice_boots/ice_trail/equipped(mob/user, slot) . = ..() if(slot == ITEM_SLOT_FEET) - ADD_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT) + ADD_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT(type)) /obj/item/clothing/shoes/winterboots/ice_boots/ice_trail/dropped(mob/user) . = ..() // Could have been blown off in an explosion from the previous owner - REMOVE_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT) + REMOVE_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT(type)) /obj/item/clothing/shoes/winterboots/ice_boots/ice_trail/ui_action_click(mob/user) on = !on diff --git a/code/modules/spells/spell_types/godhand.dm b/code/modules/spells/spell_types/godhand.dm index 186d6c64c8a..4039a91db51 100644 --- a/code/modules/spells/spell_types/godhand.dm +++ b/code/modules/spells/spell_types/godhand.dm @@ -141,16 +141,17 @@ duffelvictim.apply_damage(80, STAMINA) duffelvictim.Knockdown(5 SECONDS) - if(HAS_TRAIT(target, TRAIT_DUFFEL_CURSED)) + if(HAS_TRAIT(target, TRAIT_DUFFEL_CURSE_PROOF)) to_chat(user, "The burden of [duffelvictim]'s duffel bag becomes too much, shoving them to the floor!") to_chat(duffelvictim, "The weight of this bag becomes overburdening!") return ..() - - var/obj/item/storage/backpack/duffelbag/cursed/conjuredduffel= new get_turf(target) + + var/obj/item/storage/backpack/duffelbag/cursed/conjuredduffel = new get_turf(target) duffelvictim.visible_message("A growling duffel bag appears on [duffelvictim]!", \ "You feel something attaching itself to you, and a strong desire to discuss your [elaborate_backstory] at length!") + ADD_TRAIT(duffelvictim, TRAIT_DUFFEL_CURSE_PROOF, CURSED_ITEM_TRAIT(conjuredduffel.name)) if(duffelvictim.dropItemToGround(duffelvictim.back)) duffelvictim.equip_to_slot_if_possible(conjuredduffel, ITEM_SLOT_BACK, TRUE, TRUE) else diff --git a/tgstation.dme b/tgstation.dme index f39d26fb753..cee4d9778a6 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -476,6 +476,8 @@ #include "code\datums\ai\telegraph_effects.dm" #include "code\datums\ai\bane\bane_behaviors.dm" #include "code\datums\ai\bane\bane_controller.dm" +#include "code\datums\ai\cursed\cursed_behaviors.dm" +#include "code\datums\ai\cursed\cursed_controller.dm" #include "code\datums\ai\dog\dog_behaviors.dm" #include "code\datums\ai\dog\dog_controller.dm" #include "code\datums\ai\hauntium\haunted_controller.dm" @@ -520,6 +522,7 @@ #include "code\datums\components\codeword_hearing.dm" #include "code\datums\components\construction.dm" #include "code\datums\components\creamed.dm" +#include "code\datums\components\curse_of_hunger.dm" #include "code\datums\components\customizable_reagent_holder.dm" #include "code\datums\components\deadchat_control.dm" #include "code\datums\components\dejavu.dm" @@ -704,6 +707,8 @@ #include "code\datums\elements\cleaning.dm" #include "code\datums\elements\climbable.dm" #include "code\datums\elements\connect_loc.dm" +#include "code\datums\elements\curse_announcement.dm" +#include "code\datums\elements\cursed.dm" #include "code\datums\elements\decal.dm" #include "code\datums\elements\deferred_aquarium_content.dm" #include "code\datums\elements\digitalcamo.dm"