diff --git a/code/game/objects/structures/outfit_wardrobe.dm b/code/game/objects/structures/outfit_wardrobe.dm new file mode 100644 index 00000000000..f204559c3a9 --- /dev/null +++ b/code/game/objects/structures/outfit_wardrobe.dm @@ -0,0 +1,87 @@ +// if you need to access this elsewhere you already fucked up +#define TRAIT_WARDROBE_USED "wardrobe_used" + +/obj/structure/outfit_wardrobe + name = "outfit wardrobe" + desc = "Peek in and select one of several snazzy outfits. Narnia not included." + icon = 'icons/obj/storage/closet.dmi' + icon_state = "fullcabinet" + base_icon_state = "fullcabinet" + obj_flags = INDESTRUCTIBLE + density = TRUE + anchored = TRUE + /** + * Assoc list of outfits to how many charges left. + * If value is INFINITY, the amount is infinite. Otherwise, it decreases by one per use. + * At zero it can't be picked. + * Value is unique per wardrobe. + */ + var/list/selectable_outfits_to_amount = list( + /datum/outfit/cat_butcher = INFINITY, + /datum/outfit/job/clown = 2, + ) + /// Adds a trait with the source of wardrobe_id if this is TRUE, which is checked to prevent reuse. + var/one_use = TRUE + /// All wardrobes that share this id, share the one use restriction. If one_use is FALSE, it effectively does nothing. + var/wardrobe_id = "asparagus" + /// Humanize species that need unique environments to survive. + var/humanize_plasmamen = TRUE + +/obj/structure/outfit_wardrobe/attack_hand(mob/living/user, list/modifiers) + . = ..() + if(.) + return + + var/mob/living/carbon/human/human_user = user + if(!ishuman(human_user)) + return + + if(one_use && HAS_TRAIT_FROM(human_user, TRAIT_WARDROBE_USED, wardrobe_id)) + to_chat(human_user, span_notice("You already picked an outfit!")) + return + + var/list/display_classes = list() + var/datum/outfit/chosen_class + + for(var/datum/outfit/dressup as anything in selectable_outfits_to_amount) + if(selectable_outfits_to_amount[dressup] == 0) + continue + var/datum/radial_menu_choice/option = new(src) + // We take the type of either the suit storage or head item (Likely to be relevant) or barring that the uniform + var/obj/item/sprite_path = initial(dressup.suit_store) || initial(dressup.head) || initial(dressup.uniform) + if(!sprite_path) + stack_trace("[dressup] outfit got no god damn items to use for sprite") + sprite_path = /obj/item/food/grown/citrus/orange_3d + option.image = image(icon = initial(sprite_path.icon), icon_state = initial(sprite_path.icon_state)) + option.info = span_boldnotice("[initial(dressup.name)]") // no desc.. + display_classes[dressup] = option + + if(!length(display_classes)) + to_chat(human_user, span_warning("There are no available outfits!")) + return + + sort_list(display_classes) + var/choice = show_radial_menu(human_user, src, display_classes, radius = 38, require_near = TRUE) + if(!choice) + return + + chosen_class = choice + + human_user.balloon_alert(human_user, LOWER_TEXT(chosen_class.name)) + playsound(human_user, 'sound/items/zip/un_zip.ogg', 33) + playsound(src, 'sound/machines/closet/wooden_closet_open.ogg', 25) + icon_state = "fullcabinet_open" + if(!do_after(human_user, 3 SECONDS) || selectable_outfits_to_amount[choice] == 0) + playsound(src, 'sound/machines/closet/wooden_closet_close.ogg', 50) + icon_state = base_icon_state + return + selectable_outfits_to_amount[choice]-- + playsound(src, 'sound/machines/closet/wooden_closet_close.ogg', 50) + icon_state = base_icon_state + playsound(human_user, 'sound/items/zip/zip_up.ogg', 33) + + human_user.drop_everything() + human_user.equipOutfit(chosen_class) + ADD_TRAIT(human_user, TRAIT_WARDROBE_USED, wardrobe_id) + +#undef TRAIT_WARDROBE_USED diff --git a/code/modules/bitrunning/server/_parent.dm b/code/modules/bitrunning/server/_parent.dm index 281b80c2148..09ea24ae2da 100644 --- a/code/modules/bitrunning/server/_parent.dm +++ b/code/modules/bitrunning/server/_parent.dm @@ -48,6 +48,8 @@ var/broadcasting = FALSE /// Cooldown between being able to toggle broadcasting COOLDOWN_DECLARE(broadcast_toggle_cd) + /// Cooldown for how often you're allowed to harass deadchat for PVP domains + COOLDOWN_DECLARE(polling_cooldown) /obj/machinery/quantum_server/post_machine_initialize() diff --git a/code/modules/bitrunning/server/map_handling.dm b/code/modules/bitrunning/server/map_handling.dm index 915b7d951ef..58dd8dd4eda 100644 --- a/code/modules/bitrunning/server/map_handling.dm +++ b/code/modules/bitrunning/server/map_handling.dm @@ -1,3 +1,5 @@ +#define POLLING_COOLDOWN_TIME 2 MINUTES + /// Gives all current occupants a notification that the server is going down /obj/machinery/quantum_server/proc/begin_shutdown(mob/user) if(isnull(generated_domain)) @@ -84,12 +86,40 @@ for(var/datum/lazy_template/virtual_domain/available in SSbitrunning.all_domains) if(map_key == available.key && points >= available.cost) generated_domain = available - RegisterSignal(generated_domain, COMSIG_LAZY_TEMPLATE_LOADED, PROC_REF(on_template_loaded)) - generated_domain.lazy_load() - return TRUE + break - return FALSE + if(!generated_domain) + return FALSE + if(generated_domain.mission_min_candidates && (!COOLDOWN_FINISHED(src, polling_cooldown))) + say("Advanced NPC algorithms resetting, please wait [DisplayTimeText(polling_cooldown)] or load a different domain.") + playsound(src, "sound/machines/buzz-[pick("sigh", "two")].ogg", 50, TRUE) + return FALSE + + var/list/mob/lucky_ghosts + if(generated_domain.mission_min_candidates) + playsound(src, 'sound/machines/chime.ogg', 50, TRUE) + say("Loading advanced NPCs...") + var/list/mob/candidates = SSpolling.poll_ghost_candidates("Do you want to play as a virtual [generated_domain.spawner_role] in a bitrunner domain?", ROLE_GHOST_ROLE, ROLE_GHOST_ROLE, 15 SECONDS, POLL_IGNORE_SHUTTLE_DENIZENS, TRUE) + for(var/amount in 1 to generated_domain.mission_max_candidates) + if(length(candidates)) // If no candidates, fails in code below anyways + LAZYADD(lucky_ghosts, pick_n_take(candidates)) + + if(length(lucky_ghosts) < generated_domain.mission_min_candidates) + notify_ghosts("Not enough candidates for [generated_domain.spawner_role]! Aborting mission!") + playsound(src, "sound/machines/buzz-[pick("sigh", "two")].ogg", 50, TRUE) + say("Error! Unable to load advanced NPCs. Please try again or select different domain.") + COOLDOWN_START(src, polling_cooldown, POLLING_COOLDOWN_TIME) + return FALSE + + playsound(src, 'sound/machines/ping.ogg', 50, TRUE) + say("Success!") + + generated_domain.load_advanced_npcs(lucky_ghosts) + RegisterSignal(generated_domain, COMSIG_LAZY_TEMPLATE_LOADED, PROC_REF(on_template_loaded)) + generated_domain.lazy_load() + + return TRUE /// Loads in necessary map items like hololadder spawns, caches, etc /obj/machinery/quantum_server/proc/load_map_items() @@ -116,6 +146,10 @@ qdel(thing) continue + if(istype(thing, /obj/effect/mob_spawn)) + generated_domain.ghost_spawners += thing + continue + if(istype(thing, /obj/effect/landmark/bitrunning/curiosity_spawn)) curiosity_turfs += get_turf(thing) qdel(thing) @@ -134,12 +168,10 @@ new /obj/structure/hololadder(tile) - if(!length(exit_turfs)) CRASH("Failed to find exit turfs on generated domain.") if(!length(goal_turfs)) CRASH("Failed to find send turfs on generated domain.") - if(!attempt_spawn_cache(cache_turfs)) return FALSE @@ -199,3 +231,5 @@ generated_domain = null mutation_candidate_refs.Cut() spawned_threat_refs.Cut() + +#undef POLLING_COOLDOWN_TIME diff --git a/code/modules/bitrunning/virtual_domain/virtual_domain.dm b/code/modules/bitrunning/virtual_domain/virtual_domain.dm index 7467b8e4c5e..9bb6a609ce1 100644 --- a/code/modules/bitrunning/virtual_domain/virtual_domain.dm +++ b/code/modules/bitrunning/virtual_domain/virtual_domain.dm @@ -74,12 +74,39 @@ var/list/custom_spawns = list() /// Set TRUE if you want reusable custom spawners var/keep_custom_spawns = FALSE + /// The domain must have this many ghost candidates willing to join as entities, or else it will not load. + var/mission_min_candidates = 0 + /// Maximum amount possible of above. + var/mission_max_candidates = 0 + /// Ghosts that will be spawned as, presumably, an antagonist in the map. + var/list/chosen_ghosts + /// List of spawners used for candidates. + var/list/obj/effect/mob_spawn/ghost_role/ghost_spawners + /// Current domain mobs being held by ghosts + var/list/mob/living/ghost_mobs + /// The role that ghosts will get. Only used for poll text. + var/spawner_role = "Antagonist" +/datum/lazy_template/virtual_domain/Destroy(force) + QDEL_NULL(ghost_spawners) + QDEL_NULL(ghost_mobs) + . = ..() /// Sends a point to any loot signals on the map -/datum/lazy_template/virtual_domain/proc/add_points(points_to_add) +/datum/lazy_template/virtual_domain/proc/add_points(points_to_add = 1) SEND_SIGNAL(src, COMSIG_BITRUNNER_GOAL_POINT, points_to_add) +/// Loads the ghost candidates. +/datum/lazy_template/virtual_domain/proc/load_advanced_npcs(list/mob/lucky_ghosts) + for(var/mob/lucky_ghost as anything in lucky_ghosts) + var/obj/effect/mob_spawn/ghost_role/ghost_spawner = pick(ghost_spawners) + LAZYREMOVE(ghost_spawners, ghost_spawner) + + var/mob/new_mob = ghost_spawner.create(lucky_ghost, lucky_ghost.real_name) + LAZYADD(ghost_mobs, new_mob) + + var/ghostname = lucky_ghost.name + notify_ghosts("[ghostname] has been selected to be a [ghost_spawner.prompt_name]!", source = new_mob, header = "001010110") /// Overridable proc to be called after the map is loaded. /datum/lazy_template/virtual_domain/proc/setup_domain(list/created_atoms) diff --git a/icons/obj/storage/closet.dmi b/icons/obj/storage/closet.dmi index bfa929fb0d5..6561bacb6e2 100644 Binary files a/icons/obj/storage/closet.dmi and b/icons/obj/storage/closet.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 2ebef165b02..ffdf4b77521 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2984,6 +2984,7 @@ #include "code\game\objects\structures\mystery_box.dm" #include "code\game\objects\structures\noticeboard.dm" #include "code\game\objects\structures\ore_containers.dm" +#include "code\game\objects\structures\outfit_wardrobe.dm" #include "code\game\objects\structures\petrified_statue.dm" #include "code\game\objects\structures\pinatas.dm" #include "code\game\objects\structures\plasticflaps.dm"