From 3503e9c5655cc7f89a59dcc5ddda8b7db6020fe9 Mon Sep 17 00:00:00 2001
From: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com>
Date: Mon, 13 Apr 2026 14:07:42 -0400
Subject: [PATCH] Re-Implements the Bounty List for cargo, and various bounty
improvements. (#95412)
## About The Pull Request
This PR readds a previously removed feature from cargo, that being a
global list of bounties that the whole station has access to, with some
tweaks and adjustments from the original system.
_Seen above: The new Global Bounty Interface._
The civilian bounty pad has a second tab added to it, which contains a
list of all the bounties that are available globally for all crew. You
can select one in order to view the contents of the bounty, as well as
have the option to send items on the pad to contribute to that global
bounty. You may also, as you could several years ago, print a sheet of
paper from the console that consists of all the global bounties on the
station, their their reward values. In addition to those bounties, some
bounties may be labeled has "High Priority", which means that they're
worth more to complete for both you and for cargo, and are denoted with
the different coloring and the star icon on the bounty list.
When a bounty is completed, it will create a bounty cube, the same as
personal bounties. However, for global bounties, multiple people may
contribute to their completion, and when the bounty cube is made and
sold, you will each get a cut of the profits proportional to your
individual contribution. So, if a bounty cube is sold worth 1000
credits, and Person A and Person B contribute 4 items and 6 items
respectively, the total "cut" of the profits crew would receive is 30%
of the total value, so 300 credits. Of those 300 credits, Person A would
receive 120 credits and 180 credits.
(In a real round, the value of the cube's 1000 credits may also increase
if the speedy delivery bonus is met by the cargo department.)
In addition, when a bounty goes from on the list to completed and cubed
up, the global bounty list will likewise automatically update, and
create a new bounty. The number of bounties available for the crew to
complete starts at 5 (Scaled to add one more for every 8 living players,
up to a maximum of 10 to begin with), and for every _3_ completed
bounties, that bounty maximum will go up by an additional 1 bounty. The
new bounty is random from all possible job categories. This may need to
be tweaked so that it pulls 1 bounty from each major department
category, but for now this is how it stands.
Most of the changes in this project fall under tweaks to the pricetag
component, UI work, and then a lot of file-cleanup and tweaking based on
testing.
## Why It's Good For The Game
There is something that was lost in the original shift from global
bounties to personal bounties. Cargo would reach out to crew in order to
get their assistance on projects that let them do their jobs better,
made forced them to have to work with the rest of the crew as opposed to
merely beating them to death when they break in to print their round
start multitools. One of the reasons that we justified that choice was
that bounties were very lopsided. You could be a a superstar helper and
complete a dozen bounties for cargo in a shift, but not only would you
run out of bounties to complete, but you would also receive nothing in
return. Switching to personal bounties did in fact let players generate
their own wealth, but now suddenly the impetus to complete bounties was
that cargo was more of a hang-up to the process than it actually being
part of their job responsibilities.
By re-implementing global bounties, we're giving crew the chance to not
only have that kind of soft community goal, but also we're incentivizing
people to contribute as much as they can to it's completion without
really requiring them to make it their whole shift as tends to be the
case with personal bounties.
There is more that could be done to this system, including using a
unique bounty pool, or at least unique "big" bounties that could be
rolled into the pool that would allow the crew to work on more varied
stuff, but for now this is at least a servable way to re-implement this
from both a longevity and nostalgia standpoint.
I also really, really like printing out sheets of paper and pinning them
to doors. Big mistake on my part by losing that.
---
code/__DEFINES/cargo.dm | 3 +
code/__DEFINES/economy.dm | 2 +
code/datums/components/pricetag.dm | 30 +-
code/game/machinery/civilian_bounties.dm | 526 ------------------
.../machinery/civilian_bounty/bounty_cube.dm | 144 +++++
.../civilian_bounty/bounty_machinery.dm | 444 +++++++++++++++
.../civilian_bounty/civilian_bounty_trims.dm | 92 +++
.../pirate/pirate_shuttle_equipment.dm | 31 +-
code/modules/cargo/bounties/atmos.dm | 5 +
code/modules/cargo/bounties/item.dm | 30 +-
code/modules/cargo/bounties/mecha.dm | 2 +-
code/modules/cargo/bounties/reagent.dm | 10 +
code/modules/cargo/bounties/security.dm | 1 +
code/modules/cargo/bounties/special.dm | 18 +-
code/modules/cargo/bounty.dm | 37 +-
code/modules/cargo/universal_scanner.dm | 7 +-
code/modules/recycling/sortingmachinery.dm | 4 +-
sound/attributions.txt | 3 +
sound/machines/data_transmission.ogg | Bin 0 -> 343290 bytes
tgstation.dme | 4 +-
.../tgui/interfaces/CivCargoHoldTerminal.jsx | 163 ------
.../tgui/interfaces/CivCargoHoldTerminal.tsx | 391 +++++++++++++
.../interfaces/CivCargoHoldTerminal.scss | 23 +
tgui/packages/tgui/styles/main.scss | 1 +
24 files changed, 1243 insertions(+), 728 deletions(-)
delete mode 100644 code/game/machinery/civilian_bounties.dm
create mode 100644 code/game/machinery/civilian_bounty/bounty_cube.dm
create mode 100644 code/game/machinery/civilian_bounty/bounty_machinery.dm
create mode 100644 code/game/machinery/civilian_bounty/civilian_bounty_trims.dm
create mode 100644 sound/machines/data_transmission.ogg
delete mode 100644 tgui/packages/tgui/interfaces/CivCargoHoldTerminal.jsx
create mode 100644 tgui/packages/tgui/interfaces/CivCargoHoldTerminal.tsx
create mode 100644 tgui/packages/tgui/styles/interfaces/CivCargoHoldTerminal.scss
diff --git a/code/__DEFINES/cargo.dm b/code/__DEFINES/cargo.dm
index f81b5d08b94..c293a1724e3 100644
--- a/code/__DEFINES/cargo.dm
+++ b/code/__DEFINES/cargo.dm
@@ -92,3 +92,6 @@
#define DEPARTMENTAL_ORDER_REWARD_COEFFICIENT 120
/// Used for the power of the logarithmic value for the free crate delivery reward
#define DEPARTMENTAL_ORDER_REWARD_EXPONENT 1.5
+
+/// How many global bounties are spawned at minimum when refreshing the list?
+#define CIV_BOUNTY_BASELINE 5
diff --git a/code/__DEFINES/economy.dm b/code/__DEFINES/economy.dm
index 6c80e95d555..8977af0b1cc 100644
--- a/code/__DEFINES/economy.dm
+++ b/code/__DEFINES/economy.dm
@@ -83,6 +83,8 @@
#define MARKET_TREND_STABLE 0
#define MARKET_EVENT_PROBABILITY 8 //Probability of a market event firing, in percent. Fires once per material, every stock market tick.
+/// How much of the total value of a bounty cube does the player receive when the cube is exported?
+#define BOUNTY_CUT_STANDARD 0.3
// Fair warning that these defines at present are not used in all tgui, static descriptions, or any varible names or comments
/// The symbol for the default type of money used in the code.
diff --git a/code/datums/components/pricetag.dm b/code/datums/components/pricetag.dm
index d8636952921..c6c43ec3fdf 100644
--- a/code/datums/components/pricetag.dm
+++ b/code/datums/components/pricetag.dm
@@ -11,15 +11,29 @@
/// List of bank accounts this pricetag pays out to. Format is payees[bank_account] = profit_ratio.
var/list/payees = list()
-/datum/component/pricetag/Initialize(pay_to_account, profit_ratio = 1, delete_on_unwrap = TRUE)
+/datum/component/pricetag/Initialize(list/pay_to_account, profit_ratio = 1, delete_on_unwrap = TRUE)
if(!isobj(parent)) //Has to account for both objects and sellable structures like crates.
return COMPONENT_INCOMPATIBLE
if(isnull(pay_to_account))
- stack_trace("[type] component was added to something without a pay_to_account!")
+ stack_trace("[type] component was added to something without a pay_to_account list!")
return COMPONENT_INCOMPATIBLE
- payees[pay_to_account] = profit_ratio
+ if(!length(pay_to_account))
+ stack_trace("[type] component was created with an empty pay_to_account list!")
+ return COMPONENT_INCOMPATIBLE
+
+ var/total_contribution = 0
+ for(var/totaler in pay_to_account)
+ if(!pay_to_account[totaler])
+ pay_to_account[totaler] = 1 //In case we just want a list with one item to prevent things from getting fucked up.
+
+ total_contribution += pay_to_account[totaler]
+
+ for(var/individual in pay_to_account)
+ var/individual_cut = profit_ratio * (pay_to_account[individual]/ total_contribution)
+ payees[individual] = individual_cut
+
src.delete_on_unwrap = delete_on_unwrap
/datum/component/pricetag/RegisterWithParent()
@@ -47,10 +61,11 @@
* (Don't go from non-deleting to deleting)
*/
/datum/component/pricetag/InheritComponent(datum/component/pricetag/new_comp, i_am_original, pay_to_account, profit_ratio = 1, delete_on_unwrap = TRUE)
- if(!isnull(payees[pay_to_account]) && payees[pay_to_account] >= profit_ratio) // They're already getting a better ratio, don't scam them
- return
+ if(length(payees) == 1)
+ if(!isnull(payees[pay_to_account]) && payees[pay_to_account] >= profit_ratio) // They're already getting a better ratio, don't scam them
+ return
- payees[pay_to_account] = profit_ratio
+ payees[pay_to_account] = profit_ratio
if(!delete_on_unwrap)
src.delete_on_unwrap = delete_on_unwrap
@@ -83,6 +98,7 @@
// Gotta see how much money we've lost by the end of things.
var/overall_item_price = item_value
+ var/running_tally = 0
for(var/datum/bank_account/payee as anything in payees)
// Every payee with a ratio gets a cut based on the item's total value
@@ -92,7 +108,7 @@
payee.adjust_money(payee_cut, "Pricetag: [capitalize(format_text(source.name))] Sale")
payee.bank_card_talk("Sale of [source] recorded. [payee_cut] [MONEY_NAME] added to account.")
-
+ running_tally += payee_cut
// Update the report with the modified final price
report.total_value[export] += overall_item_price
report.total_amount[export] += export.get_amount(source) * export.amount_report_multiplier
diff --git a/code/game/machinery/civilian_bounties.dm b/code/game/machinery/civilian_bounties.dm
deleted file mode 100644
index f90b1b579b1..00000000000
--- a/code/game/machinery/civilian_bounties.dm
+++ /dev/null
@@ -1,526 +0,0 @@
-///Percentage of a civilian bounty the civilian will make.
-#define CIV_BOUNTY_SPLIT 30
-
-///Pad for the Civilian Bounty Control.
-/obj/machinery/piratepad/civilian
- name = "civilian bounty pad"
- desc = "A machine designed to send civilian bounty targets to centcom."
- layer = TABLE_LAYER
- resistance_flags = FIRE_PROOF
- circuit = /obj/item/circuitboard/machine/bountypad
- var/cooldown_reduction = 0
-
-/obj/machinery/piratepad/civilian/screwdriver_act(mob/living/user, obj/item/tool)
- . = ..()
- if(!.)
- return default_deconstruction_screwdriver(user, "lpad-idle-open", "lpad-idle-off", tool)
-
-/obj/machinery/piratepad/civilian/crowbar_act(mob/living/user, obj/item/tool)
- . = ..()
- if(!.)
- return default_deconstruction_crowbar(tool)
-
-/obj/machinery/piratepad/civilian/RefreshParts()
- . = ..()
- var/T = -2
- for(var/datum/stock_part/micro_laser/micro_laser in component_parts)
- T += micro_laser.tier
-
- for(var/datum/stock_part/scanning_module/scanning_module in component_parts)
- T += scanning_module.tier
-
- cooldown_reduction = T * (30 SECONDS)
-
-/obj/machinery/piratepad/civilian/proc/get_cooldown_reduction()
- return cooldown_reduction
-
-///Computer for assigning new civilian bounties, and sending bounties for collection.
-/obj/machinery/computer/piratepad_control/civilian
- name = "civilian bounty control terminal"
- desc = "A console for assigning civilian bounties to inserted ID cards, and for controlling the bounty pad for export."
- status_report = "Ready for delivery."
- icon_screen = "civ_bounty"
- icon_keyboard = "id_key"
- warmup_time = 3 SECONDS
- circuit = /obj/item/circuitboard/computer/bountypad
- interface_type = "CivCargoHoldTerminal"
- ///Typecast of an inserted, scanned ID card inside the console, as bounties are held within the ID card.
- var/obj/item/card/id/inserted_scan_id
-
-/obj/machinery/computer/piratepad_control/civilian/attackby(obj/item/I, mob/living/user, list/modifiers, list/attack_modifiers)
- if(isidcard(I))
- if(id_insert(user, I, inserted_scan_id))
- inserted_scan_id = I
- return TRUE
- return ..()
-
-/obj/machinery/computer/piratepad_control/multitool_act(mob/living/user, obj/item/multitool/I)
- if(istype(I) && istype(I.buffer,/obj/machinery/piratepad/civilian))
- to_chat(user, span_notice("You link [src] with [I.buffer] in [I] buffer."))
- pad_ref = WEAKREF(I.buffer)
- return TRUE
-
-/obj/machinery/computer/piratepad_control/civilian/post_machine_initialize()
- . = ..()
- if(cargo_hold_id)
- for(var/obj/machinery/piratepad/civilian/C as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/piratepad/civilian))
- if(C.cargo_hold_id == cargo_hold_id)
- pad_ref = WEAKREF(C)
- return
- else
- var/obj/machinery/piratepad/civilian/pad = locate() in range(4,src)
- pad_ref = WEAKREF(pad)
-
-/obj/machinery/computer/piratepad_control/civilian/recalc()
- if(sending)
- return FALSE
- if(!inserted_scan_id)
- status_report = "Please insert your ID first."
- playsound(loc, 'sound/machines/synth/synth_no.ogg', 30 , TRUE)
- return FALSE
- if(!inserted_scan_id.registered_account.civilian_bounty)
- status_report = "Please accept a new civilian bounty first."
- playsound(loc, 'sound/machines/synth/synth_no.ogg', 30 , TRUE)
- return FALSE
- status_report = "Civilian Bounty: "
- var/obj/machinery/piratepad/civilian/pad = pad_ref?.resolve()
- for(var/atom/movable/possible_shippable in get_turf(pad))
- if(possible_shippable == pad)
- continue
- if(possible_shippable.flags_1 & HOLOGRAM_1)
- continue
- if(isitem(possible_shippable))
- var/obj/item/possible_shippable_item = possible_shippable
- if(possible_shippable_item.item_flags & ABSTRACT)
- continue
- if(inserted_scan_id.registered_account.civilian_bounty.applies_to(possible_shippable))
- status_report += "Target Applicable."
- playsound(loc, 'sound/machines/synth/synth_yes.ogg', 30 , TRUE)
- return
- status_report += "Not Applicable."
- playsound(loc, 'sound/machines/synth/synth_no.ogg', 30 , TRUE)
-
-/**
- * This fully rewrites base behavior in order to only check for bounty objects, and no other types of objects like pirate-pads do.
- */
-/obj/machinery/computer/piratepad_control/civilian/send()
- playsound(loc, 'sound/machines/wewewew.ogg', 70, TRUE)
- if(!sending)
- return
- var/datum/bank_account/id_account = inserted_scan_id?.registered_account
- var/datum/bounty/current_bounty = id_account?.civilian_bounty
- if(!current_bounty)
- stop_sending()
- return FALSE
- var/active_stack = 0
- var/obj/machinery/piratepad/civilian/pad = pad_ref?.resolve()
- for(var/atom/movable/possible_shippable in get_turf(pad))
- if(possible_shippable == pad)
- continue
- if(possible_shippable.flags_1 & HOLOGRAM_1)
- continue
- if(isitem(possible_shippable))
- var/obj/item/possible_shippable_item = possible_shippable
- if(possible_shippable_item.item_flags & ABSTRACT)
- continue
- if(current_bounty.applies_to(possible_shippable))
- active_stack ++
- current_bounty.ship(possible_shippable)
- qdel(possible_shippable)
- if(active_stack >= 1)
- status_report += "Bounty Target Found x[active_stack]. "
- else
- status_report = "No applicable targets found. Aborting."
- stop_sending()
- if(current_bounty.can_claim())
- //Pay for the bounty with the ID's department funds.
- status_report += " Bounty completed! Please give your bounty cube to cargo for your automated payout shortly."
- SSblackbox.record_feedback("tally", "bounties_completed", 1, current_bounty.type)
- current_bounty.on_claimed(inserted_scan_id)
- id_account.reset_bounty(inserted_scan_id)
- SSeconomy.civ_bounty_tracker++
-
- var/obj/item/bounty_cube/reward = new /obj/item/bounty_cube(drop_location())
- reward.set_up(current_bounty, inserted_scan_id)
-
- pad.visible_message(span_notice("[pad] activates!"))
- flick(pad.sending_state,pad)
- pad.icon_state = pad.idle_state
- playsound(loc, 'sound/machines/synth/synth_yes.ogg', 30 , TRUE)
- sending = FALSE
-
-///Here is where cargo bounties are added to the player's bank accounts, then adjusted and scaled into a civilian bounty.
-/obj/machinery/computer/piratepad_control/civilian/proc/add_bounties(mob/user, cooldown_reduction = 0)
- var/datum/bank_account/id_account = inserted_scan_id?.registered_account
- if(!id_account)
- return
- if((id_account.civilian_bounty || id_account.bounties) && !COOLDOWN_FINISHED(id_account, bounty_timer))
- var/time_left = DisplayTimeText(COOLDOWN_TIMELEFT(id_account, bounty_timer), round_seconds_to = 1)
- balloon_alert(user, "try again in [time_left]!")
- return FALSE
- if(!inserted_scan_id.trim)
- say("Requesting ID card has no job assignment registered!")
- return FALSE
-
- var/list/datum/bounty/crumbs = inserted_scan_id.trim.generate_bounty_list()
- COOLDOWN_START(id_account, bounty_timer, (5 MINUTES) - cooldown_reduction)
- id_account.bounties = crumbs
-
-/**
- * Generates a list of bounties for use with the civilian bounty pad.
- *
- * @param bounty_types the define taken from a job for selection of a random_bounty() proc.
- * @param bounty_rolls the number of bounties to be selected from.
- * @param assistant_failsafe Do we guarentee one assistant bounty per generated list? Used for non-assistant jobs to give an easier alternative to that job's default bounties.
- */
-/datum/id_trim/proc/generate_bounty_list(bounty_rolls = 3, assistant_failsafe = TRUE)
- var/datum/job/our_job = find_job()
- var/bounty_type = our_job?.bounty_types || CIV_JOB_RANDOM
-
- var/list/rolling_list = list()
- if(assistant_failsafe)
- var/random_assistant = get_random_bounty_type(CIV_JOB_BASIC)
- var/datum/bounty/assistant_bounty = new random_assistant()
- if(assistant_bounty.can_get())
- rolling_list += assistant_bounty
- else
- qdel(assistant_bounty)
-
- var/attempts = 20
- while(length(rolling_list) < bounty_rolls && attempts > 0)
- var/random_job = get_random_bounty_type(attempts <= 5 ? CIV_JOB_BASIC : bounty_type)
- var/datum/bounty/job_bounty = new random_job()
- attempts -= 1
- if(!job_bounty.can_get() || has_duplicate_bounty(rolling_list, job_bounty))
- qdel(job_bounty)
- continue
-
- rolling_list += job_bounty
-
- return rolling_list
-
-/// Helper to see if there's a duplicate bounty in a list of bounties
-/datum/id_trim/proc/has_duplicate_bounty(list/datum/bounty/bounty_list, datum/bounty/check_bounty)
- PRIVATE_PROC(TRUE)
-
- for(var/datum/bounty/existing as anything in bounty_list)
- if(existing.type != check_bounty.type)
- continue
- if(existing.allow_duplicate && check_bounty.allow_duplicate)
- continue
- return TRUE
-
- return FALSE
-
-/// Returns a /datum/bounty typepath for a given bounty type
-/datum/id_trim/proc/get_random_bounty_type(input_bounty_type)
- if(!input_bounty_type || input_bounty_type == CIV_JOB_RANDOM)
- input_bounty_type = rand(1, MAXIMUM_BOUNTY_JOBS)
-
- switch(input_bounty_type)
- if(CIV_JOB_BASIC)
- return pick(subtypesof(/datum/bounty/item/assistant))
- if(CIV_JOB_ROBO)
- return pick(subtypesof(/datum/bounty/item/mech))
- if(CIV_JOB_CHEF)
- return pick(subtypesof(/datum/bounty/item/chef) + subtypesof(/datum/bounty/reagent/chef))
- if(CIV_JOB_SEC)
- if(prob(75))
- return /datum/bounty/patrol
- return /datum/bounty/item/contraband
- if(CIV_JOB_DRINK)
- if(prob(50))
- return /datum/bounty/reagent/simple_drink
- return /datum/bounty/reagent/complex_drink
- if(CIV_JOB_CHEM)
- if(prob(50))
- return /datum/bounty/reagent/chemical_simple
- return/datum/bounty/reagent/chemical_complex
- if(CIV_JOB_VIRO)
- return pick(subtypesof(/datum/bounty/virus))
- if(CIV_JOB_SCI)
- if(prob(50))
- return pick(subtypesof(/datum/bounty/item/science))
- return pick(subtypesof(/datum/bounty/item/slime))
- if(CIV_JOB_ENG)
- return pick(subtypesof(/datum/bounty/item/engineering))
- if(CIV_JOB_MINE)
- return pick(subtypesof(/datum/bounty/item/mining))
- if(CIV_JOB_MED)
- return pick(subtypesof(/datum/bounty/item/medical))
- if(CIV_JOB_GROW)
- return pick(subtypesof(/datum/bounty/item/botany))
- if(CIV_JOB_ATMOS)
- return pick(subtypesof(/datum/bounty/item/atmospherics))
- if(CIV_JOB_BITRUN)
- return pick(subtypesof(/datum/bounty/item/bitrunning))
-
- stack_trace("Failed to get random bounty type for input type [input_bounty_type]")
- return null
-
-/**
- * Proc that assigned a civilian bounty to an ID card, from the list of potential bounties that that bank account currently has available.
- * Available choices are assigned during add_bounties, and one is locked in here.
- *
- * @param choice The index of the bounty in the list of bounties that the player can choose from.
- */
-/obj/machinery/computer/piratepad_control/civilian/proc/pick_bounty(datum/bounty/choice)
- var/datum/bank_account/id_account = inserted_scan_id?.registered_account
- if(!id_account?.bounties?[choice])
- playsound(loc, 'sound/machines/synth/synth_no.ogg', 40 , TRUE)
- return
- id_account.set_bounty(id_account.bounties[choice], inserted_scan_id)
- id_account.bounties = null
- SSblackbox.record_feedback("tally", "bounties_assigned", 1, id_account.civilian_bounty.type)
- return id_account.civilian_bounty
-
-/obj/machinery/computer/piratepad_control/civilian/click_alt(mob/user)
- id_eject(user, inserted_scan_id)
- return CLICK_ACTION_SUCCESS
-
-/obj/machinery/computer/piratepad_control/civilian/ui_data(mob/user)
- var/list/data = list()
- data["points"] = points
- data["pad"] = pad_ref?.resolve() ? TRUE : FALSE
- data["sending"] = sending
- data["status_report"] = status_report
- data["id_inserted"] = inserted_scan_id
- if(inserted_scan_id?.registered_account)
- if(inserted_scan_id.registered_account.civilian_bounty)
- data["id_bounty_info"] = inserted_scan_id.registered_account.civilian_bounty.description
- data["id_bounty_num"] = inserted_scan_id.registered_account.bounty_num()
- data["id_bounty_value"] = (inserted_scan_id.registered_account.civilian_bounty.get_bounty_reward()) * (CIV_BOUNTY_SPLIT / 100)
- if(inserted_scan_id.registered_account.bounties)
- data["picking"] = TRUE
- data["id_bounty_names"] = list()
- data["id_bounty_infos"] = list()
- data["id_bounty_values"] = list()
- for(var/datum/bounty/bounty as anything in inserted_scan_id.registered_account.bounties)
- data["id_bounty_names"] += bounty.name
- data["id_bounty_infos"] += bounty.description
- data["id_bounty_values"] += bounty.get_bounty_reward() * (CIV_BOUNTY_SPLIT / 100)
-
- else
- data["picking"] = FALSE
-
- return data
-
-/obj/machinery/computer/piratepad_control/civilian/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
- . = ..()
- if(.)
- return
- var/obj/machinery/piratepad/civilian/pad = pad_ref?.resolve()
- if(!pad)
- return
- var/mob/user = ui.user
- if(!user.can_perform_action(src) || (machine_stat & (NOPOWER|BROKEN)))
- return
- switch(action)
- if("recalc")
- recalc()
- if("send")
- start_sending()
- if("stop")
- stop_sending()
- if("pick")
- pick_bounty(params["value"])
- if("bounty")
- add_bounties(user, pad.get_cooldown_reduction())
- if("eject")
- id_eject(user, inserted_scan_id)
- inserted_scan_id = null
- . = TRUE
-
-///Self explanitory, holds the ID card in the console for bounty payout and manipulation.
-/obj/machinery/computer/piratepad_control/civilian/proc/id_insert(mob/user, obj/item/inserting_item, obj/item/target)
- var/obj/item/card/id/card_to_insert = inserting_item
- var/holder_item = FALSE
-
- if(!isidcard(card_to_insert))
- card_to_insert = inserting_item.remove_id()
- holder_item = TRUE
-
- if(!card_to_insert || !user.transferItemToLoc(card_to_insert, src))
- return FALSE
-
- if(target)
- if(holder_item && inserting_item.insert_id(target))
- playsound(src, 'sound/machines/terminal/terminal_insert_disc.ogg', 50, FALSE)
- else
- id_eject(user, target)
-
- user.visible_message(span_notice("[user] inserts \the [card_to_insert] into \the [src]."),
- span_notice("You insert \the [card_to_insert] into \the [src]."))
- playsound(src, 'sound/machines/terminal/terminal_insert_disc.ogg', 50, FALSE)
- ui_interact(user)
- return TRUE
-
-///Removes A stored ID card.
-/obj/machinery/computer/piratepad_control/civilian/proc/id_eject(mob/user, obj/item/target)
- if(!target)
- to_chat(user, span_warning("That slot is empty!"))
- return FALSE
- else
- try_put_in_hand(target, user)
- user.visible_message(span_notice("[user] gets \the [target] from \the [src]."), \
- span_notice("You get \the [target] from \the [src]."))
- playsound(src, 'sound/machines/terminal/terminal_insert_disc.ogg', 50, FALSE)
- inserted_scan_id = null
- return TRUE
-
-///Upon completion of a civilian bounty, one of these is created. It is sold to cargo to give the cargo budget bounty money, and the person who completed it cash.
-/obj/item/bounty_cube
- name = "bounty cube"
- desc = "A bundle of compressed hardlight data, containing a completed bounty. Sell this on the cargo shuttle to claim it!"
- icon = 'icons/obj/economy.dmi'
- icon_state = "bounty_cube"
- ///Value of the bounty that this bounty cube sells for.
- var/bounty_value = 0
- ///Multiplier for the bounty payout received by the Supply budget if the cube is sent without having to nag.
- var/speed_bonus = 0.2
- ///Multiplier for the bounty payout received by the person who completed the bounty.
- var/holder_cut = 0.3
- ///Multiplier for the bounty payout received by the person who claims the handling tip.
- var/handler_tip = 0.1
- ///Time between nags.
- var/nag_cooldown = 5 MINUTES
- ///How much the time between nags extends each nag.
- var/nag_cooldown_multiplier = 1.25
- ///Next world tick to nag Supply listeners.
- var/next_nag_time
- ///Who completed the bounty.
- var/bounty_holder
- ///What job the bounty holder had.
- var/bounty_holder_job
- ///What the bounty was for.
- var/bounty_name
- ///Bank account of the person who completed the bounty.
- var/datum/bank_account/bounty_holder_account
- ///Bank account of the person who receives the handling tip.
- var/datum/bank_account/bounty_handler_account
-
-/obj/item/bounty_cube/Initialize(mapload)
- . = ..()
- ADD_TRAIT(src, TRAIT_NO_BARCODES, INNATE_TRAIT) // Don't allow anyone to override our pricetag component with a barcode
-
-/obj/item/bounty_cube/examine()
- . = ..()
- if(speed_bonus)
- . += span_notice("[time2text(next_nag_time - world.time,"mm:ss", NO_TIMEZONE)] remains until [bounty_value * speed_bonus] [MONEY_NAME_SINGULAR] speedy delivery bonus lost.")
- if(handler_tip && !bounty_handler_account)
- . += span_notice("Scan this in the cargo shuttle with an export scanner to register your bank account for the [bounty_value * handler_tip] [MONEY_NAME_SINGULAR] handling tip.")
-
-/obj/item/bounty_cube/process(seconds_per_tick)
- //if our nag cooldown has finished and we aren't on Centcom or in transit, then nag
- if(COOLDOWN_FINISHED(src, next_nag_time) && !is_centcom_level(z) && !is_reserved_level(z))
- //set up our fallback message, in case of AAS being broken it will be sent to card holders
- var/nag_message = "[src] is unsent in [get_area(src)]."
-
- //nag on Supply channel and reduce the speed bonus multiplier to nothing
- var/obj/machinery/announcement_system/aas = get_announcement_system(/datum/aas_config_entry/bounty_cube_unsent, src, list(RADIO_CHANNEL_SUPPLY))
- if (aas)
- nag_message = aas.compile_config_message(/datum/aas_config_entry/bounty_cube_unsent, list("LOCATION" = get_area_name(src), "COST" = bounty_value), "Regular Message")
- if (speed_bonus)
- aas.announce(/datum/aas_config_entry/bounty_cube_unsent, list("LOCATION" = get_area_name(src), "COST" = bounty_value, "BONUSLOST" = bounty_value * speed_bonus), list(RADIO_CHANNEL_SUPPLY), "When Bonus Lost")
- else
- aas.broadcast("[nag_message]", list(RADIO_CHANNEL_SUPPLY))
- speed_bonus = 0
-
- //alert the holder
- bounty_holder_account.bank_card_talk("[nag_message]")
-
- //if someone has registered for the handling tip, nag them
- bounty_handler_account?.bank_card_talk(nag_message)
-
- //increase our cooldown length and start it again
- nag_cooldown = nag_cooldown * nag_cooldown_multiplier
- COOLDOWN_START(src, next_nag_time, nag_cooldown)
-
-/obj/item/bounty_cube/proc/set_up(datum/bounty/my_bounty, obj/item/card/id/holder_id)
- bounty_value = my_bounty.get_bounty_reward()
- bounty_name = my_bounty.name
- bounty_holder = holder_id.registered_name
- bounty_holder_job = holder_id.assignment
- bounty_holder_account = holder_id.registered_account
- name = "\improper [bounty_value] [MONEY_SYMBOL] [name]"
- desc += " The sales tag indicates it was [bounty_holder] ([bounty_holder_job])'s reward for completing the [bounty_name] bounty."
- AddComponent(/datum/component/pricetag, holder_id.registered_account, holder_cut, FALSE)
- AddComponent(/datum/component/gps, "[src]")
- START_PROCESSING(SSobj, src)
- COOLDOWN_START(src, next_nag_time, nag_cooldown)
- aas_config_announce(/datum/aas_config_entry/bounty_cube_created, list(
- "LOCATION" = get_area_name(src),
- "PERSON" = bounty_holder,
- "RANK" = bounty_holder_job,
- "BONUSTIME" = time2text(next_nag_time - world.time,"mm:ss", NO_TIMEZONE),
- "COST" = bounty_value
- ), src, list(RADIO_CHANNEL_SUPPLY))
-
-//for when you need a REAL bounty cube to test with and don't want to do a bounty each time your code changes
-/obj/item/bounty_cube/debug_cube
- name = "debug bounty cube"
- desc = "Use in-hand to set it up with a random bounty. Requires an ID it can detect with a bank account attached. \
- This will alert Supply over the radio with your name and location, and cargo techs will be dispatched with kill on sight clearance."
- var/set_up = FALSE
-
-/obj/item/bounty_cube/debug_cube/attack_self(mob/user)
- if(!isliving(user))
- to_chat(user, span_warning("You aren't eligible to use this!"))
- return ..()
-
- if(!set_up)
- var/mob/living/squeezer = user
- if(squeezer.get_bank_account())
- set_up(random_bounty(), squeezer.get_idcard())
- set_up = TRUE
- return ..()
- to_chat(user, span_notice("It can't detect your bank account."))
-
- return ..()
-
-///Beacon to launch a new bounty setup when activated.
-/obj/item/civ_bounty_beacon
- name = "civilian bounty beacon"
- desc = "N.T. approved civilian bounty beacon, toss it down and you will have a bounty pad and computer delivered to you."
- icon = 'icons/obj/machines/floor.dmi'
- icon_state = "floor_beacon"
- var/uses = 2
-
-/obj/item/civ_bounty_beacon/attack_self()
- loc.visible_message(span_warning("\The [src] begins to beep loudly!"))
- addtimer(CALLBACK(src, PROC_REF(launch_payload)), 1 SECONDS)
-
-/obj/item/civ_bounty_beacon/proc/launch_payload()
- playsound(src, SFX_SPARKS, 80, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
- switch(uses)
- if(2)
- new /obj/machinery/piratepad/civilian(drop_location())
- if(1)
- new /obj/machinery/computer/piratepad_control/civilian(drop_location())
- qdel(src)
- uses--
-
-/datum/aas_config_entry/bounty_cube_created
- name = "Cargo Alert: Bounty Cube Created"
- announcement_lines_map = list(
- "Message" = "A %COST cr bounty cube has been created in %LOCATION by %PERSON (%RANK). Speedy delivery bonus lost in %BONUSTIME.")
- vars_and_tooltips_map = list(
- "LOCATION" = "will be replaced with the location of the cube.",
- "PERSON" = "with who created the cube.",
- "RANK" = "with their job.",
- "BONUSTIME" = "with the time left for speedy delivery tip.",
- "COST" = "with the cost of the cube.",
- )
-
-/datum/aas_config_entry/bounty_cube_unsent
- name = "Cargo Alert: Bounty Cube Unsent"
- announcement_lines_map = list(
- "Regular Message" = "The %COST cr bounty cube is unsent in %LOCATION.",
- "When Bonus Lost" = "The %COST cr bounty cube is unsent in %LOCATION. Speedy delivery bonus of %BONUSLOST credits lost.")
- vars_and_tooltips_map = list(
- "LOCATION" = "will be replaced with the location of the cube.",
- "COST" = "with the cost of the cube.",
- "BONUSLOST" = "with the lost bonus tip, it will be sent just for When Bonus Lost message!",
- )
-
-#undef CIV_BOUNTY_SPLIT
diff --git a/code/game/machinery/civilian_bounty/bounty_cube.dm b/code/game/machinery/civilian_bounty/bounty_cube.dm
new file mode 100644
index 00000000000..1c73395a043
--- /dev/null
+++ b/code/game/machinery/civilian_bounty/bounty_cube.dm
@@ -0,0 +1,144 @@
+
+/**
+ * Upon completion of a civilian bounty, one of these is created.
+ * It is sold to cargo to give the cargo budget bounty money, and the person who completed it cash.
+ */
+/obj/item/bounty_cube
+ name = "bounty cube"
+ desc = "A bundle of compressed hardlight data, containing a completed bounty. Sell this on the cargo shuttle to claim it!"
+ icon = 'icons/obj/economy.dmi'
+ icon_state = "bounty_cube"
+ ///Value of the bounty that this bounty cube sells for.
+ var/bounty_value = 0
+ ///Multiplier for the bounty payout received by the Supply budget if the cube is sent without having to nag.
+ var/speed_bonus = 0.2
+ ///Percentage of the bounty payout received by the people who completed the bounty. Split between multiple people in the event multiple people finished a global bounty.
+ var/holder_cut = BOUNTY_CUT_STANDARD
+ ///Multiplier for the bounty payout received by the person who claims the handling tip.
+ var/handler_tip = 0.1
+ ///Time between nags.
+ var/nag_cooldown = 5 MINUTES
+ ///How much the time between nags extends each nag.
+ var/nag_cooldown_multiplier = 1.25
+ ///Next world tick to nag Supply listeners.
+ var/next_nag_time
+ ///Who completed the bounty.
+ var/bounty_holder
+ ///What job the bounty holder had.
+ var/bounty_holder_job
+ ///What the bounty was for.
+ var/bounty_name
+ ///Bank account of the people who completed the bounty.
+ var/list/datum/bank_account/bounty_holder_accounts
+ ///Bank account of the person who receives the handling tip.
+ var/datum/bank_account/bounty_handler_account
+
+/obj/item/bounty_cube/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_NO_BARCODES, INNATE_TRAIT) // Don't allow anyone to override our pricetag component with a barcode
+
+/obj/item/bounty_cube/examine()
+ . = ..()
+ if(speed_bonus)
+ . += span_notice("[time2text(next_nag_time - world.time,"mm:ss", NO_TIMEZONE)] remains until [bounty_value * speed_bonus] [MONEY_NAME_SINGULAR] speedy delivery bonus lost.")
+ if(handler_tip && !bounty_handler_account)
+ . += span_notice("Scan this in the cargo shuttle with an export scanner to register your bank account for the [bounty_value * handler_tip] [MONEY_NAME_SINGULAR] handling tip.")
+
+/obj/item/bounty_cube/process(seconds_per_tick)
+ //if our nag cooldown has finished and we aren't on Centcom or in transit, then nag
+ if(COOLDOWN_FINISHED(src, next_nag_time) && !is_centcom_level(z) && !is_reserved_level(z))
+ //set up our fallback message, in case of AAS being broken it will be sent to card holders
+ var/nag_message = "[src] is unsent in [get_area(src)]."
+
+ //nag on Supply channel and reduce the speed bonus multiplier to nothing
+ var/obj/machinery/announcement_system/aas = get_announcement_system(/datum/aas_config_entry/bounty_cube_unsent, src, list(RADIO_CHANNEL_SUPPLY))
+ if (aas)
+ nag_message = aas.compile_config_message(/datum/aas_config_entry/bounty_cube_unsent, list("LOCATION" = get_area_name(src), "COST" = bounty_value), "Regular Message")
+ if (speed_bonus)
+ aas.announce(/datum/aas_config_entry/bounty_cube_unsent, list("LOCATION" = get_area_name(src), "COST" = bounty_value, "BONUSLOST" = bounty_value * speed_bonus), list(RADIO_CHANNEL_SUPPLY), "When Bonus Lost")
+ else
+ aas.broadcast("[nag_message]", list(RADIO_CHANNEL_SUPPLY))
+ speed_bonus = 0
+
+ //alert the holder
+ for(var/datum/bank_account/bounty_holder_account in bounty_holder_accounts)
+ bounty_holder_account.bank_card_talk("[nag_message]")
+
+ //if someone has registered for the handling tip, nag them
+ bounty_handler_account?.bank_card_talk(nag_message)
+
+ //increase our cooldown length and start it again
+ nag_cooldown = nag_cooldown * nag_cooldown_multiplier
+ COOLDOWN_START(src, next_nag_time, nag_cooldown)
+
+/**
+ * Configures the bounty cube's name, value, and annouces to the crew
+ */
+/obj/item/bounty_cube/proc/set_up(datum/bounty/my_bounty, obj/item/card/id/holder_id)
+ bounty_value = my_bounty.get_bounty_reward()
+ bounty_name = my_bounty.name
+ bounty_holder = holder_id.registered_name
+ bounty_holder_job = holder_id.assignment
+ bounty_holder_accounts = my_bounty.contribution
+
+ name = "\improper [bounty_value] [MONEY_SYMBOL] [name]"
+ desc += " The sales tag indicates it was [bounty_holder] ([bounty_holder_job])'s reward for completing the [bounty_name] bounty."
+ AddComponent(/datum/component/pricetag, bounty_holder_accounts.Copy(), holder_cut, FALSE)
+ AddComponent(/datum/component/gps, "[src]")
+
+ START_PROCESSING(SSobj, src)
+ COOLDOWN_START(src, next_nag_time, nag_cooldown)
+ aas_config_announce(/datum/aas_config_entry/bounty_cube_created, list(
+ "LOCATION" = get_area_name(src),
+ "PERSON" = bounty_holder,
+ "RANK" = bounty_holder_job,
+ "BONUSTIME" = time2text(next_nag_time - world.time,"mm:ss", NO_TIMEZONE),
+ "COST" = bounty_value
+ ), src, list(RADIO_CHANNEL_SUPPLY))
+
+//for when you need a REAL bounty cube to test with and don't want to do a bounty each time your code changes
+/obj/item/bounty_cube/debug_cube
+ name = "debug bounty cube"
+ desc = "Use in-hand to set it up with a random bounty. Requires an ID it can detect with a bank account attached. \
+ This will alert Supply over the radio with your name and location, and cargo techs will be dispatched with kill on sight clearance."
+ var/set_up = FALSE
+
+/obj/item/bounty_cube/debug_cube/attack_self(mob/user)
+ if(!isliving(user))
+ to_chat(user, span_warning("You aren't eligible to use this!"))
+ return ..()
+
+ if(!set_up)
+ var/mob/living/squeezer = user
+ if(squeezer.get_bank_account())
+ set_up(random_bounty(), squeezer.get_idcard())
+ set_up = TRUE
+ return ..()
+ to_chat(user, span_notice("It can't detect your bank account."))
+
+ return ..()
+
+// Bounty Cube AAS Config Entries
+
+/datum/aas_config_entry/bounty_cube_created
+ name = "Cargo Alert: Bounty Cube Created"
+ announcement_lines_map = list(
+ "Message" = "A %COST cr bounty cube has been created in %LOCATION by %PERSON (%RANK). Speedy delivery bonus lost in %BONUSTIME.")
+ vars_and_tooltips_map = list(
+ "LOCATION" = "will be replaced with the location of the cube.",
+ "PERSON" = "with who created the cube.",
+ "RANK" = "with their job.",
+ "BONUSTIME" = "with the time left for speedy delivery tip.",
+ "COST" = "with the cost of the cube.",
+ )
+
+/datum/aas_config_entry/bounty_cube_unsent
+ name = "Cargo Alert: Bounty Cube Unsent"
+ announcement_lines_map = list(
+ "Regular Message" = "The %COST cr bounty cube is unsent in %LOCATION.",
+ "When Bonus Lost" = "The %COST cr bounty cube is unsent in %LOCATION. Speedy delivery bonus of %BONUSLOST credits lost.")
+ vars_and_tooltips_map = list(
+ "LOCATION" = "will be replaced with the location of the cube.",
+ "COST" = "with the cost of the cube.",
+ "BONUSLOST" = "with the lost bonus tip, it will be sent just for When Bonus Lost message!",
+ )
diff --git a/code/game/machinery/civilian_bounty/bounty_machinery.dm b/code/game/machinery/civilian_bounty/bounty_machinery.dm
new file mode 100644
index 00000000000..96cae89c8d1
--- /dev/null
+++ b/code/game/machinery/civilian_bounty/bounty_machinery.dm
@@ -0,0 +1,444 @@
+///Percentage of a civilian bounty the civilian will make.
+#define CIV_BOUNTY_SPLIT 30
+#define HIGH_PRIORITY_BOUNTY_ODDS 20
+
+///Pad for the Civilian Bounty Control.
+/obj/machinery/piratepad/civilian
+ name = "civilian bounty pad"
+ desc = "A machine designed to send civilian bounty targets to centcom."
+ layer = TABLE_LAYER
+ resistance_flags = FIRE_PROOF
+ circuit = /obj/item/circuitboard/machine/bountypad
+ var/cooldown_reduction = 0
+
+/obj/machinery/piratepad/civilian/RefreshParts()
+ . = ..()
+ var/T = -2
+ for(var/datum/stock_part/micro_laser/micro_laser in component_parts)
+ T += micro_laser.tier
+
+ for(var/datum/stock_part/scanning_module/scanning_module in component_parts)
+ T += scanning_module.tier
+
+ cooldown_reduction = T * (30 SECONDS)
+
+/obj/machinery/piratepad/civilian/proc/get_cooldown_reduction()
+ return cooldown_reduction
+
+///Computer for assigning new civilian bounties, and sending bounties for collection.
+/obj/machinery/computer/piratepad_control/civilian
+ name = "civilian bounty control terminal"
+ desc = "A console for assigning civilian bounties to inserted ID cards, and for controlling the bounty pad for export."
+ status_report = "Ready for delivery."
+ icon_screen = "civ_bounty"
+ icon_keyboard = "id_key"
+ warmup_time = 3 SECONDS
+ circuit = /obj/item/circuitboard/computer/bountypad
+ interface_type = "CivCargoHoldTerminal"
+ load_holding_facility = FALSE
+ ///Typecast of an inserted, scanned ID card inside the console, as bounties are held within the ID card.
+ var/obj/item/card/id/inserted_scan_id
+ ///Cooldown for printing the bounty sheet, and not breaking people's eardrums.
+ COOLDOWN_DECLARE(sheet_printer_cooldown)
+
+/obj/machinery/computer/piratepad_control/civilian/attackby(obj/item/I, mob/living/user, list/modifiers, list/attack_modifiers)
+ if(isidcard(I))
+ if(id_insert(user, I, inserted_scan_id))
+ inserted_scan_id = I
+ return TRUE
+ return ..()
+
+/obj/machinery/computer/piratepad_control/multitool_act(mob/living/user, obj/item/multitool/I)
+ if(istype(I) && istype(I.buffer,/obj/machinery/piratepad/civilian))
+ to_chat(user, span_notice("You link [src] with [I.buffer] in [I] buffer."))
+ pad_ref = WEAKREF(I.buffer)
+ return TRUE
+
+/obj/machinery/computer/piratepad_control/civilian/post_machine_initialize()
+ . = ..()
+ if(cargo_hold_id)
+ for(var/obj/machinery/piratepad/civilian/C as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/piratepad/civilian))
+ if(C.cargo_hold_id == cargo_hold_id)
+ pad_ref = WEAKREF(C)
+ return
+ else
+ var/obj/machinery/piratepad/civilian/pad = locate() in range(4,src)
+ pad_ref = WEAKREF(pad)
+
+/obj/machinery/computer/piratepad_control/civilian/recalc()
+ if(sending)
+ return FALSE
+ if(!inserted_scan_id)
+ status_report = "Please insert your ID first."
+ playsound(loc, 'sound/machines/synth/synth_no.ogg', 30 , TRUE)
+ return FALSE
+ if(!inserted_scan_id.registered_account.civilian_bounty)
+ status_report = "Please accept a new civilian bounty first."
+ playsound(loc, 'sound/machines/synth/synth_no.ogg', 30 , TRUE)
+ return FALSE
+ status_report = "Civilian Bounty: "
+ var/obj/machinery/piratepad/civilian/pad = pad_ref?.resolve()
+ for(var/atom/movable/possible_shippable in get_turf(pad))
+ if(possible_shippable == pad)
+ continue
+ if(possible_shippable.flags_1 & HOLOGRAM_1)
+ continue
+ if(isitem(possible_shippable))
+ var/obj/item/possible_shippable_item = possible_shippable
+ if(possible_shippable_item.item_flags & ABSTRACT)
+ continue
+ if(inserted_scan_id.registered_account.civilian_bounty.applies_to(possible_shippable))
+ status_report += "Target Applicable."
+ playsound(loc, 'sound/machines/synth/synth_yes.ogg', 30 , TRUE)
+ return
+ status_report += "Not Applicable."
+ playsound(loc, 'sound/machines/synth/synth_no.ogg', 30 , TRUE)
+
+
+/**
+ * This fully rewrites base behavior in order to only check for bounty objects, and no other types of objects like pirate-pads do.
+ */
+/obj/machinery/computer/piratepad_control/civilian/send(check_global = FALSE, mob/user)
+ status_report = ""
+ playsound(loc, 'sound/machines/wewewew.ogg', 70, TRUE)
+ if(!sending)
+ return FALSE
+ var/datum/bank_account/id_account = inserted_scan_id?.registered_account
+
+ // To account for check_global, we're going to construct a list of all bounties we want to check.
+ var/list/datum/bounty/bounty_stack = list()
+
+ if(check_global)
+ bounty_stack = GLOB.shared_crew_bounties.Copy()
+ else
+ bounty_stack += id_account?.civilian_bounty
+
+ if(!length(bounty_stack))
+ stop_sending()
+ return FALSE
+ var/active_count = 0
+ var/obj/machinery/piratepad/civilian/pad = pad_ref?.resolve()
+ for(var/atom/movable/possible_shippable in get_turf(pad))
+ if(possible_shippable == pad)
+ continue
+ if(possible_shippable.flags_1 & HOLOGRAM_1)
+ continue
+ if(isitem(possible_shippable))
+ var/obj/item/possible_shippable_item = possible_shippable
+ if(possible_shippable_item.item_flags & ABSTRACT)
+ continue
+
+ for(var/datum/bounty/stack_item in bounty_stack)
+ if(stack_item.applies_to(possible_shippable))
+ active_count++
+ LAZYADDASSOC(stack_item.contribution, id_account, stack_item.contribution_amount(possible_shippable))
+ if(stack_item.contribution[id_account] <= 0 || !stack_item.contribution[id_account])
+ stack_item.contribution[id_account] = stack_item.contribution_amount(possible_shippable)
+ stack_item.ship(possible_shippable)
+ qdel(possible_shippable)
+
+ if(active_count >= 1)
+ status_report += "Bounty Target[active_count > 1 ? "s" : ""] Found x[active_count]. "
+
+ SStgui.update_uis(src) //update Ui data to display how much of the bounty remains
+ else
+ status_report = "No applicable target found. Aborting. "
+ stop_sending()
+
+ active_count = 0 // We'll just re-use this for the second message setter.
+ for(var/datum/bounty/stack_item in bounty_stack)
+ if(stack_item.can_claim())
+ active_count++
+ //Pay for the bounty with the ID's department funds.
+ SSblackbox.record_feedback("tally", "bounties_completed", 1, stack_item.type)
+ stack_item.claimed = TRUE
+ stack_item.on_claimed(inserted_scan_id)
+ // Unique case: A global bounty is completed, and you have the same bounty as a personal bounty,
+ // it will complete your personal one as well. It will however only increment the tracker by one.
+ if(check_global)
+ SSeconomy.civ_bounty_tracker++ //This is the tracker for adding more global bounties, not for logging purposes.
+ for(var/datum/bank_account/helper in stack_item.contribution)
+ if(istype(helper?.civilian_bounty, stack_item.type))
+ helper.reset_bounty(inserted_scan_id)
+ helper.bank_card_talk("Your [stack_item.name] bounty has been completed for matching the completed station bounty!")
+ else
+ id_account.reset_bounty(inserted_scan_id)
+
+ var/obj/item/bounty_cube/reward = new /obj/item/bounty_cube(drop_location())
+ reward.set_up(stack_item, inserted_scan_id)
+ if(active_count >= 1)
+ status_report += "x[active_count] Bount[active_count > 1 ? "ies" : "y"] completed! \
+ Please give your bounty cube[active_count > 1 ? "s" : ""] to cargo for your automated payout shortly. "
+
+ if(check_global)
+ update_global_bounty_list(round(CIV_BOUNTY_BASELINE + (SSeconomy.civ_bounty_tracker / 3)), FALSE)
+
+ pad.visible_message(span_notice("[pad] activates!"))
+ flick(pad.sending_state,pad)
+ pad.icon_state = pad.idle_state
+ playsound(loc, 'sound/machines/synth/synth_yes.ogg', 30 , TRUE)
+ sending = FALSE
+ return TRUE
+
+///Here is where cargo bounties are added to the player's bank accounts, then adjusted and scaled into a civilian bounty.
+/obj/machinery/computer/piratepad_control/civilian/proc/add_bounties(mob/user, cooldown_reduction = 0)
+ var/datum/bank_account/id_account = inserted_scan_id?.registered_account
+ if(!id_account)
+ return FALSE
+ if((id_account.civilian_bounty || id_account.bounties) && !COOLDOWN_FINISHED(id_account, bounty_timer))
+ var/time_left = DisplayTimeText(COOLDOWN_TIMELEFT(id_account, bounty_timer), round_seconds_to = 1)
+ balloon_alert(user, "try again in [time_left]!")
+ return FALSE
+ if(!inserted_scan_id.trim)
+ say("Requesting ID card has no job assignment registered!")
+ return FALSE
+
+ var/list/datum/bounty/crumbs = inserted_scan_id.trim.generate_bounty_list()
+ COOLDOWN_START(id_account, bounty_timer, (5 MINUTES) - cooldown_reduction)
+ id_account.bounties = crumbs
+ return TRUE
+
+
+/**
+ * Proc that assigned a civilian bounty to an ID card, from the list of potential bounties that that bank account currently has available.
+ * Available choices are assigned during add_bounties, and one is locked in here.
+ *
+ * @param choice The index of the bounty in the list of bounties that the player can choose from.
+ */
+/obj/machinery/computer/piratepad_control/civilian/proc/pick_bounty(datum/bounty/choice)
+ var/datum/bank_account/id_account = inserted_scan_id?.registered_account
+ if(!id_account?.bounties?[choice])
+ playsound(loc, 'sound/machines/synth/synth_no.ogg', 40 , TRUE)
+ return
+ id_account.set_bounty(id_account.bounties[choice], inserted_scan_id)
+ id_account.bounties = null
+ SSblackbox.record_feedback("tally", "bounties_assigned", 1, id_account.civilian_bounty.type)
+ return id_account.civilian_bounty
+
+/obj/machinery/computer/piratepad_control/civilian/click_alt(mob/user)
+ id_eject(user, inserted_scan_id)
+ return CLICK_ACTION_SUCCESS
+
+/obj/machinery/computer/piratepad_control/civilian/ui_data(mob/user)
+ var/list/data = list()
+ data["points"] = points
+ data["pad"] = pad_ref?.resolve() ? TRUE : FALSE
+ data["sending"] = sending
+ data["status_report"] = status_report
+ data["id_inserted"] = inserted_scan_id
+ data["claimed_bounties"] = SSeconomy.civ_bounty_tracker
+
+// Personal bounty data:
+ if(inserted_scan_id?.registered_account)
+ if(inserted_scan_id.registered_account.civilian_bounty)
+ data["id_bounty_info"] = inserted_scan_id.registered_account.civilian_bounty.description
+ data["id_bounty_num"] = inserted_scan_id.registered_account.bounty_num()
+ data["id_bounty_value"] = (inserted_scan_id.registered_account.civilian_bounty.get_bounty_reward()) * (CIV_BOUNTY_SPLIT / 100)
+ if(inserted_scan_id.registered_account.bounties)
+ data["picking"] = TRUE
+ data["id_bounty_names"] = list()
+ data["id_bounty_infos"] = list()
+ data["id_bounty_values"] = list()
+ for(var/datum/bounty/bounty as anything in inserted_scan_id.registered_account.bounties)
+ data["id_bounty_names"] += bounty.name
+ data["id_bounty_infos"] += bounty.description
+ data["id_bounty_values"] += bounty.get_bounty_reward() * (CIV_BOUNTY_SPLIT / 100)
+
+ else
+ data["picking"] = FALSE
+
+// Global bounty data:
+ data["listBounty"] = list()
+ for(var/datum/bounty/global_bounty as anything in GLOB.shared_crew_bounties)
+
+ var/ship_max = global_bounty.get_max()
+ var/ship_total = global_bounty.get_total() //Present value as a percentage, we'll handle 0 and 100 as constants
+
+ if(data["listBounty"]["name"] == global_bounty.name)
+ continue
+ data["listBounty"] += list(list(
+ "name" = global_bounty.name,
+ "description" = global_bounty.description,
+ "reward" = global_bounty.get_bounty_reward(),
+ "claimed" = global_bounty.claimed,
+ "shipped" = ship_total,
+ "maximum" = ship_max,
+ "priority" = global_bounty.high_priority,
+ "unique" = global_bounty.unique
+ ))
+
+ return data
+
+/obj/machinery/computer/piratepad_control/civilian/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ var/obj/machinery/piratepad/civilian/pad = pad_ref?.resolve()
+ if(!pad)
+ return
+ var/mob/user = ui.user
+ if(!user.can_perform_action(src) || (machine_stat & (NOPOWER|BROKEN)))
+ return
+ switch(action) //several ui_acts are handled on parent by piratepad
+ if("pick")
+ pick_bounty(params["value"])
+ return TRUE
+ if("bounty")
+ add_bounties(user, pad.get_cooldown_reduction())
+ return TRUE
+ if("eject")
+ id_eject(user, inserted_scan_id)
+ inserted_scan_id = null
+ return TRUE
+ if("update_list")
+ playsound(src, 'sound/machines/data_transmission.ogg', 50) // Should only need to play once per round due to the list auto-updating afterwards.
+
+ var/bonus_bounties = clamp(round(length(GLOB.player_list) / 8), 0, 5) // The number of bounties to be generated is 5 + 1-per every 8 players on the server, up to a max of 10 total.
+ looped_global_update(1, CIV_BOUNTY_BASELINE + bonus_bounties, first_time = TRUE) // Just for visual flair
+ return TRUE
+ if("print")
+ print_sheet(user)
+
+/// Self explanitory, holds the ID card in the console for bounty payout and manipulation.
+/obj/machinery/computer/piratepad_control/civilian/proc/id_insert(mob/user, obj/item/inserting_item, obj/item/target)
+ var/obj/item/card/id/card_to_insert = inserting_item
+ var/holder_item = FALSE
+
+ if(!isidcard(card_to_insert))
+ card_to_insert = inserting_item.remove_id()
+ holder_item = TRUE
+
+ if(!card_to_insert || !user.transferItemToLoc(card_to_insert, src))
+ return FALSE
+
+ if(target)
+ if(holder_item && inserting_item.insert_id(target))
+ playsound(src, 'sound/machines/terminal/terminal_insert_disc.ogg', 50, FALSE)
+ else
+ id_eject(user, target)
+
+ user.visible_message(span_notice("[user] inserts \the [card_to_insert] into \the [src]."),
+ span_notice("You insert \the [card_to_insert] into \the [src]."))
+ playsound(src, 'sound/machines/terminal/terminal_insert_disc.ogg', 50, FALSE)
+ ui_interact(user)
+ return TRUE
+
+///Removes A stored ID card.
+/obj/machinery/computer/piratepad_control/civilian/proc/id_eject(mob/user, obj/item/target)
+ if(!target)
+ to_chat(user, span_warning("That slot is empty!"))
+ return FALSE
+ else
+ try_put_in_hand(target, user)
+ user.visible_message(span_notice("[user] gets \the [target] from \the [src]."), \
+ span_notice("You get \the [target] from \the [src]."))
+ playsound(src, 'sound/machines/terminal/terminal_insert_disc.ogg', 50, FALSE)
+ inserted_scan_id = null
+ return TRUE
+
+/**
+ * Updates the global bounty list: First by sorting through all completed bounties on the list and deleting them.
+ * Then, adds new bounties up to the limit, defined by the update_up_to argument.
+ * The bounties to be added should not share duplicates between job subtypes.
+ * @param update_up_to How many new bounties to add to the list, up to the maximum defined by MAXIMUM_BOUNTY_JOBS.
+ * @param enable_high_priority If enabled, means that past a prob(HIGH_PRIORITY_BOUNTY_ODDS), the created bounty will have a 1.5x value multiplier and be labeled in the UI.
+ * @param running_jobs A list we generate when making multiple bounties at once, this stores the job define to prevent double dipping on the same job type.
+ */
+/obj/machinery/computer/piratepad_control/civilian/proc/update_global_bounty_list(update_up_to = CIV_BOUNTY_BASELINE, enable_high_priority = FALSE, list/running_jobs)
+ //First, clear out completed bounties.
+ for(var/datum/bounty/complete_or_unique in GLOB.shared_crew_bounties)
+ if(complete_or_unique.claimed)
+ GLOB.shared_crew_bounties -= complete_or_unique
+ if(complete_or_unique.unique)
+ update_up_to++ // We're doing this to ignore the quantity of unique bounties in the global list.
+
+ //Then, add new bounties up to the limit.
+ var/list/jobs_picked = running_jobs || list()
+ while(length(GLOB.shared_crew_bounties) < update_up_to)
+ var/job_code = rand(CIV_JOB_BASIC, CIV_JOB_BITRUN) //CIV_JOB_ defines taken from _DEFINES/economy.dm. If new job bounty classes are added, swap out our maximum.
+ if(job_code in jobs_picked)
+ continue
+ jobs_picked += job_code
+
+ var/datum/bounty/new_bounty = random_bounty(job_code)
+ if(new_bounty.global_exempt)
+ continue
+
+ GLOB.shared_crew_bounties.Insert(0, new_bounty)
+
+ if(enable_high_priority && prob(HIGH_PRIORITY_BOUNTY_ODDS))
+ new_bounty.high_priority = TRUE
+ new_bounty.description += "\
+ This bounty is marked as high priority, and will reward 1.5x the normal payout!"
+ return jobs_picked
+
+/// Performs several global bounty updates in a row on a callback loop, adding one each time.
+/obj/machinery/computer/piratepad_control/civilian/proc/looped_global_update(current_count, update_to, inherited_list, first_time = FALSE)
+ var/jobs_picked = update_global_bounty_list(current_count, enable_high_priority = TRUE, running_jobs = inherited_list)
+
+ if(current_count == update_to)
+ if(first_time)
+ setup_special_procs()
+ return TRUE
+ current_count++
+ addtimer(CALLBACK(src, PROC_REF(looped_global_update), current_count, update_to, jobs_picked, first_time), 0.8 SECONDS)
+ return FALSE
+
+/// Spawns the roundstart "special" bounties.
+/obj/machinery/computer/piratepad_control/civilian/proc/setup_special_procs()
+ for(var/selected_special in subtypesof(/datum/bounty/item/special))
+ GLOB.shared_crew_bounties += new selected_special
+
+/**
+ * Handles cooldowns and creation of a new cargo bounty sheet.
+ */
+/obj/machinery/computer/piratepad_control/civilian/proc/print_sheet(mob/living/user)
+ if(!COOLDOWN_FINISHED(src, sheet_printer_cooldown))
+ balloon_alert(user, "printer spooling!")
+ return FALSE
+
+ var/obj/item/paper/paper = new(loc)
+ paper.name = "paper - Bounties"
+
+ var/list/printout_text = list()
+ printout_text += "
8zyhkj
z1s`50ul}b1ocpi@>gGS9mgmer;pd-v>dOD7z+OP*{G$zjB5>dSw*?4;y7Jk7TF}bh
z%a6bGPrW PQmz
z$CxZ0Ch&zIb(VV LkiA#SwvIMhuw}bvCX%e5
z#BRxKl86ziA)VNPDw6;=A*m7>A6#=(V>$+Pe{0-Ux*?q >YI<0eJF{uH_LDnCUR$xIQk-LeUTuD5M;!8C8s;&7ti~nU=&zqw
z5x9SFW-G>LIuM`!o?A2XQd0`%_n6tZgO!XOc6p~(T)H6NxJG7Vo^+8{kYM*}MkWQ>
zZIkAT9b0v}@7hK*gO|2HHF)X=ycs?hTu;P}#-S&C!_}5np1@Zn5bUkZ^_!gl
z3Q6kQb3zBfE&52J2mZA}56#E}9c-^U-lm1AWmps}7HP{J4e9*q>uSz=MgD4Ddf`H$
zd`wHk
mz1PMv1Es(GVM2~&Ya6!EkbztHh=
u8!bT0r`}*7)n1<}_vWs9)pB
z^iL(~G4exTyTaF{S=mN;Uw`nJH8&3pdN#4U>AS&Kb6~1AAfE$YinkxEqe_VoRBbTF
zCjR{_Z06`hSg`G$pvprIkbJw{p#88gCVnKhtg34I$G-JGf1`oahy}lD5$t}b{^%QK
z@5_~mm-T!DsMPPD%VPDkYt}3dBRzrt@!XXKu?Rl=t1pb(3){!1hRE-S!7-?`jZ4IX
zjNR1mKt%m>l^yb9?^a~J?G))n5i0c6Nr6CBO%s&fi|!*|xy>0DID}
tECU-jLg9O!mEeKSbBb
z!sNJb{*HNhj)3vOX=?p9f-CN+0BR!l$i1V~@k||k(d=h5|ETxsI-!}Gf+kz*;~IPq
z0L$<)-lI?VZ#%kKwwm>=lP>}6GWsH0INo_sSh-9qPo#LH+
9Nj$