The Coupon Master PDA app (#80240)

## About The Pull Request
This PR adds a new PDA program to the supply category, which allows
users to redeems coupons for various cargo packs (mostly goodies), like
the ones also found at the bottom of cig packs.

How it works is fairly simple: 

- Once installed, the modular computer subsystem will periodically, at a
3-5 minutes interval, generate a coupon code datum associated to a plain
text code, which is sent out to everyone with the program installed.
- The user can then open the program and insert the text into an input
box to redeem the coupon code, which is then associated with their bank
account.
- He will then have to find a photocopier, and tap it with the PDA to
print the coupon. Only one coupon can be printed. Photocopier fees
apply, so it'd cost 5 creds to the average assistant to print the
coupon.
- He can then insert the coupon in a cargo console and order/reuest the
associated pack (same deal as cig coupons).
- Some coupon codes however, especially those with juicer discounts,
will expire after a while if not printed.

Albeit mostly innocuous, the program provides negative Detomatix
resistance, slowly fills the computer file storage with trash files with
each redeemed coupon, and halves the download speed of new apps. Not
really the cleanest ware out there.

This PR also extends coupons to several non-goody packs, since they have
been privately buyable for over the last couple years now. Some packs
get discounts less frequently however, with those in the uncommon
category being roughly 1 in a 12 chance and the rare being 1 in 50.

Here's a screenshot of the UI (outdated, I've reduced the height from
500 to 400 and the notice box tip to specify the right click):
![Coupon
Master](https://github.com/tgstation/tgstation/assets/42542238/1d242d09-0f62-4e2e-8a6e-014daa3f6a55)

Fun fact: Right now, the odds of a 75% discount coming from the Coupon
Master for the 1.000.000 credits bycycle pack are roughly 0.0012%, while
that of a 50% for the same pack, from a cig pack coupon are 0.0042%.

## Why It's Good For The Game
These last couple days I've been wanting to test myself at making simple
UIs, as well as contributing to the modular computers feature, which has
started to become pretty neat ever since PDAs were reworked into a
subtype of it.

Beside, coupons are a very small feature limited to the bottom of
cigarette packs (also possibly cursed) in the current state of affairs.
Cargo is filled with packs that are niche or fluff. Modular computers
also has those little things that, while interesting, do not contribute
a whole lot. Maybe this is one of them, but I guess free* coupons are
always a big W.

## Changelog

🆑
add: Added the 'Coupon Master' program for the PDA. Install it to
receive periodical, redeemable coupons for several cargo packs. Requires
NTnet connection and the messenger enabled to work.
add: Coupons are no longer only limited to goodies, but may also apply
discount to some other packs as well.
/🆑

<sup>*minus the photocopier fee</sup>

---------

Co-authored-by: Watermelon914 <37270891+Watermelon914@users.noreply.github.com>
This commit is contained in:
Ghom
2023-12-25 20:19:56 +01:00
committed by GitHub
parent 85595e5777
commit fec7ccc6fd
26 changed files with 414 additions and 34 deletions

View File

@@ -53,21 +53,14 @@
/// Universal Scanner mode for using the price tagger.
#define SCAN_PRICE_TAG 3
GLOBAL_LIST_EMPTY(supplypod_loading_bays)
///Used by coupons to define that they're cursed
#define COUPON_OMEN "omen"
GLOBAL_LIST_INIT(podstyles, list(\
list(POD_SHAPE_NORML, "pod", TRUE, "default", "yellow", RUBBLE_NORMAL, "supply pod", "A Nanotrasen supply drop pod."),\
list(POD_SHAPE_NORML, "advpod", TRUE, "bluespace", "blue", RUBBLE_NORMAL, "bluespace supply pod" , "A Nanotrasen Bluespace supply pod. Teleports back to CentCom after delivery."),\
list(POD_SHAPE_NORML, "advpod", TRUE, "centcom", "blue", RUBBLE_NORMAL, "\improper CentCom supply pod", "A Nanotrasen supply pod, this one has been marked with Central Command's designations. Teleports back to CentCom after delivery."),\
list(POD_SHAPE_NORML, "darkpod", TRUE, "syndicate", "red", RUBBLE_NORMAL, "blood-red supply pod", "An intimidating supply pod, covered in the blood-red markings of the Syndicate. It's probably best to stand back from this."),\
list(POD_SHAPE_NORML, "darkpod", TRUE, "deathsquad", "blue", RUBBLE_NORMAL, "\improper Deathsquad drop pod", "A Nanotrasen drop pod. This one has been marked the markings of Nanotrasen's elite strike team."),\
list(POD_SHAPE_NORML, "pod", TRUE, "cultist", "red", RUBBLE_NORMAL, "bloody supply pod", "A Nanotrasen supply pod covered in scratch-marks, blood, and strange runes."),\
list(POD_SHAPE_OTHER, "missile", FALSE, FALSE, FALSE, RUBBLE_THIN, "cruise missile", "A big ass missile that didn't seem to fully detonate. It was likely launched from some far-off deep space missile silo. There appears to be an auxillery payload hatch on the side, though manually opening it is likely impossible."),\
list(POD_SHAPE_OTHER, "smissile", FALSE, FALSE, FALSE, RUBBLE_THIN, "\improper Syndicate cruise missile", "A big ass, blood-red missile that didn't seem to fully detonate. It was likely launched from some deep space Syndicate missile silo. There appears to be an auxillery payload hatch on the side, though manually opening it is likely impossible."),\
list(POD_SHAPE_OTHER, "box", TRUE, FALSE, FALSE, RUBBLE_WIDE, "\improper Aussec supply crate", "An incredibly sturdy supply crate, designed to withstand orbital re-entry. Has 'Aussec Armory - 2532' engraved on the side."),\
list(POD_SHAPE_NORML, "clownpod", TRUE, "clown", "green", RUBBLE_NORMAL, "\improper HONK pod", "A brightly-colored supply pod. It likely originated from the Clown Federation."),\
list(POD_SHAPE_OTHER, "orange", TRUE, FALSE, FALSE, RUBBLE_NONE, "\improper Orange", "An angry orange."),\
list(POD_SHAPE_OTHER, FALSE, FALSE, FALSE, FALSE, RUBBLE_NONE, "\improper S.T.E.A.L.T.H. pod MKVII", "A supply pod that, under normal circumstances, is completely invisible to conventional methods of detection. How are you even seeing this?"),\
list(POD_SHAPE_OTHER, "gondola", FALSE, FALSE, FALSE, RUBBLE_NONE, "gondola", "The silent walker. This one seems to be part of a delivery agency."),\
list(POD_SHAPE_OTHER, FALSE, FALSE, FALSE, FALSE, RUBBLE_NONE, FALSE, FALSE, "rl_click", "give_po")\
))
///Discount categories for coupons. This one is for anything that isn't discountable.
#define SUPPLY_PACK_NOT_DISCOUNTABLE null
///Discount category for the standard stuff, mostly goodies.
#define SUPPLY_PACK_STD_DISCOUNTABLE "standard_discount"
///Discount category for stuff that's mostly niche and/or that might be useful.
#define SUPPLY_PACK_UNCOMMON_DISCOUNTABLE "uncommon_discount"
///Discount category for the silly, overpriced, joke content, sometimes useful or plain bad.
#define SUPPLY_PACK_RARE_DISCOUNTABLE "rare_discount"

View File

@@ -47,6 +47,8 @@
///This app grants a larger protection against being PDA bombed if installed.
///(can sometimes prevent it from being sent, while wasting a PDA bomb from the sender).
#define DETOMATIX_RESIST_MAJOR 2
///This app gives a diminished protection against being PDA bombed if installed.
#define DETOMATIX_RESIST_MALUS -4
/**
* NTNet transfer speeds, used when downloading/uploading a file/program.

View File

@@ -782,10 +782,12 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Trait applied when an integrated circuit opens a UI on a player (see list pick component)
#define TRAIT_CIRCUIT_UI_OPEN "circuit_ui_open"
/// PDA Traits. This one makes PDAs explode if the user opens the messages menu
/// PDA/ModPC Traits. This one makes PDAs explode if the user opens the messages menu
#define TRAIT_PDA_MESSAGE_MENU_RIGGED "pda_message_menu_rigged"
/// This one denotes a PDA has received a rigged message and will explode when the user tries to reply to a rigged PDA message
#define TRAIT_PDA_CAN_EXPLODE "pda_can_explode"
///The download speeds of programs from the dowloader is halved.
#define TRAIT_MODPC_HALVED_DOWNLOAD_SPEED "modpc_halved_download_speed"
/// If present on a [/mob/living/carbon], will make them appear to have a medium level disease on health HUDs.
#define TRAIT_DISEASELIKE_SEVERITY_MEDIUM "diseaselike_severity_medium"

View File

@@ -0,0 +1,34 @@
GLOBAL_LIST_INIT(discountable_packs, init_discountable_packs())
/proc/init_discountable_packs()
var/list/packs = list()
for(var/datum/supply_pack/prototype as anything in subtypesof(/datum/supply_pack))
var/discountable = initial(prototype.discountable)
if(discountable)
LAZYADD(packs[discountable], prototype)
return packs
GLOBAL_LIST_INIT(pack_discount_odds, list(
SUPPLY_PACK_STD_DISCOUNTABLE = 45,
SUPPLY_PACK_UNCOMMON_DISCOUNTABLE = 4,
SUPPLY_PACK_RARE_DISCOUNTABLE = 1,
))
GLOBAL_LIST_EMPTY(supplypod_loading_bays)
GLOBAL_LIST_INIT(podstyles, list(\
list(POD_SHAPE_NORML, "pod", TRUE, "default", "yellow", RUBBLE_NORMAL, "supply pod", "A Nanotrasen supply drop pod."),\
list(POD_SHAPE_NORML, "advpod", TRUE, "bluespace", "blue", RUBBLE_NORMAL, "bluespace supply pod" , "A Nanotrasen Bluespace supply pod. Teleports back to CentCom after delivery."),\
list(POD_SHAPE_NORML, "advpod", TRUE, "centcom", "blue", RUBBLE_NORMAL, "\improper CentCom supply pod", "A Nanotrasen supply pod, this one has been marked with Central Command's designations. Teleports back to CentCom after delivery."),\
list(POD_SHAPE_NORML, "darkpod", TRUE, "syndicate", "red", RUBBLE_NORMAL, "blood-red supply pod", "An intimidating supply pod, covered in the blood-red markings of the Syndicate. It's probably best to stand back from this."),\
list(POD_SHAPE_NORML, "darkpod", TRUE, "deathsquad", "blue", RUBBLE_NORMAL, "\improper Deathsquad drop pod", "A Nanotrasen drop pod. This one has been marked the markings of Nanotrasen's elite strike team."),\
list(POD_SHAPE_NORML, "pod", TRUE, "cultist", "red", RUBBLE_NORMAL, "bloody supply pod", "A Nanotrasen supply pod covered in scratch-marks, blood, and strange runes."),\
list(POD_SHAPE_OTHER, "missile", FALSE, FALSE, FALSE, RUBBLE_THIN, "cruise missile", "A big ass missile that didn't seem to fully detonate. It was likely launched from some far-off deep space missile silo. There appears to be an auxillery payload hatch on the side, though manually opening it is likely impossible."),\
list(POD_SHAPE_OTHER, "smissile", FALSE, FALSE, FALSE, RUBBLE_THIN, "\improper Syndicate cruise missile", "A big ass, blood-red missile that didn't seem to fully detonate. It was likely launched from some deep space Syndicate missile silo. There appears to be an auxillery payload hatch on the side, though manually opening it is likely impossible."),\
list(POD_SHAPE_OTHER, "box", TRUE, FALSE, FALSE, RUBBLE_WIDE, "\improper Aussec supply crate", "An incredibly sturdy supply crate, designed to withstand orbital re-entry. Has 'Aussec Armory - 2532' engraved on the side."),\
list(POD_SHAPE_NORML, "clownpod", TRUE, "clown", "green", RUBBLE_NORMAL, "\improper HONK pod", "A brightly-colored supply pod. It likely originated from the Clown Federation."),\
list(POD_SHAPE_OTHER, "orange", TRUE, FALSE, FALSE, RUBBLE_NONE, "\improper Orange", "An angry orange."),\
list(POD_SHAPE_OTHER, FALSE, FALSE, FALSE, FALSE, RUBBLE_NONE, "\improper S.T.E.A.L.T.H. pod MKVII", "A supply pod that, under normal circumstances, is completely invisible to conventional methods of detection. How are you even seeing this?"),\
list(POD_SHAPE_OTHER, "gondola", FALSE, FALSE, FALSE, RUBBLE_NONE, "gondola", "The silent walker. This one seems to be part of a delivery agency."),\
list(POD_SHAPE_OTHER, FALSE, FALSE, FALSE, FALSE, RUBBLE_NONE, FALSE, FALSE, "rl_click", "give_po")\
))

View File

@@ -527,6 +527,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_COMPONENT_MMI" = TRAIT_COMPONENT_MMI,
),
/obj/item/modular_computer = list(
"TRAIT_MODPC_HALVED_DOWNLOAD_SPEED" = TRAIT_MODPC_HALVED_DOWNLOAD_SPEED,
"TRAIT_PDA_CAN_EXPLODE" = TRAIT_PDA_CAN_EXPLODE,
"TRAIT_PDA_MESSAGE_MENU_RIGGED" = TRAIT_PDA_MESSAGE_MENU_RIGGED,
),

View File

@@ -1,9 +1,11 @@
///The maximum amount of logs that can be generated before they start overwriting eachother.
s///The maximum amount of logs that can be generated before they start overwriting eachother.
#define MAX_LOG_COUNT 300
SUBSYSTEM_DEF(modular_computers)
name = "Modular Computers"
flags = SS_NO_FIRE
wait = 1 MINUTES
runlevels = RUNLEVEL_GAME
///List of all logs generated by ModPCs through the round.
///Stops at MAX_LOG_COUNT and must be purged to keep logging.
@@ -22,11 +24,94 @@ SUBSYSTEM_DEF(modular_computers)
var/intrusion_detection_alarm = FALSE
var/next_picture_id = 0
///Lazylist of coupons used by the Coupon Master PDA app. e.g. "COUPONCODE25" = coupon_code
var/list/discount_coupons
///When will the next coupon drop?
var/next_discount = 0
/datum/controller/subsystem/modular_computers/Initialize()
build_software_lists()
initialized = TRUE
return SS_INIT_SUCCESS
/datum/controller/subsystem/modular_computers/fire(resumed = FALSE)
if(discount_coupons && world.time >= next_discount)
announce_coupon()
///Generate new coupon codes that can be redeemed with the Coupon Master App
/datum/controller/subsystem/modular_computers/proc/announce_coupon()
//If there's no way to announce the coupon, we may as well skip it.
if(!length(GLOB.announcement_systems))
return
var/obj/machinery/announcement_system/announcement_system = pick(GLOB.announcement_systems)
var/static/list/discounts = list("0.10" = 7, "0.15" = 16, "0.20" = 20, "0.25" = 16, "0.50" = 8, "0.66" = 1)
var/static/list/flash_discounts = list("0.30" = 3, "0.40" = 8, "0.50" = 8, "0.66" = 2, "0.75" = 1)
///Eliminates non-alphanumeri characters, as well as the word "Single-Pack" or "Pack" or "Crate" from the coupon code
var/static/regex/strip_pack_name = regex("\[^a-zA-Z0-9]|(Single-)?Pack|Crate", "g")
var/datum/supply_pack/discounted_pack = pick(GLOB.discountable_packs[pick_weight(GLOB.pack_discount_odds)])
var/pack_name = initial(discounted_pack.name)
var/chosen_discount
var/expires_in = 0
if(prob(75))
chosen_discount = text2num(pick_weight(discounts))
if(prob(20))
expires_in = rand(8,10)
else
chosen_discount = text2num(pick_weight(flash_discounts))
expires_in = rand(2, 4)
var/coupon_code = "[uppertext(strip_pack_name.Replace(pack_name, ""))][chosen_discount*100]"
var/list/targets = list()
for (var/messenger_ref in GLOB.pda_messengers)
var/datum/computer_file/program/messenger/messenger = GLOB.pda_messengers[messenger_ref]
if(locate(/datum/computer_file/program/coupon) in messenger?.computer.stored_files)
targets += messenger
///Don't go any further if the same coupon code has been done alrady or if there's no recipient for the 'promo'.
if((coupon_code in discount_coupons) || !length(targets))
return
var/datum/coupon_code/coupon = new(chosen_discount, discounted_pack, expires_in)
discount_coupons[coupon_code] = coupon
///pda message code here
var/static/list/promo_messages = list(
"A new discount has dropped for %GOODY: %DISCOUNT.",
"Check this new offer out: %GOODY, now %DISCOUNT off.",
"Now on sales: %GOODY, at %DISCOUNT discount!",
"This item is now on sale (%DISCOUNT off): %GOODY.",
"Would you look at that! A %DISCOUNT discount on %GOODY!",
"Exclusive offer for %GOODY. Only %DISCOUNT! Get it now:",
"%GOODY is now %DISCOUNT off.",
"*RING* A new discount has dropped: %GOODY, %DISCOUNT off.",
"%GOODY - %DISCOUNT off."
)
var/static/list/code_messages = list(
"Here's the code",
"Use this code to redeem it",
"Open the app to redeem it",
"Code",
"Redeem it now",
"Buy it now",
)
var/chosen_promo_message = replacetext(replacetext(pick(promo_messages), "%GOODY", pack_name), "%DISCOUNT", "[chosen_discount*100]%")
var/datum/signal/subspace/messaging/tablet_message/signal = new(announcement_system, list(
"fakename" = "Coupon Master",
"fakejob" = "Goodies Promotion",
"message" = "[chosen_promo_message] [pick(code_messages)]: [coupon_code][expires_in ? " (EXPIRES IN [expires_in] MINUTES)" : ""].",
"targets" = targets,
"automated" = TRUE,
))
signal.send_to_receivers()
next_discount = world.time + rand(3, 5) MINUTES
///Finds all downloadable programs and adds them to their respective downloadable list.
/datum/controller/subsystem/modular_computers/proc/build_software_lists()
for(var/datum/computer_file/program/prog as anything in subtypesof(/datum/computer_file/program))

View File

@@ -213,7 +213,7 @@
balloon_alert(user, "ooh, free coupon")
var/obj/item/coupon/attached_coupon = new
user.put_in_hands(attached_coupon)
attached_coupon.generate(rigged_omen)
attached_coupon.generate(rigged_omen ? COUPON_OMEN : null)
attached_coupon = null
spawn_coupon = FALSE
name = "discarded cigarette packet"

View File

@@ -1,4 +1,49 @@
#define COUPON_OMEN "omen"
///datum used by the Coupon Master PDA app to generate coupon items redeemed by a bank account.
/datum/coupon_code
///The pack that'll receive the discount
var/datum/supply_pack/discounted_pack
///The discount of the pack, on a 0 to 1 range.
var/discount
/**
* If set, copies of the coupon code will delete itself after a while if not printed.
* The ones SSmodular_computer.discount_coupons stay intact.
*/
var/expires_in
///Has the coupon been printed. Dictates in which section it's shown, and that it cannot be printed again.
var/printed = FALSE
///The timerid for deletion if expires_in is set.
var/timerid
///Reference to the associated bank account, since we need to clear refs on deletion.
var/datum/bank_account/associated_account
/datum/coupon_code/New(discount, discounted_pack, expires_in)
..()
src.discounted_pack = discounted_pack
src.discount = discount
if(expires_in)
src.expires_in = world.time + expires_in
/datum/coupon_code/Destroy()
if(associated_account)
associated_account.redeemed_coupons -= src
associated_account = null
return ..()
/datum/coupon_code/proc/copy(datum/bank_account/account)
var/datum/coupon_code/copy = new(discount, discounted_pack, expires_in)
copy.associated_account = account
if(account)
LAZYADD(account.redeemed_coupons, src)
if(expires_in)
copy.timerid = QDEL_IN_STOPPABLE(copy, expires_in - world.time)
/datum/coupon_code/proc/generate()
var/obj/item/coupon/coupon = new()
coupon.generate(discount, discounted_pack)
printed = TRUE
deltimer(timerid)
timerid = null
return coupon
/obj/item/coupon
name = "coupon"
@@ -12,18 +57,15 @@
var/obj/machinery/computer/cargo/inserted_console
/// Choose what our prize is :D
/obj/item/coupon/proc/generate(rig_omen=FALSE)
discounted_pack = pick(subtypesof(/datum/supply_pack/goody))
var/list/chances = list("0.10" = 4, "0.15" = 8, "0.20" = 10, "0.25" = 8, "0.50" = 4, COUPON_OMEN = 1)
if(rig_omen)
discount_pct_off = COUPON_OMEN
else
discount_pct_off = pick_weight(chances)
/obj/item/coupon/proc/generate(discount, datum/supply_pack/discounted_pack)
src.discounted_pack = discounted_pack || pick(GLOB.discountable_packs[pick_weight(GLOB.pack_discount_odds)])
var/static/list/chances = list("0.10" = 4, "0.15" = 8, "0.20" = 10, "0.25" = 8, "0.50" = 4, COUPON_OMEN = 1)
discount_pct_off = discount || pick_weight(chances)
if(discount_pct_off != COUPON_OMEN)
discount_pct_off = text2num(discount_pct_off)
name = "coupon - [round(discount_pct_off * 100)]% off [initial(discounted_pack.name)]"
if(!discount) // the discount arg should be a number already, while the keys in the chances list cannot be numbers
discount_pct_off = text2num(discount_pct_off)
name = "coupon - [round(discount_pct_off * 100)]% off [initial(src.discounted_pack.name)]"
return
name = "coupon - fuck you"
@@ -71,5 +113,3 @@
LAZYREMOVE(inserted_console.loaded_coupons, src)
inserted_console = null
. = ..()
#undef COUPON_OMEN

View File

@@ -3,6 +3,7 @@
access = NONE
group = "Goodies"
goody = TRUE
discountable = SUPPLY_PACK_STD_DISCOUNTABLE
/datum/supply_pack/goody/clear_pda
name = "Mint Condition Nanotrasen Clear PDA"

View File

@@ -39,6 +39,8 @@
var/admin_spawned = FALSE
/// Goodies can only be purchased by private accounts and can have coupons apply to them. They also come in a lockbox instead of a full crate, so the 700 min doesn't apply
var/goody = FALSE
/// Can coupons target this pack? If so, how rarely?
var/discountable = SUPPLY_PACK_NOT_DISCOUNTABLE
/datum/supply_pack/New()
id = type

View File

@@ -30,6 +30,7 @@
)
crate_name = "collectable hats crate"
crate_type = /obj/structure/closet/crate/wooden
discountable = SUPPLY_PACK_RARE_DISCOUNTABLE
/datum/supply_pack/costumes_toys/formalwear
name = "Formalwear Crate"
@@ -70,6 +71,7 @@
contains = list(/obj/item/firing_pin/clown)
crate_name = "toy crate" // It's /technically/ a toy. For the clown, at least.
crate_type = /obj/structure/closet/crate/wooden
discountable = SUPPLY_PACK_RARE_DISCOUNTABLE
/datum/supply_pack/costumes_toys/lasertag
name = "Laser Tag Crate"
@@ -232,6 +234,7 @@
cost = 1000
contains = list()
crate_name = "booster pack pack"
discountable = SUPPLY_PACK_STD_DISCOUNTABLE
/datum/supply_pack/costumes_toys/randomised/tcg/fill(obj/structure/closet/crate/C)
var/cardpacktype
@@ -244,6 +247,7 @@
desc = "This crate contains a random assortment of stickers."
cost = CARGO_CRATE_VALUE * 3
contains = list()
discountable = SUPPLY_PACK_STD_DISCOUNTABLE
/datum/supply_pack/costumes_toys/stickers/fill(obj/structure/closet/crate/crate)
for(var/i in 1 to rand(1,2))
@@ -262,3 +266,4 @@
)
crate_name = "corgi pinata kit"
crate_type = /obj/structure/closet/crate/wooden
discountable = SUPPLY_PACK_STD_DISCOUNTABLE

View File

@@ -1,5 +1,6 @@
/datum/supply_pack/emergency
group = "Emergency"
discountable = SUPPLY_PACK_UNCOMMON_DISCOUNTABLE
/datum/supply_pack/emergency/bio
name = "Biological Emergency Crate"

View File

@@ -271,6 +271,7 @@
crate_name = "supermatter shard crate"
crate_type = /obj/structure/closet/crate/secure/radiation
dangerous = TRUE
discountable = SUPPLY_PACK_RARE_DISCOUNTABLE
/datum/supply_pack/engine/tesla_coils
name = "Tesla Coil Crate"

View File

@@ -25,6 +25,7 @@
/obj/item/toner = 2)
crate_name = "tattoo crate"
crate_type = /obj/structure/closet/crate/wooden
discountable = SUPPLY_PACK_STD_DISCOUNTABLE
/datum/supply_pack/misc/bicycle
name = "Bicycle"
@@ -33,6 +34,7 @@
contains = list(/obj/vehicle/ridden/bicycle)
crate_name = "bicycle crate"
crate_type = /obj/structure/closet/crate/large
discountable = SUPPLY_PACK_RARE_DISCOUNTABLE
/datum/supply_pack/misc/bigband
name = "Big Band Instrument Collection"

View File

@@ -17,6 +17,7 @@
cost = CARGO_CRATE_VALUE * 2
contains = list(/obj/item/gun/ballistic/shotgun/toy = 8)
crate_name = "foam force crate"
discountable = SUPPLY_PACK_STD_DISCOUNTABLE
/datum/supply_pack/imports/foamforce/bonus
name = "Foam Force Pistols Crate"
@@ -37,6 +38,7 @@
contains = list(/obj/item/storage/backpack/meat)
crate_name = "MEAT MEAT MEAT MEAT MEAT"
crate_type = /obj/structure/closet/crate/necropolis
discountable = SUPPLY_PACK_RARE_DISCOUNTABLE
/datum/supply_pack/imports/duct_spider
name = "Duct Spider Crate"
@@ -45,6 +47,7 @@
contains = list(/mob/living/basic/spider/maintenance)
crate_name = "duct spider crate"
crate_type = /obj/structure/closet/crate/critter
discountable = SUPPLY_PACK_UNCOMMON_DISCOUNTABLE
/datum/supply_pack/imports/duct_spider/dangerous
name = "Duct Spider Crate?"
@@ -66,6 +69,7 @@
cost = CARGO_CRATE_VALUE * 100
contains = list(/obj/item/stack/sheet/mineral/bananium)
crate_name = "bananium sheet crate"
discountable = SUPPLY_PACK_RARE_DISCOUNTABLE
/datum/supply_pack/imports/naturalbait
name = "Freshness Jars full of Natural Bait"
@@ -226,6 +230,7 @@
/obj/item/clothing/suit/armor/vest/russian_coat,
/obj/item/storage/toolbox/guncase/soviet = 2,
)
discountable = SUPPLY_PACK_RARE_DISCOUNTABLE
/datum/supply_pack/imports/russian/fill(obj/structure/closet/crate/our_crate)
for(var/items in 1 to 10)
@@ -268,6 +273,7 @@
crate_name = "abandoned crate"
contraband = TRUE
dangerous = TRUE //these are literally bombs so....
discountable = SUPPLY_PACK_RARE_DISCOUNTABLE
/datum/supply_pack/imports/shambler_evil
name = "Shamber's Juice Eldritch Energy! Crate"

View File

@@ -222,6 +222,7 @@
cost = CARGO_CRATE_VALUE * 20
contains = list(/mob/living/basic/garden_gnome)
crate_name = "garden gnome crate"
discountable = SUPPLY_PACK_RARE_DISCOUNTABLE
/datum/supply_pack/critter/garden_gnome/generate()
. = ..()

View File

@@ -189,3 +189,4 @@
cost = CARGO_CRATE_VALUE * 6
contains = list(/obj/item/organ/internal/cyberimp/arm/muscle = 2)
crate_name = "Strong-Arm implant crate"
discountable = SUPPLY_PACK_RARE_DISCOUNTABLE

View File

@@ -125,6 +125,7 @@
access = ACCESS_THEATRE
access_view = ACCESS_THEATRE
crate_type = /obj/structure/closet/crate/secure
discountable = SUPPLY_PACK_RARE_DISCOUNTABLE
/datum/supply_pack/organic/hydroponics
name = "Hydroponics Crate"
@@ -306,6 +307,7 @@
/obj/machinery/grill/unwrenched,
)
crate_name = "grilling starter kit crate"
discountable = SUPPLY_PACK_UNCOMMON_DISCOUNTABLE
/datum/supply_pack/organic/grillfuel
name = "Grilling Fuel Kit"
@@ -317,6 +319,7 @@
/obj/item/reagent_containers/cup/soda_cans/monkey_energy,
)
crate_name = "grilling fuel kit crate"
discountable = SUPPLY_PACK_UNCOMMON_DISCOUNTABLE
/datum/supply_pack/organic/tiziran_supply
name = "Tiziran Supply Box"

View File

@@ -140,6 +140,7 @@
/obj/item/clothing/mask/gas/sechailer,
)
crate_name = "security clothing crate"
discountable = SUPPLY_PACK_RARE_DISCOUNTABLE
/datum/supply_pack/security/baton
name = "Stun Batons Crate"
@@ -168,6 +169,7 @@
/obj/item/clothing/mask/whistle,
/obj/item/conversion_kit,
)
discountable = SUPPLY_PACK_RARE_DISCOUNTABLE
/// Armory packs

View File

@@ -103,6 +103,7 @@
contains = list(/obj/item/stack/tile/carpet/fifty = 2,
/obj/item/stack/tile/carpet/black/fifty = 2)
crate_name = "premium carpet crate"
discountable = SUPPLY_PACK_UNCOMMON_DISCOUNTABLE
/datum/supply_pack/service/carpet_exotic
name = "Exotic Carpet Crate"
@@ -119,6 +120,7 @@
/obj/item/stack/tile/carpet/royalblack/fifty = 2,
)
crate_name = "exotic carpet crate"
discountable = SUPPLY_PACK_UNCOMMON_DISCOUNTABLE
/datum/supply_pack/service/carpet_neon
name = "Simple Neon Carpet Crate"
@@ -140,6 +142,7 @@
/obj/item/stack/tile/carpet/neon/simple/pink/sixty = 2,
)
crate_name = "neon carpet crate"
discountable = SUPPLY_PACK_UNCOMMON_DISCOUNTABLE
/datum/supply_pack/service/lightbulbs
name = "Replacement Lights"
@@ -243,6 +246,7 @@
)
crate_name = "\improper Ready-Donk crate"
crate_type = /obj/structure/closet/crate/freezer/food
discountable = SUPPLY_PACK_UNCOMMON_DISCOUNTABLE
/datum/supply_pack/service/randomized/ready_donk/fill(obj/structure/closet/crate/C)
for(var/i in 1 to 3)
@@ -265,6 +269,7 @@
/obj/item/reagent_containers/cup/bottle/syrup_bottle/caramel, //one extra syrup as a treat
)
crate_name = "coffee equipment crate"
discountable = SUPPLY_PACK_UNCOMMON_DISCOUNTABLE
/datum/supply_pack/service/coffeemaker
name = "Impressa Coffeemaker Crate"
@@ -273,6 +278,7 @@
contains = list(/obj/machinery/coffeemaker/impressa)
crate_name = "coffeemaker crate"
crate_type = /obj/structure/closet/crate/large
discountable = SUPPLY_PACK_UNCOMMON_DISCOUNTABLE
/datum/supply_pack/service/aquarium_kit
name = "Aquarium Kit"
@@ -287,6 +293,7 @@
)
crate_name = "aquarium kit crate"
crate_type = /obj/structure/closet/crate/wooden
discountable = SUPPLY_PACK_UNCOMMON_DISCOUNTABLE
/// Spare bar sign wallmount
/datum/supply_pack/service/bar_sign
@@ -295,3 +302,4 @@
cost = CARGO_CRATE_VALUE * 14
contains = list(/obj/item/wallframe/barsign/all_access)
crate_name = "bar sign crate"
discountable = SUPPLY_PACK_RARE_DISCOUNTABLE

View File

@@ -35,6 +35,8 @@
var/pay_token
///List with a transaction history for NT pay app
var/list/transaction_history = list()
///A lazylist of coupons redeemed with the Coupon Master pda app associated with this account.
var/list/redeemed_coupons
/datum/bank_account/New(newname, job, modifier = 1, player_account = TRUE)
account_holder = newname
@@ -47,6 +49,7 @@
/datum/bank_account/Destroy()
if(add_to_accounts)
SSeconomy.bank_accounts_by_id -= "[account_id]"
QDEL_LIST(redeemed_coupons)
return ..()
/**

View File

@@ -0,0 +1,119 @@
#define COUPON_PAPER_USE 1
#define COUPON_TONER_USE 0.250
///A program that enables the user to redeem randomly generated coupons for several cargo packs (mostly goodies).
/datum/computer_file/program/coupon
filename = "couponmaster"
filedesc = "Coupon Master"
downloader_category = PROGRAM_CATEGORY_SUPPLY
extended_desc = "Program for receiving discounts for several cargo goodies. After redeeming a coupon, hit a photocopier with your PDA to print it."
program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_REQUIRES_NTNET
size = 5
tgui_id = "NtosCouponMaster"
program_icon = "ticket"
can_run_on_flags = PROGRAM_PDA //It relies on the PDA messenger to let you know of new codes
detomatix_resistance = DETOMATIX_RESIST_MALUS
/datum/computer_file/program/coupon/on_install()
. = ..()
///set the discount_coupons list, which means SSmodular_computers will now begin to periodically produce new coupon codes.
LAZYINITLIST(SSmodular_computers.discount_coupons)
ADD_TRAIT(computer, TRAIT_MODPC_HALVED_DOWNLOAD_SPEED, REF(src)) //All that glitters is not gold
/datum/computer_file/program/coupon/Destroy()
if(computer)
REMOVE_TRAIT(computer, TRAIT_MODPC_HALVED_DOWNLOAD_SPEED, REF(src))
return ..()
/datum/computer_file/program/coupon/ui_data(mob/user)
var/list/data = list()
data["printed_coupons"] = list()
data["redeemed_coupons"] = list()
data["valid_id"] = FALSE
var/obj/item/card/id/user_id = computer.computer_id_slot
if(user_id?.registered_account.add_to_accounts)
for(var/datum/coupon_code/coupon as anything in user_id.registered_account.redeemed_coupons)
var/list/coupon_data = list(
"goody" = initial(coupon.discounted_pack.name),
"discount" = coupon.discount*100,
)
if(coupon.printed)
data["printed_coupons"] += list(coupon_data)
else
data["redeemed_coupons"] += list(coupon_data)
data["valid_id"] = TRUE
return data
/datum/computer_file/program/coupon/ui_act(action, params, datum/tgui/ui)
var/obj/item/card/id/user_id = computer.computer_id_slot
if(!(user_id?.registered_account.add_to_accounts))
return TRUE
switch(action)
if("redeem")
var/code = params["code"]
if(!length(code))
return TRUE
var/datum/coupon_code/coupon = SSmodular_computers.discount_coupons[code]
if(isnull(coupon))
user_id.registered_account.bank_card_talk("Invalid coupon code.", TRUE)
return TRUE
if(coupon.expires_in && coupon.expires_in < world.time)
user_id.registered_account.bank_card_talk("Expired coupon code.", TRUE)
return TRUE
if(coupon in user_id.registered_account.redeemed_coupons)
user_id.registered_account.bank_card_talk("Coupon [code] already redeemed.", TRUE)
return TRUE
coupon.copy(user_id.registered_account)
var/static/list/goodbye = list(
"Have a wonderful day.",
"Don't forget to print it.",
"Time to get shopping!",
"Enjoy your discount!",
"Congratulations!",
"Bye Bye~.",
)
user_id.registered_account.bank_card_talk("Coupon [code] redeemed. [pick(goodbye)]", TRUE)
//Well, guess you're redeeming something else too.
if(prob(40) && computer.used_capacity < computer.max_capacity)
var/datum/computer_file/warez = new()
warez.filename = random_string(rand(6, 12), GLOB.alphabet + GLOB.alphabet_upper + GLOB.numerals)
warez.filetype = pick("DAT", "XXX", "TMP", "FILE", "MNT", "MINER", "SYS", "PNG.EXE")
warez.size = min(rand(1, 4), computer.max_capacity - computer.used_capacity)
if(prob(25))
warez.undeletable = TRUE
computer.store_file(warez)
/**
* Normally, modular PCs can be print paper already, but I find this additional step
* to be less lazy and fitting to the "I gotta go print it before it expires" aspect of it.
*/
/datum/computer_file/program/coupon/tap(atom/tapped_atom, mob/living/user, params)
if(!istype(tapped_atom, /obj/machinery/photocopier))
return FALSE
var/obj/item/card/id/user_id = computer.computer_id_slot
if(!(user_id?.registered_account))
computer.balloon_alert(user, "no bank account found!")
return TRUE
var/obj/machinery/photocopier/copier = tapped_atom
if(copier.check_busy(user))
return TRUE
var/num_coupons = 0
for(var/datum/coupon_code/coupon as anything in user_id.registered_account.redeemed_coupons)
if(!coupon.printed)
num_coupons++
if(!num_coupons)
computer.balloon_alert(user, "no coupon available!")
return TRUE
copier.do_copies(CALLBACK(src, PROC_REF(print_coupon), user_id.registered_account), user, COUPON_PAPER_USE, COUPON_TONER_USE, num_coupons)
return TRUE
/datum/computer_file/program/coupon/proc/print_coupon(datum/bank_account/account)
var/datum/coupon_code/coupon
for(var/datum/coupon_code/possible_coupon as anything in account.redeemed_coupons)
if(!possible_coupon.printed)
coupon = possible_coupon
break
return coupon?.generate()
#undef COUPON_PAPER_USE
#undef COUPON_TONER_USE

View File

@@ -100,6 +100,8 @@
if(NTNET_ETHERNET_SIGNAL)
download_netspeed = NTNETSPEED_ETHERNET
if(download_netspeed)
if(HAS_TRAIT(computer, TRAIT_MODPC_HALVED_DOWNLOAD_SPEED))
download_netspeed *= 0.5
download_completion += download_netspeed
/datum/computer_file/program/ntnetdownload/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)

View File

@@ -283,6 +283,9 @@ GLOBAL_LIST_INIT(paper_blanks, init_paper_blanks())
/// Will invoke `do_copy_loop` asynchronously. Passes the supplied arguments on to it.
/obj/machinery/photocopier/proc/do_copies(datum/callback/copy_cb, mob/user, paper_use, toner_use, copies_amount)
if(machine_stat & (BROKEN|NOPOWER))
return
busy = TRUE
update_use_power(ACTIVE_POWER_USE)
// fucking god proc

View File

@@ -512,6 +512,7 @@
#include "code\_globalvars\lists\achievements.dm"
#include "code\_globalvars\lists\ambience.dm"
#include "code\_globalvars\lists\canisters.dm"
#include "code\_globalvars\lists\cargo.dm"
#include "code\_globalvars\lists\client.dm"
#include "code\_globalvars\lists\color.dm"
#include "code\_globalvars\lists\crafting.dm"
@@ -5012,6 +5013,7 @@
#include "code\modules\modular_computers\file_system\programs\budgetordering.dm"
#include "code\modules\modular_computers\file_system\programs\card.dm"
#include "code\modules\modular_computers\file_system\programs\cargoship.dm"
#include "code\modules\modular_computers\file_system\programs\coupon.dm"
#include "code\modules\modular_computers\file_system\programs\crewmanifest.dm"
#include "code\modules\modular_computers\file_system\programs\emojipedia.dm"
#include "code\modules\modular_computers\file_system\programs\file_browser.dm"

View File

@@ -0,0 +1,61 @@
import { BooleanLike } from 'common/react';
import { useBackend } from '../backend';
import { Box, Input, NoticeBox, Section } from '../components';
import { NtosWindow } from '../layouts';
type Data = {
valid_id: BooleanLike;
redeemed_coupons: CouponData[];
printed_coupons: CouponData[];
};
type CouponData = {
goody: string;
discount: number;
};
export const NtosCouponMaster = (props) => {
const { act, data } = useBackend<Data>();
const { valid_id, redeemed_coupons = [], printed_coupons = [] } = data;
return (
<NtosWindow width={400} height={400}>
<NtosWindow.Content scrollable>
{!valid_id ? (
<NoticeBox danger>
No valid bank account detected. Insert a valid ID.
</NoticeBox>
) : (
<>
<NoticeBox info>
You can print redeemed coupons by right-clicking a photocopier.
</NoticeBox>
<Input
fontSize={1.2}
placeholder="Insert your coupon code here"
onEnter={(e, value) =>
act('redeem', {
code: value,
})
}
/>
<Section title="Redeemed Coupons">
{redeemed_coupons.map((coupon, index) => (
<Box key={index} className="candystripe">
{coupon.goody} ({coupon.discount}% OFF)
</Box>
))}
</Section>
<Section title="Printed Coupons">
{printed_coupons.map((coupon, index) => (
<Box key={index} className="candystripe">
{coupon.goody} ({coupon.discount}% OFF)
</Box>
))}
</Section>
</>
)}
</NtosWindow.Content>
</NtosWindow>
);
};