From 13c3cc803c7b01053d56bb4749ae7faf561c09ff Mon Sep 17 00:00:00 2001 From: unknownuser782 <126365777+unknownuser782@users.noreply.github.com> Date: Thu, 7 May 2026 13:03:30 +0800 Subject: [PATCH] Ape Uprising (#31609) * Allow monkey-like species to craft * Add signal for mobs entering vents * Implement basic uplifted antag datum & nest * Make phrasing nicer * Allow primitive species to vary & add time requirements * Add spawning functionality to nest * Make primitives unable to be humanised a little less violently * Fix linter complaints * Fix typo which made the linter mad * Add objectives and antag teams * Make nests consider other nests for spawn limit * Add greeting text * Add guaranteed early spawns * Allow deconstruction of nests & recovery of scrap * Allow uplifted mobs to ventcrawl with small items * Clarify nest spawning conditions on examine * Tweak NPC numbers & AI * Add obtain objective and give an extra objective * Give thanks to the linter * Change team objective & make the alert nicer * Make event only spawn monkeys by default * Add examine text explaining deconstruction * Allow ghosts to see nest materials * Handle stacks properly in nest deconstruction * Add on-demand ghost spawn to reduce roll spam * Add nest crafting * Allow lesser mobs to use medical stacks I don't believe this should affect anything else, as every other mob capable of holding these items is an advanced tool user. * Add hint about crafting on examine * Ensure only nest monkeys retaliate to nest damage * Add uplifted primitive nests to the spawners menu --- code/__DEFINES/dcs/mob_signals.dm | 4 + code/__DEFINES/gamemode.dm | 1 + code/__DEFINES/genetics_defines.dm | 3 +- code/__DEFINES/hud.dm | 1 + code/__DEFINES/role_preferences.dm | 3 +- code/_onclick/hud/human_hud.dm | 6 +- code/datums/ai/monkey/monkey_controller.dm | 18 + code/datums/atom_hud.dm | 1 + code/datums/mind.dm | 27 ++ code/game/dna/dna2_domutcheck.dm | 3 + code/game/gamemodes/game_mode.dm | 5 + .../uplifted_primitive_event.dm | 60 +++ .../uplifted_primitive_items.dm | 23 ++ .../uplifted_primitive_nest.dm | 380 ++++++++++++++++++ .../uplifted_primitive_objectives.dm | 97 +++++ .../uplifted_primitive_spells.dm | 47 +++ code/game/jobs/job_exp.dm | 1 + .../objects/items/stacks/medical_packs.dm | 4 - code/modules/admin/player_panel.dm | 5 + code/modules/antagonists/_common/antag_hud.dm | 12 + .../datum_uplifted_primitive.dm | 160 ++++++++ .../team_uplifted_primitive.dm | 77 ++++ code/modules/client/preference/preferences.dm | 1 + code/modules/events/event_container.dm | 1 + .../modules/mob/living/carbon/carbon_procs.dm | 7 +- .../living/carbon/human/species/_species.dm | 3 +- .../mob/living/carbon/human/species/monkey.dm | 1 - icons/mob/hud/antaghud.dmi | Bin 5035 -> 2876 bytes icons/obj/uplifted_primitive.dmi | Bin 0 -> 768 bytes paradise.dme | 7 + 30 files changed, 942 insertions(+), 16 deletions(-) create mode 100644 code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_event.dm create mode 100644 code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_items.dm create mode 100644 code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_nest.dm create mode 100644 code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_objectives.dm create mode 100644 code/game/gamemodes/miniantags/uplifted_primitive/uplifted_primitive_spells.dm create mode 100644 code/modules/antagonists/uplifted_primitive/datum_uplifted_primitive.dm create mode 100644 code/modules/antagonists/uplifted_primitive/team_uplifted_primitive.dm create mode 100644 icons/obj/uplifted_primitive.dmi 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 5d522722709497576cd7842e3da58a757e4cfd04..b257846aca0228d1145fea6d66a94f8fe92fb844 100644 GIT binary patch literal 2876 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 literal 5035 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 diff --git a/icons/obj/uplifted_primitive.dmi b/icons/obj/uplifted_primitive.dmi new file mode 100644 index 0000000000000000000000000000000000000000..6c98c46bd70d37c71864497e33635a6a98ae9505 GIT binary patch literal 768 zcmV+b1ONPqP)j7CIvpJ0ul5A{2p2 zCx1vKJR}utI2LO+7J*7BcS0e5NG5V=-0C=2JR&a84_w-Y6@%7{?OD!tS%+FJ>RWQ*r z;NmRLOex6#a*U0*I5Sc+(=$pSoZ^zil2jm5DKE9SM2U+tC9|j)$Tj5ROe;#vO@*-G zijqq6QgTvpDN|N(^>YDR4geFKB;D?YDh2=m0i8)iK~zYI?UGwl!ypiaS#B#xLV}f+ zG}cRF(;Bt?|G!R%q+LNEK z2$)9TEFlD>HKmPsdry#32#_#^z=2D_xk!WwcE(w>0*F+C9L0baC_m0hA*M~E!U2HA z-nkGxVC=_BBO&Y0TIURjEwqUHfDi)!&Smry2_r!1uJSya#ba?TTv2GxuIF>N7y?L- zNY|%8hlIk0N(iNFHnYkYAHuS}sm*7=dkHdqNACb}y?x_mxYwe8z)a8CZsvt=c z`05=O*80OMu0q}V1Klc>QWSZ04ERQfdz*C~8b;A(TKipCy3rJV)P>on46rOLUpkSi zz}H7p__2aZ*S;JM3c4m5=Z<0Hy>nk=@Eu^a*-&p+8`R9RdV!rTy>CwT#|#X!7CEDF ydd=ek0kNF<(*coCV)_*p?G~}G9Z&DzZ}kTIAQ;5jl6p%30000