From 1feffb747a33434a9d28450fc52ade75253aeba5 Mon Sep 17 00:00:00 2001 From: carlarctg <53100513+carlarctg@users.noreply.github.com> Date: Sat, 26 Aug 2023 17:17:21 -0300 Subject: [PATCH] Adds a syndicate AI card for nuke ops (#76546) ## About The Pull Request Adds a syndicate AI card for nuke ops. Costs 12 TC, can be refunded. Activating it in-hand opens up a ghost poll like normal reinforcements. ## Why It's Good For The Game > Adds a syndicate AI card for nuke ops. Costs 12 TC, can be refunded. Activating it in-hand opens up a ghost poll. It'll be fun to have a lil modsuit pal with you who can maybe help if you get stunned, in critical condition, or else. With an injector module they can even save your life, if it's loaded with medicine! Unless they can't actually inject, but oh well, out of scope. 12 TC seems fair to me, not so expensive that nobody's going to bother with something that's not really useful in the vast majority of circumstances, but not so cheap that everyone will get one. There are also doubtless a huge amount of gimmicks that savvy nukies can get up to, having a pocket AI. **Since this needs two players to be fully tested, I wasn't able to do so. Please testmerge this before merging so we can confirm it works ingame!** ## Changelog :cl: add: Adds a syndicate AI card for nuke ops. Costs 12 TC, can be refunded. Activating it in-hand opens up a ghost poll like normal reinforcements. Base interaction range for syndicate AIs is one, which means they can handle electronics only in proximity. add: Adds a syndicate AI interaction range upgrade for nuke ops, costs 4 TC and can be applied onto a syndicate AI (inside any container) to increase its interaction range by two per. (Three purchases are recommended for seven tiles of range!) /:cl: --- code/_onclick/cyborg.dm | 22 ++--- code/game/machinery/firealarm.dm | 2 +- code/game/objects/items/devices/aicard.dm | 86 +++++++++++++++++- .../items/devices/radio/encryptionkey.dm | 8 ++ .../objects/items/devices/radio/headset.dm | 9 ++ code/modules/mob/living/silicon/ai/ai.dm | 16 +++- code/modules/mob/living/silicon/silicon.dm | 1 + code/modules/uplink/uplink_items/nukeops.dm | 18 ++++ icons/obj/aicards.dmi | Bin 4114 -> 4576 bytes 9 files changed, 142 insertions(+), 20 deletions(-) diff --git a/code/_onclick/cyborg.dm b/code/_onclick/cyborg.dm index d77923f9c94..516b771c247 100644 --- a/code/_onclick/cyborg.dm +++ b/code/_onclick/cyborg.dm @@ -109,7 +109,7 @@ CtrlShiftClick(user) /obj/machinery/door/airlock/BorgCtrlShiftClick(mob/living/silicon/robot/user) // Sets/Unsets Emergency Access Override Forwards to AI code. - if(get_dist(src, user) <= user.interaction_range) + if(get_dist(src, user) <= user.interaction_range && !(user.control_disabled)) AICtrlShiftClick(user) else ..() @@ -118,7 +118,7 @@ ShiftClick(user) /obj/machinery/door/airlock/BorgShiftClick(mob/living/silicon/robot/user) // Opens and closes doors! Forwards to AI code. - if(get_dist(src, user) <= user.interaction_range) + if(get_dist(src, user) <= user.interaction_range && !(user.control_disabled)) AIShiftClick(user) else ..() @@ -128,44 +128,44 @@ CtrlClick(user) /obj/machinery/door/airlock/BorgCtrlClick(mob/living/silicon/robot/user) // Bolts doors. Forwards to AI code. - if(get_dist(src, user) <= user.interaction_range) + if(get_dist(src, user) <= user.interaction_range && !(user.control_disabled)) AICtrlClick(user) else ..() /obj/machinery/power/apc/BorgCtrlClick(mob/living/silicon/robot/user) // turns off/on APCs. Forwards to AI code. - if(get_dist(src, user) <= user.interaction_range) + if(get_dist(src, user) <= user.interaction_range && !(user.control_disabled)) AICtrlClick(user) else ..() /obj/machinery/power/apc/BorgCtrlShiftClick(mob/living/silicon/robot/user) - if(get_dist(src, user) <= user.interaction_range) + if(get_dist(src, user) <= user.interaction_range && !(user.control_disabled)) AICtrlShiftClick(user) else ..() /obj/machinery/power/apc/BorgShiftClick(mob/living/silicon/robot/user) - if(get_dist(src, user) <= user.interaction_range) + if(get_dist(src, user) <= user.interaction_range && !(user.control_disabled)) AIShiftClick(user) else ..() /obj/machinery/power/apc/BorgAltClick(mob/living/silicon/robot/user) - if(get_dist(src, user) <= user.interaction_range) + if(get_dist(src, user) <= user.interaction_range && !(user.control_disabled)) AIAltClick(user) else ..() /obj/machinery/power/apc/attack_robot_secondary(mob/living/silicon/user, list/modifiers) - if(get_dist(src, user) <= user.interaction_range) + if(get_dist(src, user) <= user.interaction_range && !(user.control_disabled)) return attack_ai_secondary(user, modifiers) else ..() /obj/machinery/turretid/BorgCtrlClick(mob/living/silicon/robot/user) //turret control on/off. Forwards to AI code. - if(get_dist(src, user) <= user.interaction_range) + if(get_dist(src, user) <= user.interaction_range && !(user.control_disabled)) AICtrlClick(user) else ..() @@ -175,13 +175,13 @@ return /obj/machinery/door/airlock/BorgAltClick(mob/living/silicon/robot/user) // Eletrifies doors. Forwards to AI code. - if(get_dist(src, user) <= user.interaction_range) + if(get_dist(src, user) <= user.interaction_range && !(user.control_disabled)) AIAltClick(user) else ..() /obj/machinery/turretid/BorgAltClick(mob/living/silicon/robot/user) //turret lethal on/off. Forwards to AI code. - if(get_dist(src, user) <= user.interaction_range) + if(get_dist(src, user) <= user.interaction_range && !(user.control_disabled)) AIAltClick(user) else ..() diff --git a/code/game/machinery/firealarm.dm b/code/game/machinery/firealarm.dm index e4ff928985d..680a7a837d5 100644 --- a/code/game/machinery/firealarm.dm +++ b/code/game/machinery/firealarm.dm @@ -471,7 +471,7 @@ // Allows Silicons to disable thermal sensor /obj/machinery/firealarm/BorgCtrlClick(mob/living/silicon/robot/user) - if(get_dist(src,user) <= user.interaction_range) + if(get_dist(src,user) <= user.interaction_range && !(user.control_disabled)) AICtrlClick(user) return return ..() diff --git a/code/game/objects/items/devices/aicard.dm b/code/game/objects/items/devices/aicard.dm index c748d70e6fc..4b03fd8013a 100644 --- a/code/game/objects/items/devices/aicard.dm +++ b/code/game/objects/items/devices/aicard.dm @@ -18,6 +18,88 @@ . = ..() ADD_TRAIT(src, TRAIT_CASTABLE_LOC, INNATE_TRAIT) +/obj/item/computer_disk/syndie_ai_upgrade + name = "AI interaction range upgrade" + desc = "A NT data chip containing information that a syndiCard AI can utilize to improve its wireless interfacing abilities. Simply slap it on top of an intelliCard, MODsuit, or AI core and watch it do its work! It's rumoured that there's something 'pretty awful' in it." + icon = 'icons/obj/antags/syndicate_tools.dmi' + icon_state = "something_awful" + max_capacity = 1000 + w_class = WEIGHT_CLASS_NORMAL + +/obj/item/computer_disk/syndie_ai_upgrade/pre_attack(atom/A, mob/living/user, params) + var/mob/living/silicon/ai/AI + if(isAI(A)) + AI = A + else + AI = locate() in A + if(!AI || AI.interaction_range == INFINITY) + playsound(src,'sound/machines/buzz-sigh.ogg',50,FALSE) + to_chat(user, span_notice("Error! Incompatible object!")) + return ..() + AI.interaction_range += 2 + if(AI.interaction_range > 7) + AI.interaction_range = INFINITY + playsound(src,'sound/machines/twobeep.ogg',50,FALSE) + to_chat(user, span_notice("You insert [src] into [AI]'s compartment, and it beeps as it processes the data.")) + to_chat(AI, span_notice("You process [src], and find yourself able to manipulate electronics from up to [AI.interaction_range] meters!")) + qdel(src) + +/obj/item/aicard/syndie + name = "syndiCard" + desc = "A storage device for AIs. Nanotrasen forgot to make the patent, so the Syndicate made their own version!" + icon = 'icons/obj/aicards.dmi' + icon_state = "syndicard" + base_icon_state = "syndicard" + item_flags = null + force = 7 + +/obj/item/aicard/syndie/loaded + var/being_or_was_used = FALSE + +/obj/item/aicard/syndie/loaded/examine(mob/user) + . = ..() + + . += span_notice("This one has a little S.E.L.F. insignia on the back, and a label next to it that says 'Activate for one FREE aligned AI! Please attempt uplink reintegration or ask your employers for reimbursal if AI is unavailable or belligerent.") + +/obj/item/aicard/syndie/loaded/attack_self(mob/user, modifiers) + if(AI || being_or_was_used) + return ..() + being_or_was_used = TRUE + to_chat(user, span_notice("Connecting to S.E.L.F. dispatch...")) + being_or_was_used = procure_ai(user) + +/obj/item/aicard/syndie/loaded/proc/procure_ai(mob/user) + var/datum/antagonist/nukeop/creator_op = user.mind?.has_antag_datum(/datum/antagonist/nukeop,TRUE) + if(!creator_op) + return FALSE + var/list/nuke_candidates = poll_ghost_candidates("Do you want to play as a syndicate artifical intelligence inside an intelliCard?", ROLE_OPERATIVE, ROLE_OPERATIVE, 150, POLL_IGNORE_SYNDICATE) + if(QDELETED(src)) + return FALSE + if(!LAZYLEN(nuke_candidates)) + to_chat(user, span_warning("Unable to connect to S.E.L.F. dispatch. Please wait and try again later or use the intelliCard on your uplink to get your points refunded.")) + return FALSE + // pick ghost, create AI and transfer + var/mob/dead/observer/ghos = pick(nuke_candidates) + var/mob/living/silicon/ai/weak_syndie/new_ai = new /mob/living/silicon/ai/weak_syndie(get_turf(src), null, ghos) // wow so cool i love how laws go before the mob to insert for no reason this definitely didnt delay this pr for weeks + new_ai.key = ghos.key + // create and apply syndie datum + var/datum/antagonist/nukeop/nuke_datum = new() + nuke_datum.send_to_spawnpoint = FALSE + new_ai.mind.add_antag_datum(nuke_datum, creator_op.nuke_team) + new_ai.mind.special_role = "Syndicate AI" + new_ai.faction |= ROLE_SYNDICATE + // Make it look evil!!! + new_ai.hologram_appearance = mutable_appearance('icons/mob/silicon/ai.dmi',"xeno_queen") //good enough + new_ai.icon_state = resolve_ai_icon("hades") // evli + pre_attack(new_ai, user) // i love shitcode! + AI.control_disabled = FALSE // re-enable wireless activity + AI.radio_enabled = TRUE // ditto + var/obj/structure/ai_core/deactivated/detritus = locate() in get_turf(src) + qdel(detritus) + do_sparks(4, TRUE, src) + playsound(src, 'sound/machines/chime.ogg', 25, TRUE) + return TRUE + /obj/item/aicard/Destroy(force) if(AI) AI.ghostize(can_reenter_corpse = FALSE) @@ -142,10 +224,6 @@ . = TRUE if("wireless") AI.control_disabled = !AI.control_disabled - if(!AI.control_disabled) - AI.interaction_range = INFINITY - else - AI.interaction_range = 0 to_chat(AI, span_warning("[src]'s wireless port has been [AI.control_disabled ? "disabled" : "enabled"]!")) . = TRUE if("radio") diff --git a/code/game/objects/items/devices/radio/encryptionkey.dm b/code/game/objects/items/devices/radio/encryptionkey.dm index d3f26b13f34..0e124aafd01 100644 --- a/code/game/objects/items/devices/radio/encryptionkey.dm +++ b/code/game/objects/items/devices/radio/encryptionkey.dm @@ -190,5 +190,13 @@ /obj/item/encryptionkey/ai //ported from NT, this goes 'inside' the AI. channels = list(RADIO_CHANNEL_COMMAND = 1, RADIO_CHANNEL_SECURITY = 1, RADIO_CHANNEL_ENGINEERING = 1, RADIO_CHANNEL_SCIENCE = 1, RADIO_CHANNEL_MEDICAL = 1, RADIO_CHANNEL_SUPPLY = 1, RADIO_CHANNEL_SERVICE = 1, RADIO_CHANNEL_AI_PRIVATE = 1) +/obj/item/encryptionkey/ai/evil //ported from NT, this goes 'inside' the AI. + name = "syndicate binary encryption key" + icon_state = "cypherkey_syndicate" + channels = list(RADIO_CHANNEL_SYNDICATE = 1) + syndie = TRUE + greyscale_config = /datum/greyscale_config/encryptionkey_syndicate + greyscale_colors = "#171717#990000" + /obj/item/encryptionkey/secbot channels = list(RADIO_CHANNEL_AI_PRIVATE = 1, RADIO_CHANNEL_SECURITY = 1) diff --git a/code/game/objects/items/devices/radio/headset.dm b/code/game/objects/items/devices/radio/headset.dm index 2b1337f44ee..4623916338b 100644 --- a/code/game/objects/items/devices/radio/headset.dm +++ b/code/game/objects/items/devices/radio/headset.dm @@ -356,6 +356,15 @@ GLOBAL_LIST_INIT(channel_tokens, list( keyslot2 = new /obj/item/encryptionkey/ai command = TRUE +/obj/item/radio/headset/silicon/ai/evil + name = "\proper Evil Integrated Subspace Transceiver " + keyslot2 = new /obj/item/encryptionkey/ai/evil + command = FALSE + +/obj/item/radio/headset/silicon/ai/evil/Initialize(mapload) + . = ..() + make_syndie() + /obj/item/radio/headset/screwdriver_act(mob/living/user, obj/item/tool) user.set_machine(src) if(keyslot || keyslot2) diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index b27f56567f8..ba60806133f 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -52,7 +52,6 @@ var/can_dominate_mechs = FALSE var/shunted = FALSE //1 if the AI is currently shunted. Used to differentiate between shunted and ghosted/braindead var/obj/machinery/ai_voicechanger/ai_voicechanger = null // reference to machine that holds the voicechanger - var/control_disabled = FALSE // Set to 1 to stop AI from interacting via Click() var/malfhacking = FALSE // More or less a copy of the above var, so that malf AIs can hack and still get new cyborgs -- NeoFite var/malf_cooldown = 0 //Cooldown var for malf modules, stores a worldtime + cooldown @@ -79,7 +78,7 @@ var/mob/camera/ai_eye/eyeobj var/sprint = 10 var/cooldown = 0 - var/acceleration = 1 + var/acceleration = TRUE var/obj/structure/ai_core/deactivated/linked_core //For exosuit control var/mob/living/silicon/robot/deployed_shell = null //For shell control @@ -200,6 +199,16 @@ RegisterSignal(alert_control.listener, COMSIG_ALARM_LISTENER_TRIGGERED, PROC_REF(alarm_triggered)) RegisterSignal(alert_control.listener, COMSIG_ALARM_LISTENER_CLEARED, PROC_REF(alarm_cleared)) +/mob/living/silicon/ai/weak_syndie + radio = /obj/item/radio/headset/silicon/ai/evil + radio_enabled = TRUE + interaction_range = 1 + sprint = 5 + +/mob/living/silicon/ai/weak_syndie/Initialize(mapload, datum/ai_laws/L, mob/target_ai) + . = ..() + laws = new /datum/ai_laws/syndicate_override + /mob/living/silicon/ai/key_down(_key, client/user) if(findtext(_key, "numpad")) //if it's a numpad number, we can convert it to just the number _key = _key[7] //strings, lists, same thing really @@ -870,7 +879,6 @@ new_core.circuit.battery = battery ai_restore_power()//So the AI initially has power. control_disabled = TRUE //Can't control things remotely if you're stuck in a card! - interaction_range = 0 radio_enabled = FALSE //No talking on the built-in radio for you either! forceMove(card) card.AI = src @@ -990,7 +998,7 @@ update_sight() if(client.eye != src) var/atom/AT = client.eye - AT.get_remote_view_fullscreens(src) + AT?.get_remote_view_fullscreens(src) else clear_fullscreen("remote_view", 0) diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm index 1b20b5d4b15..fbe9986c3ca 100644 --- a/code/modules/mob/living/silicon/silicon.dm +++ b/code/modules/mob/living/silicon/silicon.dm @@ -49,6 +49,7 @@ var/emagged = FALSE var/hack_software = FALSE //Will be able to use hacking actions interaction_range = 7 //wireless control range + var/control_disabled = FALSE // Set to 1 to stop AI from interacting via Click() var/obj/item/modular_computer/pda/silicon/modularInterface diff --git a/code/modules/uplink/uplink_items/nukeops.dm b/code/modules/uplink/uplink_items/nukeops.dm index 1be40c8501b..f5af968993d 100644 --- a/code/modules/uplink/uplink_items/nukeops.dm +++ b/code/modules/uplink/uplink_items/nukeops.dm @@ -597,6 +597,24 @@ cost = 2 purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS +/datum/uplink_item/suits/syndi_intellicard + name = "Pre-Loaded Syndicate Intellicard" + desc = "A syndicate intellicard that can be activated to download a captured Nanotrasen AI, modified with the standard syndicate lawset. You can slot it into your modsuit for a conversational partner! It can additionally control the MODsuit's modules at will, and move your body around even if you're in critical condition or dead. \ + However, due to failsafes activated during the extraction process, the AI is unable to interact with electronics from anywhere but direct proximity..." + item = /obj/item/aicard/syndie/loaded + cost = 12 + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS + refundable = TRUE + +/datum/uplink_item/suits/synd_ai_upgrade + name = "Syndicate AI Upgrade" + desc = "...unless you buy the Syndicate Upgrade! This data chip allows the captured AI to increase its interaction range by two tiles per application. The Syndicate recommends three or four purchases at most, for a total of seven or infinite meters of range." + item = /obj/item/computer_disk/syndie_ai_upgrade + cost = 4 + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS + cant_discount = TRUE + refundable = TRUE + // Devices /datum/uplink_item/device_tools/assault_pod diff --git a/icons/obj/aicards.dmi b/icons/obj/aicards.dmi index 86501e4b43b3f52f50a1237be229c8b663c1fcbd..2de7da5f6c88acad3581e0a88767c4ece8e030cf 100644 GIT binary patch delta 4396 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 delta 3930 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