From 08da259a7bd317df0fc1066f85d07431edf7dcde Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Wed, 8 Apr 2026 03:42:23 +0300 Subject: [PATCH 01/18] =?UTF-8?q?yup,=20it's=20govnocode=E2=84=A2=20again!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../metacoins/code/award_overrides.dm | 35 ++++---- .../metacoins/code/metacoin_deathmatch.dm | 84 +++++++++++++++++++ 2 files changed, 104 insertions(+), 15 deletions(-) create mode 100644 modular_meta/features/metacoins/code/metacoin_deathmatch.dm diff --git a/modular_meta/features/metacoins/code/award_overrides.dm b/modular_meta/features/metacoins/code/award_overrides.dm index 8d0c742365c8..c9a94cbeebf6 100644 --- a/modular_meta/features/metacoins/code/award_overrides.dm +++ b/modular_meta/features/metacoins/code/award_overrides.dm @@ -76,13 +76,13 @@ // CLOSE_TO_NOTHING -// ONE_POINT +// You genuinely don't want to award much for every "robot" served in kitchen. Five points is okay, in my opinion. -/datum/award/score/drake_score - reward = METACOIN_AWARD_ONE_POINT +/datum/award/score/bartender_tourist_score -/datum/award/score/hierophant_score - reward = METACOIN_AWARD_ONE_POINT +/datum/award/score/chef_tourist_score + +// ONE_POINT /datum/award/score/maintenance_pill reward = METACOIN_AWARD_ONE_POINT @@ -101,24 +101,29 @@ /datum/award/score/wendigo_score reward = METACOIN_AWARD_SMALL -/datum/award/score/colussus_score +/datum/award/score/hardcore_random reward = METACOIN_AWARD_SMALL -/datum/award/score/thething_score +/datum/award/score/style_score reward = METACOIN_AWARD_SMALL -/datum/award/score/bartender_tourist_score +/datum/award/score/drake_score reward = METACOIN_AWARD_SMALL -/datum/award/score/chef_tourist_score +/datum/award/score/hierophant_score reward = METACOIN_AWARD_SMALL -/datum/award/score/hardcore_random - reward = METACOIN_AWARD_SMALL +// METACOIN_AWARD_MED -/datum/award/score/style_score - reward = METACOIN_AWARD_SMALL +/datum/award/score/bubblegum_score + reward = METACOIN_AWARD_MED + +/datum/award/score/colussus_score + reward = METACOIN_AWARD_MED + +// METACOIN_AWARD_BIG +/datum/award/score/thething_score + reward = METACOIN_AWARD_BIG //overall rare boss, why not? -// METACOIN_AWARD_MED /datum/award/score/legion_score - reward = METACOIN_AWARD_MED // this one is hard to kill y'know? + reward = METACOIN_AWARD_BIG // this one is hard to kill y'know? diff --git a/modular_meta/features/metacoins/code/metacoin_deathmatch.dm b/modular_meta/features/metacoins/code/metacoin_deathmatch.dm new file mode 100644 index 000000000000..e717b0a253ec --- /dev/null +++ b/modular_meta/features/metacoins/code/metacoin_deathmatch.dm @@ -0,0 +1,84 @@ +/// Tries to charge the player for current entry fee before adding them to players list. +/datum/deathmatch_lobby/proc/pay_fee(mob/player) + if(!player?.ckey) + return FALSE + + var/already_paid = fees_paid[player.ckey] || 0 + if(entry_fee <= already_paid) + return TRUE + + var/to_pay = entry_fee - already_paid + var/datum/metacoin_shop_controller/shop = get_metacoin_shop_controller() + if(!shop) + to_chat(player, span_warning("Metacoin subsystem is unavailable.")) + return FALSE + + var/list/take_result = shop.take_metacoins(player.ckey, to_pay) + if(!take_result["ok"]) + switch(take_result["error"]) + if("not_enough") + to_chat(player, span_warning("Not enough metacoins for entry fee ([entry_fee]).")) + if("db_unavailable", "db_failed") + to_chat(player, span_warning("Metacoin database is unavailable.")) + else + to_chat(player, span_warning("Failed to pay lobby entry fee.")) + return FALSE + + fees_paid[player.ckey] = already_paid + to_pay + prize_pool += to_pay + to_chat(player, span_boldnicegreen("Entry fee paid: [to_pay] metacoins.")) + return TRUE + +/// Returns paid fee to the player while lobby is not in active match state. +/datum/deathmatch_lobby/proc/refund_fee(target_ckey, reason) + if(!target_ckey) + return FALSE + + var/paid_amount = fees_paid[target_ckey] || 0 + if(paid_amount <= 0) + fees_paid -= target_ckey + return TRUE + + var/datum/metacoin_shop_controller/shop = get_metacoin_shop_controller() + if(!shop || !shop.add_metacoins(target_ckey, paid_amount)) + log_game("Deathmatch lobby [host] failed to refund [paid_amount] metacoins to [target_ckey].") + return FALSE + + prize_pool = max(prize_pool - paid_amount, 0) + fees_paid -= target_ckey + + var/mob/player_mob = get_mob_by_ckey(target_ckey) + if(player_mob) + to_chat(player_mob, span_notice("Entry fee refunded: [paid_amount] metacoins. [reason]")) + return TRUE + +/// Pays prize pool to winner. If payout fails, tries to refund everyone. +/datum/deathmatch_lobby/proc/pay_pool(winner_ckey, mob/winner) + if(prize_pool <= 0) + return + + var/payout_amount = prize_pool + var/list/paid_snapshot = fees_paid?.Copy() || list() + var/datum/metacoin_shop_controller/shop = get_metacoin_shop_controller() + + if(winner_ckey && shop?.add_metacoins(winner_ckey, payout_amount)) + announce(span_boldnicegreen("[winner ? winner.real_name : winner_ckey] received [payout_amount] metacoins from the prize pool.")) + if(winner) + to_chat(winner, span_boldnicegreen("You won [payout_amount] metacoins from this deathmatch prize pool.")) + log_game("Deathmatch lobby [host] paid [payout_amount] metacoins to [winner_ckey].") + prize_pool = 0 + fees_paid = list() + return + + var/payout_target = winner_ckey || "no winner" + log_game("Deathmatch lobby [host] failed to pay prize pool [payout_amount] to [payout_target], trying refunds.") + if(shop) + for(var/paid_ckey in paid_snapshot) + var/paid_amount = paid_snapshot[paid_ckey] || 0 + if(paid_amount <= 0) + continue + shop.add_metacoins(paid_ckey, paid_amount) + + announce(span_warning("Prize payout failed, entry fees were refunded when possible.")) + prize_pool = 0 + fees_paid = list() From ce0e1392e8701a57c1bfa6a5c0c097dd23227dab Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Tue, 14 Apr 2026 00:23:15 +0300 Subject: [PATCH 02/18] datumize everything! --- .../features/metacoins/code/metacoin_shop.dm | 221 ++++-------------- .../features/metacoins/code/shop_items.dm | 93 ++++++++ modular_meta/features/metacoins/includes.dm | 1 + 3 files changed, 137 insertions(+), 178 deletions(-) create mode 100644 modular_meta/features/metacoins/code/shop_items.dm diff --git a/modular_meta/features/metacoins/code/metacoin_shop.dm b/modular_meta/features/metacoins/code/metacoin_shop.dm index 6d237096e17c..43ded8629e9a 100644 --- a/modular_meta/features/metacoins/code/metacoin_shop.dm +++ b/modular_meta/features/metacoins/code/metacoin_shop.dm @@ -6,26 +6,6 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) GLOB.metacoin_shop_controller.register_signals() return GLOB.metacoin_shop_controller -/datum/metacoin_shop_listing - var/id - var/name - var/desc - var/price - var/item_type - var/listing_kind = "item" - var/icon - var/icon_state - -/datum/metacoin_shop_listing/New(id, name, desc, price, item_type, listing_kind = "item", icon, icon_state) - src.id = id - src.name = name - src.desc = desc - src.price = price - src.item_type = item_type - src.listing_kind = listing_kind - src.icon = icon - src.icon_state = icon_state - /datum/metacoin_shop_controller var/list/preround_catalog = list() var/list/preround_pending_by_ckey = list() @@ -39,89 +19,16 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) . = ..() setup_catalog() -//Add your items here!!!! ~`_-´ -//In the list preround_catalog - -/* EXAMPLE: - alist( - "listing_name" = "donut_box", - "listing_display_name" = "Donut Box", - "listing_display_desc" = "A box of donuts delivered on your first roundstart spawn.", - "listing_price" = 5, - "listing_typepath" = /obj/item/storage/fancy/donut_box, - ), -*/ - /datum/metacoin_shop_controller/proc/setup_catalog() - var/list/raw_preround_catalog = list( - alist( - "listing_name" = "donut_box", - "listing_display_name" = "Donut Box", - "listing_display_desc" = "A box of donuts... what else do you expect?", - "listing_price" = 50, - "listing_typepath" = /obj/item/storage/fancy/donut_box, - ), - alist( - "listing_name" = "spray_libital", - "listing_display_name" = "Libital Spray", - "listing_display_desc" = "An medigel full of libital, mainly used to treat bruises", - "listing_price" = 75, - "listing_typepath" = /obj/item/reagent_containers/medigel/libital, - ), - alist( - "listing_name" = "spray_auri", - "listing_display_name" = "Aiuri Spray", - "listing_display_desc" = "An medigel full of aiuri, mainly used to treat burns", - "listing_price" = 75, - "listing_typepath" = /obj/item/reagent_containers/medigel/aiuri, - ), - alist( - "listing_name" = "antag_token", - "listing_display_name" = "Antag Token", - "listing_display_desc" = "Guarantees one chosen antagonist role at roundstart.", - "listing_price" = 650, - "listing_typepath" = /obj/item/coin/antagtoken, // to get the display icon of ours - "listing_kind" = "antag_token", - ), - ) - preround_catalog = alist() - for(var/listing_data in raw_preround_catalog) - if(!listing_data) - continue + for(var/listing_path in subtypesof(/datum/metacoinshop/listing/preround)) + var/datum/metacoinshop/listing/listing = new listing_path + if(listing.item_type && !listing.icon) + var/obj/item/type_cast_item_path = listing.item_type + listing.icon = initial(type_cast_item_path.icon) + listing.icon_state = initial(type_cast_item_path.icon_state) - var/listing_name = listing_data["listing_name"] - if(!listing_name) - continue - - var/listing_display_name = listing_data["listing_display_name"] - var/listing_display_desc = listing_data["listing_display_desc"] - var/listing_price = listing_data["listing_price"] - var/listing_typepath = listing_data["listing_typepath"] - var/listing_kind = listing_data["listing_kind"] - if(!listing_kind) - listing_kind = "item" - var/listing_icon = listing_data["listing_icon"] - var/listing_icon_state = listing_data["listing_icon_state"] - - if(listing_kind == "item" && !listing_typepath) - continue - - if(listing_typepath && !listing_icon) - var/obj/item/type_cast_item_path = listing_typepath - listing_icon = initial(type_cast_item_path.icon) - listing_icon_state = initial(type_cast_item_path.icon_state) - - preround_catalog[listing_name] = new /datum/metacoin_shop_listing( - listing_name, - listing_display_name, - listing_display_desc, - listing_price, - listing_typepath, - listing_kind, - listing_icon, - listing_icon_state, - ) + preround_catalog[listing.id] = listing /datum/metacoin_shop_controller/proc/register_signals() if(signals_registered) @@ -205,57 +112,22 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return "Warning: you have restricted jobs enabled in preferences ([english_list(restricted_preferences)]). If one of these jobs is assigned at roundstart, antag token will be refunded." -/datum/metacoin_shop_controller/proc/get_antag_token_role_definitions() - var/static/list/role_definitions = list( - alist( - "id" = "traitor", - "name" = "Traitor", - "desc" = "An unpaid debt. A score to be settled. Maybe you were just in the wrong \ - place at the wrong time. Whatever the reasons, you were selected to \ - infiltrate Space Station 13.", - "ruleset_tag" = "Roundstart Traitor", - "jobban_flag" = ROLE_TRAITOR, - "antag_datum" = /datum/antagonist/traitor, - "default_min_pop" = 3, - ), - alist( - "id" = "changeling", - "name" = "Changeling", - "desc" = "A highly intelligent alien predator that is capable of altering their \ - shape to flawlessly resemble a human.", - "ruleset_tag" = "Roundstart Changeling", - "jobban_flag" = ROLE_CHANGELING, - "antag_datum" = /datum/antagonist/changeling, - "default_min_pop" = 15, - ), - alist( - "id" = "heretic", - "name" = "Heretic", - "desc" = " Forgotten, devoured, gutted. Humanity has forgotten the eldritch forces \ - of decay, but the mansus veil has weakened. We will make them taste fear \ - again...", - "ruleset_tag" = "Roundstart Heretics", - "jobban_flag" = ROLE_HERETIC, - "antag_datum" = /datum/antagonist/heretic, - "default_min_pop" = 30, - ), +/datum/metacoin_shop_controller/proc/get_antag_roles() + var/static/list/antag_roles = list( + new /datum/metacoinshop/antag_role/traitor, + new /datum/metacoinshop/antag_role/changeling, + new /datum/metacoinshop/antag_role/heretic, ) - return role_definitions + return antag_roles -/datum/metacoin_shop_controller/proc/get_antag_token_role_definition(role_id) +/datum/metacoin_shop_controller/proc/get_antag_role(role_id) if(!role_id) return null - var/list/role_definitions = get_antag_token_role_definitions() - for(var/role_key in role_definitions) - var/list/role_definition = role_definitions[role_key] - if(!islist(role_definition) && islist(role_key)) - role_definition = role_key - if(!islist(role_definition)) - continue - if(role_definition["id"] == role_id) - return role_definition + for(var/datum/metacoinshop/antag_role/role as anything in get_antag_roles()) + if(role.id == role_id) + return role return null @@ -263,10 +135,10 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) if(!role_id) return null - var/list/role_definition = get_antag_token_role_definition(role_id) - if(!role_definition) + var/datum/metacoinshop/antag_role/role = get_antag_role(role_id) + if(!role) return null - return role_definition["name"] + return role.name /datum/metacoin_shop_controller/proc/dynamic_weight_has_positive_value(weight_setting) if(isnull(weight_setting)) @@ -299,11 +171,11 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return max(text2num("[fallback_value]"), 0) /datum/metacoin_shop_controller/proc/get_antag_token_role_block_info(target_ckey, role_id, datum/job/current_job = null) - var/list/role_definition = get_antag_token_role_definition(role_id) - if(!role_definition) + var/datum/metacoinshop/antag_role/role = get_antag_role(role_id) + if(!role) return list("code" = "unknown_role") - var/role_ban_flag = role_definition["jobban_flag"] + var/role_ban_flag = role.jobban_flag if(target_ckey && is_banned_from(target_ckey, list(ROLE_SYNDICATE, role_ban_flag))) return list("code" = "job_banned") @@ -313,11 +185,11 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) "job_title" = current_job.title, ) - var/default_min_pop = role_definition["default_min_pop"] + var/default_min_pop = role.default_min_pop var/min_pop_setting = default_min_pop if(CONFIG_GET(flag/dynamic_config_enabled)) - var/ruleset_tag = role_definition["ruleset_tag"] + var/ruleset_tag = role.ruleset_tag var/list/ruleset_config = SSdynamic.get_config()?[ruleset_tag] if(!isnull(ruleset_config?["weight"]) && !dynamic_weight_has_positive_value(ruleset_config["weight"])) @@ -364,21 +236,14 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) /datum/metacoin_shop_controller/proc/get_antag_token_roles_ui_data(target_ckey) var/list/roles_ui_data = list() - var/list/role_definitions = get_antag_token_role_definitions() - for(var/role_key in role_definitions) - var/list/role_definition = role_definitions[role_key] - if(!islist(role_definition) && islist(role_key)) - role_definition = role_key - if(!islist(role_definition)) - continue - - var/role_id = role_definition["id"] + for(var/datum/metacoinshop/antag_role/role as anything in get_antag_roles()) + var/role_id = role.id var/list/block_info = get_antag_token_role_block_info(target_ckey, role_id) roles_ui_data += list(list( "id" = role_id, - "name" = role_definition["name"], - "desc" = role_definition["desc"], + "name" = role.name, + "desc" = role.desc, "prefIconClass" = role_id, "fallbackIcon" = default_listing_fallback_icon, "available" = isnull(block_info), @@ -398,7 +263,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) log_game("[src] antag token refund skipped for [target_ckey]: no pending reservation.") return FALSE - var/datum/metacoin_shop_listing/antag_listing = get_antag_token_listing() + var/datum/metacoinshop/listing/antag_listing = get_antag_token_listing() var/refund_amount = antag_listing?.price || 0 antag_token_pending_by_ckey -= target_ckey @@ -451,11 +316,11 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) var/balance = fetch_metacoin_balance(target_ckey) for(var/listing_id in preround_catalog) - var/datum/metacoin_shop_listing/listing = preround_catalog[listing_id] + var/datum/metacoinshop/listing/listing = preround_catalog[listing_id] if(!listing) continue - var/is_antag_token = listing.listing_kind == "antag_token" + var/is_antag_token = listing.id == "antag_token" var/is_owned = FALSE if(is_antag_token) is_owned = !isnull(selected_antag_role) @@ -464,7 +329,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) var/list/listing_payload = list( "id" = listing.id, - "kind" = listing.listing_kind, + "kind" = is_antag_token ? "antag_token" : "item", "name" = listing.name, "desc" = listing.desc, "price" = listing.price, @@ -589,11 +454,11 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) if(!is_preround_purchase_open()) return list("ok" = FALSE, "error" = "shop_closed") - var/datum/metacoin_shop_listing/listing = preround_catalog[item_id] + var/datum/metacoinshop/listing/listing = preround_catalog[item_id] if(!listing) return list("ok" = FALSE, "error" = "unknown_item") - if(listing.listing_kind == "antag_token") + if(listing.listing_type != "item") return list("ok" = FALSE, "error" = "open_antag_panel") var/list/pending_items = preround_pending_by_ckey[target_ckey] @@ -662,7 +527,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) if(block_info) return list("ok" = FALSE, "error" = block_info["code"]) - var/datum/metacoin_shop_listing/listing = get_antag_token_listing() + var/datum/metacoinshop/listing/listing = get_antag_token_listing() if(!listing) return list("ok" = FALSE, "error" = "unknown_item") @@ -772,13 +637,13 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) refund_antag_token_purchase(target_ckey, "Antag token failed: no valid player mind found.", notify_mob) return - var/list/role_definition = get_antag_token_role_definition(selected_role) - if(!role_definition) + var/datum/metacoinshop/antag_role/role = get_antag_role(selected_role) + if(!role) log_game("[src] antag token grant failed for [target_ckey]: invalid role definition '[selected_role]'.") refund_antag_token_purchase(target_ckey, "Antag token failed: selected role is invalid.", notify_mob) return - var/antag_datum_path = role_definition["antag_datum"] + var/antag_datum_path = role.antag_datum var/datum/antagonist/created_antag = new antag_datum_path() created_antag.silent = TRUE human_spawned.mind.add_antag_datum(created_antag) @@ -796,7 +661,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) /*if(notify_mob) unnecessary actually. why do you think we have stinger sounds? - var/role_name = role_definition["name"] + var/role_name = role.name to_chat(notify_mob, span_boldnicegreen("Antag token applied successfully: [role_name].")) notify_mob.playsound_local(notify_mob, 'sound/misc/server-ready.ogg', 25, TRUE, use_reverb = FALSE) */ @@ -865,8 +730,8 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) var/mob/living/carbon/human/human_spawned = spawned for(var/item_id in pending_items) - var/datum/metacoin_shop_listing/listing = preround_catalog[item_id] - if(listing?.listing_kind != "item" || !listing?.item_type) + var/datum/metacoinshop/listing/listing = preround_catalog[item_id] + if(listing?.listing_type != "item" || !listing?.item_type) continue var/obj/item/new_item = new listing.item_type(human_spawned) @@ -986,7 +851,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) var/datum/metacoin_shop_controller/shop = get_metacoin_shop_controller() var/balance = shop.fetch_metacoin_balance(client_ckey) var/selected_role = shop.antag_token_pending_by_ckey[client_ckey] - var/datum/metacoin_shop_listing/antag_listing = shop.get_antag_token_listing() + var/datum/metacoinshop/listing/antag_listing = shop.get_antag_token_listing() data["isPregame"] = shop.is_preround_purchase_open() data["balance"] = isnull(balance) ? 0 : balance diff --git a/modular_meta/features/metacoins/code/shop_items.dm b/modular_meta/features/metacoins/code/shop_items.dm new file mode 100644 index 000000000000..06394b249c43 --- /dev/null +++ b/modular_meta/features/metacoins/code/shop_items.dm @@ -0,0 +1,93 @@ +/datum/metacoinshop/listing + var/id + var/name + var/desc + var/price + var/item_type + var/listing_type = "item" + var/icon + var/icon_state + +/datum/metacoinshop/listing/preround + +/datum/metacoinshop/listing/preround/donut_box + id = "donut_box" + name = "Donut Box" + desc = "A box of donuts... what else do you expect?" + price = 50 + item_type = /obj/item/storage/fancy/donut_box + +/datum/metacoinshop/listing/preround/spray_libital + id = "spray_libital" + name = "Libital Spray" + desc = "An medigel full of libital, mainly used to treat bruises" + price = 75 + item_type = /obj/item/reagent_containers/medigel/libital + +/datum/metacoinshop/listing/preround/spray_auri + id = "spray_auri" + name = "Aiuri Spray" + desc = "An medigel full of aiuri, mainly used to treat burns" + price = 75 + item_type = /obj/item/reagent_containers/medigel/aiuri + +/datum/metacoinshop/listing/preround/antag_token + id = "antag_token" + name = "Antag Token" + desc = "Guarantees one chosen antagonist role at roundstart." + price = 650 + item_type = /obj/item/coin/antagtoken // to get the display icon of ours + listing_type = "other" + +/datum/metacoin_shop_listing + parent_type = /datum/metacoinshop/listing + +/datum/metacoinshop/antag_role + var/id + var/name + var/desc + var/ruleset_tag + var/jobban_flag + var/antag_datum + var/default_min_pop = 0 + +/datum/metacoinshop/antag_role/traitor + id = "traitor" + name = "Traitor" + desc = "An unpaid debt. A score to be settled. Maybe you were just in the wrong \ + place at the wrong time. Whatever the reasons, you were selected to \ + infiltrate Space Station 13." + ruleset_tag = "Roundstart Traitor" + jobban_flag = ROLE_TRAITOR + antag_datum = /datum/antagonist/traitor + default_min_pop = 3 + +/datum/metacoinshop/antag_role/changeling + id = "changeling" + name = "Changeling" + desc = "A highly intelligent alien predator that is capable of altering their \ + shape to flawlessly resemble a human." + ruleset_tag = "Roundstart Changeling" + jobban_flag = ROLE_CHANGELING + antag_datum = /datum/antagonist/changeling + default_min_pop = 15 + +/datum/metacoinshop/antag_role/heretic + id = "heretic" + name = "Heretic" + desc = " Forgotten, devoured, gutted. Humanity has forgotten the eldritch forces \ + of decay, but the mansus veil has weakened. We will make them taste fear \ + again..." + ruleset_tag = "Roundstart Heretics" + jobban_flag = ROLE_HERETIC + antag_datum = /datum/antagonist/heretic + default_min_pop = 30 + +/datum/metacoinshop/antag_role/spy + id = "spy" + name = "Spy" + desc = "123" + ruleset_tag = "Roundstart Spies" + jobban_flag = ROLE_CHANGELING + antag_datum = /datum/antagonist/spy + default_min_pop = 15 diff --git a/modular_meta/features/metacoins/includes.dm b/modular_meta/features/metacoins/includes.dm index d57d692317c9..cbdb8cdce6f6 100644 --- a/modular_meta/features/metacoins/includes.dm +++ b/modular_meta/features/metacoins/includes.dm @@ -1,5 +1,6 @@ #include "code\metacoin_deathmatch.dm" #include "code\award_overrides.dm" +#include "code\shop_listings.dm" #include "code\metacoin.dm" #include "code\metacoin_shop.dm" #include "code\metacoin_gambling.dm" From fded9f76f10286c4d4a498e0b367f21ddc4be832 Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Tue, 14 Apr 2026 00:33:06 +0300 Subject: [PATCH 03/18] =?UTF-8?q?=E2=98=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ты чё сделал нахуй --- modular_meta/features/metacoins/code/metacoin_shop.dm | 10 +++++----- modular_meta/features/metacoins/includes.dm | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modular_meta/features/metacoins/code/metacoin_shop.dm b/modular_meta/features/metacoins/code/metacoin_shop.dm index 43ded8629e9a..5a1195d12ed3 100644 --- a/modular_meta/features/metacoins/code/metacoin_shop.dm +++ b/modular_meta/features/metacoins/code/metacoin_shop.dm @@ -113,11 +113,11 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return "Warning: you have restricted jobs enabled in preferences ([english_list(restricted_preferences)]). If one of these jobs is assigned at roundstart, antag token will be refunded." /datum/metacoin_shop_controller/proc/get_antag_roles() - var/static/list/antag_roles = list( - new /datum/metacoinshop/antag_role/traitor, - new /datum/metacoinshop/antag_role/changeling, - new /datum/metacoinshop/antag_role/heretic, - ) + var/static/list/antag_roles + if(isnull(antag_roles)) + antag_roles = list() + for(var/role_path in subtypesof(/datum/metacoinshop/antag_role)) + antag_roles += new role_path return antag_roles diff --git a/modular_meta/features/metacoins/includes.dm b/modular_meta/features/metacoins/includes.dm index cbdb8cdce6f6..5e4ee5455402 100644 --- a/modular_meta/features/metacoins/includes.dm +++ b/modular_meta/features/metacoins/includes.dm @@ -1,6 +1,6 @@ #include "code\metacoin_deathmatch.dm" #include "code\award_overrides.dm" -#include "code\shop_listings.dm" +#include "code\shop_items.dm" #include "code\metacoin.dm" #include "code\metacoin_shop.dm" #include "code\metacoin_gambling.dm" From df4088759f9022641e3b9ea4599a70be31bed8d7 Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Tue, 14 Apr 2026 02:44:36 +0300 Subject: [PATCH 04/18] boxify also makes the window bigger by default. also adds spy to shop :) --- .../features/metacoins/code/metacoin_shop.dm | 9 ++ .../features/metacoins/code/shop_items.dm | 22 +++- .../tgui/interfaces/MetaCoinAntagToken.tsx | 119 +++++++++++------- 3 files changed, 105 insertions(+), 45 deletions(-) diff --git a/modular_meta/features/metacoins/code/metacoin_shop.dm b/modular_meta/features/metacoins/code/metacoin_shop.dm index 5a1195d12ed3..1c04c6ce32b6 100644 --- a/modular_meta/features/metacoins/code/metacoin_shop.dm +++ b/modular_meta/features/metacoins/code/metacoin_shop.dm @@ -6,6 +6,13 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) GLOB.metacoin_shop_controller.register_signals() return GLOB.metacoin_shop_controller +/proc/cmp_antag_role_ui(datum/metacoinshop/antag_role/a, datum/metacoinshop/antag_role/b) + var/order_diff = cmp_numeric_asc(a.ui_order, b.ui_order) + if(order_diff) + return order_diff + + return cmp_text_asc(a.id, b.id) + /datum/metacoin_shop_controller var/list/preround_catalog = list() var/list/preround_pending_by_ckey = list() @@ -119,6 +126,8 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) for(var/role_path in subtypesof(/datum/metacoinshop/antag_role)) antag_roles += new role_path + antag_roles = sort_list(antag_roles, GLOBAL_PROC_REF(cmp_antag_role_ui)) + return antag_roles /datum/metacoin_shop_controller/proc/get_antag_role(role_id) diff --git a/modular_meta/features/metacoins/code/shop_items.dm b/modular_meta/features/metacoins/code/shop_items.dm index 06394b249c43..6290b0c417c7 100644 --- a/modular_meta/features/metacoins/code/shop_items.dm +++ b/modular_meta/features/metacoins/code/shop_items.dm @@ -44,16 +44,26 @@ /datum/metacoinshop/antag_role var/id + /// Displayed name. var/name + /// Displayed description. var/desc + /// as in code\modules\jobs\departments\departments.dm. Needed for UI sorting purpouses. + var/ui_order = 100 +/// Check dynamic.toml, put here your ruleset tag \ +(The name of it, e.g ["Roundstart Traitor"]) Under no circumstances there shall be midround antag, or any other that spawns with unique loadout. var/ruleset_tag + /// check code\__DEFINES\role_preferences.dm , when bought role is banned, then it will try to refund the metacoins. var/jobban_flag + /// Your antag datum. var/antag_datum + /// Defaulted value, if for some reason config is unavailable. var/default_min_pop = 0 /datum/metacoinshop/antag_role/traitor id = "traitor" name = "Traitor" + ui_order = 10 desc = "An unpaid debt. A score to be settled. Maybe you were just in the wrong \ place at the wrong time. Whatever the reasons, you were selected to \ infiltrate Space Station 13." @@ -65,6 +75,7 @@ /datum/metacoinshop/antag_role/changeling id = "changeling" name = "Changeling" + ui_order = 20 desc = "A highly intelligent alien predator that is capable of altering their \ shape to flawlessly resemble a human." ruleset_tag = "Roundstart Changeling" @@ -75,6 +86,7 @@ /datum/metacoinshop/antag_role/heretic id = "heretic" name = "Heretic" + ui_order = 30 desc = " Forgotten, devoured, gutted. Humanity has forgotten the eldritch forces \ of decay, but the mansus veil has weakened. We will make them taste fear \ again..." @@ -86,8 +98,12 @@ /datum/metacoinshop/antag_role/spy id = "spy" name = "Spy" - desc = "123" + ui_order = 40 + desc = "Your mission, should you choose to accept it: Infiltrate Space Station 13. \ + Disguise yourself as a member of their crew and steal vital equipment. \ + Should you be caught or killed, your employer will disavow any knowledge of your actions. Good luck agent. \ + Complete Spy Bounties to earn rewards from your employer. Use these rewards to sow chaos and mischief!" ruleset_tag = "Roundstart Spies" - jobban_flag = ROLE_CHANGELING + jobban_flag = ROLE_SPY antag_datum = /datum/antagonist/spy - default_min_pop = 15 + default_min_pop = 5 diff --git a/tgui/packages/tgui/interfaces/MetaCoinAntagToken.tsx b/tgui/packages/tgui/interfaces/MetaCoinAntagToken.tsx index 988f7061b6a1..a79e27874698 100644 --- a/tgui/packages/tgui/interfaces/MetaCoinAntagToken.tsx +++ b/tgui/packages/tgui/interfaces/MetaCoinAntagToken.tsx @@ -1,11 +1,4 @@ -import { - Box, - Button, - Icon, - NoticeBox, - Section, - Stack, -} from 'tgui-core/components'; +import { Box, Button, Icon, NoticeBox, Section } from 'tgui-core/components'; import { classes } from 'tgui-core/react'; import { useBackend } from '../backend'; @@ -75,7 +68,7 @@ export const MetaCoinAntagToken = () => { isPregame && !hasPurchasedToken && slotsLeft > 0 && balance >= price; return ( - + {!isPregame && ( @@ -112,34 +105,76 @@ export const MetaCoinAntagToken = () => { )}
- + {roles.map((role) => { const fallbackName = role.fallbackIcon || 'question-circle'; const fallbackNode = ; const roleDisabled = !canBuyToken || !role.available; const unavailableReasonText = role.unavailableReason || 'Role is unavailable right now.'; + const statusText = role.available + ? null + : role.unavailableCode === 'min_pop' + ? `Not enough population (${Number(role.minPopCurrent ?? 0)}/${Number(role.minPopRequired ?? 0)}).` + : unavailableReasonText; + const statusColor = role.available ? 'label' : 'bad'; const iconBorderColor = roleDisabled ? 'var(--color-red)' : 'var(--color-green)'; return ( - -
- act('buy_antag_token_role', { - roleId: role.id, - }) - } - > - Choose - - } + + + {role.name} + + + + { )} - {role.desc} - - {!role.available && role.unavailableCode === 'min_pop' && ( - - {`Not enough population (${Number(role.minPopCurrent ?? 0)}/${Number(role.minPopRequired ?? 0)}).`} - - )} - - {!role.available && role.unavailableCode !== 'min_pop' && ( - - {unavailableReasonText} - - )} -
-
+ {role.desc} + + + {!!statusText && ( + {statusText} + )} + +
+ ); })} -
+
From 4383906b04d1bc7c24271994b27ae04e8dabb902 Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Tue, 14 Apr 2026 03:48:10 +0300 Subject: [PATCH 05/18] simplify ctrl+f replace this shit --- .../deathmatch/deathmatch_controller.dm | 4 +- .../features/metacoins/code/metacoin.dm | 2 +- .../metacoins/code/metacoin_deathmatch.dm | 6 +- .../metacoins/code/metacoin_gambling.dm | 22 +-- .../features/metacoins/code/metacoin_shop.dm | 174 +++++++++--------- 5 files changed, 104 insertions(+), 104 deletions(-) diff --git a/code/modules/deathmatch/deathmatch_controller.dm b/code/modules/deathmatch/deathmatch_controller.dm index 275c7401b07f..1ed835f74854 100644 --- a/code/modules/deathmatch/deathmatch_controller.dm +++ b/code/modules/deathmatch/deathmatch_controller.dm @@ -35,11 +35,11 @@ entry_fee = min(max(round(text2num("[entry_fee]") || 0), 0), 1000) if(entry_fee > 0) - var/datum/metacoin_shop_controller/shop = get_metacoin_shop_controller() + var/datum/metacoin_shop_controller/shop = get_metacoin_controller() if(!shop) return list("ok" = FALSE, "error" = "shop_unavailable") - var/current_balance = shop.fetch_metacoin_balance(host.ckey) + var/current_balance = shop.fetch_balance(host.ckey) if(isnull(current_balance)) return list("ok" = FALSE, "error" = "db_unavailable") if(current_balance < entry_fee) diff --git a/modular_meta/features/metacoins/code/metacoin.dm b/modular_meta/features/metacoins/code/metacoin.dm index 658c303ff84e..0165ac0750a1 100644 --- a/modular_meta/features/metacoins/code/metacoin.dm +++ b/modular_meta/features/metacoins/code/metacoin.dm @@ -20,7 +20,7 @@ GLOBAL_DATUM(metacoins_controller, /datum/metacoins_controller) if(.) return get_metacoins_controller() - get_metacoin_shop_controller() + get_metacoin_controller() /datum/metacoins_controller var/list/roundstart_ready_ckeys = list() diff --git a/modular_meta/features/metacoins/code/metacoin_deathmatch.dm b/modular_meta/features/metacoins/code/metacoin_deathmatch.dm index e717b0a253ec..b91a2b66f79e 100644 --- a/modular_meta/features/metacoins/code/metacoin_deathmatch.dm +++ b/modular_meta/features/metacoins/code/metacoin_deathmatch.dm @@ -8,7 +8,7 @@ return TRUE var/to_pay = entry_fee - already_paid - var/datum/metacoin_shop_controller/shop = get_metacoin_shop_controller() + var/datum/metacoin_shop_controller/shop = get_metacoin_controller() if(!shop) to_chat(player, span_warning("Metacoin subsystem is unavailable.")) return FALSE @@ -39,7 +39,7 @@ fees_paid -= target_ckey return TRUE - var/datum/metacoin_shop_controller/shop = get_metacoin_shop_controller() + var/datum/metacoin_shop_controller/shop = get_metacoin_controller() if(!shop || !shop.add_metacoins(target_ckey, paid_amount)) log_game("Deathmatch lobby [host] failed to refund [paid_amount] metacoins to [target_ckey].") return FALSE @@ -59,7 +59,7 @@ var/payout_amount = prize_pool var/list/paid_snapshot = fees_paid?.Copy() || list() - var/datum/metacoin_shop_controller/shop = get_metacoin_shop_controller() + var/datum/metacoin_shop_controller/shop = get_metacoin_controller() if(winner_ckey && shop?.add_metacoins(winner_ckey, payout_amount)) announce(span_boldnicegreen("[winner ? winner.real_name : winner_ckey] received [payout_amount] metacoins from the prize pool.")) diff --git a/modular_meta/features/metacoins/code/metacoin_gambling.dm b/modular_meta/features/metacoins/code/metacoin_gambling.dm index 0b04986efa42..007c52ccdab5 100644 --- a/modular_meta/features/metacoins/code/metacoin_gambling.dm +++ b/modular_meta/features/metacoins/code/metacoin_gambling.dm @@ -171,7 +171,7 @@ if(!target_ckey) return list("ok" = FALSE, "error" = "invalid_request") - if(!is_preround_purchase_open() && !isobserver(request_user)) + if(!is_open() && !isobserver(request_user)) return list("ok" = FALSE, "error" = "shop_closed") if(slot_spin_locks_by_ckey[target_ckey]) @@ -192,7 +192,7 @@ var/list/result = list("ok" = FALSE, "error" = "unknown") - var/current_balance = fetch_metacoin_balance(target_ckey) + var/current_balance = fetch_balance(target_ckey) if(isnull(current_balance)) result["error"] = "db_unavailable" slot_spin_locks_by_ckey -= target_ckey @@ -219,7 +219,7 @@ return result qdel(debit_query) - var/post_debit_balance = fetch_metacoin_balance(target_ckey) + var/post_debit_balance = fetch_balance(target_ckey) if(isnull(post_debit_balance)) result["error"] = "db_failed" slot_spin_locks_by_ckey -= target_ckey @@ -253,7 +253,7 @@ return result qdel(payout_query) - var/final_balance = fetch_metacoin_balance(target_ckey) + var/final_balance = fetch_balance(target_ckey) if(isnull(final_balance)) result["error"] = "db_failed" slot_spin_locks_by_ckey -= target_ckey @@ -282,7 +282,7 @@ /datum/metacoin_slot_panel/New(client/owner, mob/viewer) src.owner = owner - current_reels = get_metacoin_shop_controller().roll_slot_reels() + current_reels = get_metacoin_controller().roll_slot_reels() last_spin = list( "lineLength" = 0, "payout" = 0, @@ -306,7 +306,7 @@ var/list/data = list() data["icons"] = list() - var/list/icons_catalog = get_metacoin_shop_controller().get_slot_icons_catalog() + var/list/icons_catalog = get_metacoin_controller().get_slot_icons_catalog() for(var/icon_name in icons_catalog) var/list/icon_info = icons_catalog[icon_name] data["icons"] += list(list( @@ -324,11 +324,11 @@ /datum/metacoin_slot_panel/ui_data(mob/user) var/list/data = list() - var/datum/metacoin_shop_controller/shop = get_metacoin_shop_controller() + var/datum/metacoin_shop_controller/shop = get_metacoin_controller() var/client_ckey = owner?.ckey - var/balance = shop.fetch_metacoin_balance(client_ckey) + var/balance = shop.fetch_balance(client_ckey) - data["isPregame"] = shop.is_preround_purchase_open() + data["isPregame"] = shop.is_open() data["isObserver"] = isobserver(user) data["working"] = working data["balance"] = isnull(balance) ? 0 : balance @@ -351,7 +351,7 @@ working = TRUE var/mob/user_mob = ui?.user - var/list/result = get_metacoin_shop_controller().try_slot_spin(owner?.ckey, user_mob) + var/list/result = get_metacoin_controller().try_slot_spin(owner?.ckey, user_mob) if(!result["ok"]) if(user_mob) @@ -421,7 +421,7 @@ if(payout_amount >= METACOIN_SLOT_PAYOUT_LINE5) var/winner_name = user_mob?.real_name || owner?.ckey || "Unknown" - get_metacoin_shop_controller().announce_slot_big_win(winner_name, payout_amount, jackpot_hit, user_mob) + get_metacoin_controller().announce_slot_big_win(winner_name, payout_amount, jackpot_hit, user_mob) if(user_mob) if(jackpot_hit) diff --git a/modular_meta/features/metacoins/code/metacoin_shop.dm b/modular_meta/features/metacoins/code/metacoin_shop.dm index 1c04c6ce32b6..2ad1c753cd17 100644 --- a/modular_meta/features/metacoins/code/metacoin_shop.dm +++ b/modular_meta/features/metacoins/code/metacoin_shop.dm @@ -1,12 +1,12 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) -/proc/get_metacoin_shop_controller() +/proc/get_metacoin_controller() if(!GLOB.metacoin_shop_controller) GLOB.metacoin_shop_controller = new /datum/metacoin_shop_controller() GLOB.metacoin_shop_controller.register_signals() return GLOB.metacoin_shop_controller -/proc/cmp_antag_role_ui(datum/metacoinshop/antag_role/a, datum/metacoinshop/antag_role/b) +/proc/cmp_antag_role(datum/metacoinshop/antag_role/a, datum/metacoinshop/antag_role/b) var/order_diff = cmp_numeric_asc(a.ui_order, b.ui_order) if(order_diff) return order_diff @@ -42,7 +42,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return signals_registered = TRUE - RegisterSignal(SSdcs, COMSIG_GLOB_JOB_AFTER_SPAWN, PROC_REF(on_job_after_spawn)) + RegisterSignal(SSdcs, COMSIG_GLOB_JOB_AFTER_SPAWN, PROC_REF(on_spawn)) SSticker.OnRoundstart(CALLBACK(src, PROC_REF(on_round_start))) SSticker.OnRoundend(CALLBACK(src, PROC_REF(on_round_end))) @@ -50,24 +50,24 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) preround_delivered_by_ckey = list() /datum/metacoin_shop_controller/proc/on_round_end() - refund_all_pending_antag_tokens() + refund_all_tokens() preround_pending_by_ckey = list() preround_delivered_by_ckey = list() antag_token_pending_by_ckey = list() antag_token_slots_left = 3 -/datum/metacoin_shop_controller/proc/is_preround_purchase_open() +/datum/metacoin_shop_controller/proc/is_open() if(!SSticker) return FALSE return SSticker.current_state == GAME_STATE_PREGAME -/datum/metacoin_shop_controller/proc/get_antag_token_listing() +/datum/metacoin_shop_controller/proc/get_token_listing() return preround_catalog["antag_token"] -/datum/metacoin_shop_controller/proc/get_antag_token_slots_left() +/datum/metacoin_shop_controller/proc/get_token_slots() return max(antag_token_slots_left, 0) -/datum/metacoin_shop_controller/proc/get_antag_token_restricted_jobs() +/datum/metacoin_shop_controller/proc/get_restricted_jobs() var/static/list/antag_token_restricted_jobs = list( JOB_CAPTAIN, JOB_HEAD_OF_PERSONNEL, @@ -94,26 +94,26 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return antag_token_restricted_jobs -/datum/metacoin_shop_controller/proc/is_antag_token_restricted_job(job_title) +/datum/metacoin_shop_controller/proc/is_restricted_job(job_title) if(!job_title) return FALSE - return job_title in get_antag_token_restricted_jobs() + return job_title in get_restricted_jobs() -/datum/metacoin_shop_controller/proc/get_antag_token_restricted_job_preferences_for_client(client/target_client) +/datum/metacoin_shop_controller/proc/get_restricted_prefs(client/target_client) var/list/restricted_preferences = list() var/list/job_preferences = target_client?.prefs?.job_preferences if(!islist(job_preferences)) return restricted_preferences - for(var/job_title in get_antag_token_restricted_jobs()) + for(var/job_title in get_restricted_jobs()) if(!isnull(job_preferences[job_title])) restricted_preferences += job_title return restricted_preferences -/datum/metacoin_shop_controller/proc/get_antag_token_restricted_job_preferences_warning_for_client(client/target_client) - var/list/restricted_preferences = get_antag_token_restricted_job_preferences_for_client(target_client) +/datum/metacoin_shop_controller/proc/get_restricted_warn(client/target_client) + var/list/restricted_preferences = get_restricted_prefs(target_client) if(!length(restricted_preferences)) return null @@ -126,7 +126,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) for(var/role_path in subtypesof(/datum/metacoinshop/antag_role)) antag_roles += new role_path - antag_roles = sort_list(antag_roles, GLOBAL_PROC_REF(cmp_antag_role_ui)) + antag_roles = sort_list(antag_roles, GLOBAL_PROC_REF(cmp_antag_role)) return antag_roles @@ -140,7 +140,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return null -/datum/metacoin_shop_controller/proc/get_antag_token_role_display_name(role_id) +/datum/metacoin_shop_controller/proc/get_role_name(role_id) if(!role_id) return null @@ -149,7 +149,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return null return role.name -/datum/metacoin_shop_controller/proc/dynamic_weight_has_positive_value(weight_setting) +/datum/metacoin_shop_controller/proc/has_weight(weight_setting) if(isnull(weight_setting)) return FALSE @@ -163,7 +163,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return FALSE -/datum/metacoin_shop_controller/proc/dynamic_resolve_min_pop(min_pop_setting, fallback_value) +/datum/metacoin_shop_controller/proc/resolve_min_pop(min_pop_setting, fallback_value) if(isnum(min_pop_setting)) return max(text2num("[min_pop_setting]"), 0) @@ -179,7 +179,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return max(text2num("[fallback_value]"), 0) -/datum/metacoin_shop_controller/proc/get_antag_token_role_block_info(target_ckey, role_id, datum/job/current_job = null) +/datum/metacoin_shop_controller/proc/get_role_block(target_ckey, role_id, datum/job/current_job = null) var/datum/metacoinshop/antag_role/role = get_antag_role(role_id) if(!role) return list("code" = "unknown_role") @@ -188,7 +188,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) if(target_ckey && is_banned_from(target_ckey, list(ROLE_SYNDICATE, role_ban_flag))) return list("code" = "job_banned") - if(current_job && is_antag_token_restricted_job(current_job.title)) + if(current_job && is_restricted_job(current_job.title)) return list( "code" = "restricted_job", "job_title" = current_job.title, @@ -201,13 +201,13 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) var/ruleset_tag = role.ruleset_tag var/list/ruleset_config = SSdynamic.get_config()?[ruleset_tag] - if(!isnull(ruleset_config?["weight"]) && !dynamic_weight_has_positive_value(ruleset_config["weight"])) + if(!isnull(ruleset_config?["weight"]) && !has_weight(ruleset_config["weight"])) return list("code" = "disabled_by_config") if(!isnull(ruleset_config?["min_pop"])) min_pop_setting = ruleset_config["min_pop"] - var/min_pop = dynamic_resolve_min_pop(min_pop_setting, default_min_pop) + var/min_pop = resolve_min_pop(min_pop_setting, default_min_pop) var/current_population = length(GLOB.new_player_list) if(current_population < min_pop) return list( @@ -218,7 +218,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return null -/datum/metacoin_shop_controller/proc/get_antag_token_role_block_text(list/block_info) +/datum/metacoin_shop_controller/proc/get_block_text(list/block_info) if(!islist(block_info)) return null @@ -242,12 +242,12 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return "Role is currently unavailable." -/datum/metacoin_shop_controller/proc/get_antag_token_roles_ui_data(target_ckey) +/datum/metacoin_shop_controller/proc/get_roles_ui(target_ckey) var/list/roles_ui_data = list() for(var/datum/metacoinshop/antag_role/role as anything in get_antag_roles()) var/role_id = role.id - var/list/block_info = get_antag_token_role_block_info(target_ckey, role_id) + var/list/block_info = get_role_block(target_ckey, role_id) roles_ui_data += list(list( "id" = role_id, @@ -256,7 +256,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) "prefIconClass" = role_id, "fallbackIcon" = default_listing_fallback_icon, "available" = isnull(block_info), - "unavailableReason" = get_antag_token_role_block_text(block_info), + "unavailableReason" = get_block_text(block_info), "unavailableCode" = block_info?["code"], "minPopCurrent" = block_info?["current_pop"], "minPopRequired" = block_info?["required_pop"], @@ -264,7 +264,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return roles_ui_data -/datum/metacoin_shop_controller/proc/refund_antag_token_purchase(target_ckey, failure_text, mob/notify_mob) +/datum/metacoin_shop_controller/proc/refund_token(target_ckey, failure_text, mob/notify_mob) if(!target_ckey) return FALSE @@ -272,7 +272,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) log_game("[src] antag token refund skipped for [target_ckey]: no pending reservation.") return FALSE - var/datum/metacoinshop/listing/antag_listing = get_antag_token_listing() + var/datum/metacoinshop/listing/antag_listing = get_token_listing() var/refund_amount = antag_listing?.price || 0 antag_token_pending_by_ckey -= target_ckey @@ -293,36 +293,36 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) notify_mob.playsound_local(notify_mob, 'sound/machines/compiler/compiler-failure.ogg', 40, TRUE, use_reverb = FALSE) else log_game("[src] antag token refund notify deferred for [target_ckey]: no client on notify mob.") - addtimer(CALLBACK(src, PROC_REF(retry_notify_antag_token_result), target_ckey, message, 20), 1 SECONDS) + addtimer(CALLBACK(src, PROC_REF(retry_refund_notice), target_ckey, message, 20), 1 SECONDS) return TRUE -/datum/metacoin_shop_controller/proc/retry_notify_antag_token_result(target_ckey, message, attempts_left) +/datum/metacoin_shop_controller/proc/retry_refund_notice(target_ckey, message, attempts_left) if(!target_ckey || !message) return var/mob/target_mob = get_mob_by_ckey(target_ckey) if(!target_mob?.client) if(attempts_left > 0) - addtimer(CALLBACK(src, PROC_REF(retry_notify_antag_token_result), target_ckey, message, attempts_left - 1), 0.5 SECONDS) + addtimer(CALLBACK(src, PROC_REF(retry_refund_notice), target_ckey, message, attempts_left - 1), 0.5 SECONDS) return to_chat(target_mob, span_warning(message)) target_mob.playsound_local(target_mob, 'sound/machines/compiler/compiler-failure.ogg', 40, TRUE, use_reverb = FALSE) -/datum/metacoin_shop_controller/proc/refund_all_pending_antag_tokens() +/datum/metacoin_shop_controller/proc/refund_all_tokens() if(!length(antag_token_pending_by_ckey)) return var/list/ckeys_to_refund = antag_token_pending_by_ckey.Copy() for(var/target_ckey in ckeys_to_refund) - refund_antag_token_purchase(target_ckey, null, null) + refund_token(target_ckey, null, null) -/datum/metacoin_shop_controller/proc/get_catalog_ui_data(target_ckey) +/datum/metacoin_shop_controller/proc/get_catalog_ui(target_ckey) var/list/catalog_data = list() - var/list/pending_items = get_pending_item_ids(target_ckey) + var/list/pending_items = get_pending_items(target_ckey) var/selected_antag_role = antag_token_pending_by_ckey[target_ckey] - var/balance = fetch_metacoin_balance(target_ckey) + var/balance = fetch_balance(target_ckey) for(var/listing_id in preround_catalog) var/datum/metacoinshop/listing/listing = preround_catalog[listing_id] @@ -350,15 +350,15 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) ) if(is_antag_token) - listing_payload["tokensLeft"] = get_antag_token_slots_left() + listing_payload["tokensLeft"] = get_token_slots() listing_payload["selectedRole"] = selected_antag_role - listing_payload["selectedRoleName"] = get_antag_token_role_display_name(selected_antag_role) + listing_payload["selectedRoleName"] = get_role_name(selected_antag_role) catalog_data += list(listing_payload) return catalog_data -/datum/metacoin_shop_controller/proc/get_pending_item_ids(target_ckey) +/datum/metacoin_shop_controller/proc/get_pending_items(target_ckey) if(!target_ckey) return list() @@ -368,7 +368,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return pending_items.Copy() -/datum/metacoin_shop_controller/proc/fetch_metacoin_balance(target_ckey) +/datum/metacoin_shop_controller/proc/fetch_balance(target_ckey) if(!target_ckey) return 0 @@ -423,7 +423,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) if(!SSdbcore.Connect()) return list("ok" = FALSE, "error" = "db_unavailable") - var/current_balance = fetch_metacoin_balance(target_ckey) + var/current_balance = fetch_balance(target_ckey) if(isnull(current_balance)) return list("ok" = FALSE, "error" = "db_unavailable") @@ -444,7 +444,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return list("ok" = FALSE, "error" = "db_failed") qdel(take_query) - var/new_balance = fetch_metacoin_balance(target_ckey) + var/new_balance = fetch_balance(target_ckey) if(isnull(new_balance)) return list("ok" = FALSE, "error" = "db_failed") @@ -456,11 +456,11 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) "balance" = new_balance, ) -/datum/metacoin_shop_controller/proc/try_purchase_preround_item(target_ckey, item_id) +/datum/metacoin_shop_controller/proc/buy_item(target_ckey, item_id) if(!target_ckey || !item_id) return list("ok" = FALSE, "error" = "invalid_request") - if(!is_preround_purchase_open()) + if(!is_open()) return list("ok" = FALSE, "error" = "shop_closed") var/datum/metacoinshop/listing/listing = preround_catalog[item_id] @@ -481,7 +481,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) if(!SSdbcore.Connect()) return list("ok" = FALSE, "error" = "db_unavailable") - var/current_balance = fetch_metacoin_balance(target_ckey) + var/current_balance = fetch_balance(target_ckey) if(isnull(current_balance)) return list("ok" = FALSE, "error" = "db_unavailable") @@ -502,7 +502,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return list("ok" = FALSE, "error" = "db_failed") qdel(buy_query) - var/new_balance = fetch_metacoin_balance(target_ckey) + var/new_balance = fetch_balance(target_ckey) if(isnull(new_balance)) return list("ok" = FALSE, "error" = "db_failed") @@ -519,31 +519,31 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return list("ok" = TRUE) -/datum/metacoin_shop_controller/proc/try_purchase_antag_token(target_ckey, role_id) +/datum/metacoin_shop_controller/proc/buy_token(target_ckey, role_id) if(!target_ckey || !role_id) return list("ok" = FALSE, "error" = "invalid_request") - if(!is_preround_purchase_open()) + if(!is_open()) return list("ok" = FALSE, "error" = "shop_closed") if(antag_token_pending_by_ckey[target_ckey]) return list("ok" = FALSE, "error" = "already_owned") - if(get_antag_token_slots_left() <= 0) + if(get_token_slots() <= 0) return list("ok" = FALSE, "error" = "sold_out") - var/list/block_info = get_antag_token_role_block_info(target_ckey, role_id) + var/list/block_info = get_role_block(target_ckey, role_id) if(block_info) return list("ok" = FALSE, "error" = block_info["code"]) - var/datum/metacoinshop/listing/listing = get_antag_token_listing() + var/datum/metacoinshop/listing/listing = get_token_listing() if(!listing) return list("ok" = FALSE, "error" = "unknown_item") if(!SSdbcore.Connect()) return list("ok" = FALSE, "error" = "db_unavailable") - var/current_balance = fetch_metacoin_balance(target_ckey) + var/current_balance = fetch_balance(target_ckey) if(isnull(current_balance)) return list("ok" = FALSE, "error" = "db_unavailable") @@ -564,7 +564,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return list("ok" = FALSE, "error" = "db_failed") qdel(buy_query) - var/new_balance = fetch_metacoin_balance(target_ckey) + var/new_balance = fetch_balance(target_ckey) if(isnull(new_balance)) return list("ok" = FALSE, "error" = "db_failed") @@ -573,7 +573,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) antag_token_pending_by_ckey[target_ckey] = role_id antag_token_slots_left = max(antag_token_slots_left - 1, 0) - var/role_name = get_antag_token_role_display_name(role_id) + var/role_name = get_role_name(role_id) log_game("[src] antag token purchase: ckey=[target_ckey], role=[role_id]/[role_name], price=[listing.price], balance_before=[current_balance], balance_after=[new_balance], slots_left=[antag_token_slots_left].") var/mob/player_mob = get_mob_by_ckey(target_ckey) @@ -603,7 +603,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return conflicts -/datum/metacoin_shop_controller/proc/try_grant_antag_token_after_spawn(target_ckey, mob/living/spawned, client/player_client) +/datum/metacoin_shop_controller/proc/grant_token_on_spawn(target_ckey, mob/living/spawned, client/player_client) if(!target_ckey) return @@ -625,31 +625,31 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) log_rulesets = "unknown" var/failure_text = "Antag token was refunded due to Dynamic subsystem role assignment ([conflict_text])." log_game("[src] antag token grant canceled for [target_ckey]: code=dynamic_interference, role=[selected_role], rulesets=[log_rulesets], job=[current_job?.title].") - refund_antag_token_purchase(target_ckey, failure_text, notify_mob) + refund_token(target_ckey, failure_text, notify_mob) return - var/list/block_info = get_antag_token_role_block_info(target_ckey, selected_role, current_job) + var/list/block_info = get_role_block(target_ckey, selected_role, current_job) if(block_info) - var/failure_text = "Antag token could not be applied: [get_antag_token_role_block_text(block_info)]" + var/failure_text = "Antag token could not be applied: [get_block_text(block_info)]" log_game("[src] antag token grant blocked for [target_ckey]: code=[block_info["code"]], job=[current_job?.title].") - refund_antag_token_purchase(target_ckey, failure_text, notify_mob) + refund_token(target_ckey, failure_text, notify_mob) return if(!ishuman(spawned)) log_game("[src] antag token grant failed for [target_ckey]: spawned mob is not human ([spawned?.type]).") - refund_antag_token_purchase(target_ckey, "Antag token requires a human roundstart spawn.", notify_mob) + refund_token(target_ckey, "Antag token requires a human roundstart spawn.", notify_mob) return var/mob/living/carbon/human/human_spawned = spawned if(!human_spawned.mind) log_game("[src] antag token grant failed for [target_ckey]: human has no mind.") - refund_antag_token_purchase(target_ckey, "Antag token failed: no valid player mind found.", notify_mob) + refund_token(target_ckey, "Antag token failed: no valid player mind found.", notify_mob) return var/datum/metacoinshop/antag_role/role = get_antag_role(selected_role) if(!role) log_game("[src] antag token grant failed for [target_ckey]: invalid role definition '[selected_role]'.") - refund_antag_token_purchase(target_ckey, "Antag token failed: selected role is invalid.", notify_mob) + refund_token(target_ckey, "Antag token failed: selected role is invalid.", notify_mob) return var/antag_datum_path = role.antag_datum @@ -660,10 +660,10 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) var/datum/antagonist/granted_antag = human_spawned.mind.has_antag_datum(antag_datum_path, TRUE) if(!granted_antag) log_game("[src] antag token grant failed for [target_ckey]: antag datum [antag_datum_path] not present after add.") - refund_antag_token_purchase(target_ckey, "Antag token failed to grant the selected role.", notify_mob) + refund_token(target_ckey, "Antag token failed to grant the selected role.", notify_mob) return - addtimer(CALLBACK(src, PROC_REF(retry_show_antag_token_intro), target_ckey, granted_antag, 20), 1 SECONDS) + addtimer(CALLBACK(src, PROC_REF(retry_intro), target_ckey, granted_antag, 20), 1 SECONDS) antag_token_pending_by_ckey -= target_ckey log_game("[src] antag token grant success for [target_ckey]: role=[selected_role], slots_left=[antag_token_slots_left].") @@ -676,7 +676,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) */ SStgui.update_uis(src) -/datum/metacoin_shop_controller/proc/retry_show_antag_token_intro(target_ckey, datum/antagonist/granted_antag, attempts_left) +/datum/metacoin_shop_controller/proc/retry_intro(target_ckey, datum/antagonist/granted_antag, attempts_left) if(!target_ckey || !granted_antag || QDELETED(granted_antag)) return @@ -686,13 +686,13 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) if(!player_mob?.client) if(attempts_left > 0) - addtimer(CALLBACK(src, PROC_REF(retry_show_antag_token_intro), target_ckey, granted_antag, attempts_left - 1), 0.5 SECONDS) + addtimer(CALLBACK(src, PROC_REF(retry_intro), target_ckey, granted_antag, attempts_left - 1), 0.5 SECONDS) return var/datum/action/antag_info/info_button = granted_antag.info_button_ref?.resolve() if(granted_antag.ui_name && !info_button) if(attempts_left > 0) - addtimer(CALLBACK(src, PROC_REF(retry_show_antag_token_intro), target_ckey, granted_antag, attempts_left - 1), 0.5 SECONDS) + addtimer(CALLBACK(src, PROC_REF(retry_intro), target_ckey, granted_antag, attempts_left - 1), 0.5 SECONDS) return granted_antag.silent = FALSE @@ -706,7 +706,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) if(type_policy) to_chat(player_mob, type_policy) -/datum/metacoin_shop_controller/proc/on_job_after_spawn(datum/source, datum/job/job, mob/living/spawned, client/player_client) +/datum/metacoin_shop_controller/proc/on_spawn(datum/source, datum/job/job, mob/living/spawned, client/player_client) SIGNAL_HANDLER if(!player_client) @@ -715,16 +715,16 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) var/target_ckey = ckey(player_client.ckey) var/selected_role = antag_token_pending_by_ckey[target_ckey] if(selected_role) - log_game("[src] on_job_after_spawn for token owner [target_ckey]: role=[selected_role], state=[SSticker?.current_state], round_started=[SSticker?.HasRoundStarted()], job=[job?.title], assigned=[spawned?.mind?.assigned_role?.title].") + log_game("[src] on_spawn for token owner [target_ckey]: role=[selected_role], state=[SSticker?.current_state], round_started=[SSticker?.HasRoundStarted()], job=[job?.title], assigned=[spawned?.mind?.assigned_role?.title].") if(SSticker?.HasRoundStarted()) if(selected_role) - log_game("[src] skipping antag token grant for [target_ckey]: round already started in on_job_after_spawn.") + log_game("[src] skipping antag token grant for [target_ckey]: round already started in on_spawn.") return if(!target_ckey) return - try_grant_antag_token_after_spawn(target_ckey, spawned, player_client) + grant_token_on_spawn(target_ckey, spawned, player_client) if(!ishuman(spawned)) return @@ -778,13 +778,13 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) /datum/metacoin_shop_panel/ui_data(mob/user) var/list/data = list() var/client_ckey = owner?.ckey - var/datum/metacoin_shop_controller/shop = get_metacoin_shop_controller() - var/balance = shop.fetch_metacoin_balance(client_ckey) + var/datum/metacoin_shop_controller/shop = get_metacoin_controller() + var/balance = shop.fetch_balance(client_ckey) - data["isPregame"] = shop.is_preround_purchase_open() + data["isPregame"] = shop.is_open() data["balance"] = isnull(balance) ? 0 : balance - data["antagTokenSlotsLeft"] = shop.get_antag_token_slots_left() - data["preroundItems"] = shop.get_catalog_ui_data(client_ckey) + data["antagTokenSlotsLeft"] = shop.get_token_slots() + data["preroundItems"] = shop.get_catalog_ui(client_ckey) data["persistentItems"] = list() return data @@ -807,7 +807,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) if(!target_item) return FALSE - var/result = get_metacoin_shop_controller().try_purchase_preround_item(owner?.ckey, target_item) + var/result = get_metacoin_controller().buy_item(owner?.ckey, target_item) if(!result["ok"]) var/mob/user_mob = ui?.user if(user_mob) @@ -857,21 +857,21 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) /datum/metacoin_antag_token_panel/ui_data(mob/user) var/list/data = list() var/client_ckey = owner?.ckey - var/datum/metacoin_shop_controller/shop = get_metacoin_shop_controller() - var/balance = shop.fetch_metacoin_balance(client_ckey) + var/datum/metacoin_shop_controller/shop = get_metacoin_controller() + var/balance = shop.fetch_balance(client_ckey) var/selected_role = shop.antag_token_pending_by_ckey[client_ckey] - var/datum/metacoinshop/listing/antag_listing = shop.get_antag_token_listing() + var/datum/metacoinshop/listing/antag_listing = shop.get_token_listing() - data["isPregame"] = shop.is_preround_purchase_open() + data["isPregame"] = shop.is_open() data["balance"] = isnull(balance) ? 0 : balance data["price"] = antag_listing?.price || 40 - data["slotsLeft"] = shop.get_antag_token_slots_left() + data["slotsLeft"] = shop.get_token_slots() data["alreadyPurchased"] = !isnull(selected_role) data["selectedRole"] = selected_role - data["selectedRoleName"] = shop.get_antag_token_role_display_name(selected_role) - data["roles"] = shop.get_antag_token_roles_ui_data(client_ckey) - data["restrictedJobPreferences"] = shop.get_antag_token_restricted_job_preferences_for_client(owner) - data["restrictedJobWarning"] = shop.get_antag_token_restricted_job_preferences_warning_for_client(owner) + data["selectedRoleName"] = shop.get_role_name(selected_role) + data["roles"] = shop.get_roles_ui(client_ckey) + data["restrictedJobPreferences"] = shop.get_restricted_prefs(owner) + data["restrictedJobWarning"] = shop.get_restricted_warn(owner) return data @@ -885,7 +885,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) if(!role_id) return FALSE - var/result = get_metacoin_shop_controller().try_purchase_antag_token(owner?.ckey, role_id) + var/result = get_metacoin_controller().buy_token(owner?.ckey, role_id) if(result["ok"]) return TRUE From 00563f754d96296189681a7a4e25120bfbe59e42 Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Tue, 14 Apr 2026 04:53:02 +0300 Subject: [PATCH 06/18] =?UTF-8?q?=D0=BD=D0=B5=20=D0=BF=D0=BE=D0=BD=D1=8F?= =?UTF-8?q?=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- code/modules/deathmatch/deathmatch_controller.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/deathmatch/deathmatch_controller.dm b/code/modules/deathmatch/deathmatch_controller.dm index 1ed835f74854..bd3d81da510d 100644 --- a/code/modules/deathmatch/deathmatch_controller.dm +++ b/code/modules/deathmatch/deathmatch_controller.dm @@ -52,7 +52,7 @@ lobbies[host.ckey] = new_lobby deadchat_broadcast(" has opened a new deathmatch lobby. (Join)", "[host]") return list("ok" = TRUE) -//MASSMETA EDIT CHANGE START (metacoins) +//MASSMETA EDIT CHANGE END (metacoins) /datum/deathmatch_controller/proc/remove_lobby(ckey) var/lobby = lobbies[ckey] From 726cb542d591c776c085199bc744e04046ea9cc7 Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Tue, 14 Apr 2026 23:43:55 +0300 Subject: [PATCH 07/18] UI .scss --- .../metacoins/code/metacoin_gambling.dm | 381 +++++---- modular_meta/features/metacoins/readme.md | 1 + .../packages/tgui/interfaces/MetaCoinSlot.tsx | 790 +++++++++++++----- .../tgui/styles/interfaces/MetaCoinSlot.scss | 288 +++++++ tgui/packages/tgui/styles/main.scss | 3 + 5 files changed, 1061 insertions(+), 402 deletions(-) create mode 100644 tgui/packages/tgui/styles/interfaces/MetaCoinSlot.scss diff --git a/modular_meta/features/metacoins/code/metacoin_gambling.dm b/modular_meta/features/metacoins/code/metacoin_gambling.dm index 007c52ccdab5..3c3cf5b58408 100644 --- a/modular_meta/features/metacoins/code/metacoin_gambling.dm +++ b/modular_meta/features/metacoins/code/metacoin_gambling.dm @@ -1,16 +1,20 @@ -#define METACOIN_SLOT_SPIN_COST 5 -#define METACOIN_SLOT_PAYOUT_LINE3 15 -#define METACOIN_SLOT_PAYOUT_LINE4 50 +#define METACOIN_SLOT_BASE_BET 5 +#define METACOIN_SLOT_MIN_BET 5 +#define METACOIN_SLOT_MAX_BET 100 +#define METACOIN_SLOT_DEFAULT_BET 5 +#define METACOIN_SLOT_BET_STEP 5 +#define METACOIN_SLOT_PAYOUT_LINE3 20 +#define METACOIN_SLOT_PAYOUT_LINE4 55 #define METACOIN_SLOT_PAYOUT_LINE5 150 -#define METACOIN_SLOT_PAYOUT_JACKPOT 1000 +#define METACOIN_SLOT_PAYOUT_JACKPOT 666 #define METACOIN_SLOT_COOLDOWN_DS 5 #define METACOIN_SLOT_JACKPOT_ICON FA_ICON_7 /datum/metacoin_shop_controller - var/list/slot_spin_locks_by_ckey = list() - var/list/slot_next_spin_time_by_ckey = list() + var/list/lock_by_ckey = list() + var/list/cd_by_ckey = list() -/datum/metacoin_shop_controller/proc/get_slot_icons_catalog() +/datum/metacoin_shop_controller/proc/get_icons() var/static/list/slot_icons = list( FA_ICON_LEMON = list("colour" = "yellow"), FA_ICON_STAR = list("colour" = "yellow"), @@ -22,110 +26,121 @@ ) return slot_icons -/datum/metacoin_shop_controller/proc/roll_slot_reels() - var/list/icons_catalog = get_slot_icons_catalog() +/datum/metacoin_shop_controller/proc/roll_reels() + var/list/icons = get_icons() var/list/reels = list() for(var/reel_index in 1 to 5) var/list/reel = list() for(var/row_index in 1 to 3) - var/chosen_icon_name = pick(icons_catalog) - var/list/icon_data = icons_catalog[chosen_icon_name] + var/icon_name = pick(icons) reel += list(list( - "icon_name" = chosen_icon_name, - "colour" = icon_data["colour"] || "white", + "icon_name" = icon_name, + "colour" = icons[icon_name]["colour"] || "white", )) reels += list(reel) return reels -/datum/metacoin_shop_controller/proc/get_slot_longest_line(list/reels) - if(!islist(reels) || length(reels) < 5) - return 0 - - var/best_line = 0 +/datum/metacoin_shop_controller/proc/get_line(list/reels) + var/best = 0 for(var/row_index in 1 to 3) - var/current_streak = 0 - var/last_icon_name + var/streak = 0 + var/last_icon for(var/reel_index in 1 to 5) - var/list/reel = reels[reel_index] - if(!islist(reel) || length(reel) < row_index) - current_streak = 0 - last_icon_name = null - continue - - var/list/symbol = reel[row_index] - var/icon_name = symbol?["icon_name"] - if(isnull(icon_name)) - current_streak = 0 - last_icon_name = null - continue - - if(icon_name == last_icon_name) - current_streak++ + var/icon = reels[reel_index][row_index]["icon_name"] + if(icon == last_icon) + streak++ else - current_streak = 1 - last_icon_name = icon_name + streak = 1 + last_icon = icon - best_line = max(best_line, current_streak) + best = max(best, streak) - return best_line + return best -/datum/metacoin_shop_controller/proc/is_slot_jackpot(list/reels) - if(!islist(reels) || length(reels) < 5) - return FALSE - - var/jackpot_icon_name = "[METACOIN_SLOT_JACKPOT_ICON]" +/datum/metacoin_shop_controller/proc/is_jackpot(list/reels) + var/icon = "[METACOIN_SLOT_JACKPOT_ICON]" for(var/reel_index in 1 to 5) - var/list/reel = reels[reel_index] - if(!islist(reel) || length(reel) < 2) - return FALSE - - var/list/symbol = reel[2] - if(symbol?["icon_name"] != jackpot_icon_name) + if(reels[reel_index][2]["icon_name"] != icon) return FALSE return TRUE -/datum/metacoin_shop_controller/proc/get_slot_payout(line_length, is_jackpot) - if(is_jackpot) +/datum/metacoin_shop_controller/proc/get_base_win(line, jackpot) + if(jackpot) return METACOIN_SLOT_PAYOUT_JACKPOT - if(line_length >= 5) + if(line >= 5) return METACOIN_SLOT_PAYOUT_LINE5 - if(line_length >= 4) + if(line >= 4) return METACOIN_SLOT_PAYOUT_LINE4 - if(line_length >= 3) + if(line >= 3) return METACOIN_SLOT_PAYOUT_LINE3 return 0 -/datum/metacoin_shop_controller/proc/get_slot_cooldown_left_ds(target_ckey) - target_ckey = ckey(target_ckey) - if(!target_ckey) +/datum/metacoin_shop_controller/proc/get_win(line, jackpot, bet) + var/base = get_base_win(line, jackpot) + if(base <= 0) + return 0 + + return round((base * bet) / METACOIN_SLOT_BASE_BET) + +/datum/metacoin_shop_controller/proc/normalize_bet(raw_bet) + var/bet = METACOIN_SLOT_DEFAULT_BET + if(isnull(raw_bet)) + return bet + + if(isnum(raw_bet)) + bet = round(raw_bet) + else if(istext(raw_bet)) + bet = round(text2num(raw_bet)) + else + return null + + if(bet < METACOIN_SLOT_MIN_BET || bet > METACOIN_SLOT_MAX_BET) + return null + + if((bet - METACOIN_SLOT_MIN_BET) % METACOIN_SLOT_BET_STEP) + return null + + return bet + +/datum/metacoin_shop_controller/proc/get_cd(ck) + ck = ckey(ck) + if(!ck) return 0 - var/next_spin_time = text2num(slot_next_spin_time_by_ckey[target_ckey]) || 0 - if(next_spin_time <= world.time) + var/next = cd_by_ckey[ck] || 0 + if(next <= world.time) return 0 - return max(next_spin_time - world.time, 0) + return next - world.time -/datum/metacoin_shop_controller/proc/announce_slot_big_win(winner_name, payout_amount, jackpot_hit, atom/winner_source) - if(!winner_name || payout_amount <= 0) +/datum/metacoin_shop_controller/proc/unlock_spin(ck) + ck = ckey(ck) + if(!ck) + return + + lock_by_ckey -= ck + + +/datum/metacoin_shop_controller/proc/announce_win(winner_name, payout, line, jackpot, atom/winner_source) + if(!winner_name || payout <= 0) return var/announce_text var/announce_title var/announce_sound - if(jackpot_hit) - announce_text = "[winner_name] hit the metacoin slot JACKPOT and won [payout_amount] metacoins!" + if(jackpot) + announce_text = "[winner_name] hit the metacoin slot JACKPOT and won [payout] metacoins!" announce_title = "Metacoin Slot Jackpot" announce_sound = 'sound/machines/roulette/roulettejackpot.ogg' - else if(payout_amount >= METACOIN_SLOT_PAYOUT_LINE5) - announce_text = "[winner_name] won a huge metacoin slot payout: [payout_amount] metacoins!" + else if(line >= 5) + announce_text = "[winner_name] won a huge metacoin slot payout: [payout] metacoins!" announce_title = "Metacoin Slot Big Win" announce_sound = 'sound/effects/kaching.ogg' else @@ -166,131 +181,132 @@ header = announce_title ) -/datum/metacoin_shop_controller/proc/try_slot_spin(target_ckey, mob/request_user) - target_ckey = ckey(target_ckey) - if(!target_ckey) +/datum/metacoin_shop_controller/proc/spin(ck, mob/user, raw_bet) + ck = ckey(ck) + if(!ck) return list("ok" = FALSE, "error" = "invalid_request") - if(!is_open() && !isobserver(request_user)) + if(!is_open() && !isobserver(user)) return list("ok" = FALSE, "error" = "shop_closed") - if(slot_spin_locks_by_ckey[target_ckey]) + var/lock_until = lock_by_ckey[ck] || 0 + if(lock_until > world.time) return list("ok" = FALSE, "error" = "busy") + if(lock_until) + lock_by_ckey -= ck if(!SSdbcore.Connect()) return list("ok" = FALSE, "error" = "db_unavailable") - var/cooldown_left_ds = get_slot_cooldown_left_ds(target_ckey) - if(cooldown_left_ds > 0) + var/cd = get_cd(ck) + if(cd > 0) return list( "ok" = FALSE, "error" = "cooldown", - "cooldownLeftDs" = cooldown_left_ds, + "cooldownLeftDs" = cd, ) - slot_spin_locks_by_ckey[target_ckey] = TRUE + var/bet = normalize_bet(raw_bet) + if(isnull(bet)) + return list("ok" = FALSE, "error" = "invalid_bet") - var/list/result = list("ok" = FALSE, "error" = "unknown") + lock_by_ckey[ck] = world.time + 40 - var/current_balance = fetch_balance(target_ckey) - if(isnull(current_balance)) - result["error"] = "db_unavailable" - slot_spin_locks_by_ckey -= target_ckey - return result + var/balance = fetch_balance(ck) + if(isnull(balance)) + lock_by_ckey -= ck + return list("ok" = FALSE, "error" = "db_unavailable") - if(current_balance < METACOIN_SLOT_SPIN_COST) - result["error"] = "not_enough" - slot_spin_locks_by_ckey -= target_ckey - return result + if(balance < bet) + lock_by_ckey -= ck + return list("ok" = FALSE, "error" = "not_enough") var/table_player = format_table_name("player") var/datum/db_query/debit_query = SSdbcore.NewQuery( "UPDATE [table_player] SET metacoins = metacoins - :price WHERE ckey = :ckey AND metacoins >= :price", list( - "price" = METACOIN_SLOT_SPIN_COST, - "ckey" = target_ckey, + "price" = bet, + "ckey" = ck, ), ) if(!debit_query.warn_execute(async = FALSE)) qdel(debit_query) - result["error"] = "db_failed" - slot_spin_locks_by_ckey -= target_ckey - return result + lock_by_ckey -= ck + return list("ok" = FALSE, "error" = "db_failed") qdel(debit_query) - var/post_debit_balance = fetch_balance(target_ckey) - if(isnull(post_debit_balance)) - result["error"] = "db_failed" - slot_spin_locks_by_ckey -= target_ckey - return result + var/debit_balance = fetch_balance(ck) + if(isnull(debit_balance)) + lock_by_ckey -= ck + return list("ok" = FALSE, "error" = "db_failed") - if(post_debit_balance > (current_balance - METACOIN_SLOT_SPIN_COST)) - result["error"] = "not_enough" - slot_spin_locks_by_ckey -= target_ckey - return result + if(debit_balance > (balance - bet)) + lock_by_ckey -= ck + return list("ok" = FALSE, "error" = "not_enough") - slot_next_spin_time_by_ckey[target_ckey] = world.time + METACOIN_SLOT_COOLDOWN_DS + cd_by_ckey[ck] = world.time + METACOIN_SLOT_COOLDOWN_DS - var/list/reels = roll_slot_reels() - var/line_length = get_slot_longest_line(reels) - var/is_jackpot = is_slot_jackpot(reels) - var/payout = get_slot_payout(line_length, is_jackpot) + var/list/reels = roll_reels() + var/line = get_line(reels) + var/jackpot = is_jackpot(reels) + var/payout = get_win(line, jackpot, bet) if(payout > 0) var/datum/db_query/payout_query = SSdbcore.NewQuery( "UPDATE [table_player] SET metacoins = metacoins + :amount WHERE ckey = :ckey", list( "amount" = payout, - "ckey" = target_ckey, + "ckey" = ck, ), ) if(!payout_query.warn_execute(async = FALSE)) qdel(payout_query) - result["error"] = "db_failed" - slot_spin_locks_by_ckey -= target_ckey - return result + lock_by_ckey -= ck + return list("ok" = FALSE, "error" = "db_failed") qdel(payout_query) - var/final_balance = fetch_balance(target_ckey) - if(isnull(final_balance)) - result["error"] = "db_failed" - slot_spin_locks_by_ckey -= target_ckey - return result + var/end_balance = fetch_balance(ck) + if(isnull(end_balance)) + lock_by_ckey -= ck + return list("ok" = FALSE, "error" = "db_failed") - result = list( + addtimer(CALLBACK(src, PROC_REF(unlock_spin), ck), 40) + return list( "ok" = TRUE, "reels" = reels, - "lineLength" = line_length, - "isJackpot" = is_jackpot, + "lineLength" = line, + "isJackpot" = jackpot, "payout" = payout, - "cost" = METACOIN_SLOT_SPIN_COST, - "balance" = final_balance, - "cooldownLeftDs" = get_slot_cooldown_left_ds(target_ckey), + "bet" = bet, + "balance" = end_balance, + "cooldownLeftDs" = get_cd(ck), ) - slot_spin_locks_by_ckey -= target_ckey - return result - /datum/metacoin_slot_panel var/client/owner - var/list/current_reels = list() + var/list/reels = list() var/working = FALSE - var/list/last_spin = list() - var/list/spin_history = list() + var/balance = 0 + var/list/last = list() + var/list/history = list() /datum/metacoin_slot_panel/New(client/owner, mob/viewer) src.owner = owner - current_reels = get_metacoin_controller().roll_slot_reels() - last_spin = list( + reels = get_metacoin_controller().roll_reels() + last = list( + "bet" = METACOIN_SLOT_DEFAULT_BET, "lineLength" = 0, "payout" = 0, "isJackpot" = FALSE, "net" = 0, "resultState" = "idle", ) - spin_history = list() + history = list() + balance = get_metacoin_controller().fetch_balance(owner?.ckey) + if(isnull(balance)) + balance = 0 ui_interact(viewer) /datum/metacoin_slot_panel/ui_state() @@ -306,15 +322,20 @@ var/list/data = list() data["icons"] = list() - var/list/icons_catalog = get_metacoin_controller().get_slot_icons_catalog() - for(var/icon_name in icons_catalog) - var/list/icon_info = icons_catalog[icon_name] + var/list/icons = get_metacoin_controller().get_icons() + for(var/icon_name in icons) + var/list/icon_info = icons[icon_name] data["icons"] += list(list( "icon_name" = icon_name, "colour" = icon_info["colour"], )) - data["cost"] = METACOIN_SLOT_SPIN_COST + data["baseBet"] = METACOIN_SLOT_BASE_BET + data["defaultBet"] = METACOIN_SLOT_DEFAULT_BET + data["minBet"] = METACOIN_SLOT_MIN_BET + data["maxBet"] = METACOIN_SLOT_MAX_BET + data["betStep"] = METACOIN_SLOT_BET_STEP + data["quickBets"] = list(5, 10, 25, 50) data["payoutLine3"] = METACOIN_SLOT_PAYOUT_LINE3 data["payoutLine4"] = METACOIN_SLOT_PAYOUT_LINE4 data["payoutLine5"] = METACOIN_SLOT_PAYOUT_LINE5 @@ -326,16 +347,20 @@ var/list/data = list() var/datum/metacoin_shop_controller/shop = get_metacoin_controller() var/client_ckey = owner?.ckey - var/balance = shop.fetch_balance(client_ckey) + + if(!working) + var/live = shop.fetch_balance(client_ckey) + if(!isnull(live)) + balance = live data["isPregame"] = shop.is_open() data["isObserver"] = isobserver(user) data["working"] = working - data["balance"] = isnull(balance) ? 0 : balance - data["state"] = current_reels - data["lastSpin"] = last_spin - data["history"] = spin_history.Copy() - data["cooldownLeftDs"] = shop.get_slot_cooldown_left_ds(client_ckey) + data["balance"] = balance + data["state"] = reels + data["lastSpin"] = last + data["history"] = history.Copy() + data["cooldownLeftDs"] = shop.get_cd(client_ckey) return data @@ -351,18 +376,19 @@ working = TRUE var/mob/user_mob = ui?.user - var/list/result = get_metacoin_controller().try_slot_spin(owner?.ckey, user_mob) + var/list/result = get_metacoin_controller().spin(owner?.ckey, user_mob, params?["bet"]) if(!result["ok"]) if(user_mob) - var/cooldown_seconds = (text2num(result["cooldownLeftDs"]) || 0) / 10 switch(result["error"]) if("shop_closed") to_chat(user_mob, span_warning("Metacoin slot machine is available only before round start.")) + if("invalid_bet") + to_chat(user_mob, span_warning("Invalid bet selected.")) if("not_enough") to_chat(user_mob, span_warning("Not enough metacoins for a spin.")) if("cooldown") - to_chat(user_mob, span_warning("Spin cooldown active: [round(cooldown_seconds, 0.1)]s left.")) + to_chat(user_mob, span_warning("Spin cooldown active: [round((result["cooldownLeftDs"] || 0) / 10, 0.1)]s left.")) if("busy") to_chat(user_mob, span_warning("Spin is already being processed.")) if("db_unavailable", "db_failed") @@ -375,60 +401,67 @@ working = FALSE return FALSE + reels = result["reels"] var/datum/weakref/user_ref = user_mob ? WEAKREF(user_mob) : null - addtimer(CALLBACK(src, PROC_REF(finish_spin), result, user_ref), 10) + play_spin_sound(user_ref, 'sound/machines/lever/lever_start.ogg', 45) + addtimer(CALLBACK(src, PROC_REF(play_spin_sound), user_ref, 'sound/machines/roulette/roulettewheel.ogg', 20), 3) + addtimer(CALLBACK(src, PROC_REF(finish_spin), result, user_ref), 40) SStgui.update_uis(src) return TRUE return FALSE -/datum/metacoin_slot_panel/proc/finish_spin(list/result, datum/weakref/user_ref) - if(!islist(result)) - working = FALSE - SStgui.update_uis(src) +/datum/metacoin_slot_panel/proc/play_spin_sound(datum/weakref/user_ref, snd, volume) + var/mob/user_mob = user_ref?.resolve() + if(!user_mob) return + user_mob.playsound_local(user_mob, snd, volume, TRUE, use_reverb = FALSE) + +/datum/metacoin_slot_panel/proc/finish_spin(list/result, datum/weakref/user_ref) var/mob/user_mob = user_ref?.resolve() || owner?.mob - current_reels = result["reels"] - var/payout_amount = text2num(result["payout"]) || 0 - var/spin_cost = text2num(result["cost"]) || METACOIN_SLOT_SPIN_COST - var/line_length = text2num(result["lineLength"]) || 0 + var/payout = result["payout"] || 0 + var/bet = result["bet"] || METACOIN_SLOT_DEFAULT_BET + var/line = result["lineLength"] || 0 var/jackpot_hit = !!result["isJackpot"] - var/net_amount = payout_amount - spin_cost - var/result_state = payout_amount > 0 ? "win" : "loss" + var/net_amount = payout - bet + var/state = payout > 0 ? "win" : "loss" if(jackpot_hit) - result_state = "jackpot" + state = "jackpot" + + if(!isnull(result["balance"])) + balance = result["balance"] || 0 - last_spin = list( - "lineLength" = line_length, - "payout" = payout_amount, + last = list( + "bet" = bet, + "lineLength" = line, + "payout" = payout, "isJackpot" = jackpot_hit, "net" = net_amount, - "resultState" = result_state, + "resultState" = state, ) - var/list/history_entry = list( + history = list(list( "time" = time2text(world.realtime, "hh:mm:ss"), - "lineLength" = line_length, - "payout" = payout_amount, + "bet" = bet, + "lineLength" = line, + "payout" = payout, "net" = net_amount, "isJackpot" = jackpot_hit, - ) - spin_history = list(history_entry) + spin_history - if(length(spin_history) > 5) - spin_history.Cut(6) + )) + history + if(length(history) > 5) + history.Cut(6) - if(payout_amount >= METACOIN_SLOT_PAYOUT_LINE5) - var/winner_name = user_mob?.real_name || owner?.ckey || "Unknown" - get_metacoin_controller().announce_slot_big_win(winner_name, payout_amount, jackpot_hit, user_mob) + if(jackpot_hit || line >= 5) + get_metacoin_controller().announce_win(user_mob?.real_name || owner?.ckey || "Unknown", payout, line, jackpot_hit, user_mob) if(user_mob) if(jackpot_hit) - to_chat(user_mob, span_boldnicegreen("JACKPOT! You won [payout_amount] metacoins.")) + to_chat(user_mob, span_boldnicegreen("JACKPOT! You won [payout] metacoins.")) user_mob.playsound_local(user_mob, 'sound/machines/roulette/roulettejackpot.ogg', 45, TRUE, use_reverb = FALSE) - else if(payout_amount > 0) - to_chat(user_mob, span_boldnicegreen("You won [payout_amount] metacoins.")) + else if(payout > 0) + to_chat(user_mob, span_boldnicegreen("You won [payout] metacoins.")) user_mob.playsound_local(user_mob, 'sound/effects/kaching.ogg', 40, TRUE, use_reverb = FALSE) else to_chat(user_mob, span_warning("No luck this spin.")) @@ -439,7 +472,11 @@ if(user_mob) SStgui.update_user_uis(user_mob) -#undef METACOIN_SLOT_SPIN_COST +#undef METACOIN_SLOT_BASE_BET +#undef METACOIN_SLOT_MIN_BET +#undef METACOIN_SLOT_MAX_BET +#undef METACOIN_SLOT_DEFAULT_BET +#undef METACOIN_SLOT_BET_STEP #undef METACOIN_SLOT_PAYOUT_LINE3 #undef METACOIN_SLOT_PAYOUT_LINE4 #undef METACOIN_SLOT_PAYOUT_LINE5 diff --git a/modular_meta/features/metacoins/readme.md b/modular_meta/features/metacoins/readme.md index 88b7b6f28d68..7d0498929ffa 100644 --- a/modular_meta/features/metacoins/readme.md +++ b/modular_meta/features/metacoins/readme.md @@ -23,6 +23,7 @@ TODO: - tgui\packages\tgui\interfaces\MetaCoins.tsx - tgui\packages\tgui\interfaces\MetaCoinShop.tsx - tgui\packages\tgui\interfaces\MetaCoinSlot.tsx +- tgui\packages\tgui\styles\interfaces\MetaCoinSlot.scss ### Credits: diff --git a/tgui/packages/tgui/interfaces/MetaCoinSlot.tsx b/tgui/packages/tgui/interfaces/MetaCoinSlot.tsx index 6671b1a6a611..1318bdd06906 100644 --- a/tgui/packages/tgui/interfaces/MetaCoinSlot.tsx +++ b/tgui/packages/tgui/interfaces/MetaCoinSlot.tsx @@ -1,14 +1,16 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { Box, Button, Icon, NoticeBox, + NumberInput, Section, Stack, Table, } from 'tgui-core/components'; +import { classes } from 'tgui-core/react'; import { useBackend } from '../backend'; import { Window } from '../layouts'; @@ -19,6 +21,7 @@ type ReelSymbol = { }; type LastSpin = { + bet: number; lineLength: number; payout: number; isJackpot: boolean; @@ -28,6 +31,7 @@ type LastSpin = { type HistoryEntry = { time: string; + bet: number; lineLength: number; payout: number; net: number; @@ -44,63 +48,128 @@ type Data = { lastSpin: LastSpin; history: HistoryEntry[]; cooldownLeftDs: number; - cost: number; + baseBet: number; + defaultBet: number; + minBet: number; + maxBet: number; + betStep: number; + quickBets: number[]; payoutLine3: number; payoutLine4: number; payoutLine5: number; payoutJackpot: number; }; -type ReelProps = { - reel: ReelSymbol[]; +type IconStripProps = { + iconPool: ReelSymbol[]; + symbolsNeeded: ReelSymbol[]; + spinning: boolean; }; -type TileProps = { - symbol: ReelSymbol; +type BannerProps = { + balance: number; + selectedBet: number; + topPayout: number; + cooldownActive: boolean; + cooldownSeconds: string; + working: boolean; + lastSpin: LastSpin | undefined; }; -const Reel = (props: ReelProps) => { - const { reel } = props; - return ( - - {reel.map((symbol, index) => ( - - ))} - - ); +type ResultView = { + text: string; + color: 'label' | 'bad' | 'good' | 'average'; + flashy: boolean; + jackpot: boolean; }; -const Tile = (props: TileProps) => { - const { symbol } = props; - return ( - - - +const ICON_STRIP_LENGTH = 30; + +const BANNER_TEXTS = [ + 'SHINY SLOTS', + 'SPIN OF FATE', + 'HEY-Y-Y!', + "IT'S SPIN TIME!", + 'READY TO WIN?', + 'LUCK LOVES BOLD', + 'ONE MORE SPIN', + 'METACOIN MAYHEM', + '220% GAMBLERS LEAVE BEFORE THE JACKPOT', +]; + +const FALLBACK_SYMBOL: ReelSymbol = { + icon_name: 'question', + colour: 'white', +}; + +const pickRandom = (items: T[]) => { + return items[Math.floor(Math.random() * items.length)]; +}; + +const pickRandomMany = (items: T[], count: number) => { + const values: T[] = []; + for (let i = 0; i < count; i += 1) { + values.push(pickRandom(items)); + } + return values; +}; + +const toSafeInt = (value: number | undefined, fallback: number) => { + const parsed = Number(value); + if (!Number.isFinite(parsed)) { + return fallback; + } + return Math.round(parsed); +}; + +const normalizeBet = ( + value: number, + minBet: number, + maxBet: number, + betStep: number, + fallbackBet: number, +) => { + const safeMin = Math.max(1, toSafeInt(minBet, 1)); + const safeStep = Math.max(1, toSafeInt(betStep, 1)); + const safeMax = Math.max(safeMin, toSafeInt(maxBet, safeMin)); + const maxSteps = Math.floor((safeMax - safeMin) / safeStep); + + const fallbackSteps = Math.max( + 0, + Math.min( + maxSteps, + Math.round((toSafeInt(fallbackBet, safeMin) - safeMin) / safeStep), + ), ); + + if (!Number.isFinite(value)) { + return safeMin + fallbackSteps * safeStep; + } + + const parsedValue = toSafeInt(value, safeMin); + const parsedSteps = Math.round((parsedValue - safeMin) / safeStep); + const snappedSteps = Math.max(0, Math.min(maxSteps, parsedSteps)); + return safeMin + snappedSteps * safeStep; +}; + +const scalePayout = ( + basePayout: number, + selectedBet: number, + baseBet: number, +) => { + const safeBasePayout = toSafeInt(basePayout, 0); + const safeBaseBet = Math.max(1, toSafeInt(baseBet, 1)); + const safeSelectedBet = Math.max(1, toSafeInt(selectedBet, 1)); + return Math.round((safeBasePayout * safeSelectedBet) / safeBaseBet); }; -const getResultView = (lastSpin: LastSpin | undefined) => { - if (!lastSpin) { +const getResultView = (lastSpin: LastSpin | undefined): ResultView => { + if (!lastSpin || lastSpin.resultState === 'idle') { return { text: 'Ready to spin', - color: 'label' as const, + color: 'label', + flashy: false, + jackpot: false, }; } @@ -108,26 +177,175 @@ const getResultView = (lastSpin: LastSpin | undefined) => { case 'jackpot': return { text: `JACKPOT +${lastSpin.payout}`, - color: 'good' as const, + color: 'good', + flashy: true, + jackpot: true, }; case 'win': return { text: `WIN +${lastSpin.payout}`, - color: 'good' as const, + color: 'good', + flashy: true, + jackpot: false, }; case 'loss': return { - text: `LOSE ${lastSpin.net}`, - color: 'bad' as const, + text: `LOSS ${lastSpin.net}`, + color: 'bad', + flashy: false, + jackpot: false, }; default: return { text: 'Ready to spin', - color: 'label' as const, + color: 'label', + flashy: false, + jackpot: false, }; } }; +const IconStrip = (props: IconStripProps) => { + const { iconPool, symbolsNeeded, spinning } = props; + + const poolKey = iconPool + .map((symbol) => `${symbol.icon_name}:${symbol.colour}`) + .join('|'); + const neededKey = symbolsNeeded + .map((symbol) => `${symbol.icon_name}:${symbol.colour}`) + .join('|'); + + const safePool = useMemo(() => { + return iconPool.length ? iconPool : [FALLBACK_SYMBOL]; + }, [poolKey]); + + const safeNeeded = useMemo(() => { + return symbolsNeeded.length + ? symbolsNeeded + : [FALLBACK_SYMBOL, FALLBACK_SYMBOL, FALLBACK_SYMBOL]; + }, [neededKey]); + + const [drawnSymbols, setDrawnSymbols] = useState([ + ...pickRandomMany(safePool, ICON_STRIP_LENGTH - 3), + ...safeNeeded, + ]); + + useEffect(() => { + if (spinning) { + setDrawnSymbols((previous) => [ + ...previous.slice(-3), + ...pickRandomMany(safePool, ICON_STRIP_LENGTH - 6), + ...safeNeeded, + ]); + return; + } + + setDrawnSymbols([ + ...pickRandomMany(safePool, ICON_STRIP_LENGTH - 3), + ...safeNeeded, + ]); + }, [spinning, safePool, safeNeeded]); + + return ( +
+ {drawnSymbols.map((symbol, index) => ( +
+ +
+ ))} +
+ ); +}; + +const Banner = (props: BannerProps) => { + const { + balance, + selectedBet, + topPayout, + cooldownActive, + cooldownSeconds, + working, + lastSpin, + } = props; + + const [page, setPage] = useState(0); + const defaultTitle = useRef(pickRandom(BANNER_TEXTS)); + + useEffect(() => { + const interval = window.setInterval(() => { + setPage((current) => (current + 1) % 4); + }, 4500); + return () => window.clearInterval(interval); + }, []); + + let title = defaultTitle.current; + let subtitle = `Set your bet and chase up to ${topPayout} metacoins.`; + let flashy = false; + let jackpot = false; + + if (working) { + title = 'REELS SPINNING'; + subtitle = 'Settlement lands after the reveal.'; + } else if (lastSpin?.resultState === 'jackpot') { + title = `JACKPOT +${lastSpin.payout}`; + subtitle = 'Five 7s on center line.'; + flashy = true; + jackpot = true; + } else if (lastSpin?.resultState === 'win') { + title = `WIN +${lastSpin.payout}`; + subtitle = `Line ${lastSpin.lineLength} at bet ${lastSpin.bet}.`; + flashy = true; + } else { + switch (page) { + case 0: + title = defaultTitle.current; + subtitle = `Current stake: ${selectedBet}`; + break; + case 1: + title = `BALANCE ${balance}`; + subtitle = `Each spin costs your selected bet.`; + break; + case 2: + title = cooldownActive ? `COOLDOWN ${cooldownSeconds}s` : 'SPIN READY'; + subtitle = cooldownActive + ? 'Spin unlocks when timer ends.' + : 'Press spin to roll all reels.'; + break; + default: + title = `TOP PRIZE ${topPayout}`; + subtitle = 'Hit 7 7 7 7 7 on center line.'; + } + } + + return ( +
+
{title}
+
{subtitle}
+
+ ); +}; + export const MetaCoinSlot = () => { const { act, data } = useBackend(); const { @@ -140,248 +358,360 @@ export const MetaCoinSlot = () => { lastSpin, history: historyRaw = [], cooldownLeftDs, - cost, + baseBet, + defaultBet, + minBet, + maxBet, + betStep, + quickBets: quickBetsRaw = [], payoutLine3, payoutLine4, payoutLine5, payoutJackpot, } = data; - const history = Array.isArray(historyRaw) ? historyRaw : []; - - const [displayState, setDisplayState] = useState(state); - const [localRolling, setLocalRolling] = useState(false); - const [rollMinEndsAtMs, setRollMinEndsAtMs] = useState(0); - const [cooldownUntilMs, setCooldownUntilMs] = useState(0); - const [clockMs, setClockMs] = useState(Date.now()); - const [frozenLastSpin, setFrozenLastSpin] = useState( - lastSpin, + const safeMinBet = Math.max(1, toSafeInt(minBet, 1)); + const safeStep = Math.max(1, toSafeInt(betStep, 1)); + const safeMaxBet = Math.max(safeMinBet, toSafeInt(maxBet, safeMinBet)); + const safeDefaultBet = normalizeBet( + toSafeInt(defaultBet, safeMinBet), + safeMinBet, + safeMaxBet, + safeStep, + safeMinBet, ); - const [frozenHistory, setFrozenHistory] = useState(history); + const safeBaseBet = Math.max(1, toSafeInt(baseBet, safeDefaultBet)); const iconPool = useMemo(() => { - if (icons.length) { - return icons; - } - return [ - { - icon_name: 'question', - colour: 'white', - }, - ]; + return icons.length ? icons : [FALLBACK_SYMBOL]; }, [icons]); - useEffect(() => { - if (!localRolling) { - setDisplayState(state); + const reels = useMemo(() => { + if (state.length) { + return state; } - }, [state, localRolling]); + const filler = pickRandomMany(iconPool, 3); + return [filler, filler, filler, filler, filler]; + }, [state, iconPool]); + + const history = Array.isArray(historyRaw) ? historyRaw : []; + + const [selectedBet, setSelectedBet] = useState(safeDefaultBet); + const [cooldownUntilMs, setCooldownUntilMs] = useState(0); + const [clockMs, setClockMs] = useState(Date.now()); useEffect(() => { - if (!cooldownLeftDs) { + setSelectedBet((current) => + normalizeBet(current, safeMinBet, safeMaxBet, safeStep, safeDefaultBet), + ); + }, [safeMinBet, safeMaxBet, safeStep, safeDefaultBet]); + + useEffect(() => { + if (cooldownLeftDs > 0) { + setCooldownUntilMs(Date.now() + cooldownLeftDs * 100); return; } - - setCooldownUntilMs(Date.now() + cooldownLeftDs * 100); + setCooldownUntilMs(0); }, [cooldownLeftDs]); useEffect(() => { const interval = window.setInterval(() => { setClockMs(Date.now()); }, 100); - return () => window.clearInterval(interval); }, []); - useEffect(() => { - if (!localRolling) { - return; + const quickBets = useMemo(() => { + const result: number[] = []; + const seen = new Set(); + + for (const rawBet of quickBetsRaw) { + const bet = normalizeBet( + Number(rawBet), + safeMinBet, + safeMaxBet, + safeStep, + safeDefaultBet, + ); + if (seen.has(bet)) { + continue; + } + seen.add(bet); + result.push(bet); } - const interval = window.setInterval(() => { - if (Date.now() >= rollMinEndsAtMs && !working) { - setLocalRolling(false); - setDisplayState(state); - window.clearInterval(interval); - return; + if (result.length) { + return result.slice(0, 4); + } + + for (const fallbackBet of [safeMinBet, safeDefaultBet, safeMaxBet]) { + if (seen.has(fallbackBet)) { + continue; } + seen.add(fallbackBet); + result.push(fallbackBet); + } - setDisplayState((previous) => - previous.map((reel) => - reel.map(() => iconPool[Math.floor(Math.random() * iconPool.length)]), - ), - ); - }, 70); - - return () => window.clearInterval(interval); - }, [iconPool, localRolling, rollMinEndsAtMs, state, working]); + return result.slice(0, 4); + }, [quickBetsRaw, safeMinBet, safeMaxBet, safeStep, safeDefaultBet]); const cooldownMsLeft = Math.max(0, cooldownUntilMs - clockMs); const cooldownActive = cooldownMsLeft > 0; const cooldownSeconds = (cooldownMsLeft / 1000).toFixed(1); - const displayedLastSpin = localRolling ? frozenLastSpin : lastSpin; - const displayedHistory = localRolling ? frozenHistory : history; - const resultView = getResultView(displayedLastSpin); + const scaledLine3 = scalePayout(payoutLine3, selectedBet, safeBaseBet); + const scaledLine4 = scalePayout(payoutLine4, selectedBet, safeBaseBet); + const scaledLine5 = scalePayout(payoutLine5, selectedBet, safeBaseBet); + const scaledJackpot = scalePayout(payoutJackpot, selectedBet, safeBaseBet); + + const resultView = getResultView(lastSpin); const spinLocked = working || - localRolling || + cooldownActive || (!isPregame && !isObserver) || - balance < cost || - cooldownActive; + balance < selectedBet; const handleSpin = () => { if (spinLocked) { return; } - - setFrozenLastSpin(lastSpin); - setFrozenHistory(history); - setRollMinEndsAtMs(Date.now() + 500); - setLocalRolling(true); - act('spin'); + act('spin', { + bet: selectedBet, + }); }; return ( - + - {!isPregame && !isObserver && ( - - Slot machine is available only before round start. - - )} +
+ {!isPregame && !isObserver && ( + + Slot machine is available only before round start. + + )} + + -
- - - Balance: {balance} - - - Spin cost: {cost} - - - {cooldownActive - ? `Cooldown: ${cooldownSeconds}s` - : 'Spin ready'} - + +
+
+ {reels.map((reel, index) => ( +
+ +
+ ))} +
+
+ +
+ + {resultView.text} + + + + + + Bet: {lastSpin?.bet ?? selectedBet} + + + + + Line: {lastSpin?.lineLength ?? 0} + + + + + Payout: {lastSpin?.payout ?? 0} + + + + = 0 ? 'good' : 'bad'}> + Net: {lastSpin?.net ?? 0} + + + +
- - - -
-
-
- - {resultView.text} - - - - - Line length: {displayedLastSpin?.lineLength || 0} - - - - - Payout: {displayedLastSpin?.payout || 0} - - - = 0 ? 'good' : 'bad'}> - Net: {displayedLastSpin?.net || 0} - +
+ + Balance: {balance} + + + Base bet: {safeBaseBet} + + + {cooldownActive + ? `Cooldown: ${cooldownSeconds}s` + : 'Spin ready'} + + + + Bet + + + setSelectedBet( + normalizeBet( + value, + safeMinBet, + safeMaxBet, + safeStep, + safeDefaultBet, + ), + ) + } + /> + +
+ {quickBets.map((bet) => ( + + ))} +
+ + + + +
+ +
+ + Values scale with bet {selectedBet}. + + + + Condition + + Reward + + + + 3 in a row + {scaledLine3} + + + 4 in a row + {scaledLine4} + + + 5 in a row + {scaledLine5} + + + Jackpot (center 7 7 7 7 7) + {scaledJackpot} + +
+
-
- -
- - - Condition - - Reward - - - - 3 in a row - {payoutLine3} - - - 4 in a row - {payoutLine4} - - - 5 in a row - {payoutLine5} - - - Jackpot (middle row 7 7 7 7 7) - {payoutJackpot} - -
-
- -
- - {displayState.map((reel, index) => ( - - ))} - -
- -
- {!displayedHistory.length ? ( - No spins yet. - ) : ( - - - - Time - - - Line - - - Payout - - - Net - - - {displayedHistory.map((entry, index) => ( - - {entry.time} - - {entry.lineLength} + {!history.length ? ( + No spins yet. + ) : ( +
+ + + Time - - {entry.payout} + + Bet - = 0 ? 'good' : 'bad'} - > - {entry.net} + + Line + + + Payout + + + Net - ))} -
- )} -
+ {history.map((entry, index) => ( + + {entry.time} + + {entry.bet} + + + {entry.lineLength} + + + {entry.payout} + + = 0 ? 'good' : 'bad'} + > + {entry.net} + + + ))} + + )} + +
); diff --git a/tgui/packages/tgui/styles/interfaces/MetaCoinSlot.scss b/tgui/packages/tgui/styles/interfaces/MetaCoinSlot.scss new file mode 100644 index 000000000000..30bbedaa9359 --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/MetaCoinSlot.scss @@ -0,0 +1,288 @@ +.MetaCoinSlot { + --slot-tile-size: 64px; + --slot-banner-a: #f48c26; + --slot-banner-b: #e6427a; + --slot-banner-c: #23a5d8; + --slot-banner-d: #30bf78; + --slot-reel-top: #f2f6fb; + --slot-reel-mid: #c7ced8; + --slot-reel-low: #8f97a4; +} + +.MetaCoinSlot__Banner { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 76px; + margin-bottom: 0.5rem; + text-align: center; + border: 1px solid rgba(255, 255, 255, 0.35); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.35), + 0 6px 14px rgba(0, 0, 0, 0.35); + background: linear-gradient( + -45deg, + var(--slot-banner-a), + var(--slot-banner-b), + var(--slot-banner-c), + var(--slot-banner-d) + ); + background-size: 320% 320%; + animation: MetaCoinSlot__BannerGradient 9s ease infinite; +} + +.MetaCoinSlot__Banner--winning { + animation: + MetaCoinSlot__BannerGradient 9s ease infinite, + MetaCoinSlot__BannerFlash 0.45s steps(2, end) infinite; +} + +.MetaCoinSlot__Banner--jackpot { + animation: + MetaCoinSlot__BannerGradient 9s ease infinite, + MetaCoinSlot__JackpotFlash 0.35s steps(2, end) infinite; +} + +.MetaCoinSlot__BannerTitle { + font-family: 'Trebuchet MS', 'Franklin Gothic Medium', sans-serif; + font-size: 1.7rem; + font-weight: 800; + letter-spacing: 0.06em; + text-shadow: + 0 0 10px rgba(0, 0, 0, 0.65), + 1px 1px 0 rgba(0, 0, 0, 0.75); +} + +.MetaCoinSlot__BannerText { + margin-top: 0.2rem; + font-family: 'Lucida Sans Unicode', 'Trebuchet MS', sans-serif; + font-size: 0.92rem; + letter-spacing: 0.03em; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.55); +} + +.MetaCoinSlot__Reels { + display: flex; + gap: 0.35rem; +} + +.MetaCoinSlot__Reel { + position: relative; + flex: 1; + height: calc(var(--slot-tile-size) * 3); + overflow: hidden; + border-radius: 0.3rem; + border: 1px solid rgba(30, 33, 37, 0.9); + background: linear-gradient( + to bottom, + var(--slot-reel-top) 0%, + var(--slot-reel-mid) 35%, + var(--slot-reel-low) 100% + ); + box-shadow: + inset 0 2px 0 rgba(255, 255, 255, 0.55), + inset 0 -2px 0 rgba(0, 0, 0, 0.3), + 0 4px 8px rgba(0, 0, 0, 0.45); +} + +.MetaCoinSlot__Reel::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + 90deg, + rgba(0, 0, 0, 0.18), + rgba(255, 255, 255, 0.12), + rgba(0, 0, 0, 0.22) + ); + pointer-events: none; +} + +.MetaCoinSlot__Reel::after { + content: ''; + position: absolute; + left: 0; + right: 0; + top: calc(var(--slot-tile-size) - 2px); + bottom: calc(var(--slot-tile-size) - 2px); + border-top: 2px solid rgba(0, 0, 0, 0.9); + border-bottom: 2px solid rgba(0, 0, 0, 0.9); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.2), + 0 0 8px rgba(255, 232, 150, 0.45); + pointer-events: none; +} + +.MetaCoinSlot__Reel:nth-child(1) .MetaCoinSlot__IconStrip { + animation-delay: 0s; + animation-duration: 2.8s; +} + +.MetaCoinSlot__Reel:nth-child(2) .MetaCoinSlot__IconStrip { + animation-delay: 0.1s; + animation-duration: 3s; +} + +.MetaCoinSlot__Reel:nth-child(3) .MetaCoinSlot__IconStrip { + animation-delay: 0.2s; + animation-duration: 3.2s; +} + +.MetaCoinSlot__Reel:nth-child(4) .MetaCoinSlot__IconStrip { + animation-delay: 0.3s; + animation-duration: 3.4s; +} + +.MetaCoinSlot__Reel:nth-child(5) .MetaCoinSlot__IconStrip { + animation-delay: 0.4s; + animation-duration: 3.6s; +} + +.MetaCoinSlot__IconStrip { + transform: translateY(calc(-1 * var(--slot-tile-size) * 27)); + text-align: center; + will-change: transform; +} + +.MetaCoinSlot__IconStrip--spinning { + transform: translateY(0); + animation-name: MetaCoinSlot__SpinStrip; + animation-iteration-count: 1; + animation-fill-mode: forwards; + animation-timing-function: cubic-bezier(1, 1.24, 0.88, 0.96); +} + +.MetaCoinSlot__Symbol { + height: var(--slot-tile-size); + display: flex; + align-items: center; + justify-content: center; + background: radial-gradient( + ellipse at center, + rgba(255, 255, 255, 0.15) 0%, + rgba(255, 255, 255, 0.02) 65%, + rgba(0, 0, 0, 0.15) 100% + ); + border-bottom: 1px solid rgba(0, 0, 0, 0.15); +} + +.MetaCoinSlot__SymbolIcon { + filter: drop-shadow(1px 1px 1px rgba(0, 0, 0, 0.85)) + drop-shadow(0 0 4px rgba(255, 255, 255, 0.22)); +} + +.MetaCoinSlot__ResultBadge { + padding: 0.35rem 0.6rem; + border-radius: 0.25rem; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(17, 20, 28, 0.55); +} + +.MetaCoinSlot__ResultBadge--flashy { + animation: MetaCoinSlot__ResultPulse 0.9s ease-in-out infinite; +} + +.MetaCoinSlot__ResultBadge--jackpot { + animation: MetaCoinSlot__JackpotPulse 0.55s ease-in-out infinite; +} + +.MetaCoinSlot__QuickBets { + margin-top: 0.45rem; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.3rem; +} + +.MetaCoinSlot__QuickBet { + width: 100%; + text-align: center; +} + +@keyframes MetaCoinSlot__BannerGradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +@keyframes MetaCoinSlot__BannerFlash { + 0% { + color: #fff6b0; + } + 100% { + color: #ffffff; + } +} + +@keyframes MetaCoinSlot__JackpotFlash { + 0% { + color: #ffe787; + filter: saturate(1.1); + } + 100% { + color: #ffffff; + filter: saturate(1.55); + } +} + +@keyframes MetaCoinSlot__SpinStrip { + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(calc(-1 * var(--slot-tile-size) * 27)); + } +} + +@keyframes MetaCoinSlot__ResultPulse { + 0% { + box-shadow: 0 0 0 rgba(255, 230, 90, 0.2); + } + 50% { + box-shadow: 0 0 14px rgba(255, 230, 90, 0.55); + } + 100% { + box-shadow: 0 0 0 rgba(255, 230, 90, 0.2); + } +} + +@keyframes MetaCoinSlot__JackpotPulse { + 0% { + box-shadow: 0 0 2px rgba(255, 255, 255, 0.2); + transform: scale(1); + } + 50% { + box-shadow: + 0 0 10px rgba(255, 234, 108, 0.8), + 0 0 20px rgba(255, 132, 0, 0.55); + transform: scale(1.015); + } + 100% { + box-shadow: 0 0 2px rgba(255, 255, 255, 0.2); + transform: scale(1); + } +} + +@media (max-width: 900px) { + .MetaCoinSlot { + --slot-tile-size: 56px; + } + + .MetaCoinSlot__BannerTitle { + font-size: 1.35rem; + } + + .MetaCoinSlot__BannerText { + font-size: 0.82rem; + } + + .MetaCoinSlot__QuickBets { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index 5764ef03d2b9..73ba423ab0bd 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -71,3 +71,6 @@ :root { --scaling-amount: 1; } + +// MASSMETA DOWN HERE +@include meta.load-css('./interfaces/MetaCoinSlot.scss'); From e6a9dfd64cf03055104b7a7b83c0fc69e5814e9c Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Sat, 18 Apr 2026 20:06:13 +0300 Subject: [PATCH 08/18] =?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=D0=B4?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9=20=D0=BF=D0=BE=D0=BD=D0=B5=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=B8=D0=BA=20=D1=84=D0=B0=D1=80=D0=BC=D0=B8=D1=88?= =?UTF-8?q?=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modular_meta/features/metacoins/code/award_overrides.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modular_meta/features/metacoins/code/award_overrides.dm b/modular_meta/features/metacoins/code/award_overrides.dm index c9a94cbeebf6..e1dd936c070d 100644 --- a/modular_meta/features/metacoins/code/award_overrides.dm +++ b/modular_meta/features/metacoins/code/award_overrides.dm @@ -105,7 +105,7 @@ reward = METACOIN_AWARD_SMALL /datum/award/score/style_score - reward = METACOIN_AWARD_SMALL + reward = METACOIN_AWARD_NONE /datum/award/score/drake_score reward = METACOIN_AWARD_SMALL From 79c9729f060f5be951219ffc2856276e5636102f Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Mon, 20 Apr 2026 09:00:47 +0300 Subject: [PATCH 09/18] adds space explo kit --- config/dbconfig.txt | 4 +- .../features/metacoins/code/shop_items.dm | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/config/dbconfig.txt b/config/dbconfig.txt index eed180704a32..8c9f275defb8 100644 --- a/config/dbconfig.txt +++ b/config/dbconfig.txt @@ -13,7 +13,7 @@ ADDRESS localhost PORT 3306 ## Database for all SQL functions, not just feedback. -FEEDBACK_DATABASE tgdb +FEEDBACK_DATABASE massmeta ## Prefix to be added to the name of every table, older databases will require this be set to erro_ ## Note, this does not change the table names in the database, you will have to do that yourself. @@ -27,7 +27,7 @@ FEEDBACK_TABLEPREFIX FEEDBACK_LOGIN root ## Password used to access the database. -FEEDBACK_PASSWORD 12345678 +FEEDBACK_PASSWORD 123 ## Time in seconds for asynchronous queries to timeout ## Set to 0 for infinite diff --git a/modular_meta/features/metacoins/code/shop_items.dm b/modular_meta/features/metacoins/code/shop_items.dm index 6290b0c417c7..cebe7081ccf9 100644 --- a/modular_meta/features/metacoins/code/shop_items.dm +++ b/modular_meta/features/metacoins/code/shop_items.dm @@ -31,6 +31,43 @@ price = 75 item_type = /obj/item/reagent_containers/medigel/aiuri +/datum/metacoinshop/listing/preround/eva_kit + id = "extra_vehicular" + name = "Premium EVA-ready kit" + desc = "Full-kit containing a bluespace-compressed jetpack, an oxygen tank, a suit, a helmet and a medkit! Gun included!" + price = 300 + item_type = /obj/item/storage/box/eva_kit + +/obj/item/storage/box/eva_kit + name = "EVA kit" + desc = "A sturdy looking box, label says \"it has everything needed for space exploration\"" + +/obj/item/storage/box/eva_kit/Initialize(mapload) + .=..() + var/obj/item/stack/medical/suture/suture = new/obj/item/stack/medical/suture(src) + suture.amount = 10 + var/obj/item/stack/medical/mesh/mesh = new /obj/item/stack/medical/mesh(src) + mesh.amount = 10 + var/obj/item/stock_parts/power_store/cell/high/cell = new /obj/item/stock_parts/power_store/cell/high(src) + cell.charge = 10000 // the fuck is a cell unit? a thousand I guess? cuz hundred is 0.1% + var/obj/item/tank/jetpack/jpack = new /obj/item/tank/jetpack(src) + //get current gas + var/datum/gas_mixture/gas = jpack.return_air() + gas.assert_gas(jpack.gas_type) + // quadruple it and give it to the next jetpack + gas.gases[jpack.gas_type][MOLES] += ((24 * ONE_ATMOSPHERE) * jpack.volume / (R_IDEAL_GAS_EQUATION * T20C)) + jpack.desc += span_notice(" \n Though it definetly seems to have enlarged in proportions when I took it from the box..") + new /obj/item/clothing/suit/space(src) + new /obj/item/clothing/head/helmet/space(src) + new /obj/item/tank/internals/oxygen(src) + new /obj/item/storage/medkit/advanced(src) + new /obj/item/storage/belt/utility/full(src) + new /obj/item/knife/combat/survival(src) + new /obj/item/gun/energy/e_gun/mini(src) + new /obj/item/case_portable_recharger(src) + new /obj/item/manual_cell_recharger(src) + new /obj/item/stock_parts/servo/pico(src) + /datum/metacoinshop/listing/preround/antag_token id = "antag_token" name = "Antag Token" From 77f7b1068467a20a867d64d47f92ae45c925c8c1 Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Mon, 20 Apr 2026 09:02:27 +0300 Subject: [PATCH 10/18] =?UTF-8?q?=D0=BC=D0=BE=D0=BB=D0=BE=D0=B4=D0=BE?= =?UTF-8?q?=D0=B9=20=D1=87=D0=B5=D0=BB=D0=BE=D0=B2=D0=B5=D0=BA,=20=D0=B2?= =?UTF-8?q?=D1=8B=20=D1=87=D0=B5=D0=B3=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/dbconfig.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/dbconfig.txt b/config/dbconfig.txt index 8c9f275defb8..eed180704a32 100644 --- a/config/dbconfig.txt +++ b/config/dbconfig.txt @@ -13,7 +13,7 @@ ADDRESS localhost PORT 3306 ## Database for all SQL functions, not just feedback. -FEEDBACK_DATABASE massmeta +FEEDBACK_DATABASE tgdb ## Prefix to be added to the name of every table, older databases will require this be set to erro_ ## Note, this does not change the table names in the database, you will have to do that yourself. @@ -27,7 +27,7 @@ FEEDBACK_TABLEPREFIX FEEDBACK_LOGIN root ## Password used to access the database. -FEEDBACK_PASSWORD 123 +FEEDBACK_PASSWORD 12345678 ## Time in seconds for asynchronous queries to timeout ## Set to 0 for infinite From 4d867b4b34914493e05c22b792e6acb3cfe4f342 Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Fri, 24 Apr 2026 05:59:49 +0300 Subject: [PATCH 11/18] to explore the space you must move with the light! a seclite, to be exact :) --- modular_meta/features/metacoins/code/metacoin_shop.dm | 2 -- modular_meta/features/metacoins/code/shop_items.dm | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/modular_meta/features/metacoins/code/metacoin_shop.dm b/modular_meta/features/metacoins/code/metacoin_shop.dm index 2ad1c753cd17..1d59940686db 100644 --- a/modular_meta/features/metacoins/code/metacoin_shop.dm +++ b/modular_meta/features/metacoins/code/metacoin_shop.dm @@ -925,5 +925,3 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) set desc = "Open metacoin shop window." new /datum/metacoin_shop_panel(src, usr) - -//TODO: Admin logs, paid metacoin deathmath matches, prettier ui, background change, more sounds upon clicks, diff --git a/modular_meta/features/metacoins/code/shop_items.dm b/modular_meta/features/metacoins/code/shop_items.dm index cebe7081ccf9..e3fb8e907405 100644 --- a/modular_meta/features/metacoins/code/shop_items.dm +++ b/modular_meta/features/metacoins/code/shop_items.dm @@ -52,7 +52,7 @@ cell.charge = 10000 // the fuck is a cell unit? a thousand I guess? cuz hundred is 0.1% var/obj/item/tank/jetpack/jpack = new /obj/item/tank/jetpack(src) //get current gas - var/datum/gas_mixture/gas = jpack.return_air() + var/datum/gas_mixture/gas = jpack.return_air() // bitch it's literally oxygen gas.assert_gas(jpack.gas_type) // quadruple it and give it to the next jetpack gas.gases[jpack.gas_type][MOLES] += ((24 * ONE_ATMOSPHERE) * jpack.volume / (R_IDEAL_GAS_EQUATION * T20C)) @@ -67,6 +67,7 @@ new /obj/item/case_portable_recharger(src) new /obj/item/manual_cell_recharger(src) new /obj/item/stock_parts/servo/pico(src) + new /obj/item/flashlight/seclite(src) /datum/metacoinshop/listing/preround/antag_token id = "antag_token" From 58690dfdc03ee86dff38c669de0875aa1b0e6b8c Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Fri, 24 Apr 2026 11:32:08 +0300 Subject: [PATCH 12/18] WIP --- SQL/tgstation_schema.sql | 12 + .../features/metacoins/code/metacoin_shop.dm | 303 ++++++++++++------ .../features/metacoins/code/persistent.dm | 25 ++ .../features/metacoins/code/shop_items.dm | 30 +- modular_meta/features/metacoins/includes.dm | 1 + .../packages/tgui/interfaces/MetaCoinShop.tsx | 57 +++- 6 files changed, 334 insertions(+), 94 deletions(-) create mode 100644 modular_meta/features/metacoins/code/persistent.dm diff --git a/SQL/tgstation_schema.sql b/SQL/tgstation_schema.sql index fb6d7cea7472..84486eded0e4 100644 --- a/SQL/tgstation_schema.sql +++ b/SQL/tgstation_schema.sql @@ -413,6 +413,18 @@ CREATE TABLE `player` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `metacoin_purchases` +-- MASSMETA EDIT ADDITION START (metacoins) +DROP TABLE IF EXISTS `metacoin_purchases`; +CREATE TABLE `metacoin_purchases` ( + `ckey` varchar(32) NOT NULL, + `listing` varchar(64) NOT NULL, + `owned` tinyint(1) NOT NULL DEFAULT 1, + PRIMARY KEY (`ckey`, `listing`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- MASSMETA EDIT ADDITION END (metacoins) + -- -- Table structure for table `poll_option` -- diff --git a/modular_meta/features/metacoins/code/metacoin_shop.dm b/modular_meta/features/metacoins/code/metacoin_shop.dm index 1d59940686db..69a01f093b49 100644 --- a/modular_meta/features/metacoins/code/metacoin_shop.dm +++ b/modular_meta/features/metacoins/code/metacoin_shop.dm @@ -15,6 +15,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) /datum/metacoin_shop_controller var/list/preround_catalog = list() + var/list/persistent_catalog = list() var/list/preround_pending_by_ckey = list() var/list/preround_delivered_by_ckey = list() var/list/antag_token_pending_by_ckey = list() @@ -37,6 +38,16 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) preround_catalog[listing.id] = listing + persistent_catalog = alist() + for(var/listing_path in subtypesof(/datum/metacoinshop/listing/persistent)) + var/datum/metacoinshop/listing/listing = new listing_path + if(listing.item_type && !listing.icon) + var/obj/item/type_cast_item_path = listing.item_type + listing.icon = initial(type_cast_item_path.icon) + listing.icon_state = initial(type_cast_item_path.icon_state) + + persistent_catalog[listing.id] = listing + /datum/metacoin_shop_controller/proc/register_signals() if(signals_registered) return @@ -318,27 +329,32 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) for(var/target_ckey in ckeys_to_refund) refund_token(target_ckey, null, null) -/datum/metacoin_shop_controller/proc/get_catalog_ui(target_ckey) +/datum/metacoin_shop_controller/proc/catalog_ui(target_ckey, kind = "preround") var/list/catalog_data = list() - var/list/pending_items = get_pending_items(target_ckey) + var/list/catalog = kind == "persistent" ? persistent_catalog : preround_catalog + var/list/pending_items = kind == "preround" ? get_pending_items(target_ckey) : list() + var/list/owned_items = kind == "persistent" ? owned_persistent(target_ckey) : null var/selected_antag_role = antag_token_pending_by_ckey[target_ckey] var/balance = fetch_balance(target_ckey) - for(var/listing_id in preround_catalog) - var/datum/metacoinshop/listing/listing = preround_catalog[listing_id] + for(var/listing_id in catalog) + var/datum/metacoinshop/listing/listing = catalog[listing_id] if(!listing) continue var/is_antag_token = listing.id == "antag_token" + var/is_persistent = listing.listing_type == "persistent" var/is_owned = FALSE if(is_antag_token) is_owned = !isnull(selected_antag_role) + else if(is_persistent) + is_owned = !!owned_items?[listing.id] else is_owned = (listing.id in pending_items) var/list/listing_payload = list( "id" = listing.id, - "kind" = is_antag_token ? "antag_token" : "item", + "kind" = is_antag_token ? "antag_token" : listing.listing_type, "name" = listing.name, "desc" = listing.desc, "price" = listing.price, @@ -358,6 +374,105 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return catalog_data +/datum/metacoin_shop_controller/proc/owned_persistent(target_ckey) + target_ckey = ckey(target_ckey) + if(!target_ckey) + return list() + + if(!SSdbcore.Connect()) + return null + + var/table_purchases = format_table_name("metacoin_purchases") + var/datum/db_query/select_query = SSdbcore.NewQuery( + "SELECT listing FROM [table_purchases] WHERE ckey = :ckey AND owned = TRUE", + list("ckey" = target_ckey), + ) + + if(!select_query.warn_execute(async = FALSE)) + qdel(select_query) + return null + + var/list/owned_items = list() + while(select_query.NextRow(async = FALSE)) + var/listing_id = select_query.item[1] + if(listing_id) + owned_items[listing_id] = TRUE + + qdel(select_query) + return owned_items + +/datum/metacoin_shop_controller/proc/owns_persistent(target_ckey, listing_id) + target_ckey = ckey(target_ckey) + if(!target_ckey || !listing_id) + return FALSE + + if(!SSdbcore.Connect()) + return null + + var/table_purchases = format_table_name("metacoin_purchases") + var/datum/db_query/select_query = SSdbcore.NewQuery( + "SELECT owned FROM [table_purchases] WHERE ckey = :ckey AND listing = :listing LIMIT 1", + list( + "ckey" = target_ckey, + "listing" = listing_id, + ), + ) + + if(!select_query.warn_execute(async = FALSE)) + qdel(select_query) + return null + + var/is_owned = FALSE + if(select_query.NextRow(async = FALSE)) + is_owned = text2num("[select_query.item[1]]") > 0 + + qdel(select_query) + return is_owned + +/// You may use this to manually set any listing_id to TRUE or FALSE. upsert queries add new lines in a table, so there "shall" be no issues with it +/datum/metacoin_shop_controller/proc/set_persistent(target_ckey, listing_id, owned = TRUE) + target_ckey = ckey(target_ckey) + if(!target_ckey || !listing_id) + return FALSE + + if(!SSdbcore.Connect()) + return FALSE + + var/table_purchases = format_table_name("metacoin_purchases") + var/datum/db_query/upsert_query = SSdbcore.NewQuery( + "INSERT INTO [table_purchases] (ckey, listing, owned) VALUES (:ckey, :listing, :owned) ON DUPLICATE KEY UPDATE owned = VALUES(owned)", + list( + "ckey" = target_ckey, + "listing" = listing_id, + "owned" = owned ? TRUE : FALSE, + ), + ) + + if(!upsert_query.warn_execute(async = FALSE)) + qdel(upsert_query) + return FALSE + + qdel(upsert_query) + return TRUE + +/datum/metacoin_shop_controller/proc/grant_persistents(target_ckey, mob/living/spawned, client/player_client) + target_ckey = ckey(target_ckey) + if(!target_ckey) + return FALSE + + var/list/owned_items = owned_persistent(target_ckey) + if(isnull(owned_items)) + return FALSE + + for(var/listing_id in owned_items) + var/datum/metacoinshop/listing/listing = persistent_catalog[listing_id] + if(!listing) + continue + + listing.persistent_grant(src, target_ckey, spawned, player_client) + + return TRUE + /datum/metacoin_shop_controller/proc/get_pending_items(target_ckey) if(!target_ckey) return list() @@ -456,72 +571,79 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) "balance" = new_balance, ) -/datum/metacoin_shop_controller/proc/buy_item(target_ckey, item_id) +/datum/metacoin_shop_controller/proc/buy(target_ckey, item_id, role_id = null, client/player_client = null) + target_ckey = ckey(target_ckey || player_client?.ckey) if(!target_ckey || !item_id) return list("ok" = FALSE, "error" = "invalid_request") - if(!is_open()) - return list("ok" = FALSE, "error" = "shop_closed") + var/datum/metacoinshop/listing/listing = persistent_catalog[item_id] + if(!listing) + listing = preround_catalog[item_id] - var/datum/metacoinshop/listing/listing = preround_catalog[item_id] if(!listing) return list("ok" = FALSE, "error" = "unknown_item") - if(listing.listing_type != "item") - return list("ok" = FALSE, "error" = "open_antag_panel") + var/mob/player_mob = player_client?.mob || get_mob_by_ckey(target_ckey) + var/list/take - var/list/pending_items = preround_pending_by_ckey[target_ckey] - if(!islist(pending_items)) - pending_items = list() - preround_pending_by_ckey[target_ckey] = pending_items + if(listing.listing_type == "persistent") + var/is_owned = owns_persistent(target_ckey, item_id) + if(isnull(is_owned)) + return list("ok" = FALSE, "error" = "db_unavailable") - if(item_id in pending_items) - return list("ok" = FALSE, "error" = "already_owned") + if(is_owned) + return list("ok" = FALSE, "error" = "already_owned") - if(!SSdbcore.Connect()) - return list("ok" = FALSE, "error" = "db_unavailable") + take = take_metacoins(target_ckey, listing.price) + if(!take["ok"]) + return take - var/current_balance = fetch_balance(target_ckey) - if(isnull(current_balance)) - return list("ok" = FALSE, "error" = "db_unavailable") + if(!set_persistent(target_ckey, item_id, TRUE)) + if(!add_metacoins(target_ckey, listing.price)) + log_game("[src] persistent purchase refund failed: ckey=[target_ckey], listing=[item_id], price=[listing.price].") + return list("ok" = FALSE, "error" = "db_failed") - if(current_balance < listing.price) - return list("ok" = FALSE, "error" = "not_enough") + listing.on_bought(src, target_ckey, player_mob, player_client, take["balance"]) - var/table_player = format_table_name("player") - var/datum/db_query/buy_query = SSdbcore.NewQuery( - "UPDATE [table_player] SET metacoins = metacoins - :price WHERE ckey = :ckey AND metacoins >= :price", - list( - "price" = listing.price, - "ckey" = target_ckey, - ), - ) + if(player_mob) + to_chat(player_mob, span_boldnicegreen("Purchased [listing.name] for [listing.price] metacoins.")) + player_mob.playsound_local(player_mob, 'sound/effects/kaching.ogg', 40, TRUE, use_reverb = FALSE) + SStgui.update_user_uis(player_mob) - if(!buy_query.warn_execute(async = FALSE)) - qdel(buy_query) - return list("ok" = FALSE, "error" = "db_failed") - qdel(buy_query) + return list("ok" = TRUE) - var/new_balance = fetch_balance(target_ckey) - if(isnull(new_balance)) - return list("ok" = FALSE, "error" = "db_failed") + if(listing.listing_type == "item") + if(!is_open()) + return list("ok" = FALSE, "error" = "shop_closed") - if(new_balance > (current_balance - listing.price)) - return list("ok" = FALSE, "error" = "not_enough") + var/list/pending_items = preround_pending_by_ckey[target_ckey] + if(!islist(pending_items)) + pending_items = list() + preround_pending_by_ckey[target_ckey] = pending_items - pending_items += item_id + if(item_id in pending_items) + return list("ok" = FALSE, "error" = "already_owned") - var/mob/player_mob = get_mob_by_ckey(target_ckey) - if(player_mob) - to_chat(player_mob, span_boldnicegreen("Purchased [listing.name] for [listing.price] metacoins. It will be delivered on first roundstart spawn.")) - player_mob.playsound_local(player_mob, 'sound/effects/kaching.ogg', 40, TRUE, use_reverb = FALSE) - SStgui.update_user_uis(player_mob) + take = take_metacoins(target_ckey, listing.price) + if(!take["ok"]) + return take - return list("ok" = TRUE) + pending_items += item_id -/datum/metacoin_shop_controller/proc/buy_token(target_ckey, role_id) - if(!target_ckey || !role_id) - return list("ok" = FALSE, "error" = "invalid_request") + listing.on_bought(src, target_ckey, player_mob, player_client, take["balance"]) + + if(player_mob) + to_chat(player_mob, span_boldnicegreen("Purchased [listing.name] for [listing.price] metacoins. It will be delivered on first roundstart spawn.")) + player_mob.playsound_local(player_mob, 'sound/effects/kaching.ogg', 40, TRUE, use_reverb = FALSE) + SStgui.update_user_uis(player_mob) + + return list("ok" = TRUE) + + if(listing.id != "antag_token") + return list("ok" = FALSE, "error" = "unknown_item") + + if(!role_id) + return list("ok" = FALSE, "error" = "open_antag_panel") if(!is_open()) return list("ok" = FALSE, "error" = "shop_closed") @@ -536,47 +658,18 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) if(block_info) return list("ok" = FALSE, "error" = block_info["code"]) - var/datum/metacoinshop/listing/listing = get_token_listing() - if(!listing) - return list("ok" = FALSE, "error" = "unknown_item") - - if(!SSdbcore.Connect()) - return list("ok" = FALSE, "error" = "db_unavailable") - - var/current_balance = fetch_balance(target_ckey) - if(isnull(current_balance)) - return list("ok" = FALSE, "error" = "db_unavailable") - - if(current_balance < listing.price) - return list("ok" = FALSE, "error" = "not_enough") - - var/table_player = format_table_name("player") - var/datum/db_query/buy_query = SSdbcore.NewQuery( - "UPDATE [table_player] SET metacoins = metacoins - :price WHERE ckey = :ckey AND metacoins >= :price", - list( - "price" = listing.price, - "ckey" = target_ckey, - ), - ) - - if(!buy_query.warn_execute(async = FALSE)) - qdel(buy_query) - return list("ok" = FALSE, "error" = "db_failed") - qdel(buy_query) - - var/new_balance = fetch_balance(target_ckey) - if(isnull(new_balance)) - return list("ok" = FALSE, "error" = "db_failed") - - if(new_balance > (current_balance - listing.price)) - return list("ok" = FALSE, "error" = "not_enough") + take = take_metacoins(target_ckey, listing.price) + if(!take["ok"]) + return take antag_token_pending_by_ckey[target_ckey] = role_id antag_token_slots_left = max(antag_token_slots_left - 1, 0) var/role_name = get_role_name(role_id) - log_game("[src] antag token purchase: ckey=[target_ckey], role=[role_id]/[role_name], price=[listing.price], balance_before=[current_balance], balance_after=[new_balance], slots_left=[antag_token_slots_left].") + var/balance_after = take["balance"] + log_game("[src] antag token purchase: ckey=[target_ckey], role=[role_id]/[role_name], price=[listing.price], balance_after=[balance_after], slots_left=[antag_token_slots_left].") + + listing.on_bought(src, target_ckey, player_mob, player_client, balance_after, role_id) - var/mob/player_mob = get_mob_by_ckey(target_ckey) if(player_mob) to_chat(player_mob, span_boldnicegreen("Purchased Antag Token ([role_name]) for [listing.price] metacoins. It will be applied at roundstart.")) player_mob.playsound_local(player_mob, 'sound/effects/kaching.ogg', 40, TRUE, use_reverb = FALSE) @@ -725,6 +818,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return grant_token_on_spawn(target_ckey, spawned, player_client) + grant_persistents(target_ckey, spawned, player_client) if(!ishuman(spawned)) return @@ -744,6 +838,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) continue var/obj/item/new_item = new listing.item_type(human_spawned) + listing.bought_on_spawn(src, target_ckey, human_spawned, new_item, player_client) if(human_spawned.back?.atom_storage?.attempt_insert(new_item, human_spawned, override = TRUE)) continue @@ -784,8 +879,8 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) data["isPregame"] = shop.is_open() data["balance"] = isnull(balance) ? 0 : balance data["antagTokenSlotsLeft"] = shop.get_token_slots() - data["preroundItems"] = shop.get_catalog_ui(client_ckey) - data["persistentItems"] = list() + data["preroundItems"] = shop.catalog_ui(client_ckey) + data["persistentItems"] = shop.catalog_ui(client_ckey, "persistent") return data @@ -807,7 +902,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) if(!target_item) return FALSE - var/result = get_metacoin_controller().buy_item(owner?.ckey, target_item) + var/result = get_metacoin_controller().buy(owner?.ckey, target_item, null, owner) if(!result["ok"]) var/mob/user_mob = ui?.user if(user_mob) @@ -834,6 +929,32 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) //those exist justin cause ^^ return TRUE + if(action == "buy_persistent") + var/target_item = params["itemId"] + if(!target_item) + return FALSE + + var/result = get_metacoin_controller().buy(owner?.ckey, target_item, null, owner) + if(!result["ok"]) + var/mob/user_mob = ui?.user + if(user_mob) + switch(result["error"]) + if("already_owned") + to_chat(user_mob, span_warning("You already own this persistent reward.")) + user_mob.playsound_local(user_mob, 'sound/machines/compiler/compiler-failure.ogg', 40, TRUE, use_reverb = FALSE) + if("not_enough") + to_chat(user_mob, span_warning("Not enough metacoins.")) + user_mob.playsound_local(user_mob, 'sound/machines/compiler/compiler-failure.ogg', 40, TRUE, use_reverb = FALSE) + if("db_unavailable", "db_failed") + to_chat(user_mob, span_warning("Database error. Try again later.")) + user_mob.playsound_local(user_mob, 'sound/machines/compiler/compiler-failure.ogg', 40, TRUE, use_reverb = FALSE) + else + to_chat(user_mob, span_warning("Purchase failed.")) + user_mob.playsound_local(user_mob, 'sound/machines/compiler/compiler-failure.ogg', 40, TRUE, use_reverb = FALSE) + return FALSE + + return TRUE + return FALSE /datum/metacoin_antag_token_panel @@ -885,7 +1006,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) if(!role_id) return FALSE - var/result = get_metacoin_controller().buy_token(owner?.ckey, role_id) + var/result = get_metacoin_controller().buy(owner?.ckey, "antag_token", role_id, owner) if(result["ok"]) return TRUE diff --git a/modular_meta/features/metacoins/code/persistent.dm b/modular_meta/features/metacoins/code/persistent.dm new file mode 100644 index 000000000000..0e9f633bf4b0 --- /dev/null +++ b/modular_meta/features/metacoins/code/persistent.dm @@ -0,0 +1,25 @@ +/* + /datum/metacoinshop/listing/persistent/example_reward + id = "example_reward" + name = "Example Reward" + desc = "Displayed text" + price = 220 + +ID is the variable you use in owns_persistnent() proc, it refers to a line in DB, and returns 0 or 1 depending whether was it bought. + +To check if a reward is bought + var/datum/metacoin_shop_controller/shop = get_metacoin_controller() + if(shop.owns_persistent(user.ckey, "example_reward") == TRUE) + // There goes your feature/additional logic/etc. + // you can lock anything behind it, unique reskins? job titles? jobs? like be able to spawn as an NT official? anything whatsoever! +*/ + +// DO NOT CHANGE ID'S, DOING SO WILL RESULT A DUPLICATE IN DB +/datum/metacoinshop/listing/persistent/wycc_soul + id = "wycc_soul" + name = "Unusual Cursed Strange Genuine Unique Vintage Collector's Decorated Community Self-made massmeta Wycc's own Soul" + desc = "Spooky!" + listing_type = "persistent" + price = 220**2.20 + icon = 'icons/mob/human/species/ghost.dmi' + icon_state = "ghost_base" diff --git a/modular_meta/features/metacoins/code/shop_items.dm b/modular_meta/features/metacoins/code/shop_items.dm index e3fb8e907405..8daffdfaab77 100644 --- a/modular_meta/features/metacoins/code/shop_items.dm +++ b/modular_meta/features/metacoins/code/shop_items.dm @@ -8,8 +8,36 @@ var/icon var/icon_state +/// Is called after a successful purchase. +/datum/metacoinshop/listing/proc/on_bought(datum/metacoin_shop_controller/shop, target_ckey, mob/player_mob, client/player_client, balance_after, role_id = null) + return TRUE +// here lies any additional logic you might want to add, mainly, I added it because I wanted to see a cool announcement when wycc's soul is bought + +/datum/metacoinshop/listing/proc/bought_on_spawn(datum/metacoin_shop_controller/shop, target_ckey, mob/living/carbon/human/human_spawned, obj/item/item, client/player_client) + return + +// It's like the same ^^^ but you may edit variables of stuff in params, like human_spawned or item. e.g you may buy a baton, \ +then have it variable edit'ed like so item.force = 25, potentially escaping any additional hardcode or unneeded bloat + +/datum/metacoinshop/listing/proc/persistent_grant(datum/metacoin_shop_controller/shop, target_ckey, mob/living/spawned, client/player_client) + return +// it's persistenly repeated code on each round if the ss.db.query() returns true whether something's been bought, use it for any effects on demand +// later on we'll add a preference flag to disable bought items on demand + /datum/metacoinshop/listing/preround +/* +/datum/metacoinshop/listing/preround/donut_box/on_bought(datum/metacoin_shop_controller/shop, target_ckey, mob/player_mob, client/player_client, balance_after, role_id = null) + to_chat(player_mob, span_green("success! die!")) // no idea why's there mob/player mob in params, but it's there, it exists! and it matters! + +/datum/metacoinshop/listing/preround/donut_box/bought_on_spawn(datum/metacoin_shop_controller/shop, target_ckey, mob/living/carbon/human/human_spawned, obj/item/item, client/player_client) + to_chat(human_spawned, span_green("success!")) + sleep(20) + to_chat(human_spawned, span_red("die!")) + sleep(10) + human_spawned.gib()// DIE! + +*/ /datum/metacoinshop/listing/preround/donut_box id = "donut_box" name = "Donut Box" @@ -49,7 +77,7 @@ var/obj/item/stack/medical/mesh/mesh = new /obj/item/stack/medical/mesh(src) mesh.amount = 10 var/obj/item/stock_parts/power_store/cell/high/cell = new /obj/item/stock_parts/power_store/cell/high(src) - cell.charge = 10000 // the fuck is a cell unit? a thousand I guess? cuz hundred is 0.1% + cell.charge = 10000 // the fuck is a cell unit? ten thousand I guess? because after test a hundred is turned to be 0.1% var/obj/item/tank/jetpack/jpack = new /obj/item/tank/jetpack(src) //get current gas var/datum/gas_mixture/gas = jpack.return_air() // bitch it's literally oxygen diff --git a/modular_meta/features/metacoins/includes.dm b/modular_meta/features/metacoins/includes.dm index 5e4ee5455402..d114466a7def 100644 --- a/modular_meta/features/metacoins/includes.dm +++ b/modular_meta/features/metacoins/includes.dm @@ -1,6 +1,7 @@ #include "code\metacoin_deathmatch.dm" #include "code\award_overrides.dm" #include "code\shop_items.dm" +#include "code\persistent.dm" #include "code\metacoin.dm" #include "code\metacoin_shop.dm" #include "code\metacoin_gambling.dm" diff --git a/tgui/packages/tgui/interfaces/MetaCoinShop.tsx b/tgui/packages/tgui/interfaces/MetaCoinShop.tsx index 89076704a8ee..f882db1cd388 100644 --- a/tgui/packages/tgui/interfaces/MetaCoinShop.tsx +++ b/tgui/packages/tgui/interfaces/MetaCoinShop.tsx @@ -58,7 +58,7 @@ const renderListingIcon = (item: ShopItem) => { export const MetaCoinShop = () => { const { act, data } = useBackend(); - const { isPregame, balance, preroundItems = [] } = data; + const { isPregame, balance, preroundItems = [], persistentItems = [] } = data; const [activeTab, setActiveTab] = useLocalState<'preround' | 'persistent'>( 'metacoinShopTab', @@ -218,7 +218,60 @@ export const MetaCoinShop = () => { {activeTab === 'persistent' && (
- Persistent rewards are not implemented yet. + {!persistentItems.length ? ( + No persistent rewards available. + ) : ( + + {persistentItems.map((item) => { + const owned = Boolean(item.owned); + const canAfford = Boolean(item.canAfford); + const buttonDisabled = owned || !canAfford; + + return ( + +
+ act('buy_persistent', { + itemId: item.id, + }) + } + > + Buy ({item.price}) + + } + > + + {renderListingIcon(item)} + + {item.desc} + + Price: {item.price} + + + {owned && ( + + Already owned. + + )} + + {!canAfford && !owned && ( + + Not enough metacoins. + + )} + + +
+
+ ); + })} +
+ )}
)} From 038398e9d471354f66baf585ef787d73298bca41 Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Sun, 26 Apr 2026 09:01:10 +0300 Subject: [PATCH 13/18] =?UTF-8?q?govnocoders=E2=84=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit бля, ещё раз ты будешь сука копировать не читая ебало снесу ублюдок --- .../features/metacoins/code/metacoin_shop.dm | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/modular_meta/features/metacoins/code/metacoin_shop.dm b/modular_meta/features/metacoins/code/metacoin_shop.dm index 69a01f093b49..b2dbdc5cb309 100644 --- a/modular_meta/features/metacoins/code/metacoin_shop.dm +++ b/modular_meta/features/metacoins/code/metacoin_shop.dm @@ -76,7 +76,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return preround_catalog["antag_token"] /datum/metacoin_shop_controller/proc/get_token_slots() - return max(antag_token_slots_left, 0) + return antag_token_slots_left /datum/metacoin_shop_controller/proc/get_restricted_jobs() var/static/list/antag_token_restricted_jobs = list( @@ -165,30 +165,30 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return FALSE if(isnum(weight_setting)) - return text2num("[weight_setting]") > 0 + return weight_setting > 0 if(islist(weight_setting)) for(var/key in weight_setting) - if(text2num("[weight_setting[key]]") > 0) + if(weight_setting[key] > 0) return TRUE return FALSE /datum/metacoin_shop_controller/proc/resolve_min_pop(min_pop_setting, fallback_value) if(isnum(min_pop_setting)) - return max(text2num("[min_pop_setting]"), 0) + return min_pop_setting if(islist(min_pop_setting)) var/best_value for(var/key in min_pop_setting) - var/current_value = max(text2num("[min_pop_setting[key]]"), 0) + var/current_value = min_pop_setting[key] if(isnull(best_value) || current_value < best_value) best_value = current_value if(!isnull(best_value)) return best_value - return max(text2num("[fallback_value]"), 0) + return fallback_value /datum/metacoin_shop_controller/proc/get_role_block(target_ckey, role_id, datum/job/current_job = null) var/datum/metacoinshop/antag_role/role = get_antag_role(role_id) @@ -424,7 +424,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) var/is_owned = FALSE if(select_query.NextRow(async = FALSE)) - is_owned = text2num("[select_query.item[1]]") > 0 + is_owned = select_query.item[1] > 0 qdel(select_query) return is_owned @@ -502,7 +502,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) var/metacoin_balance = 0 if(select_query.NextRow(async = FALSE)) - metacoin_balance = text2num(select_query.item[1]) || 0 + metacoin_balance = select_query.item[1] qdel(select_query) return metacoin_balance @@ -663,7 +663,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return take antag_token_pending_by_ckey[target_ckey] = role_id - antag_token_slots_left = max(antag_token_slots_left - 1, 0) + antag_token_slots_left-- var/role_name = get_role_name(role_id) var/balance_after = take["balance"] log_game("[src] antag token purchase: ckey=[target_ckey], role=[role_id]/[role_name], price=[listing.price], balance_after=[balance_after], slots_left=[antag_token_slots_left].") From ae8613816f5c4806c6b7a17cdabd19f01a0b777a Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Sun, 26 Apr 2026 09:38:10 +0300 Subject: [PATCH 14/18] logs && award_entries() things --- .../features/metacoins/code/metacoin.dm | 93 ++++++++++++++----- 1 file changed, 71 insertions(+), 22 deletions(-) diff --git a/modular_meta/features/metacoins/code/metacoin.dm b/modular_meta/features/metacoins/code/metacoin.dm index 0165ac0750a1..21a60f12351f 100644 --- a/modular_meta/features/metacoins/code/metacoin.dm +++ b/modular_meta/features/metacoins/code/metacoin.dm @@ -69,12 +69,9 @@ GLOBAL_DATUM(metacoins_controller, /datum/metacoins_controller) processed_ckeys[player_ckey] = TRUE - if(METACOIN_REWARD_SURVIVE_EVAC > 0 && is_evacuation_condition_met(player_ckey)) - award_metacoins(player_ckey, METACOIN_REWARD_SURVIVE_EVAC, "survived_shift", "Survived Shift") - if(METACOIN_REWARD_IMPORTANT_JOBS > 0 && is_important_role(player_ckey)) - award_metacoins(player_ckey, METACOIN_REWARD_IMPORTANT_JOBS, "social_role", "Highly Important Role") - if(METACOIN_REWARD_ANTAG_GREENTEXT > 0 && is_antag_greentext(player_ckey)) - award_metacoins(player_ckey, METACOIN_REWARD_ANTAG_GREENTEXT, "antag_greentext", "Antagonist Greentext") + var/list/rewards = get_round_rewards(player_ckey) + if(length(rewards)) + award_entries(player_ckey, rewards) /// Main proc for your awards. Integrate it wherever you like to /// /// Arguments: @@ -95,8 +92,17 @@ GLOBAL_DATUM(metacoins_controller, /datum/metacoins_controller) if(!target_ckey || amount <= 0) return FALSE - var/sanitized_source = source || "unknown" - var/sanitized_reason = reason || "Reward" + var/list/rewards = list(list( + "amount" = amount, + "source" = source || "unknown", + "reason" = reason || "Reward", + "by_award_type" = resolve_from_award_type, + )) + return award_entries(target_ckey, rewards, allow_repeat, sound) + +/datum/metacoins_controller/proc/award_entries(target_ckey, list/rewards, allow_repeat = FALSE, sound = TRUE) + if(!target_ckey || !length(rewards)) + return FALSE var/list/source_awards if(!allow_repeat) @@ -105,28 +111,48 @@ GLOBAL_DATUM(metacoins_controller, /datum/metacoins_controller) source_awards = list() awarded_sources_by_ckey[target_ckey] = source_awards - if(source_awards[sanitized_source]) - return FALSE + var/total_amount = 0 + var/list/pay_rewards = list() + for(var/list/reward_entry as anything in rewards) + var/amount = reward_entry["amount"] || 0 + if(amount <= 0) + continue + + var/sanitized_source = reward_entry["source"] || "unknown" + var/sanitized_reason = reward_entry["reason"] || "Reward" + if(!allow_repeat && source_awards[sanitized_source]) + log_game("[src] metacoin payout skipped: ckey=[target_ckey], amount=[amount], source='[sanitized_source]', reason='[sanitized_reason]', cause='duplicate source'.") + continue + + pay_rewards += list(list( + "amount" = amount, + "source" = sanitized_source, + "reason" = sanitized_reason, + "by_award_type" = reward_entry["by_award_type"] || FALSE, + )) + total_amount += amount + + if(total_amount <= 0) + return FALSE if(!SSdbcore.Connect()) + log_game("[src] metacoin payout failed: ckey=[target_ckey], amount=[total_amount], cause='db unavailable', rewards=[json_encode(pay_rewards)].") return FALSE - if(!add_metacoins(target_ckey, amount)) + if(!add_metacoins(target_ckey, total_amount)) + log_game("[src] metacoin payout failed: ckey=[target_ckey], amount=[total_amount], cause='update failed', rewards=[json_encode(pay_rewards)].") return FALSE if(!allow_repeat) - source_awards[sanitized_source] = TRUE + for(var/list/reward_entry as anything in pay_rewards) + source_awards[reward_entry["source"]] = TRUE - log_game("[src] metacoin payout: ckey=[target_ckey], amount=[amount], source='[sanitized_source]', reason='[sanitized_reason]', allow_repeat=[allow_repeat], by_award_type=[resolve_from_award_type].") + log_game("[src] metacoin payout: ckey=[target_ckey], amount=[total_amount], allow_repeat=[allow_repeat], rewards=[json_encode(pay_rewards)].") - add_round_award_log_entry(target_ckey, amount, sanitized_source, sanitized_reason) + for(var/list/reward_entry as anything in pay_rewards) + add_round_award_log_entry(target_ckey, reward_entry["amount"], reward_entry["source"], reward_entry["reason"]) - var/list/reward_entries = list(list( - "amount" = amount, - "source" = sanitized_source, - "reason" = sanitized_reason, - )) - notify_player_reward_awarded(target_ckey, amount, reward_entries, sound) + notify_player_reward_awarded(target_ckey, total_amount, pay_rewards, sound) var/mob/player_mob = get_mob_by_ckey(target_ckey) if(player_mob) @@ -134,6 +160,28 @@ GLOBAL_DATUM(metacoins_controller, /datum/metacoins_controller) return TRUE +/datum/metacoins_controller/proc/get_round_rewards(target_ckey) + var/list/rewards = list() + if(METACOIN_REWARD_SURVIVE_EVAC > 0 && is_evacuation_condition_met(target_ckey)) + rewards += list(list( + "amount" = METACOIN_REWARD_SURVIVE_EVAC, + "source" = "survived_shift", + "reason" = "Survived Shift", + )) + if(METACOIN_REWARD_IMPORTANT_JOBS > 0 && is_important_role(target_ckey)) + rewards += list(list( + "amount" = METACOIN_REWARD_IMPORTANT_JOBS, + "source" = "social_role", + "reason" = "Highly Important Role", + )) + if(METACOIN_REWARD_ANTAG_GREENTEXT > 0 && is_antag_greentext(target_ckey)) + rewards += list(list( + "amount" = METACOIN_REWARD_ANTAG_GREENTEXT, + "source" = "antag_greentext", + "reason" = "Antagonist Greentext", + )) + return rewards + /datum/metacoins_controller/proc/get_reward_amount(award_type) if(!ispath(award_type, /datum/award)) return 0 @@ -222,7 +270,7 @@ GLOBAL_DATUM(metacoins_controller, /datum/metacoins_controller) if(!player_turf) return FALSE - if(player_turf.onCentCom()) + if(player_turf.onCentCom() || player_turf.onSyndieBase() || player_turf.on_escaped_shuttle()) return TRUE return !!SSshuttle.emergency.shuttle_areas[player_area] @@ -298,8 +346,9 @@ GLOBAL_DATUM(metacoins_controller, /datum/metacoins_controller) ) var/success = update_query.warn_execute(async = FALSE) + var/affected = update_query.affected qdel(update_query) - return success + return success && affected > 0 /datum/metacoins_panel var/client/owner From 3946d158312681384f650a9450fd74d816623643 Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Sun, 26 Apr 2026 09:57:31 +0300 Subject: [PATCH 15/18] comment out that useless shit gotta clarify 'bout that you can gamble as a ghost :) also makes proc names actually readable --- .../features/metacoins/code/metacoin.dm | 26 +++++++++---------- .../metacoins/code/metacoin_gambling.dm | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/modular_meta/features/metacoins/code/metacoin.dm b/modular_meta/features/metacoins/code/metacoin.dm index 21a60f12351f..25f12ca19e0a 100644 --- a/modular_meta/features/metacoins/code/metacoin.dm +++ b/modular_meta/features/metacoins/code/metacoin.dm @@ -81,7 +81,7 @@ GLOBAL_DATUM(metacoins_controller, /datum/metacoins_controller) /// * reason - reason shown in reward chat message. /// * allow_repeat - If TRUE, skips source dedupe and allows payout on every call. /// * resolve_from_award_type - If TRUE, reward_value is resolved through the award datum's reward var. -/// * sound - If TRUE plays a sound, check notify_player_reward_awarded +/// * sound - If TRUE plays a sound, check notify_reward /// Returns TRUE when payout is persisted, FALSE otherwise. /datum/metacoins_controller/proc/award_metacoins(target_ckey, reward_value, source, reason, allow_repeat = FALSE, resolve_from_award_type = FALSE, sound = TRUE) var/amount @@ -152,7 +152,7 @@ GLOBAL_DATUM(metacoins_controller, /datum/metacoins_controller) for(var/list/reward_entry as anything in pay_rewards) add_round_award_log_entry(target_ckey, reward_entry["amount"], reward_entry["source"], reward_entry["reason"]) - notify_player_reward_awarded(target_ckey, total_amount, pay_rewards, sound) + notify_reward(target_ckey, total_amount, pay_rewards, sound) var/mob/player_mob = get_mob_by_ckey(target_ckey) if(player_mob) @@ -293,13 +293,13 @@ GLOBAL_DATUM(metacoins_controller, /datum/metacoins_controller) return TRUE if(antag_datum.antag_flags & ANTAG_FAKE) continue - if(!is_antag_objectives_successful(antag_datum)) + if(!is_greentext(antag_datum)) continue return TRUE return FALSE -/datum/metacoins_controller/proc/is_antag_objectives_successful(datum/antagonist/antag_datum) +/datum/metacoins_controller/proc/is_greentext(datum/antagonist/antag_datum) if(!antag_datum) return FALSE @@ -312,7 +312,7 @@ GLOBAL_DATUM(metacoins_controller, /datum/metacoins_controller) return TRUE -/datum/metacoins_controller/proc/notify_player_reward_awarded(target_ckey, total_reward, list/reward_entries, sound = TRUE) +/datum/metacoins_controller/proc/notify_reward(target_ckey, total_reward, list/reward_entries, sound = TRUE) if(total_reward <= 0) return @@ -378,7 +378,7 @@ GLOBAL_DATUM(metacoins_controller, /datum/metacoins_controller) data["roundAwardLog"] = client_ckey ? controller.get_round_award_log(client_ckey) : list() data["canOpenShop"] = TRUE - var/balance = fetch_metacoin_balance(client_ckey) + var/balance = fetch_balance(client_ckey) data["dbConnected"] = !isnull(balance) data["balance"] = isnull(balance) ? 0 : balance @@ -395,7 +395,7 @@ GLOBAL_DATUM(metacoins_controller, /datum/metacoins_controller) return FALSE -/datum/metacoins_panel/proc/fetch_metacoin_balance(target_ckey) +/datum/metacoins_panel/proc/fetch_balance(target_ckey) if(!target_ckey) return 0 @@ -441,7 +441,7 @@ ADMIN_VERB(mc_give, R_ADMIN, "Grant Metacoins", "Grant metacoins to a target cke if(!length(grant_reason)) grant_reason = "Manual admin grant" - var/create_note = tgui_alert(user, "Include a note?", "Grant Metacoins", list("No", "Yes")) == "Yes" + // var/create_note = tgui_alert(user, "Include a note?", "Grant Metacoins", list("No", "Yes")) == "Yes" var/datum/metacoins_controller/controller = get_metacoins_controller() if(!controller) @@ -458,15 +458,15 @@ ADMIN_VERB(mc_give, R_ADMIN, "Grant Metacoins", "Grant metacoins to a target cke log_admin("[key_name(user)] failed to grant [amount] metacoins to [target_ckey]. Reason='[grant_reason]'.") to_chat(user, span_warning("Failed to grant metacoins. Check SQL logs"), confidential = TRUE) return - +/* // i've thought about it, that's kinda useless if(create_note) var/note_text = "Metacoins granted: +[amount]. Reason: [grant_reason]" create_message("note", target_ckey, user.ckey, note_text, null, null, 0, 0, null, 0, "none") - - var/admin_msg = "[key_name_admin(user)] granted [amount] metacoins to [target_ckey]. Reason='[grant_reason]'. Auto-note=[create_note ? "yes" : "no"]." +*/ + var/admin_msg = "[key_name_admin(user)] granted [amount] metacoins to [target_ckey]. Reason='[grant_reason]']." message_admins(admin_msg) - log_admin("[key_name(user)] granted [amount] metacoins to [target_ckey]. Reason='[grant_reason]'. Auto-note=[create_note ? "yes" : "no"].") - log_game("[key_name(user)] granted [amount] metacoins to [target_ckey]. Reason='[grant_reason]'. Auto-note=[create_note ? "yes" : "no"].") + log_admin("[key_name(user)] granted [amount] metacoins to [target_ckey]. Reason='[grant_reason]'.") + log_game("[key_name(user)] granted [amount] metacoins to [target_ckey]. Reason='[grant_reason]'].") /client/verb/view_metacoins() set name = "View Metacoins" diff --git a/modular_meta/features/metacoins/code/metacoin_gambling.dm b/modular_meta/features/metacoins/code/metacoin_gambling.dm index 3c3cf5b58408..57f61d630926 100644 --- a/modular_meta/features/metacoins/code/metacoin_gambling.dm +++ b/modular_meta/features/metacoins/code/metacoin_gambling.dm @@ -382,7 +382,7 @@ if(user_mob) switch(result["error"]) if("shop_closed") - to_chat(user_mob, span_warning("Metacoin slot machine is available only before round start.")) + to_chat(user_mob, span_warning("Metacoin slot machine is available only for ghosts and in pre-game lobby.")) if("invalid_bet") to_chat(user_mob, span_warning("Invalid bet selected.")) if("not_enough") From c1e9dd8217cebb03d497aeff77175c5c39fd8f63 Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Sun, 26 Apr 2026 13:01:38 +0300 Subject: [PATCH 16/18] that's just not true --- modular_meta/features/metacoins/code/shop_items.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modular_meta/features/metacoins/code/shop_items.dm b/modular_meta/features/metacoins/code/shop_items.dm index 8daffdfaab77..e6727ab53cd5 100644 --- a/modular_meta/features/metacoins/code/shop_items.dm +++ b/modular_meta/features/metacoins/code/shop_items.dm @@ -21,7 +21,7 @@ then have it variable edit'ed like so item.force = 25, potentially escaping any /datum/metacoinshop/listing/proc/persistent_grant(datum/metacoin_shop_controller/shop, target_ckey, mob/living/spawned, client/player_client) return -// it's persistenly repeated code on each round if the ss.db.query() returns true whether something's been bought, use it for any effects on demand +// it's persistenly repeated code on each round if the SSdb returns true whether something's been bought, use it for any effects on demand // later on we'll add a preference flag to disable bought items on demand /datum/metacoinshop/listing/preround From b704dd710cba5a4bfc7cdefc58308c717eab54f0 Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Sun, 26 Apr 2026 14:02:57 +0300 Subject: [PATCH 17/18] surgery chip && "other" now works properly --- .../features/metacoins/code/metacoin_shop.dm | 14 +++++++++++--- modular_meta/features/metacoins/code/shop_items.dm | 12 ++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/modular_meta/features/metacoins/code/metacoin_shop.dm b/modular_meta/features/metacoins/code/metacoin_shop.dm index b2dbdc5cb309..c879448d4e03 100644 --- a/modular_meta/features/metacoins/code/metacoin_shop.dm +++ b/modular_meta/features/metacoins/code/metacoin_shop.dm @@ -612,7 +612,7 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) return list("ok" = TRUE) - if(listing.listing_type == "item") + if(listing.listing_type == "item" || (listing.listing_type == "other" && listing.id != "antag_token")) if(!is_open()) return list("ok" = FALSE, "error" = "shop_closed") @@ -633,7 +633,8 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) listing.on_bought(src, target_ckey, player_mob, player_client, take["balance"]) if(player_mob) - to_chat(player_mob, span_boldnicegreen("Purchased [listing.name] for [listing.price] metacoins. It will be delivered on first roundstart spawn.")) + var/delivery_text = listing.listing_type == "item" ? "delivered" : "applied" + to_chat(player_mob, span_boldnicegreen("Purchased [listing.name] for [listing.price] metacoins. It will be [delivery_text] on first roundstart spawn.")) player_mob.playsound_local(player_mob, 'sound/effects/kaching.ogg', 40, TRUE, use_reverb = FALSE) SStgui.update_user_uis(player_mob) @@ -834,7 +835,14 @@ GLOBAL_DATUM(metacoin_shop_controller, /datum/metacoin_shop_controller) for(var/item_id in pending_items) var/datum/metacoinshop/listing/listing = preround_catalog[item_id] - if(listing?.listing_type != "item" || !listing?.item_type) + if(!listing) + continue + + if(listing.listing_type == "other") + listing.bought_on_spawn(src, target_ckey, human_spawned, null, player_client) + continue + + if(listing.listing_type != "item" || !listing.item_type) continue var/obj/item/new_item = new listing.item_type(human_spawned) diff --git a/modular_meta/features/metacoins/code/shop_items.dm b/modular_meta/features/metacoins/code/shop_items.dm index e6727ab53cd5..98f39cfbc0b6 100644 --- a/modular_meta/features/metacoins/code/shop_items.dm +++ b/modular_meta/features/metacoins/code/shop_items.dm @@ -97,6 +97,18 @@ then have it variable edit'ed like so item.force = 25, potentially escaping any new /obj/item/stock_parts/servo/pico(src) new /obj/item/flashlight/seclite(src) +/datum/metacoinshop/listing/preround/self_surgery + id = "surgery" + name = "surgeon" + desc = "surg" + price = 250 + item_type = /obj/item/skillchip/self_surgery + listing_type = "other" + +/datum/metacoinshop/listing/preround/self_surgery/bought_on_spawn(datum/metacoin_shop_controller/shop, target_ckey, mob/living/carbon/human/human_spawned, obj/item/item, client/player_client) + var/obj/item/skillchip/skillchip = new item_type() + human_spawned.implant_skillchip(skillchip, TRUE) + /datum/metacoinshop/listing/preround/antag_token id = "antag_token" name = "Antag Token" From bfc0683feffe0fbf970e94a0f557b18720475f8c Mon Sep 17 00:00:00 2001 From: Bruh-24 Date: Sun, 26 Apr 2026 14:10:59 +0300 Subject: [PATCH 18/18] =?UTF-8?q?desc=20&&=20name=20=20(=E2=98=95=E2=84=A2?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modular_meta/features/metacoins/code/shop_items.dm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modular_meta/features/metacoins/code/shop_items.dm b/modular_meta/features/metacoins/code/shop_items.dm index 98f39cfbc0b6..458142a95921 100644 --- a/modular_meta/features/metacoins/code/shop_items.dm +++ b/modular_meta/features/metacoins/code/shop_items.dm @@ -99,8 +99,9 @@ then have it variable edit'ed like so item.force = 25, potentially escaping any /datum/metacoinshop/listing/preround/self_surgery id = "surgery" - name = "surgeon" - desc = "surg" + name = "4U70-P3R4710N skillchip" + desc = "A skillchip containing old Nanotrasen medical training protocols, which one could use to perform surgical operations on themselves. \ + This one is new, I'd say, \"almost pristine condition.\" It will be already implanted in your brain upon purchause and your arrival." price = 250 item_type = /obj/item/skillchip/self_surgery listing_type = "other"