Files
Leland KembleandGitHub 06608081d2 Moves a greater manyer morer things from attackby() to item_interaction() (#96739)
## About The Pull Request

50 files, plus additonal file that was necessary because I slightly
refactored windoor construction. This ended up like three and a half
times larger than most of the other ones, I really wasn't meaning for
that to happen. Maybe its because I more vigorously enforced the empty
line after every return thing. The only way to find out is to read 1700
lines of changes, and I don't feel like doing that again. I pinky
promise it all works, and I've tested every item changed individually.

Changes beyond conversion:
You can't welder down reinforced falsewalls anymore, pretty sure it was
an oversight because they're supposed to be taken down by wirecutters
You can right click to close gun lockers with an empty hand, because
otherwise you can't close the lockers at all if there's a gun in them
Meatspikes no longer runtime on construction due to the old trick of
balloon alert + qdel
Windoor assembly no longer SUCKS DICK!!, it's no longer storing
essentially a boolean in an undocumented number in a string, it no
longer requires a VERB to flip its direction.
on a related note, the creator of the windoor assembly construction
steps has been sent to live on a farm far out in the country

Also, a quick note about how these prs are gonna fuck everything up
until they're all together. There's a lot more places where `attackby()`
is directly called than I would have assumed, and given as those are
being changed to call `item_interaction()` potentially(usually) in a
different pr than the pr in which that atom's `attackby()` is actually
converted, that function's not going to work at all until both are
merged. The good news is that almost every instance this is happening,
it's pure convenience, where the user would have access to both items
anyway and could just manually call the attack chain by clicking them
together. Instances where this is not the case are being skipped over
until they can be packaged together to avoid the issue, but I'm not
bothering to do this on ones where the desync won't make an interaction
actually unusable, because that would require me to turn a 1700 line pr
into a 3500 line pr and I really was aiming for like 500

## Why It's Good For The Game

Ignore the paragraph about how I expect this to create a hostage
situation where the only fix to the bugs I make is to merge my other
prs, and instead think about how cool swing combat would be and how much
you want it

## Changelog
🆑
fix: you now can only use a wirecutter to take down a reinforced
falsewall
fix: meatspikes no longer runtime on construction
fix: you no longer have to use a verb that you can't use to flip windoor
assemblies

qol: you can right click a gun locker to close it

code: 50 files have been moved from attackby() to item_interaction()
/🆑
2026-07-16 15:07:43 +02:00

531 lines
19 KiB
Plaintext

#define TAPPED_ANGLE 90
#define UNTAPPED_ANGLE 0
/obj/item/tcgcard
name = "Coder"
desc = "Wow, a mint condition coder card! Better tell the GitHub all about this!"
icon = DEFAULT_TCG_DMI_ICON
icon_state = "runtime"
w_class = WEIGHT_CLASS_TINY
///Unique ID, for use in lookups and storage, used to index the global datum list where the rest of the card's info is stored
var/id = "code"
///Used along with the id for lookup
var/series = "coderbus"
///Is the card flipped?
var/flipped = FALSE
///Has this card been "tapped"? AKA, is it horizontal?
var/tapped = FALSE
///Cached icon used for inspecting the card
var/icon/cached_flat_icon
/obj/item/tcgcard/Initialize(mapload, datum_series, datum_id)
. = ..()
AddElement(/datum/element/item_scaling, 0.3, 1)
//If they are passed as null let's replace them with the vars on the card. this also means we can allow for map loaded ccards
if(!datum_series)
datum_series = series
if(!datum_id)
datum_id = id
var/list/temp_list = SStrading_card_game.cached_cards[datum_series]
if(!temp_list)
return
var/datum/card/temp = temp_list["ALL"][datum_id]
if(!temp)
return
name = temp.name
desc = "<i>[temp.desc]</i>"
icon = icon(temp.icon)
icon_state = temp.icon_state
id = temp.id
series = temp.series
// This totally isn't overengineered to hell, shut up
/**
* Alright so some brief details here, we store all "immutable" (Think like power) card variables in a global list, indexed by id
* This proc gets the card's associated card datum to play with
*/
/obj/item/tcgcard/proc/extract_datum()
var/list/cached_cards = SStrading_card_game.cached_cards[series]
if(!cached_cards)
return null
if(!cached_cards["ALL"][id])
CRASH("A card without a datum has appeared, either the global list is empty, or you fucked up bad. Series{[series]} ID{[id]} Len{[SStrading_card_game.cached_cards.len]}")
return cached_cards["ALL"][id]
/obj/item/tcgcard/get_name_chaser(mob/user, list/name_chaser = list())
if(flipped)
return ..()
var/datum/card/data_holder = extract_datum()
name_chaser += "Faction: [data_holder.faction]"
name_chaser += "Cost: [data_holder.summoncost]"
name_chaser += "Type: [data_holder.cardtype] - [data_holder.cardsubtype]"
name_chaser += "Power/Resolve: [data_holder.power]/[data_holder.resolve]"
if(data_holder.rules) //This can sometimes be empty
name_chaser += "Ruleset: [data_holder.rules]"
name_chaser += list("[icon2html(get_cached_flat_icon(), user, "extra_classes" = "hugeicon")]")
return name_chaser
/obj/item/tcgcard/proc/get_cached_flat_icon()
if(!cached_flat_icon)
cached_flat_icon = getFlatIcon(src)
return cached_flat_icon
GLOBAL_LIST_EMPTY(tcgcard_radial_choices)
/obj/item/tcgcard/attack_hand(mob/user, list/modifiers)
if(!isturf(loc))
return ..()
var/list/choices = GLOB.tcgcard_radial_choices
if(!length(choices))
choices = GLOB.tcgcard_radial_choices = list(
"Pickup" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_pickup"),
"Tap" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_tap"),
"Flip" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_flip"),
)
var/choice = show_radial_menu(user, src, choices, custom_check = CALLBACK(src, PROC_REF(check_menu), user), require_near = TRUE, tooltips = TRUE)
if(!check_menu(user))
return
switch(choice)
if("Tap")
tap_card(user)
if("Pickup")
user.put_in_hands(src)
if("Flip")
flip_card(user)
if(null)
return
/obj/item/tcgcard/attack_self(mob/user)
. = ..()
flip_card(user)
/obj/item/tcgcard/update_name(updates)
. = ..()
if(!flipped)
var/datum/card/template = extract_datum()
name = template.name
else
name = "Trading Card"
/obj/item/tcgcard/update_desc(updates)
. = ..()
if(!flipped)
var/datum/card/template = extract_datum()
desc = "<i>[template.desc]</i>"
else
desc = "It's the back of a trading card... no peeking!"
/obj/item/tcgcard/update_icon_state()
if(flipped)
icon_state = "cardback"
return ..()
var/datum/card/template = extract_datum()
if(!template)
return
icon_state = template.icon_state
return ..()
/obj/item/tcgcard/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
if(istype(tool, /obj/item/tcgcard))
var/obj/item/tcgcard/second_card = tool
var/obj/item/tcgcard_deck/new_deck = new /obj/item/tcgcard_deck(drop_location())
new_deck.flipped = flipped
user.transferItemToLoc(second_card, new_deck)//Start a new pile with both cards, in the order of card placement.
user.transferItemToLoc(src, new_deck)
new_deck.update_icon_state()
user.put_in_hands(new_deck)
return ITEM_INTERACT_SUCCESS
if(istype(tool, /obj/item/tcgcard_deck))
var/obj/item/tcgcard_deck/old_deck = tool
if(length(old_deck.contents) >= 30)
to_chat(user, span_notice("This pile has too many cards for a regular deck!"))
return ITEM_INTERACT_BLOCKING
user.transferItemToLoc(src, old_deck)
flipped = old_deck.flipped
old_deck.update_appearance()
update_appearance()
return ITEM_INTERACT_SUCCESS
return NONE
/obj/item/tcgcard/proc/check_menu(mob/living/user)
if(!istype(user))
return FALSE
if(user.incapacitated || !user.Adjacent(src))
return FALSE
return TRUE
/obj/item/tcgcard/proc/tap_card(mob/user)
var/matrix/ntransform = matrix(transform)
if(tapped)
ntransform.TurnTo(TAPPED_ANGLE , UNTAPPED_ANGLE)
else
ntransform.TurnTo(UNTAPPED_ANGLE , TAPPED_ANGLE)
tapped = !tapped
animate(src, transform = ntransform, time = 2, easing = SINE_EASING)
/obj/item/tcgcard/proc/flip_card(mob/user)
to_chat(user, span_notice("You turn the card over."))
if(!flipped)
name = "Trading Card"
desc = "It's the back of a trading card... no peeking!"
icon_state = "cardback"
else
var/datum/card/template = extract_datum()
name = template.name
desc = "<i>[template.desc]</i>"
icon_state = template.icon_state
flipped = !flipped
/**
* A stack item that's not actually a stack because ORDER MATTERS with a deck of cards!
* The "top" card of the deck will always be the bottom card in the stack for our purposes.
*/
/obj/item/tcgcard_deck
name = "Trading Card Pile"
desc = "A stack of TCG cards."
icon = 'icons/obj/toys/tcgmisc.dmi'
icon_state = "deck_up"
base_icon_state = "deck"
obj_flags = UNIQUE_RENAME
spawn_blacklisted = TRUE
var/flipped = FALSE
var/static/radial_draw = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_draw")
var/static/radial_shuffle = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_shuffle")
var/static/radial_pickup = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_pickup")
/obj/item/tcgcard_deck/Initialize(mapload)
. = ..()
create_storage(storage_type = /datum/storage/tcg)
RegisterSignal(atom_storage, COMSIG_STORAGE_REMOVED_ITEM, PROC_REF(on_item_removed))
/obj/item/tcgcard_deck/update_icon_state()
if(!flipped)
icon_state = "[base_icon_state]_up"
return ..()
switch(contents.len)
if(1 to 10)
icon_state = "[base_icon_state]_tcg_low"
if(11 to 20)
icon_state = "[base_icon_state]_tcg_half"
if(21 to INFINITY)
icon_state = "[base_icon_state]_tcg_full"
else
icon_state = "[base_icon_state]_tcg"
return ..()
/obj/item/tcgcard_deck/examine(mob/user)
. = ..()
. += span_notice("\The [src] has [contents.len] cards inside.")
/obj/item/tcgcard_deck/attack_hand(mob/user, list/modifiers)
var/list/choices = list(
"Draw" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_draw"),
"Shuffle" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_shuffle"),
"Pickup" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_pickup"),
"Flip" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_flip"),
)
var/choice = show_radial_menu(user, src, choices, custom_check = CALLBACK(src, PROC_REF(check_menu), user), require_near = TRUE, tooltips = TRUE)
if(!check_menu(user))
return
switch(choice)
if("Draw")
draw_card(user)
if("Shuffle")
shuffle_deck(user)
if("Pickup")
user.put_in_hands(src)
if("Flip")
flip_deck()
if(null)
return
/obj/item/tcgcard_deck/Destroy()
for(var/card in 1 to contents.len)
var/obj/item/tcgcard/stored_card = contents[card]
stored_card.forceMove(drop_location())
. = ..()
/obj/item/tcgcard_deck/proc/check_menu(mob/living/user)
if(!istype(user))
return FALSE
if(user.incapacitated || !user.Adjacent(src))
return FALSE
return TRUE
/obj/item/tcgcard_deck/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
if(!istype(tool, /obj/item/tcgcard))
return NONE
if(contents.len >= 30)
to_chat(user, span_notice("This pile has too many cards for a regular deck!"))
return ITEM_INTERACT_BLOCKING
var/obj/item/tcgcard/new_card = tool
new_card.flipped = flipped
new_card.forceMove(src)
return ITEM_INTERACT_SUCCESS
/obj/item/tcgcard_deck/attack_self(mob/living/carbon/user)
shuffle_deck(user)
return ..()
/**
* The user draws a single card. The deck is then handled based on how many cards are left.
*/
/obj/item/tcgcard_deck/proc/draw_card(mob/user)
if(!contents.len)
CRASH("A TCG deck was created with no cards inside of it.")
var/obj/item/tcgcard/drawn_card = contents[contents.len]
user.put_in_hands(drawn_card)
drawn_card.flipped = flipped //If it's a face down deck, it'll be drawn face down, if it's a face up pile you'll draw it face up.
drawn_card.update_icon_state()
user.visible_message(span_notice("[user] draws a card from \the [src]!"), \
span_notice("You draw a card from \the [src]!"))
if(contents.len <= 1)
var/obj/item/tcgcard/final_card = contents[1]
user.transferItemToLoc(final_card, drop_location())
qdel(src)
/**
* The user shuffles the order of the deck, then closes any visability into the deck's storage to prevent cheesing.
* *User: The person doing the shuffling, used in visable message and closing UI.
* *Visible: Will anyone need to hear the visable message about the shuffling?
*/
/obj/item/tcgcard_deck/proc/shuffle_deck(mob/user, visable = TRUE)
if(!contents)
return
contents = shuffle(contents)
if(user.active_storage)
user.active_storage.hide_contents(user)
if(visable)
user.visible_message(span_notice("[user] shuffles \the [src]!"), \
span_notice("You shuffle \the [src]!"))
/**
* The user flips the deck, turning it into a face up/down pile, and reverses the order of the cards from top to bottom.
*/
/obj/item/tcgcard_deck/proc/flip_deck()
flipped = !flipped
var/list/temp_deck = contents.Copy()
contents = reverse_range(temp_deck)
//Now flip the cards to their opposite positions.
for (var/obj/item/tcgcard/nu_card as anything in contents)
nu_card.flipped = flipped
nu_card.update_icon_state()
update_icon_state()
/**
* Signal handler for COMSIG_STORAGE_REMOVED_ITEM. Qdels src if contents are empty, flips the removed card if needed.
*/
/obj/item/tcgcard_deck/proc/on_item_removed(datum/storage/storage_datum, obj/item/thing, atom/remove_to_loc, silent = FALSE)
SIGNAL_HANDLER
if (!istype(thing, /obj/item/tcgcard))
return
var/obj/item/tcgcard/card = thing
card.flipped = flipped
card.update_appearance(UPDATE_ICON_STATE)
if(length(contents) == 0)
qdel(src)
/obj/item/cardpack
name = "Trading Card Pack: Coder"
desc = "Contains six complete fuckups by the coders. Report this on GitHub please!"
icon = 'icons/obj/toys/tcgmisc.dmi'
icon_state = "error"
w_class = WEIGHT_CLASS_TINY
custom_price = PAYCHECK_CREW * 0.75 //Price reduced from * 2 to * 0.75, this is planned as a temporary measure until card persistence is added.
///The card series to look in
var/series = "MEME"
///Chance of the pack having a coin in it out of 10
var/contains_coin = -1
///The amount of cards to draw from the rarity table
var/card_count = 5
///The rarity table, the set must contain at least one of each
var/list/rarity_table = list(
"common" = 900,
"uncommon" = 300,
"rare" = 100,
"epic" = 30,
"legendary" = 5,
"misprint" = 1)
///The amount of cards to draw from the guaranteed rarity table
var/guaranteed_count = 1
///The guaranteed rarity table, acts about the same as the rarity table. it can have as many or as few raritys as you'd like
var/list/guar_rarity = list(
"legendary" = 1,
"epic" = 9,
"rare" = 30,
"uncommon" = 60)
var/drop_all_cards = FALSE
/obj/item/cardpack/series_one
name = "Trading Card Pack: Series 1"
desc = "Contains six cards of varying rarity from the 2560 Core Set. Collect them all!"
icon_state = "cardpack_series1"
series = "coreset2020"
contains_coin = 10
/obj/item/cardpack/resin
name = "Trading Card Pack: Resin Frontier Booster Pack"
desc = "Contains six cards of varying rarity from the Resin Frontier set. Collect them all!"
icon_state = "cardpack_resin"
series = "resinfront"
contains_coin = 0
rarity_table = list(
"common" = 900,
"uncommon" = 300,
"rare" = 100,
"epic" = 30,
"legendary" = 5)
/obj/item/cardpack/Initialize(mapload)
. = ..()
AddElement(/datum/element/item_scaling, 0.4, 1)
rarity_table = SStrading_card_game.get_rarity_table(type, rarity_table)
guar_rarity = SStrading_card_game.get_guarenteed_rarity_table(type, guar_rarity)
/obj/item/cardpack/attack_self(mob/user)
. = ..()
var/list/cards
if(drop_all_cards)
cards = SStrading_card_game.cached_cards[series]["ALL"]
else
cards = buildCardListWithRarity(card_count, guaranteed_count)
for(var/template in cards)
//Makes a new card based of the series of the pack.
new /obj/item/tcgcard(get_turf(user), series, template)
to_chat(user, span_notice("Wow! Check out these cards!"))
new /obj/effect/decal/cleanable/wrapping(get_turf(user))
playsound(loc, 'sound/items/poster/poster_ripped.ogg', 20, TRUE)
if(prob(contains_coin))
to_chat(user, span_notice("...and it came with a flipper, too!"))
new /obj/item/coin/thunderdome(get_turf(user))
qdel(src)
/obj/item/coin/thunderdome
name = "\improper TGC Flipper"
desc = "A TGC flipper, for deciding who gets to go first. Also conveniently acts as a counter, for various purposes."
icon = 'icons/obj/toys/tcgmisc.dmi'
icon_state = "coin_nanotrasen"
custom_materials = list(/datum/material/plastic = SMALL_MATERIAL_AMOUNT*5)
material_flags = NONE
sideslist = list("nanotrasen", "syndicate")
override_material_worth = TRUE
/obj/item/storage/card_binder
name = "card binder"
desc = "The perfect way to keep your collection of cards safe and valuable."
icon = 'icons/obj/toys/tcgmisc.dmi'
icon_state = "binder"
inhand_icon_state = "album"
lefthand_file = 'icons/mob/inhands/items/books_lefthand.dmi'
righthand_file = 'icons/mob/inhands/items/books_righthand.dmi'
resistance_flags = FLAMMABLE //burn your enemies' collections, for only you can Collect Them All!
w_class = WEIGHT_CLASS_SMALL
flags_1 = PREVENT_CONTENTS_EXPLOSION_1
storage_type = /datum/storage/card_binder
///Returns a list of cards ids of card_cnt weighted by rarity from the pack's tables that have matching series, with gnt_cnt of the guaranteed table.
/obj/item/cardpack/proc/buildCardListWithRarity(card_cnt, rarity_cnt)
var/list/toReturn = list()
//You can always get at least one of some rarity
toReturn += returnCardsByRarity(rarity_cnt, guar_rarity)
toReturn += returnCardsByRarity(card_cnt, rarity_table)
return toReturn
///Returns a list of card datums of the length cardCount that match a random rarity weighted by rarity_table[]
/obj/item/cardpack/proc/returnCardsByRarity(cardCount, list/rarity_table)
var/list/toReturn = list()
for(var/card in 1 to cardCount)
var/rarity = 0
//Some number between 1 and the sum of all values in the list
var/weight = 0
for(var/chance in rarity_table)
weight += rarity_table[chance]
var/random = rand(weight)
for(var/bracket in rarity_table)
//Steals blatantly from pick_weight(), sorry buddy I need the index
random -= rarity_table[bracket]
if(random <= 0)
rarity = bracket
break
//What we're doing here is using the cached the results of the rarity we find.
//This allows us to only have to run this once per rarity, ever.
//Unless you reload the cards of course, in which case we have to do this again.
var/list/cards = SStrading_card_game.cached_cards[series][rarity]
if(cards.len)
toReturn += pick(cards)
else
//If we still don't find anything yell into the void. Lazy coders.
log_runtime("The index [rarity] of rarity_table does not exist in the global cache")
return toReturn
//All of these values should be overridden by either a template or a card itself
/datum/card
///Unique ID, for use in lookups and (eventually) for persistence. MAKE SURE THIS IS UNIQUE FOR EACH CARD IN AS SERIES, OR THE ENTIRE SYSTEM WILL BREAK, AND I WILL BE VERY DISAPPOINTED.
var/id = "coder"
var/name = "Coder"
var/desc = "Wow, a mint condition coder card! Better tell the GitHub all about this!"
///This handles any extra rules for the card, i.e. extra attributes, special effects, etc. If you've played any other card game, you know how this works.
var/rules = "There are no rules here. There is no escape. No Recall or Intervention can work in this place."
var/icon = DEFAULT_TCG_DMI
var/icon_state = "template"
///What it costs to summon this card to the battlefield.
var/summoncost = -1
///How hard this card hits (by default)
var/power = -1
///How hard this card can get hit (by default)
var/resolve = -1
///Someone please come up with a ruleset so I can comment this
var/faction = "socks"
///Used to define the behaviour the card uses during the game.
var/cardtype ="C43a7u43?"
///An extra descriptor for the card. Combined with the cardtype for a larger card descriptor, i.e. Creature- Xenomorph, Spell- Instant, that sort of thing. For creatures, this has no effect, for spells, this is important.
var/cardsubtype = "Weeb"
///Defines the series that the card originates from, this is *very* important for spawning the cards via packs.
var/series = "hunter2"
///The rarity of this card, determines how much (or little) it shows up in packs. Rarities are common, uncommon, rare, epic, legendary and misprint.
var/rarity = "uber rare to the extreme"
///Icon file that summons are pulled from
var/summon_icon_file = "icons/obj/toys/tcgmisc.dmi"
///Icon state for summons to use
var/summon_icon_state = "template"
/datum/card/New(list/data = list(), list/templates = list())
applyTemplates(data, templates)
apply(data)
applyKeywords(data | templates)
///For each var that the card datum and the json entry share, we set the datum var to the json entry
/datum/card/proc/apply(list/data)
for(var/name in (data & vars))
vars[name] = data[name]
///Applies a json file to a card datum
/datum/card/proc/applyTemplates(list/data, list/templates = list())
apply(templates["default"])
apply(templates[data["template"]])
///Searches for keywords in the card's variables, marked by wrapping them in {$}
///Adds on hovor logic to them, using the passed in list
///We use the changed_vars list just to make the var searching faster
/datum/card/proc/applyKeywords(list/changed_vars)
for(var/name in (changed_vars & vars))
var/value = vars[name]
if(!istext(value))
continue
vars[name] = SStrading_card_game.resolve_keywords(value)
#undef TAPPED_ANGLE
#undef UNTAPPED_ANGLE