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)