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"