diff --git a/code/datums/supplypacks.dm b/code/datums/supplypacks.dm index ecf6b04620a..c36bb358f60 100644 --- a/code/datums/supplypacks.dm +++ b/code/datums/supplypacks.dm @@ -2961,6 +2961,13 @@ GLOBAL_LIST_INIT(all_supply_groups, list(SUPPLY_EMERGENCY,SUPPLY_SECURITY,SUPPLY cost = 15 containername = "chinese supply crate" +/datum/supply_packs/vending/customat + name = "Customat Resupply Canister Crate" + contains = list(/obj/item/vending_refill/custom, + /obj/item/vending_refill/custom) + cost = 30 + containername = "customat canister supply crate" + ////////////////////////////////////////////////////////////////////////////// //////////////////////////// CONTRABAND SUPPLY /////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// diff --git a/code/game/machinery/constructable_frame.dm b/code/game/machinery/constructable_frame.dm index 6560c38de50..be03be6937e 100644 --- a/code/game/machinery/constructable_frame.dm +++ b/code/game/machinery/constructable_frame.dm @@ -360,7 +360,8 @@ to destroy them and players will be able to make replacements. "Departament Law ClothesMate" = /obj/machinery/vending/clothing/departament/law, "Service Departament ClothesMate Botanical" = /obj/machinery/vending/clothing/departament/service/botanical, "Service Departament ClothesMate Chaplain" = /obj/machinery/vending/clothing/departament/service/chaplain, - "RoboFriends" = /obj/machinery/vending/pai,) + "RoboFriends" = /obj/machinery/vending/pai, + "Customat" = /obj/machinery/customat,) var/static/list/unique_vendors = list( "ShadyCigs Ultra" = /obj/machinery/vending/cigarette/beach, diff --git a/code/game/machinery/customat.dm b/code/game/machinery/customat.dm new file mode 100644 index 00000000000..db6013927da --- /dev/null +++ b/code/game/machinery/customat.dm @@ -0,0 +1,753 @@ +// customat flick sequence bitflags +/// Machine is not using vending/denying overlays +#define FLICK_NONE 0 +/// Machine is currently vending wares, and will not update its icon, unless its stat change. +#define FLICK_VEND 1 +/// Machine is currently denying wares, and will not update its icon, unless its stat change. +#define FLICK_DENY 2 + + + +/** + * Datum used to hold information about a product in a vending machine + */ +/datum/data/customat_product + name = "generic" + ///How many of this product we currently have + var/amount = 0 + ///The key by which the object is pushed into the machine's row + var/key = "generic_0" + ///List of items in row + var/list/obj/item/containtment = list() + /// Price to buy one + var/price = 0 + ///Icon in tgui + var/icon = "" + +/datum/data/customat_product/New(obj/item/I) + name = I.name + amount = 0 + containtment = list() + price = 0 + icon = icon2base64(icon(initial(I.icon), initial(I.icon_state), SOUTH, 1, FALSE)) + + +/obj/machinery/customat + name = "\improper Customat" + desc = "Торговый автомат с кастомным содержимым." + icon = 'icons/obj/machines/customat.dmi' + icon_state = "custommate-off" + layer = BELOW_OBJ_LAYER + anchored = TRUE + density = TRUE + max_integrity = 600 // base vending integrity * 2 + armor = list(melee = 20, bullet = 0, laser = 0, energy = 0, bomb = 0, bio = 0, rad = 0, fire = 50, acid = 70) // base vending protection + resistance_flags = FIRE_PROOF + + // All the overlay controlling variables + /// Overlay of customat maintenance panel. + var/panel_overlay = "custommate-panel" + /// Overlay of a customat screen, will not apply of stat is NOPOWER. + var/screen_overlay = "custommate" + /// Lightmask used when customat is working properly. + var/lightmask_overlay = "" + /// Damage overlay applied if customat is damaged enough. + var/broken_overlay = "custommate-broken" + /// Special lightmask for broken overlay. If customat is BROKEN, but not dePOWERED we will see this, instead of `lightmask_overlay`. + var/broken_lightmask_overlay = "" + /// Overlay applied when machine is vending goods. + var/vend_overlay = "" + /// Special lightmask that will override default `lightmask_overlay`, while machine is vending goods. + var/vend_lightmask = "" + /// Amount of time until vending sequence is reseted. + var/vend_overlay_time = 5 SECONDS + /// Overlay applied when machine is denying its wares. + var/deny_overlay = "" + /// Special lightmask that will override default `lightmask_overlay`, while machine is denying its wares. + var/deny_lightmask = "" + /// Amount of time until denying sequence is reseted. + var/deny_overlay_time = 1.5 SECONDS + /// Flags used to correctly manipulate with vend/deny sequences. + var/flick_sequence = FLICK_NONE + + // Power + use_power = IDLE_POWER_USE + idle_power_usage = 10 + /// Power used for one vend + var/vend_power_usage = 150 + + // Vending-related + /// No sales pitches if off + var/active = TRUE + /// If off, customat is busy and unusable until current action finishes + var/vend_ready = TRUE + /// How long customat takes to vend one item. + var/vend_delay = 1 SECONDS + /// Item currently being bought + var/datum/data/customat_product/currently_vending = null + + + // Stuff relating vocalizations + /// List of slogans the customat will say, optional + var/list/ads_list = list("Купи самый дорогой предмет из моего содержимого! Не пожалеешь!", + "Мое содержимое разнообразней чем вся твоя жизнь!", + "У меня богатый внутренний мир.", + "Во мне может быть что угодно.", + "Не ядерный ли это диск во мне продается, всего за 1984 кредита?", + "Не хочешь платить за содержимое? Сломай меня и получи все бесплатно!", + "Товары на любой вкус и цвет!", + "Может во мне продается контробанда?", + "Не нравится мое содержимое? Создай свой кастомат, со своим уникальным содержимым!", + "Каждый раз, когда вы что-то покупаете, где-то в мире радуется один ассистент!") + + /// List of replies the customat will say after vends + var/list/vend_reply = list("Спасибо за покупку, приходите еще!", + "Вы купили что-то, а разнообразие моего содержимого не уменьшилось!", + "Ваши кредиты пойдут на разработку новых уникальных товаров!", + "Спасибо что выбрали нас!", + "А ведь мог сломать и не платить...") + + /// If true, prevent saying sales pitches + var/shut_up = FALSE + var/last_reply = 0 + var/reply_delay = 20 SECONDS + COOLDOWN_DECLARE(reply_cooldown) + var/last_slogan = 0 //When did we last pitch? + var/slogan_delay = 600 SECONDS //How long until we can pitch again? + COOLDOWN_DECLARE(slogan_cooldown) + var/alarm_delay = 10 SECONDS + COOLDOWN_DECLARE(alarm_cooldown) + + ///The type of refill canisters used by this machine. + var/obj/item/vending_refill/custom/canister = null + /// Type of canister used to build it + var/obj/item/vending_refill/refill_canister = /obj/item/vending_refill/custom // we need it for req_components of vendomat circuitboard + + // Things that can go wrong + /// Makes all prices 0 + emagged = 0 + + /// blocks further flickering while true + var/flickering = FALSE + /// do I look unpowered, even when powered? + var/force_no_power_icon_state = FALSE + + var/light_range_on = 1 + var/light_power_on = 0.5 + + /// Last costs of inserted types of items + var/list/remembered_costs = list("akula plushie" = 666) // Why not? + /// ID that was used to block customat + var/obj/item/card/id/connected_id = null // Id that was used to block src + // If true, price will be equal last prict of the same item + var/fast_insert = TRUE // If true, new price of inserted item will be equal previous price of the same item + + /// Map of {key; customat_product} + var/list/products = list() + + var/inserted_items_count = 0 + var/max_items_inside = 60 + + COOLDOWN_DECLARE(emp_cooldown) + var/weak_emp_cooldown = 60 SECONDS + var/strong_emp_cooldown = 180 SECONDS + + /// Direct ref to the trunk pipe underneath us + var/obj/structure/disposalpipe/trunk/trunk + +/obj/machinery/customat/proc/set_up_components() + component_parts = list() + var/obj/item/circuitboard/vendor/V = new + V.set_type(replacetext(initial(name), "\improper", "")) + component_parts += V + canister = new /obj/item/vending_refill/custom + component_parts += canister + +/obj/machinery/customat/RefreshParts() + . = ..() + for(var/obj/item/vending_refill/custom/VR in component_parts) + canister.linked_accounts = VR.linked_accounts.Copy() + canister.accounts_weights = VR.accounts_weights.Copy() + canister.sum_of_weigths = VR.sum_of_weigths + +/obj/machinery/customat/Initialize(mapload) + . = ..() + set_up_components() + RefreshParts() + update_icon(UPDATE_OVERLAYS) + +/obj/machinery/customat/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum) + . = ..(AM, skipcatch, hitpush, blocked, throwingdatum) + if (!AM.throwforce) + return + + if(COOLDOWN_FINISHED(src, emp_cooldown) && COOLDOWN_FINISHED(src, alarm_cooldown)) + playsound(src, 'sound/machines/burglar_alarm.ogg', AM.throwforce * 5, 0) + COOLDOWN_START(src, alarm_cooldown, alarm_delay) + return ..() + +/obj/machinery/customat/bullet_act(obj/item/projectile/P, def_zone) + . = ..(P, def_zone) + + if(COOLDOWN_FINISHED(src, emp_cooldown) && COOLDOWN_FINISHED(src, alarm_cooldown)) + playsound(src, 'sound/machines/burglar_alarm.ogg', P.damage * 5, 0) + COOLDOWN_START(src, alarm_cooldown, alarm_delay) + return ..() + +/obj/machinery/customat/proc/eject_all() + for (var/key in products) + var/datum/data/customat_product/product = products[key] + for (var/obj/item/I in product.containtment) + I.forceMove(get_turf(src)) + product.amount = 0 + inserted_items_count -= product.containtment.len + product.containtment = list() + +/obj/machinery/customat/Destroy() + eject_all() + if (trunk) + var/obj/structure/disposalholder/holder = locate() in trunk + if(holder) + trunk.expel(holder) + trunk.linked = null + trunk = null + return ..() + +/obj/machinery/customat/LateInitialize() + . = ..() + set_up_components() + RefreshParts() + update_icon(UPDATE_OVERLAYS) + var/obj/structure/disposalpipe/trunk/found_trunk = locate() in loc + if(found_trunk) + found_trunk.set_linked(src) + trunk = found_trunk + +/obj/machinery/customat/update_icon(updates = ALL) + return ..() + + +/obj/machinery/customat/update_overlays() + . = ..() + + underlays.Cut() + + if((stat & NOPOWER) || force_no_power_icon_state || !COOLDOWN_FINISHED(src, emp_cooldown)) + if(broken_overlay && (stat & BROKEN)) + . += broken_overlay + + if(panel_overlay && panel_open) + . += panel_overlay + return + + if(stat & BROKEN) + if(broken_overlay) + . += broken_overlay + if(broken_lightmask_overlay) + underlays += emissive_appearance(icon, broken_lightmask_overlay, src) + if(panel_overlay && panel_open) + . += panel_overlay + return + + if(screen_overlay) + . += screen_overlay + + var/lightmask_used = FALSE + if(vend_overlay && (flick_sequence & FLICK_VEND)) + . += vend_overlay + if(vend_lightmask) + lightmask_used = TRUE + . += vend_lightmask + + else if(deny_overlay && (flick_sequence & FLICK_DENY)) + . += deny_overlay + if(deny_lightmask) + lightmask_used = TRUE + . += deny_lightmask + + if(!lightmask_used && lightmask_overlay) + underlays += emissive_appearance(icon, lightmask_overlay, src) + + if(panel_overlay && panel_open) + . += panel_overlay + + +/obj/machinery/customat/power_change(forced = FALSE) + . = ..() + if(stat & NOPOWER) + set_light_on(FALSE) + else + set_light(light_range_on, light_power_on, l_on = TRUE) + if(.) + update_icon(UPDATE_OVERLAYS) + + +/obj/machinery/customat/extinguish_light(force = FALSE) + if(light_on) + set_light_on(FALSE) + underlays.Cut() + + +/obj/machinery/customat/proc/flick_vendor_overlay(flick_flag = FLICK_NONE) + if(flick_sequence & (FLICK_VEND|FLICK_DENY)) + return + if((flick_flag & FLICK_VEND) && !vend_overlay) + return + if((flick_flag & FLICK_DENY) && !deny_overlay) + return + flick_sequence = flick_flag + update_icon(UPDATE_OVERLAYS) + var/flick_time = (flick_flag & FLICK_VEND) ? vend_overlay_time : (flick_flag & FLICK_DENY) ? deny_overlay_time : 0 + addtimer(CALLBACK(src, PROC_REF(flick_reset)), flick_time) + + +/obj/machinery/customat/proc/flick_reset() + flick_sequence = FLICK_NONE + update_icon(UPDATE_OVERLAYS) + + +/* + * Reimp, flash the screen on and off repeatedly. + */ +/obj/machinery/customat/flicker() + if(flickering) + return FALSE + + if((stat & (BROKEN|NOPOWER)) || !COOLDOWN_FINISHED(src, emp_cooldown)) + return FALSE + + flickering = TRUE + INVOKE_ASYNC(src, TYPE_PROC_REF(/obj/machinery/customat, flicker_event)) + + return TRUE + +/* + * Proc to be called by invoke_async in the above flicker() proc. + */ +/obj/machinery/customat/proc/flicker_event() + var/amount = rand(5, 15) + + for(var/i in 1 to amount) + force_no_power_icon_state = TRUE + update_icon(UPDATE_OVERLAYS) + sleep(rand(1, 3)) + + force_no_power_icon_state = FALSE + update_icon(UPDATE_OVERLAYS) + sleep(rand(1, 10)) + update_icon(UPDATE_OVERLAYS) + flickering = FALSE + +/obj/machinery/customat/deconstruct(disassembled = TRUE) + if(!canister) //the non constructable customats drop metal instead of a machine frame. + new /obj/item/stack/sheet/metal(loc, 3) + qdel(src) + else + ..() + +/obj/machinery/customat/proc/idcard_act(mob/user, obj/item/I) + if (!isLocked()) + connected_id = I + balloon_alert(user, "заблокировано") + else if (connected_id == I) + connected_id = null + balloon_alert(user, "разблокировано") + else + balloon_alert(user, "карта не подходит") + +/obj/machinery/customat/proc/get_key(obj/item/I, cost) + return I.name + "_[cost]" + +/obj/machinery/customat/proc/insert(mob/user, obj/item/I, cost) + if (inserted_items_count >= max_items_inside) + if (user) + to_chat(user, span_warning("Лимит в [max_items_inside] предметов достигнут.")) + return + remembered_costs[I.name] = cost + var/key = get_key(I, cost) + if(user && !user.drop_transfer_item_to_loc(I, src)) + to_chat(user, span_warning("Вы не можете положить это внутрь.")) + return + + if (!user) // If from pipe, transfer into src. + I.forceMove(src) + + var/datum/data/customat_product/product + if (!(key in products)) + product = new /datum/data/customat_product(I) + product.price = !emagged ? cost : 0 + product.key = key + products[key] = product + + product = products[key] + product.containtment += I + product.amount++ + inserted_items_count++ + +/obj/machinery/customat/proc/try_insert(mob/user, obj/item/I, from_tube = FALSE) + var/cost = 100 + if (from_tube) + if (I.name in remembered_costs) + cost = remembered_costs[I.name] + else if (fast_insert && (I.name in remembered_costs)) + cost = remembered_costs[I.name] + else + var/input_cost = tgui_input_number(user, "Пожалуйста, выберите цену для этого товара. Цена не может быть ниже 0 и выше 1000000 кредитов.", "Выбор цены", 0, 1000000, 0) + if (!input_cost) + to_chat(user, span_warning("Цена не указанна!")) + return + cost = input_cost + if (user && get_dist(get_turf(user), get_turf(src)) > 1) + to_chat(user, span_warning("Вы слишком далеко!")) + return + insert(user, I, cost) + +/obj/machinery/customat/attackby(obj/item/I, mob/user, params) + if(user.a_intent == INTENT_HARM && COOLDOWN_FINISHED(src, emp_cooldown) && COOLDOWN_FINISHED(src, alarm_cooldown)) + playsound(src, 'sound/machines/burglar_alarm.ogg', I.force * 5, 0) + COOLDOWN_START(src, alarm_cooldown, alarm_delay) + return ..() + + if(istype(I, /obj/item/crowbar) || istype(I, /obj/item/wrench)) + return ATTACK_CHAIN_PROCEED_SUCCESS + + if (panel_open) + if (istype(I, /obj/item/card/id)) + idcard_act(user, I) + return ATTACK_CHAIN_BLOCKED_ALL + else if (!isLocked()) + try_insert(user, I) + return ATTACK_CHAIN_BLOCKED_ALL + + if (!istype(I, /obj/item/stack/nanopaste) && !istype(I, /obj/item/detective_scanner) && COOLDOWN_FINISHED(src, emp_cooldown) && COOLDOWN_FINISHED(src, alarm_cooldown)) + COOLDOWN_START(src, alarm_cooldown, alarm_delay) + playsound(src, 'sound/machines/burglar_alarm.ogg', I.force * 5, 0) + + return ..() + + +/obj/machinery/customat/crowbar_act(mob/user, obj/item/I) + if(!component_parts) + return + if (isLocked()) + to_chat(user, span_warning("[src] is locked.")) + return + . = TRUE + eject_all() + default_deconstruction_crowbar(user, I) + +/obj/machinery/customat/screwdriver_act(mob/user, obj/item/I) + . = TRUE + if(!I.use_tool(src, user, 0, volume = I.tool_volume)) + return + if(anchored) + panel_open = !panel_open + panel_open ? SCREWDRIVER_OPEN_PANEL_MESSAGE : SCREWDRIVER_CLOSE_PANEL_MESSAGE + update_icon() + SStgui.update_uis(src) + +/obj/machinery/customat/wrench_act(mob/user, obj/item/I) + . = TRUE + if(!I.use_tool(src, user, 0, volume = 0)) + return + default_unfasten_wrench(user, I, time = 60) + if (anchored) + trunk_check() + +/obj/machinery/customat/exchange_parts(mob/user, obj/item/storage/part_replacer/W) + if(!istype(W)) + return FALSE + if(!W.works_from_distance) + return FALSE + if(!component_parts || !canister) + return FALSE + + var/moved = 0 + if(panel_open || W.works_from_distance) + if(W.works_from_distance) + to_chat(user, display_parts(user)) + else + to_chat(user, display_parts(user)) + if(moved) + to_chat(user, "[moved] items restocked.") + W.play_rped_sound() + return TRUE + +/obj/machinery/customat/emag_act(mob/user) + emagged = TRUE + for (var/key in products) + var/datum/data/customat_product/product = products[key] + product.price = 0 + products[key] = product + if(user) + to_chat(user, "You short out the product lock on [src]") + +/obj/machinery/customat/attack_ai(mob/user) + return attack_hand(user) + +/obj/machinery/customat/attack_ghost(mob/user) + return attack_hand(user) + +/obj/machinery/customat/attack_hand(mob/user) + if((stat & (BROKEN|NOPOWER)) || !COOLDOWN_FINISHED(src, emp_cooldown)) + return + + if(..()) + return TRUE + + add_fingerprint(user) + ui_interact(user) + +/obj/machinery/customat/ui_interact(mob/user, datum/tgui/ui = null) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Customat", name) + ui.open() + +/obj/machinery/customat/ui_data(mob/user) + var/list/data = list() + var/datum/money_account/account = null + data["guestNotice"] = "Идентификационной карты не обнаружено."; + data["userMoney"] = 0 + data["user"] = null + if(issilicon(user) && !istype(user, /mob/living/silicon/robot/drone) && !istype(user, /mob/living/silicon/pai)) + account = get_card_account(user) + data["user"] = list() + data["user"]["name"] = account.owner_name + data["userMoney"] = account.money + data["user"]["job"] = "Silicon" + if(ishuman(user)) + account = get_card_account(user) + var/mob/living/carbon/human/H = user + var/obj/item/stack/spacecash/S = H.get_active_hand() + if(istype(S)) + data["userMoney"] = S.amount + data["guestNotice"] = "Accepting Cash. You have: [S.amount] credits." + else if(istype(H)) + var/obj/item/card/id/idcard = H.get_id_card() + if(istype(account)) + data["user"] = list() + data["user"]["name"] = account.owner_name + data["userMoney"] = account.money + data["user"]["job"] = (istype(idcard) && idcard.rank) ? idcard.rank : "No Job" + else + data["guestNotice"] = "Unlinked ID detected. Present cash to pay."; + data["products"] = list() + for (var/key in products) + var/datum/data/customat_product/product = products[key] + var/list/data_pr = list( + name = product.name, + price = product.price, + stock = product.amount, + icon = product.icon, + Key = product.key + ) + data["products"] += list(data_pr) + data["vend_ready"] = vend_ready + data["panel_open"] = panel_open ? TRUE : FALSE + data["speaker"] = shut_up ? FALSE : TRUE + return data + + +/obj/machinery/customat/ui_static_data(mob/user) + var/list/data = list() + return data + +/obj/machinery/customat/ui_act(action, params) + . = ..() + if(.) + return + if(issilicon(usr) && !isrobot(usr)) + to_chat(usr, span_warning("The vending machine refuses to interface with you, as you are not in its target demographic!")) + return + switch(action) + if("toggle_voice") + if(panel_open) + shut_up = !shut_up + . = TRUE + if("vend") + if(!vend_ready) + to_chat(usr, span_warning("The vending machine is busy!")) + return + if(panel_open) + to_chat(usr, span_warning("The vending machine cannot dispense products while its service panel is open!")) + return + var/key = params["Key"] + var/datum/data/customat_product/product = products[key] + if (product.amount <= 0) + to_chat(usr, "Sold out of [product.name].") + flick_vendor_overlay(FLICK_VEND) + return + + vend_ready = FALSE // From this point onwards, customat is locked to performing this transaction only, until it is resolved. + + if(!(ishuman(usr) || issilicon(usr)) || product.price <= 0) + // Either the purchaser is not human nor silicon, or the item is free. + // Skip all payment logic. + vend(product, usr) + add_fingerprint(usr) + vend_ready = TRUE + . = TRUE + return + + // --- THE REST OF THIS PROC IS JUST PAYMENT LOGIC --- + if(!GLOB.vendor_account || GLOB.vendor_account.suspended) + to_chat(usr, "Vendor account offline. Unable to process transaction.") + flick_vendor_overlay(FLICK_DENY) + vend_ready = TRUE + return + + currently_vending = product + var/paid = FALSE + + if(istype(usr.get_active_hand(), /obj/item/stack/spacecash)) + var/obj/item/stack/spacecash/S = usr.get_active_hand() + paid = FALSE + var/left = currently_vending.price + for (var/ind = 1; ind <= canister.linked_accounts.len; ++ind) + var/pay_now = round(currently_vending.price * canister.accounts_weights[ind] / canister.sum_of_weigths) + pay_now = min(pay_now, left) + left -= pay_now + paid = pay_with_cash(S, usr, pay_now, currently_vending.name, canister.linked_accounts[ind]) || paid + else if(get_card_account(usr)) + var/datum/money_account/customer_account = get_card_account(usr) + paid = FALSE + var/left = currently_vending.price + for (var/ind = 1; ind <= canister.linked_accounts.len; ++ind) + var/pay_now = round(currently_vending.price * canister.accounts_weights[ind] / canister.sum_of_weigths) + pay_now = min(pay_now, left) + left -= pay_now + paid = customer_account.charge(pay_now, canister.linked_accounts[ind], + "Purchase of [product.name]", name, canister.linked_accounts[ind].owner_name, + "Sale of [product.name]", customer_account.owner_name) || paid + + else if(usr.can_advanced_admin_interact()) + to_chat(usr, span_notice("Vending object due to admin interaction.")) + paid = TRUE + else + to_chat(usr, span_warning("Payment failure: you have no ID or other method of payment.")) + vend_ready = TRUE + flick_vendor_overlay(FLICK_DENY) + . = TRUE // we set this because they shouldn't even be able to get this far, and we want the UI to update. + return + if(paid) + vend(currently_vending, usr) + . = TRUE + else + to_chat(usr, span_warning("Payment failure: unable to process payment.")) + vend_ready = TRUE + if(.) + add_fingerprint(usr) + +/obj/machinery/customat/proc/isLocked() + return connected_id != null + +/obj/machinery/customat/proc/vend(datum/data/customat_product/product, mob/user) + if(!product.amount) + to_chat(user, span_warning("В автомате не осталось содержимого.")) + vend_ready = TRUE + return + + vend_ready = FALSE //One thing at a time!! + + product.amount-- + + if(COOLDOWN_FINISHED(src, reply_cooldown) && vend_reply) + speak(pick(src.vend_reply)) + COOLDOWN_START(src, reply_cooldown, reply_delay) + + use_power(vend_power_usage) //actuators and stuff + flick_vendor_overlay(FLICK_VEND) //Show the vending animation if needed + playsound(get_turf(src), 'sound/machines/machine_vend.ogg', 50, TRUE) + addtimer(CALLBACK(src, PROC_REF(delayed_vend), product, user), vend_delay) + + +/obj/machinery/customat/proc/delayed_vend(datum/data/customat_product/product, mob/user) + do_vend(product, user) + vend_ready = TRUE + currently_vending = null + + +/** + * Override this proc to add handling for what to do with the vended product + * when you have a inserted item and remember to include a parent call for this generic handling + */ +/obj/machinery/customat/proc/do_vend(datum/data/customat_product/product, mob/user) + var/put_on_turf = TRUE + var/obj/item/vended = product.containtment[1] + if(istype(vended) && user && iscarbon(user) && user.Adjacent(src)) + if(user.put_in_hands(vended, ignore_anim = FALSE)) + put_on_turf = FALSE + if(put_on_turf) + var/turf/T = get_turf(src) + vended.forceMove(T) + product.containtment.Remove(product.containtment[1]) + inserted_items_count-- + return TRUE + +/obj/machinery/customat/process() + if((stat & (BROKEN|NOPOWER)) || !COOLDOWN_FINISHED(src, emp_cooldown)) + return + + if(!active) + return + + //Pitch to the people! Really sell it! + if(COOLDOWN_FINISHED(src, slogan_cooldown) && (LAZYLEN(ads_list)) && (!shut_up) && prob(5)) + var/slogan = pick(src.ads_list) + speak(slogan) + COOLDOWN_START(src, slogan_cooldown, slogan_delay) + + +/obj/machinery/customat/proc/speak(message) + if(stat & NOPOWER) + return + if(!message) + return + + atom_say(message) + +/obj/machinery/customat/obj_break(damage_flag) + if(stat & BROKEN) + return + + stat |= BROKEN + update_icon(UPDATE_OVERLAYS) + +/obj/machinery/customat/AltClick(atom/movable/A) + if (!panel_open) + balloon_alert(A, "панель закрыта") + return + if (isLocked()) + balloon_alert(A, "автомат заблокирован") + return + + balloon_alert(A, "быстрый режим " + (fast_insert ? "отключен" : "включен")) + fast_insert = !fast_insert + +/obj/machinery/customat/emp_act(severity) + switch(severity) + if(1) + COOLDOWN_START(src, emp_cooldown, weak_emp_cooldown) + if(2) + COOLDOWN_START(src, emp_cooldown, strong_emp_cooldown) + +/obj/machinery/customat/proc/expel(obj/structure/disposalholder/holder) + var/turf/origin_turf = get_turf(src) + var/list/contents = holder.contents + for (var/atom/movable/content in contents) + if (istype(content, /obj/item)) + try_insert(null, content, TRUE) + else + content.forceMove(origin_turf) + qdel(holder) + +/obj/machinery/customat/proc/trunk_check() + var/obj/structure/disposalpipe/trunk/found_trunk = locate() in loc + if(found_trunk) + found_trunk.set_linked(src) // link the pipe trunk to self + trunk = found_trunk + +#undef FLICK_NONE +#undef FLICK_VEND +#undef FLICK_DENY diff --git a/code/game/objects/items/weapons/vending_items.dm b/code/game/objects/items/weapons/vending_items.dm index 174a998c3a7..279ed6c1b9a 100644 --- a/code/game/objects/items/weapons/vending_items.dm +++ b/code/game/objects/items/weapons/vending_items.dm @@ -187,3 +187,122 @@ /obj/item/vending_refill/pai machine_name = "RoboFriends" icon_state = "restock_pai" + +/obj/item/vending_refill/custom + machine_name = "Customat" + icon = 'icons/obj/machines/customat.dmi' + icon_state = "custommate-refill" + var/list/datum/money_account/linked_accounts = list() + var/list/datum/money_account/accounts_weights = list() + var/sum_of_weigths = 0 + +/obj/item/vending_refill/custom/Initialize() + linked_accounts = list(GLOB.station_account) + accounts_weights = list(100) + sum_of_weigths = 100 + . = ..() + + + +/obj/item/vending_refill/custom/proc/add_account(datum/money_account/new_account, weight) + linked_accounts += new_account + accounts_weights += weight + sum_of_weigths += weight + + +/obj/item/vending_refill/custom/proc/clear_accounts(mob/user) + linked_accounts = list() + accounts_weights = list() + sum_of_weigths = 0 + balloon_alert(user, "счета отвязаны") + + +/obj/item/vending_refill/custom/proc/try_add_account(mob/user) + . = FALSE + if (linked_accounts.len >= 150) // better to do it + balloon_alert(user, "лимит привязки достигнут") + return + + var/new_acc_number = tgui_input_number(user, "Пожалуйста, введите номер счета, который вы хотите привязать.", "Выбор счета", (user.mind && user.mind.initial_account) ? user.mind.initial_account.account_number : 999999, 999999, 0, ui_state = GLOB.hands_state, ui_source = src) + + if (isnull(new_acc_number)) + balloon_alert(user, "номер не введен") + return + + var/new_account = attempt_account_access(new_acc_number, pin_needed = FALSE, security_level_passed = 3, pin_needed = FALSE) + if (!new_account) + balloon_alert(user, "аккаунт не существует") + return + + if (new_account in linked_accounts) + balloon_alert(user, "аккаунт уже привязан") + return + + var/weight = tgui_input_number(user, "Пожалуйста, введите вес счета от 1 до 1000000.", "Выбор веса", 100, 1000000, 1, ui_state = GLOB.hands_state, ui_source = src) + + if (isnull(weight)) + balloon_alert(user, "вес не введен") + return + + add_account(new_account, weight) + balloon_alert(user, "новый счет добавлен") + return TRUE + + +/obj/item/vending_refill/custom/proc/try_add_station_account(mob/user) + . = FALSE + var/weight = tgui_input_number(user, "Пожалуйста, введите вес для счета станции от 1 до 1000000.", "Выбор веса", 100, 1000000, 1, ui_state = GLOB.hands_state, ui_source = src) + + if (isnull(weight)) + balloon_alert(user, "вес не введен") + return + + if (GLOB.station_account in linked_accounts) + balloon_alert(user, "аккаунт станции уже привязан") + return + + add_account(GLOB.station_account, weight) + balloon_alert(user, "счет станции привязан") + return TRUE + + +/obj/item/vending_refill/custom/attack_self(mob/user) // It works this way not because I'm lazy, but for better immersion. + var/operation = tgui_input_number(user, "Введите 0 чтобы сбросить список сохраненных счетов, 1 чтобы добавить новый счет в список получателей, 2 чтобы добавить счет станции.", "Настройка счетов", 0, 2, 0, ui_state = GLOB.hands_state, ui_source = src) + + if (isnull(operation)) + balloon_alert(user, "значение не введено") + playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 30, 1) + return + + + var/correct = TRUE + switch (operation) + if (0) + correct = clear_accounts(user) + if (1) + correct = try_add_account(user) + if (2) + correct = try_add_station_account(user) + if (-INFINITY to -1) + correct = FALSE + balloon_alert(user, "значение должно быть больше 0") + if (3 to INFINITY) + correct = FALSE + balloon_alert(user, "значение должно быть меньше 3") + + if (correct) + playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 30, 0) + else + playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 30, 1) + + +/obj/item/vending_refill/custom/examine(mob/user) + . = ..() + if(in_range(user, src)) + if (!linked_accounts.len) + . += span_notice("К этой канистре не привязанно ни одного счета.") + else + . += span_notice("К этой канистре привязанны следующее счета:") + for (var/i = 1; i <= linked_accounts.len; ++i) + . += span_notice("Владелец: " + linked_accounts[i].owner_name + ", вес: [accounts_weights[i]], доля: [round(accounts_weights[i]/sum_of_weigths, 0.01)].") + diff --git a/code/modules/economy/EFTPOS.dm b/code/modules/economy/EFTPOS.dm index 7e1366fe6da..3755d2be661 100644 --- a/code/modules/economy/EFTPOS.dm +++ b/code/modules/economy/EFTPOS.dm @@ -139,7 +139,7 @@ "[rand(0,99999)]",". = ..() RETURN FUCK_NT","IT'S OVER 9000!","three hundred bucks", "Nineteen Eighty-Four","alla money frum ur bank acc")) if(linked_account && linked_account.security_level == 0) - //если уровень защиты привязанного аккаунта нулевой, то глобально переписывает имя владельца + //if the security level of the linked account is zero, then globally rewrites the owner's name linked_account.owner_name = pick(list( "Taargüs Taargüs","n4n07r453n 7074lly 5ux","Maya Normousbutt","Al Coholic","Stu Piddiddiot", "Yuri Nator","HAI GUYZ! LEARN HA TO CHANGE SECURITY SETTINGS! LOL!!")) @@ -193,7 +193,7 @@ print_reference(user) if("link_account") if(duty_mode) - //запрещает редактировать это поле на служебном устройстве + //prevents editing of this field on the service device to_chat(user, "[bicon(src)] Feature not available on this device.") playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 30, 1) return @@ -227,17 +227,15 @@ return transaction_amount = try_num if("toggle_lock") - //вообще, это три разные кнопки, по-хорошему, их надо разбить на три события - //но для этого нужно eftpos.js редактировать, заодно и все input перевести на tgui if(transaction_locked && !transaction_paid) - //выход из режима оплаты c помощью карты или если код 0 (приоритетный выход) + //exit from card payment mode or if code 0 (priority exit) var/list/access = user.get_access() if((ACCESS_CENT_COMMANDER in access) || (ACCESS_CAPTAIN in access) || (ACCESS_HOP in access) || !access_code) transaction_locked = 0 transaction_paid = 0 playsound(src, 'sound/machines/terminal_prompt.ogg', 30, 0) return - //выход с проверкой кода доступа + //exit with access code verification var/attempt_code = tgui_input_number(user, "Enter EFTPOS access code", "Reset Transaction", max_value = 9999, min_value = 1000) if(!Adjacent(user)) return @@ -250,13 +248,13 @@ playsound(src, 'sound/machines/terminal_prompt.ogg', 30, 0) return if(transaction_locked && transaction_paid) - //завершение оплаты с печатью чека + //completion of payment with receipt printing transaction_locked = 0 transaction_paid = 0 print_check(user) return if(linked_account && !transaction_locked) - //переводит EFTPOS в режим оплаты, если введен аккаунт получателя + //switches EFTPOS to payment mode if the recipient account is entered transaction_locked = 1 playsound(src, 'sound/machines/terminal_prompt.ogg', 30, 0) else @@ -291,13 +289,13 @@ reconnect_database() if(linked_db) if(during_paid) - //такая проверка необходима для предотвращения множественных операций оплаты при закликивании + //This check is necessary to prevent multiple payment transactions when clicking to_chat(user, "[bicon(src)] End the current operation first.") playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 30, 1) return if(!transaction_locked || transaction_paid) - //прерывает процедуру, если EFTPOS не был переведен в режим оплаты или транзакция уже была оплачена + //aborts the procedure if EFTPOS has not been switched to payment mode or the transaction has already been paid return during_paid = TRUE diff --git a/code/modules/economy/utils.dm b/code/modules/economy/utils.dm index 28aa126ae1b..94c1cbca33f 100644 --- a/code/modules/economy/utils.dm +++ b/code/modules/economy/utils.dm @@ -24,7 +24,7 @@ return get_money_account(id.associated_account_number) return null -/obj/machinery/proc/pay_with_cash(obj/item/stack/spacecash/cashmoney, mob/user, price, vended_name) +/obj/machinery/proc/pay_with_cash(obj/item/stack/spacecash/cashmoney, mob/user, price, vended_name, datum/money_account/account_we_pay_on = GLOB.vendor_account) if(price > cashmoney.amount) // This is not a status display message, since it's something the character // themselves is meant to see BEFORE putting the money in @@ -42,10 +42,10 @@ visible_message("[user] inserts a credit chip into [src].") // Vending machines have no idea who paid with cash - GLOB.vendor_account.credit(price, "Sale of [vended_name]", name, "(cash)") + account_we_pay_on.credit(price, "Sale of [vended_name]", name, "(cash)") return TRUE -/obj/machinery/proc/pay_with_card(mob/M, price, vended_name) +/obj/machinery/proc/pay_with_card(mob/M, price, vended_name, datum/money_account/account_we_pay_on = GLOB.vendor_account) if(iscarbon(M)) visible_message("[M] swipes a card through [src].") var/datum/money_account/customer_account = get_card_account(M) @@ -67,8 +67,8 @@ to_chat(M, "Your bank account has insufficient money to purchase this.") return FALSE // Okay to move the money at this point - customer_account.charge(price, GLOB.vendor_account, - "Purchase of [vended_name]", name, GLOB.vendor_account.owner_name, + customer_account.charge(price, account_we_pay_on, + "Purchase of [vended_name]", name, account_we_pay_on.owner_name, "Sale of [vended_name]", customer_account.owner_name) if(customer_account.owner_name == GLOB.station_account.owner_name) add_game_logs("as silicon purchased [vended_name] in [COORD(src)]", M) diff --git a/code/modules/recycling/disposal/pipe.dm b/code/modules/recycling/disposal/pipe.dm index 745e0b63d68..0e12dfc9e8f 100644 --- a/code/modules/recycling/disposal/pipe.dm +++ b/code/modules/recycling/disposal/pipe.dm @@ -310,6 +310,9 @@ null_linked_refs() linked = null var/turf/our_turf = get_turf(src) + var/obj/machinery/customat/customat = locate() in our_turf + if(customat) + set_linked(customat) var/obj/machinery/disposal/disposal = locate() in our_turf if(disposal) set_linked(disposal) @@ -357,7 +360,11 @@ outlet.expel(holder) // expel at outlet else var/obj/machinery/disposal/disposal = linked - disposal.expel(holder) // expel at disposal + if(istype(disposal)) + disposal.expel(holder) // expel at disposal + else + var/obj/machinery/customat/customat = linked + customat.expel(holder) // expel at customat // Returning null without expelling holder makes the holder expell itself return null diff --git a/code/modules/research/designs/misc_designs.dm b/code/modules/research/designs/misc_designs.dm index e54786dd974..089cb6de111 100644 --- a/code/modules/research/designs/misc_designs.dm +++ b/code/modules/research/designs/misc_designs.dm @@ -141,3 +141,13 @@ reagents_list = list("firefighting_foam" = 1) build_path = /obj/item/extinguisher_refill category = list("Miscellaneous") + +/datum/design/customat_canister + name = "Customat Canister" + desc = "Канистра для Кастомата." + id = "customat_canister" + req_tech = list("programming" = 3) + build_type = PROTOLATHE + materials = list(MAT_METAL = 800, MAT_GLASS = 600) + build_path = /obj/item/vending_refill/custom + category = list("Miscellaneous") diff --git a/code/modules/tgui/states.dm b/code/modules/tgui/states.dm index cb79ab85189..6e35b29cb2e 100644 --- a/code/modules/tgui/states.dm +++ b/code/modules/tgui/states.dm @@ -34,9 +34,13 @@ . = max(., UI_UPDATE) // Check if the state allows interaction - var/result = state.can_use_topic(src_object, user) + var/result = state.can_use_topic(src_object, user, state.ui_source) . = max(., result) + +/datum/ui_state + var/atom/ui_source = null + /** * private * @@ -48,7 +52,7 @@ * * return UI_state The state of the UI. */ -/datum/ui_state/proc/can_use_topic(src_object, mob/user) +/datum/ui_state/proc/can_use_topic(src_object, mob/user, atom/ui_source) // Don't allow interaction by default. return UI_CLOSE diff --git a/code/modules/tgui/states/admin.dm b/code/modules/tgui/states/admin.dm index 61fc3731188..b9a8842ce1d 100644 --- a/code/modules/tgui/states/admin.dm +++ b/code/modules/tgui/states/admin.dm @@ -6,7 +6,7 @@ GLOBAL_DATUM_INIT(admin_state, /datum/ui_state/admin_state, new) -/datum/ui_state/admin_state/can_use_topic(src_object, mob/user) +/datum/ui_state/admin_state/can_use_topic(src_object, mob/user, atom/ui_source) if(check_rights_for(user.client, R_ADMIN)) return UI_INTERACTIVE return UI_CLOSE diff --git a/code/modules/tgui/states/always.dm b/code/modules/tgui/states/always.dm index 2406dbb2b9b..dba928cff3e 100644 --- a/code/modules/tgui/states/always.dm +++ b/code/modules/tgui/states/always.dm @@ -11,5 +11,5 @@ GLOBAL_DATUM_INIT(always_state, /datum/ui_state/always_state, new) -/datum/ui_state/always_state/can_use_topic(src_object, mob/user) +/datum/ui_state/always_state/can_use_topic(src_object, mob/user, atom/ui_source) return UI_INTERACTIVE diff --git a/code/modules/tgui/states/conscious.dm b/code/modules/tgui/states/conscious.dm index 8e35a97da32..e4123d5a3d2 100644 --- a/code/modules/tgui/states/conscious.dm +++ b/code/modules/tgui/states/conscious.dm @@ -11,7 +11,7 @@ GLOBAL_DATUM_INIT(conscious_state, /datum/ui_state/conscious_state, new) -/datum/ui_state/conscious_state/can_use_topic(src_object, mob/user) +/datum/ui_state/conscious_state/can_use_topic(src_object, mob/user, atom/ui_source) if(user.stat == CONSCIOUS) return UI_INTERACTIVE return UI_CLOSE diff --git a/code/modules/tgui/states/contained.dm b/code/modules/tgui/states/contained.dm index 98187b746e0..f209a0492af 100644 --- a/code/modules/tgui/states/contained.dm +++ b/code/modules/tgui/states/contained.dm @@ -11,7 +11,7 @@ GLOBAL_DATUM_INIT(contained_state, /datum/ui_state/contained_state, new) -/datum/ui_state/contained_state/can_use_topic(atom/src_object, mob/user) +/datum/ui_state/contained_state/can_use_topic(atom/src_object, mob/user, atom/ui_source) if(!src_object.contains(user)) return UI_CLOSE return user.shared_ui_interaction(src_object) diff --git a/code/modules/tgui/states/deep_inventory.dm b/code/modules/tgui/states/deep_inventory.dm index a7351a0d2d9..c5dec2a147f 100644 --- a/code/modules/tgui/states/deep_inventory.dm +++ b/code/modules/tgui/states/deep_inventory.dm @@ -12,7 +12,7 @@ GLOBAL_DATUM_INIT(deep_inventory_state, /datum/ui_state/deep_inventory_state, new) -/datum/ui_state/deep_inventory_state/can_use_topic(src_object, mob/user) +/datum/ui_state/deep_inventory_state/can_use_topic(src_object, mob/user, atom/ui_source) if(!user.contains(src_object)) return UI_CLOSE return user.shared_ui_interaction(src_object) diff --git a/code/modules/tgui/states/default.dm b/code/modules/tgui/states/default.dm index 9a3f1457e8f..3da0c549eb8 100644 --- a/code/modules/tgui/states/default.dm +++ b/code/modules/tgui/states/default.dm @@ -10,7 +10,7 @@ GLOBAL_DATUM_INIT(default_state, /datum/ui_state/default, new) -/datum/ui_state/default/can_use_topic(src_object, mob/user) +/datum/ui_state/default/can_use_topic(src_object, mob/user, atom/ui_source) return user.default_can_use_topic(src_object) // Call the individual mob-overridden procs. /mob/proc/default_can_use_topic(src_object) diff --git a/code/modules/tgui/states/hands.dm b/code/modules/tgui/states/hands.dm index 84ec33dc190..5696d4b6e20 100644 --- a/code/modules/tgui/states/hands.dm +++ b/code/modules/tgui/states/hands.dm @@ -11,27 +11,27 @@ GLOBAL_DATUM_INIT(hands_state, /datum/ui_state/hands_state, new) -/datum/ui_state/hands_state/can_use_topic(src_object, mob/user) +/datum/ui_state/hands_state/can_use_topic(src_object, mob/user, atom/ui_source) . = user.shared_ui_interaction(src_object) if(. > UI_CLOSE) - return min(., user.hands_can_use_topic(src_object)) + return min(., user.hands_can_use_topic(ui_source ? ui_source : src_object)) -/mob/proc/hands_can_use_topic(src_object) +/mob/proc/hands_can_use_topic(ui_source) return UI_CLOSE -/mob/living/hands_can_use_topic(src_object) +/mob/living/hands_can_use_topic(ui_source) if(HAS_TRAIT(src, TRAIT_HANDS_BLOCKED)) return UI_CLOSE - if(is_in_active_hand(src_object) || is_in_inactive_hand(src_object)) + if(is_in_active_hand(ui_source) || is_in_inactive_hand(ui_source)) return UI_INTERACTIVE return UI_CLOSE -/mob/living/silicon/robot/hands_can_use_topic(src_object) +/mob/living/silicon/robot/hands_can_use_topic(ui_source) if(HAS_TRAIT(src, TRAIT_HANDS_BLOCKED)) return UI_CLOSE - if(activated(src_object)) + if(activated(ui_source)) return UI_INTERACTIVE return UI_CLOSE -/mob/living/simple_animal/revenant/hands_can_use_topic(src_object) +/mob/living/simple_animal/revenant/hands_can_use_topic(ui_source) return UI_UPDATE diff --git a/code/modules/tgui/states/human_adjacent.dm b/code/modules/tgui/states/human_adjacent.dm index b9208f96cd6..ef71fcdba7a 100644 --- a/code/modules/tgui/states/human_adjacent.dm +++ b/code/modules/tgui/states/human_adjacent.dm @@ -12,7 +12,7 @@ GLOBAL_DATUM_INIT(human_adjacent_state, /datum/ui_state/human_adjacent_state, new) -/datum/ui_state/human_adjacent_state/can_use_topic(src_object, mob/user) +/datum/ui_state/human_adjacent_state/can_use_topic(src_object, mob/user, atom/ui_source) . = user.default_can_use_topic(src_object) var/dist = get_dist(src_object, user) diff --git a/code/modules/tgui/states/inventory.dm b/code/modules/tgui/states/inventory.dm index 7c4002b6c7d..d4ee6c8de02 100644 --- a/code/modules/tgui/states/inventory.dm +++ b/code/modules/tgui/states/inventory.dm @@ -6,7 +6,7 @@ GLOBAL_DATUM_INIT(inventory_state, /datum/ui_state/inventory_state, new) -/datum/ui_state/inventory_state/can_use_topic(src_object, mob/user) +/datum/ui_state/inventory_state/can_use_topic(src_object, mob/user, atom/ui_source) if(!(src_object in user)) if(issilicon(user)) var/mob/living/silicon/robot/R = user diff --git a/code/modules/tgui/states/not_incapacitated.dm b/code/modules/tgui/states/not_incapacitated.dm index f7278c86de4..e20409e81b8 100644 --- a/code/modules/tgui/states/not_incapacitated.dm +++ b/code/modules/tgui/states/not_incapacitated.dm @@ -26,7 +26,7 @@ GLOBAL_DATUM_INIT(not_incapacitated_turf_state, /datum/ui_state/not_incapacitate ..() turf_check = no_turfs -/datum/ui_state/not_incapacitated_state/can_use_topic(src_object, mob/user) +/datum/ui_state/not_incapacitated_state/can_use_topic(src_object, mob/user, atom/ui_source) if(user.stat != CONSCIOUS) return UI_CLOSE if(HAS_TRAIT(src, TRAIT_UI_BLOCKED) || user.incapacitated() || (turf_check && !isturf(user.loc))) diff --git a/code/modules/tgui/states/notcontained.dm b/code/modules/tgui/states/notcontained.dm index 5fd79a68848..96c4e7b9919 100644 --- a/code/modules/tgui/states/notcontained.dm +++ b/code/modules/tgui/states/notcontained.dm @@ -13,7 +13,7 @@ GLOBAL_DATUM_INIT(notcontained_state, /datum/ui_state/notcontained_state, new) -/datum/ui_state/notcontained_state/can_use_topic(atom/src_object, mob/user) +/datum/ui_state/notcontained_state/can_use_topic(atom/src_object, mob/user, atom/ui_source) . = user.shared_ui_interaction(src_object) if(. > UI_CLOSE) return min(., user.notcontained_can_use_topic(src_object)) diff --git a/code/modules/tgui/states/observer.dm b/code/modules/tgui/states/observer.dm index 5028834c9e1..dacade83d6b 100644 --- a/code/modules/tgui/states/observer.dm +++ b/code/modules/tgui/states/observer.dm @@ -6,7 +6,7 @@ GLOBAL_DATUM_INIT(observer_state, /datum/ui_state/observer_state, new) -/datum/ui_state/observer_state/can_use_topic(src_object, mob/user) +/datum/ui_state/observer_state/can_use_topic(src_object, mob/user, atom/ui_source) if(isobserver(user)) return UI_INTERACTIVE if(check_rights(R_ADMIN, 0, src)) diff --git a/code/modules/tgui/states/physical.dm b/code/modules/tgui/states/physical.dm index 362e4ecd000..e9c7d604721 100644 --- a/code/modules/tgui/states/physical.dm +++ b/code/modules/tgui/states/physical.dm @@ -11,7 +11,7 @@ GLOBAL_DATUM_INIT(physical_state, /datum/ui_state/physical, new) -/datum/ui_state/physical/can_use_topic(src_object, mob/user) +/datum/ui_state/physical/can_use_topic(src_object, mob/user, atom/ui_source) . = user.shared_ui_interaction(src_object) if(. > UI_CLOSE) return min(., user.physical_can_use_topic(src_object)) @@ -42,7 +42,7 @@ GLOBAL_DATUM_INIT(physical_state, /datum/ui_state/physical, new) GLOBAL_DATUM_INIT(physical_obscured_state, /datum/ui_state/physical_obscured_state, new) -/datum/ui_state/physical_obscured_state/can_use_topic(src_object, mob/user) +/datum/ui_state/physical_obscured_state/can_use_topic(src_object, mob/user, atom/ui_source) . = user.shared_ui_interaction(src_object) if(. > UI_CLOSE) return min(., user.physical_obscured_can_use_topic(src_object)) diff --git a/code/modules/tgui/states/self.dm b/code/modules/tgui/states/self.dm index f7cef3f6005..bf8f3c70554 100644 --- a/code/modules/tgui/states/self.dm +++ b/code/modules/tgui/states/self.dm @@ -11,7 +11,7 @@ GLOBAL_DATUM_INIT(self_state, /datum/ui_state/self_state, new) -/datum/ui_state/self_state/can_use_topic(src_object, mob/user) +/datum/ui_state/self_state/can_use_topic(src_object, mob/user, atom/ui_source) if(src_object != user) return UI_CLOSE return user.shared_ui_interaction(src_object) diff --git a/code/modules/tgui/states/strippable_state.dm b/code/modules/tgui/states/strippable_state.dm index 581ce058046..e1337970fd9 100644 --- a/code/modules/tgui/states/strippable_state.dm +++ b/code/modules/tgui/states/strippable_state.dm @@ -7,7 +7,7 @@ GLOBAL_DATUM_INIT(strippable_state, /datum/ui_state/strippable_state, new) -/datum/ui_state/strippable_state/can_use_topic(src_object, mob/user) +/datum/ui_state/strippable_state/can_use_topic(src_object, mob/user, atom/ui_source) if(!ismob(src_object)) return UI_CLOSE . = user.default_can_use_topic(src_object) diff --git a/code/modules/tgui/states/zlevel.dm b/code/modules/tgui/states/zlevel.dm index 5e3ccfb7de2..80db21fef81 100644 --- a/code/modules/tgui/states/zlevel.dm +++ b/code/modules/tgui/states/zlevel.dm @@ -6,7 +6,7 @@ GLOBAL_DATUM_INIT(z_state, /datum/ui_state/z_state, new) -/datum/ui_state/z_state/can_use_topic(src_object, mob/user) +/datum/ui_state/z_state/can_use_topic(src_object, mob/user, atom/ui_source) var/turf/turf_obj = get_turf(src_object) var/turf/turf_usr = get_turf(user) if(turf_obj && turf_usr && turf_obj.z == turf_usr.z) diff --git a/code/modules/tgui/tgui_input/number_input.dm b/code/modules/tgui/tgui_input/number_input.dm index 5afa06d6516..b9dad2677b4 100644 --- a/code/modules/tgui/tgui_input/number_input.dm +++ b/code/modules/tgui/tgui_input/number_input.dm @@ -15,7 +15,7 @@ * * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout. * * round_value - whether the inputted number is rounded down into an integer. */ -/proc/tgui_input_number(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, timeout = 0, round_value = TRUE, ui_state = GLOB.always_state) +/proc/tgui_input_number(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, timeout = 0, round_value = TRUE, ui_state = GLOB.always_state, ui_source = null) if(!user) user = usr @@ -33,7 +33,7 @@ var/input_number = input(user, message, title, default) as null|num return clamp(round_value ? round(input_number) : input_number, min_value, max_value) - var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, round_value, ui_state) + var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, round_value, ui_state, ui_source, ui_source) number_input.ui_interact(user) number_input.wait() @@ -73,14 +73,15 @@ /// The TGUI UI state that will be returned in ui_state(). Default: always_state var/datum/ui_state/state -/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, round_value, ui_state) +/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, round_value, datum/ui_state, ui_source) src.default = default src.max_value = max_value src.message = message src.min_value = min_value src.title = title src.round_value = round_value - src.state = ui_state + src.state = new ui_state.type() + src.state.ui_source = ui_source if(timeout) src.timeout = timeout diff --git a/icons/obj/machines/customat.dmi b/icons/obj/machines/customat.dmi new file mode 100644 index 00000000000..d692dd21141 Binary files /dev/null and b/icons/obj/machines/customat.dmi differ diff --git a/paradise.dme b/paradise.dme index 2211c093aac..3aa865449fd 100644 --- a/paradise.dme +++ b/paradise.dme @@ -896,6 +896,7 @@ #include "code\game\machinery\constructable_frame.dm" #include "code\game\machinery\cryo.dm" #include "code\game\machinery\cryopod.dm" +#include "code\game\machinery\customat.dm" #include "code\game\machinery\dance_machine.dm" #include "code\game\machinery\defib_mount.dm" #include "code\game\machinery\deployable.dm" diff --git a/tgui/packages/tgui/interfaces/Customat.js b/tgui/packages/tgui/interfaces/Customat.js new file mode 100644 index 00000000000..a991c8ce436 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Customat.js @@ -0,0 +1,115 @@ +import { classes } from 'common/react'; +import { useBackend } from '../backend'; +import { Box, Button, Section, Stack, Table } from '../components'; +import { Window } from '../layouts'; + +const CustomatRow = (props, context) => { + const { act, data } = useBackend(context); + const { product } = props; + const { user, userMoney, vend_ready } = data; + const free = product.price === 0; + let buttonText = 'ERROR!'; + let rowIcon = ''; + if (free) { + buttonText = 'FREE'; + rowIcon = 'arrow-circle-down'; + } else { + buttonText = product.price; + rowIcon = 'shopping-cart'; + } + let buttonDisabled = + !vend_ready || product.stock === 0 || (!free && product.price > userMoney); + return ( + + + + + {product.name} + + + {product.stock} in stock + + + +