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.

<img width="578" height="377" alt="image"
src="https://github.com/user-attachments/assets/c8e1cb8d-dda6-4983-8588-93dffffc2b4e"
/>

_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.
This commit is contained in:
ArcaneMusic
2026-04-13 13:07:42 -05:00
committed by GitHub
parent 7fa4d49912
commit 3503e9c565
24 changed files with 1243 additions and 728 deletions
@@ -190,6 +190,16 @@
balloon_alert(user, "saved to multitool buffer")
return TRUE
/obj/machinery/piratepad/screwdriver_act(mob/living/user, obj/item/tool)
. = ..()
if(!.)
return default_deconstruction_screwdriver(user, "lpad-idle-open", "lpad-idle-off", tool)
/obj/machinery/piratepad/crowbar_act(mob/living/user, obj/item/tool)
. = ..()
if(!.)
return default_deconstruction_crowbar(tool)
/obj/machinery/piratepad/screwdriver_act_secondary(mob/living/user, obj/item/screwdriver/screw)
. = ..()
if(!.)
@@ -222,6 +232,8 @@
var/interface_type = "CargoHoldTerminal"
///Typecache of things that shouldn't be sold and shouldn't have their contents sold.
var/static/list/nosell_typecache
/// When we send the pad for this machine, do we want to lazyload in the ninja holding facility?
var/load_holding_facility = TRUE
/obj/machinery/computer/piratepad_control/Initialize(mapload)
..()
@@ -274,11 +286,7 @@
recalc()
. = TRUE
if("send")
start_sending()
//We ensure that the holding facility is loaded in time in case we're selling mobs.
//This isn't the prettiest place to put it, but 'start_sending()' is also used by civilian bounty computers
//And we don't need them to also load the holding facility.
SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NINJA_HOLDING_FACILITY)
start_sending(params["global"], usr)
. = TRUE
if("stop")
stop_sending()
@@ -303,8 +311,10 @@
if(!value)
status_report += "0"
/// Deletes and sells the item
/obj/machinery/computer/piratepad_control/proc/send()
/**
* Sorts through all items on the control pad via pirate_export_loop, then generates a printout to view in the TGUI.
*/
/obj/machinery/computer/piratepad_control/proc/send(check_global = FALSE, mob/user)
if(!sending)
return
@@ -369,7 +379,7 @@
return report
/// Prepares to sell the items on the pad
/obj/machinery/computer/piratepad_control/proc/start_sending()
/obj/machinery/computer/piratepad_control/proc/start_sending(check_global = FALSE, mob/user)
var/obj/machinery/piratepad/pad = pad_ref?.resolve()
if(!pad)
status_report = "No pad detected. Build or link a pad."
@@ -385,7 +395,10 @@
status_report = "Sending... "
pad.visible_message(span_notice("[pad] starts charging up."))
pad.icon_state = pad.warmup_state
sending_timer = addtimer(CALLBACK(src, PROC_REF(send)),warmup_time, TIMER_STOPPABLE)
sending_timer = addtimer(CALLBACK(src, PROC_REF(send), check_global, user), warmup_time, TIMER_STOPPABLE)
if(load_holding_facility)
//We ensure that the holding facility is loaded in time in case we're selling mobs.
SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NINJA_HOLDING_FACILITY)
/// Finishes the sending state of the pad
/obj/machinery/computer/piratepad_control/proc/stop_sending(custom_report)
+5
View File
@@ -17,6 +17,11 @@
return FALSE
return our_mix.gases[gas_type][MOLES] >= moles_required
/datum/bounty/item/atmospherics/contribution_amount(obj/shipped)
var/obj/item/tank/shipped_tank = shipped
var/datum/gas_mixture/our_mix = shipped_tank.return_air()
return our_mix.gases[gas_type][MOLES]
/datum/bounty/item/atmospherics/pluox_tank
name = "Full Tank of Pluoxium"
description = "CentCom RnD is researching extra compact internals. Ship us a tank full of Pluoxium and you'll be compensated. (20 Moles)"
+27 -3
View File
@@ -35,9 +35,12 @@
shipped_count += 1
return TRUE
/// If the user can actually get this bounty as a selection.
/datum/bounty/proc/can_get()
return TRUE
/datum/bounty/item/get_total()
return shipped_count
/datum/bounty/item/get_max()
return required_count
/**
* Debug item because it took less time to code this than it did to roll ONE toolbox bounty.
@@ -62,3 +65,24 @@
balloon_alert(user, "new bounty acquired!")
playsound(src, 'sound/effects/coin2.ogg', 30, TRUE)
qdel(src)
/// As above, but it spawns a global bounty for testing.
/obj/item/bounty_voucher/stationwide
name = "stationwide bounty voucher"
desc = "A certificate for ONE FREE BOUNTY of your choice! For everyone! Wowzers!"
color = "#ff8800"
/obj/item/bounty_voucher/stationwide/attack_self(mob/user, modifiers)
. = ..()
if(!isliving(user))
return
var/mob/living/living_user = user
var/choice = tgui_input_list(living_user, "Choose a bounty.", "New Bounty", subtypesof(/datum/bounty))
var/datum/bounty/new_chore = text2path("[choice]")
if(new_chore.global_exempt)
to_chat(user, span_warning("Can't use that one, try another!"))
return
GLOB.shared_crew_bounties += new_chore
balloon_alert(user, "new bounty provided to the crew!")
playsound(src, 'sound/effects/coin2.ogg', 30, TRUE)
qdel(src)
+1 -1
View File
@@ -5,7 +5,7 @@
/datum/bounty/item/mech/New()
..()
description = "Upper management has requested holodiagnostic scans of \a [name] mech be sent as soon as possible. A diagnostic holoscan can be generated from inside a new mecha. Ship it to receive a large payment."
description = "Upper management has requested holodiagnostic scans of \a [name] mech be sent as soon as possible. A <b>diagnostic holoscan</b> can be generated from inside a new mecha. Ship it to receive a large payment."
/datum/bounty/item/mech/applies_to(obj/shipped)
. = ..()
+10
View File
@@ -26,6 +26,16 @@
shipped_volume = required_volume
return TRUE
/datum/bounty/reagent/contribution_amount(obj/shipped)
return shipped.reagents.get_reagent_amount(wanted_reagent.type)
/datum/bounty/reagent/get_total()
return shipped_volume
/datum/bounty/reagent/get_max()
return required_volume
/datum/bounty/reagent/simple_drink
name = "Simple Drink"
reward = CARGO_CRATE_VALUE * 3
+1
View File
@@ -6,6 +6,7 @@
Your ID card will update you as you progress."
reward = CARGO_CRATE_VALUE * 5
allow_duplicate = TRUE
global_exempt = TRUE
/// Ref to the component applied to the ID card to track movement.
VAR_PRIVATE/datum/tracker
+7 -11
View File
@@ -1,4 +1,7 @@
/datum/bounty/item/alien_organs
/datum/bounty/item/special
unique = TRUE
/datum/bounty/item/special/alien_organs
name = "Alien Organs"
description = "Nanotrasen is interested in studying Xenomorph biology. Ship a set of organs to be thoroughly compensated."
reward = CARGO_CRATE_VALUE * 50
@@ -12,7 +15,7 @@
/obj/item/organ/eyes/alien = TRUE,
)
/datum/bounty/item/syndicate_documents
/datum/bounty/item/special/syndicate_documents
name = "Syndicate Documents"
description = "Intel regarding the syndicate is highly prized at CentCom. If you find syndicate documents, ship them. You could save lives."
reward = CARGO_CRATE_VALUE * 30
@@ -21,7 +24,7 @@
/obj/item/documents/photocopy = TRUE,
)
/datum/bounty/item/syndicate_documents/applies_to(obj/O)
/datum/bounty/item/special/syndicate_documents/applies_to(obj/O)
if(!..())
return FALSE
if(istype(O, /obj/item/documents/photocopy))
@@ -29,16 +32,9 @@
return (Copy.copy_type && ispath(Copy.copy_type, /obj/item/documents/syndicate))
return TRUE
/datum/bounty/item/adamantine
/datum/bounty/item/special/adamantine
name = "Adamantine"
description = "Nanotrasen's anomalous materials division is in desperate need of adamantine. Send them a large shipment and we'll make it worth your while."
reward = CARGO_CRATE_VALUE * 70
required_count = 10
wanted_types = list(/obj/item/stack/sheet/mineral/adamantine = TRUE)
/datum/bounty/item/trash
name = "Trash"
description = "Recently a group of janitors have run out of trash to clean up, and CentCom wants to fire them to cut costs. Send a shipment of trash to keep them employed, and they'll give you a small compensation."
reward = CARGO_CRATE_VALUE * 2
required_count = 10
wanted_types = list(/obj/item/trash = TRUE)
+35 -2
View File
@@ -1,8 +1,24 @@
// The list of all current global, shared crew bounties, does not contain any personal bounties however.
GLOBAL_LIST_EMPTY(shared_crew_bounties)
/datum/bounty
/// A name for the bounty. Displayed on the bounty console/Paper sheets.
var/name
/// A description for the bounty.
var/description
/// Whether or not the bounty has been claimed by cargo. Only applies to cargo list bounties.
var/claimed = FALSE
/// Whether or not the bounty is high priority. High priority bounties appear at the top of the list.
var/high_priority = FALSE
/// The reward for completing the bounty in credits, before being split by cargo/the player.
VAR_PROTECTED/reward = CARGO_CRATE_VALUE * 5 // In credits.
var/allow_duplicate = FALSE
/// Can this bounty be selected got a new global bounty?
var/global_exempt = FALSE
///A list consisting of the accounts who sent several of the items required for a bounty payout. Used for distributing payout with the payment component.
var/list/contribution = list()
/// Is this bounty considered unique? This is for weird, singleton bounties that we don't want to roll into randomly, and we provide one of these
var/unique = FALSE
/// Can this bounty be claimed right now?
/datum/bounty/proc/can_claim()
@@ -12,6 +28,10 @@
/datum/bounty/proc/applies_to(obj/shipped)
return FALSE
/// When calculating the contribution breakdown of an object shipped, how much does that object increase that account's cut?
/datum/bounty/proc/contribution_amount(obj/shipped)
return 1
/// Called when an object is sent on the bounty pad.
/datum/bounty/proc/ship(obj/shipped)
return
@@ -22,7 +42,8 @@
/// Returns the adjusted reward for this bounty, taking into account any global modifiers.
/datum/bounty/proc/get_bounty_reward()
return reward * SSeconomy.bounty_modifier
var/high_priority_mult = high_priority ? 1.5 : 1
return reward * SSeconomy.bounty_modifier * high_priority_mult
/// Called when this bounty is selected by the passed ID card
/datum/bounty/proc/on_selected(obj/item/card/id/id_card)
@@ -36,7 +57,19 @@
/datum/bounty/proc/on_reset(obj/item/card/id/id_card)
return
/** Returns a new bounty of random type, but does not add it to GLOB.bounties_list.
/// Proc that returns the current quantity of this bounty.
/datum/bounty/proc/get_total()
return
/// Proc that returns the current maximum quantity of this bounty.
/datum/bounty/proc/get_max()
return
/// If the user can actually get this bounty as a selection.
/datum/bounty/proc/can_get()
return TRUE
/** Returns a new bounty of random type.
*
* * Category determines what specific catagory of bounty should be chosen.
*/
+4 -3
View File
@@ -228,13 +228,14 @@
to_chat(user, span_warning("Bank account for handling tip already registered!"))
else if(scanner_account)
cube.AddComponent(/datum/component/pricetag, scanner_account, cube.handler_tip, FALSE)
cube.AddComponent(/datum/component/pricetag, list(scanner_account), cube.handler_tip, FALSE)
cube.bounty_handler_account = scanner_account
cube.bounty_handler_account.bank_card_talk("Bank account for [price ? "<b>[price * cube.handler_tip]</b> [MONEY_NAME_SINGULAR] " : ""]handling tip successfully registered.")
if(cube.bounty_holder_account != cube.bounty_handler_account) //No need to send a tracking update to the person scanning it
cube.bounty_holder_account.bank_card_talk("<b>[cube]</b> was scanned in \the <b>[get_area(cube)]</b> by <b>[scan_human] ([scan_human.job])</b>.")
for(var/datum/bank_account/shareholder in cube.bounty_holder_accounts)
if(shareholder != cube.bounty_handler_account) //No need to send a tracking update to the person scanning it
shareholder.bank_card_talk("<b>[cube]</b> was scanned in \the <b>[get_area(cube)]</b> by <b>[scan_human] ([scan_human.job])</b>.")
else
to_chat(user, span_warning("Bank account not detected. Handling tip not registered."))
+2 -2
View File
@@ -159,7 +159,7 @@
for(var/obj/wrapped_item in get_all_contents())
if(HAS_TRAIT(wrapped_item, TRAIT_NO_BARCODES))
continue
wrapped_item.AddComponent(/datum/component/pricetag, sticker.payments_acc, sales_tagger.cut_multiplier)
wrapped_item.AddComponent(/datum/component/pricetag, list(sticker.payments_acc), sales_tagger.cut_multiplier)
update_appearance()
else if(istype(item, /obj/item/barcode))
@@ -177,7 +177,7 @@
for(var/obj/wrapped_item in get_all_contents())
if(HAS_TRAIT(wrapped_item, TRAIT_NO_BARCODES))
continue
wrapped_item.AddComponent(/datum/component/pricetag, sticker.payments_acc, sticker.cut_multiplier)
wrapped_item.AddComponent(/datum/component/pricetag, list(sticker.payments_acc), sticker.cut_multiplier)
update_appearance()
else if(istype(item, /obj/item/boxcutter))