Files
Bubberstation/code/modules/cargo/order.dm
ArcaneMusic 43e62163fe Adds a Contraband trait, and implements contraband as a mechanic to security bounties. (#84003)
## About The Pull Request

This PR does a few things but centrally it's all centered around
mechanically enforcing what items are and are-not considered contraband
in-game.

### What does something being contraband MEAN?

Contraband items are visually indistinguishable from non-contraband. If
an item is Contraband, it can only be detected in two ways:
* After being scanned by an N-Spect scanner, which is a standard item
security item, assuming it still has a charge to do so.
* Via a scanner gate, which can now be upgraded with an N-spect scanner
to allow for it to scan a person and all their contents for contraband.

### What items ARE contraband?

Contraband items are intended to be determined both logically and
through other relevant examine text. However, here's the short list of
items that are considered contraband, reserving the right to expand the
list.

<details>
  <summary>In hindsight it's kind of a long list.</summary>
  
* Items that have "contraband" or "illegal" in the name or description.
* Items that allow for the player to obtain other illegal items, that
are NOT particularly stealthy.
* This means that a syndicate uplink is NOT considered contraband, as
they're typically hidden on your person as something else.
* Stealth items under the syndicate uplink, the revolutionary flash, and
some mapped in dangerous items that can come from both syndicate and
company-aligned resources are not considered dangerous.
* Items that are purchased from cargo after emagging or switching to
extended cargo range.
* Items purchased FROM syndicate uplinks, the wizard knowledge scroll,
or other antagonist shops.
* Cursed artifacts/tools magically produced by cultists or heretics.
* Items purchased from the blackmarket.
* Items purchased from the contraband section of vending machines.
* Some drugs and overtly dangerous or criminal byproducts.
  
</details>

### How does this interact with the round?
Well, primarily, this is an aid for in-game enforcement of space law.
Based on the length of the above list, we have a LONG, LONG list of
items in-game that are technically considered, in one way or another,
illegal to have on the station, and yet without either metaknowledge of
what those items are, or how they're used, security officers lack some
of the certainty of how to deal with these kinds of encounters.

Additionally to the knowledge aspect of this trait, security officers
may now receive a new civilian bounty to collect items that are
considered contraband, also giving them an incentive to look for and
confiscate contraband that's been found across the station while
upholding space law.

### Other minor changes that I rolled into this

Security has a bounty for 3 different rechargers, and considering access
limitations, most security players aren't going to make this exchange,
so I've lowered the required amount down to 1.

Adjusted the N-spect scanner's description to match it's new
functionality.

The Civilian bounty TGUI now has an additional 1 point of padding to
make it feel less cramped.



https://github.com/tgstation/tgstation/assets/41715314/c3cd4752-b03a-4e0b-959e-1252fcc2369d

**Updated as of 6/19/2024:**
Additionally, some storage items will block the presence of contraband
when going through a contraband aligned scanning gate. These items
include the infiltrator modsuit core, storage implant, void cloak, the
aptly named smuggler's satchel, and the chameleon kit's backpack.

**Updated as of 6/23/2024:**
N-spect scanner now has contextual screentips.

**Updated as of 6/29/2024:**
Scanner gates are now available in all lathes that have a feature
specific to how scanner gates function. So, includes cargo (contraband),
security (weapons), and medbay (diseases).

## Why It's Good For The Game

Originally, this started out as a way to be able to provide more
in-character and in-flavor bounties for security officers, because they
suck! Most security bounties as they exist right now do the worst
possible things from all respective bounties:
* They detract away from a job's actual responsibilities as opposed to
working with them.
* They're best completed while sitting next to your lathe and running
items back to the bounty pad.
* They exist with such esoteric rarity of high quantity of items that
it's miserable to fulfil.

As a result, I started work on this as a framework to allow security
officers to be further incentivized to collect contraband across the
station, either as a result of the gamemode or just through routine
patrols across the station.

Implementing it as a learning tool for security as well just happened to
work out as an additional bonus, and having a function in-game allowing
newer or less experienced players to know if an item is considered
dangerous or conspicuous also works as a particularly good way to
provide information where a player may not know what they're up against.

If nothing else, this might be interesting to try, and if not, I'll just
snip out the QOL changes from it and we'll see how it goes.

Going forward, I am a bit hesitant about the contraband scanner gate
mode, and as such, will try working with the admin team to determine if
that's a good feature to keep around for game health, while hoping to
give it a chance in the fullness of time.

## Changelog

🆑
add: Items spawned via traitor uplinks or are known illegal contraband
on the station can now be scanned and identified as such by the N-spect
scanners in security. These only applies to overt traitor or antagonist
items, and "stealth" items will not be seen as such.
add: Scanner gates can now be upgraded by using an N-spect scanner on it
to unlock "contraband scanning" mode.
add: Security officers can now be offered a bounty to turn in pieces of
contraband.
add: Some stealthy storage items like storage implants, smuggler's
satchels, void cloaks, the infiltrator modsuit, and the chameleon
backpack will block the presence of contraband on your person when
placed inside.
qol: N-spect scanner contextual screentips.
balance: Recharger security bounties ask for a quantity of 1, down from
3.
qol: security, cargo, and medbay have access to scanner gate boards.
/🆑

---------

Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
2024-07-11 03:08:25 +02:00

228 lines
8.3 KiB
Plaintext

/// The chance for a manifest or crate to be created with errors
#define MANIFEST_ERROR_CHANCE 5
// MANIFEST BITFLAGS
/// Determines if the station name will be incorrect on the manifest
#define MANIFEST_ERROR_NAME (1 << 0)
/// Determines if contents will be deleted from the manifest but still be present in the crate
#define MANIFEST_ERROR_CONTENTS (1 << 1)
/// Determines if contents will be deleted from the crate but still be present in the manifest
#define MANIFEST_ERROR_ITEM (1 << 2)
/obj/item/paper/fluff/jobs/cargo/manifest
var/order_cost = 0
var/order_id = 0
var/errors = 0
/obj/item/paper/fluff/jobs/cargo/manifest/Initialize(mapload, id, cost, manifest_can_fail = TRUE)
. = ..()
order_id = id
order_cost = cost
if(!manifest_can_fail)
return
if(prob(MANIFEST_ERROR_CHANCE) && (world.time-SSticker.round_start_time > STATION_RENAME_TIME_LIMIT)) //Too confusing if station name gets changed
errors |= MANIFEST_ERROR_NAME
investigate_log("Supply order #[order_id] generated a manifest with an incorrect station name.", INVESTIGATE_CARGO)
if(prob(MANIFEST_ERROR_CHANCE))
errors |= MANIFEST_ERROR_CONTENTS
investigate_log("Supply order #[order_id] generated a manifest missing listed contents.", INVESTIGATE_CARGO)
else if(prob(MANIFEST_ERROR_CHANCE)) //Content and item errors could remove the same items, so only one at a time
errors |= MANIFEST_ERROR_ITEM
investigate_log("Supply order #[order_id] generated with incorrect contents shipped.", INVESTIGATE_CARGO)
/obj/item/paper/fluff/jobs/cargo/manifest/proc/is_approved()
return LAZYLEN(stamp_cache) && !is_denied()
/obj/item/paper/fluff/jobs/cargo/manifest/proc/is_denied()
return LAZYLEN(stamp_cache) && ("stamp-deny" in stamp_cache)
/datum/supply_order
var/id
var/cost_type
var/orderer
var/orderer_rank
var/orderer_ckey
var/reason
var/discounted_pct
///If set to FALSE, we won't charge when the cargo shuttle arrives with this.
var/charge_on_purchase = TRUE
///area this order wants to reach, if not null then it will come with the deliver_first component set to this area
var/department_destination
var/datum/supply_pack/pack
var/datum/bank_account/paying_account
var/obj/item/coupon/applied_coupon
///Boolean on whether the manifest can fail or not.
var/manifest_can_fail = TRUE
///Boolean on whether the manifest can be cancelled through cargo consoles.
var/can_be_cancelled = TRUE
/datum/supply_order/New(
datum/supply_pack/pack,
orderer,
orderer_rank,
orderer_ckey,
reason,
paying_account,
department_destination,
coupon,
charge_on_purchase = TRUE,
manifest_can_fail = TRUE,
cost_type = "cr",
can_be_cancelled = TRUE,
)
id = SSshuttle.order_number++
src.cost_type = cost_type
src.pack = pack
src.orderer = orderer
src.orderer_rank = orderer_rank
src.orderer_ckey = orderer_ckey
src.reason = reason
src.paying_account = paying_account
src.department_destination = department_destination
src.applied_coupon = coupon
src.charge_on_purchase = charge_on_purchase
src.manifest_can_fail = manifest_can_fail
src.can_be_cancelled = can_be_cancelled
/datum/supply_order/Destroy(force)
QDEL_NULL(applied_coupon)
return ..()
//returns the total cost of this order. Its not the total price paid by cargo but the total value of this order
/datum/supply_order/proc/get_final_cost()
var/cost = pack.get_cost()
if(applied_coupon) //apply discount price
cost *= (1 - applied_coupon.discount_pct_off)
if(paying_account && !pack.goody) //privately purchased and not a goody means 1.1x the cost
cost *= 1.1
return round(cost)
/datum/supply_order/proc/generateRequisition(turf/T)
var/obj/item/paper/requisition_paper = new(T)
requisition_paper.name = "requisition form - #[id] ([pack.name])"
var/requisition_text = "<h2>[station_name()] Supply Requisition</h2>"
requisition_text += "<hr/>"
requisition_text += "Order #[id]<br/>"
requisition_text+= "Time of Order: [station_time_timestamp()]<br/>"
requisition_text += "Item: [pack.name]<br/>"
requisition_text += "Access Restrictions: [SSid_access.get_access_desc(pack.access)]<br/>"
requisition_text += "Requested by: [orderer]<br/>"
if(paying_account)
requisition_text += "Paid by: [paying_account.account_holder]<br/>"
requisition_text += "Rank: [orderer_rank]<br/>"
requisition_text += "Comment: [reason]<br/>"
requisition_paper.add_raw_text(requisition_text)
requisition_paper.update_appearance()
return requisition_paper
/datum/supply_order/proc/generateManifest(obj/container, owner, packname, cost) //generates-the-manifests.
var/obj/item/paper/fluff/jobs/cargo/manifest/manifest_paper = new(null, id, cost, manifest_can_fail)
var/station_name = (manifest_paper.errors & MANIFEST_ERROR_NAME) ? new_station_name() : station_name()
manifest_paper.name = "shipping manifest - [packname?"#[id] ([pack.name])":"(Grouped Item Crate)"]"
var/manifest_text = "<h2>[command_name()] Shipping Manifest</h2>"
manifest_text += "<hr/>"
if(owner && !(owner == "Cargo"))
manifest_text += "Direct purchase from [owner]<br/>"
manifest_paper.name += " - Purchased by [owner]"
manifest_text += "Order[packname?"":"s"]: [id]<br/>"
manifest_text += "Destination: [station_name]<br/>"
if(packname)
manifest_text += "Item: [packname]<br/>"
manifest_text += "Contents: <br/>"
manifest_text += "<ul>"
var/container_contents = list() // Associative list with the format (item_name = nº of occurences, ...)
for(var/atom/movable/AM in container.contents - manifest_paper)
container_contents[AM.name]++
if((manifest_paper.errors & MANIFEST_ERROR_CONTENTS) && container_contents)
if(HAS_TRAIT(container, TRAIT_NO_MANIFEST_CONTENTS_ERROR))
manifest_paper.errors &= ~MANIFEST_ERROR_CONTENTS
else
for(var/iteration in 1 to rand(1, round(container.contents.len * 0.5))) // Remove anywhere from one to half of the items
var/missing_item = pick(container_contents)
container_contents[missing_item]--
if(container_contents[missing_item] == 0) // To avoid 0s and negative values on the manifest
container_contents -= missing_item
for(var/item in container_contents)
manifest_text += "<li> [container_contents[item]] [item][container_contents[item] == 1 ? "" : "s"]</li>"
manifest_text += "</ul>"
manifest_text += "<h4>Stamp below to confirm receipt of goods:</h4>"
manifest_paper.add_raw_text(manifest_text)
if(manifest_paper.errors & MANIFEST_ERROR_ITEM)
if(HAS_TRAIT(container, TRAIT_NO_MISSING_ITEM_ERROR))
manifest_paper.errors &= ~MANIFEST_ERROR_ITEM
else
var/lost = max(round(container.contents.len / 10), 1)
while(--lost >= 0)
qdel(pick(container.contents))
manifest_paper.update_appearance()
manifest_paper.forceMove(container)
if(istype(container, /obj/structure/closet/crate))
var/obj/structure/closet/crate/C = container
C.manifest = manifest_paper
C.update_appearance()
else
container.contents += manifest_paper
return manifest_paper
/datum/supply_order/proc/generate(atom/A)
var/account_holder
if(paying_account)
account_holder = paying_account.account_holder
else
account_holder = "Cargo"
var/obj/structure/closet/crate/crate = pack.generate(A, paying_account)
if(pack.contraband)
for(var/atom/movable/item_within as anything in crate.get_all_contents())
ADD_TRAIT(item_within, TRAIT_CONTRABAND, INNATE_TRAIT)
if(department_destination)
crate.AddElement(/datum/element/deliver_first, department_destination, pack.cost)
generateManifest(crate, account_holder, pack, pack.cost)
return crate
/datum/supply_order/proc/generateCombo(miscbox, misc_own, misc_contents, misc_cost)
for (var/I in misc_contents)
new I(miscbox)
generateManifest(miscbox, misc_own, "", misc_cost)
return
/datum/supply_order/proc/append_order(list/new_contents, cost_increase)
for(var/i as anything in new_contents)
if(pack.contains[i])
pack.contains[i] += new_contents[i]
else
pack.contains += i
pack.contains[i] = new_contents[i]
pack.cost += cost_increase
/// Custom type of order who's supply pack can be safely deleted
/datum/supply_order/disposable
/datum/supply_order/disposable/Destroy(force)
QDEL_NULL(pack)
return ..()
/// Custom material order to append cargo crate value to the final order cost
/datum/supply_order/disposable/materials
/datum/supply_order/disposable/materials/get_final_cost()
return (..() + CARGO_CRATE_VALUE)
#undef MANIFEST_ERROR_CHANCE
#undef MANIFEST_ERROR_NAME
#undef MANIFEST_ERROR_CONTENTS
#undef MANIFEST_ERROR_ITEM