diff --git a/code/__DEFINES/dcs/atom_signals.dm b/code/__DEFINES/dcs/atom_signals.dm index f5f9a204bf69..9c358c6854f8 100644 --- a/code/__DEFINES/dcs/atom_signals.dm +++ b/code/__DEFINES/dcs/atom_signals.dm @@ -143,3 +143,5 @@ #define COMPONENT_NO_MOUSEDROP (1<<0) ///from base of atom/MouseDrop_T: (/atom/from, /mob/user) #define COMSIG_MOUSEDROPPED_ONTO "mousedropped_onto" +/// On a ranged attack: base of mob/living/carbon/human/RangedAttack (/mob/living/carbon/human) +#define COMSIG_ATOM_RANGED_ATTACKED "atom_range_attacked" diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index d5dc2ff3513a..a6df2f627416 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -76,6 +76,8 @@ #define isstack(I) (istype(I, /obj/item/stack)) +#define istable(S) (istype(S, /obj/structure/table)) + GLOBAL_LIST_INIT(pointed_types, typecacheof(list( /obj/item/pen, /obj/item/screwdriver, diff --git a/code/__HELPERS/trait_helpers.dm b/code/__HELPERS/trait_helpers.dm index b66d321f9890..b8501ede03e3 100644 --- a/code/__HELPERS/trait_helpers.dm +++ b/code/__HELPERS/trait_helpers.dm @@ -237,6 +237,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_NPC_ZOMBIE "npc_zombie" // A trait for checking if a zombie should act like an NPC and attack #define TRAIT_ABSTRACT_HANDS "abstract_hands" // Mobs with this trait can only pick up abstract items. #define TRAIT_LANGUAGE_LOCKED "language_locked" // cant add/remove languages until removed (excludes babel because fuck everything i guess) +#define TRAIT_PLAYING_CARDS "playing_cards" #define TRAIT_EMP_IMMUNE "emp_immune" //The mob will take no damage from EMPs #define TRAIT_EMP_RESIST "emp_resist" //The mob will take less damage from EMPs #define TRAIT_MINDFLAYER_NULLIFIED "flayer_nullified" //The mindflayer will not be able to activate their abilities, or drain swarms from people diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index 437425a29e3f..0c1bda08b3ae 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -273,9 +273,11 @@ /mob/proc/RangedAttack(atom/A, params) if(SEND_SIGNAL(src, COMSIG_MOB_ATTACK_RANGED, A, params) & COMPONENT_CANCEL_ATTACK_CHAIN) return TRUE + if(SEND_SIGNAL(A, COMSIG_ATOM_RANGED_ATTACKED, src) & COMPONENT_CANCEL_ATTACK_CHAIN) + return TRUE + /* Restrained ClickOn - Used when you are handcuffed and click things. Not currently used by anything but could easily be. */ diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm index 48cd25069e21..c9f8fccfd1f7 100644 --- a/code/_onclick/other_mobs.dm +++ b/code/_onclick/other_mobs.dm @@ -41,6 +41,8 @@ /mob/living/carbon/human/RangedAttack(atom/A, params) . = ..() + if(.) + return if(gloves) var/obj/item/clothing/gloves/G = gloves if(istype(G) && G.Touch(A, 0)) // for magic gloves @@ -54,7 +56,6 @@ if(isturf(A) && get_dist(src, A) <= 1) Move_Pulled(A) - /* Animals & All Unspecified */ diff --git a/code/datums/components/proximity_monitor.dm b/code/datums/components/proximity_monitor.dm index b37712b9d1f7..53c91dc95775 100644 --- a/code/datums/components/proximity_monitor.dm +++ b/code/datums/components/proximity_monitor.dm @@ -286,3 +286,167 @@ . = ..() if(active && AM != monitor.hasprox_receiver && !(AM in monitor.nested_receiver_locs)) monitor.hasprox_receiver.HasProximity(AM) + +/// A custom proximity monitor used for tracking players around a table of cards. + +/datum/component/proximity_monitor/table + /// How far away you can be (in terms of table squares). + var/max_table_distance + /// How far away you can be (euclidean distance). + var/max_total_distance + /// The UID of the deck + var/deck_uid + /// Whether the monitors created should be visible. Used for debugging. + var/monitors_visible = FALSE + +/datum/component/proximity_monitor/table/Initialize(_radius = 1, _always_active = FALSE, _max_table_distance = 5) + max_table_distance = _max_table_distance + max_total_distance = _max_table_distance + . = ..(_radius, _always_active) + if(istype(parent, /obj/item/deck)) + // this is important for tracking traits and attacking multiple cards. so it's not a true UID, sue me + var/obj/item/deck/D = parent + deck_uid = D.main_deck_id + else + deck_uid = parent.UID() + addtimer(CALLBACK(src, PROC_REF(refresh)), 0.5 SECONDS) + +/datum/component/proximity_monitor/table/proc/refresh() + var/list/tables = list() + var/list/prox_mon_spots = list() + crawl_along(get_turf(parent), tables, prox_mon_spots, 0) + QDEL_LIST_CONTENTS(proximity_checkers) + create_prox_checkers() + +/// Crawl along an extended table, and return a list of all turfs that we should start tracking. +/datum/component/proximity_monitor/table/proc/crawl_along(turf/current_turf, list/visited_tables = list(), list/prox_mon_spots = list(), distance_from_start) + var/obj/structure/current_table = locate(/obj/structure/table) in current_turf + + if(QDELETED(current_table)) + // if there's no table here, we're still adjacent to a table, so this is a spot you could play from + prox_mon_spots |= current_turf + return + + if(current_table in visited_tables) + return + + visited_tables |= current_table + prox_mon_spots |= current_turf + + if(distance_from_start + 1 > max_table_distance) + return + + for(var/direction in GLOB.alldirs) + var/turf/next_turf = get_step(current_table, direction) + if(!istype(next_turf)) + stack_trace("Failed to proceed in direction [dir2text(direction)] when building card proximity monitors.") + continue + if(get_dist_euclidian(get_turf(parent), next_turf) > max_total_distance) + continue + .(next_turf, visited_tables, prox_mon_spots, distance_from_start + 1) + +/datum/component/proximity_monitor/table/create_prox_checkers() + update_prox_checkers(FALSE) + +/** + * Update the proximity monitors making up this component. + * Arguments: + * * clear_existing - If true, any existing proximity monitors attached to this will be deleted. + */ +/datum/component/proximity_monitor/table/proc/update_prox_checkers(clear_existing = TRUE) + var/list/tables = list() + var/list/prox_mon_spots = list() + if(length(proximity_checkers)) + QDEL_LIST_CONTENTS(proximity_checkers) + + var/atom/movable/atom_parent = parent + + // if we don't have a parent, just treat it normally + if(!isturf(atom_parent.loc) || !locate(/obj/structure/table) in get_turf(parent)) + return + + + LAZYINITLIST(proximity_checkers) + crawl_along(get_turf(parent), tables, prox_mon_spots, 0) + + // For whatever reason their turf is null. Create the checkers in nullspace for now. When the parent moves to a valid turf, they can be recenetered. + for(var/T in prox_mon_spots) + create_single_prox_checker(T, /obj/effect/abstract/proximity_checker/table) + + for(var/atom/table in tables) + RegisterSignal(table, COMSIG_PARENT_QDELETING, PROC_REF(on_table_qdel), TRUE) + +/datum/component/proximity_monitor/table/on_receiver_move(datum/source, atom/old_loc, dir) + update_prox_checkers() + +/datum/component/proximity_monitor/table/RegisterWithParent() + if(ismovable(hasprox_receiver)) + RegisterSignal(hasprox_receiver, COMSIG_MOVABLE_MOVED, PROC_REF(on_receiver_move)) + +/datum/component/proximity_monitor/table/proc/on_table_qdel() + SIGNAL_HANDLER // COMSIG_PARENT_QDELETED + update_prox_checkers() + +/obj/effect/abstract/proximity_checker/table + /// The UID for the deck, used in the setting and removal of traits + var/deck_uid + +/obj/effect/abstract/proximity_checker/table/Initialize(mapload, datum/component/proximity_monitor/table/P) + . = ..() + deck_uid = P.deck_uid + // catch any mobs on our tile + for(var/mob/living/L in get_turf(src)) + register_on_mob(L) + + if(P.monitors_visible) + icon = 'icons/obj/playing_cards.dmi' + icon_state = "tarot_the_unknown" + invisibility = INVISIBILITY_MINIMUM + layer = MOB_LAYER + +/obj/effect/abstract/proximity_checker/table/Destroy() + var/obj/effect/abstract/proximity_checker/table/same_monitor + for(var/obj/effect/abstract/proximity_checker/table/mon in get_turf(src)) + if(mon != src && mon.deck_uid == src) + // if we have another monitor on our space that shares our deck, + // transfer the signals to it. + same_monitor = mon + + for(var/mob/living/L in get_turf(src)) + remove_from_mob(L) + if(!isnull(same_monitor)) + same_monitor.register_on_mob(L) + return ..() + +/obj/effect/abstract/proximity_checker/table/proc/register_on_mob(mob/living/L) + ADD_TRAIT(L, TRAIT_PLAYING_CARDS, "deck_[deck_uid]") + RegisterSignal(L, COMSIG_MOVABLE_MOVED, PROC_REF(on_move_from_monitor), TRUE) + RegisterSignal(L, COMSIG_PARENT_QDELETING, PROC_REF(remove_from_mob), TRUE) + + +/obj/effect/abstract/proximity_checker/table/proc/remove_from_mob(mob/living/L) + if(QDELETED(L)) + return + // otherwise, clean up + REMOVE_TRAIT(L, TRAIT_PLAYING_CARDS, "deck_[deck_uid]") + UnregisterSignal(L, COMSIG_MOVABLE_MOVED) + +/obj/effect/abstract/proximity_checker/table/Crossed(atom/movable/AM, oldloc) + if(!isliving(AM)) + return + + var/mob/mover = AM + + // This should hopefully ensure that multiple decks around each other don't overlap + register_on_mob(mover) + +/// Triggered when someone moves from a tile that contains our monitor. +/obj/effect/abstract/proximity_checker/table/proc/on_move_from_monitor(atom/movable/tracked, atom/old_loc) + SIGNAL_HANDLER // COMSIG_MOVABLE_MOVED + for(var/obj/effect/abstract/proximity_checker/table/mon in get_turf(tracked)) + // if we're moving onto a turf that shares our stuff, keep the signals and stuff registered + if(mon.deck_uid == deck_uid) + return + + // otherwise, clean up + remove_from_mob(tracked) diff --git a/code/game/gamemodes/wizard/magic_tarot.dm b/code/game/gamemodes/wizard/magic_tarot.dm index d51101e0e3d1..9eed402337ce 100644 --- a/code/game/gamemodes/wizard/magic_tarot.dm +++ b/code/game/gamemodes/wizard/magic_tarot.dm @@ -234,7 +234,7 @@ /obj/item/magic_tarot_card/proc/pre_activate(mob/user, atom/movable/thrower) has_been_activated = TRUE forceMove(user) - var/obj/effect/temp_visual/tarot_preview/draft = new /obj/effect/temp_visual/tarot_preview(user, our_tarot.card_icon) + var/obj/effect/temp_visual/card_preview/tarot/draft = new(user, "tarot_[our_tarot.card_icon]") user.vis_contents += draft user.visible_message("[user] holds up [src]!") addtimer(CALLBACK(our_tarot, TYPE_PROC_REF(/datum/tarot, activate), user), 0.5 SECONDS) @@ -242,17 +242,35 @@ add_attack_logs(thrower, user, "[thrower] has activated [our_tarot.name] on [user]", ATKLOG_FEW) QDEL_IN(src, 0.6 SECONDS) -/obj/effect/temp_visual/tarot_preview - name = "a tarot card" +/obj/effect/temp_visual/card_preview + name = "a card" icon = 'icons/obj/playing_cards.dmi' icon_state = "tarot_the_unknown" pixel_y = 20 duration = 1.5 SECONDS -/obj/effect/temp_visual/tarot_preview/Initialize(mapload, new_icon_state) +/obj/effect/temp_visual/card_preview/Initialize(mapload, new_icon_state) . = ..() if(new_icon_state) - icon_state = "tarot_[new_icon_state]" + icon_state = new_icon_state + + flourish() + +/obj/effect/temp_visual/card_preview/proc/flourish() + var/new_filter = isnull(get_filter("ray")) + ray_filter_helper(1, 40, "#fcf3dc", 6, 20) + if(new_filter) + animate(get_filter("ray"), alpha = 0, offset = 10, time = duration, loop = -1) + animate(offset = 0, time = duration) + +/obj/effect/temp_visual/card_preview/tarot + name = "a tarot card" + icon = 'icons/obj/playing_cards.dmi' + icon_state = "tarot_the_unknown" + pixel_y = 20 + duration = 1.5 SECONDS + +/obj/effect/temp_visual/card_preview/tarot/flourish() var/new_filter = isnull(get_filter("ray")) ray_filter_helper(1, 40,"#fcf3dc", 6, 20) if(new_filter) diff --git a/code/modules/games/cards.dm b/code/modules/games/cards.dm index 32af569e0691..ef51b912bf8f 100644 --- a/code/modules/games/cards.dm +++ b/code/modules/games/cards.dm @@ -1,6 +1,8 @@ /datum/playingcard var/name = "playing card" + /// The front of the card, with all the fun stuff. var/card_icon = "card_back" + /// The back of the card, shown when face-down. var/back_icon = "card_back" /datum/playingcard/New(newname, newcard_icon, newback_icon) @@ -16,12 +18,18 @@ w_class = WEIGHT_CLASS_SMALL icon = 'icons/obj/playing_cards.dmi' actions_types = list(/datum/action/item_action/draw_card, /datum/action/item_action/deal_card, /datum/action/item_action/deal_card_multi, /datum/action/item_action/shuffle) + + throw_speed = 3 + throw_range = 10 + throwforce = 0 + force = 0 + var/list/cards = list() - /// To prevent spam shuffle - var/cooldown = 0 - /// Decks default to a single pack, setting it higher will multiply them by that number + /// How often the deck can be shuffled. + var/shuffle_cooldown = 0 + /// How many copies of the base deck (built in build_deck()) should be added? var/deck_size = 1 - /// The total number of cards. Set on init after the deck is fully built + /// The number of cards in a full deck. Set on init after all cards are created/added. var/deck_total = 0 /// Styling for the cards, if they have multiple sets of sprites var/card_style = null @@ -29,10 +37,6 @@ var/deck_style = null /// For decks without a full set of sprites var/simple_deck = FALSE - throw_speed = 3 - throw_range = 10 - throwforce = 0 - force = 0 /// Inherited card hit sound var/card_hitsound /// Inherited card force @@ -47,12 +51,60 @@ var/card_attack_verb /// Inherited card resistance var/card_resistance_flags = FLAMMABLE - -/obj/item/deck/Initialize(mapload) + /// ID used to track the decks and cardhands that can be combined into one another. + var/main_deck_id = -1 + /// The name of the last player to interact with this deck. + var/last_player_name + /// The action that the last player made. Should be in the form of "played a card", "drew a card." + var/last_player_action + +/obj/item/deck/Initialize(mapload, parent_deck_id = -1) . = ..() build_decks() - update_icon(UPDATE_ICON_STATE) + update_icon(UPDATE_ICON_STATE|UPDATE_OVERLAYS) + if(parent_deck_id == -1) + // we're our own deck + main_deck_id = rand(1, 99999) + else + main_deck_id = parent_deck_id + + return INITIALIZE_HINT_LATELOAD + +/obj/item/deck/LateInitialize(mapload) + . = ..() + AddComponent(/datum/component/proximity_monitor/table) + RegisterSignal(src, COMSIG_ATOM_RANGED_ATTACKED, PROC_REF(on_ranged_attack)) +/obj/item/deck/examine(mob/user) + . = ..() + . += "It contains [length(cards) ? length(cards) : "no"] card\s." + if(last_player_name && last_player_action) + . += "Most recent action: [last_player_name] [last_player_action]." + if(in_play_range(user) && !Adjacent(user)) + . += "You're in range of this card-hand, and can use it at a distance!" + else + . += "You're too far away from this deck to play from it." + . += "" + . += "Drag [src] to yourself to pick it up." + . += "Examine this again to see some shortcuts for interacting with it." + +/obj/item/deck/examine_more(mob/user) + . = ..() + . += "Click to draw a card." + . += "Alt-Click to place a card." + . += "Ctrl-Shift-Click to split [src]." + . += "If you draw or return cards with harm intent, your plays will be public!" + . += "With cards in your active hand..." + . += "\tCtrl-Click with cards to place them at the bottom of the deck." + . += "" + . += "You also notice a little number on the corner of [src]: it's tagged [main_deck_id]." + +/obj/item/deck/proc/add_most_recent_action(mob/user, action) + last_player_name = "[user]" + last_player_action = action + +/// Fill the deck with all the specified cards. +/// Uses deck_size to determine how many times to call build_deck() /obj/item/deck/proc/build_decks() if(length(cards)) // prevent building decks more than once @@ -61,36 +113,187 @@ build_deck() deck_total = length(cards) +/// Stub, override this to define how a base deck should be filled and built. /obj/item/deck/proc/build_deck() return +/obj/item/deck/proc/on_ranged_attack(mob/source, mob/living/carbon/human/attacker) + SIGNAL_HANDLER // COMSIG_ATOM_RANGED_ATTACKED + if(!istype(attacker)) + return + INVOKE_ASYNC(src, TYPE_PROC_REF(/atom, attack_hand), attacker) + return COMPONENT_CANCEL_ATTACK_CHAIN + /obj/item/deck/attackby(obj/O, mob/user) - if(istype(O, /obj/item/cardhand)) - var/obj/item/cardhand/H = O - if(H.parentdeck != src) - to_chat(user,"You can't mix cards from different decks!") + // clicking is for drawing + if(istype(O, /obj/item/deck)) + var/obj/item/deck/other_deck = O + if(other_deck.main_deck_id != main_deck_id) + to_chat(user, "These aren't the same deck!") return - if(length(H.cards) > 1) - var/confirm = tgui_alert(user, "Are you sure you want to put your [length(H.cards)] cards back into the deck?", "Return Hand", list("Yes", "No")) - if(confirm != "Yes" || !Adjacent(user) || HAS_TRAIT(user, TRAIT_HANDS_BLOCKED)) - return - for(var/datum/playingcard/P in H.cards) - cards += P - qdel(H) - to_chat(user, "You place your cards on the bottom of [src].") - update_icon(UPDATE_ICON_STATE) + merge_deck(user, O) return - ..() -/obj/item/deck/examine(mob/user) + if(istype(O, /obj/item/pen)) + rename_interactive(user, O) + return + + if(!istype(O, /obj/item/cardhand)) + return ..() + var/obj/item/cardhand/H = O + if(H.parent_deck_id != main_deck_id) + to_chat(user, "You can't mix cards from different decks!") + return + + draw_card(user, user.a_intent == INTENT_HARM) + +/obj/item/deck/proc/in_play_range(mob/user) + + if(HAS_TRAIT(user, TRAIT_TELEKINESIS)) + return TRUE + + if(HAS_TRAIT_FROM(user, TRAIT_PLAYING_CARDS, "deck_[main_deck_id]")) + return TRUE + + return Adjacent(user) + +/obj/item/deck/CtrlShiftClick(mob/living/carbon/human/user) . = ..() - . +="It contains [length(cards) ? length(cards) : "no"] cards" + if(!Adjacent(user) || !istype(user)) + return + if(length(cards) <= 1) + to_chat(user, "You can't split this deck, it's too small!") + return + + var/num_cards = tgui_input_number(user, "How many cards would you like the new split deck to have?", "Split Deck", length(cards) / 2, length(cards), 0) + if(isnull(num_cards) || !in_play_range(user)) + return + + // split it off, with our deck ID. + var/obj/item/deck/new_deck = new src.type(get_turf(src), main_deck_id) + QDEL_LIST_CONTENTS(new_deck.cards) + new_deck.cards = cards.Copy(1, num_cards + 1) + cards.Cut(1, num_cards + 1) + user.put_in_any_hand_if_possible(new_deck) + user.visible_message("[user] splits [src] in two.", "You split [src] in two.") + update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_ICON_STATE|UPDATE_OVERLAYS) + new_deck.update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_ICON_STATE|UPDATE_OVERLAYS) + +/obj/item/deck/proc/merge_deck(mob/user, obj/item/deck/other_deck) + if(main_deck_id != other_deck.main_deck_id) + if(user) + to_chat(user, "These decks didn't both come from the same original deck, you can't merge them!") + return + for(var/card in other_deck.cards) + cards += card + other_deck.cards -= card + qdel(other_deck) + update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_ICON_STATE|UPDATE_OVERLAYS) + if(user) + add_most_recent_action(user, "merged with [other_deck]") + user.visible_message("[user] mixes the two decks together.", "You merge the two decks together.") + +/obj/item/deck/attack_hand(mob/user) + draw_card(user, user.a_intent == INTENT_HARM) + +/obj/item/deck/AltClick(mob/living/carbon/human/user) + // alt-clicking is for putting back + return_hand_click(user, TRUE) + +/// Return a single card to the deck. +/obj/item/deck/proc/return_hand_click(mob/living/carbon/human/user, on_top = TRUE) + if(!istype(user)) + return + var/obj/item/cardhand/hand = user.get_active_card_hand() + if(!istype(hand)) + return + + if(length(hand.cards) == 1) + // we need to put this into a new list, otherwise things get funky if they reference both the hand list and this other list + return_cards(user, hand, on_top, hand.cards.Copy()) + return + var/datum/playingcard/selected_card = hand.select_card_radial(user) + if(QDELETED(selected_card)) + return + return_cards(user, hand, on_top, list(selected_card)) + +/obj/item/deck/MouseDrop_T(obj/item/I, mob/user) + if(!istype(I, /obj/item/cardhand)) + return + if(!in_play_range(user) || !user.Adjacent(I)) + return + var/choice = tgui_alert(user, "Where would you like to return your hand to the deck?", "Return Hand", list("Top", "Bottom", "Cancel")) + if(!in_play_range(user) || !user.Adjacent(I) || HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) || isnull(choice) || choice == "Cancel") + return + + return_cards(user, I, choice == "Top") + +// is this getting too complicated? -/obj/item/deck/attack_hand(mob/user as mob) - draw_card(user) +/** + * Return a number of cards to a deck. + * + * Arguments: + * * user - The mob returning the cards. + * * hand - The hand from which the cards are being returned. + * * place_on_top - If true, cards will be placed on the top of the deck. Otherwise, they'll be placed on the bottom. + * * chosen_cards - If not empty, will essentially override any selection dialogs and force these cards to be returned. + */ +/obj/item/deck/proc/return_cards(mob/living/carbon/human/user, obj/item/cardhand/hand, place_on_top = TRUE, chosen_cards = list()) -// Datum actions + if(!istype(hand)) + return + + if(!in_play_range(user)) + return + + if(hand.parent_deck_id != main_deck_id) + to_chat(user, "You can't mix cards from different decks!") + return + + var/side = place_on_top ? "top" : "bottom" + + // if we have chosen cards, we can skip confirmation since that should have probably happened before us + if(!length(chosen_cards)) + if(length(hand.cards) > 1) + var/confirm = tgui_alert(user, "Are you sure you want to put your [length(hand.cards)] card\s into the [side] of the deck?", "Return Hand to Bottom", list("Yes", "No")) + if(confirm != "Yes" || !in_play_range(user) || HAS_TRAIT(user, TRAIT_HANDS_BLOCKED)) + return + chosen_cards = hand.cards.Copy() // copy the list since we might be deleting the ref + + if(place_on_top) + cards = chosen_cards + cards + else + // equiv to += but here for clarity + cards = cards + chosen_cards + hand.cards -= chosen_cards + if(!length(hand.cards)) + qdel(hand) + else + hand.update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) + if(length(chosen_cards) == 1) + if(!hand.concealed) + // do the attack animation with the single card being played + var/datum/playingcard/P = chosen_cards[1] + if(user.a_intent == INTENT_HARM) + var/obj/effect/temp_visual/card_preview/draft = new /obj/effect/temp_visual/card_preview(user, P.card_icon) + user.vis_contents += draft + QDEL_IN(draft, 1 SECONDS) + sleep(1 SECONDS) + add_most_recent_action(user, "placed [P] on the [side]") + user.visible_message("[user] places [P] on the [side] of [src].", "You place [P] on the [side] of [src].") + user.do_attack_animation(src, hand) + update_icon(UPDATE_ICON_STATE|UPDATE_OVERLAYS) + return + // don't attack with the open hand lol + user.do_attack_animation(src, no_effect = TRUE) + + add_most_recent_action(user, "placed [length(chosen_cards)] card\s on the [side]") + user.visible_message("[user] returns [length(chosen_cards)] card\s to the [side] of [src].", "You return [length(chosen_cards)] card\s to the [side] of [src].") + + +// deck datum actions /datum/action/item_action/draw_card name = "Draw - Draw one card" button_overlay_icon_state = "draw" @@ -110,7 +313,7 @@ /datum/action/item_action/deal_card/Trigger(left_click) if(istype(target, /obj/item/deck)) var/obj/item/deck/D = target - return D.deal_card() + return D.deal_card(owner) return ..() /datum/action/item_action/deal_card_multi @@ -121,7 +324,7 @@ /datum/action/item_action/deal_card_multi/Trigger(left_click) if(istype(target, /obj/item/deck)) var/obj/item/deck/D = target - return D.deal_card_multi() + return D.deal_card_multi(owner) return ..() /datum/action/item_action/shuffle @@ -132,23 +335,27 @@ /datum/action/item_action/shuffle/Trigger(left_click) if(istype(target, /obj/item/deck)) var/obj/item/deck/D = target - return D.deckshuffle() + return D.deckshuffle(owner) return ..() -// Datum actions +/** + * Draw a card from this deck. + * Arguments: + * * user - The mob drawing the card. + * * public - If true, the drawn card will be displayed to people around the table. + * * draw_from_top - If true, the card will be drawn from the top of the deck. + */ +/obj/item/deck/proc/draw_card(mob/living/carbon/human/user, public = FALSE, draw_from_top = TRUE) -/obj/item/deck/proc/draw_card(mob/user) - var/mob/living/carbon/human/M = user - - if(user.incapacitated() || !Adjacent(user)) + if(HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) || !in_play_range(user) || !istype(user)) return if(!length(cards)) to_chat(user,"There are no cards in the deck.") return - var/obj/item/cardhand/H = M.is_in_hands(/obj/item/cardhand) - if(H && (H.parentdeck != src)) + var/obj/item/cardhand/H = user.get_active_card_hand() + if(H && (H.parent_deck_id != main_deck_id)) to_chat(user,"You can't mix cards from different decks!") return @@ -156,77 +363,86 @@ H = new(get_turf(src)) user.put_in_hands(H) - var/datum/playingcard/P = cards[1] + var/datum/playingcard/P = draw_from_top ? cards[1] : cards[length(cards)] H.cards += P cards -= P - update_icon(UPDATE_ICON_STATE) - H.parentdeck = src - H.update_values() + update_icon(UPDATE_ICON_STATE|UPDATE_OVERLAYS) + H.parent_deck_id = main_deck_id + H.update_values_from_deck(src) H.update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) - user.visible_message( - "[user] draws a card.", - "You draw a card.", - "You hear a card being drawn." - ) - to_chat(user,"It's the [P].") -/obj/item/deck/proc/deal_card() - if(usr.incapacitated() || !Adjacent(usr)) + user.do_attack_animation(src, no_effect = TRUE) + if(public) + add_most_recent_action(user, "drew \a [P.name]") + user.visible_message("[user] draws \a [P.name]!", "You draw \a [P]!", "You hear a card be drawn.") + var/obj/effect/temp_visual/card_preview/draft = new /obj/effect/temp_visual/card_preview(user, P.card_icon) + user.vis_contents += draft + QDEL_IN(draft, 1 SECONDS) + sleep(1 SECONDS) + else + add_most_recent_action(user, "drew a card") + user.visible_message("[user] draws a card.", "You draw a card.", "You hear a card be drawn.") + to_chat(user, "It's \a [P.name].") + +// Classic action +/obj/item/deck/proc/deal_card(mob/user) + if(HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) || !in_play_range(user)) return if(!length(cards)) - to_chat(usr,"There are no cards in the deck.") + to_chat(user, "There are no cards in the deck.") return var/list/players = list() for(var/mob/living/player in viewers(3)) - if(!player.incapacitated()) - players += player - //players -= usr + players += player - var/mob/living/M = tgui_input_list(usr, "Who do you wish to deal a card to?", "Deal Card", players) - if(!usr || !src || !M) return + var/mob/living/M = tgui_input_list(user, "Who do you wish to deal a card to?", "Deal Card", players) + if(QDELETED(user) || QDELETED(src) || QDELETED(M) || !in_play_range(user)) + return deal_at(usr, M, 1) -/obj/item/deck/proc/deal_card_multi() - if(usr.incapacitated() || !Adjacent(usr)) +/obj/item/deck/proc/deal_card_multi(mob/user) + if(HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) || !Adjacent(user)) return if(!length(cards)) - to_chat(usr,"There are no cards in the deck.") + to_chat(user, "There are no cards in the deck.") return var/list/players = list() for(var/mob/living/player in viewers(3)) if(!player.incapacitated()) players += player - var/dcard = tgui_input_number(usr, "How many card(s) do you wish to deal? You may deal up to [length(cards)] cards.", "Deal Cards", 1, length(cards), 1) + var/dcard = tgui_input_number(user, "How many card(s) do you wish to deal? You may deal up to [length(cards)] card\s.", "Deal Cards", 1, length(cards), 1) if(isnull(dcard)) return - var/mob/living/M = tgui_input_list(usr, "Who do you wish to deal [dcard] card(s)?", "Deal Card", players) - if(!usr || !src || !M || !Adjacent(usr)) + var/mob/living/M = tgui_input_list(user, "Who do you wish to deal [dcard] card\s?", "Deal Card", players) + if(!user || !src || !M || !Adjacent(user)) return - deal_at(usr, M, dcard) + deal_at(user, M, dcard) /obj/item/deck/proc/deal_at(mob/user, mob/target, dcard) // Take in the no. of card to be dealt var/obj/item/cardhand/H = new(get_step(user, user.dir)) for(var/i in 1 to dcard) H.cards += cards[1] cards -= cards[1] - update_icon(UPDATE_ICON_STATE) - H.parentdeck = src - H.update_values() - H.concealed = TRUE - H.update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) + update_icon(UPDATE_ICON_STATE|UPDATE_OVERLAYS) + H.parent_deck_id = main_deck_id + H.update_values_from_deck(src) + H.concealed = TRUE + H.update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) if(user == target) + add_most_recent_action(user, "dealt [dcard] card\s to [user.p_themselves()]") user.visible_message( "[user] deals [dcard] card\s to [user.p_themselves()].", "You deal [dcard] card\s to yourself.", "You hear cards being dealt." ) else + add_most_recent_action(user, "dealt [dcard] card\s to [target]") user.visible_message( "[user] deals [dcard] card\s to [target].", "You deal [dcard] card\s to [target].", @@ -234,34 +450,32 @@ ) H.throw_at(get_step(target, target.dir), 3, 1, null) -/obj/item/deck/attack_self() - deckshuffle() +/obj/item/deck/attack_self(mob/user) + deckshuffle(user) -/obj/item/deck/AltClick() - deckshuffle() - -/obj/item/deck/proc/deckshuffle() - var/mob/living/user = usr - if(cooldown < world.time - 1 SECONDS) +/obj/item/deck/proc/deckshuffle(mob/user) + if(shuffle_cooldown < world.time - 1 SECONDS) cards = shuffle(cards) if(user) + add_most_recent_action(user, "shuffled [src]") user.visible_message( "[user] shuffles [src].", "You shuffle [src].", "You hear cards being shuffled." ) playsound(user, 'sound/items/cardshuffle.ogg', 50, TRUE) - cooldown = world.time + shuffle_cooldown = world.time + update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) /obj/item/deck/MouseDrop(atom/over, src_location, over_location, src_control, over_control, params) var/mob/M = usr if(M.incapacitated() || !Adjacent(M)) - return + return ..() if(!ishuman(M)) - return + return ..() if(is_screen_atom(over)) if(!remove_item_from_storage(get_turf(M))) @@ -282,6 +496,9 @@ add_fingerprint(M) usr.visible_message("[usr] picks up the deck.") + return ..() + + /obj/item/pack name = "card pack" desc = "For those with disposable income." @@ -315,19 +532,127 @@ icon = 'icons/obj/playing_cards.dmi' w_class = WEIGHT_CLASS_TINY actions_types = list(/datum/action/item_action/remove_card, /datum/action/item_action/discard) - + /// If true, the cards will be face down. var/concealed = FALSE + /// All of the cards in the deck. var/list/cards = list() /// Tracked direction, which is used when updating the hand's appearance instead of messing with the local dir var/direction = NORTH - var/parentdeck = null - /// The player's picked card they want to take out. Stored in the hand so it can be passed onto the verb - var/pickedcard = null + /// The ID of the base deck that we belong to. + // This ID can correspond to multiple decks, but those decks will only ever be sub-decks of the original deck this hand's cards came from. + var/parent_deck_id = null + +/obj/item/cardhand/examine(mob/user) + . = ..() + if(!concealed && length(cards)) + . += "It contains:" + for(var/datum/playingcard/P as anything in cards) + . += "\tthe [P.name]." + + if(Adjacent(user)) + . += "Click this in-hand to select a card to draw." + . += "Ctrl-Click this to flip it." + if(loc == user) + . += "Alt-Click this in-hand to see the legacy interaction menu." + else + . += "Alt-Click this to add a card from your deck to it." + + . += "Drag this to its associated deck to return all cards at once to it." + . += "Enable throw mode to automatically catch cards and add them to your hand." + . += "Drag-and-Drop this onto another hand to merge the cards together." + if(loc != user) + . += "You can Drag this to yourself from here to draw cards from it." + +/obj/item/cardhand/examine_more(mob/user) + . = ..() + . += "You notice a little number on the corner of [src]: it's tagged [parent_deck_id]." -/obj/item/cardhand/proc/update_values() - if(!parentdeck) +/obj/item/cardhand/proc/single() + return length(cards) == 1 + +/obj/item/cardhand/afterattack(atom/target, mob/user, proximity_flag, click_parameters) + // this is how we handle our ranged attacks. + . = ..() + if(!istype(target, /obj/item/deck) || proximity_flag) + // if we're adjacent to the deck, don't do anything since we'll already be using attackby. + return + + var/obj/item/deck/D = target + if(D.in_play_range(user)) + return D.attackby(src, user) + +/obj/item/deck/hitby(atom/movable/thrown, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum) + + if(!istype(thrown, /obj/item/cardhand)) + return ..() + + var/obj/item/cardhand/hand = thrown + + if(hand.parent_deck_id != main_deck_id) + return ..() + + // you can only throw single cards in this way + if(length(hand.cards) > 1) + return ..() + + if(hand.concealed) + visible_message("A card lands on top of [src].") + else + var/datum/playingcard/C = hand.cards[1] + visible_message("[C] lands atop [src]!") + + cards = hand.cards + cards + update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) + hand.cards.Cut() + qdel(hand) + +/obj/item/cardhand/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum) + if(!ishuman(hit_atom)) + return ..() + + var/mob/living/carbon/human/M = hit_atom + + + var/obj/item/cardhand/hand = M.get_active_card_hand() + if(!istype(hand) || !M.in_throw_mode || hand.parent_deck_id != parent_deck_id) + return ..() + + M.visible_message( + "[M] catches [hand] and adds it to [M.p_their()] hand!", + "You catch [hand] and add it to your existing hand!" + ) + add_cardhand_to_self(hand) + +/// Merge the target cardhand into the current cardhand +/obj/item/cardhand/proc/add_cardhand_to_self(obj/item/cardhand/hand) + if(!parent_deck_id == hand.parent_deck_id) + stack_trace("merge_into tried to merge two different parent decks together!") + return + + for(var/card in hand.cards) + cards += card + hand.cards -= card + + update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) + + qdel(hand) + +/obj/item/cardhand/proc/transfer_card_to_self(obj/item/cardhand/source_hand, datum/playingcard/card) + if(!istype(source_hand) || !(card in source_hand.cards) || (source_hand.parent_deck_id != parent_deck_id)) + return + source_hand.cards -= card + cards += card + + update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) + if(!length(source_hand.cards)) + qdel(source_hand) + else + source_hand.update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) + +/// Update our card values based on those set on a deck +/obj/item/cardhand/proc/update_values_from_deck(obj/item/deck/D) + if(!parent_deck_id) return - var/obj/item/deck/D = parentdeck hitsound = D.card_hitsound force = D.card_force throwforce = D.card_throwforce @@ -336,57 +661,190 @@ attack_verb = D.card_attack_verb resistance_flags = D.card_resistance_flags +/// Update our own card values to reflect that of some source hand +/obj/item/cardhand/proc/update_values_from_cards(obj/item/cardhand/source) + if(isnull(source) || !istype(source)) + return + + hitsound = source.hitsound + force = source.force + throwforce = source.throwforce + throw_speed = source.throw_speed + throw_range = source.throw_range + attack_verb = source.attack_verb + resistance_flags = source.resistance_flags + /obj/item/cardhand/attackby(obj/O, mob/user) + // Augh I really don't like this if(length(cards) == 1 && is_pen(O)) var/datum/playingcard/P = cards[1] if(P.name != "Blank Card") - to_chat(user,"You cannot write on that card.") + to_chat(user,"You cannot write on this card.") return - var/t = rename_interactive(user, P, use_prefix = FALSE, actually_rename = FALSE) - if(t && P.name == "Blank Card") - P.name = t + var/card_text = rename_interactive(user, O, use_prefix = FALSE, actually_rename = FALSE) + if(card_text && P.name == "Blank Card") + P.name = card_text // SNOWFLAKE FOR CAG, REMOVE IF OTHER CARDS ARE ADDED THAT USE THIS. P.card_icon = "cag_white_card" update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) - else if(istype(O,/obj/item/cardhand)) + else if(istype(O, /obj/item/cardhand)) var/obj/item/cardhand/H = O - if(H.parentdeck == parentdeck) - H.concealed = concealed - cards.Add(H.cards) - qdel(H) - update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) + // if you're attacking a cardhand with one in hand, merge it into our deck. + // remember that "we" are the one being attacked here. + if(H.parent_deck_id == parent_deck_id) + if(!Adjacent(user)) + return + var/datum/playingcard/chosen = select_card_radial(user) + if(QDELETED(chosen)) + return + user.visible_message( + "[user] adds [concealed ? "a card" : chosen.name] to [user.p_their()] hand.", + "You add [chosen.name] to your deck.", + "You hear cards being shuffled together." + ) + H.transfer_card_to_self(src, chosen) return else to_chat(user, "You cannot mix cards from other decks!") return - ..() + return ..() /obj/item/cardhand/attack_self(mob/user) if(length(cards) == 1) turn_hand(user) return - user.set_machine(src) - interact(user) + var/datum/playingcard/card = select_card_radial(user) + if(QDELETED(card)) + return + remove_card(user, card) + +/mob/living/carbon/proc/get_active_card_hand() + var/obj/item/cardhand/hand = get_active_hand() + if(!istype(hand)) + hand = get_inactive_hand() + if(istype(hand)) + return hand + +/obj/item/cardhand/AltClick(mob/living/carbon/human/user) + . = ..() + if(!istype(user)) + return + var/obj/item/cardhand/active_hand = user.get_active_card_hand() + if(istype(active_hand) && active_hand == src) + user.set_machine(src) + interact(user) + return + // otherwise, it's somewhere else. We'll try to play a card to that hand. + if(!Adjacent(user)) + return + + if(!istype(active_hand)) + return + + if(active_hand.parent_deck_id != parent_deck_id) + to_chat(user, "These cards don't all come from the same deck!") + return + + + var/datum/playingcard/card_to_insert = active_hand.select_card_radial(user) + if(QDELETED(card_to_insert)) + return + + transfer_card_to_self(active_hand, card_to_insert) + user.visible_message( + "[user] moves [concealed ? "a card" : "[card_to_insert]"] to the other hand.", + "You move [concealed ? "a card" : "[card_to_insert]"] to the other hand.", + "You hear a card being drawn, followed by a card being added to a hand." + ) + + + +/obj/item/cardhand/CtrlClick(mob/user) + turn_hand(user) + +/obj/item/cardhand/AltShiftClick(mob/user) + . = ..() + if(!Adjacent(user) || HAS_TRAIT(user, TRAIT_HANDS_BLOCKED)) + return + shuffle_inplace(cards) + update_appearance(UPDATE_DESC|UPDATE_OVERLAYS) + playsound(user, 'sound/items/cardshuffle.ogg', 30, TRUE) + user.visible_message( + "[user] shuffles [user.p_their()] hand.", + "You shuffle your hand.", + "You hear cards shuffling." + ) + +/// Dragging a card to your hand will let you draw from it without picking it up. +/obj/item/cardhand/MouseDrop(atom/over, src_location, over_location, src_control, over_control, params) + if(!isliving(usr) || HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED)) + return + if(istype(over, /obj/item/deck)) + return over.MouseDrop_T(src, usr) + if(over != usr || !Adjacent(usr)) + return ..() + + to_chat(usr, "Select a card to draw from the hand.") + var/datum/playingcard/card_chosen = select_card_radial(usr) + if(QDELETED(card_chosen)) + return + + remove_card(usr, card_chosen) + +/obj/item/cardhand/MouseDrop_T(obj/item/I, mob/user) + // dropping our hand onto another + if(!istype(I, /obj/item/cardhand)) + return + if(!user.Adjacent(I) || !Adjacent(user)) + return + var/obj/item/cardhand/other_hand = I + if(other_hand.parent_deck_id != parent_deck_id) + to_chat(user, "These cards don't all come from the same deck!") + return + if(length(other_hand.cards) > 1) + var/response = tgui_alert(user, "Are you sure you want to merge [length(other_hand.cards)] cards into your currently held hand?", "Merge cards", list("Yes", "No")) + if(response != "Yes" || QDELETED(src) || QDELETED(other_hand) || HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) || !Adjacent(user)) + return + + add_cardhand_to_self(other_hand) + + +/// Open a radial menu to select a single card from a hand. +/obj/item/cardhand/proc/select_card_radial(mob/user) + if(!length(cards) || QDELETED(user) || !Adjacent(user)) + return + var/list/options = list() + for(var/datum/playingcard/P in cards) + if(isnull(options[P])) + options[P] = image(icon = 'icons/obj/playing_cards.dmi', icon_state = !concealed ? P.card_icon : P.back_icon) + + var/datum/playingcard/choice = show_radial_menu(user, src, options) + if(HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) || user.stat != CONSCIOUS || QDELETED(choice) || !(choice in cards) || !Adjacent(user)) + return + + return choice + /obj/item/cardhand/proc/turn_hand(mob/user) concealed = !concealed update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) user.visible_message( - "[user] [concealed ? "conceals" : "reveals"] their hand.", + "[user] [concealed ? "conceals" : "reveals"] [user.p_their()] hand.", "You [concealed ? "conceal" : "reveal"] your hand.", "You hear a hand of cards being flipped over." ) /obj/item/cardhand/interact(mob/user) var/dat = "You have:
" - for(var/t in cards) - dat += "The [t]
" + for(var/datum/playingcard/C in cards) + dat += "The [C]
" dat += "Which card will you remove next?
" dat += "Turn the hand over" var/datum/browser/popup = new(user, "cardhand", "Hand of Cards", 400, 240) popup.set_content(dat) popup.open() + /obj/item/cardhand/Topic(href, href_list) if(..()) return @@ -398,16 +856,30 @@ turn_hand(usr) else if(cardUser.get_item_by_slot(ITEM_SLOT_LEFT_HAND) == src || cardUser.get_item_by_slot(ITEM_SLOT_RIGHT_HAND) == src) - pickedcard = href_list["pick"] - Removecard() + var/datum/playingcard/picked_card = locateUID(href_list["pick"]) + if(istype(picked_card) && Adjacent(cardUser) && (picked_card in cards) && !QDELETED(src)) + remove_card(cardUser, picked_card) cardUser << browse(null, "window=cardhand") -/obj/item/cardhand/examine(mob/user) - . = ..() - if(!concealed && length(cards)) - . +="It contains:" - for(var/datum/playingcard/P in cards) - . +="the [P.name]." + +/obj/item/cardhand/point_at(atom/pointed_atom) + + if(!isturf(loc)) + return + + if(length(cards) != 1) + return ..() + + var/datum/playingcard/card = cards[1] + + if(!(pointed_atom in src) && !(pointed_atom.loc in src)) + return + + var/obj/effect/temp_visual/card_preview/draft = new /obj/effect/temp_visual/card_preview(usr, card.card_icon) + usr.vis_contents += draft + + QDEL_IN(draft, 0.6 SECONDS) + // Datum action here @@ -427,55 +899,91 @@ return if(istype(target, /obj/item/cardhand)) var/obj/item/cardhand/C = target - return C.Removecard() + return C.remove_card(owner) return ..() /datum/action/item_action/discard - name = "Discard - Place (a) card(s) from your hand in front of you." + name = "Discard - Place one or more cards from your hand in front of you." button_overlay_icon_state = "discard" use_itemicon = FALSE /datum/action/item_action/discard/Trigger(left_click) if(istype(target, /obj/item/cardhand)) var/obj/item/cardhand/C = target - return C.discard() + return C.discard(owner) return ..() // No more datum action here -/obj/item/cardhand/proc/Removecard() - var/mob/living/carbon/user = usr +/// Create a new card-hand from a list of cards in the other hand. +/obj/item/cardhand/proc/split(list/cards_in_new_hand) + if(length(cards) == 0 || length(cards_in_new_hand) == 0) + return + + var/obj/item/cardhand/new_hand = new() + for(var/datum/playingcard/card in cards_in_new_hand) + new_hand.cards += card + cards -= card + + new_hand.parent_deck_id = parent_deck_id + new_hand.update_values_from_cards(src) + new_hand.concealed = concealed + new_hand.update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) + + return new_hand + + - if(user.incapacitated() || !Adjacent(user)) +/// Draw a card from a card hand. +/// If a picked card isn't given, +/obj/item/cardhand/proc/remove_card(mob/living/carbon/user, datum/playingcard/picked_card) + + if(!istype(user) || HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) || !Adjacent(user)) return - var/pickablecards = list() - for(var/datum/playingcard/P in cards) - pickablecards[P.name] = P - if(!pickedcard) - pickedcard = tgui_input_list(usr, "Which card do you want to remove from the hand?", "Remove Card", pickablecards) - if(!pickedcard) + if(!picked_card) + var/pickablecards = list() + for(var/datum/playingcard/P in cards) + pickablecards[P.name] = P + var/selected_card = tgui_input_list(user, "Which card do you want to remove from the hand?", "Remove Card", pickablecards) + picked_card = pickablecards[selected_card] + if(!picked_card) return if(QDELETED(src)) return - var/datum/playingcard/card = pickablecards[pickedcard] - if(loc != user) // Don't want people teleporting cards + if(!Adjacent(user) || HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) || !ishuman(user)) // Don't want people teleporting cards return - user.visible_message( - "[user] draws a card from [user.p_their()] hand.", - "You take \the [pickedcard] from your hand.", - "You hear a card being drawn." - ) - pickedcard = null + + // let people draw cards from tables with this mechanism, as well as removing from their hand. + var/obj/item/cardhand/active_hand = user.get_active_card_hand() + if(user.l_hand == src || user.r_hand == src) + // if you're drawing a card from your left hand, you probably want it in your right. + user.visible_message( + "[user] draws [concealed ? "a card" : "[picked_card]"] from [user.p_their()] hand.", + "You take \the [picked_card] from your hand.", + "You hear a card being drawn." + ) + else if(istype(active_hand)) + // you're drawing from a hand the user isn't holding to one that the user is. + // try to put that card into our currently held hand. + active_hand.transfer_card_to_self(src, picked_card) + user.visible_message( + "[user] draws [concealed ? "a card" : "[picked_card]"] to [user.p_their()].", + "You draw \the [picked_card] to your hand.", + "You hear a card being drawn." + ) + return + var/obj/item/cardhand/H = new(get_turf(src)) - user.put_in_hands(H) - H.cards += card - cards -= card - H.parentdeck = parentdeck - H.update_values() + . = H + + H.cards += picked_card + cards -= picked_card + H.parent_deck_id = parent_deck_id + H.update_values_from_cards(src) H.concealed = concealed H.update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) if(!length(cards)) @@ -483,48 +991,46 @@ return update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) -/obj/item/cardhand/proc/discard() - var/mob/living/carbon/user = usr + user.put_in_hands(H) + +/obj/item/cardhand/proc/discard(mob/living/carbon/user) - var/maxcards = min(length(cards), 5) - var/discards = tgui_input_number(usr, "How many cards do you want to discard? You may discard up to [maxcards] card(s)", "Discard Cards", max_value = maxcards) - if(discards > maxcards) + if(!istype(user) || HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) || !Adjacent(user)) return - for(var/i in 1 to discards) - var/list/to_discard = list() - for(var/datum/playingcard/P in cards) - to_discard[P.name] = P - var/discarding = tgui_input_list(usr, "Which card do you wish to put down?", "Discard", to_discard) - if(!discarding) - continue + var/discards = tgui_input_number(user, "How many cards do you want to discard?", "Discard Cards", max_value = length(cards)) - if(loc != user) // Don't want people teleporting cards - return + if(!istype(user) || HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) || !Adjacent(user) || QDELETED(src)) + return + for(var/i in 1 to discards) + if(!length(cards)) + break + var/datum/playingcard/selected = select_card_radial(user) + if(!selected || !Adjacent(user) || HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) || QDELETED(src)) + break if(QDELETED(src)) return - var/datum/playingcard/card = to_discard[discarding] - to_discard.Cut() + var/obj/item/cardhand/new_hand = remove_card(user, selected) - var/obj/item/cardhand/H = new type(get_turf(src)) - H.cards += card - cards -= card - H.concealed = FALSE - H.parentdeck = parentdeck - H.update_values() - H.direction = user.dir - H.update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) + new_hand.direction = user.dir + new_hand.update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) if(length(cards)) update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_OVERLAYS) - if(length(H.cards)) + if(length(new_hand.cards)) user.visible_message( - "[user] plays \the [discarding].", - "You play \the [discarding].", + "[user] plays \the [selected].", + "You play \the [selected].", "You hear a card being played." ) - H.loc = get_step(user, user.dir) + user.unEquip(new_hand) + var/atom/drop_location = get_step(user, user.dir) + var/obj/item/cardhand/hand_on_the_table = locate(/obj/item/cardhand) in drop_location + if(istype(hand_on_the_table) && parent_deck_id == hand_on_the_table.parent_deck_id) + hand_on_the_table.add_cardhand_to_self(new_hand) + continue // get outtie since this qdels the hand + new_hand.forceMove(drop_location) if(!length(cards)) qdel(src) @@ -541,7 +1047,7 @@ /obj/item/cardhand/update_name() . = ..() if(length(cards) > 1) - name = "hand of [length(cards)] cards" + name = "hand of [length(cards)] card\s" else name = "playing card" diff --git a/code/modules/games/tarot.dm b/code/modules/games/tarot.dm index 6f0485d0da3f..b8e3c32d2677 100644 --- a/code/modules/games/tarot.dm +++ b/code/modules/games/tarot.dm @@ -15,7 +15,7 @@ /obj/item/deck/tarot/deckshuffle() var/mob/living/user = usr - if(cooldown < world.time - 1 SECONDS) + if(shuffle_cooldown < world.time - 1 SECONDS) var/list/newcards = list() while(length(cards)) var/datum/playingcard/P = pick(cards) @@ -27,9 +27,9 @@ cards = newcards playsound(user, 'sound/items/cardshuffle.ogg', 50, TRUE) user.visible_message( - "[user] shuffles [src].", + "[user] shuffles [src].", "You shuffle [src].", "You hear cards being shuffled." ) - cooldown = world.time + shuffle_cooldown = world.time diff --git a/code/modules/games/unum.dm b/code/modules/games/unum.dm index 5f4da0aa17a1..09e4e5f2096a 100644 --- a/code/modules/games/unum.dm +++ b/code/modules/games/unum.dm @@ -4,6 +4,19 @@ desc = "A deck of UNUM! cards. House rules to argue over not included." icon_state = "deck_unum_full" card_style = "unum" + /// Whether or not this deck should show the backs or fronts of its cards. + var/show_front = FALSE + +/obj/item/deck/unum/examine(mob/user) + . = ..() + . += "When held in hand, Alt-Shift-Click to flip [src]." + +/obj/item/deck/unum/AltShiftClick(mob/user) + if(!Adjacent(user) || (user.get_active_hand() != src) && (user.get_inactive_hand() != src)) + return + show_front = !show_front + visible_message("[user] flips over [src].") + update_appearance(UPDATE_ICON_STATE|UPDATE_OVERLAYS) /obj/item/deck/unum/build_deck() for(var/color in list("Red", "Yellow", "Green", "Blue")) @@ -21,6 +34,7 @@ /obj/item/deck/unum/update_icon_state() if(!length(cards)) icon_state = "deck_[card_style]_empty" + show_front = FALSE return var/percent = round((length(cards) / deck_total) * 100) switch(percent) @@ -31,3 +45,20 @@ else icon_state = "deck_[deck_style ? "[deck_style]_" : ""][card_style]_full" +/obj/item/deck/unum/update_overlays() + . = ..() + if(!length(cards) || !show_front) + return + var/percent = round((length(cards) / deck_total) * 100) + var/datum/playingcard/P = cards[1] + var/image/I = new(icon, P.card_icon) + switch(percent) + if(0 to 20) + I.pixel_y = 1 + if(21 to 50) + I.pixel_y = 2 + else + I.pixel_y = 4 + . += I + + diff --git a/code/modules/mob/living/carbon/human/human_inventory.dm b/code/modules/mob/living/carbon/human/human_inventory.dm index 56e1ca7d20e1..f84517791958 100644 --- a/code/modules/mob/living/carbon/human/human_inventory.dm +++ b/code/modules/mob/living/carbon/human/human_inventory.dm @@ -11,7 +11,6 @@ return l_hand if(istype(r_hand,typepath)) return r_hand - return 0 /mob/living/carbon/human/proc/has_organ(name)