diff --git a/code/__DEFINES/dcs/mob_signals.dm b/code/__DEFINES/dcs/mob_signals.dm
index 4a67eefb231..6a7f432a59f 100644
--- a/code/__DEFINES/dcs/mob_signals.dm
+++ b/code/__DEFINES/dcs/mob_signals.dm
@@ -255,6 +255,10 @@
#define COMSIG_MOB_PRE_JAUNT "spell_mob_pre_jaunt"
#define COMPONENT_BLOCK_JAUNT (1<<0)
+/// from handle_ventcrawl(): (mob/living/crawler)
+#define COMSIG_LIVING_TRY_VENTCRAWL "living_try_ventcrawl"
+/// from add_ventcrawler(): (mob/living/crawler)
+#define COMSIG_LIVING_ENTER_VENTCRAWL "living_enter_ventcrawl"
/// from remove_ventcrawler(): (mob/living/crawler)
#define COMSIG_LIVING_EXIT_VENTCRAWL "living_exit_ventcrawl"
diff --git a/code/__DEFINES/gamemode.dm b/code/__DEFINES/gamemode.dm
index 0208c376777..621b7659a16 100644
--- a/code/__DEFINES/gamemode.dm
+++ b/code/__DEFINES/gamemode.dm
@@ -62,6 +62,7 @@
#define SPECIAL_ROLE_XENOMORPH_SENTINEL "Xenomorph Sentinel"
#define SPECIAL_ROLE_XENOMORPH_LARVA "Xenomorph Larva"
#define SPECIAL_ROLE_ZOMBIE "Zombie"
+#define SPECIAL_ROLE_UPLIFTED_PRIMITIVE "Uplifted Primitive"
#define SPECIAL_ROLE_TOURIST "Tourist"
#define SPECIAL_ROLE_HERETIC "Heretic"
#define SPECIAL_ROLE_EVENTMISC "Event Role"
diff --git a/code/__DEFINES/genetics_defines.dm b/code/__DEFINES/genetics_defines.dm
index b04336f6c4a..1e8ec44225a 100644
--- a/code/__DEFINES/genetics_defines.dm
+++ b/code/__DEFINES/genetics_defines.dm
@@ -57,7 +57,8 @@
//Ventcrawling defines
#define VENTCRAWLER_NONE 0
#define VENTCRAWLER_NUDE 1
-#define VENTCRAWLER_ALWAYS 2
+#define VENTCRAWLER_SIGNAL 2
+#define VENTCRAWLER_ALWAYS 3
//Used for calculations for negative effects of having genetics powers
#define DEFAULT_GENE_STABILITY 100
diff --git a/code/__DEFINES/hud.dm b/code/__DEFINES/hud.dm
index 984c62d46d4..c88504a454c 100644
--- a/code/__DEFINES/hud.dm
+++ b/code/__DEFINES/hud.dm
@@ -68,6 +68,7 @@
#define DATA_HUD_HERETIC 27
#define ANTAG_HUD_HERETIC 28
#define ANTAG_HUD_HERETIC_BEAST 29
+#define ANTAG_HUD_UPLIFTED_TEAMLESS 30
// Notification action types
#define NOTIFY_JUMP "jump"
diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm
index 8c86281e709..397a4ad16dd 100644
--- a/code/__DEFINES/role_preferences.dm
+++ b/code/__DEFINES/role_preferences.dm
@@ -46,6 +46,7 @@
#define ROLE_GHOST "ghost role"
#define ROLE_ELITE "lavaland elite"
#define ROLE_NINJA "ninja"
+#define ROLE_UPLIFTED_PRIMITIVE "uplifted primitive"
// Misc jobban categories
#define ROLEBAN_AHUD "AntagHUD"
@@ -73,7 +74,7 @@ GLOBAL_LIST_INIT(special_roles_antags, list(
ROLE_MIND_FLAYER,
ROLE_HERETIC,
ROLE_NINJA,
-
+ ROLE_UPLIFTED_PRIMITIVE,
// UNUSED/BROKEN ANTAGS
// ROLE_HOG_GOD = /datum/game_mode/hand_of_god,
// ROLE_HOG_CULTIST = /datum/game_mode/hand_of_god,
diff --git a/code/_onclick/hud/human_hud.dm b/code/_onclick/hud/human_hud.dm
index 614f6c5e599..298a99a509f 100644
--- a/code/_onclick/hud/human_hud.dm
+++ b/code/_onclick/hud/human_hud.dm
@@ -406,11 +406,7 @@
else
inv.alpha = hud_alpha
for(var/atom/movable/screen/craft/crafting in static_inventory)
- if(!S.can_craft)
- crafting.invisibility = INVISIBILITY_ABSTRACT
- H.handcrafting?.close(H)
- else
- crafting.invisibility = initial(crafting.invisibility)
+ crafting.invisibility = initial(crafting.invisibility)
/datum/hud/human/hidden_inventory_update(mob/viewer)
if(!mymob)
diff --git a/code/datums/ai/monkey/monkey_controller.dm b/code/datums/ai/monkey/monkey_controller.dm
index ef65dd7b0dc..172386a0e8e 100644
--- a/code/datums/ai/monkey/monkey_controller.dm
+++ b/code/datums/ai/monkey/monkey_controller.dm
@@ -64,6 +64,24 @@ have ways of interacting with a specific mob and control it.
)
idle_behavior = /datum/idle_behavior/idle_monkey/pun_pun
+/datum/ai_controller/monkey/uplifted_npc
+ movement_delay = 7 DECISECONDS // uplifted npcs move slower so that they're less annoying
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/generic_resist,
+ /datum/ai_planning_subtree/monkey_combat,
+ )
+
+/datum/ai_controller/monkey/uplifted_npc/retaliate(mob/living/living_mob)
+ var/mob/living/carbon/human/me = pawn
+ var/mob/living/carbon/human/target = living_mob
+
+ if(istype(me) && istype(target) \
+ && target.mind && target.mind.has_antag_datum(/datum/antagonist/uplifted_primitive) \
+ && target.dna.species.type == me.dna.species.type)
+ return
+
+ return ..()
+
/datum/ai_controller/monkey/angry
/datum/ai_controller/monkey/angry/try_possess_pawn(atom/new_pawn)
diff --git a/code/datums/atom_hud.dm b/code/datums/atom_hud.dm
index 319806cdc95..6c7c77eb631 100644
--- a/code/datums/atom_hud.dm
+++ b/code/datums/atom_hud.dm
@@ -32,6 +32,7 @@ GLOBAL_LIST_INIT(huds, alist(
DATA_HUD_HERETIC = new/datum/atom_hud/data/heretic(),
ANTAG_HUD_HERETIC = new/datum/atom_hud/antag/hidden(),
ANTAG_HUD_HERETIC_BEAST = new/datum/atom_hud/antag/hidden(),
+ ANTAG_HUD_UPLIFTED_TEAMLESS = new/datum/atom_hud/antag/hidden/secondary(),
))
/datum/atom_hud
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index a696947cbfc..9dc0dbf6fd5 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -502,6 +502,15 @@
else
. += "NO|zombie|infect"
+/datum/mind/proc/memory_edit_uplifted(mob/living/H)
+ . = _memory_edit_header("uplifted", list())
+ if(has_antag_datum(/datum/antagonist/uplifted_primitive))
+ . += "no|UPLIFTED"
+ else
+ . += "NO|uplifted"
+
+ . += _memory_edit_role_enabled(ROLE_UPLIFTED_PRIMITIVE)
+
/datum/mind/proc/memory_edit_eventmisc(mob/living/H)
. = _memory_edit_header("event", list())
if(has_antag_datum(/datum/antagonist/eventmisc))
@@ -643,6 +652,8 @@
sections["abductor"] = memory_edit_abductor(H)
/** Zombies **/
sections["zombie"] = memory_edit_zombie(H)
+ /** Uplifted Primitives **/
+ sections["uplifted"] = memory_edit_uplifted(H)
sections["eventmisc"] = memory_edit_eventmisc(H)
/** TRAITOR ***/
sections["traitor"] = memory_edit_traitor()
@@ -1426,6 +1437,22 @@
log_admin("[key_name(usr)] has removed the zombie virus from [key_name(current)].")
current.create_log(MISC_LOG, "[key_name(current)] had their zombie virus admin-removed by [key_name_admin(usr)]")
+ else if(href_list["uplifted"])
+ switch(href_list["uplifted"])
+ if("clear")
+ if(!has_antag_datum(/datum/antagonist/uplifted_primitive))
+ return
+ remove_antag_datum(/datum/antagonist/uplifted_primitive)
+ message_admins("[key_name_admin(usr)] has de-uplifted'ed [key_name(current)].")
+ log_admin("[key_name(usr)] has de-uplifted'ed [key_name(current)].")
+ if("uplifted")
+ if(has_antag_datum(/datum/antagonist/uplifted_primitive))
+ return
+ add_antag_datum(/datum/antagonist/uplifted_primitive)
+ message_admins("[key_name_admin(usr)] has uplifted'ed [key_name(current)].")
+ log_admin("[key_name(usr)] has uplifted'ed [key_name(current)].")
+ current.create_log(MISC_LOG, "[key_name(current)] was made into an uplifted primitive by [key_name_admin(usr)]")
+
else if(href_list["traitor"])
switch(href_list["traitor"])
if("clear")
diff --git a/code/game/dna/dna2_domutcheck.dm b/code/game/dna/dna2_domutcheck.dm
index f0363324c20..f45f7c08c21 100644
--- a/code/game/dna/dna2_domutcheck.dm
+++ b/code/game/dna/dna2_domutcheck.dm
@@ -3,6 +3,9 @@
// M: Mob to mess with
// flags: See below, bitfield.
/proc/domutcheck(mob/living/M, flags = 0)
+ if(HAS_TRAIT(M, TRAIT_GENELESS))
+ return
+
for(var/mutation_type in GLOB.dna_mutations)
var/datum/mutation/mutation = GLOB.dna_mutations[mutation_type]
if(!M || !M.dna)
diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm
index 5bfe341dadb..f0257eb902a 100644
--- a/code/game/gamemodes/game_mode.dm
+++ b/code/game/gamemodes/game_mode.dm
@@ -116,6 +116,11 @@
/// A list of all minds that are infected with the zombie virus, but aren't zombies yet
var/list/datum/mind/zombie_infected = list()
+ /// An associative list between a species and all the minds that are uplifted primitive of that species.
+ var/list/datum/mind/uplifted_primitives = alist()
+ /// An associative list between the species of the team and the team's datum.
+ var/list/datum/team/uplifted_primitive/uplifted_teams = alist()
+
/datum/game_mode/proc/announce() //to be calles when round starts
to_chat(world, "Notice: [src] did not define announce()")
diff --git a/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_event.dm b/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_event.dm
new file mode 100644
index 00000000000..d4cfb1fccc6
--- /dev/null
+++ b/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_event.dm
@@ -0,0 +1,60 @@
+/datum/event/spawn_uplifted_primitive
+ name = "Uplifted Primitive Spawn"
+ nominal_severity = EVENT_LEVEL_MAJOR
+
+ /// The list of species this event can spawn uplifted primitives of.
+ var/list/allowed_species = list(
+ /datum/species/monkey,
+ )
+
+/datum/event/spawn_uplifted_primitive/start()
+ INVOKE_ASYNC(src, PROC_REF(make_uplifted_primitive))
+
+/datum/event/spawn_uplifted_primitive/proc/make_uplifted_primitive()
+ // disallow species which have existing players
+ var/list/currently_allowed_species = allowed_species - SSticker.mode.uplifted_primitives
+ if(!length(currently_allowed_species))
+ message_admins("Warning: The uplifted primitives event could not be triggered because all allowed species already have an uplifted team.")
+ kill()
+ return
+ var/datum/species/selected_species = pick(currently_allowed_species)
+
+ var/list/candidates = SSghost_spawns.poll_candidates("Do you want to play as an uplifted primitive?", ROLE_UPLIFTED_PRIMITIVE, TRUE, source = /obj/structure/uplifted_primitive/nest)
+ if(!length(candidates))
+ message_admins("no candidates were found for the uplifted primitive event.")
+ kill()
+ return
+
+ var/mob/C = pick(candidates)
+ var/key = C.key
+
+ if(!key)
+ kill()
+ return
+
+ var/list/vents = get_valid_vent_spawns(exclude_mobs_nearby = TRUE)
+ if(!length(vents))
+ message_admins("Warning: No suitable vents detected for spawning uplifted primitives. Force picking from station vents regardless of state!")
+ vents = get_valid_vent_spawns(unwelded_only = FALSE, min_network_size = 0)
+ if(!length(vents))
+ message_admins("Warning: No vents detected for spawning uplifted primitives at all!")
+ kill()
+ return
+
+ var/obj/selected_vent = pick(vents)
+
+ dust_if_respawnable(C)
+
+ var/datum/mind/player_mind = new /datum/mind(key)
+ var/mob/living/carbon/human/primitive = new(selected_vent, selected_species)
+
+ player_mind.active = TRUE
+ player_mind.transfer_to(primitive)
+ player_mind.add_antag_datum(/datum/antagonist/uplifted_primitive)
+
+ SSticker.mode.uplifted_teams[selected_species].guaranteed_sentient_spawns = 2
+
+ primitive.add_ventcrawl(selected_vent)
+
+ message_admins("[key_name_admin(primitive)] has been made into an uplifted [initial(primitive.name)] by an event.")
+ log_game("[key_name_admin(primitive)] was spawned as an uplifted [initial(primitive.name)] by an event.")
diff --git a/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_items.dm b/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_items.dm
new file mode 100644
index 00000000000..4e2c32f2497
--- /dev/null
+++ b/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_items.dm
@@ -0,0 +1,23 @@
+/obj/item/uplifted
+ icon = 'icons/obj/uplifted_primitive.dmi'
+
+/obj/item/uplifted/nest_bundle
+ name = "bundle of junk"
+ desc = "A loosely put together collection of scrap metal and food items."
+ icon_state = "bundle"
+
+ w_class = WEIGHT_CLASS_SMALL
+
+ var/scrap = 0
+ var/food = 0
+
+/obj/item/uplifted/nest_bundle/Initialize(mapload, new_scrap, new_food)
+ . = ..()
+ scrap = new_scrap
+ food = new_food
+
+/obj/item/uplifted/nest_bundle/examine(mob/user)
+ . = ..()
+ if(user.mind && user.mind.has_antag_datum(/datum/antagonist/uplifted_primitive))
+ . += SPAN_NOTICE("It contains [scrap] units of scrap.")
+ . += SPAN_NOTICE("It contains [food] units of food.")
diff --git a/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_nest.dm b/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_nest.dm
new file mode 100644
index 00000000000..290849cda4a
--- /dev/null
+++ b/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_nest.dm
@@ -0,0 +1,380 @@
+/// The amount of integrity to regenerate in exchange for scrap every process().
+#define INTEGRITY_REGEN_AMOUNT 5
+/// The amount of scrap to consume to regenerate every process().
+#define INTEGRITY_REGEN_SCRAP_COST 2000
+/// The amount of scrap to consume when spawning a new mobs.
+#define SPAWN_SCRAP_COST 5000
+/// The amount of food to consume when spawning a new mobs.
+#define SPAWN_FOOD_COST 20
+/// The minimum amount of time to wait between spawning new mobs.
+#define SPAWN_COOLDOWN_TIME (120 SECONDS)
+/// The number of percentage points to increase the probability of the next spawned mob being sentient by.
+#define SPAWN_SENTIENT_PROB_INCREASE 25
+/// The maximum number the spawn count can reach before disabling NPC spawning.
+#define SPAWN_MAX_LIMIT 2
+/// The number of additional mobs permitted by each extra nearby nest.
+#define SPAWN_EXTRA_NEST_MOBS 1
+
+/obj/structure/uplifted_primitive
+ icon = 'icons/obj/uplifted_primitive.dmi'
+ anchored = TRUE
+
+/obj/structure/uplifted_primitive/nest
+ icon_state = "nest"
+ desc = "A home for some primitive form of sentient being."
+
+ max_integrity = 200
+
+ /// The species of lesser human to spawn.
+ var/datum/species/nest_species
+
+ /// The amount of scrap (recycled materials) currently stored in the nest.
+ var/available_scrap = 0
+
+ /// The amount of food currently stored in the nest.
+ var/available_food = 0
+
+ /// The cooldown between successfully spawning new mobs.
+ COOLDOWN_DECLARE(spawn_cooldown)
+
+ /// The probability that the next spawned mob will be sentient.
+ var/sentient_probability = 5
+
+ /// The spawner used to let ghosts spawn without a roll. If null, the nest is not in on demand mode.
+ var/obj/effect/mob_spawn/uplifted_primitive/on_demand_spawner
+
+ /// The mapping between the path of a crafting output and a list containing
+ /// its scrap and food costs, and an optional argument to be passed to new().
+ var/list/crafting_items = alist(
+ /obj/item/stack/sheet/wood = list(5000, 0, 5),
+ /obj/item/stack/tile/grass = list(5000, 2, 10),
+ /obj/item/stack/medical/bruise_pack/comfrey = list(2000, 10, 3),
+ /obj/item/stack/medical/ointment/aloe = list(2000, 10, 3),
+ /obj/item/grown/bananapeel = list(0, 3),
+ )
+ var/list/crafting_choices_cache
+ var/list/crafting_mapping_cache
+
+/obj/structure/uplifted_primitive/nest/Initialize(mapload, datum/species/species = /datum/species/monkey)
+ . = ..(mapload)
+ AddElement(/datum/element/relay_attackers)
+ RegisterSignal(src, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(rally_retaliation))
+ START_PROCESSING(SSobj, src)
+ COOLDOWN_START(src, spawn_cooldown, SPAWN_COOLDOWN_TIME)
+ nest_species = species
+
+/obj/structure/uplifted_primitive/nest/Destroy()
+ QDEL_NULL(on_demand_spawner)
+ STOP_PROCESSING(SSobj, src)
+ UnregisterSignal(src, COMSIG_ATOM_WAS_ATTACKED)
+ RemoveElement(/datum/element/relay_attackers)
+ return ..()
+
+/obj/structure/uplifted_primitive/nest/deconstruct(disassembled)
+ if(available_scrap > 0 || available_food > 0)
+ new /obj/item/uplifted/nest_bundle(get_turf(src), available_scrap, available_food)
+
+ return ..()
+
+/obj/structure/uplifted_primitive/nest/examine(mob/user)
+ . = ..()
+ . += SPAN_NOTICE("It appears to have been built by [lowertext(nest_species::name_plural)].")
+
+ var/datum/antagonist/uplifted_primitive/antag = user.mind?.has_antag_datum(/datum/antagonist/uplifted_primitive)
+ if((antag && user.dna?.species.type == nest_species) || isobserver(user))
+ . += SPAN_NOTICE("It contains [available_scrap] units of scrap.")
+ . += SPAN_NOTICE("It contains [available_food] units of food.")
+ . += SPAN_NOTICE("It needs at least [SPAWN_SCRAP_COST] scrap and [SPAWN_FOOD_COST] food to produce another primitive.")
+ . += SPAN_NOTICE("A new primitive can emerge [COOLDOWN_FINISHED(src, spawn_cooldown) \
+ ? "soon" \
+ : "in [round(COOLDOWN_TIMELEFT(src, spawn_cooldown) / (1 SECONDS))] seconds"].")
+
+ . += SPAN_NOTICE("Control-Click to open the crafting radial.")
+ if(locateUID(antag?.nest_uid) == src)
+ . += SPAN_NOTICE("Alt-Click to deconstruct the nest.")
+
+ if(isobserver(user))
+ . += SPAN_NOTICE("Click to spawn as a primitive when this nest is ready.")
+
+/obj/structure/uplifted_primitive/nest/process()
+ if(obj_integrity < max_integrity && available_scrap >= INTEGRITY_REGEN_SCRAP_COST)
+ available_scrap -= INTEGRITY_REGEN_SCRAP_COST
+ obj_integrity = min(obj_integrity + INTEGRITY_REGEN_AMOUNT, max_integrity)
+
+ if(!on_demand_spawner && available_scrap >= SPAWN_SCRAP_COST && available_food >= SPAWN_FOOD_COST && COOLDOWN_FINISHED(src, spawn_cooldown))
+ COOLDOWN_START(src, spawn_cooldown, SPAWN_COOLDOWN_TIME)
+
+ var/rolled = roll_sentience()
+ if(rolled && try_spawn_uplifted())
+ available_scrap -= SPAWN_SCRAP_COST
+ available_food -= SPAWN_FOOD_COST
+ sentient_probability = initial(sentient_probability)
+
+ if(has_guaranteed_spawn())
+ SSticker.mode.uplifted_teams[nest_species].guaranteed_sentient_spawns -= 1
+ else
+ if(count_spawn_limit() < SPAWN_MAX_LIMIT)
+ spawn_npc()
+ available_scrap -= SPAWN_SCRAP_COST
+ available_food -= SPAWN_FOOD_COST
+ else if(rolled)
+ on_demand_spawner = new(src)
+ // accumulating probability ensures a sentient spawn occurs in a finite number of iterations
+ sentient_probability = min(sentient_probability + SPAWN_SENTIENT_PROB_INCREASE, 100)
+
+/obj/structure/uplifted_primitive/nest/proc/roll_sentience()
+ return has_guaranteed_spawn() || prob(sentient_probability)
+
+/obj/structure/uplifted_primitive/nest/proc/has_guaranteed_spawn()
+ var/datum/team/uplifted_primitive/team = SSticker.mode.uplifted_teams[nest_species]
+ return team?.guaranteed_sentient_spawns > 0
+
+/obj/structure/uplifted_primitive/nest/proc/spawn_npc()
+ if(QDELETED(src))
+ return
+ var/mob/living/carbon/human/primitive = new(get_turf(src), nest_species)
+ primitive.ai_controller = new /datum/ai_controller/monkey/uplifted_npc(primitive)
+ visible_message(SPAN_NOTICE("[primitive.name] emerges out of the nest!"))
+
+/// Returns TRUE if a new player mob was successfully spawned, otherwise FALSE.
+/obj/structure/uplifted_primitive/nest/proc/try_spawn_uplifted()
+ var/list/candidates = SSghost_spawns.poll_candidates("Do you want to play as an uplifted primitive?", ROLE_UPLIFTED_PRIMITIVE, TRUE, source = /obj/structure/uplifted_primitive/nest)
+ if(!length(candidates) || QDELETED(src))
+ return FALSE
+
+ var/mob/C = pick(candidates)
+ return incarnate_uplifted_mob(C)
+
+/obj/structure/uplifted_primitive/nest/proc/incarnate_uplifted_mob(mob/target)
+ var/key = target.key
+ if(!key)
+ return FALSE
+
+ dust_if_respawnable(target)
+
+ var/datum/mind/player_mind = new /datum/mind(key)
+ var/mob/living/carbon/human/primitive = new(get_turf(src), nest_species)
+
+ player_mind.active = TRUE
+ player_mind.transfer_to(primitive)
+ player_mind.add_antag_datum(/datum/antagonist/uplifted_primitive)
+
+ visible_message(SPAN_NOTICE("[primitive.name] emerges out of the nest with a curious look!"))
+ return TRUE
+
+/// Returns the number of mobs currently contributing to the spawn limit.
+/// Currently, any NPC human with the same species as the nest and a monkey ai controller within range is counted.
+/obj/structure/uplifted_primitive/nest/proc/count_spawn_limit()
+ var/count = 0
+
+ for(var/atom/movable/A in orange(src, 7))
+ var/mob/living/carbon/human/H = A
+ if(istype(H))
+ if(H.dna.species.type != nest_species || H.mind)
+ continue
+ if(!istype(H.ai_controller, /datum/ai_controller/monkey/uplifted_npc))
+ continue
+ count += 1
+
+ var/obj/structure/uplifted_primitive/nest/N = A
+ if(istype(N))
+ if(N.nest_species != nest_species)
+ continue
+ count -= SPAWN_EXTRA_NEST_MOBS
+
+ return count
+
+/obj/structure/uplifted_primitive/nest/attack_ghost(mob/dead/observer/user)
+ if(..() || !istype(user))
+ return TRUE
+
+ if(!SSghost_spawns.is_eligible(user, role_text = ROLE_UPLIFTED_PRIMITIVE, check_antaghud = TRUE))
+ to_chat(user, SPAN_WARNING("You are not eligible to spawn as an uplifted primitive!"))
+ return TRUE
+
+ if(on_demand_spawner \
+ && available_scrap >= SPAWN_SCRAP_COST \
+ && available_food >= SPAWN_FOOD_COST \
+ && COOLDOWN_FINISHED(src, spawn_cooldown))
+ if(tgui_alert(user, "Are you sure you want to spawn as an uplifted primitive?", "Are you sure?", list("Yes", "No")) != "Yes")
+ return
+ incarnate_uplifted_mob(user)
+ available_scrap -= SPAWN_SCRAP_COST
+ available_food -= SPAWN_FOOD_COST
+ sentient_probability = initial(sentient_probability)
+ QDEL_NULL(on_demand_spawner)
+ COOLDOWN_START(src, spawn_cooldown, SPAWN_COOLDOWN_TIME)
+ else
+ to_chat(user, SPAN_WARNING("This nest is not ready to spawn another primitive!"))
+
+/obj/structure/uplifted_primitive/nest/AltClick(mob/user, modifiers)
+ var/datum/antagonist/uplifted_primitive/antag = user.mind.has_antag_datum(/datum/antagonist/uplifted_primitive)
+
+ if(!antag || locateUID(antag.nest_uid) != src)
+ return ..()
+
+ to_chat(user, SPAN_NOTICE("You start disassembling the nest."))
+ if(!do_after(user, 7 SECONDS, target = src))
+ return
+
+ to_chat(user, SPAN_NOTICE("You finish disassembling the nest and collect the remaining scrap and food."))
+ deconstruct(TRUE)
+
+/obj/structure/uplifted_primitive/nest/CtrlClick(mob/user, modifiers)
+ if(!user.mind?.has_antag_datum(/datum/antagonist/uplifted_primitive))
+ return ..()
+
+ if(user.dna?.species.type != nest_species)
+ return ..()
+
+ show_crafting_radial(user)
+
+/obj/structure/uplifted_primitive/nest/proc/show_crafting_radial(mob/user)
+ if(!crafting_choices_cache || !crafting_mapping_cache)
+ build_crafting_caches()
+
+ var/raw_choice = show_radial_menu(user, src, crafting_choices_cache, radius = 48, require_near = TRUE)
+ if(!raw_choice)
+ return
+ var/path = crafting_mapping_cache[raw_choice]
+ var/list/data = crafting_items[path]
+ var/scrap_cost = data[1]
+ var/food_cost = data[2]
+
+ if(available_scrap < scrap_cost || available_food < food_cost)
+ to_chat(user, SPAN_WARNING("The nest does not have enough resources to make this!"))
+ return
+
+ if(!do_after(user, 5 SECONDS, target = src))
+ return
+
+ available_scrap -= scrap_cost
+ available_food -= food_cost
+
+ if(data.len >= 3)
+ new path(get_turf(src), data[3])
+ else
+ new path(get_turf(src))
+
+/obj/structure/uplifted_primitive/nest/proc/build_crafting_caches()
+ var/choices = list()
+ var/mapping = list()
+ for(var/path in crafting_items)
+ var/list/data = crafting_items[path]
+ var/list/costs = list()
+
+ if(data[1] > 0)
+ costs += "[data[1]] scrap"
+ if(data[2] > 0)
+ costs += "[data[2]] food"
+
+ var/key = "[path:name] ([costs.Join(", ")])"
+ choices[key] = image(icon = path:icon, icon_state = path:icon_state)
+ mapping[key] = path
+ crafting_choices_cache = choices
+ crafting_mapping_cache = mapping
+
+/obj/structure/uplifted_primitive/nest/item_interaction(mob/living/user, obj/item/used, list/modifiers)
+ if(!user.mind || !user.mind.has_antag_datum(/datum/antagonist/uplifted_primitive))
+ return NONE
+
+ // mobs of other uplifted species should be able to interact normally
+ if(user.dna.species.type != nest_species)
+ return NONE
+
+ if(user.a_intent != INTENT_HELP)
+ return NONE
+
+ if(used.resistance_flags & INDESTRUCTIBLE)
+ to_chat(user, SPAN_WARNING("You don't think it's a good idea to put [used] in the nest."))
+ return ITEM_INTERACT_COMPLETE
+
+ var/potential_scrap = 0
+ var/potential_food = 0
+ if(used.materials && length(used.materials) > 0)
+ for(var/mat in used.materials)
+ potential_scrap += used.materials[mat]
+
+ if(used.reagents)
+ for(var/datum/reagent/consumable/C in used.reagents.reagent_list)
+ if(nest_species.dietflags & C.diet_flags)
+ potential_food += C.nutriment_factor
+
+ var/obj/item/uplifted/nest_bundle/scrap_ball = used
+ if(istype(scrap_ball))
+ potential_scrap += scrap_ball.scrap
+ potential_food += scrap_ball.food
+
+ var/obj/item/stack/pile = used
+ var/multiplier = istype(pile) ? pile.amount : 1
+
+ if(potential_scrap == 0 && potential_food == 0)
+ to_chat(user, SPAN_WARNING("[used] does not seem suitable to disassemble or place in the nest."))
+ return ITEM_INTERACT_COMPLETE
+
+ user.visible_message(
+ SPAN_WARNING("[user] starts finding a place to put [used] in the nest."),
+ SPAN_NOTICE("You start finding a place to put [used] in the nest.")
+ )
+
+ if(!do_after(user, multiplier SECONDS, target = src))
+ return ITEM_INTERACT_COMPLETE
+
+ if(potential_scrap > 0)
+ to_chat(user, SPAN_NOTICE("You dismantle [used] and place the scrap around the nest."))
+ available_scrap += potential_scrap * multiplier
+
+ if(potential_food > 0)
+ to_chat(user, SPAN_NOTICE("You place the edible parts of [used] in the nest."))
+ available_food += potential_food * multiplier
+
+ qdel(used)
+
+ return ITEM_INTERACT_COMPLETE
+
+/obj/structure/uplifted_primitive/nest/play_attack_sound(damage_amount, damage_type, damage_flag)
+ if(!damage_amount)
+ return
+
+ switch(damage_type)
+ if(BRUTE)
+ playsound(loc, 'sound/weapons/slash.ogg', 80, TRUE)
+ if(BURN)
+ playsound(loc, 'sound/items/welder.ogg', 80, TRUE)
+
+/obj/structure/uplifted_primitive/nest/proc/rally_retaliation(atom/source, atom/attacker, flags)
+ SIGNAL_HANDLER
+ if(!istype(attacker, /mob/living) || !(flags & ATTACKER_DAMAGING_ATTACK))
+ return
+ for(var/mob/living/carbon/human/defender in oview(src, 7))
+ if(defender.mind || defender.dna.species.type != nest_species)
+ continue
+ if(defender == attacker) // Do not commit suicide attacking yourself
+ continue
+
+ var/datum/ai_controller/monkey/uplifted_npc/M = defender.ai_controller
+ if(istype(M))
+ M.retaliate(attacker)
+
+/obj/effect/mob_spawn/uplifted_primitive
+ name = "Uplifted Primitive"
+ description = "You are a primitive being who has awoken with sentience. Survive on station with your fellow primitives and complete objectives."
+ roundstart = FALSE
+
+/obj/effect/mob_spawn/uplifted_primitive/Initialize(mapload)
+ var/obj/structure/uplifted_primitive/nest/N = loc
+ if(istype(N))
+ name = "Uplifted [N.nest_species::name]"
+ return ..()
+
+/obj/effect/mob_spawn/uplifted_primitive/attack_ghost(mob/user)
+ return loc.attack_ghost(user)
+
+#undef INTEGRITY_REGEN_AMOUNT
+#undef INTEGRITY_REGEN_SCRAP_COST
+#undef SPAWN_SCRAP_COST
+#undef SPAWN_FOOD_COST
+#undef SPAWN_COOLDOWN_TIME
+#undef SPAWN_SENTIENT_PROB_INCREASE
+#undef SPAWN_MAX_LIMIT
+#undef SPAWN_EXTRA_NEST_MOBS
diff --git a/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_objectives.dm b/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_objectives.dm
new file mode 100644
index 00000000000..59f1ba6bfd3
--- /dev/null
+++ b/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_objectives.dm
@@ -0,0 +1,97 @@
+/datum/objective/uplifted
+ needs_target = FALSE
+
+/datum/objective/uplifted/propagate
+ explanation_text = "Propagate your kind across the station by filling your nest with food and scrap."
+ completed = TRUE
+
+/datum/objective/uplifted/collect_items
+ explanation_text = "Decorate your nest with whatever interesting items you find around the station."
+ completed = TRUE
+
+/datum/objective/uplifted/collect_animals
+ explanation_text = "Find creatures who do not have the gift of sentience and keep them safe near your nest."
+ completed = TRUE
+
+/datum/objective/uplifted/barter
+ explanation_text = "Trade whatever you can find or steal with anyone willing to give you appropriate payment."
+ completed = TRUE
+
+/datum/objective/uplifted/fortify
+ explanation_text = "Fortify your nest against potential attackers and arm your fellow primitives."
+ completed = TRUE
+
+/datum/objective/uplifted/expand
+ explanation_text = "Expand your territory into nearby rooms and areas to fit your species' growing needs."
+ completed = TRUE
+
+/datum/objective/uplifted/build_nest_in_area
+ /// The area in which the nest must be built to complete the objective.
+ var/area/target_area
+
+/datum/objective/uplifted/build_nest_in_area/New()
+ ..()
+ target_area = select_target_location()
+ update_explanation_text()
+
+/datum/objective/uplifted/build_nest_in_area/update_explanation_text()
+ explanation_text = "Build and defend a collective nest in [target_area.name]."
+
+/datum/objective/uplifted/build_nest_in_area/proc/select_target_location()
+ var/list/potential_areas = list()
+ for(var/area/station/A in SSmapping.existing_station_areas)
+ // using the cult summon validity as a heuristic here should be fine,
+ // since the goal here is similar
+ if(!A.valid_territory)
+ continue
+
+ potential_areas += A
+ return pick(potential_areas)
+
+/datum/objective/uplifted/build_nest_in_area/check_completion()
+ if(completed)
+ return TRUE
+ for(var/datum/mind/M in get_owners())
+ var/datum/antagonist/uplifted_primitive/U = M.has_antag_datum(/datum/antagonist/uplifted_primitive)
+ var/obj/nest = locateUID(U.nest_uid)
+ if(!QDELETED(nest) && get_area(nest) == target_area)
+ return TRUE
+ return FALSE
+
+/datum/objective/uplifted/obtain
+ /// The typepath of the item which must be possessed to complete the objective.
+ var/obj/item/target_type
+
+ /// The possible typepaths that can be selected as a target.
+ var/list/allowed_targets = list(
+ /obj/item/clothing/head/helmet,
+ /obj/item/melee/baton/cattleprod,
+ /obj/item/fireaxe,
+ /obj/item/gun/energy/laser/practice,
+ /obj/item/clothing/gloves/color/yellow,
+ /obj/item/camera,
+ /obj/item/aicard,
+ /obj/item/pda,
+ /obj/item/megaphone,
+ )
+
+/datum/objective/uplifted/obtain/New()
+ ..()
+ target_type = pick(allowed_targets)
+ update_explanation_text()
+
+/datum/objective/uplifted/obtain/update_explanation_text()
+ explanation_text = "Obtain [target_type::name] through barter, theft, craftiness, or any other means."
+
+/datum/objective/uplifted/obtain/check_completion()
+ if(completed)
+ return TRUE
+ for(var/datum/mind/M in get_owners())
+ if(locate(target_type) in M.current.contents)
+ return TRUE
+
+ var/datum/antagonist/uplifted_primitive/U = M.has_antag_datum(/datum/antagonist/uplifted_primitive)
+ var/obj/nest = locateUID(U.nest_uid)
+ if(!QDELETED(nest) && locate(target_type) in get_turf(nest))
+ return TRUE
+ return FALSE
diff --git a/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_spells.dm b/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_spells.dm
new file mode 100644
index 00000000000..928d224a5e7
--- /dev/null
+++ b/code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_spells.dm
@@ -0,0 +1,47 @@
+/datum/spell/uplifted_make_nest
+ name = "Make Nest"
+ desc = "Build a nest at your current location."
+
+ action_icon = 'icons/obj/uplifted_primitive.dmi'
+ action_icon_state = "nest"
+ action_background_icon_state = "bg_default"
+
+ base_cooldown = 15 SECONDS
+ clothes_req = FALSE
+ antimagic_flags = NONE
+
+/datum/spell/uplifted_make_nest/create_new_targeting()
+ return new /datum/spell_targeting/self
+
+/datum/spell/uplifted_make_nest/cast(list/targets, mob/user)
+ var/datum/antagonist/uplifted_primitive/U = user.mind.has_antag_datum(/datum/antagonist/uplifted_primitive)
+
+ user.visible_message(SPAN_NOTICE("[user] starts building a nest!"), SPAN_NOTICE("You start building a nest!"))
+
+ if(!do_after(user, 10 SECONDS, target = user))
+ return
+
+ var/obj/structure/uplifted_primitive/nest/nest = new(get_turf(user), U.initial_species)
+ U.nest_uid = nest.UID()
+
+/datum/spell/uplifted_make_nest/can_cast(mob/user, charge_check, show_message)
+ if(!..())
+ return FALSE
+
+ var/datum/antagonist/uplifted_primitive/U = user.mind.has_antag_datum(/datum/antagonist/uplifted_primitive)
+
+ if(!U)
+ return FALSE
+
+ var/obj/structure/uplifted_primitive/nest/nest = locateUID(U.nest_uid)
+ if(!QDELETED(nest))
+ if(show_message)
+ to_chat(user, SPAN_WARNING("You already have a nest!"))
+ return FALSE
+
+ if(!isturf(user.loc))
+ if(show_message)
+ to_chat(user, SPAN_WARNING("You cannot make a nest here!"))
+ return FALSE
+
+ return TRUE
diff --git a/code/game/jobs/job_exp.dm b/code/game/jobs/job_exp.dm
index ba3507e9da3..66f05b70858 100644
--- a/code/game/jobs/job_exp.dm
+++ b/code/game/jobs/job_exp.dm
@@ -30,6 +30,7 @@ GLOBAL_LIST_INIT(role_playtime_requirements, list(
ROLE_CULTIST = 20,
ROLE_ALIEN = 10,
ROLE_ABDUCTOR = 20,
+ ROLE_UPLIFTED_PRIMITIVE = 10,
))
// Admin Verbs
diff --git a/code/game/objects/items/stacks/medical_packs.dm b/code/game/objects/items/stacks/medical_packs.dm
index 3a329bc4d16..2570899be29 100644
--- a/code/game/objects/items/stacks/medical_packs.dm
+++ b/code/game/objects/items/stacks/medical_packs.dm
@@ -26,10 +26,6 @@
to_chat(user, SPAN_DANGER("[src] cannot be applied to [M]!"))
return FALSE
- if(!user.IsAdvancedToolUser())
- to_chat(user, SPAN_DANGER("You don't have the dexterity to do this!"))
- return FALSE
-
if(ishuman(M))
var/mob/living/carbon/human/H = M
var/obj/item/organ/external/affecting = H.get_organ(user.zone_selected)
diff --git a/code/modules/admin/player_panel.dm b/code/modules/admin/player_panel.dm
index 7791b69c376..862a0a66cf9 100644
--- a/code/modules/admin/player_panel.dm
+++ b/code/modules/admin/player_panel.dm
@@ -485,6 +485,11 @@
if(length(SSticker.mode.zombie_infected))
dat += check_role_table_mob("Pre-zombie infected", SSticker.mode.zombie_infected)
+ if(length(SSticker.mode.uplifted_primitives))
+ for(var/datum/species/species_path in SSticker.mode.uplifted_primitives)
+ var/minds = SSticker.mode.uplifted_primitives[species_path]
+ dat += check_role_table("Uplifted Primitives ([species_path::name])", minds)
+
if(length(GLOB.ts_spiderlist))
var/list/spider_minds = list()
for(var/mob/living/simple_animal/hostile/poison/terror_spider/S in GLOB.ts_spiderlist)
diff --git a/code/modules/antagonists/_common/antag_hud.dm b/code/modules/antagonists/_common/antag_hud.dm
index b4428723b92..35293ef2dd2 100644
--- a/code/modules/antagonists/_common/antag_hud.dm
+++ b/code/modules/antagonists/_common/antag_hud.dm
@@ -26,6 +26,18 @@
if(M.mind)
M.mind.antag_hud = null
+/datum/atom_hud/antag/hidden/secondary/join_hud(mob/M, slave)
+ if(!istype(M))
+ CRASH("join_hud(): [M] ([M.type]) is not a mob!")
+ add_to_hud(M)
+ if(self_visible)
+ add_hud_to(M)
+
+/datum/atom_hud/antag/hidden/secondary/leave_hud(mob/M)
+ if(!istype(M))
+ CRASH("leave_hud(): [M] ([M.type]) is not a mob!")
+ remove_from_hud(M)
+ remove_hud_from(M)
//GAME_MODE PROCS
//called to set a mob's antag icon state
diff --git a/code/modules/antagonists/uplifted_primitive/datum_uplifted_primitive.dm b/code/modules/antagonists/uplifted_primitive/datum_uplifted_primitive.dm
new file mode 100644
index 00000000000..aad620392b6
--- /dev/null
+++ b/code/modules/antagonists/uplifted_primitive/datum_uplifted_primitive.dm
@@ -0,0 +1,160 @@
+RESTRICT_TYPE(/datum/antagonist/uplifted_primitive)
+
+/datum/antagonist/uplifted_primitive
+ name = "Uplifted Primitive"
+ job_rank = ROLE_UPLIFTED_PRIMITIVE
+ special_role = SPECIAL_ROLE_UPLIFTED_PRIMITIVE
+ antag_hud_name = "huduplifted"
+ antag_hud_type = ANTAG_HUD_UPLIFTED_TEAMLESS
+ // TODO: wiki page
+ //wiki_page_name = "Uplifted_Primitive"
+
+ /// The UID of the nest built by the owner.
+ var/nest_uid
+
+ /// The species of the owner at the time this datum was added.
+ var/datum/species/initial_species
+
+ /// The list of objective types which can be selected from when picking a personal objective.
+ var/list/potential_objectives = list(
+ /datum/objective/uplifted/collect_items,
+ /datum/objective/uplifted/collect_animals,
+ /datum/objective/uplifted/barter,
+ /datum/objective/uplifted/fortify,
+ /datum/objective/uplifted/obtain,
+ )
+
+/datum/antagonist/uplifted_primitive/greet()
+ . = ..()
+ . += SPAN_BOLDNOTICE("Despite your primitive form, you have enlightened with sentience.")
+ . += SPAN_BOLDNOTICE("With this newfound awareness you are able to spread your species by building a nest and supplying it with food and scrap metal.")
+ . += SPAN_BOLDNOTICE("By propagating your kind and working with your fellow primitives, you may finally be able to defend yourself from the crew of the station.")
+ . += SPAN_BOLDNOTICE("Alternatively, you could try to keep a friendly relationship with the crew, though they may see you as a threat not worth keeping alive.")
+ . += SPAN_BOLDNOTICE("Regardless of the method, you must help your species to survive and prosper on the station.")
+
+/datum/antagonist/uplifted_primitive/give_objectives()
+ add_antag_objective(pick_n_take(potential_objectives))
+ add_antag_objective(pick_n_take(potential_objectives))
+
+/datum/antagonist/uplifted_primitive/apply_innate_effects(mob/living/mob_override)
+ var/mob/living/carbon/human/H = ..()
+ if(!istype(H))
+ return
+
+ if(H.dna.species.is_small)
+ ADD_TRAIT(H, TRAIT_GENELESS, UNIQUE_TRAIT_SOURCE(src))
+
+ H.ventcrawler = VENTCRAWLER_SIGNAL
+ RegisterSignal(H, COMSIG_LIVING_TRY_VENTCRAWL, PROC_REF(attempt_ventcrawl))
+ RegisterSignal(H, COMSIG_LIVING_ENTER_VENTCRAWL, PROC_REF(apply_ventcrawl_effects))
+ RegisterSignal(H, COMSIG_LIVING_EXIT_VENTCRAWL, PROC_REF(remove_ventcrawl_effects))
+
+ owner.AddSpell(new /datum/spell/uplifted_make_nest)
+
+/datum/antagonist/uplifted_primitive/remove_innate_effects(mob/living/mob_override)
+ var/mob/living/carbon/human/H = ..()
+ if(!istype(H))
+ return
+
+ REMOVE_TRAIT(H, TRAIT_GENELESS, UNIQUE_TRAIT_SOURCE(src))
+
+ H.ventcrawler = initial(H.ventcrawler)
+ UnregisterSignal(H, COMSIG_LIVING_TRY_VENTCRAWL)
+ UnregisterSignal(H, COMSIG_LIVING_ENTER_VENTCRAWL)
+ UnregisterSignal(H, COMSIG_LIVING_EXIT_VENTCRAWL)
+
+ owner.RemoveSpell(/datum/spell/uplifted_make_nest)
+
+/datum/antagonist/uplifted_primitive/proc/attempt_ventcrawl()
+ SIGNAL_HANDLER
+ var/mob/living/L = owner.current
+
+ for(var/obj/item/I in L.contents)
+ if(istype(I, /obj/item/bio_chip))
+ continue
+ if(istype(I, /obj/item/reagent_containers/patch))
+ continue
+ if(I.flags & ABSTRACT)
+ continue
+ if(I.w_class > WEIGHT_CLASS_NORMAL)
+ to_chat(L, SPAN_WARNING("You cannot crawl into a vent with large items!"))
+ return FALSE
+
+ return TRUE
+
+/datum/antagonist/uplifted_primitive/proc/apply_ventcrawl_effects()
+ SIGNAL_HANDLER
+ var/mob/living/L = owner.current
+ ADD_TRAIT(L, TRAIT_RESISTLOWPRESSURE, UNIQUE_TRAIT_SOURCE(src))
+ ADD_TRAIT(L, TRAIT_RESISTHIGHPRESSURE, UNIQUE_TRAIT_SOURCE(src))
+
+/datum/antagonist/uplifted_primitive/proc/remove_ventcrawl_effects()
+ SIGNAL_HANDLER
+ var/mob/living/L = owner.current
+ REMOVE_TRAIT(L, TRAIT_RESISTLOWPRESSURE, UNIQUE_TRAIT_SOURCE(src))
+ REMOVE_TRAIT(L, TRAIT_RESISTHIGHPRESSURE, UNIQUE_TRAIT_SOURCE(src))
+
+/datum/antagonist/uplifted_primitive/add_antag_hud(mob/living/antag_mob)
+ var/datum/team/uplifted_primitive/team = get_team()
+ var/datum/atom_hud/antag/global_hud = GLOB.huds[antag_hud_type]
+
+ global_hud.join_hud(antag_mob)
+ if(team)
+ team.team_hud.join_hud(antag_mob)
+
+ set_antag_hud(antag_mob, antag_hud_name)
+
+/datum/antagonist/uplifted_primitive/remove_antag_hud(mob/living/antag_mob)
+ var/datum/team/uplifted_primitive/team = get_team()
+ var/datum/atom_hud/antag/global_hud = GLOB.huds[antag_hud_type]
+
+ global_hud.leave_hud(antag_mob)
+ if(team)
+ team.team_hud.leave_hud(antag_mob)
+
+ set_antag_hud(antag_mob, null)
+
+/datum/antagonist/uplifted_primitive/create_team(datum/team/team)
+ var/datum/species/owner_species = get_owner_species()
+ if(!owner_species)
+ return null
+
+ var/datum/team/uplifted_primitive/existing = SSticker.mode.uplifted_teams[owner_species]
+ if(!existing)
+ existing = new /datum/team/uplifted_primitive(new_species = owner_species)
+ return existing
+
+/datum/antagonist/uplifted_primitive/get_team()
+ var/datum/species/owner_species = get_owner_species()
+ if(!owner_species)
+ return null
+
+ return SSticker.mode.uplifted_teams[owner_species]
+
+/datum/antagonist/uplifted_primitive/add_owner_to_gamemode()
+ var/datum/species/owner_species = get_owner_species()
+
+ var/list/minds = SSticker.mode.uplifted_primitives[owner_species]
+ if(!minds)
+ minds = list()
+ SSticker.mode.uplifted_primitives[owner_species] = minds
+ minds |= owner
+
+/datum/antagonist/uplifted_primitive/remove_owner_from_gamemode()
+ var/datum/species/owner_species = get_owner_species()
+
+ var/list/minds = SSticker.mode.uplifted_primitives[owner_species]
+ if(minds)
+ minds -= owner
+ if(!length(minds))
+ SSticker.mode.uplifted_primitives[owner_species] = null
+ SSticker.mode.uplifted_primitives -= owner_species
+
+/datum/antagonist/uplifted_primitive/proc/get_owner_species()
+ if(initial_species == null)
+ var/mob/living/carbon/human/H = owner.current
+ if(istype(H))
+ initial_species = H.dna.species.type
+ else
+ initial_species = FALSE
+ return initial_species
diff --git a/code/modules/antagonists/uplifted_primitive/team_uplifted_primitive.dm b/code/modules/antagonists/uplifted_primitive/team_uplifted_primitive.dm
new file mode 100644
index 00000000000..a8638d025bc
--- /dev/null
+++ b/code/modules/antagonists/uplifted_primitive/team_uplifted_primitive.dm
@@ -0,0 +1,77 @@
+RESTRICT_TYPE(/datum/team/uplifted_primitive)
+
+/// The number of players a team must have before receiving their team objective.
+#define TEAM_OBJECTIVE_MEMBER_THRESHOLD 3
+
+/// An team containing all uplifted primitives of a given species.
+/datum/team/uplifted_primitive
+ name = "Uplifted Primitives"
+
+ /// The species this team represents.
+ var/datum/species/team_species
+ /// The antag hud specific to this team, only shown to other members of the team.
+ var/datum/atom_hud/antag/team_hud = new()
+
+ /// Whether or not the team objective has been added yet.
+ var/has_added_team_objective = FALSE
+
+ /// All potential objectives which can be added as the team objective.
+ var/list/potential_objectives = list(
+ /datum/objective/uplifted/expand,
+ )
+
+ /// The number of spawns that should always poll ghosts for nests of this team.
+ var/guaranteed_sentient_spawns = 0
+
+/datum/team/uplifted_primitive/New(list/starting_members, datum/species/new_species = /datum/species/monkey)
+ team_species = new_species
+ return ..(starting_members)
+
+/datum/team/uplifted_primitive/create_team(list/starting_members)
+ . = ..()
+ name = "[team_species::name] Uprising"
+
+ add_team_objective(/datum/objective/uplifted/propagate)
+ add_team_objective(/datum/objective/uplifted/build_nest_in_area)
+
+/datum/team/uplifted_primitive/can_create_team()
+ return SSticker.mode.uplifted_teams[team_species] == null
+
+/datum/team/uplifted_primitive/assign_team(list/starting_members)
+ SSticker.mode.uplifted_teams[team_species] = src
+
+/datum/team/uplifted_primitive/clear_team_reference()
+ SSticker.mode.uplifted_teams[team_species] = null
+ SSticker.mode.uplifted_teams -= team_species
+
+/datum/team/uplifted_primitive/handle_adding_member(datum/mind/new_member)
+ ..()
+
+ if(!has_added_team_objective && length(members) >= TEAM_OBJECTIVE_MEMBER_THRESHOLD)
+ has_added_team_objective = TRUE
+ addtimer(CALLBACK(src, PROC_REF(add_new_objective)), 10 SECONDS)
+
+/datum/team/uplifted_primitive/proc/add_new_objective()
+ var/selected_objective_path = pick_n_take(potential_objectives)
+ var/datum/objective/new_objective = new selected_objective_path()
+ add_team_objective(new_objective)
+ announce_new_objective(members, new_objective)
+
+/datum/team/uplifted_primitive/proc/announce_new_objective(list/announce_to, datum/objective/new_objective)
+ var/message = prepare_new_objective_message(new_objective)
+ for(var/datum/mind/member in announce_to)
+ if(!member.current || !isliving(member.current))
+ continue
+ to_chat(member.current, message)
+ SEND_SOUND(member.current, sound('sound/ambience/alarm4.ogg'))
+
+/datum/team/uplifted_primitive/proc/prepare_new_objective_message(datum/objective/new_objective)
+ var/list/messages = list()
+
+ messages += SPAN_USERDANGER("Your species grows!")
+ messages += SPAN_NOTICE("With your numbers rising, you devise a new objective:")
+ messages += new_objective.explanation_text
+
+ return chat_box_red(messages.Join("
"))
+
+#undef TEAM_OBJECTIVE_MEMBER_THRESHOLD
diff --git a/code/modules/client/preference/preferences.dm b/code/modules/client/preference/preferences.dm
index d75ac947f49..5808ed6f478 100644
--- a/code/modules/client/preference/preferences.dm
+++ b/code/modules/client/preference/preferences.dm
@@ -12,6 +12,7 @@ GLOBAL_LIST_INIT(special_role_times, list(
ROLE_VAMPIRE = 14,
ROLE_BLOB = 14,
ROLE_REVENANT = 14,
+ ROLE_UPLIFTED_PRIMITIVE = 14,
ROLE_OPERATIVE = 21,
ROLE_CULTIST = 21,
ROLE_ALIEN = 21,
diff --git a/code/modules/events/event_container.dm b/code/modules/events/event_container.dm
index 2ccd6c423ce..1fa1a65902d 100644
--- a/code/modules/events/event_container.dm
+++ b/code/modules/events/event_container.dm
@@ -222,6 +222,7 @@ GLOBAL_LIST_EMPTY(event_last_fired)
new /datum/event_meta(EVENT_LEVEL_MAJOR, /datum/event/space_ninja, 9),
new /datum/event_meta(EVENT_LEVEL_MAJOR, /datum/event/wizard_adept, 5),
new /datum/event_meta(EVENT_LEVEL_MAJOR, /datum/event/spawn_morph, 16, is_one_shot = TRUE),
+ new /datum/event_meta(EVENT_LEVEL_MAJOR, /datum/event/spawn_uplifted_primitive, 16, is_one_shot = TRUE),
new /datum/event_meta(EVENT_LEVEL_MAJOR, /datum/event/aurora_caelus, 5, is_one_shot = TRUE),
//new /datum/event_meta(EVENT_LEVEL_MAJOR, /datum/event/spawn_pulsedemon, 20, is_one_shot = TRUE)
)
diff --git a/code/modules/mob/living/carbon/carbon_procs.dm b/code/modules/mob/living/carbon/carbon_procs.dm
index 8873371b108..656207e563e 100644
--- a/code/modules/mob/living/carbon/carbon_procs.dm
+++ b/code/modules/mob/living/carbon/carbon_procs.dm
@@ -580,7 +580,10 @@ GLOBAL_LIST_INIT(ventcrawl_machinery, list(/obj/machinery/atmospherics/unary/ven
to_chat(src, SPAN_WARNING("You cannot crawl into a vent while buckled to something!"))
return
- if(iscarbon(src) && length(contents) && ventcrawlerlocal < VENTCRAWLER_ALWAYS) // If we're here you can only ventcrawl while completely nude
+ if(ventcrawlerlocal == VENTCRAWLER_SIGNAL && !SEND_SIGNAL(src, COMSIG_LIVING_TRY_VENTCRAWL))
+ return
+
+ if(ventcrawlerlocal == VENTCRAWLER_NUDE && iscarbon(src) && length(contents)) // If we're here you can only ventcrawl while completely nude
for(var/obj/item/I in contents)
if(istype(I, /obj/item/bio_chip))
continue
@@ -602,7 +605,7 @@ GLOBAL_LIST_INIT(ventcrawl_machinery, list(/obj/machinery/atmospherics/unary/ven
var/datum/pipeline/pipenet = starting_machine.returnPipenet(target_move)
pipenet.add_ventcrawler(src)
add_ventcrawl_images(pipenet)
-
+ SEND_SIGNAL(src, COMSIG_LIVING_ENTER_VENTCRAWL)
/mob/living/proc/add_ventcrawl_images(datum/pipeline/pipenet)
var/list/totalMembers = list()
diff --git a/code/modules/mob/living/carbon/human/species/_species.dm b/code/modules/mob/living/carbon/human/species/_species.dm
index a69cb77531e..6ee74466546 100644
--- a/code/modules/mob/living/carbon/human/species/_species.dm
+++ b/code/modules/mob/living/carbon/human/species/_species.dm
@@ -96,7 +96,6 @@
var/meat_type = /obj/item/food/meat
var/no_equip // bitflags of slots the race can't equip stuff to
var/nojumpsuit = 0 // this is sorta... weird. it basically lets you equip stuff that usually needs jumpsuits without one, like belts and pockets and ids
- var/can_craft = TRUE // Can this mob using crafting or not?
var/bodyflags = 0
var/dietflags = 0 // Make sure you set this, otherwise it won't be able to digest a lot of foods
@@ -971,7 +970,7 @@
if(radiation > RAD_MOB_VOMIT && prob(RAD_MOB_VOMIT_PROB))
H.vomit(10, TRUE)
- if(radiation > RAD_MOB_MUTATE)
+ if(radiation > RAD_MOB_MUTATE && !HAS_TRAIT(H, TRAIT_GENELESS))
if(prob(1))
to_chat(H, SPAN_DANGER("You mutate!"))
randmutb(H)
diff --git a/code/modules/mob/living/carbon/human/species/monkey.dm b/code/modules/mob/living/carbon/human/species/monkey.dm
index 79df021e042..ac18a059f78 100644
--- a/code/modules/mob/living/carbon/human/species/monkey.dm
+++ b/code/modules/mob/living/carbon/human/species/monkey.dm
@@ -15,7 +15,6 @@
greater_form = /datum/species/human
no_equip = ITEM_SLOT_BELT | ITEM_SLOT_ID | ITEM_SLOT_LEFT_EAR | ITEM_SLOT_RIGHT_EAR | ITEM_SLOT_EYES | ITEM_SLOT_GLOVES | ITEM_SLOT_SHOES | ITEM_SLOT_OUTER_SUIT | ITEM_SLOT_JUMPSUIT | ITEM_SLOT_LEFT_POCKET | ITEM_SLOT_RIGHT_POCKET | ITEM_SLOT_SUIT_STORE | ITEM_SLOT_PDA | ITEM_SLOT_NECK
inherent_factions = list("jungle", "monkey")
- can_craft = FALSE
is_small = 1
has_fine_manipulation = 0
ventcrawler = VENTCRAWLER_NUDE
diff --git a/icons/mob/hud/antaghud.dmi b/icons/mob/hud/antaghud.dmi
index 5d522722709..b257846aca0 100644
Binary files a/icons/mob/hud/antaghud.dmi and b/icons/mob/hud/antaghud.dmi differ
diff --git a/icons/obj/uplifted_primitive.dmi b/icons/obj/uplifted_primitive.dmi
new file mode 100644
index 00000000000..6c98c46bd70
Binary files /dev/null and b/icons/obj/uplifted_primitive.dmi differ
diff --git a/paradise.dme b/paradise.dme
index 069625b0b47..c8d7929f5b7 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -986,6 +986,11 @@
#include "code\game\gamemodes\miniantags\morph\spells\reproduce.dm"
#include "code\game\gamemodes\miniantags\tourist\tourist_arrivals.dm"
#include "code\game\gamemodes\miniantags\tourist\tourist_objectives.dm"
+#include "code\game\gamemodes\miniantags\uplifted_primitive\uplifted_primitive_event.dm"
+#include "code\game\gamemodes\miniantags\uplifted_primitive\uplifted_primitive_items.dm"
+#include "code\game\gamemodes\miniantags\uplifted_primitive\uplifted_primitive_nest.dm"
+#include "code\game\gamemodes\miniantags\uplifted_primitive\uplifted_primitive_objectives.dm"
+#include "code\game\gamemodes\miniantags\uplifted_primitive\uplifted_primitive_spells.dm"
#include "code\game\gamemodes\nuclear\nuclear.dm"
#include "code\game\gamemodes\nuclear\nuclear_challenge.dm"
#include "code\game\gamemodes\nuclear\nuclearbomb.dm"
@@ -1893,6 +1898,8 @@
#include "code\modules\antagonists\traitor\contractor\items\contractor_pinpointer.dm"
#include "code\modules\antagonists\traitor\contractor\items\contractor_uplink.dm"
#include "code\modules\antagonists\traitor\contractor\items\extraction_items.dm"
+#include "code\modules\antagonists\uplifted_primitive\datum_uplifted_primitive.dm"
+#include "code\modules\antagonists\uplifted_primitive\team_uplifted_primitive.dm"
#include "code\modules\antagonists\vampire\vamp_datum.dm"
#include "code\modules\antagonists\vampire\vamp_thrall.dm"
#include "code\modules\antagonists\vampire\vampire_subclasses.dm"