mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-11 10:11:09 +00:00
Adds a sports betting/polling app (#90421)
## About The Pull Request Adds a new PDA app that allows you to create polls that people can bet on with credits, the owner can then lock the bets, decide what the answer is (up to the player for whatever poll they made), and send it off, giving the money the losers bet into the accounts of the winners. It's a small PDA app that currently doesn't make any announcements or come preinstalled in anything, but that's subject to change. PDA screen sprite is codersprited. Video demonstration https://github.com/user-attachments/assets/449e1f0b-7fd3-4948-bff8-2793af831360 Not shown (as I added later), it now uses newscasters as well.  ###### Code bounty of https://forums.tgstation13.org/viewtopic.php?t=38278 ## Why It's Good For The Game Allows people to host polls for station events, allowing for the SS13 version of sports betting. ## Changelog 🆑 add: Adds a new sports betting app on your PDA, you can now host and vote on polls using in-game credits. /🆑 --------- Co-authored-by: san7890 <the@san7890.com>
This commit is contained in:
@@ -5,3 +5,7 @@
|
||||
#define ANNOUNCEMENT_TYPE_CAPTAIN "Captain"
|
||||
/// Make it sound like it's coming from the Syndicate
|
||||
#define ANNOUNCEMENT_TYPE_SYNDICATE "Syndicate"
|
||||
|
||||
//Defines for newscaster news stations, the defined thing is what it'll be called in the Newscaster.
|
||||
#define NEWSCASTER_STATION_ANNOUNCEMENTS "Station Announcements"
|
||||
#define NEWSCASTER_SPACE_BETTING "SpaceBet"
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
header += SUBHEADER_ANNOUNCEMENT_TITLE(title)
|
||||
if(ANNOUNCEMENT_TYPE_CAPTAIN)
|
||||
header = MAJOR_ANNOUNCEMENT_TITLE("Captain's Announcement")
|
||||
GLOB.news_network.submit_article(text, "Captain's Announcement", "Station Announcements", null)
|
||||
GLOB.news_network.submit_article(text, "Captain's Announcement", NEWSCASTER_STATION_ANNOUNCEMENTS, null)
|
||||
if(ANNOUNCEMENT_TYPE_SYNDICATE)
|
||||
header = MAJOR_ANNOUNCEMENT_TITLE("Syndicate Captain's Announcement")
|
||||
else
|
||||
@@ -87,9 +87,9 @@
|
||||
|
||||
if(isnull(sender_override) && players == GLOB.player_list)
|
||||
if(length(title) > 0)
|
||||
GLOB.news_network.submit_article(title + "<br><br>" + text, "[command_name()]", "Station Announcements", null)
|
||||
GLOB.news_network.submit_article(title + "<br><br>" + text, "[command_name()]", NEWSCASTER_STATION_ANNOUNCEMENTS, null)
|
||||
else
|
||||
GLOB.news_network.submit_article(text, "[command_name()] Update", "Station Announcements", null)
|
||||
GLOB.news_network.submit_article(text, "[command_name()] Update", NEWSCASTER_STATION_ANNOUNCEMENTS, null)
|
||||
|
||||
/proc/print_command_report(text = "", title = null, announce=TRUE)
|
||||
if(!title)
|
||||
|
||||
@@ -187,7 +187,7 @@ SUBSYSTEM_DEF(economy)
|
||||
update_alerts = TRUE
|
||||
inflict_moneybags(moneybags)
|
||||
earning_report += "That's all from the <i>Nanotrasen Economist Division</i>."
|
||||
GLOB.news_network.submit_article(earning_report, "Station Earnings Report", "Station Announcements", null, update_alert = update_alerts)
|
||||
GLOB.news_network.submit_article(earning_report, "Station Earnings Report", NEWSCASTER_STATION_ANNOUNCEMENTS, null, update_alert = update_alerts)
|
||||
return TRUE
|
||||
|
||||
/**
|
||||
|
||||
@@ -185,7 +185,8 @@ GLOBAL_LIST_EMPTY(request_list)
|
||||
var/message_count = 0
|
||||
|
||||
/datum/feed_network/New()
|
||||
create_feed_channel("Station Announcements", "SS13", "Company news, staff announcements, and all the latest information. Have a secure shift!", locked = TRUE, hardset_channel = 1000)
|
||||
create_feed_channel(NEWSCASTER_STATION_ANNOUNCEMENTS, "SS13", "Company news, staff announcements, and all the latest information. Have a secure shift!", locked = TRUE, hardset_channel = 1000)
|
||||
create_feed_channel(NEWSCASTER_SPACE_BETTING, "NtOS", "News from the SpaceBet PDA App! Download now and make your own bets!", locked = TRUE, hardset_channel = 1001)
|
||||
wanted_issue = new /datum/wanted_message
|
||||
|
||||
/datum/feed_network/proc/create_feed_channel(channel_name, author, desc, locked, adminChannel = FALSE, hardset_channel = null, author_ckey = null, cross_sector = FALSE, cross_sector_delay = null, receiving_cross_sector = FALSE)
|
||||
@@ -236,7 +237,6 @@ GLOBAL_LIST_EMPTY(request_list)
|
||||
newMsg.parent_ID = channel.channel_ID
|
||||
if (!channel.cross_sector)
|
||||
break
|
||||
|
||||
// Newscaster articles could be huge, and usefulness of first 50 symbols is dubious
|
||||
message_admins(span_adminnotice("Outgoing cross-sector newscaster article by [key_name(author_mob) || author] in channel [channel_name]."))
|
||||
var/list/payload = list(
|
||||
@@ -249,6 +249,18 @@ GLOBAL_LIST_EMPTY(request_list)
|
||||
|
||||
for(var/obj/machinery/newscaster/caster in GLOB.allCasters)
|
||||
caster.news_alert(channel_name, update_alert)
|
||||
return newMsg
|
||||
|
||||
///Submits a comment on the news network
|
||||
/datum/feed_network/proc/submit_comment(mob/user, comment_text, newscaster_username, datum/feed_message/current_message)
|
||||
var/datum/feed_comment/new_feed_comment = new/datum/feed_comment
|
||||
new_feed_comment.author = newscaster_username
|
||||
new_feed_comment.body = comment_text
|
||||
new_feed_comment.time_stamp = station_time_timestamp()
|
||||
GLOB.news_network.last_action ++
|
||||
current_message.comments += new_feed_comment
|
||||
if(user)
|
||||
user.log_message("(as [newscaster_username]) commented on message [current_message.return_body(-1)] -- [current_message.body]", LOG_COMMENT)
|
||||
|
||||
/datum/feed_network/proc/submit_wanted(criminal, body, scanned_user, datum/picture/picture, adminMsg = FALSE, newMessage = FALSE)
|
||||
wanted_issue.active = TRUE
|
||||
|
||||
@@ -741,13 +741,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster, 30)
|
||||
if(!newscaster_username)
|
||||
creating_comment = FALSE
|
||||
return TRUE
|
||||
var/datum/feed_comment/new_feed_comment = new/datum/feed_comment
|
||||
new_feed_comment.author = newscaster_username
|
||||
new_feed_comment.body = comment_text
|
||||
new_feed_comment.time_stamp = station_time_timestamp()
|
||||
GLOB.news_network.last_action ++
|
||||
current_message.comments += new_feed_comment
|
||||
user.log_message("(as [newscaster_username]) commented on message [current_message.return_body(-1)] -- [current_message.body]", LOG_COMMENT)
|
||||
GLOB.news_network.submit_comment(user, comment_text, newscaster_username, current_message)
|
||||
creating_comment = FALSE
|
||||
|
||||
/**
|
||||
|
||||
@@ -191,7 +191,7 @@ GLOBAL_LIST_EMPTY(req_console_ckey_departments)
|
||||
message = L.treat_message(message)["message"]
|
||||
|
||||
minor_announce(message, "[department] Announcement:", html_encode = FALSE, sound_override = 'sound/announcer/announcement/announce_dig.ogg')
|
||||
GLOB.news_network.submit_article(message, department, "Station Announcements", null)
|
||||
GLOB.news_network.submit_article(message, department, NEWSCASTER_STATION_ANNOUNCEMENTS, null)
|
||||
usr.log_talk(message, LOG_SAY, tag="station announcement from [src]")
|
||||
message_admins("[ADMIN_LOOKUPFLW(usr)] has made a station announcement from [src] at [AREACOORD(usr)].")
|
||||
deadchat_broadcast(" made a station announcement from [span_name("[get_area_name(usr, TRUE)]")].", span_name("[usr.real_name]"), usr, message_type=DEADCHAT_ANNOUNCEMENT)
|
||||
|
||||
285
code/modules/modular_computers/file_system/programs/betting.dm
Normal file
285
code/modules/modular_computers/file_system/programs/betting.dm
Normal file
@@ -0,0 +1,285 @@
|
||||
GLOBAL_LIST_EMPTY_TYPED(active_bets, /datum/active_bet)
|
||||
|
||||
///Max amount of characters you can have in an active bet's title
|
||||
#define MAX_LENGTH_TITLE 64
|
||||
///Max amount of characters you can have in an active bet's description
|
||||
#define MAX_LENGTH_DESCRIPTION 200
|
||||
|
||||
/datum/computer_file/program/betting
|
||||
filename = "betting"
|
||||
filedesc = "SpaceBet"
|
||||
downloader_category = PROGRAM_CATEGORY_GAMES
|
||||
program_open_overlay = "gambling"
|
||||
extended_desc = "A multi-platform network for placing requests across the station, with payment across the network being possible."
|
||||
program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_REQUIRES_NTNET
|
||||
can_run_on_flags = PROGRAM_PDA
|
||||
size = 4
|
||||
tgui_id = "NtosSpaceBetting"
|
||||
program_icon = "dice"
|
||||
|
||||
///The active bet this program made, as we can only have 1 going at a time to prevent flooding/spam.
|
||||
var/datum/active_bet/created_bet
|
||||
|
||||
/datum/computer_file/program/betting/New()
|
||||
. = ..()
|
||||
RegisterSignal(src, COMSIG_COMPUTER_FILE_DELETE, PROC_REF(on_delete))
|
||||
|
||||
///Called when we're deleted, we'll be taking the bet with us.
|
||||
/datum/computer_file/program/betting/proc/on_delete(datum/source, obj/item/modular_computer/computer_uninstalling)
|
||||
SIGNAL_HANDLER
|
||||
|
||||
created_bet.payout()
|
||||
QDEL_NULL(created_bet)
|
||||
|
||||
/datum/computer_file/program/betting/ui_data(mob/user)
|
||||
var/list/data = list()
|
||||
data["active_bets"] = list()
|
||||
for(var/datum/active_bet/bets as anything in GLOB.active_bets)
|
||||
data["active_bets"] += list(list(
|
||||
"name" = bets.name,
|
||||
"description" = bets.description,
|
||||
"owner" = bets == created_bet,
|
||||
"creator" = bets.bet_owner,
|
||||
"current_bets" = bets.get_bets(computer.computer_id_slot?.registered_account),
|
||||
"locked" = bets.locked,
|
||||
))
|
||||
|
||||
data["can_create_bet"] = !!isnull(created_bet)
|
||||
if(isnull(computer.computer_id_slot))
|
||||
data["bank_name"] = null
|
||||
data["bank_money"] = null
|
||||
else
|
||||
data["bank_name"] = computer.computer_id_slot.registered_account.account_holder
|
||||
data["bank_money"] = computer.computer_id_slot.registered_account.account_balance
|
||||
|
||||
return data
|
||||
|
||||
/datum/computer_file/program/betting/ui_static_data(mob/user)
|
||||
var/list/data = list()
|
||||
data["max_title_length"] = MAX_LENGTH_TITLE
|
||||
data["max_description_length"] = MAX_LENGTH_DESCRIPTION
|
||||
return data
|
||||
|
||||
/datum/computer_file/program/betting/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
|
||||
. = ..()
|
||||
var/mob/user = ui.user
|
||||
if(isnull(computer.computer_id_slot))
|
||||
to_chat(user, span_danger("\The [computer] flashes an \"RFID Error - Unable to scan ID\" warning."))
|
||||
return
|
||||
switch(action)
|
||||
if("create_bet")
|
||||
var/title = reject_bad_name(params["title"], allow_numbers = TRUE, max_length = MAX_LENGTH_TITLE, cap_after_symbols = FALSE)
|
||||
var/description = reject_bad_name(params["description"], allow_numbers = TRUE, max_length = MAX_LENGTH_DESCRIPTION, cap_after_symbols = FALSE)
|
||||
if(isnull(title) || isnull(description))
|
||||
return
|
||||
var/list/options = list(params["option1"], params["option2"], params["option3"], params["option4"])
|
||||
for(var/option in options)
|
||||
options -= option
|
||||
//remove nulls, empty, and duplicates.
|
||||
if(isnull(option) || option == "" || options.Find(option))
|
||||
continue
|
||||
options += option
|
||||
option = reject_bad_name(option, allow_numbers = TRUE, max_length = MAX_LENGTH_TITLE, cap_after_symbols = FALSE)
|
||||
if(length(options) < 2)
|
||||
to_chat(user, span_danger("2 options minimum required to start a bet."))
|
||||
return
|
||||
created_bet = new(user, title, description, options)
|
||||
return TRUE
|
||||
if("place_bet")
|
||||
var/datum/active_bet/bet_placed_on
|
||||
for(var/datum/active_bet/bets as anything in GLOB.active_bets)
|
||||
if(bets.name == params["bet_selected"])
|
||||
bet_placed_on = bets
|
||||
//can't bet on your own bet
|
||||
if(isnull(bet_placed_on))
|
||||
return
|
||||
if(bet_placed_on == created_bet)
|
||||
to_chat(user, span_danger("You can't bet on your own poll!"))
|
||||
return
|
||||
var/money_betting = params["money_betting"]
|
||||
if(!isnum(money_betting))
|
||||
return
|
||||
var/option = params["option_selected"]
|
||||
if(isnull(bet_placed_on))
|
||||
return
|
||||
bet_placed_on.bet_money(computer.computer_id_slot.registered_account, money_betting, option)
|
||||
return TRUE
|
||||
if("cancel_bet")
|
||||
var/datum/active_bet/bet_cancelling
|
||||
for(var/datum/active_bet/bets as anything in GLOB.active_bets)
|
||||
if(bets.name == params["bet_selected"])
|
||||
bet_cancelling = bets
|
||||
bet_cancelling.cancel_bet(computer.computer_id_slot.registered_account)
|
||||
return TRUE
|
||||
if("select_winner")
|
||||
var/datum/active_bet/bets_ending
|
||||
for(var/datum/active_bet/bets as anything in GLOB.active_bets)
|
||||
if(bets.name == params["bet_selected"])
|
||||
bets_ending = bets
|
||||
if(isnull(bets_ending) || bets_ending != created_bet)
|
||||
return
|
||||
created_bet.payout(params["winning_answer"])
|
||||
QDEL_NULL(created_bet)
|
||||
return TRUE
|
||||
if("lock_betting")
|
||||
var/datum/active_bet/bet_locking
|
||||
for(var/datum/active_bet/bets as anything in GLOB.active_bets)
|
||||
if(bets.name == params["bet_selected"])
|
||||
bet_locking = bets
|
||||
if(bet_locking != created_bet)
|
||||
return
|
||||
bet_locking.locked = TRUE
|
||||
|
||||
/**
|
||||
* The active bet that our app will create & use, handles following who owns the bet,
|
||||
* who is betting, and also takes care of paying out at the end.
|
||||
*/
|
||||
/datum/active_bet
|
||||
///The person owning the bet, who will choose which option has won.
|
||||
var/bet_owner
|
||||
///The name of the bet
|
||||
var/name
|
||||
///The description of the bet
|
||||
var/description
|
||||
///Boolean on whether the bet is locked from getting new betters, or current ones from taking their money out.
|
||||
var/locked
|
||||
///Total amount of money that has been bet.
|
||||
var/total_amount_bet
|
||||
/** Assoc list of options, with each option having a list of people betting and the amount they've bet.
|
||||
options = list(
|
||||
OPTION_A = list(
|
||||
PERSON_1_ACCOUNT = bet_amount,
|
||||
PERSON_2_ACCOUNT = bet_amount,
|
||||
),
|
||||
OPTION_B = list(
|
||||
PERSON_3_ACCOUNT = bet_amount,
|
||||
),
|
||||
)
|
||||
*/
|
||||
var/list/options
|
||||
|
||||
///The message we sent to the newscaster, which we'll then reply to once the betting is over.
|
||||
var/datum/feed_message/newscaster_message
|
||||
|
||||
/datum/active_bet/New(creator, name, description, options)
|
||||
src.bet_owner = creator
|
||||
src.name = name
|
||||
src.description = description
|
||||
src.options = options
|
||||
GLOB.active_bets += src
|
||||
for(var/option in options)
|
||||
if(!length(options[option]))
|
||||
options[option] = list()
|
||||
//we'll only advertise it on the first bet of the round, as to not make this overly annoying.
|
||||
var/should_alert = FALSE
|
||||
for(var/datum/feed_channel/FC in GLOB.news_network.network_channels)
|
||||
if(FC.channel_name == NEWSCASTER_SPACE_BETTING)
|
||||
if(!length(FC.messages))
|
||||
should_alert = TRUE
|
||||
newscaster_message = GLOB.news_network.submit_article("The bet [name] has started, place your bets now!", "NtOS Space Betting App", NEWSCASTER_SPACE_BETTING, null, update_alert = should_alert)
|
||||
|
||||
/datum/active_bet/Destroy(force)
|
||||
GLOB.active_bets -= src
|
||||
newscaster_message = null
|
||||
return ..()
|
||||
|
||||
///Returns how many bets there is per option
|
||||
/datum/active_bet/proc/get_bets(datum/bank_account/user_account)
|
||||
var/list/bets_per_option = list()
|
||||
for(var/option in options)
|
||||
var/amount_personally_invested = 0
|
||||
var/total_amount = 0
|
||||
for(var/list/existing_bets in options[option])
|
||||
var/existing_bet_amount = text2num(existing_bets[2])
|
||||
if(user_account && (existing_bets[1] == user_account))
|
||||
amount_personally_invested = existing_bet_amount
|
||||
total_amount += existing_bet_amount
|
||||
bets_per_option += list(list("option_name" = option, "amount" = total_amount, "personally_invested" = amount_personally_invested))
|
||||
return bets_per_option
|
||||
|
||||
///Pays out the loser's money equally to all the winners, or refunds it all if no winning option was given.
|
||||
/datum/active_bet/proc/payout(winning_option)
|
||||
if(isnull(winning_option) || !(winning_option in options))
|
||||
//no winner was selected (likely the host's PDA was destroyed or attempted href exploit), so let's refund everyone.
|
||||
for(var/list/option in options)
|
||||
for(var/list/existing_bets in options[option])
|
||||
var/datum/bank_account/refunded_account = existing_bets[1]
|
||||
refunded_account.adjust_money(text2num(existing_bets[2]), "Refund: [name] gamble cancelled.")
|
||||
return
|
||||
GLOB.news_network.submit_comment(
|
||||
comment_text = "The bet [name] has ended, the winner was [winning_option]!",
|
||||
newscaster_username = "NtOS Betting Results",
|
||||
current_message = newscaster_message,
|
||||
)
|
||||
var/list/winners = options[winning_option]
|
||||
if(!length(winners))
|
||||
return
|
||||
for(var/list/winner in winners)
|
||||
//they aren't winning their own money, so people betting a ton of money won't lose their money to those who bet few.
|
||||
total_amount_bet -= text2num(winner[2])
|
||||
for(var/list/winner in winners)
|
||||
var/datum/bank_account/winner_account = winner[1]
|
||||
var/money_won = text2num(winner[2]) + total_amount_bet / length(winners)
|
||||
winner_account.adjust_money(money_won, "Won gamble: [name]") //give them their money back & whatever they won.
|
||||
//they only made their money back, don't tell them they won anything.
|
||||
if((money_won - text2num(winner[2])) == 0)
|
||||
continue
|
||||
winner_account.bank_card_talk("You won [money_won]cr from having a correct guess on [name]!")
|
||||
|
||||
///Puts a bank account's money bet on a given option.
|
||||
/datum/active_bet/proc/bet_money(datum/bank_account/better, money_betting, option_betting)
|
||||
if(locked)
|
||||
return
|
||||
for(var/option in options)
|
||||
for(var/list/existing_bets in options[option])
|
||||
if(existing_bets[1] == better)
|
||||
//We're already betting, but now we're betting on another one, clear our previous and we'll bet on the new.
|
||||
if(option != option_betting)
|
||||
better.adjust_money(text2num(existing_bets[2]), "Refunded: changed bet for [name].")
|
||||
options[option] -= list(existing_bets)
|
||||
//We're already betting on the same one, we'll add it together instead of making it a separate bet, or the user is taking money out.
|
||||
else
|
||||
//putting more money in
|
||||
if(text2num(existing_bets[2]) < money_betting)
|
||||
if(better.account_balance < money_betting)
|
||||
return
|
||||
var/money_adding_in = money_betting - text2num(existing_bets[2])
|
||||
total_amount_bet += money_adding_in
|
||||
better.bank_card_talk("Additional [money_adding_in]cr deducted for your bet on [name].")
|
||||
better.adjust_money(-money_adding_in, "Gambling on [name].")
|
||||
existing_bets[2] = "[money_betting]"
|
||||
return
|
||||
//taking it all out, we remove them from the list so they aren't a winner with bets of 0.
|
||||
if(money_betting == 0)
|
||||
var/money_taking_out = text2num(existing_bets[2])
|
||||
total_amount_bet -= money_taking_out
|
||||
better.adjust_money(money_taking_out, "Refunded: changed bet for [name].")
|
||||
options[option] -= list(existing_bets)
|
||||
return
|
||||
//taking money out
|
||||
if(text2num(existing_bets[2]) > money_betting)
|
||||
var/money_taking_out = text2num(existing_bets[2]) - money_betting
|
||||
total_amount_bet -= money_taking_out
|
||||
better.bank_card_talk("Refunded [money_taking_out]cr for taking money out of your bet on [name].")
|
||||
better.adjust_money(money_taking_out, "Refund from gambling on [name].")
|
||||
existing_bets[2] = "[money_betting]"
|
||||
return
|
||||
|
||||
total_amount_bet += money_betting
|
||||
options[option_betting] += list(list(better, "[money_betting]"))
|
||||
better.adjust_money(-money_betting, "Gambling on [name]")
|
||||
better.bank_card_talk("Deducted [money_betting]cr for your bet on [name].")
|
||||
|
||||
///Cancels your bet, removing your bet and refunding your money.
|
||||
/datum/active_bet/proc/cancel_bet(datum/bank_account/better)
|
||||
for(var/option in options)
|
||||
for(var/list/existing_bets in options[option])
|
||||
if(existing_bets[1] == better)
|
||||
var/money_refunding = text2num(existing_bets[2])
|
||||
total_amount_bet -= money_refunding
|
||||
better.bank_card_talk("Refunded [money_refunding]cr for cancelling your bet on [name].")
|
||||
better.adjust_money(money_refunding, "Refunded: changed bet for [name].")
|
||||
options[option] -= list(existing_bets)
|
||||
|
||||
#undef MAX_LENGTH_TITLE
|
||||
#undef MAX_LENGTH_DESCRIPTION
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 25 KiB |
@@ -5505,6 +5505,7 @@
|
||||
#include "code\modules\modular_computers\file_system\programs\alarm.dm"
|
||||
#include "code\modules\modular_computers\file_system\programs\arcade.dm"
|
||||
#include "code\modules\modular_computers\file_system\programs\atmosscan.dm"
|
||||
#include "code\modules\modular_computers\file_system\programs\betting.dm"
|
||||
#include "code\modules\modular_computers\file_system\programs\borg_monitor.dm"
|
||||
#include "code\modules\modular_computers\file_system\programs\bounty_board.dm"
|
||||
#include "code\modules\modular_computers\file_system\programs\budgetordering.dm"
|
||||
|
||||
263
tgui/packages/tgui/interfaces/NtosSpaceBetting.tsx
Normal file
263
tgui/packages/tgui/interfaces/NtosSpaceBetting.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Collapsible,
|
||||
Divider,
|
||||
Icon,
|
||||
Input,
|
||||
NumberInput,
|
||||
Section,
|
||||
Stack,
|
||||
TextArea,
|
||||
} from 'tgui-core/components';
|
||||
import { BooleanLike } from 'tgui-core/react';
|
||||
|
||||
import { useBackend } from '../backend';
|
||||
import { NtosWindow } from '../layouts';
|
||||
|
||||
type Data = {
|
||||
active_bets: ActiveBets[];
|
||||
bank_name: string;
|
||||
bank_money: number;
|
||||
can_create_bet: BooleanLike;
|
||||
max_title_length: number;
|
||||
max_description_length: number;
|
||||
};
|
||||
|
||||
type ActiveBets = {
|
||||
name: string;
|
||||
description: string;
|
||||
owner: BooleanLike;
|
||||
creator: string;
|
||||
current_bets: CurrentBets[];
|
||||
locked: BooleanLike;
|
||||
};
|
||||
|
||||
type CurrentBets = {
|
||||
option_name: string;
|
||||
total_amount: number;
|
||||
personally_invested: number;
|
||||
};
|
||||
|
||||
export const NtosSpaceBetting = () => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
const { bank_name, bank_money, can_create_bet } = data;
|
||||
return (
|
||||
<NtosWindow width={500} height={620}>
|
||||
<NtosWindow.Content scrollable>
|
||||
<Section title="User Information">
|
||||
<Stack>
|
||||
<Stack.Item mr={1.5}>
|
||||
<Icon
|
||||
name="id-card"
|
||||
size={3}
|
||||
mr={1}
|
||||
color={bank_name ? 'green' : 'red'}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack fill vertical>
|
||||
<Stack.Item>Username: {bank_name}</Stack.Item>
|
||||
<Stack.Item>Money Available: {bank_money}cr</Stack.Item>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Section>
|
||||
<PollsSection />
|
||||
{!!can_create_bet && <BettingCreation />}
|
||||
</NtosWindow.Content>
|
||||
</NtosWindow>
|
||||
);
|
||||
};
|
||||
|
||||
export const PollsSection = () => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
const { active_bets = [] } = data;
|
||||
const [Winner, set_winner] = useState('');
|
||||
return (
|
||||
<Section>
|
||||
{!active_bets.length ? (
|
||||
<Box>
|
||||
There's currently no active polls to bet on, create one below!
|
||||
</Box>
|
||||
) : (
|
||||
active_bets.map(
|
||||
(
|
||||
{ name, description, owner, creator, current_bets = [], locked },
|
||||
index,
|
||||
) => (
|
||||
<Section title={name + ' - Created by ' + creator} key={name}>
|
||||
<Stack>
|
||||
<Stack.Item grow>
|
||||
<Stack.Item grow>{description}</Stack.Item>
|
||||
<Divider />
|
||||
{current_bets.map(
|
||||
(
|
||||
{ option_name, total_amount, personally_invested },
|
||||
index,
|
||||
) => (
|
||||
<Stack.Item
|
||||
grow
|
||||
key={option_name}
|
||||
className="candystripe"
|
||||
my={1.5}
|
||||
>
|
||||
<Stack.Item>
|
||||
<Stack.Item my={1}>
|
||||
{option_name} (Has {total_amount || 0}cr bet on it)
|
||||
{!owner ? (
|
||||
<NumberInput
|
||||
value={personally_invested}
|
||||
unit="cr"
|
||||
width="15px"
|
||||
disabled={locked}
|
||||
minValue={0}
|
||||
maxValue={10000}
|
||||
step={1}
|
||||
onChange={(value) =>
|
||||
act('place_bet', {
|
||||
bet_selected: name,
|
||||
option_selected: option_name,
|
||||
money_betting: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Button.Checkbox
|
||||
tooltip="Whether this answer won."
|
||||
checked={Winner === option_name}
|
||||
key={option_name}
|
||||
onClick={() => set_winner(option_name)}
|
||||
/>
|
||||
)}
|
||||
</Stack.Item>
|
||||
</Stack.Item>
|
||||
</Stack.Item>
|
||||
),
|
||||
)}
|
||||
{!!owner &&
|
||||
(!locked ? (
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
fluid
|
||||
icon="minus"
|
||||
tooltip="Lock the ability to place/retract bets. This is irreversible!"
|
||||
onClick={() =>
|
||||
act('lock_betting', { bet_selected: name })
|
||||
}
|
||||
>
|
||||
Lock Betting
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
) : (
|
||||
<Button.Confirm
|
||||
fluid
|
||||
icon="plus"
|
||||
tooltip="Finalize results as the checked answer being the winner."
|
||||
onClick={() =>
|
||||
act('select_winner', {
|
||||
bet_selected: name,
|
||||
winning_answer: Winner,
|
||||
})
|
||||
}
|
||||
>
|
||||
Finalize Results
|
||||
</Button.Confirm>
|
||||
))}
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
fluid
|
||||
icon="minus"
|
||||
disabled={locked}
|
||||
tooltip="If you have any bets, this will remove them and refund the money."
|
||||
onClick={() => act('cancel_bet', { bet_selected: name })}
|
||||
>
|
||||
Cancel Bet
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export const BettingCreation = () => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
const { max_title_length, max_description_length } = data;
|
||||
const [Title, setTitle] = useState('');
|
||||
const [Desc, setDesc] = useState('');
|
||||
const [Option1, setOption1] = useState('');
|
||||
const [Option2, setOption2] = useState('');
|
||||
const [Option3, setOption3] = useState('');
|
||||
const [Option4, setOption4] = useState('');
|
||||
return (
|
||||
<Collapsible title="Bet Creation">
|
||||
<Stack fill vertical>
|
||||
<Stack.Item grow>
|
||||
<Input
|
||||
fluid
|
||||
placeholder="Title"
|
||||
maxLength={max_title_length}
|
||||
onInput={(event, value) => setTitle(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<TextArea
|
||||
fluid
|
||||
placeholder="Description"
|
||||
height="100px"
|
||||
width="100%"
|
||||
maxLength={max_description_length}
|
||||
backgroundColor="black"
|
||||
textColor="white"
|
||||
onChange={(event, value) => setDesc(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Input
|
||||
fluid
|
||||
placeholder="Option 1"
|
||||
maxLength={max_title_length}
|
||||
onInput={(event, value) => setOption1(value)}
|
||||
/>
|
||||
<Input
|
||||
fluid
|
||||
placeholder="Option 2"
|
||||
maxLength={max_title_length}
|
||||
onInput={(event, value) => setOption2(value)}
|
||||
/>
|
||||
<Input
|
||||
fluid
|
||||
placeholder="Option 3 (Optional)"
|
||||
maxLength={max_title_length}
|
||||
onInput={(event, value) => setOption3(value)}
|
||||
/>
|
||||
<Input
|
||||
fluid
|
||||
placeholder="Option 4 (Optional)"
|
||||
maxLength={max_title_length}
|
||||
onInput={(event, value) => setOption4(value)}
|
||||
/>
|
||||
<Stack.Item grow>
|
||||
<Button
|
||||
fluid
|
||||
onClick={() =>
|
||||
act('create_bet', {
|
||||
title: Title,
|
||||
description: Desc,
|
||||
option1: Option1,
|
||||
option2: Option2,
|
||||
option3: Option3,
|
||||
option4: Option4,
|
||||
})
|
||||
}
|
||||
>
|
||||
Create Bet!
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user