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..28107de39ab --- /dev/null +++ b/code/game/machinery/customat.dm @@ -0,0 +1,681 @@ +// 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" + 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-off" + /// 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 + /// If `TRUE` machine will only react to BROKEN/NOPOWER stat, when updating overlays. + var/skip_non_primary_icon_updates = TRUE + + // 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) + + ///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 = 30 + + COOLDOWN_DECLARE(emp_cooldown) + var/weak_emp_cooldown = 60 SECONDS + var/strong_emp_cooldown = 180 SECONDS + +/obj/machinery/customat/Initialize(mapload) + . = ..() + 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 + RefreshParts() + + update_icon(UPDATE_OVERLAYS) + +/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() + return ..() + +/obj/machinery/customat/update_icon(updates = ALL) + if(skip_non_primary_icon_updates && !(stat & (NOPOWER|BROKEN)) && COOLDOWN_FINISHED(src, emp_cooldown)) + return ..(NONE) + 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) + skip_non_primary_icon_updates = TRUE + 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() + skip_non_primary_icon_updates = FALSE + 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) + return + remembered_costs[I.name] = cost + var/key = get_key(I, cost) + if(!user.drop_transfer_item_to_loc(I, src)) + to_chat(usr, span_warning("Вы не можете положить это внутрь.")) + return + if (!(key in products)) + var/datum/data/customat_product/product = new /datum/data/customat_product(I) + product.price = !emagged ? cost : 0 + product.key = key + products[key] = product + + var/datum/data/customat_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(usr, span_warning("Цена не указанна!")) + return + cost = input_cost + if (user && get_dist(get_turf(user), get_turf(src)) > 1) + to_chat(usr, 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)) + playsound(src, 'sound/machines/burglar_alarm.ogg', I.force * 5, 0) + 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)) // enything else will damage customat + 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(usr, 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) + +/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 + for (var/ind = 1; ind <= canister.linked_accounts.len; ++ind) + paid = pay_with_cash(S, usr, currently_vending.price * canister.accounts_weights[ind] / canister.sum_of_weigths, currently_vending.name, canister.linked_accounts[ind]) || paid + else if(get_card_account(usr)) + paid = FALSE + for (var/ind = 1; ind <= canister.linked_accounts.len; ++ind) + paid = pay_with_card(usr, currently_vending.price * canister.accounts_weights[ind] / canister.sum_of_weigths, currently_vending.name, canister.linked_accounts[ind]) || 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 + inserted_items_count-- + + +/** + * 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]) + 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/list/contents = holder.contents + for (var/content in contents) + if (istype(content, /obj/item)) + try_insert(null, content, TRUE) + contents.Remove(content) + pipe_eject(holder) + qdel(holder) + +#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..c6612c2d952 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 = "Торговый автомат с уникальным содержимым" + 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, 100000) + + if (isnull(new_acc_number)) + balloon_alert(user, "номер не введен") + return + + var/weight = tgui_input_number(user, "Пожалуйста, введите вес счета от 1 до 1000000.", "Выбор получаемой доли", 100, 1000000, 1) + + if (isnull(weight)) + balloon_alert(user, "вес не введен") + return + + var/new_account = attempt_account_access(new_acc_number, pin_needed = FALSE) + if (!new_account) + balloon_alert(user, "указанный аккаунт не существует") + return + + if (new_account in linked_accounts) + 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) + + 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) + + 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/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 8b8ca5aad37..66ce5916596 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..f3b480158df --- /dev/null +++ b/tgui/packages/tgui/interfaces/Customat.js @@ -0,0 +1,109 @@ +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 + + + +