diff --git a/code/__defines/gamemode.dm b/code/__defines/gamemode.dm index 4bafb55ace..a617b5ce75 100644 --- a/code/__defines/gamemode.dm +++ b/code/__defines/gamemode.dm @@ -43,7 +43,7 @@ GLOBAL_LIST_INIT(be_special_flags, list( "Wizard" = BE_WIZARD, "Malf AI" = BE_MALF, "Revolutionary" = BE_REV, - "Genaprawn" = BE_ALIEN, + "Alien Candidate" = BE_ALIEN, "Positronic Brain" = BE_AI, "Cultist" = BE_CULTIST, "Renegade" = BE_RENEGADE, diff --git a/code/__defines/preferences.dm b/code/__defines/preferences.dm index 6980c7f985..eb4cf69ceb 100644 --- a/code/__defines/preferences.dm +++ b/code/__defines/preferences.dm @@ -72,3 +72,6 @@ #define WRITE_PREF_NORMAL 1 #define WRITE_PREF_INSTANT 2 #define WRITE_PREF_MANUAL 3 + +#define PAI_UNSET "None Set" +#define PAI_DEFAULT_CHASSIS "Drone" diff --git a/code/_helpers/global_lists.dm b/code/_helpers/global_lists.dm index 902c554c11..106053b432 100644 --- a/code/_helpers/global_lists.dm +++ b/code/_helpers/global_lists.dm @@ -969,16 +969,13 @@ GLOBAL_LIST_INIT(special_roles, list( "ninja" = 0, // 10 "raider" = 0, // 11 "diona" = 0, // 12 - "mutineer" = 0, // 13 - "loyalist" = 0, // 14 - "pAI candidate" = 1, // 15 - "lost drone" = 1, // 16 - "maint pred" = 1, // 17 - "maint lurker" = 1, // 18 - "morph" = 1, // 19 - "corgi" = 1, // 20 - "cursed sword" = 1, // 21 - "Ship Survivor" = 1, // 22 + "loyalist" = 0, // 13 + "pAI" = 1, // 14 + "lost drone" = 1, // 15 + "maint critter" = 1, // 16 + "corgi" = 1, // 17 + "cursed sword" = 1, // 18 + "ship survivor" = 1, // 19 )) GLOBAL_LIST_INIT(maint_mob_pred_options, list( @@ -1095,23 +1092,27 @@ GLOBAL_LIST_INIT(breach_burn_descriptors, list( "huge scorched area" )) -GLOBAL_LIST_INIT(wide_chassis, list( - "rat", - "panther", - "teppi", - "pai-diredog", - "pai-horse_lune", - "pai-horse_soleil", - "pai-pdragon", - "pai-protodog" - )) +GLOBAL_LIST_EMPTY(paikeys) -GLOBAL_LIST_INIT(flying_chassis, list( - "pai-parrot", - "pai-bat", - "pai-butterfly", - "pai-hawk", - "cyberelf" +GLOBAL_LIST_EMPTY(pai_software_by_key) +GLOBAL_LIST_EMPTY(default_pai_software) +GLOBAL_LIST_INIT(pai_emotions, list( + "Neutral" = 1, + "What" = 2, + "Happy" = 3, + "Cat" = 4, + "Extremely Happy" = 5, + "Face" = 6, + "Laugh" = 7, + "Sad" = 8, + "Angry" = 9, + "Silly" = 10, + "Nose" = 11, + "Smirk" = 12, + "Exclamation Points" = 13, + "Question Mark" = 14, + "Blank" = 15, + "Off" = 16 )) //Sure I could spend all day making wacky overlays for all of the different forms @@ -1303,41 +1304,6 @@ GLOBAL_LIST_EMPTY(sparring_attack_cache) GLOBAL_LIST_EMPTY(protean_abilities) -//PAI stuff -GLOBAL_LIST_INIT(possible_chassis, list( - "Drone" = "pai-repairbot", - "Cat" = "pai-cat", - "Mouse" = "pai-mouse", - "Monkey" = "pai-monkey", - "Borgi" = "pai-borgi", - "Fox" = "pai-fox", - "Parrot" = "pai-parrot", - "Rabbit" = "pai-rabbit", - "Dire wolf" = "pai-diredog", - "Horse (Lune)" = "pai-horse_lune", - "Horse (Soleil)" = "pai-horse_soleil", - "Dragon" = "pai-pdragon", - "Bear" = "pai-bear", - "Fennec" = "pai-fen", - "Type Zero" = "pai-typezero", - "Raccoon" = "pai-raccoon", - "Raptor" = "pai-raptor", - "Corgi" = "pai-corgi", - "Bat" = "pai-bat", - "Butterfly" = "pai-butterfly", - "Hawk" = "pai-hawk", - "Duffel" = "pai-duffel", - "Rat" = "rat", - "Panther" = "panther", - "Cyber Elf" = "cyberelf", - "Teppi" = "teppi", - "Catslug" = "catslug", - "Car" = "car", - "Type One" = "typeone", - "Type Thirteen" = "13", - "Protogen Dog" = "pai-protodog" - )) - //PAI stuff GLOBAL_LIST_INIT(possible_say_verbs, list( "Robotic" = list("states","declares","queries"), diff --git a/code/_helpers/text.dm b/code/_helpers/text.dm index ec08572ddd..a24fdd681b 100644 --- a/code/_helpers/text.dm +++ b/code/_helpers/text.dm @@ -353,7 +353,7 @@ GLOBAL_LIST_INIT(alphabet_upper, list("A","B","C","D","E","F","G","H","I","J","K else return string else - return "[copytext_preserve_html(string, 1, 37)]..." + return "[copytext_preserve_html(string, 1, len - 3)]..." //alternative copytext() for encoded text, doesn't break html entities (" and other) /proc/copytext_preserve_html(var/text, var/first, var/last) diff --git a/code/controllers/admin.dm b/code/controllers/admin.dm index a44c3b7eeb..6c5235dd48 100644 --- a/code/controllers/admin.dm +++ b/code/controllers/admin.dm @@ -80,7 +80,6 @@ ADMIN_VERB(debug_controller, R_DEBUG, "Debug Controller", "Debug the various per options["LEGACY: master_controller"] = GLOB.master_controller options["LEGACY: job_master"] = GLOB.job_master options["LEGACY: emergency_shuttle"] = GLOB.emergency_shuttle - options["LEGACY: paiController"] = paiController options["LEGACY: cameranet"] = GLOB.cameranet options["LEGACY: transfer_controller"] = GLOB.transfer_controller diff --git a/code/controllers/subsystems/pai.dm b/code/controllers/subsystems/pai.dm new file mode 100644 index 0000000000..2d1bdf714b --- /dev/null +++ b/code/controllers/subsystems/pai.dm @@ -0,0 +1,224 @@ +#define PAI_DELAY_TIME 1 MINUTE + +//////////////////////////////// +//// Pai join and management subsystem +//////////////////////////////// +SUBSYSTEM_DEF(pai) + name = "Pai" + wait = 4 SECONDS + dependencies = list( + /datum/controller/subsystem/atoms + ) + VAR_PRIVATE/list/datum/pai_sprite/pai_chassis_sprites = list() + VAR_PRIVATE/list/current_run = list() + VAR_PRIVATE/list/pai_ghosts = list() + VAR_PRIVATE/list/asked = list() + +/datum/controller/subsystem/pai/Initialize() + // Get all software setup + for(var/type in subtypesof(/datum/pai_software)) + var/datum/pai_software/P = new type() + GLOB.pai_software_by_key[P.id] = P + if(P.default) + GLOB.default_pai_software[P.id] = P + + // Get all valid chassis types + for(var/datum/pai_sprite/sprite as anything in subtypesof(/datum/pai_sprite)) + if(!initial(sprite.sprite_icon) || initial(sprite.hidden)) + continue + pai_chassis_sprites[initial(sprite.name)] = new sprite() + + return SS_INIT_SUCCESS + +/datum/controller/subsystem/pai/stat_entry(msg) + msg = "C:[length(pai_ghosts)]" + return ..() + +/datum/controller/subsystem/pai/fire(resumed) + if(!resumed) + pai_ghosts.Cut() + current_run = GLOB.observer_mob_list.Copy() + + while(length(current_run)) + if(MC_TICK_CHECK) + return + + var/mob/observer/ghost = current_run[length(current_run)] + current_run.len-- + if(!invite_valid(ghost)) + continue + + // Create candidate + pai_ghosts[REF(ghost)] = WEAKREF(ghost) + +/datum/controller/subsystem/pai/proc/get_chassis_list() + RETURN_TYPE(/list/datum/pai_sprite) + SHOULD_NOT_OVERRIDE(TRUE) + return pai_chassis_sprites + +/datum/controller/subsystem/pai/proc/chassis_data(id_name) + RETURN_TYPE(/datum/pai_sprite) + SHOULD_NOT_OVERRIDE(TRUE) + if(!(id_name in pai_chassis_sprites)) + return pai_chassis_sprites[PAI_DEFAULT_CHASSIS] + return pai_chassis_sprites[id_name] + +/datum/controller/subsystem/pai/proc/invite_valid(mob/user) + SHOULD_NOT_OVERRIDE(TRUE) + if(!user.client?.prefs || !user.ckey) + return FALSE + if(!user.MayRespawn()) + return FALSE + if(jobban_isbanned(user, "pAI")) + return FALSE + if(!(user.client.prefs.be_special & BE_PAI)) + return FALSE + if(check_is_delayed(REF(user))) + return FALSE + if(check_is_already_pai(user.ckey)) + return FALSE + if(user.client.prefs.read_preference(/datum/preference/text/pai_name) == PAI_UNSET) // Forbid unset name + return FALSE + return TRUE + +/datum/controller/subsystem/pai/proc/check_is_delayed(ghost_ref) + SHOULD_NOT_OVERRIDE(TRUE) + if(ghost_ref in asked) + if(world.time < asked[ghost_ref] + PAI_DELAY_TIME) + return TRUE + return FALSE + +/datum/controller/subsystem/pai/proc/check_is_already_pai(check_ckey) + SHOULD_NOT_OVERRIDE(TRUE) + return (check_ckey in GLOB.paikeys) + +/datum/controller/subsystem/pai/proc/get_ghost_from_ref(ghost_ref) + RETURN_TYPE(/mob/observer) + SHOULD_NOT_OVERRIDE(TRUE) + PRIVATE_PROC(TRUE) + if(!ghost_ref) + return null + var/datum/weakref/WF = pai_ghosts[ghost_ref] + return WF?.resolve() + +/datum/controller/subsystem/pai/proc/get_invite_list_data() + RETURN_TYPE(/list) + SHOULD_NOT_OVERRIDE(TRUE) + + var/list/data = list() + for(var/ghost_ref in pai_ghosts) + var/mob/observer/ghost = get_ghost_from_ref(ghost_ref) + if(!istype(ghost) || !ghost.client?.prefs) + continue + + var/datum/preferences/pref = ghost.client.prefs + var/datum/asset/spritesheet_batched/pai_icons/spritesheet = get_asset_datum(/datum/asset/spritesheet_batched/pai_icons) + var/chassis = pref.read_preference(/datum/preference/text/pai_chassis) + var/datum/pai_sprite/sprite_datum = SSpai.chassis_data(chassis) + var/css_class = sanitize_css_class_name("[sprite_datum.type]") + UNTYPED_LIST_ADD(data, list( + "ref" = REF(ghost), + "name" = pref.read_preference(/datum/preference/text/pai_name), + "gender" = pref.read_preference(/datum/preference/choiced/gender/biological), // Cannot use identifying yet due to byond limits + "role" = TextPreview(pref.read_preference(/datum/preference/text/pai_role), 152), + "ad" = TextPreview(pref.read_preference(/datum/preference/text/pai_ad), 244), + "eyecolor" = pref.read_preference(/datum/preference/color/pai_eye_color), + "chassis" = chassis, + "emotion" = pref.read_preference(/datum/preference/text/pai_emotion), + "sprite_datum_class" = css_class, + "sprite_datum_size" = spritesheet.icon_size_id(css_class + "S"), // just get the south icon's size, the rest will be the same + )) + return data + +/datum/controller/subsystem/pai/proc/get_detailed_invite_data(var/ghost_ref) + RETURN_TYPE(/list) + SHOULD_NOT_OVERRIDE(TRUE) + + if(!(ghost_ref in pai_ghosts)) + return null + + var/datum/weakref/WF = pai_ghosts[ghost_ref] + var/mob/observer/ghost = WF?.resolve() + if(!istype(ghost) || !ghost.client?.prefs) + return null + + var/datum/preferences/pref = ghost.client.prefs + var/datum/asset/spritesheet_batched/pai_icons/spritesheet = get_asset_datum(/datum/asset/spritesheet_batched/pai_icons) + var/chassis = pref.read_preference(/datum/preference/text/pai_chassis) + var/datum/pai_sprite/sprite_datum = SSpai.chassis_data(chassis) + var/css_class = sanitize_css_class_name("[sprite_datum.type]") + return list( + "ref" = ghost_ref, + "name" = pref.read_preference(/datum/preference/text/pai_name), + "gender" = pref.read_preference(/datum/preference/choiced/gender/biological), // Cannot use identifying yet due to byond limits + // Description + "role" = pref.read_preference(/datum/preference/text/pai_role), + "description" = pref.read_preference(/datum/preference/text/pai_description), + "ad" = pref.read_preference(/datum/preference/text/pai_ad), + "comments" = pref.read_preference(/datum/preference/text/pai_comments), + // Appearance + "eyecolor" = pref.read_preference(/datum/preference/color/pai_eye_color), + "chassis" = chassis, + "emotion" = pref.read_preference(/datum/preference/text/pai_emotion), + // Sprites + "sprite_datum_class" = css_class, + "sprite_datum_size" = spritesheet.icon_size_id(css_class + "S"), // just get the south icon's size, the rest will be the same + ) + +/datum/controller/subsystem/pai/proc/invite_ghost(mob/inquirer, ghost_ref, obj/item/paicard/card) + SHOULD_NOT_OVERRIDE(TRUE) + // Is our card legal to inhabit? + if(QDELETED(card) || card.pai || card.is_damage_critical()) + to_chat(inquirer, span_warning("This [card] can no longer be used to house a pAI.")) + return + + // Check if the ghost stopped existing + var/mob/observer/ghost = get_ghost_from_ref(ghost_ref) + if(!isobserver(ghost) || !ghost.client) + to_chat(inquirer, span_warning("This pAI has gone offline.")) + return + + // Time delay if the ghost cancels your invite. + if(check_is_delayed(REF(ghost))) + to_chat(inquirer, span_notice("This pAI is responding to a request, but may become available again shortly...")) + return + asked[REF(ghost)] = world.time + + // Can't play, still respawning + var/time_till_respawn = ghost.time_till_respawn() + if(time_till_respawn == -1 || time_till_respawn) + to_chat(inquirer, span_warning("This pAI is still downloading...")) + return + + // Send it! + to_chat(inquirer, span_info("A request has been sent!")) + var/client/target = ghost.client + var/response = tgui_alert(target, "[inquirer] is requesting a pAI personality. Would you like to play as a personal AI?", "pAI Request", list("Yes", "No", "Never for this round")) + if(!response || !target || !isobserver(target.mob) || ghost != target.mob) + return // Nice try smartass + if(check_is_already_pai(target.ckey)) + to_chat(inquirer, span_warning("This pAI has already been downloaded.")) + return + if(QDELETED(card) || card.pai) + to_chat(inquirer, span_warning("This [card] can no longer be used to house a pAI.")) + return + + switch(response) + if("Yes") + var/new_pai = card.ghost_inhabit(target.mob, TRUE) + to_chat(inquirer, span_info("[new_pai] has accepted your pAI request!")) + return + if("Never for this round") + SSpai.block_pai_invites(REF(ghost)) + + to_chat(inquirer, span_warning("The pAI denied the request.")) + +/datum/controller/subsystem/pai/proc/block_pai_invites(ghost_ref) + SHOULD_NOT_OVERRIDE(TRUE) + asked[ghost_ref] = world.time + 99 HOURS // We never want to be asked again + +/datum/controller/subsystem/pai/proc/clear_pai_block_delay(ghost_ref) + SHOULD_NOT_OVERRIDE(TRUE) + asked -= ghost_ref + +#undef PAI_DELAY_TIME diff --git a/code/game/machinery/atmo_control.dm b/code/game/machinery/atmo_control.dm index 6eb17f9088..7d61d7d6b4 100644 --- a/code/game/machinery/atmo_control.dm +++ b/code/game/machinery/atmo_control.dm @@ -186,6 +186,9 @@ . = ..(W, user) +/obj/machinery/computer/general_air_control/allow_pai_interaction(proximity_flag) + return proximity_flag + /obj/machinery/computer/general_air_control/receive_signal(datum/signal/signal) if(!signal || signal.encryption) return diff --git a/code/game/machinery/buttons.dm b/code/game/machinery/buttons.dm index 733f0763a1..5ee5da62df 100644 --- a/code/game/machinery/buttons.dm +++ b/code/game/machinery/buttons.dm @@ -16,3 +16,6 @@ /obj/machinery/button/attackby(obj/item/W, mob/user as mob) return attack_hand(user) + +/obj/machinery/button/allow_pai_interaction(proximity_flag) + return proximity_flag diff --git a/code/game/machinery/computer/atmos_alert.dm b/code/game/machinery/computer/atmos_alert.dm index 54f67c1de2..4c78c2d9e8 100644 --- a/code/game/machinery/computer/atmos_alert.dm +++ b/code/game/machinery/computer/atmos_alert.dm @@ -17,6 +17,9 @@ /obj/machinery/computer/atmos_alert/attack_hand(mob/user) tgui_interact(user) +/obj/machinery/computer/atmos_alert/allow_pai_interaction(proximity_flag) + return proximity_flag + /obj/machinery/computer/atmos_alert/tgui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) diff --git a/code/game/machinery/computer/camera.dm b/code/game/machinery/computer/camera.dm index b6b8fc1cbf..c3a7ee5043 100644 --- a/code/game/machinery/computer/camera.dm +++ b/code/game/machinery/computer/camera.dm @@ -68,6 +68,9 @@ density = FALSE circuit = null +/obj/machinery/computer/security/telescreen/allow_pai_interaction(proximity_flag) + return proximity_flag + GLOBAL_LIST_EMPTY(entertainment_screens) /obj/machinery/computer/security/telescreen/entertainment name = "entertainment monitor" diff --git a/code/game/machinery/computer/crew.dm b/code/game/machinery/computer/crew.dm index b658638700..2045154cd0 100644 --- a/code/game/machinery/computer/crew.dm +++ b/code/game/machinery/computer/crew.dm @@ -28,6 +28,9 @@ return tgui_interact(user) +/obj/machinery/computer/crew/allow_pai_interaction(proximity_flag) + return proximity_flag + /obj/machinery/computer/crew/tgui_interact(mob/user, datum/tgui/ui = null) crew_monitor.tgui_interact(user, ui) diff --git a/code/game/machinery/computer/station_alert.dm b/code/game/machinery/computer/station_alert.dm index 0a5692a897..a365fa66a9 100644 --- a/code/game/machinery/computer/station_alert.dm +++ b/code/game/machinery/computer/station_alert.dm @@ -41,6 +41,9 @@ tgui_interact(user) return +/obj/machinery/computer/station_alert/allow_pai_interaction(proximity_flag) + return proximity_flag + /obj/machinery/computer/station_alert/tgui_interact(mob/user) alarm_monitor.tgui_interact(user) diff --git a/code/game/machinery/doors/airlock_control.dm b/code/game/machinery/doors/airlock_control.dm index 5e70300958..3779df3582 100644 --- a/code/game/machinery/doors/airlock_control.dm +++ b/code/game/machinery/doors/airlock_control.dm @@ -270,6 +270,9 @@ return TRUE return ..() +/obj/machinery/airlock_sensor/allow_pai_interaction(proximity_flag) + return proximity_flag + /obj/machinery/airlock_sensor/airlock_interior command = "cycle_interior" @@ -361,6 +364,8 @@ radio_connection.post_signal(src, signal, range = AIRLOCK_CONTROL_RANGE, radio_filter = RADIO_AIRLOCK) flick("access_button_cycle", src) +/obj/machinery/access_button/allow_pai_interaction(proximity_flag) + return proximity_flag /obj/machinery/access_button/proc/set_frequency(new_frequency) SSradio.remove_object(src, frequency) diff --git a/code/game/machinery/jukebox.dm b/code/game/machinery/jukebox.dm index 5cf746aa1b..fec3d3d3d3 100644 --- a/code/game/machinery/jukebox.dm +++ b/code/game/machinery/jukebox.dm @@ -252,6 +252,9 @@ /obj/machinery/media/jukebox/attack_hand(var/mob/user as mob) interact(user) +/obj/machinery/media/jukebox/allow_pai_interaction(proximity_flag) + return proximity_flag + /obj/machinery/media/jukebox/proc/explode() walk_to(src,0) src.visible_message(span_danger("\The [src] blows apart!"), 1) diff --git a/code/game/machinery/lightswitch.dm b/code/game/machinery/lightswitch.dm index 8e62a56d31..933cac4d64 100644 --- a/code/game/machinery/lightswitch.dm +++ b/code/game/machinery/lightswitch.dm @@ -71,6 +71,9 @@ area.power_change() GLOB.lights_switched_on_roundstat++ +/obj/machinery/light_switch/allow_pai_interaction(proximity_flag) + return proximity_flag + /obj/machinery/light_switch/power_change() if(!otherarea) diff --git a/code/game/machinery/newscaster.dm b/code/game/machinery/newscaster.dm index 69755023bd..6d5877effa 100644 --- a/code/game/machinery/newscaster.dm +++ b/code/game/machinery/newscaster.dm @@ -273,6 +273,9 @@ GLOBAL_LIST_BOILERPLATE(allCasters, /obj/machinery/newscaster) tgui_interact(user) +/obj/machinery/newscaster/allow_pai_interaction(proximity_flag) + return proximity_flag + /** * Sets a temporary message to display to the user * diff --git a/code/game/objects/items/devices/paicard.dm b/code/game/objects/items/devices/paicard.dm index e0603d8978..236157dbb2 100644 --- a/code/game/objects/items/devices/paicard.dm +++ b/code/game/objects/items/devices/paicard.dm @@ -10,17 +10,28 @@ preserve_item = 1 var/obj/item/radio/borg/pai/radio - var/looking_for_personality = 0 var/mob/living/silicon/pai/pai var/image/screen_layer var/screen_color = "#00ff0d" + var/current_emotion = 1 var/last_notify = 0 var/screen_msg pickup_sound = 'sound/items/pickup/device.ogg' drop_sound = 'sound/items/drop/device.ogg' + // Parts and upgrades + var/panel_open = FALSE + var/cell = PP_FUNCTIONAL //critical- power + var/processor = PP_FUNCTIONAL //critical- the thinky part + var/board = PP_FUNCTIONAL //critical- makes everything work + var/capacitor = PP_FUNCTIONAL //critical- power processing + var/projector = PP_FUNCTIONAL //non-critical- affects unfolding + var/emitter = PP_FUNCTIONAL //non-critical- affects unfolding + var/speech_synthesizer = PP_FUNCTIONAL //non-critical- affects speech + ///Var for attack_self chain var/special_handling = FALSE + var/selected_pai /obj/item/paicard/relaymove(var/mob/user, var/direction) if(user.stat || user.stunned) @@ -31,18 +42,17 @@ /obj/item/paicard/Initialize(mapload) . = ..() - add_overlay("pai-off") + setEmotion(16) /obj/item/paicard/Destroy() //Will stop people throwing friend pAIs into the singularity so they can respawn - if(!isnull(pai)) + if(!QDELETED(pai)) // Either the pai or card could be deleted first, prevent a loop pai.death(0) QDEL_NULL(radio) return ..() -// VOREStation Edit - Allow everyone to become a pAI /obj/item/paicard/attack_ghost(mob/user as mob) - if(pai != null) //Have a person in them already? + if(pai) //Have a person in them already? return ..() if(is_damage_critical()) to_chat(user, span_warning("That card is too damaged to activate!")) @@ -57,361 +67,250 @@ to_chat(user,span_warning("You cannot join a pAI card when you are banned from playing as a pAI.")) return - for(var/ourkey in GLOB.paikeys) - if(ourkey == user.ckey) - to_chat(user, span_warning("You can't just rejoin any old pAI card!!! Your card still exists.")) - return + if(SSpai.check_is_already_pai(user.ckey)) + to_chat(user, span_warning("You can't just rejoin any old pAI card!!! Your card still exists.")) + return - var/choice = tgui_alert(user, "You sure you want to inhabit this PAI, or submit yourself to being recruited?", "Confirmation", list("Inhabit", "Recruit", "Cancel")) - if(!choice || choice == "Cancel") - return ..() - if(choice == "Recruit") - paiController.recruitWindow(user) - return ..() - choice = tgui_alert(user, "Do you want to load your pAI data?", "Load", list("Yes", "No")) - var/actual_pai_name - var/turf/location = get_turf(src) - if(choice == "No") - var/pai_name = tgui_input_text(user, "Choose your character's name", "Character Name") - actual_pai_name = sanitize_name(pai_name, ,1) - if(isnull(actual_pai_name)) - return ..() - if(istype(src , /obj/item/paicard/typeb)) - var/obj/item/paicard/typeb/card = new(location) - var/mob/living/silicon/pai/new_pai = new(card) - new_pai.key = user.key - GLOB.paikeys |= new_pai.ckey - card.setPersonality(new_pai) - new_pai.SetName(actual_pai_name) - else - var/obj/item/paicard/card = new(location) - var/mob/living/silicon/pai/new_pai = new(card) - new_pai.key = user.key - GLOB.paikeys |= new_pai.ckey - card.setPersonality(new_pai) - new_pai.SetName(actual_pai_name) + var/pai_name = user.client?.prefs.read_preference(/datum/preference/text/pai_name) + if(!pai_name || pai_name == PAI_UNSET) + to_chat(user, span_danger("You have no pai name set.")) + return - if(choice == "Yes") - if(istype(src , /obj/item/paicard/typeb)) - var/obj/item/paicard/typeb/card = new(location) - var/mob/living/silicon/pai/new_pai = new(card) - new_pai.key = user.key - GLOB.paikeys |= new_pai.ckey - card.setPersonality(new_pai) - if(!new_pai.savefile_load(new_pai)) - var/pai_name = tgui_input_text(new_pai, "Choose your character's name", "Character Name") - actual_pai_name = sanitize_name(pai_name, ,1) - if(isnull(actual_pai_name)) - return ..() - else - var/obj/item/paicard/card = new(location) - var/mob/living/silicon/pai/new_pai = new(card) - new_pai.key = user.key - GLOB.paikeys |= new_pai.ckey - card.setPersonality(new_pai) - if(!new_pai.savefile_load(new_pai)) - var/pai_name = tgui_input_text(new_pai, "Choose your character's name", "Character Name") - actual_pai_name = sanitize_name(pai_name, ,1) - if(isnull(actual_pai_name)) - return ..() + var/choice = tgui_alert(user, "Do you want to inhabit this pAI using \"[pai_name]\"?", "Load pAI", list("Load pAI Data", "Cancel")) + if(choice == "Load pAI Data") + ghost_inhabit(user) + return - qdel(src) - return ..() +/obj/item/paicard/proc/ghost_inhabit(mob/user) + RETURN_TYPE(/mob/living/silicon/pai) + var/pai_name = user.client?.prefs.read_preference(/datum/preference/text/pai_name) + if(!pai_name || pai_name == PAI_UNSET) // Lets avoid "None Set" pais joining + to_chat(user, span_danger("You have no pai name set.")) + return + // Setup pai + var/mob/living/silicon/pai/new_pai = new(src) + new_pai.key = user.key + GLOB.paikeys |= new_pai.ckey + setPersonality(new_pai) + new_pai.apply_preferences(new_pai.client) + return new_pai -// VOREStation Edit End - -/obj/item/paicard/proc/access_screen(mob/user) +/obj/item/paicard/tgui_interact(mob/user, datum/tgui/ui) if(is_damage_critical()) to_chat(user, span_warning("WARNING: CRITICAL HARDWARE FAILURE, SERVICE DEVICE IMMEDIATELY")) return - if (!in_range(src, user)) - return - user.set_machine(src) - var/dat = {" - - - - - - - "} - if(pai) - dat += {" - "} + span_bold("Personal AI Device") + {"

- - - - - - - - - - - - - - - -
"} + span_bold("[pai.name]") + {"
Integrity: [pai.health]
Prime directive:[pai.pai_law0]
Additional directives:[pai.pai_laws]
-
- "} - dat += {" - - -
- Configure Directives -
- "} - if(pai && (!pai.master_dna || !pai.master)) - dat += {" - - -
- Imprint Master DNA -
- "} - dat += "
" - if(radio) - dat += span_bold("Radio Uplink") - dat += {" - - - - - - - - - -
Transmit:[radio.broadcasting ? "En" : "Dis" ]abled + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "PAICard", "Personal AI Device") + ui.open() -
Receive:[radio.listening ? "En" : "Dis" ]abled +/obj/item/paicard/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet_batched/pai_icons), + ) -
-
- "} - else // - dat += span_bold("Radio Uplink") + "
" - dat += span_red(span_italics("Radio firmware not loaded. Please install a pAI personality to load firmware.")) + "
" - /* - //A button for instantly deleting people from the game is lame, especially considering that pAIs on our server tend to activate without a master. - dat += {" - - -
Wipe current pAI personality +/obj/item/paicard/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state) + var/list/data = list( + "active_pai_data" = null, + "selected_pai_data" = null, + "available_pais" = null, + "waiting_for_response" = in_use, + ) -
- "} + + if(pai) // Only set pai data if we have one + data["active_pai_data"] = get_active_data() + return data + + if(selected_pai) + data["selected_pai_data"] = SSpai.get_detailed_invite_data(selected_pai) + // Only get the invite list if we can browse for them + data["available_pais"] = SSpai.get_invite_list_data() + return data + +/obj/item/paicard/proc/get_active_data() + var/list/radio_data + if(radio) + radio_data = list( + "radio_transmit" = radio.broadcasting, + "radio_recieve" = radio.listening, + ) + + var/datum/asset/spritesheet_batched/pai_icons/spritesheet = get_asset_datum(/datum/asset/spritesheet_batched/pai_icons) + var/datum/pai_sprite/sprite_datum = SSpai.chassis_data(pai.chassis_name) + var/css_class = sanitize_css_class_name("[sprite_datum.type]") + return list( + "name" = pai.name, + "color" = screen_color, + "chassis" = pai.chassis_name, + "health" = pai.health, + "law_zero" = pai.pai_law0, + "law_extra" = pai.pai_laws, + "master_name" = pai.master, + "master_dna" = pai.master_dna, + "screen_msg" = screen_msg, + "radio_data" = radio_data, + "sprite_datum_class" = css_class, + "sprite_datum_size" = spritesheet.icon_size_id(css_class + "S"), // just get the south icon's size, the rest will be the same + ) + +/obj/item/paicard/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + if(is_damage_critical()) + return FALSE + if(..()) + return TRUE + add_fingerprint(ui.user) + + switch(action) + if("preview") + if(pai) + return FALSE + if(in_use) + return FALSE + var/new_selection = params["ref"] + if(!istext(new_selection)) + return FALSE + selected_pai = new_selection + return TRUE + if("clear_preview") + if(pai) + return FALSE + if(in_use) + return FALSE + selected_pai = null + return TRUE + if("setdna") + if(!pai) + return FALSE + if(pai.master_dna) + return FALSE + + var/mob/M = ui.user + var/has_dna = FALSE + if(istype(M, /mob/living/carbon)) + var/mob/living/carbon/carby = M + var/datum/species/spec = carby.species + has_dna = TRUE + if(spec.flags & NO_DNA) + has_dna = FALSE + + if(has_dna) + var/datum/dna/dna = M.dna + pai.master = M.real_name + pai.master_dna = dna.unique_enzymes + to_chat(pai, span_warning(span_large("You have been bound to a new master."))) + return TRUE + to_chat(ui.user, span_notice("You don't have any DNA, or your DNA is incompatible with this device.")) + return FALSE + + if("cleardna") + if(!pai) + return FALSE + pai.master = null + pai.master_dna = null + return TRUE + /* + if("wipe") + if(!pai) + return FALSE + if(in_use) + return FALSE + in_use = TRUE + var/confirm = tgui_alert(usr, "Are you CERTAIN you wish to delete the current personality? This action cannot be undone.", "Personality Wipe", list("Yes", "No")) + in_use = FALSE + if(!confirm) + return FALSE + if(confirm == "Yes") + for(var/mob/M in src) + to_chat(M, span_red("

You feel yourself slipping away from reality.

")) + to_chat(M, "

Byte by byte you lose your sense of self.

") + to_chat(M, "

Your mental faculties leave you.

") + to_chat(M, "
oblivion...
") + M.death(0) + removePersonality() + return TRUE */ - if(screen_msg) - dat += span_bold("Message from [pai.name]") + "
[screen_msg]" - else - if(looking_for_personality) - dat += {" - "} + span_bold("pAI Request Module") + {"

-

Requesting AI personalities from central database... If there are no entries, or if a suitable entry is not listed, check again later as more personalities may be added.

- Searching for personalities

- - - - -
- Refresh available personalities -

- "} - else - dat += {" - "} + span_bold("pAI Request Module") + {"

-

No personality is installed.

- - - - -
Request personality -
-
-

Each time this button is pressed, a request will be sent out to any available personalities. Check back often give plenty of time for personalities to respond. This process could take anywhere from 15 seconds to several minutes, depending on the available personalities' timeliness.

- "} - dat += "" - user << browse(dat, "window=paicard") - onclose(user, "paicard") - return + if("wires") + if(!pai) + return FALSE + // WIRE_SIGNAL = 1 + // WIRE_RECEIVE = 2 + // WIRE_TRANSMIT = 4 + switch(text2num(params["wires"])) + if(4) + radio.ToggleBroadcast() + return TRUE + if(2) + radio.ToggleReception() + return TRUE + return FALSE -/obj/item/paicard/Topic(href, href_list) + if("setlaws") + if(!pai) + return FALSE + if(in_use) + return FALSE + var/newlaws = sanitize(params["directive"], MAX_MESSAGE_LEN, FALSE, FALSE, TRUE) + if(newlaws) + pai.pai_laws = newlaws + show_laws(TRUE) + return TRUE - if(!usr || usr.stat) - return + if("clearlaws") + if(!pai) + return FALSE + pai.pai_laws = null + return TRUE - if(href_list["setdna"]) - if(pai.master_dna) - return - var/mob/M = usr - if(!istype(M, /mob/living/carbon)) - to_chat(usr, span_blue("You don't have any DNA, or your DNA is incompatible with this device.")) - else - var/datum/dna/dna = usr.dna - pai.master = M.real_name - pai.master_dna = dna.unique_enzymes - to_chat(pai, span_red("

You have been bound to a new master.

")) - if(href_list["request"]) - src.looking_for_personality = 1 - paiController.findPAI(src, usr) - if(href_list["wipe"]) - var/confirm = tgui_alert(usr, "Are you CERTAIN you wish to delete the current personality? This action cannot be undone.", "Personality Wipe", list("Yes", "No")) - if(confirm == "Yes") - for(var/mob/M in src) - to_chat(M, span_red("

You feel yourself slipping away from reality.

")) - to_chat(M, "

Byte by byte you lose your sense of self.

") - to_chat(M, "

Your mental faculties leave you.

") - to_chat(M, "
oblivion...
") - M.death(0) - removePersonality() - if(href_list["wires"]) - var/t1 = text2num(href_list["wires"]) - switch(t1) - if(4) - radio.ToggleBroadcast() - if(2) - radio.ToggleReception() - if(href_list["setlaws"]) - var/newlaws = sanitize(tgui_input_text(usr, "Enter any additional directives you would like your pAI personality to follow. Note that these directives will not override the personality's allegiance to its imprinted master. Conflicting directives will be ignored.", "pAI Directive Configuration", pai.pai_laws, MAX_MESSAGE_LEN, encode = FALSE, multiline = TRUE, prevent_enter = TRUE), MAX_MESSAGE_LEN, FALSE, FALSE, TRUE) - if(newlaws) - pai.pai_laws = newlaws - to_chat(pai, "Your supplemental directives have been updated. Your new directives are:") - to_chat(pai, "Prime Directive:
[pai.pai_law0]") - to_chat(pai, "Supplemental Directives:
[pai.pai_laws]") - attack_self(usr) + if("select_pai") + if(pai) + return FALSE + if(in_use) + return FALSE + in_use = TRUE + SSpai.invite_ghost(ui.user, params["ref"], src) + in_use = FALSE + selected_pai = null + return TRUE -// WIRE_SIGNAL = 1 -// WIRE_RECEIVE = 2 -// WIRE_TRANSMIT = 4 +/obj/item/paicard/proc/show_laws(updated = FALSE) + to_chat(pai, examine_block(span_notice((updated ? "Your supplemental directives have been updated. Your new" : "Your") + " directives are:") + "
" + "Prime Directive: [span_info(pai.pai_law0)]
Supplemental Directives: [span_info(pai.pai_laws)]")) /obj/item/paicard/proc/setPersonality(mob/living/silicon/pai/personality) - src.pai = personality + pai = personality setEmotion(1) /obj/item/paicard/proc/removePersonality() - src.pai = null - cut_overlays() + pai = null setEmotion(16) -/obj/item/paicard - var/current_emotion = 1 /obj/item/paicard/proc/setEmotion(var/emotion) - if(pai) - cut_overlays() - qdel(screen_layer) - screen_layer = null - switch(emotion) - if(1) screen_layer = image(icon, "pai-neutral") - if(2) screen_layer = image(icon, "pai-what") - if(3) screen_layer = image(icon, "pai-happy") - if(4) screen_layer = image(icon, "pai-cat") - if(5) screen_layer = image(icon, "pai-extremely-happy") - if(6) screen_layer = image(icon, "pai-face") - if(7) screen_layer = image(icon, "pai-laugh") - if(8) screen_layer = image(icon, "pai-sad") - if(9) screen_layer = image(icon, "pai-angry") - if(10) screen_layer = image(icon, "pai-silly") - if(11) screen_layer = image(icon, "pai-nose") - if(12) screen_layer = image(icon, "pai-smirk") - if(13) screen_layer = image(icon, "pai-exclamation") - if(14) screen_layer = image(icon, "pai-question") - if(15) screen_layer = image(icon, "pai-blank") - if(16) screen_layer = image(icon, "pai-off") + cut_overlays() + qdel(screen_layer) + screen_layer = null + switch(emotion) + if(1) screen_layer = image(icon, "pai-neutral") + if(2) screen_layer = image(icon, "pai-what") + if(3) screen_layer = image(icon, "pai-happy") + if(4) screen_layer = image(icon, "pai-cat") + if(5) screen_layer = image(icon, "pai-extremely-happy") + if(6) screen_layer = image(icon, "pai-face") + if(7) screen_layer = image(icon, "pai-laugh") + if(8) screen_layer = image(icon, "pai-sad") + if(9) screen_layer = image(icon, "pai-angry") + if(10) screen_layer = image(icon, "pai-silly") + if(11) screen_layer = image(icon, "pai-nose") + if(12) screen_layer = image(icon, "pai-smirk") + if(13) screen_layer = image(icon, "pai-exclamation") + if(14) screen_layer = image(icon, "pai-question") + if(15) screen_layer = image(icon, "pai-blank") + if(16) screen_layer = image(icon, "pai-off") - screen_layer.color = pai.eye_color - add_overlay(screen_layer) - current_emotion = emotion + screen_layer.color = pai ? pai.eye_color : initial(screen_color) + add_overlay(screen_layer) + current_emotion = emotion /obj/item/paicard/proc/alertUpdate() if(pai) @@ -442,10 +341,401 @@ pai.show_message(rendered, type) ..() +/obj/item/paicard/proc/clear_invite_overlay() + SHOULD_NOT_OVERRIDE(TRUE) + PRIVATE_PROC(TRUE) + if(pai) // Don't wipe emotion if a pai was invited + return + setEmotion(16) +/obj/item/paicard/attackby(var/obj/item/I as obj, mob/user as mob) + if(istype(I,/obj/item/tool/screwdriver)) + if(panel_open) + panel_open = FALSE + user.visible_message(span_notice("\The [user] secured \the [src]'s maintenance panel.")) + playsound(src, 'sound/items/Screwdriver.ogg', 50, 1) + else if(pai) + if(do_after(user, 3 SECONDS, target = src)) + panel_open = TRUE + user.visible_message(span_warning("\The [user] opened \the [src]'s maintenance panel.")) + playsound(src, 'sound/items/Screwdriver.ogg', 50, 1) + if(istype(I,/obj/item/robotanalyzer)) + if(!panel_open) + to_chat(user, span_warning("The panel isn't open. You will need to unscrew it to open it.")) + else + if(cell == PP_FUNCTIONAL) + to_chat(user,"Power cell: " + span_notice("functional")) + else if(cell == PP_BROKEN) + to_chat(user,"Power cell: " + span_warning("damaged - CRITICAL")) + else + to_chat(user,"Power cell: " + span_warning("missing - CRITICAL")) + + if(processor == PP_FUNCTIONAL) + to_chat(user,"Processor: " + span_notice("functional")) + else if(processor == PP_BROKEN) + to_chat(user,"Processor: " + span_warning("damaged - CRITICAL")) + else + to_chat(user,"Processor: " + span_warning("missing - CRITICAL")) + + if(board == PP_FUNCTIONAL) + to_chat(user,"Board: " + span_notice("functional")) + else if(board == PP_BROKEN) + to_chat(user,"Board: " + span_warning("damaged - CRITICAL")) + else + to_chat(user,"Board: " + span_warning("missing - CRITICAL")) + + if(capacitor == PP_FUNCTIONAL) + to_chat(user,"Capacitors: " + span_notice("functional")) + else if(capacitor == PP_BROKEN) + to_chat(user,"Capacitors: " + span_warning("damaged - CRITICAL")) + else + to_chat(user,"Capacitors: " + span_warning("missing - CRITICAL")) + + if(projector == PP_FUNCTIONAL) + to_chat(user,"Projectors: " + span_notice("functional")) + else if(projector == PP_BROKEN) + to_chat(user,"Projectors: " + span_warning("damaged")) + else + to_chat(user,"Projectors: " + span_warning("missing")) + + if(emitter == PP_FUNCTIONAL) + to_chat(user,"Emitters: " + span_notice("functional")) + else if(emitter == PP_BROKEN) + to_chat(user,"Emitters: " + span_warning("damaged")) + else + to_chat(user,"Emitters: " + span_warning("missing")) + + if(speech_synthesizer == PP_FUNCTIONAL) + to_chat(user,"Speech Synthesizer: " + span_notice("functional")) + else if(speech_synthesizer == PP_BROKEN) + to_chat(user,"Speech Synthesizer: " + span_warning("damaged")) + else + to_chat(user,"Speech Synthesizer: " + span_warning("missing")) + + if(istype(I,/obj/item/multitool)) + if(!panel_open) + to_chat(user, span_warning("You can't do that in this state.")) + else + var/list/parts = list() + if(cell != PP_MISSING) + parts |= "cell" + if(processor != PP_MISSING) + parts |= "processor" + if(board != PP_MISSING) + parts |= "board" + if(capacitor != PP_MISSING) + parts |= "capacitor" + if(projector != PP_MISSING) + parts |= "projector" + if(emitter != PP_MISSING) + parts |= "emitter" + if(speech_synthesizer != PP_MISSING) + parts |= "speech synthesizer" + + var/choice = tgui_input_list(user, "Which part would you like to check?", "Check part", parts) + switch(choice) + if("cell") + if(cell == PP_FUNCTIONAL) + to_chat(user,"Power cell: " + span_notice("functional")) + else if(speech_synthesizer == PP_BROKEN) + to_chat(user,"Power cell: " + span_warning("damaged")) + else + to_chat(user,"Power cell: " + span_warning("missing")) + + if("processor") + if(processor == PP_FUNCTIONAL) + to_chat(user,"Processor: " + span_notice("functional")) + else if(speech_synthesizer == PP_BROKEN) + to_chat(user,"Processor: " + span_warning("damaged")) + else + to_chat(user,"Processor: " + span_warning("missing")) + + if("board") + if(board == PP_FUNCTIONAL) + to_chat(user,"Board: " + span_notice("functional")) + else if(speech_synthesizer == PP_BROKEN) + to_chat(user,"Board: " + span_warning("damaged")) + else + to_chat(user,"Board: " + span_warning("missing")) + + if("capacitor") + if(capacitor == PP_FUNCTIONAL) + to_chat(user,"Capacitors: " + span_notice("functional")) + else if(speech_synthesizer == PP_BROKEN) + to_chat(user,"Capacitors: " + span_warning("damaged")) + else + to_chat(user,"Capacitors: " + span_warning("missing")) + + if("projector") + if(projector == PP_FUNCTIONAL) + to_chat(user,"Projectors: " + span_notice("functional")) + else if(speech_synthesizer == PP_BROKEN) + to_chat(user,"Projectors: " + span_warning("damaged")) + else + to_chat(user,"Projectors: " + span_warning("missing")) + + if("emitter") + if(emitter == PP_FUNCTIONAL) + to_chat(user,"Emitters: " + span_notice("functional")) + else if(speech_synthesizer == PP_BROKEN) + to_chat(user,"Emitters: " + span_warning("damaged")) + else + to_chat(user,"Emitters: " + span_warning("missing")) + + if("speech synthesizer") + if(speech_synthesizer == PP_FUNCTIONAL) + to_chat(user,"Speech Synthesizer: " + span_notice("functional")) + else if(speech_synthesizer == PP_BROKEN) + to_chat(user,"Speech Synthesizer: " + span_warning("damaged")) + else + to_chat(user,"Speech Synthesizer: " + span_warning("missing")) + + if(istype(I,/obj/item/paiparts/cell)) + if(cell == PP_MISSING) + if(do_after(user, 3 SECONDS, target = src)) + user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src].")) + cell = PP_FUNCTIONAL + user.drop_from_inventory(I) + qdel(I) + else + to_chat(user, span_warning("You would need to remove the installed [I] first!")) + if(istype(I,/obj/item/paiparts/processor)) + if(processor == PP_MISSING) + if(do_after(user, 3 SECONDS, target = src)) + user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src].")) + processor = PP_FUNCTIONAL + user.drop_from_inventory(I) + qdel(I) + else + to_chat(user, span_warning("You would need to remove the installed [I] first!")) + if(istype(I,/obj/item/paiparts/board)) + if(board == PP_MISSING) + if(do_after(user, 3 SECONDS, target = src)) + user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src].")) + board = PP_FUNCTIONAL + user.drop_from_inventory(I) + qdel(I) + else + to_chat(user, span_warning("You would need to remove the installed [I] first!")) + if(istype(I,/obj/item/paiparts/capacitor)) + if(capacitor == PP_MISSING) + if(do_after(user, 3 SECONDS, target = src)) + user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src].")) + capacitor = PP_FUNCTIONAL + user.drop_from_inventory(I) + qdel(I) + else + to_chat(user, span_warning("You would need to remove the installed [I] first!")) + if(istype(I,/obj/item/paiparts/projector)) + if(projector == PP_MISSING) + if(do_after(user, 3 SECONDS, target = src)) + user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src].")) + projector = PP_FUNCTIONAL + user.drop_from_inventory(I) + qdel(I) + else + to_chat(user, span_warning("You would need to remove the installed [I] first!")) + if(istype(I,/obj/item/paiparts/emitter)) + if(emitter == PP_MISSING) + if(do_after(user, 3 SECONDS, target = src)) + user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src].")) + emitter = PP_FUNCTIONAL + user.drop_from_inventory(I) + qdel(I) + else + to_chat(user, span_warning("You would need to remove the installed [I] first!")) + if(istype(I,/obj/item/paiparts/speech_synthesizer)) + if(speech_synthesizer == PP_MISSING) + if(do_after(user, 3 SECONDS, target = src)) + user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src].")) + speech_synthesizer = PP_FUNCTIONAL + user.drop_from_inventory(I) + qdel(I) + else + to_chat(user, span_warning("You would need to remove the installed [I] first!")) + +/obj/item/paicard/attack_self(mob/user, callback) + . = ..(user) + if(.) + return TRUE + if(special_handling && !callback) + return FALSE + if(!panel_open) + tgui_interact(user) + return + var/list/parts = list() + if(cell != PP_MISSING) + parts |= "cell" + if(processor != PP_MISSING) + parts |= "processor" + if(board != PP_MISSING) + parts |= "board" + if(capacitor != PP_MISSING) + parts |= "capacitor" + if(projector != PP_MISSING) + parts |= "projector" + if(emitter != PP_MISSING) + parts |= "emitter" + if(speech_synthesizer != PP_MISSING) + parts |= "speech synthesizer" + + var/choice = tgui_input_list(user, "Which part would you like to remove?", "Remove part", parts) + if(choice) + playsound(src, 'sound/items/pickup/component.ogg', vary = TRUE) + else + return + if(!do_after(user, 3 SECONDS, target = src)) + return + switch(choice) + if("cell") + if(cell == PP_FUNCTIONAL) + new /obj/item/paiparts/cell(get_turf(user)) + else + new /obj/item/paiparts(get_turf(user)) + user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src].")) + cell = PP_MISSING + if("processor") + if(processor == PP_FUNCTIONAL) + new /obj/item/paiparts/processor(get_turf(user)) + else + new /obj/item/paiparts(get_turf(user)) + user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src].")) + processor = PP_MISSING + if("board") + board = PP_MISSING + if(board == PP_FUNCTIONAL) + new /obj/item/paiparts/board(get_turf(user)) + else + new /obj/item/paiparts(get_turf(user)) + user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src].")) + + if("capacitor") + if(capacitor == PP_FUNCTIONAL) + new /obj/item/paiparts/capacitor(get_turf(user)) + else + new /obj/item/paiparts(get_turf(user)) + user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src].")) + capacitor = PP_MISSING + if("projector") + if(projector == PP_FUNCTIONAL) + new /obj/item/paiparts/projector(get_turf(user)) + else + new /obj/item/paiparts(get_turf(user)) + user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src].")) + projector = PP_MISSING + if("emitter") + if(emitter == PP_FUNCTIONAL) + new /obj/item/paiparts/emitter(get_turf(user)) + else + new /obj/item/paiparts(get_turf(user)) + user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src].")) + emitter = PP_MISSING + if("speech synthesizer") + if(speech_synthesizer == PP_FUNCTIONAL) + new /obj/item/paiparts/speech_synthesizer(get_turf(user)) + else + new /obj/item/paiparts(get_turf(user)) + user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src].")) + speech_synthesizer = PP_MISSING + +/obj/item/paicard/proc/death_damage() + var/number = rand(1,4) + while(number) + number -- + switch(rand(1,4)) + if(1) + cell = PP_BROKEN + if(2) + processor = PP_BROKEN + if(3) + board = PP_BROKEN + if(4) + capacitor = PP_BROKEN + +/obj/item/paicard/proc/damage_random_component(nonfatal = FALSE) + var/datum/effect/effect/system/spark_spread/s = new /datum/effect/effect/system/spark_spread + s.set_up(2, 1, src) + s.start() + if(prob(80) || nonfatal) //Way more likely to be non-fatal part damage + switch(rand(1,3)) + if(1) + projector = PP_BROKEN + if(2) + emitter = PP_BROKEN + if(3) + speech_synthesizer = PP_BROKEN + else + switch(rand(1,4)) + if(1) + cell = PP_BROKEN + if(2) + processor = PP_BROKEN + if(3) + board = PP_BROKEN + if(4) + capacitor = PP_BROKEN + +/obj/item/paicard/proc/is_damage_critical() + if(cell != PP_FUNCTIONAL || processor != PP_FUNCTIONAL || board != PP_FUNCTIONAL || capacitor != PP_FUNCTIONAL) + return TRUE + return FALSE + +/////////////////////////////// +//////////pAI Parts ////////// +/////////////////////////////// +/obj/item/paiparts + name = "broken pAI component" + desc = "It's broken scrap from a pAI card!" + icon = 'icons/obj/paicard.dmi' + icon_state = "broken" + pickup_sound = 'sound/items/pickup/card.ogg' + drop_sound = 'sound/items/drop/card.ogg' + +/obj/item/paiparts/Initialize(mapload) + . = ..() + pixel_x = rand(-10,10) + pixel_y = rand(-10,10) + +/obj/item/paiparts/cell + name = "pAI power cell" + desc = "It's very small and efficient! It powers the pAI!" + icon_state = "cell" + +/obj/item/paiparts/processor + name = "pAI processor" + desc = "It's the brain of your computer friend!" + icon_state = "processor" + +/obj/item/paiparts/board + name = "pAI board" + desc = "It's the thing all the other parts get attatched to!" + icon_state = "board" + +/obj/item/paiparts/capacitor + name = "pAI capacitor" + desc = "It helps regulate power flow!" + icon_state = "capacitor" + +/obj/item/paiparts/projector + name = "pAI projector" + desc = "It projects the pAI's form!" + icon_state = "projector" + +/obj/item/paiparts/emitter + name = "pAI emitter" + desc = "It emits the fields to help the pAI get around!" + icon_state = "emitter" + +/obj/item/paiparts/speech_synthesizer + name = "pAI speech synthesizer" + desc = "It's a little voice box!" + icon_state = "speech_synthesizer" + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // VoreEdit: Living Machine Stuff after this. // This adds a var and proc for all machines to take a pAI. (The pAI can't control anything, it's just for RP.) // You need to add usage of the proc to each machine to actually add support. For an example of this, see code\modules\food\kitchen\microwave.dm +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /obj/machinery var/obj/item/paicard/paicard = null diff --git a/code/game/objects/items/devices/paicard_vr.dm b/code/game/objects/items/devices/paicard_vr.dm deleted file mode 100644 index e9db9a1806..0000000000 --- a/code/game/objects/items/devices/paicard_vr.dm +++ /dev/null @@ -1,386 +0,0 @@ -GLOBAL_LIST_EMPTY(paikeys) - -/obj/item/paicard - var/panel_open = FALSE - var/cell = PP_FUNCTIONAL //critical- power - var/processor = PP_FUNCTIONAL //critical- the thinky part - var/board = PP_FUNCTIONAL //critical- makes everything work - var/capacitor = PP_FUNCTIONAL //critical- power processing - var/projector = PP_FUNCTIONAL //non-critical- affects unfolding - var/emitter = PP_FUNCTIONAL //non-critical- affects unfolding - var/speech_synthesizer = PP_FUNCTIONAL //non-critical- affects speech - -/obj/item/paicard/attackby(var/obj/item/I as obj, mob/user as mob) - if(istype(I,/obj/item/tool/screwdriver)) - if(panel_open) - panel_open = FALSE - user.visible_message(span_notice("\The [user] secured \the [src]'s maintenance panel.")) - playsound(src, 'sound/items/Screwdriver.ogg', 50, 1) - else if(pai) - if(do_after(user, 3 SECONDS, target = src)) - panel_open = TRUE - user.visible_message(span_warning("\The [user] opened \the [src]'s maintenance panel.")) - playsound(src, 'sound/items/Screwdriver.ogg', 50, 1) - if(istype(I,/obj/item/robotanalyzer)) - if(!panel_open) - to_chat(user, span_warning("The panel isn't open. You will need to unscrew it to open it.")) - else - if(cell == PP_FUNCTIONAL) - to_chat(user,"Power cell: " + span_notice("functional")) - else if(cell == PP_BROKEN) - to_chat(user,"Power cell: " + span_warning("damaged - CRITICAL")) - else - to_chat(user,"Power cell: " + span_warning("missing - CRITICAL")) - - if(processor == PP_FUNCTIONAL) - to_chat(user,"Processor: " + span_notice("functional")) - else if(processor == PP_BROKEN) - to_chat(user,"Processor: " + span_warning("damaged - CRITICAL")) - else - to_chat(user,"Processor: " + span_warning("missing - CRITICAL")) - - if(board == PP_FUNCTIONAL) - to_chat(user,"Board: " + span_notice("functional")) - else if(board == PP_BROKEN) - to_chat(user,"Board: " + span_warning("damaged - CRITICAL")) - else - to_chat(user,"Board: " + span_warning("missing - CRITICAL")) - - if(capacitor == PP_FUNCTIONAL) - to_chat(user,"Capacitors: " + span_notice("functional")) - else if(capacitor == PP_BROKEN) - to_chat(user,"Capacitors: " + span_warning("damaged - CRITICAL")) - else - to_chat(user,"Capacitors: " + span_warning("missing - CRITICAL")) - - if(projector == PP_FUNCTIONAL) - to_chat(user,"Projectors: " + span_notice("functional")) - else if(projector == PP_BROKEN) - to_chat(user,"Projectors: " + span_warning("damaged")) - else - to_chat(user,"Projectors: " + span_warning("missing")) - - if(emitter == PP_FUNCTIONAL) - to_chat(user,"Emitters: " + span_notice("functional")) - else if(emitter == PP_BROKEN) - to_chat(user,"Emitters: " + span_warning("damaged")) - else - to_chat(user,"Emitters: " + span_warning("missing")) - - if(speech_synthesizer == PP_FUNCTIONAL) - to_chat(user,"Speech Synthesizer: " + span_notice("functional")) - else if(speech_synthesizer == PP_BROKEN) - to_chat(user,"Speech Synthesizer: " + span_warning("damaged")) - else - to_chat(user,"Speech Synthesizer: " + span_warning("missing")) - - if(istype(I,/obj/item/multitool)) - if(!panel_open) - to_chat(user, span_warning("You can't do that in this state.")) - else - var/list/parts = list() - if(cell != PP_MISSING) - parts |= "cell" - if(processor != PP_MISSING) - parts |= "processor" - if(board != PP_MISSING) - parts |= "board" - if(capacitor != PP_MISSING) - parts |= "capacitor" - if(projector != PP_MISSING) - parts |= "projector" - if(emitter != PP_MISSING) - parts |= "emitter" - if(speech_synthesizer != PP_MISSING) - parts |= "speech synthesizer" - - var/choice = tgui_input_list(user, "Which part would you like to check?", "Check part", parts) - switch(choice) - if("cell") - if(cell == PP_FUNCTIONAL) - to_chat(user,"Power cell: " + span_notice("functional")) - else if(speech_synthesizer == PP_BROKEN) - to_chat(user,"Power cell: " + span_warning("damaged")) - else - to_chat(user,"Power cell: " + span_warning("missing")) - - if("processor") - if(processor == PP_FUNCTIONAL) - to_chat(user,"Processor: " + span_notice("functional")) - else if(speech_synthesizer == PP_BROKEN) - to_chat(user,"Processor: " + span_warning("damaged")) - else - to_chat(user,"Processor: " + span_warning("missing")) - - if("board") - if(board == PP_FUNCTIONAL) - to_chat(user,"Board: " + span_notice("functional")) - else if(speech_synthesizer == PP_BROKEN) - to_chat(user,"Board: " + span_warning("damaged")) - else - to_chat(user,"Board: " + span_warning("missing")) - - if("capacitor") - if(capacitor == PP_FUNCTIONAL) - to_chat(user,"Capacitors: " + span_notice("functional")) - else if(speech_synthesizer == PP_BROKEN) - to_chat(user,"Capacitors: " + span_warning("damaged")) - else - to_chat(user,"Capacitors: " + span_warning("missing")) - - if("projector") - if(projector == PP_FUNCTIONAL) - to_chat(user,"Projectors: " + span_notice("functional")) - else if(speech_synthesizer == PP_BROKEN) - to_chat(user,"Projectors: " + span_warning("damaged")) - else - to_chat(user,"Projectors: " + span_warning("missing")) - - if("emitter") - if(emitter == PP_FUNCTIONAL) - to_chat(user,"Emitters: " + span_notice("functional")) - else if(speech_synthesizer == PP_BROKEN) - to_chat(user,"Emitters: " + span_warning("damaged")) - else - to_chat(user,"Emitters: " + span_warning("missing")) - - if("speech synthesizer") - if(speech_synthesizer == PP_FUNCTIONAL) - to_chat(user,"Speech Synthesizer: " + span_notice("functional")) - else if(speech_synthesizer == PP_BROKEN) - to_chat(user,"Speech Synthesizer: " + span_warning("damaged")) - else - to_chat(user,"Speech Synthesizer: " + span_warning("missing")) - - if(istype(I,/obj/item/paiparts/cell)) - if(cell == PP_MISSING) - if(do_after(user, 3 SECONDS, target = src)) - user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src].")) - cell = PP_FUNCTIONAL - user.drop_from_inventory(I) - qdel(I) - else - to_chat(user, span_warning("You would need to remove the installed [I] first!")) - if(istype(I,/obj/item/paiparts/processor)) - if(processor == PP_MISSING) - if(do_after(user, 3 SECONDS, target = src)) - user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src].")) - processor = PP_FUNCTIONAL - user.drop_from_inventory(I) - qdel(I) - else - to_chat(user, span_warning("You would need to remove the installed [I] first!")) - if(istype(I,/obj/item/paiparts/board)) - if(board == PP_MISSING) - if(do_after(user, 3 SECONDS, target = src)) - user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src].")) - board = PP_FUNCTIONAL - user.drop_from_inventory(I) - qdel(I) - else - to_chat(user, span_warning("You would need to remove the installed [I] first!")) - if(istype(I,/obj/item/paiparts/capacitor)) - if(capacitor == PP_MISSING) - if(do_after(user, 3 SECONDS, target = src)) - user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src].")) - capacitor = PP_FUNCTIONAL - user.drop_from_inventory(I) - qdel(I) - else - to_chat(user, span_warning("You would need to remove the installed [I] first!")) - if(istype(I,/obj/item/paiparts/projector)) - if(projector == PP_MISSING) - if(do_after(user, 3 SECONDS, target = src)) - user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src].")) - projector = PP_FUNCTIONAL - user.drop_from_inventory(I) - qdel(I) - else - to_chat(user, span_warning("You would need to remove the installed [I] first!")) - if(istype(I,/obj/item/paiparts/emitter)) - if(emitter == PP_MISSING) - if(do_after(user, 3 SECONDS, target = src)) - user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src].")) - emitter = PP_FUNCTIONAL - user.drop_from_inventory(I) - qdel(I) - else - to_chat(user, span_warning("You would need to remove the installed [I] first!")) - if(istype(I,/obj/item/paiparts/speech_synthesizer)) - if(speech_synthesizer == PP_MISSING) - if(do_after(user, 3 SECONDS, target = src)) - user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src].")) - speech_synthesizer = PP_FUNCTIONAL - user.drop_from_inventory(I) - qdel(I) - else - to_chat(user, span_warning("You would need to remove the installed [I] first!")) - -/obj/item/paicard/attack_self(mob/user, callback) - . = ..(user) - if(.) - return TRUE - if(special_handling && !callback) - return FALSE - if(!panel_open) - access_screen(user) - return - var/list/parts = list() - if(cell != PP_MISSING) - parts |= "cell" - if(processor != PP_MISSING) - parts |= "processor" - if(board != PP_MISSING) - parts |= "board" - if(capacitor != PP_MISSING) - parts |= "capacitor" - if(projector != PP_MISSING) - parts |= "projector" - if(emitter != PP_MISSING) - parts |= "emitter" - if(speech_synthesizer != PP_MISSING) - parts |= "speech synthesizer" - - var/choice = tgui_input_list(user, "Which part would you like to remove?", "Remove part", parts) - if(choice) - playsound(src, 'sound/items/pickup/component.ogg', vary = TRUE) - else - return - if(!do_after(user, 3 SECONDS, target = src)) - return - switch(choice) - if("cell") - if(cell == PP_FUNCTIONAL) - new /obj/item/paiparts/cell(get_turf(user)) - else - new /obj/item/paiparts(get_turf(user)) - user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src].")) - cell = PP_MISSING - if("processor") - if(processor == PP_FUNCTIONAL) - new /obj/item/paiparts/processor(get_turf(user)) - else - new /obj/item/paiparts(get_turf(user)) - user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src].")) - processor = PP_MISSING - if("board") - board = PP_MISSING - if(board == PP_FUNCTIONAL) - new /obj/item/paiparts/board(get_turf(user)) - else - new /obj/item/paiparts(get_turf(user)) - user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src].")) - - if("capacitor") - if(capacitor == PP_FUNCTIONAL) - new /obj/item/paiparts/capacitor(get_turf(user)) - else - new /obj/item/paiparts(get_turf(user)) - user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src].")) - capacitor = PP_MISSING - if("projector") - if(projector == PP_FUNCTIONAL) - new /obj/item/paiparts/projector(get_turf(user)) - else - new /obj/item/paiparts(get_turf(user)) - user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src].")) - projector = PP_MISSING - if("emitter") - if(emitter == PP_FUNCTIONAL) - new /obj/item/paiparts/emitter(get_turf(user)) - else - new /obj/item/paiparts(get_turf(user)) - user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src].")) - emitter = PP_MISSING - if("speech synthesizer") - if(speech_synthesizer == PP_FUNCTIONAL) - new /obj/item/paiparts/speech_synthesizer(get_turf(user)) - else - new /obj/item/paiparts(get_turf(user)) - user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src].")) - speech_synthesizer = PP_MISSING - -/obj/item/paicard/proc/death_damage() - - var/number = rand(1,4) - while(number) - number -- - switch(rand(1,4)) - if(1) - cell = PP_BROKEN - if(2) - processor = PP_BROKEN - if(3) - board = PP_BROKEN - if(4) - capacitor = PP_BROKEN - - -/obj/item/paicard/proc/damage_random_component(nonfatal = FALSE) - var/datum/effect/effect/system/spark_spread/s = new /datum/effect/effect/system/spark_spread - s.set_up(2, 1, src) - s.start() - if(prob(80) || nonfatal) //Way more likely to be non-fatal part damage - switch(rand(1,3)) - if(1) - projector = PP_BROKEN - if(2) - emitter = PP_BROKEN - if(3) - speech_synthesizer = PP_BROKEN - else - switch(rand(1,4)) - if(1) - cell = PP_BROKEN - if(2) - processor = PP_BROKEN - if(3) - board = PP_BROKEN - if(4) - capacitor = PP_BROKEN - -/obj/item/paicard/proc/is_damage_critical() - if(cell != PP_FUNCTIONAL || processor != PP_FUNCTIONAL || board != PP_FUNCTIONAL || capacitor != PP_FUNCTIONAL) - return TRUE - return FALSE - -/obj/item/paiparts - name = "broken pAI component" - desc = "It's broken scrap from a pAI card!" - icon = 'icons/obj/paicard.dmi' - icon_state = "broken" - pickup_sound = 'sound/items/pickup/card.ogg' - drop_sound = 'sound/items/drop/card.ogg' -/obj/item/paiparts/Initialize(mapload) - . = ..() - pixel_x = rand(-10,10) - pixel_y = rand(-10,10) - -/obj/item/paiparts/cell - name = "pAI power cell" - desc = "It's very small and efficient! It powers the pAI!" - icon_state = "cell" -/obj/item/paiparts/processor - name = "pAI processor" - desc = "It's the brain of your computer friend!" - icon_state = "processor" -/obj/item/paiparts/board - name = "pAI board" - desc = "It's the thing all the other parts get attatched to!" - icon_state = "board" -/obj/item/paiparts/capacitor - name = "pAI capacitor" - desc = "It helps regulate power flow!" - icon_state = "capacitor" -/obj/item/paiparts/projector - name = "pAI projector" - desc = "It projects the pAI's form!" - icon_state = "projector" -/obj/item/paiparts/emitter - name = "pAI emitter" - desc = "It emits the fields to help the pAI get around!" - icon_state = "emitter" -/obj/item/paiparts/speech_synthesizer - name = "pAI speech synthesizer" - desc = "It's a little voice box!" - icon_state = "speech_synthesizer" diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index c382a84b5d..1936cf9040 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -144,6 +144,10 @@ /obj/proc/container_resist(var/mob/living) return +// If returns true, pai can interact with the object with a click +/obj/proc/allow_pai_interaction(proximity_flag) + return FALSE + //To be called from things that spill objects on the floor. //Makes an object move around randomly for a couple of tiles /obj/proc/tumble(var/dist = 2) diff --git a/code/game/objects/structures/crates_lockers/__closets.dm b/code/game/objects/structures/crates_lockers/__closets.dm index dc067df234..54632231e0 100644 --- a/code/game/objects/structures/crates_lockers/__closets.dm +++ b/code/game/objects/structures/crates_lockers/__closets.dm @@ -539,6 +539,9 @@ M.Translate(closet_appearance.door_hinge, 0) return M +/obj/structure/closet/allow_pai_interaction(proximity_flag) + return proximity_flag + //verb to eat people in the same closet as yourself /obj/structure/closet/verb/hidden_vore() diff --git a/code/game/objects/structures/fence.dm b/code/game/objects/structures/fence.dm index 4e0dafc9ac..298e5e33eb 100644 --- a/code/game/objects/structures/fence.dm +++ b/code/game/objects/structures/fence.dm @@ -211,6 +211,9 @@ if(get_dist(user,src) <= 1) //not remotely though return attack_hand(user) +/obj/structure/fence/door/allow_pai_interaction(proximity_flag) + return proximity_flag + /obj/structure/fence/door/proc/toggle(mob/user) switch(open) if(FALSE) diff --git a/code/game/objects/structures/watercloset.dm b/code/game/objects/structures/watercloset.dm index 4f5e054119..dd511a7e6c 100644 --- a/code/game/objects/structures/watercloset.dm +++ b/code/game/objects/structures/watercloset.dm @@ -201,6 +201,9 @@ if(istype(I, /obj/item/analyzer)) //Lol? Why... to_chat(user, span_notice("The water temperature seems to be [current_temperature].")) +/obj/machinery/shower/allow_pai_interaction(proximity_flag) + return proximity_flag + /obj/machinery/shower/click_alt(mob/user) ..() var/list/temperature_settings = list(SHOWER_NORMAL, SHOWER_BOILING, SHOWER_FREEZING) diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm index 12699adf13..8074220965 100644 --- a/code/modules/admin/verbs/debug.dm +++ b/code/modules/admin/verbs/debug.dm @@ -150,13 +150,9 @@ pai.key = choice.key card.setPersonality(pai) if(tgui_alert(pai, "Do you want to load your pAI data?", "Load", list("Yes", "No")) == "Yes") - pai.savefile_load(pai) + pai.apply_preferences(pai.client) else pai.name = sanitizeSafe(tgui_input_text(pai, "Enter your pAI name:", "pAI Name", "Personal AI", encode = FALSE)) - card.setPersonality(pai) - for(var/datum/paiCandidate/candidate in paiController.pai_candidates) - if(candidate.key == choice.key) - paiController.pai_candidates.Remove(candidate) log_admin("made a pAI with key=[pai.key] at ([T.x],[T.y],[T.z])") feedback_add_details("admin_verb","MPAI") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! diff --git a/code/modules/asset_cache/assets/spritesheets/pai_icons.dm b/code/modules/asset_cache/assets/spritesheets/pai_icons.dm new file mode 100644 index 0000000000..75fffd8732 --- /dev/null +++ b/code/modules/asset_cache/assets/spritesheets/pai_icons.dm @@ -0,0 +1,64 @@ +/datum/asset/spritesheet_batched/pai_icons + name = "pai_icons" + +/datum/asset/spritesheet_batched/pai_icons/create_spritesheets() + for(var/name, current_sprite in SSpai.get_chassis_list()) + var/datum/pai_sprite/sprite = current_sprite + if(!sprite.name || !sprite.sprite_icon_state) + continue + + var/datum/universal_icon/I_N = uni_icon(sprite.sprite_icon, sprite.sprite_icon_state, NORTH) + var/datum/universal_icon/I_S = uni_icon(sprite.sprite_icon, sprite.sprite_icon_state, SOUTH) + var/datum/universal_icon/I_W = uni_icon(sprite.sprite_icon, sprite.sprite_icon_state, WEST) + var/datum/universal_icon/I_E = uni_icon(sprite.sprite_icon, sprite.sprite_icon_state, EAST) + + if(sprite.icon_x > sprite.icon_y) + var/buffer = (sprite.icon_x - sprite.icon_y) / 2 + I_N.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer) + I_S.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer) + I_W.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer) + I_E.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer) + else if (sprite.icon_x < sprite.icon_y) + var/buffer = (sprite.icon_y - sprite.icon_x) / 2 + I_N.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y) + I_S.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y) + I_W.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y) + I_E.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y) + I_N.scale(120, 120) + I_S.scale(120, 120) + I_W.scale(120, 120) + I_E.scale(120, 120) + + var/imgid = sanitize_css_class_name("[sprite.type]") + insert_icon(imgid + "N", I_N) + insert_icon(imgid + "S", I_S) + insert_icon(imgid + "W", I_W) + insert_icon(imgid + "E", I_E) + + if(sprite.has_eye_sprites) + var/datum/universal_icon/I_NE = uni_icon(sprite.sprite_icon, "[sprite.sprite_icon_state]-eyes", NORTH) + var/datum/universal_icon/I_SE = uni_icon(sprite.sprite_icon, "[sprite.sprite_icon_state]-eyes", SOUTH) + var/datum/universal_icon/I_WE = uni_icon(sprite.sprite_icon, "[sprite.sprite_icon_state]-eyes", WEST) + var/datum/universal_icon/I_EE = uni_icon(sprite.sprite_icon, "[sprite.sprite_icon_state]-eyes", EAST) + + if(sprite.icon_x > sprite.icon_y) + var/buffer = (sprite.icon_x - sprite.icon_y) / 2 + I_NE.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer) + I_SE.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer) + I_WE.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer) + I_EE.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer) + else if (sprite.icon_x < sprite.icon_y) + var/buffer = (sprite.icon_y - sprite.icon_x) / 2 + I_NE.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y) + I_SE.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y) + I_WE.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y) + I_EE.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y) + I_NE.scale(120, 120) + I_SE.scale(120, 120) + I_WE.scale(120, 120) + I_EE.scale(120, 120) + + insert_icon(imgid + "NE", I_NE) + insert_icon(imgid + "SE", I_SE) + insert_icon(imgid + "WE", I_WE) + insert_icon(imgid + "EE", I_EE) diff --git a/code/modules/asset_cache/assets/spritesheets/robot_icons.dm b/code/modules/asset_cache/assets/spritesheets/robot_icons.dm index 429e5f3432..9194281420 100644 --- a/code/modules/asset_cache/assets/spritesheets/robot_icons.dm +++ b/code/modules/asset_cache/assets/spritesheets/robot_icons.dm @@ -103,20 +103,19 @@ GLOBAL_LIST_EMPTY(robot_sprite_sheets) var/datum/universal_icon/I_NE = uni_icon(S.sprite_icon, "[S.sprite_icon_state]-eyes", NORTH) if(I_NE) I_N.blend_icon(I_NE, ICON_OVERLAY) - if(S.has_eye_sprites) + var/datum/universal_icon/I_SE = uni_icon(S.sprite_icon, "[S.sprite_icon_state]-eyes", SOUTH) if(I_SE) I_S.blend_icon(I_SE, ICON_OVERLAY) - if(S.has_eye_sprites) + var/datum/universal_icon/I_WE = uni_icon(S.sprite_icon, "[S.sprite_icon_state]-eyes", WEST) if(I_WE) I_W.blend_icon(I_WE, ICON_OVERLAY) - if(S.has_eye_sprites) + var/datum/universal_icon/I_EE = uni_icon(S.sprite_icon, "[S.sprite_icon_state]-eyes", EAST) if(I_EE) I_E.blend_icon(I_EE, ICON_OVERLAY) - var/imgid = sanitize_css_class_name("[S.type]") if(S.icon_x > S.icon_y) var/buffer = (S.icon_x - S.icon_y) / 2 I_N.crop(0, -buffer, S.icon_x, S.icon_y + buffer) @@ -134,6 +133,7 @@ GLOBAL_LIST_EMPTY(robot_sprite_sheets) I_W.scale(120, 120) I_E.scale(120, 120) + var/imgid = sanitize_css_class_name("[S.type]") insert_icon(imgid + "N", I_N) insert_icon(imgid + "S", I_S) insert_icon(imgid + "W", I_W) diff --git a/code/modules/client/preference_setup/general/07_antagonism.dm b/code/modules/client/preference_setup/general/07_antagonism.dm index 9d14c3654f..48f0c4a7fa 100644 --- a/code/modules/client/preference_setup/general/07_antagonism.dm +++ b/code/modules/client/preference_setup/general/07_antagonism.dm @@ -2,12 +2,6 @@ name = "Basic" sort_order = 7 - var/datum/paiCandidate/candidate - -/datum/category_item/player_setup_item/general/basic_antagonism/New() - . = ..() - candidate = new() - /datum/category_item/player_setup_item/general/basic_antagonism/load_character(list/save_data) pref.exploit_record = save_data["exploit_record"] pref.antag_faction = save_data["antag_faction"] @@ -19,29 +13,18 @@ save_data["antag_vis"] = pref.antag_vis /datum/category_item/player_setup_item/general/basic_antagonism/load_preferences(datum/json_savefile/savefile) - if(!candidate) - candidate = new() - var/preference_mob = preference_mob() if(!preference_mob)// No preference mob - this happens when we're called from client/New() before it calls ..() (via datum/preferences/New()) spawn() preference_mob = preference_mob() if(!preference_mob) return - candidate.savefile_load(preference_mob) return - candidate.savefile_load(preference_mob) - /datum/category_item/player_setup_item/general/basic_antagonism/save_preferences(datum/json_savefile/savefile) - if(!candidate) - return - if(!preference_mob()) return - candidate.savefile_save(preference_mob()) - /datum/category_item/player_setup_item/general/basic_antagonism/sanitize_character() if(!pref.antag_faction) pref.antag_faction = "None" if(!pref.antag_vis) pref.antag_vis = "Hidden" @@ -62,13 +45,23 @@ if(!jobban_isbanned(user, "Records")) data["exploitable_record"] = TextPreview(pref.exploit_record, 40) - if(!candidate) - CRASH("[user] pAI prefs have a null candidate var.") + data["pai_name"] = pref.read_preference(/datum/preference/text/pai_name) + data["pai_desc"] = pref.read_preference(/datum/preference/text/pai_description) + data["pai_role"] = pref.read_preference(/datum/preference/text/pai_role) + data["pai_ad"] = pref.read_preference(/datum/preference/text/pai_ad) + data["pai_comments"] = pref.read_preference(/datum/preference/text/pai_comments) + data["pai_eyecolor"] = pref.read_preference(/datum/preference/color/pai_eye_color) + var/pai_chassis = pref.read_preference(/datum/preference/text/pai_chassis) + data["pai_chassis"] = pai_chassis + data["pai_chassises"] = SSpai.get_chassis_list() + data["pai_emotion"] = pref.read_preference(/datum/preference/text/pai_emotion) + data["pai_emotions"] = GLOB.pai_emotions - data["pai_name"] = candidate.name ? candidate.name : "None Set" - data["pai_desc"] = candidate.description ? TextPreview(candidate.description, 40) : "None Set" - data["pai_role"] = candidate.role ? TextPreview(candidate.role, 40) : "None Set" - data["pai_comments"] = candidate.comments ? TextPreview(candidate.comments, 40) : "None Set" + var/datum/pai_sprite/sprite_datum = SSpai.chassis_data(pai_chassis) + if(sprite_datum) + var/datum/asset/spritesheet_batched/pai_icons/spritesheet = get_asset_datum(/datum/asset/spritesheet_batched/pai_icons) + data["pai_sprite_datum_class"] = sanitize_css_class_name("[sprite_datum.type]") + data["pai_sprite_datum_size"] = spritesheet.icon_size_id(data["pai_sprite_datum_class"] + "S") // just get the south icon's size, the rest will be the same return data @@ -111,23 +104,39 @@ pref.antag_vis = choice return TOPIC_REFRESH - if("option") + if("pai_option") var/t - switch(params["option"]) + switch(params["pai_option"]) if("name") - t = sanitizeName(tgui_input_text(user, "Enter a name for your pAI", "Global Preference", candidate.name, MAX_NAME_LEN), MAX_NAME_LEN, 1) + t = sanitizeName(params["name"], MAX_NAME_LEN, TRUE) if(t && CanUseTopic(user)) - candidate.name = t + pref.update_preference_by_type(/datum/preference/text/pai_name, t) if("desc") - t = tgui_input_text(user, "Enter a description for your pAI", "Global Preference", html_decode(candidate.description), multiline = TRUE, prevent_enter = TRUE) + t = sanitize(params["desc"], MAX_MESSAGE_LEN * 4) if(!isnull(t) && CanUseTopic(user)) - candidate.description = sanitize(t) + pref.update_preference_by_type(/datum/preference/text/pai_description, sanitize(t)) + if("ad") + t = sanitize(params["ad"]) + if(!isnull(t) && CanUseTopic(user)) + pref.update_preference_by_type(/datum/preference/text/pai_ad, sanitize(t)) if("role") - t = tgui_input_text(user, "Enter a role for your pAI", "Global Preference", html_decode(candidate.role)) + t = sanitize(params["role"]) if(!isnull(t) && CanUseTopic(user)) - candidate.role = sanitize(t) + pref.update_preference_by_type(/datum/preference/text/pai_role, sanitize(t)) if("ooc") - t = tgui_input_text(user, "Enter any OOC comments", "Global Preference", html_decode(candidate.comments), multiline = TRUE, prevent_enter = TRUE) + t = sanitize(params["ooc"]) if(!isnull(t) && CanUseTopic(user)) - candidate.comments = sanitize(t) + pref.update_preference_by_type(/datum/preference/text/pai_comments, sanitize(t)) + if("color") + var/new_color = sanitize_hexcolor(params["color"]) + if(new_color && CanUseTopic(user)) + pref.update_preference_by_type(/datum/preference/color/pai_eye_color, new_color) + if("chassis") + var/new_chassis = params["chassis"] + if(new_chassis && CanUseTopic(user) && (new_chassis in SSpai.get_chassis_list())) + pref.update_preference_by_type(/datum/preference/text/pai_chassis, new_chassis) + if("emotion") + var/new_emotion = params["emotion"] + if(new_emotion && CanUseTopic(user) && (new_emotion in GLOB.pai_emotions)) + pref.update_preference_by_type(/datum/preference/text/pai_emotion, new_emotion) return TOPIC_REFRESH diff --git a/code/modules/client/preferences/migrations/19_paifile.dm b/code/modules/client/preferences/migrations/19_paifile.dm new file mode 100644 index 0000000000..8cab27586d --- /dev/null +++ b/code/modules/client/preferences/migrations/19_paifile.dm @@ -0,0 +1,57 @@ +/// Moves pai save file to tg pref in current file slot +/datum/preferences/proc/migration_19_paifile(datum/json_savefile/S) + //Not everyone has a pai savefile. + var/pai_path = "data/player_saves/[copytext(client_ckey, 1, 2)]/[client_ckey]/pai.sav" + if(!fexists(pai_path)) + return + var/savefile/F = new /savefile(pai_path) + if(!F) + return + + // Should never happen, but if someone has a wonked out file then lets avoid it too + var/version = null + F["version"] >> version + if(isnull(version) || version != 1) + fdel(pai_path) + return + + // Put the pai in slot 0 + var/list/prefs = S.get_entry("character1", null) + if(!islist(prefs)) + return + + // Get data + var/pai_name + var/pai_description + var/pai_role + var/pai_comments + var/pai_eye_color + var/pai_chassis + var/pai_ouremotion + F["name"] >> pai_name + F["description"] >> pai_description + F["role"] >> pai_role + F["comments"] >> pai_comments + F["eyecolor"] >> pai_eye_color + F["chassis"] >> pai_chassis + F["emotion"] >> pai_ouremotion + // F["gender"] >> pai_gender // We use the character slot's now + + if(pai_name) + prefs["Pai_Name"] = pai_name + if(pai_role) + prefs["Pai_Role"] = pai_role + if(pai_description) + prefs["Pai_Desc"] = pai_description + if(pai_comments) + prefs["Pai_Comments"] = pai_comments + if(pai_eye_color) + prefs["Pai_EyeColor"] = pai_eye_color + if(pai_chassis) + prefs["Pai_Chassis"] = pai_chassis + if(pai_ouremotion) + prefs["Pai_Emotion"] = pai_ouremotion + + fdel(pai_path) + + S.save() diff --git a/code/modules/client/preferences/types/character/pai/01_basic.dm b/code/modules/client/preferences/types/character/pai/01_basic.dm new file mode 100644 index 0000000000..b5df4c5c04 --- /dev/null +++ b/code/modules/client/preferences/types/character/pai/01_basic.dm @@ -0,0 +1,116 @@ +/datum/preference/text/pai_name + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "Pai_Name" + maximum_value_length = MAX_NAME_LEN + can_randomize = FALSE + +/datum/preference/text/pai_name/create_default_value() + return PAI_UNSET + +/datum/preference/text/pai_name/apply_pref_to(mob/living, value) + return + + +/datum/preference/text/pai_description + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "Pai_Desc" + maximum_value_length = MAX_MESSAGE_LEN * 4 + can_randomize = FALSE + +/datum/preference/text/pai_description/create_default_value() + return PAI_UNSET + +/datum/preference/text/pai_description/apply_pref_to(mob/living, value) + return + + +/datum/preference/text/pai_role + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "Pai_Role" + maximum_value_length = MAX_MESSAGE_LEN + can_randomize = FALSE + +/datum/preference/text/pai_role/create_default_value() + return PAI_UNSET + +/datum/preference/text/pai_role/apply_pref_to(mob/living, value) + return + + +/datum/preference/text/pai_ad + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "Pai_Ad" + maximum_value_length = MAX_MESSAGE_LEN + can_randomize = FALSE + +/datum/preference/text/pai_ad/create_default_value() + return PAI_UNSET + +/datum/preference/text/pai_ad/apply_pref_to(mob/living, value) + return + + +/datum/preference/text/pai_comments + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "Pai_Comments" + maximum_value_length = MAX_MESSAGE_LEN + can_randomize = FALSE + +/datum/preference/text/pai_comments/create_default_value() + return PAI_UNSET + +/datum/preference/text/pai_comments/apply_pref_to(mob/living, value) + return + + +/datum/preference/color/pai_eye_color + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "Pai_EyeColor" + can_randomize = TRUE + +/datum/preference/color/pai_eye_color/apply_pref_to(mob/living, value) + return + + +/datum/preference/text/pai_chassis + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "Pai_Chassis" + maximum_value_length = MAX_NAME_LEN + can_randomize = FALSE + +/datum/preference/text/pai_chassis/create_default_value() + return PAI_DEFAULT_CHASSIS + +/datum/preference/text/pai_chassis/is_valid(value) + if(!(value in SSpai.get_chassis_list())) + return FALSE + . = ..() + +/datum/preference/text/pai_chassis/apply_pref_to(mob/living, value) + return + + +/datum/preference/text/pai_emotion + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "Pai_Emotion" + maximum_value_length = MAX_NAME_LEN + can_randomize = FALSE + +/datum/preference/text/pai_emotion/create_default_value() + return GLOB.pai_emotions[1] + +/datum/preference/text/pai_emotion/is_valid(value) + if(!(value in GLOB.pai_emotions)) + return FALSE + . = ..() + +/datum/preference/text/pai_emotion/apply_pref_to(mob/living, value) + return diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index 858a26269a..37534d4dc0 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -1,5 +1,5 @@ #define SAVEFILE_VERSION_MIN 8 -#define SAVEFILE_VERSION_MAX 18 +#define SAVEFILE_VERSION_MAX 19 /* SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Carn @@ -98,6 +98,16 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car log_world("[client_ckey] preferences successfully migrated from [current_version] to v18.") to_chat(client, span_danger("v18 savefile migration complete.")) + + // Migration for pai to tg pregs + if(current_version < 19) + log_world("[client_ckey] preferences migrating from [current_version] to v19....") + to_chat(client, span_danger("Migrating savefile from version [current_version] to v19...")) + + migration_19_paifile(S) + + log_world("[client_ckey] preferences successfully migrated from [current_version] to v19.") + to_chat(client, span_danger("v19 savefile migration complete.")) /datum/preferences/proc/update_character(current_version, list/save_data) // Migration from BYOND savefiles to JSON: Important milemark. if(current_version == -3) diff --git a/code/modules/client/preferences_tgui.dm b/code/modules/client/preferences_tgui.dm index 87e63ff698..3e1c5069ba 100644 --- a/code/modules/client/preferences_tgui.dm +++ b/code/modules/client/preferences_tgui.dm @@ -24,6 +24,7 @@ get_asset_datum(/datum/asset/simple/preferences), get_asset_datum(/datum/asset/spritesheet/preferences), get_asset_datum(/datum/asset/json/preferences), + get_asset_datum(/datum/asset/spritesheet_batched/pai_icons), ) for (var/datum/preference_middleware/preference_middleware as anything in middleware) diff --git a/code/modules/emotes/definitions/audible_furry_vr.dm b/code/modules/emotes/definitions/audible_furry_vr.dm index 4e2aa85770..479f91cde0 100644 --- a/code/modules/emotes/definitions/audible_furry_vr.dm +++ b/code/modules/emotes/definitions/audible_furry_vr.dm @@ -175,7 +175,7 @@ emote_sound = pick(bigsound) else if(ispAI(user)) var/mob/living/silicon/pai/me = user - if(me.chassis == "teppi") + if(istype(SSpai.chassis_data(me.chassis_name), /datum/pai_sprite/large)) emote_sound = pick(bigsound) else if(user.size_multiplier >= 1.5) emote_sound = pick(bigsound) @@ -202,7 +202,7 @@ emote_sound = pick(bigsound) else if(ispAI(user)) var/mob/living/silicon/pai/me = user - if(me.chassis == "teppi") + if(istype(SSpai.chassis_data(me.chassis_name), /datum/pai_sprite/large)) emote_sound = pick(bigsound) else if(user.size_multiplier >= 1.5) emote_sound = pick(bigsound) diff --git a/code/modules/instruments/stationary.dm b/code/modules/instruments/stationary.dm index d2d83fd9a0..71f4bb930c 100644 --- a/code/modules/instruments/stationary.dm +++ b/code/modules/instruments/stationary.dm @@ -40,6 +40,9 @@ /obj/structure/musician/tgui_interact(mob/user) return song.tgui_interact(user) +/obj/structure/musician/allow_pai_interaction(proximity_flag) + return proximity_flag + /* FIXME /obj/structure/musician/wrench_act(mob/living/user, obj/item/tool) . = ..() diff --git a/code/modules/mining/shelters_vr.dm b/code/modules/mining/shelters_vr.dm index 45fcac9f0f..4ae99d1af6 100644 --- a/code/modules/mining/shelters_vr.dm +++ b/code/modules/mining/shelters_vr.dm @@ -16,7 +16,7 @@ /datum/map_template/shelter/proc/check_deploy(turf/deploy_location, var/is_ship) var/affected = get_affected_turfs(deploy_location, centered=TRUE) for(var/turf/T in affected) - var/shelter_status = get_turf_deployability(T) + var/shelter_status = get_turf_deployability(T, is_ship) if(shelter_status != SHELTER_DEPLOY_ALLOWED) return shelter_status return SHELTER_DEPLOY_ALLOWED diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index 7a3f2efd65..7aaadfd01a 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -965,24 +965,28 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp to_chat(src,span_warning("You cannot alert pAI cards when you are banned from playing as a pAI.")) return - if(src.client.prefs?.be_special & BE_PAI) - var/choice = tgui_alert(src, "Would you like to submit yourself to the recruitment list too?", "Confirmation", list("No", "Yes")) - if(!choice) - return - if(choice == "Yes") - paiController.recruitWindow(src) - var/count = 0 - for(var/obj/item/paicard/p in GLOB.all_pai_cards) - var/obj/item/paicard/PP = p - if(PP.pai == null) - count++ - PP.add_overlay("pai-ghostalert") - PP.alertUpdate() - spawn(54) - PP.cut_overlays() - to_chat(src,span_notice("Flashing the displays of [count] unoccupied PAIs.")) - else - to_chat(src,span_warning("You have 'Be pAI' disabled in your character prefs, so we can't help you.")) + if(!(src.client.prefs?.be_special & BE_PAI)) + to_chat(src,span_warning("You have 'Be pAI' disabled in your character prefs.")) + return + + var/choice = tgui_alert(src, "Would you like to submit yourself to the recruitment list too?", "Confirmation", list("No", "Yes")) + if(!choice || choice != "Yes") + return + + to_chat(src,span_notice("Flashing the displays of [pai_card_ping()] unoccupied PAIs.")) + +/mob/observer/dead/proc/pai_card_ping() + var/count = 0 + for(var/obj/item/paicard/p in GLOB.all_pai_cards) + var/obj/item/paicard/PP = p + if(PP.pai) + continue + count++ + PP.cut_overlays() + PP.add_overlay("pai-ghostalert") + PP.alertUpdate() + addtimer(CALLBACK(PP, TYPE_PROC_REF(/obj/item/paicard, clear_invite_overlay)), 1 MINUTE, TIMER_DELETE_ME) + return count /mob/observer/dead/speech_bubble_appearance() return "ghost" diff --git a/code/modules/mob/living/login.dm b/code/modules/mob/living/login.dm index 1d117d730d..56bc850d3d 100644 --- a/code/modules/mob/living/login.dm +++ b/code/modules/mob/living/login.dm @@ -37,7 +37,7 @@ add_verb(src,/mob/proc/nme_vore_ch) add_verb(src,/mob/proc/enter_soulcatcher) - if(!voice_sounds_list.len || !voice_sounds_list) + if(!length(voice_sounds_list)) if(client.prefs.voice_sound) var/prefsound = client.prefs.voice_sound voice_sounds_list = get_talk_sound(prefsound) diff --git a/code/modules/mob/living/silicon/pai/chassis_ui.dm b/code/modules/mob/living/silicon/pai/chassis_ui.dm new file mode 100644 index 0000000000..4624ed60f7 --- /dev/null +++ b/code/modules/mob/living/silicon/pai/chassis_ui.dm @@ -0,0 +1,69 @@ +// Pai chassis selection +/datum/tgui_module/pai_chassis + name = "PAI Chassis Configurator" + tgui_id = "PaiChoose" + var/selected_chassis + var/selected_color + +/datum/tgui_module/pai_chassis/tgui_state(mob/user) + return GLOB.tgui_self_state + +/datum/tgui_module/pai_chassis/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet_batched/pai_icons), + ) + +/datum/tgui_module/pai_chassis/tgui_static_data() + var/list/data = ..() + var/list/available_sprites = list() + for(var/key, value in SSpai.get_chassis_list()) + var/datum/pai_sprite/current_sprite = value + var/model_type = "def" + if(istype(current_sprite, /datum/pai_sprite/large)) + model_type = "big" + UNTYPED_LIST_ADD(available_sprites, list("sprite" = current_sprite.name, "belly" = current_sprite.belly_states, "type" = model_type)) + data["pai_chassises"] = available_sprites + + return data + +/datum/tgui_module/pai_chassis/tgui_data() + var/list/data = ..() + + var/mob/living/silicon/pai/pai_host = host + data["pai_color"] = selected_color ? selected_color : pai_host.eye_color + + var/datum/pai_sprite/sprite_datum = SSpai.chassis_data(selected_chassis || pai_host.chassis_name) + if(sprite_datum) + var/datum/asset/spritesheet_batched/pai_icons/spritesheet = get_asset_datum(/datum/asset/spritesheet_batched/pai_icons) + data["pai_chassis"] = sprite_datum.name + data["selected_chassis"] = selected_chassis + data["sprite_datum_class"] = sanitize_css_class_name("[sprite_datum.type]") + data["sprite_datum_size"] = spritesheet.icon_size_id(data["sprite_datum_class"] + "S") // just get the south icon's size, the rest will be the same + + return data + +/datum/tgui_module/pai_chassis/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + . = ..() + if(.) + return + + switch(action) + if("pick_icon") + var/new_chassis = params["value"] + if(new_chassis && (new_chassis in SSpai.get_chassis_list())) + selected_chassis = new_chassis + return TRUE + if("confirm") + if(!selected_chassis) + return FALSE + var/mob/living/silicon/pai/pai_host = host + if(selected_color) + pai_host.eye_color = selected_color + pai_host.change_chassis(selected_chassis) + return TRUE + if("change_color") + var/new_color = sanitize_hexcolor(params["color"]) + if(!new_color) + return FALSE + selected_color = new_color + return TRUE diff --git a/code/modules/mob/living/silicon/pai/death.dm b/code/modules/mob/living/silicon/pai/death.dm index 5180ec1a90..30edde7839 100644 --- a/code/modules/mob/living/silicon/pai/death.dm +++ b/code/modules/mob/living/silicon/pai/death.dm @@ -19,7 +19,8 @@ card.damage_random_component() if(gibbed) - qdel(card) + if(!QDELETED(card)) // Either the pai or card could be deleted first, prevent a loop + qdel(card) ..(gibbed) else card.add_overlay("pai-dead") diff --git a/code/modules/mob/living/silicon/pai/life.dm b/code/modules/mob/living/silicon/pai/life.dm index b2893fe172..c518959c65 100644 --- a/code/modules/mob/living/silicon/pai/life.dm +++ b/code/modules/mob/living/silicon/pai/life.dm @@ -1,16 +1,7 @@ /mob/living/silicon/pai/Life() + check_retract_cable() - if(src.cable) - if(get_dist(src, src.cable) > 1) - var/turf/T = get_turf_or_move(src.loc) - for (var/mob/M in viewers(T)) - M.show_message(span_red("The data cable rapidly retracts back into its spool."), 3, span_red("You hear a click and the sound of wire spooling rapidly."), 2) - playsound(src, 'sound/machines/click.ogg', 50, 1) - - qdel(src.cable) - src.cable = null - - if (src.stat == DEAD) + if(stat == DEAD) return if(card.cell != PP_FUNCTIONAL|| card.processor != PP_FUNCTIONAL || card.board != PP_FUNCTIONAL || card.capacitor != PP_FUNCTIONAL) diff --git a/code/modules/mob/living/silicon/pai/pai.dm b/code/modules/mob/living/silicon/pai/pai.dm index e02028bfb8..0a463d9d53 100644 --- a/code/modules/mob/living/silicon/pai/pai.dm +++ b/code/modules/mob/living/silicon/pai/pai.dm @@ -6,6 +6,7 @@ emote_type = 2 // pAIs emotes are heard, not seen, so they can be seen through a container (eg. person) pass_flags = 1 mob_size = MOB_SMALL + softfall = TRUE holder_type = /obj/item/holder/pai @@ -25,7 +26,9 @@ var/obj/item/radio/borg/pai/radio // Our primary radio var/obj/item/communicator/integrated/communicator // Our integrated communicator. - var/chassis = "pai-repairbot" // A record of your chosen chassis. + var/atom/movable/screen/pai/pai_fold_display = null + + var/chassis_name = PAI_DEFAULT_CHASSIS // A record of your chosen chassis. var/obj/item/pai_cable/cable // The cable we produce and use when door or camera jacking @@ -68,15 +71,47 @@ var/our_icon_rotation = 0 + var/eye_glow = TRUE + var/hide_glow = FALSE + var/image/eye_layer = null // Holds the eye overlay. + var/eye_color = "#00ff0d" + var/icon/holo_icon_south + var/icon/holo_icon_north + var/icon/holo_icon_east + var/icon/holo_icon_west + var/holo_icon_dimension_X = 32 + var/holo_icon_dimension_Y = 32 + + //These vars keep track of whether you have the related software, used for easily updating the UI + var/soft_ut = FALSE //universal translator + var/soft_mr = FALSE //medical records + var/soft_sr = FALSE //security records + var/soft_dj = FALSE //door jack + var/soft_as = FALSE //atmosphere sensor + var/soft_si = FALSE //signaler + var/soft_ar = FALSE //ar hud + var/soft_da = FALSE //death alarm + + var/datum/tgui_module/pai_chassis/pai_ui_chassis + + vore_capacity = 1 + vore_capacity_ex = list("stomach" = 1) + + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Init and destroy +////////////////////////////////////////////////////////////////////////////////////////////////// /mob/living/silicon/pai/Initialize(mapload) . = ..() card = loc if(!istype(card)) - return INITIALIZE_HINT_QDEL + card = new(src) + card.pai = src sradio = new(src) communicator = new(src) + pai_ui_chassis = new(src) if(card) if(!card.radio) card.radio = new /obj/item/radio/borg/pai(src.card) @@ -92,6 +127,12 @@ add_verb(src, /mob/living/silicon/pai/proc/choose_chassis) add_verb(src, /mob/living/silicon/pai/proc/choose_verbs) + add_verb(src, /mob/proc/dominate_predator) + add_verb(src, /mob/living/proc/dominate_prey) + add_verb(src, /mob/living/proc/set_size) + add_verb(src, /mob/living/proc/shred_limb) + + remove_verb(src, /mob/verb/toggle_gun_mode) // Pai doesn't have support for this and shouldn't be able to use guns anyway //PDA pda = new(src) @@ -104,7 +145,11 @@ M.toff = FALSE /mob/living/silicon/pai/Login() - ..() + . = ..() + if(!holo_icon_south) + last_special = world.time + 100 //Let's give get_character_icon time to work + get_character_icon() + // Vorestation Edit: Meta Info for pAI if (client.prefs) ooc_notes = client.prefs.read_preference(/datum/preference/text/living/ooc_notes) @@ -117,19 +162,233 @@ src << sound('sound/effects/pai_login.ogg', volume = 75) //VOREStation Add -// this function shows the information about being silenced as a pAI in the Status panel -/mob/living/silicon/pai/proc/show_silenced() - . = "" - if(src.silence_time) - var/timeleft = round((silence_time - world.timeofday)/10 ,1) - . += "Communications system reboot in -[(timeleft / 60) % 60]:[add_zero(num2text(timeleft % 60), 2)]" +/// Load pref save data from client and apply it to the pai. +/mob/living/silicon/pai/proc/apply_preferences(client/cli, var/silent = 1) + if(!cli?.prefs) + return FALSE + var/datum/preferences/pref = cli.prefs + SetName(pref.read_preference(/datum/preference/text/pai_name)) + flavor_text = pref.read_preference(/datum/preference/text/pai_description) + change_chassis(pref.read_preference(/datum/preference/text/pai_chassis)) + gender = pref.read_preference(/datum/preference/choiced/gender/biological) // Cannot use identifying yet due to byond limits + eye_color = pref.read_preference(/datum/preference/color/pai_eye_color) + card.screen_color = eye_color + card.setEmotion(GLOB.pai_emotions[pref.read_preference(/datum/preference/text/pai_emotion)]) + + update_icon() + return TRUE + +/mob/living/silicon/pai/Destroy() + release_vore_contents() + check_retract_cable() + if(!QDELETED(card)) + QDEL_NULL(card) + QDEL_NULL(pai_ui_chassis) + QDEL_NULL(sradio) + QDEL_NULL(communicator) + QDEL_NULL(pda) + if(pai_fold_display) + QDEL_NULL(pai_fold_display) + securityActive1 = null + securityActive2 = null + hackdoor = null + current = null + if(ckey) + GLOB.paikeys -= ckey + return ..() + +/mob/living/silicon/pai/clear_client() + if(ckey) + GLOB.paikeys -= ckey + return ..() + +// No binary for pAIs. +/mob/living/silicon/pai/binarycheck() + return 0 + +/// Verb used to select a chassis from the list of available chassis +/mob/living/silicon/pai/proc/choose_chassis() + set category = "Abilities.pAI Commands" + set name = "Choose Chassis" + + pai_ui_chassis.tgui_interact(src) + +/// Change pai sprite and offsets based upon the selected chassis id +/mob/living/silicon/pai/proc/change_chassis(new_chassis) + if(!(new_chassis in SSpai.get_chassis_list())) + new_chassis = PAI_DEFAULT_CHASSIS + chassis_name = new_chassis + + //We resize ourselves to normal here for a moment to let the vis_height get reset + var/oursize = size_multiplier + resize(1, FALSE, TRUE, TRUE, FALSE) + + // Get icon data setup + var/datum/pai_sprite/chassis_data = SSpai.chassis_data(chassis_name) + if(chassis_data.holo_projector) + // Rebuild holosprite from character + if(!holo_icon_south) + get_character_icon() + else + // Get data from our sprite datum + icon = chassis_data.sprite_icon + pixel_x = chassis_data.pixel_x + default_pixel_x = pixel_x + pixel_y = chassis_data.pixel_y + default_pixel_y = pixel_y + vis_height = chassis_data.vis_height + + // Drops you if you change to a non-flying chassis + if(chassis_data.flying) + hovering = TRUE + else + hovering = FALSE + if(isopenspace(loc)) + fall() + + // Set vore size. + vore_capacity = max(1, chassis_data.belly_states) // Minimum of 1 + vore_capacity_ex = list("stomach" = vore_capacity) + + // Emergency eject if you change to a smaller belly + if(vore_fullness > vore_capacity && vore_selected) + vore_selected.release_all_contents(TRUE) + + update_icon() + resize(oursize, FALSE, TRUE, TRUE, FALSE) //And then back again now that we're sure the vis_height is correct. + + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Click interactions +////////////////////////////////////////////////////////////////////////////////////////////////// + +/mob/living/silicon/pai/attackby(obj/item/W as obj, mob/user as mob) + var/obj/item/card/id/ID = W.GetID() + if(ID) + if (idaccessible == 1) + switch(tgui_alert(user, "Do you wish to add access to [src] or remove access from [src]?","Access Modify",list("Add Access","Remove Access", "Cancel"))) + if("Add Access") + idcard.access |= ID.GetAccess() + to_chat(user, span_notice("You add the access from the [W] to [src].")) + to_chat(src, span_notice("\The [user] swipes the [W] over you. You copy the access codes.")) + if(radio) + radio.recalculateChannels() + return + if("Remove Access") + idcard.access = list() + to_chat(user, span_notice("You remove the access from [src].")) + to_chat(src, span_warning("\The [user] swipes the [W] over you, removing access codes from you.")) + if(radio) + radio.recalculateChannels() + return + if("Cancel", null) + return + else if (istype(W, /obj/item/card/id) && idaccessible == 0) + to_chat(user, span_notice("[src] is not accepting access modifcations at this time.")) + return + +//Overriding this will stop a number of headaches down the track. +/mob/living/silicon/pai/attackby(obj/item/W as obj, mob/user as mob) + if(W.force) + visible_message(span_danger("[user.name] attacks [src] with [W]!")) + src.adjustBruteLoss(W.force) + src.updatehealth() + else + visible_message(span_warning("[user.name] bonks [src] harmlessly with [W].")) + spawn(1) + if(stat != DEAD) close_up() + return + +/mob/living/silicon/pai/attack_hand(mob/user as mob) + if(user.a_intent == I_HELP) + visible_message(span_notice("[user.name] pats [src].")) + else + visible_message(span_danger("[user.name] boops [src] on the head.")) + close_up() + +/mob/living/silicon/pai/UnarmedAttack(atom/A, proximity_flag) + . = ..() + + // Some restricted objects to interact with + var/obj/O = A + if(istype(O) && O.allow_pai_interaction(proximity_flag)) + O.attack_hand(src) + return + + // Zmovement already allows these to be used with the verbs anyway + if(istype(A,/obj/structure/ladder)) + var/obj/structure/ladder/L = A + L.attack_hand(src) + return + + // We don't want to pick these up, just toggle them + if(istype(A,/obj/item/flashlight/lamp)) + var/obj/item/flashlight/lamp/L = A + L.toggle_light() + return + + // All other computers explain why it's not accessible by showing a firewall warning + if(istype(A,/obj/machinery/computer)) + to_chat(src,span_warning("A firewall prevents you from interfacing with this device!")) + return + + if(!ismob(A) || A == src) + return + + switch(a_intent) + if(I_HELP) + if(isliving(A)) + hug(src, A) + if(I_GRAB) + pai_nom(A) + +// Allow card inhabited machines to be interacted with +// This has to override ClickOn because of storage depth nonsense with how pAIs are in cards in GLOB.machines +/mob/living/silicon/pai/ClickOn(var/atom/A, var/params) + if(istype(A, /obj/machinery)) + var/obj/machinery/M = A + if(M.paicard == card) + M.attack_ai(src) + return + return ..() + +// Handle being picked up. +/mob/living/silicon/pai/get_scooped(var/mob/living/carbon/grabber, var/self_drop) + var/obj/item/holder/H = ..(grabber, self_drop) + if(!istype(H)) + return + + H.icon_state = SSpai.chassis_data(chassis_name).sprite_icon_state + grabber.update_inv_l_hand() + grabber.update_inv_r_hand() + return H + +/mob/living/silicon/pai/Moved(atom/oldloc, direct, forced, movetime) + . = ..() + check_retract_cable() + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Status and damage +////////////////////////////////////////////////////////////////////////////////////////////////// /mob/living/silicon/pai/get_status_tab_items() . = ..() . += "" . += show_silenced() +/mob/living/silicon/pai/adjustBruteLoss(amount, include_robo) + . = ..() + if(amount > 0 && health <= 90) //Something's probably attacking us! + if(prob(amount)) //The more damage it is doing, the more likely it is to damage something important! + card.damage_random_component() + +/mob/living/silicon/pai/adjustFireLoss(amount, include_robo) + . = ..() + if(amount > 0 && health <= 90) + if(prob(amount)) + card.damage_random_component() + /mob/living/silicon/pai/restrained() if(istype(src.loc,/obj/item/paicard)) return 0 @@ -169,145 +428,36 @@ if(3) to_chat(src, span_infoplain(span_green("You feel an electric surge run through your circuitry and become acutely aware at how lucky you are that you can still feel at all."))) -/mob/living/silicon/pai/proc/switchCamera(var/obj/machinery/camera/C) - if (!C) - src.reset_perspective() - return 0 - if (stat == 2 || !C.status || !(src.network in C.network)) return 0 +/// this function shows the information about being silenced as a pAI in the Status panel +/mob/living/silicon/pai/proc/show_silenced() + . = "" + if(src.silence_time) + var/timeleft = round((silence_time - world.timeofday)/10 ,1) + . += "Communications system reboot in -[(timeleft / 60) % 60]:[add_zero(num2text(timeleft % 60), 2)]" - // ok, we're alive, camera is good and in our network... - src.current = C - src.AddComponent(/datum/component/remote_view, focused_on = C, viewsize = null, vconfig_path = /datum/remote_view_config/camera_standard) - return 1 +/// Fully heals a pai, used when a pai is repaired +/mob/living/silicon/pai/proc/full_restore() + adjustBruteLoss(- bruteloss) + adjustFireLoss(- fireloss) + addtimer(CALLBACK(src, PROC_REF(restore_delay_start)), 5 SECONDS, TIMER_DELETE_ME) -/mob/living/silicon/pai/verb/reset_record_view() - set category = "Abilities.pAI Commands" - set name = "Reset Records Software" +/mob/living/silicon/pai/proc/restore_delay_start() + SHOULD_NOT_OVERRIDE(TRUE) + PRIVATE_PROC(TRUE) + card.setEmotion(16) + stat = CONSCIOUS + addtimer(CALLBACK(src, PROC_REF(restore_delay_end)), 1 SECONDS, TIMER_DELETE_ME) - securityActive1 = null - securityActive2 = null - security_cannotfind = 0 - medicalActive1 = null - medicalActive2 = null - medical_cannotfind = 0 - SStgui.update_uis(src) - to_chat(src, span_notice("You reset your record-viewing software.")) - -/mob/living/silicon/pai/cancel_camera() - set category = "Abilities.pAI Commands" - set name = "Cancel Camera View" - reset_perspective() - -/mob/living/silicon/pai/reset_perspective(atom/new_eye) - . = ..() - current = null - -// Procs/code after this point is used to convert the stationary pai item into a -// mobile pai mob. This also includes handling some of the general shit that can occur -// to it. Really this deserves its own file, but for the moment it can sit here. ~ Z - -/mob/living/silicon/pai/verb/fold_out() - set category = "Abilities.pAI Commands" - set name = "Unfold Chassis" - - if(stat || sleeping || paralysis || weakened) - return - - if(loc != card) - return - - // Lets not trap the pai forever. These are special cases we want to escape out of when in our card - if(istype(loc.loc, /obj/item/pda)) - var/obj/item/pda/ourpda = loc.loc - if(ourpda.pai == card) - ourpda.pai.forceMove(ourpda.loc) - ourpda.pai = null - visible_message(span_warning("\The [card] ejects itself from \the [ourpda].")) - return - if(istype(loc.loc, /obj/item/storage/vore_egg)) - var/obj/item/storage/vore_egg/ouregg = loc.loc - to_chat(src, span_notice("You craftily use your built in rumble function to break free of \the [ouregg]'s confines!")) - ouregg.hatch(src) - return - - if(is_folding_unsafe(loc.loc)) - to_chat(src, span_danger("It's not safe to unfold while inside a [loc.loc]!")) - return - - if(card.projector != PP_FUNCTIONAL && card.emitter != PP_FUNCTIONAL) - to_chat(src, span_warning("ERROR: System malfunction. Service required!")) - - if(world.time <= last_special) - to_chat(src, span_warning("You can't unfold yet.")) - return - - last_special = world.time + 100 - - if(istype(card.loc, /obj/machinery)) // VOREStation edit, this statement allows pAIs stuck in a machine to eject themselves. - var/obj/machinery/M = card.loc - M.ejectpai() - //I'm not sure how much of this is necessary, but I would rather avoid issues. - if(istype(card.loc,/obj/item/rig_module)) - to_chat(src, span_filter_notice("There is no room to unfold inside this rig module. You're good and stuck.")) - return 0 - else if(istype(card.loc,/mob)) - var/mob/holder = card.loc - if(ishuman(holder)) - var/mob/living/carbon/human/H = holder - for(var/obj/item/organ/external/affecting in H.organs) - if(card in affecting.implants) - affecting.take_damage(rand(30,50)) - affecting.implants -= card - H.visible_message(span_danger("\The [src] explodes out of \the [H]'s [affecting.name] in shower of gore!")) - break - holder.drop_from_inventory(card) - else if(isbelly(card.loc)) //VOREStation edit. - to_chat(src, span_notice("There is no room to unfold in here. You're good and stuck.")) //VOREStation edit. - return 0 //VOREStation edit. - else if(istype(card.loc,/obj/item/pda)) - var/obj/item/pda/holder = card.loc - holder.pai = null - - src.forceMove(card.loc) - card.forceMove(src) - card.screen_loc = null +/mob/living/silicon/pai/proc/restore_delay_end() + SHOULD_NOT_OVERRIDE(TRUE) + PRIVATE_PROC(TRUE) + var/mob/observer/dead/ghost = get_ghost() + if(ghost) + ghost.notify_revive("Someone is trying to revive you. Re-enter your body if you want to be revived!", 'sound/effects/pai-restore.ogg', source = card) canmove = TRUE - - if(isturf(loc)) - var/turf/T = get_turf(src) - if(istype(T)) T.visible_message(span_filter_notice(span_bold("[src]") + " folds outwards, expanding into a mobile form.")) - - add_verb(src, /mob/living/silicon/pai/proc/pai_nom) - add_verb(src, /mob/living/proc/vertical_nom) - update_icon() - -/mob/living/silicon/pai/verb/fold_up() - set category = "Abilities.pAI Commands" - set name = "Collapse Chassis" - - if(stat || sleeping || paralysis || weakened) - return - - if(src.loc == card) - return - - if(world.time <= last_special) - to_chat(src, span_warning("You can't fold up yet.")) - return - - close_up() - -/mob/living/silicon/pai/proc/choose_verbs() - set category = "Abilities.pAI Commands" - set name = "Choose Speech Verbs" - - var/choice = tgui_input_list(src,"What theme would you like to use for your speech verbs?","Theme Choice", GLOB.possible_say_verbs) - if(!choice) return - - var/list/sayverbs = GLOB.possible_say_verbs[choice] - speak_statement = sayverbs[1] - speak_exclamation = sayverbs[(sayverbs.len>1 ? 2 : sayverbs.len)] - speak_query = sayverbs[(sayverbs.len>2 ? 3 : sayverbs.len)] + card.setEmotion(15) + playsound(card, 'sound/effects/pai-restore.ogg', 50, FALSE) + card.visible_message(span_filter_notice("\The [card] chimes."), runemessage = "chime") /mob/living/silicon/pai/lay_down() set name = "Rest" @@ -320,193 +470,166 @@ if(istype(rig)) rig.force_rest(src) return - else if(chassis == "13") - resting = !resting - //update_transform() I want this to make you ROTATE like normal HUMANS do! But! There's lots of problems and I don't know how to fix them! else resting = !resting - icon_state = resting ? "[chassis]_rest" : "[chassis]" - update_icon() //VOREStation edit + update_icon() to_chat(src, span_notice("You are now [resting ? "resting" : "getting up"].")) canmove = !resting -/* -/mob/living/silicon/pai/update_transform() +/mob/living/silicon/pai/proc/check_retract_cable() + if(!cable) + return - var/desired_scale_x = size_multiplier * icon_scale_x - var/desired_scale_y = size_multiplier * icon_scale_y + var/turf/current = get_turf(src) + var/turf/cableturf = get_turf(cable) + if(get_dist(current, cableturf) <= 1) + return - // Now for the regular stuff. - var/matrix/M = matrix() - M.Scale(desired_scale_x, desired_scale_y) - M.Translate(0, (vis_height/2)*(desired_scale_y-1)) + cableturf.visible_message("The data cable rapidly retracts back into its spool.", "You hear a click and the sound of wire spooling rapidly.") + playsound(src, 'sound/machines/click.ogg', 50, 1) + QDEL_NULL(cable) - if(chassis != "13") - appearance_flags |= PIXEL_SCALE +////////////////////////////////////////////////////////////////////////////////////////////////// +// Update icons +////////////////////////////////////////////////////////////////////////////////////////////////// - var/anim_time = 3 +/mob/living/silicon/pai/update_icon() + . = ..() - if(resting) - M.Turn(90) - M.Scale(desired_scale_y, desired_scale_x) - if(holo_icon_dimension_X == 64 && holo_icon_dimension_Y == 64) - M.Translate(13,-22) - else if(holo_icon_dimension_X == 32 && holo_icon_dimension_Y == 64) - M.Translate(1,-22) - else if(holo_icon_dimension_X == 64 && holo_icon_dimension_Y == 32) - M.Translate(13,-6) + var/datum/pai_sprite/chassis_data = SSpai.chassis_data(chassis_name) + if(chassis_data.holo_projector) + icon_state = null + icon = holo_icon_south + add_eyes() + return + + update_fullness() + + // Don't get a vore belly size if we have no belly size set! + var/belly_size = CLAMP(vore_fullness, 0, chassis_data.belly_states) + if(resting && !chassis_data.resting_belly) // check if we have a belly while resting + belly_size = 0 + var/fullness_extension = "" + if(belly_size > 1) // Multibelly support + fullness_extension = "_[belly_size]" + icon_state = "[chassis_data.sprite_icon_state][resting && chassis_data.can_rest ? "_rest" : ""][belly_size ? "_full[fullness_extension]" : ""]" + + add_eyes() + +/// Applies the eye overlay if the chassis has it +/mob/living/silicon/pai/proc/add_eyes() + remove_eyes() + + var/datum/pai_sprite/chassis_data = SSpai.chassis_data(chassis_name) + if(chassis_data.holo_projector) + // Special eyes that are based on holoprojection of your character's icon size + if(holo_icon_south.Width() > 32) + holo_icon_dimension_X = 64 + pixel_x = -16 + default_pixel_x = -16 + // Get height too + if(holo_icon_south.Height() > 32) + holo_icon_dimension_Y = 64 + vis_height = 64 + // Set eyes + if(holo_icon_dimension_X == 32 && holo_icon_dimension_Y == 32) + eye_layer = image('icons/mob/pai.dmi', chassis_data.holo_eyes_icon_state) + else if(holo_icon_dimension_X == 32 && holo_icon_dimension_Y == 64) + eye_layer = image('icons/mob/pai32x64.dmi', chassis_data.holo_eyes_icon_state) + else if(holo_icon_dimension_X == 64 && holo_icon_dimension_Y == 32) + eye_layer = image('icons/mob/pai64x32.dmi', chassis_data.holo_eyes_icon_state) + else if(holo_icon_dimension_X == 64 && holo_icon_dimension_Y == 64) + eye_layer = image('icons/mob/pai64x64.dmi', chassis_data.holo_eyes_icon_state) + else if(chassis_data.has_eye_sprites) + // Default eye handling + eye_layer = image(icon, "[icon_state]-eyes") + else + // No eyes, so don't bother setting icon stuff + return + eye_layer.appearance_flags = appearance_flags + eye_layer.color = eye_color + if(eye_glow && !hide_glow) + eye_layer.plane = PLANE_LIGHTING_ABOVE + add_overlay(eye_layer) + +/// Removes the eye overlay if it has one +/mob/living/silicon/pai/proc/remove_eyes() + if(!eye_layer) + return + cut_overlay(eye_layer) + qdel(eye_layer) + eye_layer = null + +/// Gets icons for all four directions based on the character slot currently loaded +/mob/living/silicon/pai/proc/get_character_icon() + if(!client || !client.prefs) return FALSE + var/mob/living/carbon/human/dummy/dummy = new () + //This doesn't include custom_items because that's ... hard. + client.prefs.dress_preview_mob(dummy) + sleep(1 SECOND) //Strange bug in preview code? Without this, certain things won't show up. Yay race conditions? + dummy.regenerate_icons() + + var/icon/new_holo = getCompoundIcon(dummy) + + dummy.tail_layering = TRUE + dummy.set_dir(NORTH) + var/icon/new_holo_north = getCompoundIcon(dummy) + dummy.set_dir(EAST) + var/icon/new_holo_east = getCompoundIcon(dummy) + dummy.set_dir(WEST) + var/icon/new_holo_west = getCompoundIcon(dummy) + + qdel(holo_icon_south) + qdel(holo_icon_north) + qdel(holo_icon_east) + qdel(holo_icon_west) + qdel(dummy) + holo_icon_south = new_holo + holo_icon_north = new_holo_north + holo_icon_east = new_holo_east + holo_icon_west = new_holo_west + return TRUE + +/mob/living/silicon/pai/set_dir(var/new_dir) + . = ..() + if(. && SSpai.chassis_data(chassis_name).holo_projector) + switch(dir) + if(SOUTH) + icon = holo_icon_south + if(NORTH) + icon = holo_icon_north + if(EAST) + icon = holo_icon_east + if(WEST) + icon = holo_icon_west else - M.Translate(1,-6) - layer = MOB_LAYER -0.01 // Fix for a byond bug where turf entry order no longer matters - else - M.Scale(desired_scale_x, desired_scale_y) - M.Translate(0, (vis_height/2)*(desired_scale_y-1)) - layer = MOB_LAYER // Fix for a byond bug where turf entry order no longer matters - animate(src, transform = M, time = anim_time) - src.transform = M - handle_status_indicators() -*/ -//Overriding this will stop a number of headaches down the track. -/mob/living/silicon/pai/attackby(obj/item/W as obj, mob/user as mob) - if(W.force) - visible_message(span_danger("[user.name] attacks [src] with [W]!")) - src.adjustBruteLoss(W.force) - src.updatehealth() - else - visible_message(span_warning("[user.name] bonks [src] harmlessly with [W].")) - spawn(1) - if(stat != 2) close_up() - return + icon = holo_icon_north -/mob/living/silicon/pai/attack_hand(mob/user as mob) - if(user.a_intent == I_HELP) - visible_message(span_notice("[user.name] pats [src].")) - else - visible_message(span_danger("[user.name] boops [src] on the head.")) - close_up() +/mob/living/silicon/pai/a_intent_change(input as text) + . = ..() -//I'm not sure how much of this is necessary, but I would rather avoid issues. -/mob/living/silicon/pai/proc/close_up(silent= FALSE) + switch(a_intent) + if(I_HELP) + hud_used.help_intent.icon_state = "intent_help-s" + hud_used.disarm_intent.icon_state = "intent_disarm-n" + hud_used.grab_intent.icon_state = "intent_grab-n" + hud_used.hurt_intent.icon_state = "intent_harm-n" - last_special = world.time + 100 + if(I_DISARM) + hud_used.help_intent.icon_state = "intent_help-n" + hud_used.disarm_intent.icon_state = "intent_disarm-s" + hud_used.grab_intent.icon_state = "intent_grab-n" + hud_used.hurt_intent.icon_state = "intent_harm-n" - if(loc == card) - return + if(I_GRAB) + hud_used.help_intent.icon_state = "intent_help-n" + hud_used.disarm_intent.icon_state = "intent_disarm-n" + hud_used.grab_intent.icon_state = "intent_grab-s" + hud_used.hurt_intent.icon_state = "intent_harm-n" - // some snowflake locations where we really shouldn't fold up... - if(is_folding_unsafe(loc)) - to_chat(src, span_danger("It's not safe to fold up while inside a [loc]!")) - return - - release_vore_contents(FALSE) //VOREStation Add - - var/turf/T = get_turf(src) - if(istype(T) && !silent) T.visible_message(span_filter_notice(span_bold("[src]") + " neatly folds inwards, compacting down to a rectangular card.")) - - stop_pulling() - - //stop resting - resting = 0 - - // If we are being held, handle removing our holder from their inv. - var/obj/item/holder/our_holder = loc - if(istype(our_holder)) - var/turf/drop_turf = get_turf(our_holder) - var/mob/living/M = our_holder.loc - if(istype(M)) - M.drop_from_inventory(our_holder) - src.forceMove(card) - card.forceMove(drop_turf) - - if(isbelly(loc)) //If in tumby, when fold up, card go into tumby - var/obj/belly/B = loc - src.forceMove(card) - card.forceMove(B) - - if(isdisposalpacket(loc)) - var/obj/structure/disposalholder/hold = loc - src.forceMove(card) - card.forceMove(hold) - - else //Otherwise go on floor - card.forceMove(get_turf(src)) - src.forceMove(card) - - canmove = 1 - resting = 0 - icon_state = "[chassis]" - if(isopenspace(card.loc)) - fall() - remove_verb(src, /mob/living/silicon/pai/proc/pai_nom) - remove_verb(src, /mob/living/proc/vertical_nom) - -/mob/living/silicon/pai/proc/is_folding_unsafe(check_location) - return isbelly(check_location) || istype(check_location, /obj/machinery) || istype(check_location, /obj/item/storage/vore_egg || istype(check_location, /obj/item/pda)) - -// No binary for pAIs. -/mob/living/silicon/pai/binarycheck() - return 0 - -// Handle being picked up. -/mob/living/silicon/pai/get_scooped(var/mob/living/carbon/grabber, var/self_drop) - var/obj/item/holder/H = ..(grabber, self_drop) - if(!istype(H)) - return - - H.icon_state = "[chassis]" - grabber.update_inv_l_hand() - grabber.update_inv_r_hand() - return H - -/mob/living/silicon/pai/attackby(obj/item/W as obj, mob/user as mob) - var/obj/item/card/id/ID = W.GetID() - if(ID) - if (idaccessible == 1) - switch(tgui_alert(user, "Do you wish to add access to [src] or remove access from [src]?","Access Modify",list("Add Access","Remove Access", "Cancel"))) - if("Add Access") - idcard.access |= ID.GetAccess() - to_chat(user, span_notice("You add the access from the [W] to [src].")) - to_chat(src, span_notice("\The [user] swipes the [W] over you. You copy the access codes.")) - if(radio) - radio.recalculateChannels() - return - if("Remove Access") - idcard.access = list() - to_chat(user, span_notice("You remove the access from [src].")) - to_chat(src, span_warning("\The [user] swipes the [W] over you, removing access codes from you.")) - if(radio) - radio.recalculateChannels() - return - if("Cancel", null) - return - else if (istype(W, /obj/item/card/id) && idaccessible == 0) - to_chat(user, span_notice("[src] is not accepting access modifcations at this time.")) - return - -/mob/living/silicon/pai/verb/allowmodification() - set name = "Change Access Modifcation Permission" - set category = "Abilities.pAI Commands" - set desc = "Allows people to modify your access or block people from modifying your access." - - if(idaccessible == 0) - idaccessible = 1 - visible_message(span_notice("\The [src] clicks as their access modification slot opens."),span_notice("You allow access modifications."), runemessage = "click") - else - idaccessible = 0 - visible_message(span_notice("\The [src] clicks as their access modification slot closes."),span_notice("You block access modfications."), runemessage = "click") - - -/mob/living/silicon/pai/verb/wipe_software() - set name = "Enter Storage" - set category = "Abilities.pAI Commands" - set desc = "Upload your personality to the cloud and wipe your software from the card. This is functionally equivalent to cryo or robotic storage, freeing up your job slot." - - // Make sure people don't kill themselves accidentally - if(tgui_alert(src, "WARNING: This will immediately wipe your software and ghost you, removing your character from the round permanently (similar to cryo and robotic storage). Are you entirely sure you want to do this?", "Wipe Software", list("No", "Yes")) != "Yes") - return - - close_up() - visible_message(span_filter_notice(span_bold("[src]") + " fades away from the screen, the pAI device goes silent.")) - card.removePersonality() - clear_client() + if(I_HURT) + hud_used.help_intent.icon_state = "intent_help-n" + hud_used.disarm_intent.icon_state = "intent_disarm-n" + hud_used.grab_intent.icon_state = "intent_grab-n" + hud_used.hurt_intent.icon_state = "intent_harm-s" diff --git a/code/modules/mob/living/silicon/pai/pai_folding.dm b/code/modules/mob/living/silicon/pai/pai_folding.dm new file mode 100644 index 0000000000..75b1b0ebb2 --- /dev/null +++ b/code/modules/mob/living/silicon/pai/pai_folding.dm @@ -0,0 +1,148 @@ +/mob/living/silicon/pai/verb/fold_out() + set category = "Abilities.pAI Commands" + set name = "Unfold Chassis" + + if(stat || sleeping || paralysis || weakened) + return + + if(loc != card) + return + + // Lets not trap the pai forever. These are special cases we want to escape out of when in our card + if(istype(loc.loc, /obj/item/pda)) + var/obj/item/pda/ourpda = loc.loc + if(ourpda.pai == card) + ourpda.pai.forceMove(ourpda.loc) + ourpda.pai = null + visible_message(span_warning("\The [card] ejects itself from \the [ourpda].")) + return + if(istype(loc.loc, /obj/item/storage/vore_egg)) + var/obj/item/storage/vore_egg/ouregg = loc.loc + to_chat(src, span_notice("You craftily use your built in rumble function to break free of \the [ouregg]'s confines!")) + ouregg.hatch(src) + return + + if(is_folding_unsafe(loc.loc)) + to_chat(src, span_danger("It's not safe to unfold while inside a [loc.loc]!")) + return + + if(card.projector != PP_FUNCTIONAL && card.emitter != PP_FUNCTIONAL) + to_chat(src, span_warning("ERROR: System malfunction. Service required!")) + + if(world.time <= last_special) + to_chat(src, span_warning("You can't unfold yet.")) + return + + last_special = world.time + 100 + + if(istype(card.loc, /obj/machinery)) // VOREStation edit, this statement allows pAIs stuck in a machine to eject themselves. + var/obj/machinery/M = card.loc + M.ejectpai() + //I'm not sure how much of this is necessary, but I would rather avoid issues. + if(istype(card.loc,/obj/item/rig_module)) + to_chat(src, span_filter_notice("There is no room to unfold inside this rig module. You're good and stuck.")) + return 0 + else if(istype(card.loc,/mob)) + var/mob/holder = card.loc + if(ishuman(holder)) + var/mob/living/carbon/human/H = holder + for(var/obj/item/organ/external/affecting in H.organs) + if(card in affecting.implants) + affecting.take_damage(rand(30,50)) + affecting.implants -= card + H.visible_message(span_danger("\The [src] explodes out of \the [H]'s [affecting.name] in shower of gore!")) + break + holder.drop_from_inventory(card) + else if(isbelly(card.loc)) //VOREStation edit. + to_chat(src, span_notice("There is no room to unfold in here. You're good and stuck.")) //VOREStation edit. + return 0 //VOREStation edit. + else if(istype(card.loc,/obj/item/pda)) + var/obj/item/pda/holder = card.loc + holder.pai = null + + src.forceMove(card.loc) + card.forceMove(src) + card.screen_loc = null + canmove = TRUE + + if(isturf(loc)) + var/turf/T = get_turf(src) + if(istype(T)) T.visible_message(span_filter_notice(span_bold("[src]") + " folds outwards, expanding into a mobile form.")) + + add_verb(src, /mob/living/silicon/pai/proc/pai_nom) + add_verb(src, /mob/living/proc/vertical_nom) + update_icon() + +/mob/living/silicon/pai/verb/fold_up() + set category = "Abilities.pAI Commands" + set name = "Collapse Chassis" + + if(stat || sleeping || paralysis || weakened) + return + + if(src.loc == card) + return + + if(world.time <= last_special) + to_chat(src, span_warning("You can't fold up yet.")) + return + + close_up() + +//I'm not sure how much of this is necessary, but I would rather avoid issues. +/mob/living/silicon/pai/proc/close_up(silent= FALSE) + + last_special = world.time + 100 + + if(loc == card) + return + + // some snowflake locations where we really shouldn't fold up... + if(is_folding_unsafe(loc)) + to_chat(src, span_danger("It's not safe to fold up while inside a [loc]!")) + return + + release_vore_contents(FALSE) //VOREStation Add + + var/turf/T = get_turf(src) + if(istype(T) && !silent) T.visible_message(span_filter_notice(span_bold("[src]") + " neatly folds inwards, compacting down to a rectangular card.")) + + stop_pulling() + + //stop resting + resting = 0 + + // If we are being held, handle removing our holder from their inv. + var/obj/item/holder/our_holder = loc + if(istype(our_holder)) + var/turf/drop_turf = get_turf(our_holder) + var/mob/living/M = our_holder.loc + if(istype(M)) + M.drop_from_inventory(our_holder) + src.forceMove(card) + card.forceMove(drop_turf) + + if(isbelly(loc)) //If in tumby, when fold up, card go into tumby + var/obj/belly/B = loc + src.forceMove(card) + card.forceMove(B) + + if(isdisposalpacket(loc)) + var/obj/structure/disposalholder/hold = loc + src.forceMove(card) + card.forceMove(hold) + + else //Otherwise go on floor + card.forceMove(get_turf(src)) + src.forceMove(card) + + canmove = 1 + resting = 0 + icon_state = SSpai.chassis_data(chassis_name).sprite_icon_state + if(isopenspace(card.loc)) + fall() + remove_verb(src, /mob/living/silicon/pai/proc/pai_nom) + remove_verb(src, /mob/living/proc/vertical_nom) + +/mob/living/silicon/pai/proc/is_folding_unsafe(check_location) + return isbelly(check_location) || istype(check_location, /obj/machinery) || istype(check_location, /obj/item/storage/vore_egg || istype(check_location, /obj/item/pda)) diff --git a/code/modules/mob/living/silicon/pai/pai_hud.dm b/code/modules/mob/living/silicon/pai/pai_hud.dm index a8e4385a77..45af2b6941 100644 --- a/code/modules/mob/living/silicon/pai/pai_hud.dm +++ b/code/modules/mob/living/silicon/pai/pai_hud.dm @@ -1,6 +1,3 @@ -/mob/living/silicon/pai - var/atom/movable/screen/pai/pai_fold_display = null - /atom/movable/screen/pai icon = 'icons/mob/pai_hud.dmi' var/base_state diff --git a/code/modules/mob/living/silicon/pai/pai_software.dm b/code/modules/mob/living/silicon/pai/pai_software.dm new file mode 100644 index 0000000000..1240f50fff --- /dev/null +++ b/code/modules/mob/living/silicon/pai/pai_software.dm @@ -0,0 +1,136 @@ +/mob/living/silicon/pai/proc/refresh_software_status() //This manages the pAI software status buttons icon states based on if you have them and if they are enabled + for(var/thing in software) //this only gets called when you click one of the relevent buttons, rather than all the time! + var/datum/pai_software/soft = software[thing] + if(istype(soft,/datum/pai_software/med_records)) + soft_mr = TRUE + if(istype(soft,/datum/pai_software/sec_records)) + soft_sr = TRUE + if(istype(soft,/datum/pai_software/door_jack)) + soft_dj = TRUE + if(istype(soft,/datum/pai_software/atmosphere_sensor)) + soft_as = TRUE + if(istype(soft,/datum/pai_software/pai_hud)) + soft_ar = TRUE + if(istype(soft,/datum/pai_software/translator)) + soft_ut = TRUE + if(istype(soft,/datum/pai_software/signaller)) + soft_si = TRUE + if(istype(soft,/datum/pai_software/deathalarm)) + soft_da = TRUE + for(var/atom/movable/screen/pai/button in hud_used.other) + if(button.name == "medical records") + if(soft_mr) + button.icon_state = "[button.base_state]" + else + button.icon_state = "[button.base_state]_o" + if(button.name == "security records") + if(soft_sr) + button.icon_state = "[button.base_state]" + else + button.icon_state = "[button.base_state]_o" + if(button.name == "door jack") + if(soft_dj) + button.icon_state = "[button.base_state]" + else + button.icon_state = "[button.base_state]_o" + if(button.name == "atmosphere sensor") + if(soft_as) + button.icon_state = "[button.base_state]" + else + button.icon_state = "[button.base_state]_o" + if(button.name == "remote signaler") + if(soft_si) + button.icon_state = "[button.base_state]" + else + button.icon_state = "[button.base_state]_o" + if(button.name == "universal translator") + if(soft_ut && translator_on) + button.icon_state = "[button.base_state]" + else + button.icon_state = "[button.base_state]_o" + if(button.name == "ar hud") + if(soft_ar && paiHUD) + button.icon_state = "[button.base_state]" + else + button.icon_state = "[button.base_state]_o" + if(button.name == "death alarm") + if(soft_da && paiDA) + button.icon_state = "[button.base_state]" + else + button.icon_state = "[button.base_state]_o" + +/mob/living/silicon/pai/verb/wipe_software() + set name = "Enter Storage" + set category = "Abilities.pAI Commands" + set desc = "Upload your personality to the cloud and wipe your software from the card. This is functionally equivalent to cryo or robotic storage, freeing up your job slot." + + // Make sure people don't kill themselves accidentally + if(tgui_alert(src, "WARNING: This will immediately wipe your software and ghost you, removing your character from the round permanently (similar to cryo and robotic storage). Are you entirely sure you want to do this?", "Wipe Software", list("No", "Yes")) != "Yes") + return + + close_up() + visible_message(span_filter_notice(span_bold("[src]") + " fades away from the screen, the pAI device goes silent.")) + card.removePersonality() + clear_client() + +/mob/living/silicon/pai/proc/touch_window(soft_name) //This lets us touch TGUI procs and windows that may be nested behind other TGUI procs and windows + if(stat != CONSCIOUS) //so we can access our software without having to open up the software interface TGUI window + to_chat(src, span_warning("You can't do that right now.")) + return + for(var/thing in software) + var/datum/pai_software/S = software[thing] + if(istype(S, /datum/pai_software) && S.name == soft_name) + if(S.toggle) + S.toggle(src) + to_chat(src, span_notice("You toggled [S.name].")) + refresh_software_status() + else + S.tgui_interact(src) + refresh_software_status() + return + for(var/thing in GLOB.pai_software_by_key) + var/datum/pai_software/our_soft = GLOB.pai_software_by_key[thing] + if(our_soft.name == soft_name) + if(!(ram >= our_soft.ram_cost)) + to_chat(src, span_warning("Insufficient RAM for download. (Cost [our_soft.ram_cost] : [ram] Remaining)")) + return + if(tgui_alert(src, "Do you want to download [our_soft.name]? It costs [our_soft.ram_cost], and you have [ram] remaining.", "Download [our_soft.name]", list("Yes", "No")) == "Yes") + if(!(ram >= our_soft.ram_cost)) + return + if(software[our_soft.id]) + return + ram -= our_soft.ram_cost + software[our_soft.id] = our_soft + to_chat(src, span_notice("You downloaded [our_soft.name]. ([ram] RAM remaining.)")) + refresh_software_status() + +//Procs for using the various UI buttons for your softwares +/mob/living/silicon/pai/proc/directives() + touch_window("Directives") + +/mob/living/silicon/pai/proc/crew_manifest() + touch_window("Crew Manifest") + +/mob/living/silicon/pai/proc/med_records() + touch_window("Medical Records") + +/mob/living/silicon/pai/proc/sec_records() + touch_window("Security Records") + +/mob/living/silicon/pai/proc/remote_signal() + touch_window("Remote Signaler") + +/mob/living/silicon/pai/proc/atmos_sensor() + touch_window("Atmosphere Sensor") + +/mob/living/silicon/pai/proc/translator() + touch_window("Universal Translator") + +/mob/living/silicon/pai/proc/door_jack() + touch_window("Door Jack") + +/mob/living/silicon/pai/proc/ar_hud() + touch_window("AR HUD") + +/mob/living/silicon/pai/proc/death_alarm() + touch_window("Death Alarm") diff --git a/code/modules/mob/living/silicon/pai/pai_verbs.dm b/code/modules/mob/living/silicon/pai/pai_verbs.dm new file mode 100644 index 0000000000..e5319fea38 --- /dev/null +++ b/code/modules/mob/living/silicon/pai/pai_verbs.dm @@ -0,0 +1,219 @@ +/// Change currently viewed camera +/mob/living/silicon/pai/proc/switchCamera(var/obj/machinery/camera/C) + if (!C) + src.reset_perspective() + return 0 + if (stat == 2 || !C.status || !(src.network in C.network)) return 0 + + // ok, we're alive, camera is good and in our network... + src.current = C + src.AddComponent(/datum/component/remote_view, focused_on = C, viewsize = null, vconfig_path = /datum/remote_view_config/camera_standard) + return 1 + +/mob/living/silicon/pai/cancel_camera() + set category = "Abilities.pAI Commands" + set name = "Cancel Camera View" + reset_perspective() + +/mob/living/silicon/pai/reset_perspective(atom/new_eye) + . = ..() + current = null + +/mob/living/silicon/pai/verb/reset_record_view() + set category = "Abilities.pAI Commands" + set name = "Reset Records Software" + + securityActive1 = null + securityActive2 = null + security_cannotfind = 0 + medicalActive1 = null + medicalActive2 = null + medical_cannotfind = 0 + SStgui.update_uis(src) + to_chat(src, span_notice("You reset your record-viewing software.")) + +/mob/living/silicon/pai/proc/choose_verbs() + set category = "Abilities.pAI Commands" + set name = "Choose Speech Verbs" + + var/choice = tgui_input_list(src,"What theme would you like to use for your speech verbs?","Theme Choice", GLOB.possible_say_verbs) + if(!choice) return + + var/list/sayverbs = GLOB.possible_say_verbs[choice] + speak_statement = sayverbs[1] + speak_exclamation = sayverbs[(sayverbs.len>1 ? 2 : sayverbs.len)] + speak_query = sayverbs[(sayverbs.len>2 ? 3 : sayverbs.len)] + +/mob/living/silicon/pai/verb/allowmodification() + set name = "Change Access Modifcation Permission" + set category = "Abilities.pAI Commands" + set desc = "Allows people to modify your access or block people from modifying your access." + + if(idaccessible == 0) + idaccessible = 1 + visible_message(span_notice("\The [src] clicks as their access modification slot opens."),span_notice("You allow access modifications."), runemessage = "click") + else + idaccessible = 0 + visible_message(span_notice("\The [src] clicks as their access modification slot closes."),span_notice("You block access modfications."), runemessage = "click") + +/mob/living/silicon/pai/verb/toggle_gender_identity_vr() + set name = "Set Gender Identity" + set desc = "Sets the pronouns when examined and performing an emote." + set category = "IC.Settings" + var/new_gender_identity = tgui_input_list(src, "Please select a gender Identity:", "Set Gender Identity", list(FEMALE, MALE, NEUTER, PLURAL, HERM)) + if(!new_gender_identity) + return 0 + gender = new_gender_identity + return 1 + +/mob/living/silicon/pai/verb/pai_hide() + set name = "Hide" + set desc = "Allows to hide beneath tables or certain items. Toggled on or off." + set category = "Abilities.pAI" + + hide() + if(status_flags & HIDING) + hide_glow = TRUE + else + hide_glow = FALSE + update_icon() + +/mob/living/silicon/pai/verb/screen_message(message as text|null) + set category = "Abilities.pAI Commands" + set name = "Screen Message" + set desc = "Allows you to display a message on your screen. This will show up in the chat of anyone who is holding your card." + + if (src.client) + if(client.prefs.muted & MUTE_IC) + to_chat(src, span_warning("You cannot speak in IC (muted).")) + return + if(loc != card) + to_chat(src, span_warning("Your message won't be visible while unfolded!")) + if (!message) + message = tgui_input_text(src, "Enter text you would like to show on your screen.","Screen Message", encode = FALSE) + message = sanitize_or_reflect(message,src) + if (!message) + return + message = capitalize(message) + if (stat == DEAD) + return + card.screen_msg = message + var/logmsg = "(CARD SCREEN)[message]" + log_talk(logmsg, LOG_SAY) + to_chat(src, span_filter_say(span_cult("You print a message to your screen, \"[message]\""))) + if(isliving(card.loc)) + var/mob/living/L = card.loc + if(L.client) + to_chat(L, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\""))) + else return + else if(isbelly(card.loc)) + var/obj/belly/b = card.loc + if(b.owner.client) + to_chat(b.owner, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\""))) + else return + else if(istype(card.loc, /obj/item/pda)) + var/obj/item/pda/p = card.loc + if(isliving(p.loc)) + var/mob/living/L = p.loc + if(L.client) + to_chat(L, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\""))) + else return + else if(isbelly(p.loc)) + var/obj/belly/b = card.loc + if(b.owner.client) + to_chat(b.owner, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\""))) + else return + else return + else return + to_chat(src, span_notice("Your message was relayed.")) + for (var/mob/G in GLOB.player_list) + if (isnewplayer(G)) + continue + else if(isobserver(G) && G.client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears)) + if((client?.prefs?.read_preference(/datum/preference/toggle/whisubtle_vis) || check_rights_for(G.client, R_HOLDER)) && \ + G.client?.prefs?.read_preference(/datum/preference/toggle/ghost_see_whisubtle)) + to_chat(G, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\""))) + +/mob/living/silicon/pai/proc/pai_nom(var/mob/living/T in oview(1)) + set name = "pAI Nom" + set category = "Abilities.pAI Commands" + set desc = "Allows you to eat someone while unfolded. Can't be used while in card form." + + if (stat != CONSCIOUS) + return + return feed_grabbed_to_self(src,T) + +/mob/living/silicon/pai/verb/toggle_eyeglow() + set category = "Abilities.pAI Commands" + set name = "Toggle Eye Glow" + + if(!SSpai.chassis_data(chassis_name).has_eye_sprites) + to_chat(src, span_filter_notice("Your selected chassis cannot modify its eye glow!")) + return + + if(eye_glow && !hide_glow) + eye_glow = FALSE + else + eye_glow = TRUE + hide_glow = FALSE + update_icon() + +/mob/living/silicon/pai/verb/pick_eye_color() + set category = "Abilities.pAI Commands" + set name = "Pick Eye Color" + + if(!SSpai.chassis_data(chassis_name).has_eye_sprites) + to_chat(src, span_warning("Your selected chassis eye color can not be modified. The color you pick will only apply to supporting chassis and your card screen.")) + return + + var/new_eye_color = tgui_color_picker(src, "Choose your character's eye color:", "Eye Color") + if(new_eye_color) + eye_color = new_eye_color + update_icon() + card.setEmotion(card.current_emotion) + +/mob/living/silicon/pai/proc/hug(var/mob/living/silicon/pai/H, var/mob/living/target) + + var/t_him = "them" + if(ishuman(target)) + var/mob/living/carbon/human/T = target + switch(T.identifying_gender) + if(MALE) + t_him = "him" + if(FEMALE) + t_him = "her" + if(NEUTER) + t_him = "it" + if(HERM) + t_him = "hir" + else + t_him = "them" + else + switch(target.gender) + if(MALE) + t_him = "him" + if(FEMALE) + t_him = "her" + if(NEUTER) + t_him = "it" + if(HERM) + t_him = "hir" + else + t_him = "them" + + if(H.zone_sel.selecting == BP_HEAD) + H.visible_message( \ + span_notice("[H] pats [target] on the head."), \ + span_notice("You pat [target] on the head."), ) + else if(H.zone_sel.selecting == BP_R_HAND || H.zone_sel.selecting == BP_L_HAND) + H.visible_message( \ + span_notice("[H] shakes [target]'s hand."), \ + span_notice("You shake [target]'s hand."), ) + else if(H.zone_sel.selecting == "mouth") + H.visible_message( \ + span_notice("[H] boops [target]'s nose."), \ + span_notice("You boop [target] on the nose."), ) + else + H.visible_message(span_notice("[H] hugs [target] to make [t_him] feel better!"), \ + span_notice("You hug [target] to make [t_him] feel better!")) + playsound(src, 'sound/weapons/thudswoosh.ogg', 50, 1, -1) diff --git a/code/modules/mob/living/silicon/pai/pai_vr.dm b/code/modules/mob/living/silicon/pai/pai_vr.dm deleted file mode 100644 index 7c5972b46e..0000000000 --- a/code/modules/mob/living/silicon/pai/pai_vr.dm +++ /dev/null @@ -1,659 +0,0 @@ -/mob/living/silicon/pai - icon = 'icons/mob/pai_vr.dmi' - softfall = TRUE - var/eye_glow = TRUE - var/hide_glow = FALSE - var/image/eye_layer = null // Holds the eye overlay. - var/eye_color = "#00ff0d" - var/icon/holo_icon - var/icon/holo_icon_north - var/holo_icon_dimension_X = 32 - var/holo_icon_dimension_Y = 32 - //These vars keep track of whether you have the related software, used for easily updating the UI - var/soft_ut = FALSE //universal translator - var/soft_mr = FALSE //medical records - var/soft_sr = FALSE //security records - var/soft_dj = FALSE //door jack - var/soft_as = FALSE //atmosphere sensor - var/soft_si = FALSE //signaler - var/soft_ar = FALSE //ar hud - var/soft_da = FALSE //death alarm - - vore_capacity = 1 - vore_capacity_ex = list("stomach" = 1) - -/mob/living/silicon/pai/Initialize(mapload) - . = ..() - - add_verb(src, /mob/proc/dominate_predator) - add_verb(src, /mob/living/proc/dominate_prey) - add_verb(src, /mob/living/proc/set_size) - add_verb(src, /mob/living/proc/shred_limb) - -/mob/living/silicon/pai/Login() - . = ..() - if(!holo_icon) - last_special = world.time + 100 //Let's give get_character_icon time to work - get_character_icon() - if(stat == DEAD) - healths.icon_state = "health7" - -/mob/living/silicon/pai/proc/full_restore() //This is using do_after all kinds of weird... - adjustBruteLoss(- bruteloss) - adjustFireLoss(- fireloss) - do_after(src, 1 SECONDS, target = src) - card.setEmotion(16) - stat = CONSCIOUS - do_after(src, 5 SECONDS, target = src) - var/mob/observer/dead/ghost = src.get_ghost() - if(ghost) - ghost.notify_revive("Someone is trying to revive you. Re-enter your body if you want to be revived!", 'sound/effects/pai-restore.ogg', source = card) - canmove = TRUE - card.setEmotion(15) - playsound(card, 'sound/effects/pai-restore.ogg', 50, FALSE) - card.visible_message(span_filter_notice("\The [card] chimes."), runemessage = "chime") - -/mob/living/silicon/pai/proc/pai_nom(var/mob/living/T in oview(1)) - set name = "pAI Nom" - set category = "Abilities.pAI Commands" - set desc = "Allows you to eat someone while unfolded. Can't be used while in card form." - - if (stat != CONSCIOUS) - return - return feed_grabbed_to_self(src,T) - -/mob/living/silicon/pai/update_icon() //Some functions cause this to occur, such as resting - ..() - if(chassis == "13") - icon = holo_icon - add_eyes() - return - - update_fullness() - - //Add a check when selecting a chassis if you add in support for this, to set vore_capacity to 2 or however many states you have. - var/fullness_extension = "" - if(vore_capacity > 1 && vore_fullness > 1) - fullness_extension = "_[vore_fullness]" - - if(!vore_fullness && !resting) - icon_state = "[chassis]" //Using icon_state here resulted in quite a few bugs. Chassis is much less buggy. - else if(!vore_fullness && resting) - icon_state = "[chassis]_rest" - - // Unfortunately not all these states exist, ugh. - else if(vore_fullness && !resting) - if(icon_exists(icon, "[chassis]_full[fullness_extension]")) - icon_state = "[chassis]_full[fullness_extension]" - else - icon_state = "[chassis]" - else if(vore_fullness && resting) - if(icon_exists(icon, "[chassis]_rest_full[fullness_extension]")) - icon_state = "[chassis]_rest_full[fullness_extension]" - else - icon_state = "[chassis]_rest" - if(chassis in GLOB.wide_chassis) - pixel_x = -16 - default_pixel_x = -16 - else - pixel_x = 0 - default_pixel_x = 0 - add_eyes() - -/mob/living/silicon/pai/update_icons() //And other functions cause this to occur, such as digesting someone. - ..() - if(chassis == "13") - icon = holo_icon - add_eyes() - return - update_fullness() - //Add a check when selecting a chassis if you add in support for this, to set vore_capacity to 2 or however many states you have. - var/fullness_extension = "" - if(vore_capacity > 1 && vore_fullness > 1) - fullness_extension = "_[vore_fullness]" - if(!vore_fullness && !resting) - icon_state = "[chassis]" - else if(!vore_fullness && resting) - icon_state = "[chassis]_rest" - else if(vore_fullness && !resting) - icon_state = "[chassis]_full[fullness_extension]" - else if(vore_fullness && resting) - icon_state = "[chassis]_rest_full[fullness_extension]" - if(chassis in GLOB.wide_chassis) - pixel_x = -16 - default_pixel_x = -16 - else - pixel_x = 0 - default_pixel_x = 0 - add_eyes() - -//proc override to avoid pAI players being invisible while the chassis selection window is open -/mob/living/silicon/pai/proc/choose_chassis() - set category = "Abilities.pAI Commands" - set name = "Choose Chassis" - var/choice - - choice = tgui_input_list(src, "What would you like to use for your mobile chassis icon?", "Chassis Choice", GLOB.possible_chassis) - if(!choice) return - var/oursize = size_multiplier - resize(1, FALSE, TRUE, TRUE, FALSE) //We resize ourselves to normal here for a moment to let the vis_height get reset - chassis = GLOB.possible_chassis[choice] - - vore_capacity = 1 - vore_capacity_ex = list("stomach" = 1) - - if(chassis == "13") - if(!holo_icon) - if(!get_character_icon()) - return - icon_state = null - icon = holo_icon - else if(chassis in GLOB.wide_chassis) - icon = 'icons/mob/pai_vr64x64.dmi' - vis_height = 64 - else - icon = 'icons/mob/pai_vr.dmi' - vis_height = 32 - resize(oursize, FALSE, TRUE, TRUE, FALSE) //And then back again now that we're sure the vis_height is correct. - - if(chassis in GLOB.flying_chassis) - hovering = TRUE - else - hovering = FALSE - if(isopenspace(loc)) - fall() - - update_icon() - -/mob/living/silicon/pai/verb/toggle_eyeglow() - set category = "Abilities.pAI Commands" - set name = "Toggle Eye Glow" - - if(chassis in GLOB.allows_eye_color) - if(eye_glow && !hide_glow) - eye_glow = FALSE - else - eye_glow = TRUE - hide_glow = FALSE - update_icon() - else - to_chat(src, span_filter_notice("Your selected chassis cannot modify its eye glow!")) - return - - -/mob/living/silicon/pai/verb/pick_eye_color() - set category = "Abilities.pAI Commands" - set name = "Pick Eye Color" - if(!(chassis in GLOB.allows_eye_color)) - to_chat(src, span_warning("Your selected chassis eye color can not be modified. The color you pick will only apply to supporting chassis and your card screen.")) - - var/new_eye_color = tgui_color_picker(src, "Choose your character's eye color:", "Eye Color") - if(new_eye_color) - eye_color = new_eye_color - update_icon() - card.setEmotion(card.current_emotion) - -// Release belly contents before being gc'd! -/mob/living/silicon/pai/Destroy() - release_vore_contents() - if(ckey) - GLOB.paikeys -= ckey - return ..() - -/mob/living/silicon/pai/clear_client() - if(ckey) - GLOB.paikeys -= ckey - return ..() - -/mob/living/silicon/pai/proc/add_eyes() - remove_eyes() - if(chassis == "13") - if(holo_icon.Width() > 32) - holo_icon_dimension_X = 64 - pixel_x = -16 - default_pixel_x = -16 - if(holo_icon.Height() > 32) - holo_icon_dimension_Y = 64 - if(holo_icon_dimension_X == 32 && holo_icon_dimension_Y == 32) - eye_layer = image('icons/mob/pai_vr.dmi', "type13-eyes") - else if(holo_icon_dimension_X == 32 && holo_icon_dimension_Y == 64) - eye_layer = image('icons/mob/pai_vr32x64.dmi', "type13-eyes") - else if(holo_icon_dimension_X == 64 && holo_icon_dimension_Y == 32) - eye_layer = image('icons/mob/pai_vr64x32.dmi', "type13-eyes") - else if(holo_icon_dimension_X == 64 && holo_icon_dimension_Y == 64) - eye_layer = image('icons/mob/pai_vr64x64.dmi', "type13-eyes") - else if(chassis in GLOB.allows_eye_color) - eye_layer = image(icon, "[icon_state]-eyes") - else return - eye_layer.appearance_flags = appearance_flags - eye_layer.color = eye_color - if(eye_glow && !hide_glow) - eye_layer.plane = PLANE_LIGHTING_ABOVE - add_overlay(eye_layer) - -/mob/living/silicon/pai/proc/remove_eyes() - cut_overlay(eye_layer) - qdel(eye_layer) - eye_layer = null - -/mob/living/silicon/pai/UnarmedAttack(atom/A, proximity_flag) - . = ..() - - if(istype(A,/obj/structure/ladder)) - // Zmovement already allows these to be used with the verbs anyway - var/obj/structure/ladder/L = A - L.attack_hand(src) - return - - if(!ismob(A) || A == src) - return - - switch(a_intent) - if(I_HELP) - if(isliving(A)) - hug(src, A) - if(I_GRAB) - pai_nom(A) - -// Allow card inhabited machines to be interacted with -// This has to override ClickOn because of storage depth nonsense with how pAIs are in cards in GLOB.machines -/mob/living/silicon/pai/ClickOn(var/atom/A, var/params) - if(istype(A, /obj/machinery)) - var/obj/machinery/M = A - if(M.paicard == card) - M.attack_ai(src) - return - return ..() - -/mob/living/silicon/pai/proc/hug(var/mob/living/silicon/pai/H, var/mob/living/target) - - var/t_him = "them" - if(ishuman(target)) - var/mob/living/carbon/human/T = target - switch(T.identifying_gender) - if(MALE) - t_him = "him" - if(FEMALE) - t_him = "her" - if(NEUTER) - t_him = "it" - if(HERM) - t_him = "hir" - else - t_him = "them" - else - switch(target.gender) - if(MALE) - t_him = "him" - if(FEMALE) - t_him = "her" - if(NEUTER) - t_him = "it" - if(HERM) - t_him = "hir" - else - t_him = "them" - - if(H.zone_sel.selecting == BP_HEAD) - H.visible_message( \ - span_notice("[H] pats [target] on the head."), \ - span_notice("You pat [target] on the head."), ) - else if(H.zone_sel.selecting == BP_R_HAND || H.zone_sel.selecting == BP_L_HAND) - H.visible_message( \ - span_notice("[H] shakes [target]'s hand."), \ - span_notice("You shake [target]'s hand."), ) - else if(H.zone_sel.selecting == "mouth") - H.visible_message( \ - span_notice("[H] boops [target]'s nose."), \ - span_notice("You boop [target] on the nose."), ) - else - H.visible_message(span_notice("[H] hugs [target] to make [t_him] feel better!"), \ - span_notice("You hug [target] to make [t_him] feel better!")) - playsound(src, 'sound/weapons/thudswoosh.ogg', 50, 1, -1) - -/mob/living/silicon/pai/proc/savefile_path(mob/user) - return "data/player_saves/[copytext(user.ckey, 1, 2)]/[user.ckey]/pai.sav" - -/mob/living/silicon/pai/proc/savefile_save(mob/user) - if(IsGuestKey(user.key)) - return 0 - - var/savefile/F = new /savefile(src.savefile_path(user)) - - - F["name"] << src.name - F["description"] << src.flavor_text - F["eyecolor"] << src.eye_color - F["chassis"] << src.chassis - F["emotion"] << src.card.current_emotion - F["gender"] << src.gender - F["version"] << 1 - - return 1 - -/mob/living/silicon/pai/proc/savefile_load(mob/user, var/silent = 1) - if (IsGuestKey(user.key)) - return 0 - - var/path = savefile_path(user) - - if (!fexists(path)) - return 0 - - var/savefile/F = new /savefile(path) - - if(!F) return //Not everyone has a pai savefile. - - var/version = null - F["version"] >> version - - if (isnull(version) || version != 1) - fdel(path) - if (!silent) - tgui_alert_async(user, "Your savefile was incompatible with this version and was deleted.") - return 0 - var/ourname - var/ouremotion - var/ourdesc - var/oureyes - var/ourchassis - var/ourgender - F["name"] >> ourname - F["description"] >> ourdesc - F["eyecolor"] >> oureyes - F["chassis"] >> ourchassis - F["emotion"] >> ouremotion - F["gender"] >> ourgender - if(ourname) - SetName(ourname) - if(ourdesc) - flavor_text = ourdesc - if(ourchassis) - chassis = ourchassis - if(ourgender) - gender = ourgender - if(oureyes) - card.screen_color = oureyes - eye_color = oureyes - if(ouremotion) - card.setEmotion(ouremotion) - - update_icon() - return 1 - -/mob/living/silicon/pai/verb/save_pai_to_slot() - set category = "Abilities.pAI Commands" - set name = "Save Configuration" - savefile_save(src) - to_chat(src, span_filter_notice("[name] configuration saved to global pAI settings.")) - -/mob/living/silicon/pai/a_intent_change(input as text) - . = ..() - - switch(a_intent) - if(I_HELP) - hud_used.help_intent.icon_state = "intent_help-s" - hud_used.disarm_intent.icon_state = "intent_disarm-n" - hud_used.grab_intent.icon_state = "intent_grab-n" - hud_used.hurt_intent.icon_state = "intent_harm-n" - - if(I_DISARM) - hud_used.help_intent.icon_state = "intent_help-n" - hud_used.disarm_intent.icon_state = "intent_disarm-s" - hud_used.grab_intent.icon_state = "intent_grab-n" - hud_used.hurt_intent.icon_state = "intent_harm-n" - - if(I_GRAB) - hud_used.help_intent.icon_state = "intent_help-n" - hud_used.disarm_intent.icon_state = "intent_disarm-n" - hud_used.grab_intent.icon_state = "intent_grab-s" - hud_used.hurt_intent.icon_state = "intent_harm-n" - - if(I_HURT) - hud_used.help_intent.icon_state = "intent_help-n" - hud_used.disarm_intent.icon_state = "intent_disarm-n" - hud_used.grab_intent.icon_state = "intent_grab-n" - hud_used.hurt_intent.icon_state = "intent_harm-s" - -/mob/living/silicon/pai/verb/toggle_gender_identity_vr() - set name = "Set Gender Identity" - set desc = "Sets the pronouns when examined and performing an emote." - set category = "IC.Settings" - var/new_gender_identity = tgui_input_list(src, "Please select a gender Identity:", "Set Gender Identity", list(FEMALE, MALE, NEUTER, PLURAL, HERM)) - if(!new_gender_identity) - return 0 - gender = new_gender_identity - return 1 - -/mob/living/silicon/pai/verb/pai_hide() - set name = "Hide" - set desc = "Allows to hide beneath tables or certain items. Toggled on or off." - set category = "Abilities.pAI" - - hide() - if(status_flags & HIDING) - hide_glow = TRUE - else - hide_glow = FALSE - update_icon() - -/mob/living/silicon/pai/verb/screen_message(message as text|null) - set category = "Abilities.pAI Commands" - set name = "Screen Message" - set desc = "Allows you to display a message on your screen. This will show up in the chat of anyone who is holding your card." - - if (src.client) - if(client.prefs.muted & MUTE_IC) - to_chat(src, span_warning("You cannot speak in IC (muted).")) - return - if(loc != card) - to_chat(src, span_warning("Your message won't be visible while unfolded!")) - if (!message) - message = tgui_input_text(src, "Enter text you would like to show on your screen.","Screen Message", encode = FALSE) - message = sanitize_or_reflect(message,src) - if (!message) - return - message = capitalize(message) - if (stat == DEAD) - return - card.screen_msg = message - var/logmsg = "(CARD SCREEN)[message]" - log_talk(logmsg, LOG_SAY) - to_chat(src, span_filter_say(span_cult("You print a message to your screen, \"[message]\""))) - if(isliving(card.loc)) - var/mob/living/L = card.loc - if(L.client) - to_chat(L, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\""))) - else return - else if(isbelly(card.loc)) - var/obj/belly/b = card.loc - if(b.owner.client) - to_chat(b.owner, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\""))) - else return - else if(istype(card.loc, /obj/item/pda)) - var/obj/item/pda/p = card.loc - if(isliving(p.loc)) - var/mob/living/L = p.loc - if(L.client) - to_chat(L, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\""))) - else return - else if(isbelly(p.loc)) - var/obj/belly/b = card.loc - if(b.owner.client) - to_chat(b.owner, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\""))) - else return - else return - else return - to_chat(src, span_notice("Your message was relayed.")) - for (var/mob/G in GLOB.player_list) - if (isnewplayer(G)) - continue - else if(isobserver(G) && G.client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears)) - if((client?.prefs?.read_preference(/datum/preference/toggle/whisubtle_vis) || check_rights_for(G.client, R_HOLDER)) && \ - G.client?.prefs?.read_preference(/datum/preference/toggle/ghost_see_whisubtle)) - to_chat(G, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\""))) - -/mob/living/silicon/pai/proc/touch_window(soft_name) //This lets us touch TGUI procs and windows that may be nested behind other TGUI procs and windows - if(stat != CONSCIOUS) //so we can access our software without having to open up the software interface TGUI window - to_chat(src, span_warning("You can't do that right now.")) - return - for(var/thing in software) - var/datum/pai_software/S = software[thing] - if(istype(S, /datum/pai_software) && S.name == soft_name) - if(S.toggle) - S.toggle(src) - to_chat(src, span_notice("You toggled [S.name].")) - refresh_software_status() - else - S.tgui_interact(src) - refresh_software_status() - return - for(var/thing in GLOB.pai_software_by_key) - var/datum/pai_software/our_soft = GLOB.pai_software_by_key[thing] - if(our_soft.name == soft_name) - if(!(ram >= our_soft.ram_cost)) - to_chat(src, span_warning("Insufficient RAM for download. (Cost [our_soft.ram_cost] : [ram] Remaining)")) - return - if(tgui_alert(src, "Do you want to download [our_soft.name]? It costs [our_soft.ram_cost], and you have [ram] remaining.", "Download [our_soft.name]", list("Yes", "No")) == "Yes") - if(!(ram >= our_soft.ram_cost)) - return - if(software[our_soft.id]) - return - ram -= our_soft.ram_cost - software[our_soft.id] = our_soft - to_chat(src, span_notice("You downloaded [our_soft.name]. ([ram] RAM remaining.)")) - refresh_software_status() - -/mob/living/silicon/pai/proc/refresh_software_status() //This manages the pAI software status buttons icon states based on if you have them and if they are enabled - for(var/thing in software) //this only gets called when you click one of the relevent buttons, rather than all the time! - var/datum/pai_software/soft = software[thing] - if(istype(soft,/datum/pai_software/med_records)) - soft_mr = TRUE - if(istype(soft,/datum/pai_software/sec_records)) - soft_sr = TRUE - if(istype(soft,/datum/pai_software/door_jack)) - soft_dj = TRUE - if(istype(soft,/datum/pai_software/atmosphere_sensor)) - soft_as = TRUE - if(istype(soft,/datum/pai_software/pai_hud)) - soft_ar = TRUE - if(istype(soft,/datum/pai_software/translator)) - soft_ut = TRUE - if(istype(soft,/datum/pai_software/signaller)) - soft_si = TRUE - if(istype(soft,/datum/pai_software/deathalarm)) - soft_da = TRUE - for(var/atom/movable/screen/pai/button in hud_used.other) - if(button.name == "medical records") - if(soft_mr) - button.icon_state = "[button.base_state]" - else - button.icon_state = "[button.base_state]_o" - if(button.name == "security records") - if(soft_sr) - button.icon_state = "[button.base_state]" - else - button.icon_state = "[button.base_state]_o" - if(button.name == "door jack") - if(soft_dj) - button.icon_state = "[button.base_state]" - else - button.icon_state = "[button.base_state]_o" - if(button.name == "atmosphere sensor") - if(soft_as) - button.icon_state = "[button.base_state]" - else - button.icon_state = "[button.base_state]_o" - if(button.name == "remote signaler") - if(soft_si) - button.icon_state = "[button.base_state]" - else - button.icon_state = "[button.base_state]_o" - if(button.name == "universal translator") - if(soft_ut && translator_on) - button.icon_state = "[button.base_state]" - else - button.icon_state = "[button.base_state]_o" - if(button.name == "ar hud") - if(soft_ar && paiHUD) - button.icon_state = "[button.base_state]" - else - button.icon_state = "[button.base_state]_o" - if(button.name == "death alarm") - if(soft_da && paiDA) - button.icon_state = "[button.base_state]" - else - button.icon_state = "[button.base_state]_o" - -//Procs for using the various UI buttons for your softwares -/mob/living/silicon/pai/proc/directives() - touch_window("Directives") - -/mob/living/silicon/pai/proc/crew_manifest() - touch_window("Crew Manifest") - -/mob/living/silicon/pai/proc/med_records() - touch_window("Medical Records") - -/mob/living/silicon/pai/proc/sec_records() - touch_window("Security Records") - -/mob/living/silicon/pai/proc/remote_signal() - touch_window("Remote Signaler") - -/mob/living/silicon/pai/proc/atmos_sensor() - touch_window("Atmosphere Sensor") - -/mob/living/silicon/pai/proc/translator() - touch_window("Universal Translator") - -/mob/living/silicon/pai/proc/door_jack() - touch_window("Door Jack") - -/mob/living/silicon/pai/proc/ar_hud() - touch_window("AR HUD") - -/mob/living/silicon/pai/proc/death_alarm() - touch_window("Death Alarm") - -/mob/living/silicon/pai/proc/get_character_icon() - if(!client || !client.prefs) return FALSE - var/mob/living/carbon/human/dummy/dummy = new () - //This doesn't include custom_items because that's ... hard. - client.prefs.dress_preview_mob(dummy) - sleep(1 SECOND) //Strange bug in preview code? Without this, certain things won't show up. Yay race conditions? - dummy.regenerate_icons() - - var/icon/new_holo = getCompoundIcon(dummy) - - dummy.tail_layering = TRUE - dummy.set_dir(NORTH) - var/icon/new_holo_north = getCompoundIcon(dummy) - - qdel(holo_icon) - qdel(holo_icon_north) - qdel(dummy) - holo_icon = new_holo - holo_icon_north = new_holo_north - return TRUE - -/mob/living/silicon/pai/set_dir(var/new_dir) - . = ..() - if(. && (chassis == "13")) - switch(dir) - if(SOUTH) - icon = holo_icon - else - icon = holo_icon_north - -/mob/living/silicon/pai/adjustBruteLoss(amount, include_robo) - . = ..() - if(amount > 0 && health <= 90) //Something's probably attacking us! - if(prob(amount)) //The more damage it is doing, the more likely it is to damage something important! - card.damage_random_component() - -/mob/living/silicon/pai/adjustFireLoss(amount, include_robo) - . = ..() - if(amount > 0 && health <= 90) - if(prob(amount)) - card.damage_random_component() diff --git a/code/modules/mob/living/silicon/pai/personality.dm b/code/modules/mob/living/silicon/pai/personality.dm deleted file mode 100644 index 1824789f55..0000000000 --- a/code/modules/mob/living/silicon/pai/personality.dm +++ /dev/null @@ -1,65 +0,0 @@ -/* - name - key - description - role - comments - ready = 0 -*/ - -/datum/paiCandidate/proc/savefile_path(mob/user) - return "data/player_saves/[copytext(user.ckey, 1, 2)]/[user.ckey]/pai.sav" - -/datum/paiCandidate/proc/savefile_save(mob/user) - if(IsGuestKey(user.key)) - return 0 - - var/savefile/F = new /savefile(src.savefile_path(user)) - - - F["name"] << src.name - F["description"] << src.description - F["role"] << src.role - F["comments"] << src.comments - - F["version"] << 1 - - return 1 - -// loads the savefile corresponding to the mob's ckey -// if silent=true, report incompatible savefiles -// returns 1 if loaded (or file was incompatible) -// returns 0 if savefile did not exist - -/datum/paiCandidate/proc/savefile_load(mob/user, var/silent = 1) - if (IsGuestKey(user.key)) - return 0 - - var/path = savefile_path(user) - - if (!fexists(path)) - return 0 - - var/savefile/F = new /savefile(path) - - if(!F) return //Not everyone has a pai savefile. - - var/version = null - F["version"] >> version - - if (isnull(version) || version != 1) - fdel(path) - if (!silent) - tgui_alert_async(user, "Your savefile was incompatible with this version and was deleted.") - return 0 - - F["name"] >> src.name - F["description"] >> src.description - F["role"] >> src.role - F["comments"] >> src.comments - F["eyecolor"] >> src.eye_color - F["chassis"] >> src.chassis - F["emotion"] >> src.ouremotion - F["gender"] >> src.gender - - return 1 diff --git a/code/modules/mob/living/silicon/pai/recruit.dm b/code/modules/mob/living/silicon/pai/recruit.dm deleted file mode 100644 index 1a6603c78a..0000000000 --- a/code/modules/mob/living/silicon/pai/recruit.dm +++ /dev/null @@ -1,398 +0,0 @@ -//This file was auto-corrected by findeclaration.exe on 25.5.2012 20:42:33 - -// Recruiting observers to play as pAIs - -var/datum/paiController/paiController // Global handler for pAI candidates - -/datum/paiCandidate - var/name - var/key - var/description - var/role - var/comments - var/ready = 0 - var/chassis - var/ouremotion - var/eye_color - var/gender - -/hook/startup/proc/paiControllerSetup() - paiController = new /datum/paiController() - return 1 - - -/datum/paiController - var/inquirer = null - var/list/pai_candidates = list() - var/list/asked = list() - - var/askDelay = 10 * 60 * 1 // One minute [ms * sec * min] - -/datum/paiController/Topic(href, href_list[]) - if(href_list["download"]) - var/datum/paiCandidate/candidate = locate(href_list["candidate"]) - var/obj/item/paicard/card = locate(href_list["device"]) - if(card.pai) - return - if(istype(card,/obj/item/paicard) && istype(candidate,/datum/paiCandidate)) - var/mob/living/silicon/pai/pai = new(card) - pai.key = candidate.key - GLOB.paikeys |= pai.ckey - card.setPersonality(pai) - if(!candidate.name) - pai.SetName(pick(GLOB.ninja_names)) - else - pai.SetName(candidate.name) - if(candidate.description) - pai.flavor_text = candidate.description - if(candidate.eye_color) - pai.eye_color = candidate.eye_color - card.screen_color = pai.eye_color - if(candidate.chassis) - pai.chassis = candidate.chassis - if(candidate.ouremotion) - card.setEmotion(candidate.ouremotion) - if(candidate.gender) - pai.gender = candidate.gender - pai.update_icon() - pai.real_name = pai.name - card.looking_for_personality = 0 - - if(pai.mind) update_antag_icons(pai.mind) - - pai_candidates -= candidate - usr << browse(null, "window=findPai") - - if(href_list["new"]) - var/datum/paiCandidate/candidate = locate(href_list["candidate"]) - var/option = href_list["option"] - var/t = "" - - switch(option) - if("name") - t = sanitizeSafe(tgui_input_text(usr, "Enter a name for your pAI", "pAI Name", candidate.name, MAX_NAME_LEN, encode = FALSE), MAX_NAME_LEN) - if(t) - candidate.name = t - if("desc") - t = tgui_input_text(usr, "Enter a description for your pAI", "pAI Description", candidate.description, multiline = TRUE, prevent_enter = TRUE) - if(t) - candidate.description = sanitize(t, encode = FALSE) - if("role") - t = tgui_input_text(usr, "Enter a role for your pAI", "pAI Role", candidate.role) - if(t) - candidate.role = sanitize(t, encode = FALSE) - if("ooc") - t = tgui_input_text(usr, "Enter any OOC comments", "pAI OOC Comments", candidate.comments, multiline = TRUE, prevent_enter = TRUE) - if(t) - candidate.comments = sanitize(t, encode = FALSE) - if("save") - candidate.savefile_save(usr) - if("load") - candidate.savefile_load(usr) - //In case people have saved unsanitized stuff. - if(candidate.name) - candidate.name = sanitizeSafe(candidate.name, MAX_NAME_LEN) - if(candidate.description) - candidate.description = sanitize(candidate.description) - if(candidate.role) - candidate.role = sanitize(candidate.role) - if(candidate.comments) - candidate.comments = sanitize(candidate.comments) - - if("submit") - if(candidate) - candidate.ready = 1 - for(var/obj/item/paicard/p in GLOB.all_pai_cards) - if(p.looking_for_personality == 1) - p.alertUpdate() - usr << browse(null, "window=paiRecruit") - return - - recruitWindow(usr, href_list["allow_submit"] != "0") - -/datum/paiController/proc/recruitWindow(var/mob/M as mob, allowSubmit = 1) - var/datum/paiCandidate/candidate - for(var/datum/paiCandidate/c in pai_candidates) - if(!istype(c) || !istype(M)) - break - if(c.key == M.key) - candidate = c - if(!candidate) - candidate = new /datum/paiCandidate() - candidate.key = M.key - pai_candidates.Add(candidate) - - var/dat = "" - dat += {" - - "} - - dat += {" - - pAI Personality Configuration -

Please configure your pAI personality's options. Remember, what you enter here could determine whether or not the user requesting a personality chooses you!

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name:[candidate.name] 
What you plan to call yourself. Suggestions: Any character name you would choose for a station character OR an AI.
Description:[candidate.description] 
What sort of pAI you typically play; your mannerisms, your quirks, etc. This can be as sparse or as detailed as you like.
Preferred Role:[candidate.role] 
Do you like to partner with sneaky social ninjas? Like to help security hunt down thugs? Enjoy watching an engineer's back while he saves the station yet again? This doesn't have to be limited to just station jobs. Pretty much any general descriptor for what you'd like to be doing works here.
OOC Comments:[candidate.comments] 
Anything you'd like to address specifically to the player reading this in an OOC manner. \"I prefer more serious RP.\", \"I'm still learning the interface!\", etc. Feel free to leave this blank if you want.
-
- - - - - - - -
- Save Personality -
- Load Personality -

- "} - if(allowSubmit) - dat += {" - - -
Submit Personality

- "} - dat += {" - - "} - - M << browse("[dat]", "window=paiRecruit;size=580x580;") - -/datum/paiController/proc/findPAI(var/obj/item/paicard/p, var/mob/user) - requestRecruits(user) - var/list/available = list() - for(var/datum/paiCandidate/c in paiController.pai_candidates) - if(c.ready) - var/found = 0 - for(var/mob/observer/dead/o in GLOB.player_list) - if(o.key == c.key && o.MayRespawn()) - found = 1 - if(found) - available.Add(c) - var/dat = "" - - dat += {" - - - - - - - pAI Availability List

- "} - dat += "

Displaying available AI personalities from central database... If there are no entries, or if a suitable entry is not listed, check again later as more personalities may be added.

" - - for(var/datum/paiCandidate/c in available) - dat += {" - - - - - - - - - - - - - - - - - -
Name:[c.name]
Description:[c.description]
Preferred Role:[c.role]
OOC Comments:[c.comments]
- - -
Download [c.name] -
-
- "} - - dat += {" - - - "} - - user << browse(dat, "window=findPai") - - -/datum/paiController/proc/requestRecruits(var/mob/user) - inquirer = user - for(var/mob/observer/dead/O in GLOB.player_list) - if(!O.MayRespawn()) - continue - if(jobban_isbanned(O, "pAI")) - continue - if(asked.Find(O.key)) - if(world.time < asked[O.key] + askDelay) - continue - else - asked.Remove(O.key) - if(O.client) - if(O.client.prefs.be_special & BE_PAI) - question(O.client) - -/datum/paiController/proc/question(var/client/C) - spawn(0) - if(!C) return - asked.Add(C.key) - asked[C.key] = world.time - - var/mob/ourmob = C.mob - if(ourmob) - var/time_till_respawn = ourmob.time_till_respawn() - if(time_till_respawn == -1 || time_till_respawn) - return - for(var/ourkey in GLOB.paikeys) - if(ourkey == ourmob.ckey) - return - - var/response = tgui_alert(C, "[inquirer] is requesting a pAI personality. Would you like to play as a personal AI?", "pAI Request", list("Yes", "No", "Never for this round")) - if(!C) return //handle logouts that happen whilst the alert is waiting for a response. - if(response == "Yes") - recruitWindow(C.mob) - else if (response == "Never for this round") - C.prefs.be_special ^= BE_PAI diff --git a/code/modules/mob/living/silicon/pai/software.dm b/code/modules/mob/living/silicon/pai/software.dm index 67fdc68969..59693e3684 100644 --- a/code/modules/mob/living/silicon/pai/software.dm +++ b/code/modules/mob/living/silicon/pai/software.dm @@ -1,39 +1,3 @@ -var/list/pai_emotions = list( - "Neutral" = 1, - "What" = 2, - "Happy" = 3, - "Cat" = 4, - "Extremely Happy" = 5, - "Face" = 6, - "Laugh" = 7, - "Sad" = 8, - "Angry" = 9, - "Silly" = 10, - "Nose" = 11, - "Smirk" = 12, - "Exclamation Points" = 13, - "Question Mark" = 14, - "Blank" = 15, - "Off" = 16 - ) - - -GLOBAL_LIST_EMPTY(pai_software_by_key) -GLOBAL_LIST_EMPTY(default_pai_software) -/hook/startup/proc/populate_pai_software_list() - var/r = 1 // I would use ., but it'd sacrifice runtime detection - for(var/type in subtypesof(/datum/pai_software)) - var/datum/pai_software/P = new type() - if(GLOB.pai_software_by_key[P.id]) - var/datum/pai_software/O = GLOB.pai_software_by_key[P.id] - to_chat(world, span_warning("pAI software module [P.name] has the same key as [O.name]!")) - r = 0 - continue - GLOB.pai_software_by_key[P.id] = P - if(P.default) - GLOB.default_pai_software[P.id] = P - return r - /mob/living/silicon/pai/Initialize(mapload) . = ..() software = GLOB.default_pai_software.Copy() @@ -79,11 +43,12 @@ GLOBAL_LIST_EMPTY(default_pai_software) // Emotions var/list/emotions = list() - for(var/name in pai_emotions) - var/list/emote = list() - emote["name"] = name - emote["id"] = pai_emotions[name] - emotions.Add(list(emote)) + for(var/name in GLOB.pai_emotions) + var/list/emote = list( + "displayText" = name, + "value" = GLOB.pai_emotions[name] + ) + UNTYPED_LIST_ADD(emotions, emote) data["emotions"] = emotions data["current_emotion"] = card.current_emotion @@ -114,6 +79,6 @@ GLOBAL_LIST_EMPTY(default_pai_software) if("image") var/img = text2num(params["image"]) - if(1 <= img && img <= (pai_emotions.len)) + if(1 <= img && img <= (GLOB.pai_emotions.len)) card.setEmotion(img) return TRUE diff --git a/code/modules/mob/living/silicon/pai/sprites/_sprite_datum.dm b/code/modules/mob/living/silicon/pai/sprites/_sprite_datum.dm new file mode 100644 index 0000000000..c1425a2ba9 --- /dev/null +++ b/code/modules/mob/living/silicon/pai/sprites/_sprite_datum.dm @@ -0,0 +1,209 @@ +/datum/pai_sprite + var/name = DEVELOPER_WARNING_NAME + var/hidden = FALSE + + var/sprite_icon = null + var/sprite_icon_state = null + var/has_eye_sprites = TRUE + var/flying = FALSE + var/belly_states = 1 + + var/can_rest = TRUE // We really shouldn't be permitting this... eventually. Remove when all sprite can rest. + var/resting_belly = TRUE + + var/holo_projector = FALSE // Special AI hologram-like handling + var/holo_eyes_icon_state = null + + var/pixel_x = 0 + var/pixel_y = 0 + var/icon_x = 32 + var/icon_y = 32 + var/vis_height = 32 + +// PAI sprites +/datum/pai_sprite/drone + name = PAI_DEFAULT_CHASSIS + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-repairbot" + +/datum/pai_sprite/cat + name = "Cat" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-cat" + +/datum/pai_sprite/mouse + name = "Mouse" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-mouse" + +/datum/pai_sprite/monkey + name = "Monkey" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-monkey" + +/datum/pai_sprite/borgi + name = "Borgi" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-borgi" + has_eye_sprites = FALSE + +/datum/pai_sprite/fox + name = "Fox" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-fox" + has_eye_sprites = FALSE + +/datum/pai_sprite/parrot + name = "Parrot" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-parrot" + has_eye_sprites = FALSE + flying = TRUE + +/datum/pai_sprite/rabbit + name = "Rabbit" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-rabbit" + has_eye_sprites = FALSE + +/datum/pai_sprite/bear + name = "Bear" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-bear" + +/datum/pai_sprite/fennec + name = "Fennec" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-fen" + belly_states = 0 + +/datum/pai_sprite/type_zero + name = "Type Zero" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-typezero" + +/datum/pai_sprite/raccoon + name = "Raccoon" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-raccoon" + +/datum/pai_sprite/raptor + name = "Raptor" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-raptor" + +/datum/pai_sprite/corgi + name = "Corgi" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-corgi" + has_eye_sprites = FALSE + +/datum/pai_sprite/bat + name = "Bat" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-bat" + flying = TRUE + +/datum/pai_sprite/butterfly + name = "Butterfly" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-butterfly" + flying = TRUE + belly_states = 0 + +/datum/pai_sprite/hawk + name = "Hawk" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-hawk" + has_eye_sprites = FALSE + flying = TRUE + belly_states = 0 + +/datum/pai_sprite/duffel + name = "Duffel" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "pai-duffel" + has_eye_sprites = FALSE + +/datum/pai_sprite/cyber_elf + name = "Cyber Elf" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "cyberelf" + flying = TRUE + resting_belly = FALSE + +/datum/pai_sprite/catslug + name = "Catslug" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "catslug" + resting_belly = FALSE + +/datum/pai_sprite/car + name = "Car" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "car" + +/datum/pai_sprite/type_one + name = "Type One" + sprite_icon = 'icons/mob/pai.dmi' + sprite_icon_state = "typeone" + +/datum/pai_sprite/type_thirteen + name = "Type Thirteen" + sprite_icon = 'icons/mob/pai.dmi' + holo_projector = TRUE + sprite_icon_state = "type13" + holo_eyes_icon_state = "type13-eyes" + +// Large pai sprites +/datum/pai_sprite/large + pixel_x = -16 + icon_x = 64 + icon_y = 64 + vis_height = 64 + +/datum/pai_sprite/large/dire_wolf + name = "Dire wolf" + sprite_icon = 'icons/mob/pai64x64.dmi' + sprite_icon_state = "pai-diredog" + +/datum/pai_sprite/large/horse_lune + name = "Horse (Lune)" + sprite_icon = 'icons/mob/pai64x64.dmi' + sprite_icon_state = "pai-horse_lune" + can_rest = FALSE // FIX ME! + resting_belly = FALSE + +/datum/pai_sprite/large/horse_soleil + name = "Horse (Soleil)" + sprite_icon = 'icons/mob/pai64x64.dmi' + sprite_icon_state = "pai-horse_soleil" + can_rest = FALSE // FIX ME! + resting_belly = FALSE + +/datum/pai_sprite/large/dragon + name = "Dragon" + sprite_icon = 'icons/mob/pai64x64.dmi' + sprite_icon_state = "pai-pdragon" + can_rest = FALSE // FIX ME! + resting_belly = FALSE + +/datum/pai_sprite/large/rat + name = "Rat" + sprite_icon = 'icons/mob/pai64x64.dmi' + sprite_icon_state = "rat" + +/datum/pai_sprite/large/panther + name = "Panther" + sprite_icon = 'icons/mob/pai64x64.dmi' + sprite_icon_state = "panther" + +/datum/pai_sprite/large/teppi + name = "Teppi" + sprite_icon = 'icons/mob/pai64x64.dmi' + sprite_icon_state = "teppi" + +/datum/pai_sprite/large/protogen_dog + name = "Protogen Dog" + sprite_icon = 'icons/mob/pai64x64.dmi' + sprite_icon_state = "pai-protodog" diff --git a/code/modules/mob/living/silicon/robot/robot_ui_module.dm b/code/modules/mob/living/silicon/robot/robot_ui_module.dm index 2923f0c539..6548a8672f 100644 --- a/code/modules/mob/living/silicon/robot/robot_ui_module.dm +++ b/code/modules/mob/living/silicon/robot/robot_ui_module.dm @@ -84,7 +84,7 @@ model_type = "wide" if(istype(S, /datum/robot_sprite/dogborg/tall)) model_type = "tall" - available_sprites += list(list("sprite" = S.name, "belly" = S.has_vore_belly_sprites, "type" = model_type)) + UNTYPED_LIST_ADD(available_sprites, list("sprite" = S.name, "belly" = S.has_vore_belly_sprites, "type" = model_type)) data["possible_sprites"] = available_sprites data["sprite_datum"] = sprite_datum diff --git a/code/modules/mob/new_player/lobby_browser.dm b/code/modules/mob/new_player/lobby_browser.dm index 9d1391b5b1..dfd984f082 100644 --- a/code/modules/mob/new_player/lobby_browser.dm +++ b/code/modules/mob/new_player/lobby_browser.dm @@ -141,6 +141,11 @@ QDEL_NULL(mind) qdel(src) + // pAI notify if we have be pAI invite on + SSpai.clear_pai_block_delay(REF(observer)) // Reset invite cooldown if we cancelled all invites for the round + if(SSpai.invite_valid(observer)) + observer.pai_card_ping() + return TRUE if("shownews") handle_server_news() diff --git a/code/modules/organs/subtypes/standard.dm b/code/modules/organs/subtypes/standard.dm index 79fbdb6ce8..1b3aa7bdab 100644 --- a/code/modules/organs/subtypes/standard.dm +++ b/code/modules/organs/subtypes/standard.dm @@ -363,8 +363,8 @@ //Eye color/icon var/should_have_eyes = owner.should_have_organ(O_EYES) - var/has_eye_color = owner.species.appearance_flags & HAS_EYE_COLOR - if((should_have_eyes || has_eye_color) && eye_icon) + var/has_eye_sprites = owner.species.appearance_flags & HAS_EYE_COLOR + if((should_have_eyes || has_eye_sprites) && eye_icon) var/obj/item/organ/internal/eyes/eyes = owner.internal_organs_by_name[O_EYES] var/icon/eyes_icon = new/icon(eye_icon_location, eye_icon) //Do we have a special eye icon with its own coloration? Remove @@ -373,7 +373,7 @@ if(should_have_eyes) //And we have them if(eyes) - if(has_eye_color) + if(has_eye_sprites) eyes_icon.Blend(rgb(eyes.eye_colour[1], eyes.eye_colour[2], eyes.eye_colour[3]), ICON_ADD) //They're gone! else diff --git a/code/modules/power/sensors/sensor_monitoring.dm b/code/modules/power/sensors/sensor_monitoring.dm index 79210ca478..2def9f56a1 100644 --- a/code/modules/power/sensors/sensor_monitoring.dm +++ b/code/modules/power/sensors/sensor_monitoring.dm @@ -49,6 +49,9 @@ return tgui_interact(user) +/obj/machinery/computer/power_monitor/allow_pai_interaction(proximity_flag) + return proximity_flag + // Uses dark magic to operate the NanoUI of this computer. /obj/machinery/computer/power_monitor/tgui_interact(mob/user, var/datum/tgui/ui = null) power_monitor.tgui_interact(user, ui) diff --git a/code/modules/recycling/conveyor2.dm b/code/modules/recycling/conveyor2.dm index 9595d1c6d1..37321fadbd 100644 --- a/code/modules/recycling/conveyor2.dm +++ b/code/modules/recycling/conveyor2.dm @@ -342,6 +342,9 @@ to_chat(user, "You adjust the speed of the conveyor switch") return +/obj/machinery/conveyor_switch/allow_pai_interaction(proximity_flag) + return proximity_flag + /obj/machinery/conveyor_switch/oneway oneway = 1 diff --git a/code/modules/recycling/disposal_machines.dm b/code/modules/recycling/disposal_machines.dm index b698194212..7a727e316c 100644 --- a/code/modules/recycling/disposal_machines.dm +++ b/code/modules/recycling/disposal_machines.dm @@ -170,6 +170,9 @@ user.visible_message("[user] places \the [I] into the [src].", "You place \the [I] into the [src].","Ca-Clunk") update_icon() +/obj/machinery/disposal/allow_pai_interaction(proximity_flag) + return proximity_flag + // Transform into next machine type /obj/machinery/disposal/proc/alter_bin_type(mob/user) if(contents.len > 0) diff --git a/code/modules/resleeving/sleevecard.dm b/code/modules/resleeving/sleevecard.dm index 3fa313ad6c..645d1a5f4e 100644 --- a/code/modules/resleeving/sleevecard.dm +++ b/code/modules/resleeving/sleevecard.dm @@ -124,10 +124,10 @@ // Emotions var/list/emotions = list() - for(var/name in pai_emotions) + for(var/name in GLOB.pai_emotions) var/list/emote = list() emote["name"] = name - emote["id"] = pai_emotions[name] + emote["id"] = GLOB.pai_emotions[name] emotions.Add(list(emote)) data["emotions"] = emotions diff --git a/code/modules/tgui/states/default.dm b/code/modules/tgui/states/default.dm index 7010735609..b5737eee3d 100644 --- a/code/modules/tgui/states/default.dm +++ b/code/modules/tgui/states/default.dm @@ -72,7 +72,11 @@ GLOBAL_DATUM_INIT(tgui_default_state, /datum/tgui_state/default, new) . = min(., shared_living_tgui_distance(src_object)) //simple animals can only use things they're near. /mob/living/silicon/pai/default_can_use_tgui_topic(src_object) - // pAIs can only use themselves and the owner's radio. + // Allows few objects... + var/obj/check_obj = src_object + if(!stat && istype(check_obj) && check_obj.allow_pai_interaction(get_dist(check_obj, src) <= 1)) + return STATUS_INTERACTIVE + // ...otherwise pAIs can only use themselves and the owner's radio. if((src_object == src || src_object == radio || src_object == communicator) && !stat) return STATUS_INTERACTIVE else diff --git a/code/modules/turbolift/turbolift_console.dm b/code/modules/turbolift/turbolift_console.dm index 22848ceb92..9b74ee66e7 100644 --- a/code/modules/turbolift/turbolift_console.dm +++ b/code/modules/turbolift/turbolift_console.dm @@ -97,6 +97,9 @@ return lift.queue_move_to(floor) +/obj/structure/lift/button/allow_pai_interaction(proximity_flag) + return proximity_flag + /obj/structure/lift/button/proc/light_up() light_up = TRUE update_icon() @@ -140,6 +143,9 @@ /obj/structure/lift/panel/attack_ghost(var/mob/user) return interact(user) +/obj/structure/lift/panel/allow_pai_interaction(proximity_flag) + return proximity_flag + /obj/structure/lift/panel/interact(var/mob/user) if(!..()) return diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 0fb8f6fb54..8dc1f82a5c 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -106,7 +106,9 @@ #include "mapping.dm" #include "material_tests.dm" // #include "nuke_cinematic.dm" // TODO: This is probably fixed later on +#include "pai_tests.dm" #include "poster_tests.dm" +#include "special_roles.dm" // #include "preferences.dm" // This unit test is missing some other stuff #include "reagent_tests.dm" #include "recipe_tests.dm" diff --git a/code/modules/unit_tests/pai_tests.dm b/code/modules/unit_tests/pai_tests.dm new file mode 100644 index 0000000000..1b63bde038 --- /dev/null +++ b/code/modules/unit_tests/pai_tests.dm @@ -0,0 +1,95 @@ +/datum/unit_test/pai_software_shall_have_unique_id + +/datum/unit_test/pai_software_shall_have_unique_id/Run() + for(var/datum/pai_software/P as anything in subtypesof(/datum/pai_software)) + // Compare against all other pai softwares + for(var/datum/pai_software/O as anything in subtypesof(/datum/pai_software)) + if(P == O) // Don't test against ourselves + continue + if(initial(P.id) == initial(O.id)) + TEST_FAIL("pAI software module [P] has the same id as [O]!") + +/datum/unit_test/pai_sprites_shall_be_valid + +/datum/unit_test/pai_sprites_shall_be_valid/Run() + var/failed = FALSE + var/list/used_names = list() + for(var/sprite_key in SSpai.get_chassis_list()) + var/datum/pai_sprite/sprite = SSpai.chassis_data(sprite_key) + if(sprite.sprite_icon == null) + continue + + if(sprite.name in used_names) + TEST_NOTICE(src, "[sprite.type] - Pai sprite name is not unique: \"[sprite.name]\"") + failed = TRUE + used_names.Add(sprite.name) + + if(!sprite.name || sprite.name == DEVELOPER_WARNING_NAME) + TEST_NOTICE(src, "[sprite.type] - Pai sprite had invalid name: \"[sprite.name]\"") + failed = TRUE + + + if(!sprite.holo_projector) + // check if we have valid icons + if(!sprite.sprite_icon_state) + TEST_NOTICE(src, "[sprite.type] - Pai sprite had no iconstate assigned") + failed = TRUE + + if(!icon_exists(sprite.sprite_icon, sprite.sprite_icon_state)) + TEST_NOTICE(src, "[sprite.type] - Pai sprite state \"[sprite.sprite_icon_state]\" did not exist in the dmi set \"[sprite.sprite_icon]\"") + failed = TRUE + + if(sprite.can_rest) + if(!icon_exists(sprite.sprite_icon, "[sprite.sprite_icon_state]_rest")) + TEST_NOTICE(src, "[sprite.type] - Pai sprite state \"[sprite.sprite_icon_state]_rest\" did not exist in the dmi set \"[sprite.sprite_icon]\"") + failed = TRUE + + // Check for vore stuff + if(sprite.belly_states > 0) + for(var/i = 1 to sprite.belly_states) + var/fullness_extension = "" + if(i > 1) + fullness_extension = "_[i]" + // standing bellies + if(!icon_exists(sprite.sprite_icon, "[sprite.sprite_icon_state]_full[fullness_extension]")) + TEST_NOTICE(src, "[sprite.type] - Pai sprite state \"[sprite.sprite_icon_state]_full[fullness_extension]\" did not exist in the dmi set \"[sprite.sprite_icon]\"") + failed = TRUE + if(sprite.can_rest && sprite.resting_belly) + // resting bellies + if(!icon_exists(sprite.sprite_icon, "[sprite.sprite_icon_state]_rest_full[fullness_extension]")) + TEST_NOTICE(src, "[sprite.type] - Pai sprite state \"[sprite.sprite_icon_state]_rest_full[fullness_extension]\" did not exist in the dmi set \"[sprite.sprite_icon]\"") + failed = TRUE + + // Does it have colorable eyes? + if(sprite.has_eye_sprites) + if(!icon_exists(sprite.sprite_icon, "[sprite.sprite_icon_state]-eyes")) + TEST_NOTICE(src, "[sprite.type] - Pai sprite state \"[sprite.sprite_icon_state]-eyes\" did not exist in the dmi set \"[sprite.sprite_icon]\"") + failed = TRUE + if(sprite.can_rest) + if(!icon_exists(sprite.sprite_icon, "[sprite.sprite_icon_state]_rest-eyes")) + TEST_NOTICE(src, "[sprite.type] - Pai sprite state \"[sprite.sprite_icon_state]_rest-eyes\" did not exist in the dmi set \"[sprite.sprite_icon]\"") + failed = TRUE + + // Death + if(!icon_exists(sprite.sprite_icon, "[sprite.sprite_icon_state]_dead")) + TEST_NOTICE(src, "[sprite.type] - Pai sprite state \"[sprite.sprite_icon_state]_dead\" did not exist in the dmi set \"[sprite.sprite_icon]\"") + failed = TRUE + + else // Holoprojector uses ai holo instead of a sprite + // Holoprojectil eyes need to exist at all sizes for the state provided + if(sprite.holo_eyes_icon_state) + if(!icon_exists('icons/mob/pai.dmi', sprite.holo_eyes_icon_state)) + TEST_NOTICE(src, "[sprite.type] - Pai holoprojection eyes state \"[sprite.holo_eyes_icon_state]\" did not exist in the dmi set \"['icons/mob/pai.dmi']\"") + failed = TRUE + if(!icon_exists('icons/mob/pai32x64.dmi', sprite.holo_eyes_icon_state)) + TEST_NOTICE(src, "[sprite.type] - Pai holoprojection eyes state \"[sprite.holo_eyes_icon_state]\" did not exist in the dmi set \"['icons/mob/pai32x64.dmi']\"") + failed = TRUE + if(!icon_exists('icons/mob/pai64x32.dmi', sprite.holo_eyes_icon_state)) + TEST_NOTICE(src, "[sprite.type] - Pai holoprojection eyes state \"[sprite.holo_eyes_icon_state]\" did not exist in the dmi set \"['icons/mob/pai64x32.dmi']\"") + failed = TRUE + if(!icon_exists('icons/mob/pai64x64.dmi', sprite.holo_eyes_icon_state)) + TEST_NOTICE(src, "[sprite.type] - Pai holoprojection eyes state \"[sprite.holo_eyes_icon_state]\" did not exist in the dmi set \"['icons/mob/pai64x64.dmi']\"") + failed = TRUE + + if(failed) + TEST_FAIL("PAI sprites had invalid entries.") diff --git a/code/modules/unit_tests/special_roles.dm b/code/modules/unit_tests/special_roles.dm new file mode 100644 index 0000000000..ed24ed3f6a --- /dev/null +++ b/code/modules/unit_tests/special_roles.dm @@ -0,0 +1,13 @@ +/datum/unit_test/special_role_flags_syncronized + +/datum/unit_test/special_role_flags_syncronized/Run() + if(GLOB.special_roles.len != GLOB.be_special_flags.len) + TEST_FAIL("GLOB.special_roles and GLOB.be_special_flags lengths do not match. [GLOB.special_roles] vs [GLOB.be_special_flags].") + + // These get out of order way too often. + var/index = 1 + for(var/key in GLOB.special_roles) + var/check_key = GLOB.be_special_flags[index] + if(lowertext(check_key) != lowertext(key)) + TEST_FAIL("Special role flag misalignment index: [index], be_special_flags: [check_key], special_roles: [key]. Keys must also match when caste to lowercase.") + index++ diff --git a/icons/mob/pai.dmi b/icons/mob/pai.dmi index df6010159b..1e377affd3 100644 Binary files a/icons/mob/pai.dmi and b/icons/mob/pai.dmi differ diff --git a/icons/mob/pai_vr32x64.dmi b/icons/mob/pai32x64.dmi similarity index 100% rename from icons/mob/pai_vr32x64.dmi rename to icons/mob/pai32x64.dmi diff --git a/icons/mob/pai64x32.dmi b/icons/mob/pai64x32.dmi new file mode 100644 index 0000000000..468e5cd216 Binary files /dev/null and b/icons/mob/pai64x32.dmi differ diff --git a/icons/mob/pai64x64.dmi b/icons/mob/pai64x64.dmi new file mode 100644 index 0000000000..c91d49d015 Binary files /dev/null and b/icons/mob/pai64x64.dmi differ diff --git a/icons/mob/pai_vr.dmi b/icons/mob/pai_vr.dmi deleted file mode 100644 index f6c153ff45..0000000000 Binary files a/icons/mob/pai_vr.dmi and /dev/null differ diff --git a/icons/mob/pai_vr64x32.dmi b/icons/mob/pai_vr64x32.dmi deleted file mode 100644 index 547047510d..0000000000 Binary files a/icons/mob/pai_vr64x32.dmi and /dev/null differ diff --git a/icons/mob/pai_vr64x64.dmi b/icons/mob/pai_vr64x64.dmi deleted file mode 100644 index 77bd95eaa4..0000000000 Binary files a/icons/mob/pai_vr64x64.dmi and /dev/null differ diff --git a/tgui/packages/tgui-panel/chat/handlers.ts b/tgui/packages/tgui-panel/chat/handlers.ts index 023ccb6c90..d2f7870e62 100644 --- a/tgui/packages/tgui-panel/chat/handlers.ts +++ b/tgui/packages/tgui-panel/chat/handlers.ts @@ -58,3 +58,7 @@ export function chatMessage(payload: string): void { pushMessage(message); } + +export function exportChatlog() { + chatRenderer.saveToDisk(); +} diff --git a/tgui/packages/tgui-panel/events/listeners.ts b/tgui/packages/tgui-panel/events/listeners.ts index 797aaf96b6..ea6f48866c 100644 --- a/tgui/packages/tgui-panel/events/listeners.ts +++ b/tgui/packages/tgui-panel/events/listeners.ts @@ -1,7 +1,7 @@ import { loadStyleSheet } from 'common/assets'; import { EventBus } from 'tgui-core/eventbus'; import { playMusic, stopMusic } from '../audio/handlers'; -import { chatMessage } from '../chat/handlers'; +import { chatMessage, exportChatlog } from '../chat/handlers'; import { pingReply, pingSoft } from '../ping/handlers'; import { handleTelemetryData, @@ -25,6 +25,7 @@ const listeners = { 'telemetry/request': telemetryRequest, testTelemetryCommand, update: handleTelemetryData, + saveToDiskCommand: exportChatlog, } as const; export const bus = new EventBus(listeners); diff --git a/tgui/packages/tgui/interfaces/PAICard/PAIActiveCompanion.tsx b/tgui/packages/tgui/interfaces/PAICard/PAIActiveCompanion.tsx new file mode 100644 index 0000000000..656ceaea8a --- /dev/null +++ b/tgui/packages/tgui/interfaces/PAICard/PAIActiveCompanion.tsx @@ -0,0 +1,178 @@ +import { useState } from 'react'; +import { useBackend } from 'tgui/backend'; +import { + Box, + Button, + LabeledList, + Section, + Stack, + TextArea, +} from 'tgui-core/components'; +import { healthToColor } from './functions'; +import { PaiIcon } from './PaiIcon'; +import type { ActivePAIData } from './types'; + +export const PAIActiveCompanion = (props: { activeData: ActivePAIData }) => { + const { act } = useBackend(); + const [newDirective, setNewDirective] = useState(''); + + const { activeData } = props; + const { + name, + color, + health, + chassis, + law_zero, + law_extra, + master_name, + master_dna, + screen_msg, + radio_data, + sprite_datum_class, + sprite_datum_size, + } = activeData; + + return ( +
+ + +
+ + act('cleardna')} + > + Clear DNA + + + + + + + } + > + + + {!!master_name && !!master_dna ? ( + <> + + {master_name} + + : {master_dna} + + ) : ( + 'unbound' + )} + + + + + + + + {health} + + {!!screen_msg && ( + {screen_msg} + )} + + + {law_zero} + + {!!law_extra && ( + + {law_extra} + + )} + +
+
+ +
+ {radio_data ? ( + + + + + + + + + ) : ( + 'Radio firmware not loaded. Please install a pAI personality to load firmware.' + )} +
+
+ +
+ + + Clear + + + + + + + } + > + + +