Feature: bitrunner, a new supply role (READY) (#77259)

## About The Pull Request
[Design doc](https://hackmd.io/@shadowh4nd/r1P7atPjn) 

Adds a new supply role centered on short dungeon-esque runs with a focus
on unifying the job with the fun part. Some virtual domains are combat
related, some are silly, some focus on "objectives". Avatar health is
linked to your physical presence and retries are limited.


<details>
<summary>Photos, WIP</summary>

Net pod stasis

![netpod](https://github.com/tgstation/tgstation/assets/42397676/d99073a4-2438-4600-83c0-cafb7b3fee55)

Server loaded

![](https://cdn.discordapp.com/attachments/1131433451841662996/1135304774976278759/dreamseeker_N14j6dwLcK.gif)

Server cooldown

![](https://cdn.discordapp.com/attachments/1131433451841662996/1135304775945179217/dreamseeker_NkIwWLJZma.gif)

the quantum console UI
![Screenshot 2023-08-05
105742](https://github.com/tgstation/tgstation/assets/42397676/3a45feaf-8c80-46b2-81c9-355932d1b014)

Cyber police antag page
![Screenshot 2023-08-16
205303](https://github.com/tgstation/tgstation/assets/42397676/fc28bcdd-2a2a-487b-927c-904f224c1b89)

A safehouse

![image](https://github.com/tgstation/tgstation/assets/42397676/c22ac115-4e00-432a-aeeb-b6afdb1ef750)

Domain info page, every domain gets this (and sometimes help text)
![Screenshot 2023-08-04
141859](https://github.com/tgstation/tgstation/assets/42397676/ba9ed6d9-a318-4466-b246-d2da0fa05676)

Me getting steamrolled in one of the missions

![syndicant](https://github.com/tgstation/tgstation/assets/42397676/3eb3099a-f9b2-4431-854d-50d8cad9b7f0)

Ghost roles getting notified that server is kicking them out
![Screenshot 2023-08-04
135454](https://github.com/tgstation/tgstation/assets/42397676/edab916c-6255-4b83-b2e7-155df2f03aab)

Players enjoying the new combat missions
![Screenshot 2023-08-05
005727](https://github.com/tgstation/tgstation/assets/42397676/e302a66f-610d-4fe1-82e0-7c8aec3913ea)

Players exploring some of the virtual maps
![Screenshot 2023-08-06
220919](https://github.com/tgstation/tgstation/assets/42397676/75a92a0b-6aa1-4bac-8fd1-c9fb47c955da)

(Not part of the PR, but)
![Screenshot 2023-08-06
221527](https://github.com/tgstation/tgstation/assets/42397676/012475ec-bfa5-4fd3-9dcb-31d222bb7bef)

New bitrunner vendor

![dreamseeker_V62h2BGdEl](https://github.com/tgstation/tgstation/assets/42397676/37ca8f69-f75e-45c9-b324-e6b31696c7b7)

</details>

## Why It's Good For The Game
Content, firstly, and moreso being supply department content.

The framework that this implements is a great (preauthorized)
replacement for two systems which are collecting dust: BEPIS and the
gateway. They integrate into this system and it's a direct upgrade.

This adds a way for the players on station to generate materials (if
that remains). The nice part about it is that I can throw balance and
believability to the wind, as unlike its unrelated predecessor VR or
away missions, bitrunning entirely washes its hands of the map and mobs
each reboot.

It offers a very expandable map framework to add content and it's all
fairly well documented.

I'd like to add a feature that represents the idea that jobs don't have
to be mundane and "external" jobs can stay tied to the main gameplay
loop.

## Changelog
jlsnow301, kinneb, mmmiracles, ical92, spockye
🆑
add: Adds Bitrunning to supply department- a semi-offstation role that
rewards teamwork.
add: Adds new machines to complement the job- net pod, quantum server,
quantum consoles, and the nexacache vendor.
add: Adds several new maps which can be loaded and unloaded at will.
add: Some flair for the new bitrunning vendor.
add: Adds a new antagonist for the virtual domain only. Short lived
ghost role that fights bitrunners.
del: Removes the BEPIS machine, moves its tech into the Bitrunning
vendor.
/🆑

<!-- Both 🆑's are required for the changelog to work! You can put
your name to the right of the first 🆑 if you want to overwrite your
GitHub username as author ingame. -->
<!-- You can use multiple of the same prefix (they're only used for the
icon ingame) and delete the unneeded ones. Despite some of the tags,
changelogs should generally represent how a player might be affected by
the changes rather than a summary of the PR's contents. -->

---------

Co-authored-by: MMMiracles <lolaccount1@hotmail.com>
Co-authored-by: Ical <wolfsgamingtips@gmail.com>
Co-authored-by: spockye <79304582+spockye@users.noreply.github.com>
Co-authored-by: Watermelon914 <37270891+Watermelon914@users.noreply.github.com>
Co-authored-by: Zephyr <12817816+ZephyrTFA@users.noreply.github.com>
This commit is contained in:
Jeremiah
2023-09-23 02:14:12 -07:00
committed by GitHub
parent a53f614e67
commit a3849062b8
166 changed files with 52158 additions and 1540 deletions
@@ -0,0 +1,86 @@
#define CREDIT_TYPE_BITRUNNING "np"
/obj/machinery/computer/order_console/bitrunning
name = "bitrunning supplies order console"
desc = "NexaCache(tm)! Dubiously authentic gear for the digital daredevil."
icon = 'icons/obj/machines/bitrunning.dmi'
icon_state = "vendor"
icon_keyboard = null
icon_screen = null
circuit = /obj/item/circuitboard/computer/order_console/bitrunning
cooldown_time = 10 SECONDS
cargo_cost_multiplier = 0.65
express_cost_multiplier = 1
purchase_tooltip = @{"Your purchases will arrive at cargo,
and hopefully get delivered by them.
35% cheaper than express delivery."}
express_tooltip = @{"Sends your purchases instantly."}
credit_type = CREDIT_TYPE_BITRUNNING
order_categories = list(
CATEGORY_BITRUNNING_FLAIR,
CATEGORY_BITRUNNING_TECH,
CATEGORY_BEPIS,
)
blackbox_key = "bitrunning"
/obj/machinery/computer/order_console/bitrunning/subtract_points(final_cost, obj/item/card/id/card)
if(final_cost <= card.registered_account.bitrunning_points)
card.registered_account.bitrunning_points -= final_cost
return TRUE
return FALSE
/obj/machinery/computer/order_console/bitrunning/order_groceries(mob/living/purchaser, obj/item/card/id/card, list/groceries)
var/list/things_to_order = list()
for(var/datum/orderable_item/item as anything in groceries)
things_to_order[item.item_path] = groceries[item]
var/datum/supply_pack/bitrunning/pack = new(
purchaser = purchaser, \
cost = get_total_cost(), \
contains = things_to_order,
)
var/datum/supply_order/new_order = new(
pack = pack,
orderer = purchaser,
orderer_rank = "Bitrunning Vendor",
orderer_ckey = purchaser.ckey,
reason = "",
paying_account = card.registered_account,
department_destination = null,
coupon = null,
charge_on_purchase = FALSE,
manifest_can_fail = FALSE,
cost_type = credit_type,
can_be_cancelled = FALSE,
)
say("Thank you for your purchase! It will arrive on the next cargo shuttle!")
radio.talk_into(src, "A bitrunner has ordered equipment which will arrive on the cargo shuttle! Please make sure it gets to them as soon as possible!", radio_channel)
SSshuttle.shopping_list += new_order
/obj/machinery/computer/order_console/bitrunning/retrieve_points(obj/item/card/id/id_card)
return round(id_card.registered_account.bitrunning_points)
/obj/machinery/computer/order_console/bitrunning/ui_act(action, params)
. = ..()
if(!.)
flick("vendor_off", src)
/obj/machinery/computer/order_console/bitrunning/update_icon_state()
icon_state = "[initial(icon_state)][powered() ? null : "_off"]"
return ..()
/datum/supply_pack/bitrunning
name = "bitrunning order"
hidden = TRUE
crate_name = "bitrunning delivery crate"
access = list(ACCESS_BIT_DEN)
/datum/supply_pack/bitrunning/New(purchaser, cost, list/contains)
. = ..()
name = "[purchaser]'s Bitrunning Order"
src.cost = cost
src.contains = contains
#undef CREDIT_TYPE_BITRUNNING
@@ -0,0 +1,9 @@
/obj/item/clothing/glasses/sunglasses/oval
name = "oval sunglasses"
desc = "Vintage wrap around sunglasses. Provides a little protection."
icon_state = "jensenshades"
/obj/item/clothing/suit/jacket/trenchcoat
name = "trenchcoat"
desc = "A long, black trenchcoat. Makes you feel like you're the one, but you're not."
icon_state = "trenchcoat"
+146
View File
@@ -0,0 +1,146 @@
/**
* Bitrunning tech disks which let you load items or programs into the vdom on first avatar generation.
* For the record: Balance shouldn't be a primary concern.
* You can make the custom cheese spells you've always wanted.
* Just make it fun and engaging, it's PvE content.
*/
/obj/item/bitrunning_disk
name = "generic bitrunning program"
desc = "A disk containing source code."
icon = 'icons/obj/assemblies/module.dmi'
base_icon_state = "datadisk"
icon_state = "datadisk0"
/// Name of the choice made
var/choice_made
/obj/item/bitrunning_disk/Initialize(mapload)
. = ..()
icon_state = "[base_icon_state][rand(0, 7)]"
update_icon()
RegisterSignal(src, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined))
/obj/item/bitrunning_disk/proc/on_examined(datum/source, mob/examiner, list/examine_text)
SIGNAL_HANDLER
examine_text += span_infoplain("This disk must be carried on your person into a netpod to be used.")
if(isnull(choice_made))
examine_text += span_notice("To make a selection, toggle the disk in hand.")
return
examine_text += span_info("It has been used to select: <b>[choice_made]</b>.")
examine_text += span_notice("It cannot make another selection.")
/obj/item/bitrunning_disk/ability
desc = "A disk containing source code. It can be used to preload abilities into the virtual domain."
/// The selected ability that this grants
var/datum/action/granted_action
/// The list of actions that this can grant
var/list/datum/action/selectable_actions = list()
/obj/item/bitrunning_disk/ability/attack_self(mob/user, modifiers)
. = ..()
if(choice_made)
return
var/names = list()
for(var/datum/action/thing as anything in selectable_actions)
names += initial(thing.name)
var/choice = tgui_input_list(user, message = "Select an ability", title = "Bitrunning Program", items = names)
if(isnull(choice))
return
for(var/datum/action/thing as anything in selectable_actions)
if(initial(thing.name) == choice)
granted_action = thing
if(isnull(granted_action))
return
balloon_alert(user, "selected")
playsound(user, 'sound/items/click.ogg', 50, TRUE)
choice_made = choice
/// Tier 1 programs. Simple, funny, or helpful.
/obj/item/bitrunning_disk/ability/tier1
name = "bitrunning program: basic"
selectable_actions = list(
/datum/action/cooldown/spell/conjure/cheese,
/datum/action/cooldown/spell/basic_heal,
)
/// Tier 2 programs. More complex, powerful, or useful.
/obj/item/bitrunning_disk/ability/tier2
name = "bitrunning program: complex"
selectable_actions = list(
/datum/action/cooldown/spell/pointed/projectile/fireball,
/datum/action/cooldown/spell/pointed/projectile/lightningbolt,
/datum/action/cooldown/spell/forcewall,
)
/// Tier 3 abilities. Very powerful, game breaking.
/obj/item/bitrunning_disk/ability/tier3
name = "bitrunning program: elite"
selectable_actions = list(
/datum/action/cooldown/spell/shapeshift/dragon,
/datum/action/cooldown/spell/shapeshift/polar_bear,
)
/obj/item/bitrunning_disk/item
desc = "A disk containing source code. It can be used to preload items into the virtual domain."
/// The selected item that this grants
var/obj/granted_item
/// The list of actions that this can grant
var/list/obj/selectable_items = list()
/obj/item/bitrunning_disk/item/attack_self(mob/user, modifiers)
. = ..()
if(choice_made)
return
var/names = list()
for(var/obj/thing as anything in selectable_items)
names += initial(thing.name)
var/choice = tgui_input_list(user, message = "Select an ability", title = "Bitrunning Program", items = names)
if(isnull(choice))
return
for(var/obj/thing as anything in selectable_items)
if(initial(thing.name) == choice)
granted_item = thing
balloon_alert(user, "selected")
playsound(user, 'sound/items/click.ogg', 50, TRUE)
choice_made = choice
/// Tier 1 items. Simple, funny, or helpful.
/obj/item/bitrunning_disk/item/tier1
name = "bitrunning gear: simple"
selectable_items = list(
/obj/item/pizzabox/infinite,
/obj/item/gun/medbeam,
/obj/item/grenade/c4,
)
/// Tier 2 items. More complex, powerful, or useful.
/obj/item/bitrunning_disk/item/tier2
name = "bitrunning gear: complex"
selectable_items = list(
/obj/item/chainsaw,
/obj/item/gun/ballistic/automatic/pistol,
/obj/item/melee/energy/blade/hardlight,
)
/// Tier 3 items. Very powerful, game breaking.
/obj/item/bitrunning_disk/item/tier3
name = "bitrunning gear: advanced"
selectable_items = list(
/obj/item/gun/energy/tesla_cannon,
/obj/item/dualsaber/green,
/obj/item/melee/beesword,
)
@@ -0,0 +1,52 @@
/obj/structure/hololadder
name = "hololadder"
anchored = TRUE
desc = "An abstract representation of the means to disconnect from the virtual domain."
icon = 'icons/obj/structures.dmi'
icon_state = "ladder11"
obj_flags = BLOCK_Z_OUT_DOWN
/// Time req to disconnect properly
var/travel_time = 3 SECONDS
/obj/structure/hololadder/Initialize(mapload)
. = ..()
RegisterSignal(loc, COMSIG_ATOM_ENTERED, PROC_REF(on_enter))
/obj/structure/hololadder/attack_hand(mob/user, list/modifiers)
. = ..()
if(.)
return
if(!in_range(src, user) || DOING_INTERACTION(user, DOAFTER_SOURCE_CLIMBING_LADDER))
return
disconnect(user)
/// If there's a pilot ref- send the disconnect signal
/obj/structure/hololadder/proc/disconnect(mob/user)
if(isnull(user.mind))
return
var/datum/component/avatar_connection/connection = user.GetComponent(/datum/component/avatar_connection)
if(isnull(connection))
balloon_alert(user, "no connection detected.")
return
balloon_alert(user, "disconnecting...")
if(do_after(user, travel_time, src))
SEND_SIGNAL(user, COMSIG_BITRUNNER_SAFE_DISCONNECT)
/// Helper for times when you dont have hands (gondola??)
/obj/structure/hololadder/proc/on_enter(datum/source, atom/movable/arrived, turf/old_loc)
SIGNAL_HANDLER
if(!isliving(arrived))
return
var/mob/living/user = arrived
if(isnull(user.mind))
return
INVOKE_ASYNC(src, PROC_REF(disconnect), user)
@@ -0,0 +1,33 @@
/obj/item/bitrunning_host_monitor
name = "host monitor"
custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 2)
desc = "A complex medical device that, when attached to an avatar's data stream, can detect the user of their host's health."
flags_1 = CONDUCT_1
icon = 'icons/obj/device.dmi'
icon_state = "gps-b"
inhand_icon_state = "electronic"
item_flags = NOBLUDGEON
lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
slot_flags = ITEM_SLOT_BELT
throw_range = 7
throw_speed = 3
throwforce = 3
w_class = WEIGHT_CLASS_TINY
worn_icon_state = "electronic"
/obj/item/bitrunning_host_monitor/attack_self(mob/user, modifiers)
. = ..()
var/datum/component/avatar_connection/connection = user.GetComponent(/datum/component/avatar_connection)
if(isnull(connection))
balloon_alert(user, "data not recognized")
return
var/mob/living/pilot = connection.old_body_ref?.resolve()
if(isnull(pilot))
balloon_alert(user, "host not recognized")
return
to_chat(user, span_notice("Current host health: [pilot.health / pilot.maxHealth * 100]%"))
@@ -0,0 +1,70 @@
/obj/effect/landmark/bitrunning
name = "Generic bitrunning effect"
icon = 'icons/effects/bitrunning.dmi'
icon_state = "crate"
/// In case you want to gate the crate behind a special condition.
/obj/effect/landmark/bitrunning/loot_signal
name = "Mysterious aura"
/// The amount required to spawn a crate
var/points_goal = 10
/// A special condition limits this from spawning a crate
var/points_received = 0
/// Finished the special condition
var/revealed = FALSE
/obj/effect/landmark/bitrunning/loot_signal/Initialize(mapload)
. = ..()
RegisterSignal(src, COMSIG_BITRUNNER_GOAL_POINT, PROC_REF(on_add_point))
/// Listens for points to be added which will eventually spawn a crate.
/obj/effect/landmark/bitrunning/loot_signal/proc/on_add_point(datum/source, points_to_add)
SIGNAL_HANDLER
if(revealed)
return
points_received += points_to_add
if(points_received < points_goal)
return
reveal()
/// Spawns the crate with some effects
/obj/effect/landmark/bitrunning/loot_signal/proc/reveal()
playsound(src, 'sound/magic/blink.ogg', 50, TRUE)
var/turf/tile = get_turf(src)
var/obj/structure/closet/crate/secure/bitrunning/encrypted/loot = new(tile)
var/datum/effect_system/spark_spread/quantum/sparks = new(tile)
sparks.set_up(5, 1, get_turf(loot))
sparks.start()
qdel(src)
/// Where the crates get ported to station
/obj/effect/landmark/bitrunning/station_reward_spawn
name = "Bitrunning rewards spawn"
icon_state = "station"
/// Where the exit hololadder spawns
/obj/effect/landmark/bitrunning/hololadder_spawn
name = "Bitrunning hololadder spawn"
icon_state = "hololadder"
/// Where the crates need to be taken
/obj/effect/landmark/bitrunning/cache_goal_turf
name = "Bitrunning goal turf"
icon_state = "goal"
/// Where you want the crate to spawn
/obj/effect/landmark/bitrunning/cache_spawn
name = "Bitrunning crate spawn"
icon_state = "spawn"
/// Where the safehouse will spawn
/obj/effect/landmark/bitrunning/safehouse_spawn
name = "Bitrunning safehouse spawn"
icon_state = "safehouse"
@@ -0,0 +1,91 @@
#define ORE_MULTIPLIER_IRON 3
#define ORE_MULTIPLIER_GLASS 2
#define ORE_MULTIPLIER_PLASMA 1
#define ORE_MULTIPLIER_SILVER 0.7
#define ORE_MULTIPLIER_GOLD 0.6
#define ORE_MULTIPLIER_TITANIUM 0.5
#define ORE_MULTIPLIER_URANIUM 0.4
#define ORE_MULTIPLIER_DIAMOND 0.3
#define ORE_MULTIPLIER_BLUESPACE_CRYSTAL 0.2
/obj/structure/closet/crate/secure/bitrunning // Base class. Do not spawn this.
name = "base class cache"
desc = "Talk to a coder."
/// The virtual domain - side of the bitrunning crate. Deliver to the send location.
/obj/structure/closet/crate/secure/bitrunning/encrypted
name = "encrypted cache"
desc = "Needs decrypted at the safehouse to be opened."
locked = TRUE
/obj/structure/closet/crate/secure/bitrunning/encrypted/can_unlock(mob/living/user, obj/item/card/id/player_id, obj/item/card/id/registered_id)
return FALSE
/// The bitrunner den - side of the bitrunning crate. Appears in the receive location.
/obj/structure/closet/crate/secure/bitrunning/decrypted
name = "decrypted cache"
desc = "Compiled from the virtual domain. The reward of a successful bitrunner."
locked = FALSE
/obj/structure/closet/crate/secure/bitrunning/decrypted/Initialize(
mapload,
datum/lazy_template/virtual_domain/completed_domain,
rewards_multiplier = 1,
)
. = ..()
playsound(src, 'sound/magic/blink.ogg', 50, TRUE)
if(isnull(completed_domain))
return
PopulateContents(completed_domain.reward_points, completed_domain.extra_loot, rewards_multiplier)
/obj/structure/closet/crate/secure/bitrunning/decrypted/PopulateContents(reward_points, list/extra_loot, rewards_multiplier)
. = ..()
spawn_loot(extra_loot)
new /obj/item/stack/ore/iron(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_IRON))
new /obj/item/stack/ore/glass(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_GLASS))
if(reward_points > 1)
new /obj/item/stack/ore/silver(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_SILVER))
new /obj/item/stack/ore/titanium(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_TITANIUM))
if(reward_points > 2)
new /obj/item/stack/ore/plasma(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_PLASMA))
new /obj/item/stack/ore/gold(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_GOLD))
new /obj/item/stack/ore/uranium(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_URANIUM))
if(reward_points > 3)
new /obj/item/stack/ore/diamond(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_DIAMOND))
new /obj/item/stack/ore/bluespace_crystal(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_BLUESPACE_CRYSTAL))
/// Handles generating random numbers & calculating loot totals
/obj/structure/closet/crate/secure/bitrunning/decrypted/proc/calculate_loot(reward_points, rewards_multiplier, ore_multiplier)
var/base = 1 * (rewards_multiplier + reward_points)
var/random_sum = (rand() * 1.0 + 0.5) * base
return ROUND_UP(random_sum * ore_multiplier)
/// Handles spawning extra loot. This tries to handle bad flat and assoc lists
/obj/structure/closet/crate/secure/bitrunning/decrypted/proc/spawn_loot(list/extra_loot)
for(var/path in extra_loot)
if(!ispath(path))
continue
if(isnull(extra_loot[path]))
return FALSE
for(var/i in 1 to extra_loot[path])
new path(src)
return TRUE
#undef ORE_MULTIPLIER_IRON
#undef ORE_MULTIPLIER_GLASS
#undef ORE_MULTIPLIER_PLASMA
#undef ORE_MULTIPLIER_SILVER
#undef ORE_MULTIPLIER_GOLD
#undef ORE_MULTIPLIER_TITANIUM
#undef ORE_MULTIPLIER_URANIUM
#undef ORE_MULTIPLIER_DIAMOND
#undef ORE_MULTIPLIER_BLUESPACE_CRYSTAL
+472
View File
@@ -0,0 +1,472 @@
#define BASE_DISCONNECT_DAMAGE 40
/obj/machinery/netpod
name = "netpod"
base_icon_state = "netpod"
circuit = /obj/item/circuitboard/machine/netpod
desc = "A link to the netverse. It has an assortment of cables to connect yourself to a virtual domain."
icon = 'icons/obj/machines/bitrunning.dmi'
icon_state = "netpod"
max_integrity = 300
obj_flags = BLOCKS_CONSTRUCTION
state_open = TRUE
/// Whether we have an ongoing connection
var/connected = FALSE
/// A player selected outfit by clicking the netpod
var/datum/outfit/netsuit = /datum/outfit/job/bitrunner
/// Holds this to see if it needs to generate a new one
var/datum/weakref/avatar_ref
/// The linked quantum server
var/datum/weakref/server_ref
/// The amount of brain damage done from force disconnects
var/disconnect_damage
/// Static list of outfits to select from
var/list/cached_outfits = list()
/obj/machinery/netpod/Initialize(mapload)
. = ..()
return INITIALIZE_HINT_LATELOAD
/obj/machinery/netpod/LateInitialize()
. = ..()
disconnect_damage = BASE_DISCONNECT_DAMAGE
RegisterSignals(src, list(
COMSIG_QDELETING,
COMSIG_MACHINERY_BROKEN,
COMSIG_MACHINERY_POWER_LOST,
),
PROC_REF(on_broken),
)
RegisterSignal(src, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
RegisterSignal(src, COMSIG_ATOM_TAKE_DAMAGE, PROC_REF(on_take_damage))
register_context()
update_appearance()
/obj/machinery/netpod/Destroy()
. = ..()
cached_outfits.Cut()
/obj/machinery/netpod/add_context(atom/source, list/context, obj/item/held_item, mob/user)
. = ..()
if(isnull(held_item))
context[SCREENTIP_CONTEXT_LMB] = "Select Outfit"
return CONTEXTUAL_SCREENTIP_SET
if(istype(held_item, /obj/item/crowbar) && occupant)
context[SCREENTIP_CONTEXT_LMB] = "Pry Open"
return CONTEXTUAL_SCREENTIP_SET
return CONTEXTUAL_SCREENTIP_SET
/obj/machinery/netpod/update_icon_state()
if(!is_operational)
icon_state = base_icon_state
return ..()
if(state_open)
icon_state = base_icon_state + "_open_active"
return ..()
if(panel_open)
icon_state = base_icon_state + "_panel"
return ..()
icon_state = base_icon_state + "_closed"
if(occupant)
icon_state += "_active"
return ..()
/obj/machinery/netpod/MouseDrop_T(mob/target, mob/user)
var/mob/living/carbon/player = user
if(!iscarbon(player))
return
if((HAS_TRAIT(player, TRAIT_UI_BLOCKED) && !player.resting) || !Adjacent(player) || !player.Adjacent(target) || !ISADVANCEDTOOLUSER(player) || !is_operational)
return
close_machine(target)
/obj/machinery/netpod/crowbar_act(mob/living/user, obj/item/tool)
if(user.combat_mode)
attack_hand(user)
return TOOL_ACT_TOOLTYPE_SUCCESS
if(default_pry_open(tool, user) || default_deconstruction_crowbar(tool))
return TOOL_ACT_TOOLTYPE_SUCCESS
/obj/machinery/netpod/screwdriver_act(mob/living/user, obj/item/tool)
if(occupant)
balloon_alert(user, "in use!")
return TOOL_ACT_TOOLTYPE_SUCCESS
if(state_open)
balloon_alert(user, "close first.")
return TOOL_ACT_TOOLTYPE_SUCCESS
if(default_deconstruction_screwdriver(user, "[base_icon_state]_panel", "[base_icon_state]_closed", tool))
update_appearance() // sometimes icon doesnt properly update during flick()
ui_close(user)
return TOOL_ACT_TOOLTYPE_SUCCESS
/obj/machinery/netpod/attack_hand(mob/living/user, list/modifiers)
. = ..()
if(!state_open && user == occupant)
container_resist_act(user)
/obj/machinery/netpod/Exited(atom/movable/gone, direction)
. = ..()
if(!state_open && gone == occupant)
container_resist_act(gone)
/obj/machinery/netpod/Exited(atom/movable/gone, direction)
. = ..()
if(!state_open && gone == occupant)
container_resist_act(gone)
/obj/machinery/netpod/relaymove(mob/living/user, direction)
if(!state_open)
container_resist_act(user)
/obj/machinery/netpod/container_resist_act(mob/living/user)
user.visible_message(span_notice("[occupant] emerges from [src]!"),
span_notice("You climb out of [src]!"),
span_notice("With a hiss, you hear a machine opening."))
open_machine()
/obj/machinery/netpod/open_machine(drop = TRUE, density_to_set = FALSE)
unprotect_and_signal()
playsound(src, 'sound/machines/tramopen.ogg', 60, TRUE, frequency = 65000)
flick("[base_icon_state]_opening", src)
return ..()
/obj/machinery/netpod/close_machine(mob/user, density_to_set = TRUE)
if(!state_open || panel_open || !is_operational || !iscarbon(user))
return
playsound(src, 'sound/machines/tramclose.ogg', 60, TRUE, frequency = 65000)
flick("[base_icon_state]_closing", src)
..()
if(!iscarbon(occupant))
open_machine()
return
enter_matrix()
/obj/machinery/netpod/default_pry_open(obj/item/crowbar, mob/living/pryer)
if(isnull(occupant) || !iscarbon(occupant))
if(!state_open)
if(panel_open)
return FALSE
open_machine()
else
shut_pod()
return TRUE
pryer.visible_message(
span_danger("[pryer] starts prying open [src]!"),
span_notice("You start to pry open [src]."),
span_notice("You hear loud prying on metal.")
)
playsound(src, 'sound/machines/airlock_alien_prying.ogg', 100, TRUE)
SEND_SIGNAL(src, COMSIG_BITRUNNER_CROWBAR_ALERT, pryer)
if(do_after(pryer, 15 SECONDS, src))
if(!state_open)
open_machine()
return TRUE
/obj/machinery/netpod/ui_interact(mob/user, datum/tgui/ui)
if(!is_operational)
return
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "NetpodOutfits")
ui.set_autoupdate(FALSE)
ui.open()
/obj/machinery/netpod/ui_data()
var/list/data = list()
data["netsuit"] = netsuit
return data
/obj/machinery/netpod/ui_static_data()
var/list/data = list()
if(!length(cached_outfits))
cached_outfits += make_outfit_collection("Jobs", subtypesof(/datum/outfit/job))
data["collections"] = cached_outfits
return data
/obj/machinery/netpod/ui_act(action, params)
. = ..()
if(.)
return TRUE
switch(action)
if("select_outfit")
var/datum/outfit/new_suit = resolve_outfit(params["outfit"])
if(new_suit)
netsuit = new_suit
return TRUE
return FALSE
/// Disconnects the occupant after a certain time so they aren't just hibernating in netpod stasis. A balance change
/obj/machinery/netpod/proc/auto_disconnect()
if(isnull(occupant) || state_open || connected)
return
if(!iscarbon(occupant))
open_machine()
return
var/mob/living/carbon/player = occupant
player.playsound_local(src, 'sound/effects/splash.ogg', 60, TRUE)
to_chat(player, span_notice("The machine disconnects itself and begins to drain."))
open_machine()
/**
* ### Disconnect occupant
* If this goes smoothly, should reconnect a receiving mind to the occupant's body
*
* This is the second stage of the process - if you want to disconn avatars start at the mind first
*/
/obj/machinery/netpod/proc/disconnect_occupant(forced = FALSE)
var/mob/living/mob_occupant = occupant
if(isnull(occupant) || !isliving(occupant))
return
connected = FALSE
if(mob_occupant.stat == DEAD)
open_machine()
return
mob_occupant.playsound_local(src, "sound/magic/blink.ogg", 25, TRUE)
mob_occupant.set_static_vision(2 SECONDS)
mob_occupant.set_temp_blindness(1 SECONDS)
mob_occupant.Paralyze(2 SECONDS)
var/heal_time = 1
if(mob_occupant.health < mob_occupant.maxHealth)
heal_time = (mob_occupant.stat + 2) * 5
addtimer(CALLBACK(src, PROC_REF(auto_disconnect)), heal_time SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE|TIMER_DELETE_ME)
if(!forced)
return
mob_occupant.flash_act(override_blindness_check = TRUE, visual = TRUE)
mob_occupant.adjustOrganLoss(ORGAN_SLOT_BRAIN, disconnect_damage)
INVOKE_ASYNC(mob_occupant, TYPE_PROC_REF(/mob/living, emote), "scream")
to_chat(mob_occupant, span_danger("You've been forcefully disconnected from your avatar! Your thoughts feel scrambled!"))
/**
* ### Enter Matrix
* Finds any current avatars from this chair - or generates a new one
*
* New avatars cost 1 attempt, and this will eject if there's none left
*
* Connects the mind to the avatar if everything is ok
*/
/obj/machinery/netpod/proc/enter_matrix()
var/mob/living/carbon/human/neo = occupant
if(!ishuman(neo) || neo.stat == DEAD || isnull(neo.mind))
balloon_alert(neo, "invalid occupant.")
return
var/obj/machinery/quantum_server/server = find_server()
if(isnull(server))
balloon_alert(neo, "no server connected!")
return
var/datum/lazy_template/virtual_domain/generated_domain = server.generated_domain
if(isnull(generated_domain) || !server.is_ready)
balloon_alert(neo, "nothing loaded!")
return
var/mob/living/carbon/current_avatar = avatar_ref?.resolve()
var/obj/structure/hololadder/wayout
if(isnull(current_avatar) || current_avatar.stat != CONSCIOUS) // We need a viable avatar
wayout = server.generate_hololadder()
if(isnull(wayout))
balloon_alert(neo, "out of bandwidth!")
return
current_avatar = server.generate_avatar(wayout, netsuit)
avatar_ref = WEAKREF(current_avatar)
server.stock_gear(current_avatar, neo)
neo.set_static_vision(3 SECONDS)
protect_occupant(occupant)
if(!do_after(neo, 2 SECONDS, src))
return
// Very invalid
if(QDELETED(neo) || QDELETED(current_avatar) || QDELETED(src))
return
// Invalid
if(occupant != neo || isnull(neo.mind) || neo.stat == DEAD || current_avatar.stat == DEAD)
return
current_avatar.AddComponent( \
/datum/component/avatar_connection, \
old_mind = neo.mind, \
old_body = neo, \
server = server, \
pod = src, \
help_text = generated_domain.help_text, \
)
connected = TRUE
/// Finds a server and sets the server_ref
/obj/machinery/netpod/proc/find_server()
var/obj/machinery/quantum_server/server = server_ref?.resolve()
if(server)
return server
server = locate(/obj/machinery/quantum_server) in oview(4, src)
if(isnull(server))
return
server_ref = WEAKREF(server)
RegisterSignal(server, COMSIG_BITRUNNER_SERVER_UPGRADED, PROC_REF(on_server_upgraded), override = TRUE)
RegisterSignal(server, COMSIG_BITRUNNER_DOMAIN_COMPLETE, PROC_REF(on_domain_complete), override = TRUE)
return server
/// Creates a list of outfit entries for the UI.
/obj/machinery/netpod/proc/make_outfit_collection(identifier, list/outfit_list)
var/list/collection = list(
"name" = identifier,
"outfits" = list()
)
for(var/path as anything in outfit_list)
var/datum/outfit/outfit = path
var/outfit_name = initial(outfit.name)
if(findtext(outfit_name, "(") != 0 || findtext(outfit_name, "-") != 0) // No special variants please
continue
collection["outfits"] += list(list("path" = path, "name" = outfit_name))
return list(collection)
/// Machine has been broken - handles signals and reverting sprites
/obj/machinery/netpod/proc/on_broken(datum/source)
SIGNAL_HANDLER
if(!state_open)
open_machine()
if(occupant)
unprotect_and_signal()
/// Puts points on the current occupant's card account
/obj/machinery/netpod/proc/on_domain_complete(datum/source, atom/movable/crate, reward_points)
SIGNAL_HANDLER
if(isnull(occupant) || !connected || !iscarbon(occupant))
return
var/mob/living/carbon/player = occupant
var/datum/bank_account/account = player.get_bank_account()
if(isnull(account))
return
account.bitrunning_points += reward_points * 100
/obj/machinery/netpod/proc/on_examine(datum/source, mob/examiner, list/examine_text)
SIGNAL_HANDLER
if(isnull(occupant))
examine_text += span_infoplain("It is currently unoccupied.")
return
examine_text += span_infoplain("It is currently occupied by [occupant].")
examine_text += span_notice("It can be pried open with a crowbar, but its safety mechanisms will alert the occupant.")
/// On unbuckle or break, make sure the occupant ref is null
/obj/machinery/netpod/proc/unprotect_and_signal()
unprotect_occupant(occupant)
SEND_SIGNAL(src, COMSIG_BITRUNNER_SEVER_AVATAR, src)
/// When the server is upgraded, drops brain damage a little
/obj/machinery/netpod/proc/on_server_upgraded(datum/source, servo_rating)
SIGNAL_HANDLER
disconnect_damage = BASE_DISCONNECT_DAMAGE * (1 - servo_rating)
/// Checks the integrity, alerts occupants
/obj/machinery/netpod/proc/on_take_damage(datum/source, damage_amount)
SIGNAL_HANDLER
if(isnull(occupant))
return
var/total = max_integrity - damage_amount
var/integrity = (atom_integrity / total) * 100
if(integrity > 50)
return
SEND_SIGNAL(src, COMSIG_BITRUNNER_NETPOD_INTEGRITY)
/// Puts the occupant in netpod stasis, basically short-circuiting environmental conditions
/obj/machinery/netpod/proc/protect_occupant(mob/living/target)
if(target != occupant)
return
target.AddComponent(/datum/component/netpod_healing, \
brute_heal = 4, \
burn_heal = 4, \
toxin_heal = 4, \
clone_heal = 4, \
blood_heal = 4, \
)
target.playsound_local(src, 'sound/effects/submerge.ogg', 20, TRUE)
target.extinguish_mob()
update_use_power(ACTIVE_POWER_USE)
/// Removes the occupant from netpod stasis
/obj/machinery/netpod/proc/unprotect_occupant(mob/living/target)
var/datum/component/netpod_healing/healing_eff = target?.GetComponent(/datum/component/netpod_healing)
if(healing_eff)
qdel(healing_eff)
update_use_power(IDLE_POWER_USE)
/// Resolves a path to an outfit.
/obj/machinery/netpod/proc/resolve_outfit(text)
var/path = text2path(text)
if(ispath(path, /datum/outfit))
return path
/// Closes the machine without shoving in an occupant
/obj/machinery/netpod/proc/shut_pod()
state_open = FALSE
playsound(src, 'sound/machines/tramclose.ogg', 60, TRUE, frequency = 65000)
flick("[base_icon_state]_closing", src)
set_density(TRUE)
update_appearance()
#undef BASE_DISCONNECT_DAMAGE
@@ -0,0 +1,110 @@
/obj/machinery/computer/quantum_console
name = "quantum console"
circuit = /obj/item/circuitboard/computer/quantum_console
icon_keyboard = "mining"
icon_screen = "bitrunning"
req_access = list(ACCESS_MINING)
/// The server this console is connected to.
var/datum/weakref/server_ref
/obj/machinery/computer/quantum_console/Initialize(mapload, obj/item/circuitboard/circuit)
. = ..()
desc = "Even in the distant year [CURRENT_STATION_YEAR], Nanostrasen is still using REST APIs. How grim."
return INITIALIZE_HINT_LATELOAD
/obj/machinery/computer/quantum_console/LateInitialize()
. = ..()
if(isnull(server_ref))
find_server()
/obj/machinery/computer/quantum_console/ui_interact(mob/user, datum/tgui/ui)
. = ..()
if(!is_operational)
return
if(isnull(server_ref))
find_server()
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "QuantumConsole")
ui.open()
/obj/machinery/computer/quantum_console/ui_data()
var/list/data = list()
var/obj/machinery/quantum_server/server = find_server()
if(isnull(server))
data["connected"] = FALSE
return data
data["connected"] = TRUE
data["generated_domain"] = server.generated_domain?.key
data["occupants"] = length(server.avatar_connection_refs)
data["points"] = server.points
data["randomized"] = server.domain_randomized
data["ready"] = server.is_ready && server.is_operational
data["scanner_tier"] = server.scanner_tier
data["retries_left"] = length(server.exit_turfs) - server.retries_spent
return data
/obj/machinery/computer/quantum_console/ui_static_data(mob/user)
var/list/data = list()
var/obj/machinery/quantum_server/server = find_server()
if(isnull(server))
return data
data["available_domains"] = server.get_available_domains()
data["avatars"] = server.get_avatar_data()
return data
/obj/machinery/computer/quantum_console/ui_act(action, list/params, datum/tgui/ui)
. = ..()
if(.)
return TRUE
var/obj/machinery/quantum_server/server = find_server()
if(isnull(server))
return FALSE
switch(action)
if("random_domain")
var/map_id = server.get_random_domain_id()
if(!map_id)
return TRUE
server.cold_boot_map(usr, map_id)
return TRUE
if("refresh")
ui.send_full_update()
return TRUE
if("set_domain")
server.cold_boot_map(usr, params["id"])
return TRUE
if("stop_domain")
server.begin_shutdown(usr)
return TRUE
return FALSE
/// Attempts to find a quantum server.
/obj/machinery/computer/quantum_console/proc/find_server()
var/obj/machinery/quantum_server/server = server_ref?.resolve()
if(server)
return server
for(var/direction in GLOB.cardinals)
var/obj/machinery/quantum_server/nearby_server = locate(/obj/machinery/quantum_server, get_step(src, direction))
if(nearby_server)
server_ref = WEAKREF(nearby_server)
nearby_server.console_ref = WEAKREF(src)
return nearby_server
return