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 += "

Nanotrasen Cargo Bounties


" + + for(var/datum/bounty/current_bounty in GLOB.shared_crew_bounties) + if(current_bounty.claimed) + continue + printout_text += {"

[current_bounty.name]

+ "} + paper.add_raw_text(printout_text.Join("
")) + paper.update_appearance() + + playsound(src, 'sound/machines/printer.ogg', 100, TRUE) + COOLDOWN_START(src, sheet_printer_cooldown, 4 SECONDS) + return TRUE + +///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-- + + +#undef CIV_BOUNTY_SPLIT +#undef HIGH_PRIORITY_BOUNTY_ODDS diff --git a/code/game/machinery/civilian_bounty/civilian_bounty_trims.dm b/code/game/machinery/civilian_bounty/civilian_bounty_trims.dm new file mode 100644 index 00000000000..6c3c7221de6 --- /dev/null +++ b/code/game/machinery/civilian_bounty/civilian_bounty_trims.dm @@ -0,0 +1,92 @@ + +/** + * 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 diff --git a/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm b/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm index 4cc06387530..a5dded9c71f 100644 --- a/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm +++ b/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm @@ -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) diff --git a/code/modules/cargo/bounties/atmos.dm b/code/modules/cargo/bounties/atmos.dm index 16f52c459ac..3edc4f9665f 100644 --- a/code/modules/cargo/bounties/atmos.dm +++ b/code/modules/cargo/bounties/atmos.dm @@ -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)" diff --git a/code/modules/cargo/bounties/item.dm b/code/modules/cargo/bounties/item.dm index 0970cf1ad40..2ef07ded5bb 100644 --- a/code/modules/cargo/bounties/item.dm +++ b/code/modules/cargo/bounties/item.dm @@ -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) diff --git a/code/modules/cargo/bounties/mecha.dm b/code/modules/cargo/bounties/mecha.dm index 12f9a165ecd..a6a898b3124 100644 --- a/code/modules/cargo/bounties/mecha.dm +++ b/code/modules/cargo/bounties/mecha.dm @@ -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 diagnostic holoscan can be generated from inside a new mecha. Ship it to receive a large payment." /datum/bounty/item/mech/applies_to(obj/shipped) . = ..() diff --git a/code/modules/cargo/bounties/reagent.dm b/code/modules/cargo/bounties/reagent.dm index e3706407fc5..0bbac3dacc1 100644 --- a/code/modules/cargo/bounties/reagent.dm +++ b/code/modules/cargo/bounties/reagent.dm @@ -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 diff --git a/code/modules/cargo/bounties/security.dm b/code/modules/cargo/bounties/security.dm index 05ca109d80d..f94c1973cbb 100644 --- a/code/modules/cargo/bounties/security.dm +++ b/code/modules/cargo/bounties/security.dm @@ -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 diff --git a/code/modules/cargo/bounties/special.dm b/code/modules/cargo/bounties/special.dm index 08a2a117f26..8255ba6f2f3 100644 --- a/code/modules/cargo/bounties/special.dm +++ b/code/modules/cargo/bounties/special.dm @@ -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) diff --git a/code/modules/cargo/bounty.dm b/code/modules/cargo/bounty.dm index 8b3b0a38629..493ff8c64aa 100644 --- a/code/modules/cargo/bounty.dm +++ b/code/modules/cargo/bounty.dm @@ -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. */ diff --git a/code/modules/cargo/universal_scanner.dm b/code/modules/cargo/universal_scanner.dm index 9668c4c0bea..ed98427ea16 100644 --- a/code/modules/cargo/universal_scanner.dm +++ b/code/modules/cargo/universal_scanner.dm @@ -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 ? "[price * cube.handler_tip] [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("[cube] was scanned in \the [get_area(cube)] by [scan_human] ([scan_human.job]).") + 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("[cube] was scanned in \the [get_area(cube)] by [scan_human] ([scan_human.job]).") else to_chat(user, span_warning("Bank account not detected. Handling tip not registered.")) diff --git a/code/modules/recycling/sortingmachinery.dm b/code/modules/recycling/sortingmachinery.dm index 8a1b9abb9b8..997bc6540bc 100644 --- a/code/modules/recycling/sortingmachinery.dm +++ b/code/modules/recycling/sortingmachinery.dm @@ -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)) diff --git a/sound/attributions.txt b/sound/attributions.txt index 1de71db183f..ade4c8fe7e7 100644 --- a/sound/attributions.txt +++ b/sound/attributions.txt @@ -245,3 +245,6 @@ sound/music/antag/blobalert.ogg and sound/effects/blob/blobattack.ogg -- "SlimeA sound/effect/droplet.ogg -- "Drop - Water" By mattfinarelli -- https://freesound.org/people/mattfinarelli/sounds/533146/ -- License: Creative Commons 0 sound/effects/swapper/swap_A.ogg and swap_B.ogg by ArcaneMusic, License: Creative Commons 0, man-made mouth noises, slight cleanup in audacity. + +sound/machines/data_transmission.ogg -- Elements from Tim Verberne -- https://freesound.org/people/Tim_Verberne/sounds/558942/ -- License: Creative Commons 0 + and samples from Ping.ogg from throughout the codebase. diff --git a/sound/machines/data_transmission.ogg b/sound/machines/data_transmission.ogg new file mode 100644 index 00000000000..d5261f27ba2 Binary files /dev/null and b/sound/machines/data_transmission.ogg differ diff --git a/tgstation.dme b/tgstation.dme index 8d6ff832949..3ad492b29de 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2239,7 +2239,6 @@ #include "code\game\machinery\botlaunchpad.dm" #include "code\game\machinery\buttons.dm" #include "code\game\machinery\cell_charger.dm" -#include "code\game\machinery\civilian_bounties.dm" #include "code\game\machinery\constructable_frame.dm" #include "code\game\machinery\dance_machine.dm" #include "code\game\machinery\defibrillator_mount.dm" @@ -2318,6 +2317,9 @@ #include "code\game\machinery\camera\presets.dm" #include "code\game\machinery\camera\silicon_camera.dm" #include "code\game\machinery\camera\trackable.dm" +#include "code\game\machinery\civilian_bounty\bounty_cube.dm" +#include "code\game\machinery\civilian_bounty\bounty_machinery.dm" +#include "code\game\machinery\civilian_bounty\civilian_bounty_trims.dm" #include "code\game\machinery\computer\_computer.dm" #include "code\game\machinery\computer\accounting.dm" #include "code\game\machinery\computer\aifixer.dm" diff --git a/tgui/packages/tgui/interfaces/CivCargoHoldTerminal.jsx b/tgui/packages/tgui/interfaces/CivCargoHoldTerminal.jsx deleted file mode 100644 index 90659845d64..00000000000 --- a/tgui/packages/tgui/interfaces/CivCargoHoldTerminal.jsx +++ /dev/null @@ -1,163 +0,0 @@ -import { - Box, - Button, - Flex, - LabeledList, - NoticeBox, - Section, -} from 'tgui-core/components'; - -import { useBackend } from '../backend'; -import { Window } from '../layouts'; - -export const CivCargoHoldTerminal = (props) => { - const { act, data } = useBackend(); - const { pad, sending, status_report, id_inserted, id_bounty_info, picking } = - data; - const in_text = 'Welcome valued employee.'; - const out_text = 'To begin, insert your ID into the console.'; - return ( - - - - - - {id_inserted ? in_text : out_text} - -
-
- {picking ? : } -
-
-
-
- ); -}; - -const BountyTextBox = (props) => { - const { data } = useBackend(); - const { id_bounty_info, id_bounty_value, id_bounty_num } = data; - const na_text = 'N/A, please add a new bounty.'; - return ( -
- - - {id_bounty_info ? id_bounty_info : na_text} - - - {id_bounty_info ? id_bounty_num : 'N/A'} - - - {id_bounty_info ? id_bounty_value : 'N/A'} - - -
- ); -}; - -const BountyPickBox = (props) => { - const { act, data } = useBackend(); - const { id_bounty_names, id_bounty_infos, id_bounty_values } = data; - return ( -
- - - - - - - - - - - -
- ); -}; - -const BountyPickButton = (props) => { - return ( - - ); -}; diff --git a/tgui/packages/tgui/interfaces/CivCargoHoldTerminal.tsx b/tgui/packages/tgui/interfaces/CivCargoHoldTerminal.tsx new file mode 100644 index 00000000000..c0f1360798f --- /dev/null +++ b/tgui/packages/tgui/interfaces/CivCargoHoldTerminal.tsx @@ -0,0 +1,391 @@ + +import { + Box, + Button, + Flex, + LabeledList, + NoticeBox, + ProgressBar, + Section, + Stack, + Tabs, + Tooltip +} from 'tgui-core/components'; +import type { BooleanLike } from 'tgui-core/react'; + +import { useState } from 'react'; +import { useBackend } from '../backend'; +import { Window } from '../layouts'; + + +// Main window content. + +type Data = { + pad: string; + sending: BooleanLike; + status_report: string; + picking: BooleanLike; + + id_inserted: BooleanLike; + id_bounty_info: string; + id_bounty_value: number; + id_bounty_num: number; + + id_bounty_names: string[]; + id_bounty_infos: string[]; + id_bounty_values: number[]; + + listBounty: singleBounty[]; + claimed_bounties: number; +}; + +type singleBounty = { + name: string; + description: string; + reward: number; + shipped: number; + claimed: BooleanLike; + maximum: number; + priority: BooleanLike; + unique: BooleanLike; +} + + +export const CivCargoHoldTerminal = (props) => { + const { act, data } = useBackend(); + const { id_inserted } = data; + + const in_text = 'Welcome valued employee.'; + const out_text = 'To begin, insert your ID into the console.'; + const [tab, setTab] = useState('personal'); + const listBounties = data.listBounty || []; + + return ( + + + + +
+ + + setTab('personal')} + selected={tab === 'personal'} + backgroundColor={tab === 'personal' ? "green" : "default"} + > + Personal Bounties + + setTab('station')} + selected={tab === 'station'} + backgroundColor={tab === 'station' ? "brown" : "default"} + > + Station Bounties + + +
+ + + {id_inserted ? in_text : out_text} + + + {tab === 'personal' ? + + : + + } +
+
+
+
+ ); +}; + +// Block for the personal bounty information. +const PersonalBountyBlock = (props) => { + const { act, data } = useBackend(); + const { pad, sending, status_report, id_inserted, id_bounty_info, picking } = data; + return ( + <> +
+
+ {picking ? : } + + ); +}; + + +const BountyTextBox = (props) => { + const { data } = useBackend(); + const { id_bounty_info, id_bounty_value, id_bounty_num } = data; + const na_text = 'N/A, please add a new bounty.'; + return ( +
+ + + {id_bounty_info ? id_bounty_info : na_text} + + + {id_bounty_info ? id_bounty_num : 'N/A'} + + + {id_bounty_info ? id_bounty_value : 'N/A'} + + +
+ ); +}; + + +const BountyPickBox = (props) => { + const { act, data } = useBackend(); + const { id_bounty_names, id_bounty_infos, id_bounty_values } = data; + return ( +
+ + + + + + + + + + + +
+ ); +}; + +const BountyPickButton = (props) => { + return ( + + ); +}; + +const GlobalBountyBlock = (props) => { + const { act, data } = useBackend(); + const { + listBounty = [], + sending, + pad, + id_inserted, + } = data; + + const [localBounty, setBountyData] = useState({ + name: 'n/a', + description: '', + reward: 0, + shipped: 0, + claimed: false, + maximum: 0, + priority: false, + unique: false, + }); + + const [bountyTab, setBountyTab] = useState(0); + + const safeListBounty = Array.isArray(listBounty) ? listBounty : []; + return ( + + + + {safeListBounty.length < 1 ? ( + { act('update_list'); setBountyTab(0); }} + backgroundColor="blue" + textColor="white" + width="100%" + bold + icon="refresh" + > + Update List + + ) : ( + + + + {data.claimed_bounties} bount{data.claimed_bounties === 1 ? "y" : "ies"} served{data.claimed_bounties > 0 ? "!" : "."} + + + + )} + + act('print')} + backgroundColor="#ffffff70" + textColor="white" + width="100%" + bold + icon="print" + > + Printout List + + {safeListBounty.map((bounty) => ( + { setBountyData(bounty); setBountyTab(safeListBounty.indexOf(bounty)); }} + className="Tab_Flash" + bold={bountyTab === safeListBounty.indexOf(bounty)} + icon={bounty.priority ? 'star' : (bounty.unique ? 'gem' : '')} + selected={bountyTab === safeListBounty.indexOf(bounty)} + > + {bounty.name} + + ))} + + + + {localBounty.reward !== 0 ? ( +
+ + {localBounty.shipped} / {localBounty.maximum} shipped. + + +
+ + + Reward: {localBounty.reward} Credits + + + +
+ ) : ( + + Please select a bounty from the list. + + )} +
+
+ ) +} diff --git a/tgui/packages/tgui/styles/interfaces/CivCargoHoldTerminal.scss b/tgui/packages/tgui/styles/interfaces/CivCargoHoldTerminal.scss new file mode 100644 index 00000000000..c3be10fe06e --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/CivCargoHoldTerminal.scss @@ -0,0 +1,23 @@ +.Tab_Flash { + box-shadow: 0 0 1px 2px #48abe0; + animation: shadowFade 0.5s forwards; +} + +@keyframes shadowFade { + to { + box-shadow: 0 0 0 0 #48abe0; + } +} + +.Marquee { + animation: marquee 5s linear infinite; +} + +@keyframes marquee { + 0% { + transform: translateX(100%); + } + 100% { + transform: translateX(-100%); + } +} diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index 5764ef03d2b..d61fc9fc2c5 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -49,6 +49,7 @@ @include meta.load-css('./interfaces/DetectiveBoard.scss'); @include meta.load-css('./interfaces/ColorPicker.scss'); @include meta.load-css('./interfaces/AnomalyTower.scss'); +@include meta.load-css('./interfaces/CivCargoHoldTerminal.scss'); // Layouts @include meta.load-css('./layouts/Layout.scss');