From 2cbbbae8c46fbfb1758738f43e923a3976d754ff Mon Sep 17 00:00:00 2001 From: DagenbouFree Date: Fri, 22 May 2026 13:08:59 +0700 Subject: [PATCH 01/21] Pliant Soul Port From TA Ports TAT(Pliant Soul) system from TA to CC. A framework for custom classes that should help reduce subclass bloat --- code/__DEFINES/traits.dm | 6 + code/modules/admin/admin_verbs.dm | 1 + code/modules/client/preferences.dm | 11 + code/modules/client/preferences_savefile.dm | 9 + .../mob/living/carbon/human/examine.dm | 6 +- code/modules/mob/living/carbon/life.dm | 8 + code/modules/mob/living/combat/parry.dm | 5 +- .../tat_system/_defines/tat_defines_items.dm | 760 ++++++ .../tat_system/_defines/tat_defines_skills.dm | 203 ++ .../tat_system/_defines/tat_defines_stats.dm | 22 + .../tat_system/_defines/tat_defines_traits.dm | 526 ++++ .../datums/tat_system/core/tat_admin_panel.dm | 324 +++ .../code/datums/tat_system/core/tat_bans.dm | 405 +++ .../code/datums/tat_system/core/tat_build.dm | 833 ++++++ .../code/datums/tat_system/core/tat_slot.dm | 55 + .../code/datums/tat_system/core/tat_ui.dm | 789 ++++++ .../datums/tat_system/domains/tat_items.dm | 1978 +++++++++++++++ .../tat_system/domains/tat_party_leader.dm | 169 ++ .../datums/tat_system/domains/tat_skills.dm | 676 +++++ .../datums/tat_system/domains/tat_stats.dm | 154 ++ .../domains/tat_trader_lootboxes.dm | 597 +++++ .../datums/tat_system/domains/tat_traits.dm | 1204 +++++++++ .../code/datums/tat_system/tat_defines.dm | 40 + .../roguetown/adventurer/adventurer.dm | 5 + .../job_types/roguetown/adventurer/wretch.dm | 5 + .../job_types/roguetown/pilgrim/pilgrim.dm | 5 + .../roguetown/tat_build/tat_class.dm | 210 ++ .../jobs/job_types/roguetown/trader/trader.dm | 5 + roguetown.dme | 23 + tgui/packages/tgui/interfaces/TATBuild.tsx | 2248 +++++++++++++++++ .../tgui/interfaces/TATBuild.tsx\342\200\216" | 2248 +++++++++++++++++ .../tgui/interfaces/TatRoleLocksPanel.tsx | 274 ++ tgui/packages/tgui/layouts/Window.tsx | 3 +- .../tat_defines_items.dm\342\200\216" | 787 ++++++ .../_defines/tat_defines_skills.dm" | 203 ++ .../tat_defines_stats.dm\342\200\216" | 22 + .../_defines/tat_defines_traits.dm" | 535 ++++ .../tat_system/core/tat_admin_panel.dm" | 324 +++ .../tat_system/core/tat_bans.dm\342\200\216" | 420 +++ .../code/datums/tat_system/core/tat_build.dm" | 843 +++++++ .../tat_system/core/tat_slot.dm\342\200\216" | 55 + .../tat_system/core/tat_ui.dm\342\200\216" | 789 ++++++ .../domains/tat_items.dm\342\200\216" | 2052 +++++++++++++++ .../domains/tat_party_leader.dm\342\200\216" | 169 ++ .../domains/tat_skills.dm\342\200\216" | 680 +++++ .../datums/tat_system/domains/tat_stats.dm" | 154 ++ .../tat_trader_lootboxes.dm\342\200\216" | 608 +++++ .../domains/tat_traits.dm\342\200\216" | 1285 ++++++++++ .../code/datums/tat_system/tat_defines.dm" | 40 + .../adventurer/adventurer.dm\342\200\216" | 5 + .../adventurer/wretch.dm\342\200\216" | 5 + .../roguetown/pilgrim/pilgrim.dm\342\200\216" | 5 + .../tat_build/tat_class.dm\342\200\216" | 210 ++ .../roguetown/trader/trader.dm\342\200\216" | 5 + 54 files changed, 22999 insertions(+), 4 deletions(-) create mode 100644 modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_items.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_skills.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_stats.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_traits.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/core/tat_admin_panel.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/core/tat_bans.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/core/tat_build.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/core/tat_slot.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/core/tat_ui.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/domains/tat_items.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/domains/tat_party_leader.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/domains/tat_skills.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/domains/tat_stats.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/domains/tat_trader_lootboxes.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/domains/tat_traits.dm create mode 100644 modular_twilight_axis/code/datums/tat_system/tat_defines.dm create mode 100644 modular_twilight_axis/code/modules/jobs/job_types/roguetown/adventurer/adventurer.dm create mode 100644 modular_twilight_axis/code/modules/jobs/job_types/roguetown/adventurer/wretch.dm create mode 100644 modular_twilight_axis/code/modules/jobs/job_types/roguetown/pilgrim/pilgrim.dm create mode 100644 modular_twilight_axis/code/modules/jobs/job_types/roguetown/tat_build/tat_class.dm create mode 100644 modular_twilight_axis/code/modules/jobs/job_types/roguetown/trader/trader.dm create mode 100644 tgui/packages/tgui/interfaces/TATBuild.tsx create mode 100644 "tgui/packages/tgui/interfaces/TATBuild.tsx\342\200\216" create mode 100644 tgui/packages/tgui/interfaces/TatRoleLocksPanel.tsx create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_items.dm\342\200\216" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_skills.dm" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_stats.dm\342\200\216" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_traits.dm" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/core/tat_admin_panel.dm" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/core/tat_bans.dm\342\200\216" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/core/tat_build.dm" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/core/tat_slot.dm\342\200\216" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/core/tat_ui.dm\342\200\216" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/domains/tat_items.dm\342\200\216" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/domains/tat_party_leader.dm\342\200\216" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/domains/tat_skills.dm\342\200\216" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/domains/tat_stats.dm" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/domains/tat_trader_lootboxes.dm\342\200\216" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/domains/tat_traits.dm\342\200\216" create mode 100644 "\342\200\216modular_twilight_axis/code/datums/tat_system/tat_defines.dm" create mode 100644 "\342\200\216modular_twilight_axis/code/modules/jobs/job_types/roguetown/adventurer/adventurer.dm\342\200\216" create mode 100644 "\342\200\216modular_twilight_axis/code/modules/jobs/job_types/roguetown/adventurer/wretch.dm\342\200\216" create mode 100644 "\342\200\216modular_twilight_axis/code/modules/jobs/job_types/roguetown/pilgrim/pilgrim.dm\342\200\216" create mode 100644 "\342\200\216modular_twilight_axis/code/modules/jobs/job_types/roguetown/tat_build/tat_class.dm\342\200\216" create mode 100644 "\342\200\216modular_twilight_axis/code/modules/jobs/job_types/roguetown/trader/trader.dm\342\200\216" diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 62241ecc1c4..60cfaca8ec1 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -312,6 +312,10 @@ #define TRAIT_MASTER_MASON "Master Masonry" #define TRAIT_FOOD_STIPEND "Vomitorium-known" +//duds for TAT system +#define TRAIT_OUTLANDER "Outlander" +#define TRAIT_PARRYEXPERT "Parry Expert"// CC + TA TAT system perk, maybe port related ronin class if needed + // If you want description to show up you gotta have the trait name defined BEFORE this lol GLOBAL_LIST_INIT(roguetraits, list( @@ -570,6 +574,8 @@ GLOBAL_LIST_INIT(roguetraits, list( TRAIT_ROOT_WALKER = span_info("After offering lux, I can now travel along heartroot trees."), TRAIT_WHITE_STAG = span_info("The power of the white stag lives on inside of me!"), TRAIT_EDIT_DESCRIPTORS = span_info("I can change my appearance at a magic mirror in a thorough manner."), + TRAIT_OUTLANDER = span_info("The locals see me as not of their land."), + TRAIT_PARRYEXPERT = span_info("I am much better at parrying incoming strikes, having a more high probability of deflecting a blow with my weapon."), )) // trait accessor defines diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index c95edc9f920..177f6c25a1a 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -117,6 +117,7 @@ GLOBAL_PROTECT(admin_verbs_admin) GLOBAL_LIST_INIT(admin_verbs_ban, list( /client/proc/unban_panel, /client/proc/ban_panel, + /client/proc/tat_role_locks_panel, /client/proc/stickybanpanel, /client/proc/check_pq, /client/proc/adjust_pq, diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index fc34bf6c3e6..d2aadaa8d55 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -261,10 +261,13 @@ GLOBAL_LIST_EMPTY(chosen_names) var/audio_preload var/preloaded = FALSE //Bool Check + var/datum/tat_build/tat_build + /datum/preferences/New(client/C) parent = C migrant = new /datum/migrant_pref(src) familiar_prefs = new /datum/familiar_prefs(src) + tat_build = new(src) //CC + TA edit for(var/custom_name_id in GLOB.preferences_custom_names) custom_names[custom_name_id] = get_default_name(custom_name_id) @@ -715,6 +718,8 @@ GLOBAL_LIST_EMPTY(chosen_names) dat += "Clear Gallery" dat += "
Preview Examine" + dat += "
Pliant Soul Settings: Change" //CC + TA edit + dat += "
Loadout: Open Menu" dat += "" dat += "" @@ -2376,6 +2381,12 @@ Slots: [job.spawn_positions] [job.round_contrib_points ? "RCP: +[job.round_contr var/datum/loadout_menu/LM = new(user.client) LM.ui_interact(user) return + + //CC + TA edit + if("tat_build") + tat_build.ui_interact(user) + //CC + TA edit end + if("vampire_hair") var/new_vampirehair = input(user, "Choose your character's vampire hair color:", "Character Preference","#"+vampire_hair) as color|null if(new_vampirehair) diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index 17278952589..31f5c543f43 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -228,7 +228,13 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car ///Caustic edit end // Custom hotkeys S["key_bindings"] >> key_bindings + //CC + TA edit + var/list/L + S["tat_build"] >> L + if(!tat_build) + tat_build = new(src) + //CC + TA edit end //try to fix any outdated data if necessary if(needs_update >= 0) update_preferences(needs_update, S) //needs_update = savefile_version if we need an update (positive integer) @@ -272,6 +278,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car pda_color = sanitize_hexcolor(pda_color, 6, 1, initial(pda_color)) key_bindings = sanitize_islist(key_bindings, list()) masked_examine = sanitize_integer(masked_examine, 0, 1, initial(masked_examine)) + tat_build.load_from_list(sanitize_islist(L, list())) //CC + TA edit //ROGUETOWN parallax = PARALLAX_INSANE @@ -379,6 +386,8 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car WRITE_FILE(S["belch_noises"], belch_noises) WRITE_FILE(S["audio_preload"], audio_preload) ///Caustic edit end + + WRITE_FILE(S["tat_build"], tat_build.export_to_list()) //CC + TA edit return TRUE diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm index d4808c33a2b..1884479aea7 100644 --- a/code/modules/mob/living/carbon/human/examine.dm +++ b/code/modules/mob/living/carbon/human/examine.dm @@ -72,6 +72,8 @@ on_examine_face(user) var/used_name = name var/used_title = get_role_title() + if(tat_pliant_title) // CC + TA edit + used_title = tat_pliant_title // CC + TA edit if(SSticker.regentmob == src) used_title = "[used_title]" + " Regent" var/display_as_wanderer = FALSE @@ -96,14 +98,14 @@ displayed_headshot = src.headshot_link if ((valid_headshot_link(src, displayed_headshot, TRUE)) && (user.client?.prefs.chatheadshot)) - if(display_as_wanderer) + if(display_as_wanderer && !tat_pliant_title) // CC + TA edit . = list(span_info("ø ------------ ø\n\nThis is [used_name], the wandering [race_name].")) else if(used_title) . = list(span_info("ø ------------ ø\n\nThis is [used_name], the [race_name] [used_title].")) else . = list(span_info("ø ------------ ø\n\nThis is the [used_name], the [race_name].")) else - if(display_as_wanderer) + if(display_as_wanderer && !tat_pliant_title) // CC + TA edit . = list(span_info("ø ------------ ø\nThis is [used_name], the wandering [race_name].")) else if(used_title) . = list(span_info("ø ------------ ø\nThis is [used_name], the [race_name] [used_title].")) diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm index 9d2addb9840..6c8470200e5 100644 --- a/code/modules/mob/living/carbon/life.dm +++ b/code/modules/mob/living/carbon/life.dm @@ -641,6 +641,14 @@ GLOBAL_LIST_INIT(ballmer_windows_me_msg, list("Yo man, what if, we like, uh, put if(armor_blocked && !fallingas) to_chat(src, span_warning("I can't sleep like this. My armor is burdening me.")) fallingas = TRUE + // CC + TA edit + if(HAS_TRAIT(src, TRAIT_NUDE_SLEEPER) && (H.wear_armor || H.wear_pants || H.wear_shirt || H.wear_wrists || H.cloak || H.gloves || H.shoes)) + message = "I am unable to sleep in clothes. I should remove them." + if(!fallingas) + to_chat(src, span_warning(message)) + fallingas = TRUE + return + // CC + TA edit end if(!armor_blocked) if (sleepy_mod > 1) sleep_threshold = 30 diff --git a/code/modules/mob/living/combat/parry.dm b/code/modules/mob/living/combat/parry.dm index 92d8f1d380e..8c33e8032d7 100644 --- a/code/modules/mob/living/combat/parry.dm +++ b/code/modules/mob/living/combat/parry.dm @@ -225,7 +225,10 @@ if(HAS_TRAIT(U, TRAIT_ARMOUR_LIKED)) if(HAS_TRAIT(U, TRAIT_FENCERDEXTERITY)) prob2defend -= 5 - + //CC + TA edit + if(HAS_TRAIT(src, TRAIT_PARRYEXPERT)) + prob2defend += 30 + //CC + TA edit prob2defend = clamp(prob2defend, 5, 90) if(HAS_TRAIT(user, TRAIT_HARDSHELL) && H.client) //Dwarf-merc specific limitation w/ their armor on in pvp prob2defend = clamp(prob2defend, 5, 70) diff --git a/modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_items.dm b/modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_items.dm new file mode 100644 index 00000000000..392f640d70f --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_items.dm @@ -0,0 +1,760 @@ +GLOBAL_LIST_EMPTY(tat_item_catalog_cache) +GLOBAL_LIST_EMPTY(tat_item_loadout_slots_cache) +GLOBAL_VAR_INIT(tat_item_icon_cache_ready, FALSE) +GLOBAL_VAR_INIT(tat_item_icon_cache_warming, FALSE) + +#define TAT_ITEM_CATEGORY_WEAPON "weapon" +#define TAT_ITEM_CATEGORY_CLOTHING "clothing" + +#define TAT_UNLOCK_TYPE_WEAPON_SUPPLY "weapon_supply" +#define TAT_UNLOCK_TYPE_ARMOR_FAMILY "armor_family" +#define TAT_UNLOCK_TYPE_TRAIT "trait" + +#define TAT_SUPPLY_IRON "iron" +#define TAT_SUPPLY_BRONZE "bronze" +#define TAT_SUPPLY_SILVER "silver" +#define TAT_SUPPLY_STEEL "steel" +#define TAT_SUPPLY_FIREARMS "firearms" +#define TAT_SUPPLY_ARTIFACTS "artifacts" + +#define TAT_ARMOR_CLOTH "cloth" +#define TAT_ARMOR_LEATHER "leather" +#define TAT_ARMOR_MAIL "mail" +#define TAT_ARMOR_PLATE "plate" + +#define TAT_DONATION_TIER_ONE 1 +#define TAT_DONATION_TIER_TWO 2 + +#define TAT_DONATION_ACCESS_ALL_CKEYS list( \ + "gisya" \ +) + +GLOBAL_LIST_INIT(tat_donation_access_all_ckeys, TAT_DONATION_ACCESS_ALL_CKEYS) + +#ifndef TAT_ITEM_ENTRY +#define TAT_ITEM_ENTRY(_name, _cost, _category, _unlock_type, _unlock_key, _slot_group, _donat_tier) list("name" = (_name), "cost" = (_cost), "category" = (_category), "unlock_type" = (_unlock_type), "unlock_key" = (_unlock_key), "slot_group" = (_slot_group), "donat_tier" = (_donat_tier), "donat_ignore" = list()) +#endif + +#define TAT_AVAILABLE_ITEMS_LIST \ + /obj/item/tat_trader_lootbox/cheap = TAT_ITEM_ENTRY("Cheap Trader Cache", 1, "misc", TAT_UNLOCK_TYPE_TRAIT, "tat_trader_license", "trader cache"), \ + /obj/item/tat_trader_lootbox/medium = TAT_ITEM_ENTRY("Merchant Trader Cache", 3, "misc", TAT_UNLOCK_TYPE_TRAIT, "tat_trader_license", "trader cache"), \ + /obj/item/tat_trader_lootbox/expensive = TAT_ITEM_ENTRY("Luxury Trader Cache", 8, "misc", TAT_UNLOCK_TYPE_TRAIT, "tat_trader_license", "trader cache"), \ + /obj/item/tat_trader_lootbox/clothes = TAT_ITEM_ENTRY("Sewing Trader Cache", 5, "misc", TAT_UNLOCK_TYPE_TRAIT, "tat_trader_license", "trader cache"), \ + /obj/item/tat_trader_lootbox/potion = TAT_ITEM_ENTRY("Alchemical Trader Cache", 4, "misc", TAT_UNLOCK_TYPE_TRAIT, "tat_trader_license", "trader cache"), \ + /obj/item/powderflask = TAT_ITEM_ENTRY("Blackpowder Flask", 1, "weapon", "weapon_supply", TAT_SUPPLY_FIREARMS, "blackpowder"), \ + /obj/item/quiver/bulletpouch/iron = TAT_ITEM_ENTRY("20 Lead Bullets", 2, "weapon", "weapon_supply", TAT_SUPPLY_FIREARMS, "blackpowder"), \ + /obj/item/clothing/neck/roguetown/psicross/silver = TAT_ITEM_ENTRY("Silver Psycross", 1, "misc", "weapon_supply", TAT_SUPPLY_SILVER, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/silver/astrata = TAT_ITEM_ENTRY("Silver Astrata Cross", 1, "misc", "weapon_supply", TAT_SUPPLY_SILVER, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/silver/undivided = TAT_ITEM_ENTRY("Silver Tennite cross", 1, "misc", "weapon_supply", TAT_SUPPLY_SILVER, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/silver/necra = TAT_ITEM_ENTRY("Silver Necra Cross", 1, "misc", "weapon_supply", TAT_SUPPLY_SILVER, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/silver/noc = TAT_ITEM_ENTRY("Silver Noc Cross", 1, "misc", "weapon_supply", TAT_SUPPLY_SILVER, "cross"), \ + /obj/item/gun/ballistic/revolver/grenadelauncher/crossbow = TAT_ITEM_ENTRY("Crossbow", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "ranged"), \ + /obj/item/gun/ballistic/revolver/grenadelauncher/bow/recurve = TAT_ITEM_ENTRY("Recurve Bow", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "ranged"), \ + /obj/item/gun/ballistic/revolver/grenadelauncher/bow/longbow = TAT_ITEM_ENTRY("Long Bow", 3, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "ranged"), \ + /obj/item/gun/ballistic/revolver/grenadelauncher/crossbow/slurbow = TAT_ITEM_ENTRY("Slurbow", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "ranged"), \ + /obj/item/quiver/arrows = TAT_ITEM_ENTRY("Broadhead Arrows", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "munition"), \ + /obj/item/quiver/bodkin = TAT_ITEM_ENTRY("Bodkin arrows", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "munition"), \ + /obj/item/quiver/bolt/standard = TAT_ITEM_ENTRY("Bolts", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "munition"), \ + /obj/item/quiver/bolt/pyro = TAT_ITEM_ENTRY("Pyro Bolts", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "munition"), \ + /obj/item/quiver/bolt/water = TAT_ITEM_ENTRY("Water Bolts", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "munition"), \ + /obj/item/quiver/bolt/light = TAT_ITEM_ENTRY("Light Bolts", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "munition"), \ + /obj/item/quiver/silver = TAT_ITEM_ENTRY("Silver Arrows", 3, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "munition"), \ + /obj/item/quiver/bolt/silver = TAT_ITEM_ENTRY("Silver Bolts", 3, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "munition"), \ + /obj/item/rogueweapon/eaglebeak = TAT_ITEM_ENTRY("Eagle's Beak", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "polearm"), \ + /obj/item/rogueweapon/eaglebeak/lucerne = TAT_ITEM_ENTRY("Lucerne", 3, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "polearm"), \ + /obj/item/rogueweapon/shovel/silver/preblessed = TAT_ITEM_ENTRY("Silver Shovel", 2, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "polearm"), \ + /obj/item/rogueweapon/greataxe = TAT_ITEM_ENTRY("Greataxe", 3, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "axe"), \ + /obj/item/rogueweapon/greataxe/bronze = TAT_ITEM_ENTRY("Bronze Greataxe", 3, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "axe"), \ + /obj/item/rogueweapon/greataxe/silver = TAT_ITEM_ENTRY("Silver Poleaxe", 5, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "axe"), \ + /obj/item/rogueweapon/halberd/psyhalberd = TAT_ITEM_ENTRY("Psydonic Halberd", 5, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "axe"), \ + /obj/item/rogueweapon/greataxe/steel = TAT_ITEM_ENTRY("Steel Greataxe", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "axe"), \ + /obj/item/rogueweapon/woodstaff/quarterstaff/silver = TAT_ITEM_ENTRY("Silver Staff", 3, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "polearm"), \ + /obj/item/rogueweapon/greataxe/steel/doublehead = TAT_ITEM_ENTRY("Double-Headed Steel Greataxe", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "axe"), \ + /obj/item/rogueweapon/greatsword = TAT_ITEM_ENTRY("Greatsword", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "greatsword"), \ + /obj/item/rogueweapon/greatsword/grenz = TAT_ITEM_ENTRY("Steel Zweihander", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "greatsword"), \ + /obj/item/rogueweapon/greatsword/grenz/flamberge = TAT_ITEM_ENTRY("Flamberge", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "greatsword"), \ + /obj/item/rogueweapon/greatsword/iron = TAT_ITEM_ENTRY("Iron Greatsword", 3, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "greatsword"), \ + /obj/item/rogueweapon/estoc = TAT_ITEM_ENTRY("Estoc", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "greatsword"), \ + /obj/item/rogueweapon/greatsword/silver = TAT_ITEM_ENTRY("Silver Greatsword", 5, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "greatsword"), \ + /obj/item/rogueweapon/greatsword/zwei = TAT_ITEM_ENTRY("Claymore", 4, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "greatsword"), \ + /obj/item/rogueweapon/halberd = TAT_ITEM_ENTRY("Halberd", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "polearm"), \ + /obj/item/rogueweapon/halberd/bardiche = TAT_ITEM_ENTRY("Bardiche", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "polearm"), \ + /obj/item/rogueweapon/halberd/glaive = TAT_ITEM_ENTRY("Glaive", 5, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "polearm"), \ + /obj/item/rogueweapon/huntingknife = TAT_ITEM_ENTRY("Hunting Knife", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "knife"), \ + /obj/item/rogueweapon/huntingknife/bronze = TAT_ITEM_ENTRY("Bronze Knife", 1, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "knife"), \ + /obj/item/rogueweapon/huntingknife/chefknife = TAT_ITEM_ENTRY("Chef's Knife", 1, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "knife"), \ + /obj/item/rogueweapon/huntingknife/chefknife/cleaver = TAT_ITEM_ENTRY("Cleaver", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "knife"), \ + /obj/item/rogueweapon/huntingknife/combat/bronze = TAT_ITEM_ENTRY("Sydearmme", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "knife"), \ + /obj/item/rogueweapon/huntingknife/combat/iron = TAT_ITEM_ENTRY("Bauernwehr", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "knife"), \ + /obj/item/rogueweapon/huntingknife/idagger = TAT_ITEM_ENTRY("Iron Dagger", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "knife"), \ + /obj/item/rogueweapon/huntingknife/idagger/navaja = TAT_ITEM_ENTRY("Navaja", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "knife"), \ + /obj/item/rogueweapon/huntingknife/idagger/silver = TAT_ITEM_ENTRY("Silver Dagger", 3, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "knife"), \ + /obj/item/rogueweapon/huntingknife/idagger/steel = TAT_ITEM_ENTRY("Steel Dagger", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "knife"), \ + /obj/item/rogueweapon/huntingknife/idagger/steel/rondel = TAT_ITEM_ENTRY("Steel Dagger", 2.5, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "knife"), \ + /obj/item/rogueweapon/huntingknife/idagger/steel/parrying = TAT_ITEM_ENTRY("Parrying Dagger", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "knife"), \ + /obj/item/rogueweapon/huntingknife/scissors = TAT_ITEM_ENTRY("Scissors", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "misc"), \ + /obj/item/rogueweapon/huntingknife/scissors/steel = TAT_ITEM_ENTRY("Steel Scissors", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "misc"), \ + /obj/item/rogueweapon/katar = TAT_ITEM_ENTRY("Katar", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "unarmed"), \ + /obj/item/rogueweapon/katar/bronze = TAT_ITEM_ENTRY("Bronze Katar", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "unarmed"), \ + /obj/item/rogueweapon/katar/bronze/gladiator = TAT_ITEM_ENTRY("Arbelos", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "unarmed"), \ + /obj/item/rogueweapon/katar/punchdagger = TAT_ITEM_ENTRY("Punch Dagger", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "unarmed"), \ + /obj/item/rogueweapon/katar/silver = TAT_ITEM_ENTRY("Silver Katar", 3, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "unarmed"), \ + /obj/item/rogueweapon/mace = TAT_ITEM_ENTRY("Mace", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "blunt"), \ + /obj/item/rogueweapon/mace/bronze = TAT_ITEM_ENTRY("Bronze Mace", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "blunt"), \ + /obj/item/rogueweapon/mace/cudgel/flanged = TAT_ITEM_ENTRY("Flanged Mace", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "blunt"), \ + /obj/item/rogueweapon/mace/cudgel/flanged/silver = TAT_ITEM_ENTRY("Silver Flanged Mace", 4, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "blunt"), \ + /obj/item/rogueweapon/mace/maul/grand = TAT_ITEM_ENTRY("Grand Maul", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "blunt"), \ + /obj/item/rogueweapon/mace/spiked = TAT_ITEM_ENTRY("Spiked Mace", 3, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "blunt"), \ + /obj/item/rogueweapon/mace/steel = TAT_ITEM_ENTRY("Steel Mace", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "blunt"), \ + /obj/item/rogueweapon/mace/steel/morningstar = TAT_ITEM_ENTRY("Morningstar", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "blunt"), \ + /obj/item/rogueweapon/mace/steel/silver = TAT_ITEM_ENTRY("Silver Mace", 4, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "blunt"), \ + /obj/item/rogueweapon/mace/warhammer = TAT_ITEM_ENTRY("Warhammer", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "blunt"), \ + /obj/item/rogueweapon/mace/warhammer/bronze = TAT_ITEM_ENTRY("Bronze Warhammer", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "blunt"), \ + /obj/item/rogueweapon/mace/warhammer/steel = TAT_ITEM_ENTRY("Steel Warhammer", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "blunt"), \ + /obj/item/rogueweapon/mace/warhammer/steel/silver = TAT_ITEM_ENTRY("Silver Warhammer", 4, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "blunt"), \ + /obj/item/rogueweapon/flail = TAT_ITEM_ENTRY("Flail", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "blunt"), \ + /obj/item/rogueweapon/flail/alt = TAT_ITEM_ENTRY("Flail, Studded", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "blunt"), \ + /obj/item/rogueweapon/flail/bronze = TAT_ITEM_ENTRY("Bronze Flail", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "blunt"), \ + /obj/item/rogueweapon/flail/peasantwarflail = TAT_ITEM_ENTRY("Peasant War Flail", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "blunt"), \ + /obj/item/rogueweapon/flail/peasantwarflail/iron = TAT_ITEM_ENTRY("Iron Peasant War Flail", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "blunt"), \ + /obj/item/rogueweapon/flail/sflail = TAT_ITEM_ENTRY("Steel Flail", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "blunt"), \ + /obj/item/rogueweapon/flail/sflail/silver = TAT_ITEM_ENTRY("Silver Morningstar", 4, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "blunt"), \ + /obj/item/rogueweapon/spear = TAT_ITEM_ENTRY("Spear", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "polearm"), \ + /obj/item/rogueweapon/spear/assegai = TAT_ITEM_ENTRY("Steel Assegai", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "polearm"), \ + /obj/item/rogueweapon/spear/assegai/iron = TAT_ITEM_ENTRY("Iron Assegai", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "polearm"), \ + /obj/item/rogueweapon/spear/boar = TAT_ITEM_ENTRY("Boar Spear", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "polearm"), \ + /obj/item/rogueweapon/spear/bronze = TAT_ITEM_ENTRY("Bronze Spear", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "polearm"), \ + /obj/item/rogueweapon/spear/bronze/strapless = TAT_ITEM_ENTRY("Bronze Strapless Spear", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "polearm"), \ + /obj/item/rogueweapon/spear/bronze/winged = TAT_ITEM_ENTRY("Bronze Winged Spear", 3, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "polearm"), \ + /obj/item/rogueweapon/spear/bronze/winged/strapless = TAT_ITEM_ENTRY("Bronze Winged Strapless Spear", 3, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "polearm"), \ + /obj/item/rogueweapon/spear/naginata = TAT_ITEM_ENTRY("Naginata", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "polearm"), \ + /obj/item/rogueweapon/spear/partizan = TAT_ITEM_ENTRY("Partizan", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "polearm"), \ + /obj/item/rogueweapon/spear/short = TAT_ITEM_ENTRY("Short Spear", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "polearm"), \ + /obj/item/rogueweapon/spear/silver = TAT_ITEM_ENTRY("Silver Spear", 4, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "polearm"), \ + /obj/item/rogueweapon/spear/trident = TAT_ITEM_ENTRY("BronzeTrident", 4, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "polearm"), \ + /obj/item/rogueweapon/stoneaxe/battle = TAT_ITEM_ENTRY("Battle Axe", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "axe"), \ + /obj/item/rogueweapon/stoneaxe/woodcut/bronze = TAT_ITEM_ENTRY("Bronze Axe", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "axe"), \ + /obj/item/rogueweapon/stoneaxe/woodcut/bronzebattleaxe = TAT_ITEM_ENTRY("Bronze War Axe", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "axe"), \ + /obj/item/rogueweapon/stoneaxe/woodcut/silver = TAT_ITEM_ENTRY("Silver War Axe", 4, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "axe"), \ + /obj/item/rogueweapon/stoneaxe/woodcut/steel = TAT_ITEM_ENTRY("Steel Axe", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "axe"), \ + /obj/item/rogueweapon/stoneaxe/woodcut/steel/atgervi = TAT_ITEM_ENTRY("Varangian Axe", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "axe"), \ + /obj/item/rogueweapon/sword = TAT_ITEM_ENTRY("Steel Arming Sword", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/scythe = TAT_ITEM_ENTRY("Scythe", 1.5, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "polearm"), \ + /obj/item/rogueweapon/sword/broken = TAT_ITEM_ENTRY("Broken Sword", 0, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "sword"), \ + /obj/item/rogueweapon/sword/bronze = TAT_ITEM_ENTRY("Bronze Arming Sword", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "sword"), \ + /obj/item/rogueweapon/sword/cutlass = TAT_ITEM_ENTRY("Cutlass", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/falx = TAT_ITEM_ENTRY("Falx", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/iron = TAT_ITEM_ENTRY("Iron Arming Sword", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "sword"), \ + /obj/item/rogueweapon/sword/long/fencerguy = TAT_ITEM_ENTRY("Frei Longsword", 2.5, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/long = TAT_ITEM_ENTRY("Longsword", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/long/broadsword = TAT_ITEM_ENTRY("Broadsword", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "sword"), \ + /obj/item/rogueweapon/sword/long/broadsword/bronze = TAT_ITEM_ENTRY("Spatha", 3, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "sword"), \ + /obj/item/rogueweapon/sword/long/broadsword/steel = TAT_ITEM_ENTRY("Steel Broadsword", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/long/exe = TAT_ITEM_ENTRY("Executioner Sword", 3, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "sword"), \ + /obj/item/rogueweapon/sword/long/exe/silver = TAT_ITEM_ENTRY("Silver Executioner Sword", 4, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "sword"), \ + /obj/item/rogueweapon/sword/long/greatkhopesh = TAT_ITEM_ENTRY("Great Khopesh", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/long/kriegmesser = TAT_ITEM_ENTRY("Kriegmesser", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/long/kriegmesser/silver = TAT_ITEM_ENTRY("Silver Broadsword", 4, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "sword"), \ + /obj/item/rogueweapon/sword/long/kriegmesser/ssangsudo = TAT_ITEM_ENTRY("Ssangsudo", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/long/silver = TAT_ITEM_ENTRY("Silver Longsword", 4, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "sword"), \ + /obj/item/rogueweapon/sword/rapier = TAT_ITEM_ENTRY("Rapier", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/rapier/vaquero = TAT_ITEM_ENTRY("Etruscan Rapier", 5, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/rapier/silver = TAT_ITEM_ENTRY("Silver Rapier", 4, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "sword"), \ + /obj/item/rogueweapon/sword/saber/iron = TAT_ITEM_ENTRY("Iron Saber", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "sword"), \ + /obj/item/rogueweapon/sword/long/shotel = TAT_ITEM_ENTRY("Shotel", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/long/shotel/iron = TAT_ITEM_ENTRY("Iron Shotel", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "sword"), \ + /obj/item/rogueweapon/sword/sabre = TAT_ITEM_ENTRY("Sabre", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/sabre/bronzekhopesh = TAT_ITEM_ENTRY("Bronze Khopesh", 3, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "sword"), \ + /obj/item/rogueweapon/sword/sabre/mulyeog = TAT_ITEM_ENTRY("Hwando", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/short = TAT_ITEM_ENTRY("Shortsword", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/short/falchion = TAT_ITEM_ENTRY("Falchion", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/short/gladius = TAT_ITEM_ENTRY("Gladius", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "sword"), \ + /obj/item/rogueweapon/sword/short/iron = TAT_ITEM_ENTRY("Iron Shortsword", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "sword"), \ + /obj/item/rogueweapon/sword/short/messer = TAT_ITEM_ENTRY("Messer", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/short/messer/alt = TAT_ITEM_ENTRY("Hunting Sword", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/short/messer/bronze = TAT_ITEM_ENTRY("Makhaira", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "sword"), \ + /obj/item/rogueweapon/sword/short/messer/iron = TAT_ITEM_ENTRY("Iron Messer", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "sword"), \ + /obj/item/rogueweapon/sword/short/silver = TAT_ITEM_ENTRY("Silver Shortsword", 3, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "sword"), \ + /obj/item/rogueweapon/sword/short/psy = TAT_ITEM_ENTRY("Psydonic Shortsword", 3.5, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "sword"), \ + /obj/item/rogueweapon/sword/silver = TAT_ITEM_ENTRY("Silver Arming Sword", 4, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "sword"), \ + /obj/item/rogueweapon/whip/bronze = TAT_ITEM_ENTRY("Bronze Whip", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "whip"), \ + /obj/item/rogueweapon/whip/silver = TAT_ITEM_ENTRY("Silver Whip", 3, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "whip"), \ + /obj/item/clothing/gloves/roguetown/knuckles = TAT_ITEM_ENTRY("Knuckles", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "unarmed"), \ + /obj/item/clothing/gloves/roguetown/knuckles/bronze = TAT_ITEM_ENTRY("Knuckles Bronze", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "unarmed"), \ + /obj/item/clothing/gloves/roguetown/angle = TAT_ITEM_ENTRY("Heavy Leather Gloves", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "gloves"), \ + /obj/item/clothing/gloves/roguetown/angle/grenzelgloves = TAT_ITEM_ENTRY("Grenzelhoft Gloves", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "gloves"), \ + /obj/item/clothing/gloves/roguetown/eastgloves1 = TAT_ITEM_ENTRY("Swordsman Gloves", 1, "clothing", "armor_family", TAT_ARMOR_LEATHER, "gloves"), \ + /obj/item/clothing/gloves/roguetown/eastgloves2 = TAT_ITEM_ENTRY("Stylish Bandages", 1, "clothing", "armor_family", TAT_ARMOR_LEATHER, "gloves"), \ + /obj/item/clothing/gloves/roguetown/leather = TAT_ITEM_ENTRY("Leather Gloves", 1, "clothing", "armor_family", TAT_ARMOR_CLOTH, "gloves"), \ + /obj/item/clothing/gloves/roguetown/otavan = TAT_ITEM_ENTRY("Otavan Leather Gloves", 2, "clothing", "armor_family", TAT_ARMOR_LEATHER, "gloves"), \ + /obj/item/clothing/gloves/roguetown/plate = TAT_ITEM_ENTRY("Plate Gauntlets", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "gloves"), \ + /obj/item/clothing/gloves/roguetown/plate/iron = TAT_ITEM_ENTRY("Iron Gauntlets", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "gloves"), \ + /obj/item/clothing/gloves/roguetown/plate/kote = TAT_ITEM_ENTRY("Jjajeungna Gauntlets", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "gloves"), \ + /obj/item/clothing/head/roguetown/armingcap = TAT_ITEM_ENTRY("Arming cap", 1, "clothing", "armor_family", TAT_ARMOR_CLOTH, "head"), \ + /obj/item/clothing/head/roguetown/armingcap/padded = TAT_ITEM_ENTRY("Padded Arming Cap", 2, "clothing", "armor_family", TAT_ARMOR_CLOTH, "head"), \ + /obj/item/clothing/head/roguetown/helmet/bascinet = TAT_ITEM_ENTRY("Bascinet", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/bascinet/etruscan = TAT_ITEM_ENTRY("Etruscan Bascinet", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/bascinet/pigface = TAT_ITEM_ENTRY("Pigface Bascinet", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/bascinet/pigface/hounskull = TAT_ITEM_ENTRY("Hounskull Bacinet", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/bronze = TAT_ITEM_ENTRY("Bronze Helmet", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/headcage = TAT_ITEM_ENTRY("Cage Helmet", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/bronzegladiator = TAT_ITEM_ENTRY("Bronze Murmillo", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/barbute = TAT_ITEM_ENTRY("Barbute", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/barbute/great = TAT_ITEM_ENTRY("Great Barbute", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/barbute/visor = TAT_ITEM_ENTRY("Visored Barbute", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/beakhelm = TAT_ITEM_ENTRY("Beak helmet", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/bronze = TAT_ITEM_ENTRY("Bronze Barbute", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/bucket = TAT_ITEM_ENTRY("Steel Bucket Helmet", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/bucket/crusader = TAT_ITEM_ENTRY("Sugarloaf Helmet", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/bucket/iron = TAT_ITEM_ENTRY("Iron Bucket Helmet", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/frogmouth = TAT_ITEM_ENTRY("Frogmouth", 3.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/kabuto = TAT_ITEM_ENTRY("Kabuto", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/psysallet = TAT_ITEM_ENTRY("Psydonic Sallet", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/volfplate = TAT_ITEM_ENTRY("Volf-face Helm", 3.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/horned = TAT_ITEM_ENTRY("Horned Cap", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/kettle = TAT_ITEM_ENTRY("Steel Kettle", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/kettle/iron = TAT_ITEM_ENTRY("Iron Kettle", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/kettle/jingasa = TAT_ITEM_ENTRY("Jingasa", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/kettle/wide = TAT_ITEM_ENTRY("Wide Kettle", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/leather = TAT_ITEM_ENTRY("Leather Helmet", 0, "clothing", "armor_family", TAT_ARMOR_LEATHER, "head"), \ + /obj/item/clothing/head/roguetown/helmet/leather/advanced = TAT_ITEM_ENTRY("Hardened Leather Helmet", 1, "clothing", "armor_family", TAT_ARMOR_LEATHER, "head"), \ + /obj/item/clothing/head/roguetown/helmet/leather/volfhelm = TAT_ITEM_ENTRY("Volf Helmet", 0.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "head"), \ + /obj/item/clothing/head/roguetown/helmet/sallet = TAT_ITEM_ENTRY("Sallet", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/sallet/beastskull = TAT_ITEM_ENTRY("Beastskull", 3.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/sallet/iron = TAT_ITEM_ENTRY("Iron Sallet", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/sallet/raneshen = TAT_ITEM_ENTRY("Kulah Khud", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/sallet/shishak = TAT_ITEM_ENTRY("Steel Shishak", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/sallet/visored = TAT_ITEM_ENTRY("Visored Sallet", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/sallet/visored/iron = TAT_ITEM_ENTRY("Visored Iron Sallet", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/skullcap = TAT_ITEM_ENTRY("Skull cap", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/head/roguetown/helmet/winged = TAT_ITEM_ENTRY("Winged Cap", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/mask/rogue/facemask = TAT_ITEM_ENTRY("Iron Mask", 1, "clothing", "weapon_supply", TAT_SUPPLY_IRON, "mask"), \ + /obj/item/clothing/mask/rogue/facemask/psydonmask = TAT_ITEM_ENTRY("Psydonic Mask", 1.5, "clothing", "weapon_supply", TAT_SUPPLY_STEEL, "mask"), \ + /obj/item/clothing/mask/rogue/facemask/bronze = TAT_ITEM_ENTRY("Mouthless Bronze Mask", 2, "clothing", "weapon_supply", TAT_SUPPLY_BRONZE, "mask"), \ + /obj/item/clothing/mask/rogue/facemask/bronze/classic = TAT_ITEM_ENTRY("Bronze Mask", 2, "clothing", "weapon_supply", TAT_SUPPLY_BRONZE, "mask"), \ + /obj/item/clothing/mask/rogue/facemask/copper = TAT_ITEM_ENTRY("Copper Mask", 0.5, "clothing", "weapon_supply", TAT_SUPPLY_IRON, "mask"), \ + /obj/item/clothing/mask/rogue/facemask/steel = TAT_ITEM_ENTRY("Steel Mask", 2, "clothing", "weapon_supply", TAT_SUPPLY_STEEL, "mask"), \ + /obj/item/clothing/mask/rogue/wildguard = TAT_ITEM_ENTRY("Wildguard Mask", 1, "clothing", "weapon_supply", TAT_SUPPLY_IRON, "mask"), \ + /obj/item/clothing/mask/rogue/facemask/steel/kazengun = TAT_ITEM_ENTRY("Soldier's Half-Mask", 1, "clothing", "weapon_supply", TAT_SUPPLY_STEEL, "mask"), \ + /obj/item/clothing/mask/rogue/facemask/steel/kazengun/full = TAT_ITEM_ENTRY("Soldier's Mask", 2, "clothing", "weapon_supply", TAT_SUPPLY_STEEL, "mask"), \ + /obj/item/clothing/mask/rogue/facemask/steel/steppesman = TAT_ITEM_ENTRY("Steppesman Mask", 2, "clothing", "weapon_supply", TAT_SUPPLY_STEEL, "mask"), \ + /obj/item/clothing/neck/roguetown/bevor = TAT_ITEM_ENTRY("Bevor", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "neck"), \ + /obj/item/clothing/neck/roguetown/bevor/bronze = TAT_ITEM_ENTRY("Bronze Bevor", 1, "clothing", "armor_family", TAT_ARMOR_MAIL, "neck"), \ + /obj/item/clothing/neck/roguetown/bevor/iron = TAT_ITEM_ENTRY("Iron Bevor", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "neck"), \ + /obj/item/clothing/neck/roguetown/chaincoif = TAT_ITEM_ENTRY("Steel Chaincoif", 2, "clothing", "armor_family", TAT_ARMOR_PLATE, "neck"), \ + /obj/item/clothing/neck/roguetown/chaincoif/chainmantle = TAT_ITEM_ENTRY("Chainmantle", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "neck"), \ + /obj/item/clothing/neck/roguetown/chaincoif/full = TAT_ITEM_ENTRY("Chaincoif Full", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "neck"), \ + /obj/item/clothing/neck/roguetown/chaincoif/iron = TAT_ITEM_ENTRY("Iron Chaincoif", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "neck"), \ + /obj/item/clothing/neck/roguetown/coif/heavypadding = TAT_ITEM_ENTRY("Heavy Padded Coif", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "neck"), \ + /obj/item/clothing/neck/roguetown/coif/padded = TAT_ITEM_ENTRY("Padded Coif", 1, "clothing", "armor_family", TAT_ARMOR_CLOTH, "neck"), \ + /obj/item/clothing/neck/roguetown/gorget = TAT_ITEM_ENTRY("Iron Gorget", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "neck"), \ + /obj/item/clothing/neck/roguetown/gorget/forlorncollar = TAT_ITEM_ENTRY("Forlorn Gorget", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "neck"), \ + /obj/item/clothing/neck/roguetown/gorget/bronze = TAT_ITEM_ENTRY("Bronze Gorget", 0.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "neck"), \ + /obj/item/clothing/neck/roguetown/gorget/copper = TAT_ITEM_ENTRY("Copper Gorget", 0.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "neck"), \ + /obj/item/clothing/neck/roguetown/gorget/steel = TAT_ITEM_ENTRY("Steel Gorget", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "neck"), \ + /obj/item/clothing/shoes/roguetown/boots = TAT_ITEM_ENTRY("Dark Boots", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "shoes"), \ + /obj/item/clothing/shoes/roguetown/boots/armor = TAT_ITEM_ENTRY("Plated Boots", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "shoes"), \ + /obj/item/clothing/shoes/roguetown/boots/armor/bronze = TAT_ITEM_ENTRY("Bronze Sandals", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "shoes"), \ + /obj/item/clothing/shoes/roguetown/boots/armor/iron = TAT_ITEM_ENTRY("Light Plated Boots", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "shoes"), \ + /obj/item/clothing/shoes/roguetown/boots/leather/reinforced = TAT_ITEM_ENTRY("Heavy Leather Boots", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "shoes"), \ + /obj/item/clothing/shoes/roguetown/armor/rumaclan = TAT_ITEM_ENTRY("Heavy Sandals", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "shoes"), \ + /obj/item/clothing/shoes/roguetown/boots/leather/reinforced/kazengun = TAT_ITEM_ENTRY("Kazengun Armored Sandals", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "shoes"), \ + /obj/item/clothing/shoes/roguetown/boots/leather/reinforced/short = TAT_ITEM_ENTRY("Short Leather Boots", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "shoes"), \ + /obj/item/clothing/shoes/roguetown/boots/nobleboot/steppesman = TAT_ITEM_ENTRY("Aavnic Riding Boots", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "shoes"), \ + /obj/item/clothing/shoes/roguetown/boots/otavan = TAT_ITEM_ENTRY("Otavan Leather Boots", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "shoes"), \ + /obj/item/clothing/shoes/roguetown/boots/psydonboots = TAT_ITEM_ENTRY("Psydonic Boots", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "shoes"), \ + /obj/item/clothing/shoes/roguetown/grenzelhoft = TAT_ITEM_ENTRY("Grenzelhoft Boots", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "shoes"), \ + /obj/item/clothing/shoes/roguetown/shortboots = TAT_ITEM_ENTRY("Short Boots", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "shoes"), \ + /obj/item/clothing/suit/roguetown/armor/chainmail = TAT_ITEM_ENTRY("Steel Haubergeon", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "suit"), \ + /obj/item/clothing/suit/roguetown/armor/chainmail/hauberk/ornate = TAT_ITEM_ENTRY("Psydonic Hauberk", 3.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/chainmail/hauberk/heavy = TAT_ITEM_ENTRY("Mailled Hauberk", 3.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/chainmail/hauberk = TAT_ITEM_ENTRY("Steel Hauberk", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "unterarmor"), \ + /obj/item/clothing/suit/roguetown/armor/chainmail/hauberk/iron = TAT_ITEM_ENTRY("Iron Hauberk", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "unterarmor"), \ + /obj/item/clothing/suit/roguetown/armor/chainmail/iron = TAT_ITEM_ENTRY("Iron Haubergeon", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "suit"), \ + /obj/item/clothing/suit/roguetown/armor/chainmail/light = TAT_ITEM_ENTRY("Besilked Haubergeon", 3.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "suit"), \ + /obj/item/clothing/suit/roguetown/armor/leather = TAT_ITEM_ENTRY("leather armor", 1, "clothing", "armor_family", TAT_ARMOR_CLOTH, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/leather/cuirass = TAT_ITEM_ENTRY("Leather Cuirass", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/leather/heavy = TAT_ITEM_ENTRY("Hardened Leather Armor", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/leather/heavy/coat = TAT_ITEM_ENTRY("Hardened Leather Coat", 2, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/leather/heavy/coat/raneshen = TAT_ITEM_ENTRY("Megarmach Scale Coat", 2, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/leather/heavy/coat/steppe = TAT_ITEM_ENTRY("Fur-Woven Hatanga Coat", 2, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/leather/heavy/jacket = TAT_ITEM_ENTRY("Jacket", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/leather/heavy/shepherd = TAT_ITEM_ENTRY("Shepherd Vest", 2, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/leather/hide = TAT_ITEM_ENTRY("Hide Armor", 2, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/leather/studded = TAT_ITEM_ENTRY("Studded Leather Armor", 3, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/leather/studded/cuirbouilli = TAT_ITEM_ENTRY("Cuir-bouilli armor", 2, "clothing", "armor_family", TAT_ARMOR_LEATHER, "suit"), \ + /obj/item/clothing/suit/roguetown/armor/plate = TAT_ITEM_ENTRY("Steel Half-Plate", 3.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/bronze = TAT_ITEM_ENTRY("bronze cuirass", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/bronze/light = TAT_ITEM_ENTRY("Bronze Cardiophylax", 1, "clothing", "armor_family", TAT_ARMOR_MAIL, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/cuirass = TAT_ITEM_ENTRY("Steel Cuirass", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/copper = TAT_ITEM_ENTRY("Copper Cuirass", 0.5, "clothing", "armor_family", "armor", TAT_ARMOR_LEATHER), \ + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/fencer = TAT_ITEM_ENTRY("Fencer Cuirass", 3.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/fluted = TAT_ITEM_ENTRY("Fluted Cuirass", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/iron = TAT_ITEM_ENTRY("Iron Cuirass", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/fluted = TAT_ITEM_ENTRY("Fluted Half-Plate", 3.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/full = TAT_ITEM_ENTRY("Steel Plate Armor", 3.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/full/bronze = TAT_ITEM_ENTRY("Bronze Panoplic Armor", 2, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/full/bronze/alt = TAT_ITEM_ENTRY("Bronze Panoplic Assembly", 2, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/full/fluted = TAT_ITEM_ENTRY("Fluted Plate", 3.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/full/iron = TAT_ITEM_ENTRY("Iron Plate Armor", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/chainmail/hauberk/iron/heavy/zycuirass = TAT_ITEM_ENTRY("Iron Gardbrace And Fauld", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/iron/banded= TAT_ITEM_ENTRY("Iron Branded Armor", 2, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/full/samsibsa = TAT_ITEM_ENTRY("Samsibsa Scaleplate", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/iron = TAT_ITEM_ENTRY("iron half-plate", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/scale = TAT_ITEM_ENTRY("Scalemail", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/plate/scale/steppe = TAT_ITEM_ENTRY("Steel Heavy Lamellar", 3.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "armor"), \ + /obj/item/clothing/under/roguetown/brigandinelegs = TAT_ITEM_ENTRY("Chausses, Brigandine", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "pants"), \ + /obj/item/clothing/under/roguetown/chainlegs = TAT_ITEM_ENTRY("Steel Chain Chausses", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "pants"), \ + /obj/item/clothing/under/roguetown/chainlegs/iron = TAT_ITEM_ENTRY("Iron Chain Chausses", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "pants"), \ + /obj/item/clothing/under/roguetown/chainlegs/iron/kilt = TAT_ITEM_ENTRY("Iron Chain Kilt", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "pants"), \ + /obj/item/clothing/under/roguetown/chainlegs/kilt = TAT_ITEM_ENTRY("Steel Chain Kilt", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "pants"), \ + /obj/item/clothing/under/roguetown/chainlegs/skirt = TAT_ITEM_ENTRY("Steel Chain Skirt", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "pants"), \ + /obj/item/clothing/under/roguetown/heavy_leather_pants = TAT_ITEM_ENTRY("Heavy Leather Pants", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "pants"), \ + /obj/item/clothing/under/roguetown/heavy_leather_pants/bronzeskirt = TAT_ITEM_ENTRY("Bronze skirt", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "pants"), \ + /obj/item/clothing/under/roguetown/heavy_leather_pants/grenzelpants = TAT_ITEM_ENTRY("Grenzelhoftian Paumpers", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "pants"), \ + /obj/item/clothing/under/roguetown/heavy_leather_pants/shadowpants = TAT_ITEM_ENTRY("Silk Tights", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "pants"), \ + /obj/item/clothing/under/roguetown/heavy_leather_pants/shorts = TAT_ITEM_ENTRY("Leather Shorts", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "pants"), \ + /obj/item/clothing/under/roguetown/platelegs = TAT_ITEM_ENTRY("Plate legs", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "pants"), \ + /obj/item/clothing/under/roguetown/platelegs/iron = TAT_ITEM_ENTRY("Iron Plate legs", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "pants"), \ + /obj/item/clothing/under/roguetown/splintlegs = TAT_ITEM_ENTRY("Chausses, Splinted", 2, "clothing", "armor_family", TAT_ARMOR_LEATHER, "pants"), \ + /obj/item/clothing/under/roguetown/chainlegs/gronn = TAT_ITEM_ENTRY("Gronn Byrine Chausses", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "pants"), \ + /obj/item/clothing/under/roguetown/tights/sailor = TAT_ITEM_ENTRY("Sailor Pants", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "pants"), \ + /obj/item/clothing/under/roguetown/trou/artipants = TAT_ITEM_ENTRY("Tinker Trousers", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "pants"), \ + /obj/item/clothing/under/roguetown/trou = TAT_ITEM_ENTRY("Work Trousers", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "pants"), \ + /obj/item/clothing/under/roguetown/trou/leather = TAT_ITEM_ENTRY("Leather Trousers", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "pants"), \ + /obj/item/clothing/under/roguetown/trou/leather/gronn = TAT_ITEM_ENTRY("Gronnic Fur Pants", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "pants"), \ + /obj/item/clothing/under/roguetown/trou/leather/pontifex/raneshen = TAT_ITEM_ENTRY("Baggy Desert Pants", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "pants"), \ + /obj/item/clothing/wrists/roguetown/bracers = TAT_ITEM_ENTRY("Steel Bracers ", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "wrists"), \ + /obj/item/clothing/under/roguetown/trou/leathertights = TAT_ITEM_ENTRY("Leather tights", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "pants"), \ + /obj/item/clothing/wrists/roguetown/bracers/brigandine = TAT_ITEM_ENTRY("Brigandine Rerebraces", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "wrists"), \ + /obj/item/clothing/wrists/roguetown/bracers/bronze = TAT_ITEM_ENTRY("Bronze Bracers ", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "wrists"), \ + /obj/item/clothing/wrists/roguetown/bracers/cloth/monk = TAT_ITEM_ENTRY("Monk Wrappings", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "wrists"), \ + /obj/item/clothing/wrists/roguetown/bracers/copper = TAT_ITEM_ENTRY("Copper Bracers", 0.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "wrists"), \ + /obj/item/clothing/wrists/roguetown/bracers/iron = TAT_ITEM_ENTRY("Iron Bracers ", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "wrists"), \ + /obj/item/clothing/wrists/roguetown/bracers/leather = TAT_ITEM_ENTRY("Leather Bracers ", 0.5, "clothing", "armor_family", TAT_ARMOR_CLOTH, "wrists"), \ + /obj/item/clothing/wrists/roguetown/bracers/leather/heavy = TAT_ITEM_ENTRY("Heavy Leather Bracers", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "wrists"), \ + /obj/item/clothing/wrists/roguetown/bracers/splint = TAT_ITEM_ENTRY("Splint Bracers", 2.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "wrists"), \ + /obj/item/storage/belt/rogue/leather = TAT_ITEM_ENTRY("Leather Belt", 0.5, "clothing", "armor_family", TAT_ARMOR_CLOTH, "belt"), \ + /obj/item/storage/belt/rogue/leather/sash = TAT_ITEM_ENTRY("Cloth Sash", 0.5, "clothing", "armor_family", TAT_ARMOR_CLOTH, "belt"), \ + /obj/item/storage/belt/rogue/leather/plaquesilver = TAT_ITEM_ENTRY("Silver Belt", 2, "clothing", "armor_family", TAT_ARMOR_CLOTH, "belt"), \ + /obj/item/storage/belt/rogue/leather/steel/tasset = TAT_ITEM_ENTRY("Tasseted Belt", 0, "clothing", "armor_family", TAT_ARMOR_PLATE, "belt"), \ + /obj/item/storage/belt/rogue/leather/rope = TAT_ITEM_ENTRY("Rope Belt", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "belt"), \ + /obj/item/storage/belt/rogue/leather/black = TAT_ITEM_ENTRY("Black Leather Belt", 0.5, "clothing", "armor_family", TAT_ARMOR_CLOTH, "belt"), \ + /obj/item/storage/belt/rogue/leather/cloth = TAT_ITEM_ENTRY("Cloth Belt", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "belt"), \ + /obj/item/clothing/suit/roguetown/shirt/undershirt/black = TAT_ITEM_ENTRY("Shirt", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/storage/belt/rogue/leather/knifebelt/black/iron = TAT_ITEM_ENTRY("Iron Tossblade belt", 1, "clothing", "armor_family", TAT_ARMOR_LEATHER, "belt"), \ + /obj/item/storage/belt/rogue/leather/knifebelt/black/steel = TAT_ITEM_ENTRY("Steel Tossblade Belt", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "belt"), \ + /obj/item/storage/belt/rogue/leather/knifebelt/black/silver = TAT_ITEM_ENTRY("Silver Tossblade belt", 3, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "belt"), \ + /obj/item/storage/belt/rogue/leather/knifebelt/black/kazengun = TAT_ITEM_ENTRY("Eastern tossbale belt", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "belt"), \ + /obj/item/rogueweapon/spear/psyspear/old = TAT_ITEM_ENTRY("Enduring Spear", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "polearm"), \ + /obj/item/rogueweapon/mace/cudgel/psy/old = TAT_ITEM_ENTRY("Enduring Flanged Mace", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "blunt"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/psydonhelm = TAT_ITEM_ENTRY("Psydonic Helm", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/psybucket = TAT_ITEM_ENTRY("Psydonic Bucket", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/rogueweapon/huntingknife/idagger/silver/stake = TAT_ITEM_ENTRY("Silver Stake", 2, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "knife"), \ + /obj/item/rogueweapon/huntingknife/idagger/stake = TAT_ITEM_ENTRY("Stake", 1, "weapon", "weapon_supply", "knife", null), \ + /obj/item/rogueweapon/huntingknife/combat/fencerguy = TAT_ITEM_ENTRY("Grenzelhoftian Seax", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "knife"), \ + /obj/item/rogueweapon/greatsword/bsword/psy = TAT_ITEM_ENTRY("Forgoten Blade", 3, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "sword"), \ + /obj/item/flashlight/flare/torch = TAT_ITEM_ENTRY("Torch", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/flashlight/flare/torch/lantern = TAT_ITEM_ENTRY("Iron Lamptern", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/flashlight/flare/torch/lantern/bronzelamptern = TAT_ITEM_ENTRY("Bronze Lamptern", 0, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "adventur' supply"), \ + /obj/item/flashlight/flare/torch/metal = TAT_ITEM_ENTRY("Fietorch", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/clothing/under/roguetown/heavy_leather_pants/otavan/generic = TAT_ITEM_ENTRY("Fencing Breeches", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "pants"), \ + /obj/item/clothing/under/roguetown/trou/leather/atgervi = TAT_ITEM_ENTRY("Fur Pants", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "pants"), \ + /obj/item/clothing/shoes/roguetown/boots/leather= TAT_ITEM_ENTRY("Leather Boots", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "shoes"), \ + /obj/item/clothing/gloves/roguetown/angle/atgervi = TAT_ITEM_ENTRY("Fur-Lined Leather Gloves ", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "gloves"), \ + /obj/item/clothing/gloves/roguetown/chain/psydon = TAT_ITEM_ENTRY("Psydonic Gloves ", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "gloves"), \ + /obj/item/clothing/gloves/roguetown/angle/feld = TAT_ITEM_ENTRY("Stranger Doc Gloves ", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "gloves"), \ + /obj/item/clothing/gloves/roguetown/angle/phys = TAT_ITEM_ENTRY("Straying Surg Gloves ", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "gloves"), \ + /obj/item/clothing/shoes/roguetown/boots/leather/atgervi = TAT_ITEM_ENTRY("Atgervi Leather Boots", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "shoes"), \ + /obj/item/clothing/suit/roguetown/armor/leather/heavy/gronn = TAT_ITEM_ENTRY("Gronnic Ravager Mantle", 2, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/clothing/gloves/roguetown/angle/gronn = TAT_ITEM_ENTRY("Ravager Fur-Lined Leather Gloves", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "gloves"), \ + /obj/item/clothing/head/roguetown/helmet/bascinet/atgervi/gronn = TAT_ITEM_ENTRY("Gronnic Ravager Helm", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/suit/roguetown/armor/brigandine/gronn = TAT_ITEM_ENTRY("Gronn Byrine Hauberk", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "armor"), \ + /obj/item/clothing/head/roguetown/helmet/bascinet/atgervi/gronn/ownel = TAT_ITEM_ENTRY("Gronn Ownel Helm", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/clothing/suit/roguetown/armor/brigandine = TAT_ITEM_ENTRY("Steel Brigandine", 3, "clothing", "armor_family", TAT_ARMOR_MAIL, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/brigandine/light = TAT_ITEM_ENTRY("Lightweight Brigandine", 3, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/storage/belt/rogue/pouch/coins/poor = TAT_ITEM_ENTRY("Poor Coins Pouch", 0, "misc", "armor_family", TAT_ARMOR_CLOTH, "wealth"), \ + /obj/item/storage/belt/rogue/pouch/coins/mid = TAT_ITEM_ENTRY("Medium Coins Pouch", 2, "misc", "armor_family", TAT_ARMOR_CLOTH, "wealth"), \ + /obj/item/rogueweapon/scabbard/sword = TAT_ITEM_ENTRY("Scabbard", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "sheath"), \ + /obj/item/rogueweapon/scabbard/sheath = TAT_ITEM_ENTRY("Sheath", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "sheath"), \ + /obj/item/rogueweapon/huntingknife/idagger/steel/kazengun = TAT_ITEM_ENTRY("Tanto", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "knife"), \ + /obj/item/rogueweapon/sword/short/kazengun = TAT_ITEM_ENTRY("Kodachi", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/scabbard/sword/kazengun = TAT_ITEM_ENTRY("Simple Kazengun Scabbard", 4, "misc", "weapon_supply", TAT_SUPPLY_STEEL, "sheath"), \ + /obj/item/rogueweapon/sword/long/kriegmesser/ssangsudo = TAT_ITEM_ENTRY("Ssangsudo", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/scabbard/sheath/kazengun = TAT_ITEM_ENTRY("Plain Lacquer Sheath for Tanto", 0, "misc", "weapon_supply", TAT_SUPPLY_STEEL, "sheath"), \ + /obj/item/rogueweapon/scabbard/sword/kazengun/kodachi = TAT_ITEM_ENTRY("Plain Lacquer Sheath for Kodachi", 1, "misc", "weapon_supply", TAT_SUPPLY_STEEL, "sheath"), \ + /obj/item/rogueweapon/scabbard/gwstrap = TAT_ITEM_ENTRY("Greatweapon Strap", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "sheath"), \ + /obj/item/clothing/shoes/roguetown/boots/armor = TAT_ITEM_ENTRY("Plated Boots", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "shoes"), \ + /obj/item/clothing/head/roguetown/helmet = TAT_ITEM_ENTRY("Steel Nasal Helmet", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "head"), \ + /obj/item/rogueweapon/scabbard/sword/kazengun/noparry = TAT_ITEM_ENTRY("Ceremonial Kazengun Scabbard for Ssangsudo", 0, "misc", "weapon_supply", TAT_SUPPLY_STEEL, "sheath"), \ + /obj/item/clothing/suit/roguetown/armor/gambeson/heavy = TAT_ITEM_ENTRY("Padded Gambeson", 3, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/shirt/robe/monk = TAT_ITEM_ENTRY("Monk Vestments", 2, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/armor/gambeson/heavy/otavan = TAT_ITEM_ENTRY("fencing gambeson", 3, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/shirt/freifechter = TAT_ITEM_ENTRY("Padded Fencing Shirt", 3, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/armor/gambeson/heavy/chargah = TAT_ITEM_ENTRY("Padded Caftan", 3, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/armor/gambeson/heavy/grenzelhoft = TAT_ITEM_ENTRY("Grenzelhoftian Hip-Shirt", 3, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/armor/gambeson/heavy/raneshen = TAT_ITEM_ENTRY("Padded Desert Coat", 3, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/armor/gambeson/heavy/hierophant = TAT_ITEM_ENTRY("Hierophant's Shawl", 3, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/armor/gambeson/heavy/pontifex = TAT_ITEM_ENTRY("Pontifex's Kaftan", 3, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/armor/gambeson/light = TAT_ITEM_ENTRY("Light Gambeson", 1, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/armor/gambeson/lord = TAT_ITEM_ENTRY("Arming Jacket", 2, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/armor/basiceast/mentorsuit = TAT_ITEM_ENTRY("Old Dobo Robe", 2, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/basiceast = TAT_ITEM_ENTRY("Simple Dobo Robe", 2, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/brigandine/haraate = TAT_ITEM_ENTRY("Hansimhae Cuirass", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "armor"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/psydonbarbute = TAT_ITEM_ENTRY("Psydonic Barbute", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/under/roguetown/heavy_leather_pants/eastpants2 = TAT_ITEM_ENTRY("Strange Ripped Pants", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "pants"), \ + /obj/item/clothing/under/roguetown/heavy_leather_pants/kazengun = TAT_ITEM_ENTRY("Baggy Pants", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "pants"), \ + /obj/item/clothing/under/roguetown/heavy_leather_pants/otavan/shepherd = TAT_ITEM_ENTRY("Shepherd Leather Pants", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "pants"), \ + /obj/item/clothing/head/roguetown/mentorhat = TAT_ITEM_ENTRY("Bamboo Hat", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "head"), \ + /obj/item/rogueweapon/hammer/iron = TAT_ITEM_ENTRY("Iron Hammer", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "smith"), \ + /obj/item/rogueweapon/tongs = TAT_ITEM_ENTRY("Iron Tongs", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "smith"), \ + /obj/item/rogueweapon/hammer/steel = TAT_ITEM_ENTRY("Steel Hammer", 2, "misc", "weapon_supply", TAT_SUPPLY_STEEL, "smith"), \ + /obj/item/lockpickring/mundane = TAT_ITEM_ENTRY("Lockpick Ring", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/rogueweapon/blowrod = TAT_ITEM_ENTRY("Blowing Rod", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "lyfe"), \ + /obj/item/rogueweapon/shovel = TAT_ITEM_ENTRY("Shovel", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "misc"), \ + /obj/item/storage/belt/rogue/surgery_bag = TAT_ITEM_ENTRY("Surgeon's Bag", 3, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/storage/backpack/rogue/backpack = TAT_ITEM_ENTRY("Backpack", 1.5, "misc", "armor_family", TAT_ARMOR_CLOTH, "back"), \ + /obj/item/storage/gadget/messkit = TAT_ITEM_ENTRY("Mess Kit", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/reagent_containers/glass/cup/wooden = TAT_ITEM_ENTRY("Wooden Cup", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/bedroll = TAT_ITEM_ENTRY("Bedroll", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/rogueweapon/halberd/bardiche = TAT_ITEM_ENTRY("Bardiche", 3, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "polearm"), \ + /obj/item/rogueweapon/mace/goden/steel = TAT_ITEM_ENTRY("Grand Mace", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "blunt"), \ + /obj/item/rogueweapon/mace/cudgel = TAT_ITEM_ENTRY("Cudgel", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "blunt"), \ + /obj/item/rogueweapon/mace/cudgel/psyclassic/old = TAT_ITEM_ENTRY("Enduring Handmace", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "blunt"), \ + /obj/item/rogueweapon/mace/cudgel/copper = TAT_ITEM_ENTRY("Copper Bludgeon", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "blunt"), \ + /obj/item/rogueweapon/mace/goden = TAT_ITEM_ENTRY("Goedendag", 3, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "blunt"), \ + /obj/item/rogueweapon/mace/goden/kanabo = TAT_ITEM_ENTRY("Kanabo", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "blunt"), \ + /obj/item/rogueweapon/mace/goden/psymace = TAT_ITEM_ENTRY("Psydonic Mace", 4, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "blunt"), \ + /obj/item/rogueweapon/shield/wood = TAT_ITEM_ENTRY("Wooden Shield", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "shield"), \ + /obj/item/rogueweapon/shield/wood/deprived = TAT_ITEM_ENTRY("Ghastly Shield", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "shield"), \ + /obj/item/rogueweapon/shield/tower/metal = TAT_ITEM_ENTRY("Kite Shield", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "shield"), \ + /obj/item/rogueweapon/shield/tower/raneshen = TAT_ITEM_ENTRY("Rider Shield", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "shield"), \ + /obj/item/rogueweapon/shield/buckler = TAT_ITEM_ENTRY("Iron Buckler", 3, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "shield"), \ + /obj/item/rogueweapon/shield/heater = TAT_ITEM_ENTRY("Heater Shield", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "shield"), \ + /obj/item/rogueweapon/shield/iron = TAT_ITEM_ENTRY("Iron Shield", 3, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "shield"), \ + /obj/item/rogueweapon/shield/bronze = TAT_ITEM_ENTRY("Hoplon Shield", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "shield"), \ + /obj/item/rogueweapon/shield/bronze/great = TAT_ITEM_ENTRY("Hoplon Greatshield", 3, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "shield"), \ + /obj/item/rogueweapon/shield/iron/steppesman = TAT_ITEM_ENTRY("Steppesman Shield", 3, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "shield"), \ + /obj/item/rogueweapon/stoneaxe/oath = TAT_ITEM_ENTRY("Oath", 5, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "axe"), \ + /obj/item/rogueweapon/stoneaxe/woodcut/woodcutter = TAT_ITEM_ENTRY("Woodcutter's Axe", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "axe"), \ + /obj/item/rogueweapon/stoneaxe/hurlbat = TAT_ITEM_ENTRY("Hurlbat", 1.5, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "axe"), \ + /obj/item/rogueweapon/stoneaxe/handaxe/copper = TAT_ITEM_ENTRY("Copper Hatchet", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "axe"), \ + /obj/item/rogueweapon/stoneaxe/handaxe = TAT_ITEM_ENTRY("Hatchet", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "axe"), \ + /obj/item/rogueweapon/stoneaxe/woodcut/bronze = TAT_ITEM_ENTRY("Bronze Axe", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "axe"), \ + /obj/item/rogueweapon/stoneaxe/woodcut/steel/woodcutter = TAT_ITEM_ENTRY("Steel Woodcutter's Axe", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "axe"), \ + /obj/item/rogueweapon/stoneaxe/battle/steppesman/chupa = TAT_ITEM_ENTRY("Aavnic Ciupaga", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "axe"), \ + /obj/item/rogueweapon/greataxe/steel/knight = TAT_ITEM_ENTRY("Poleaxe", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "axe"), \ + /obj/item/rogueweapon/stoneaxe/woodcut/troll = TAT_ITEM_ENTRY("Crude Heavy Axe", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "axe"), \ + /obj/item/rogueweapon/sword/falchion/militia/bronze = TAT_ITEM_ENTRY("kopis", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "sword"), \ + /obj/item/rogueweapon/whip = TAT_ITEM_ENTRY("Leather Whip", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "whip"), \ + /obj/item/rogueweapon/whip/nagaika = TAT_ITEM_ENTRY("Nagaika Whip", 2, "weapon", "weapon_supply", TAT_ARMOR_LEATHER, "whip"), \ + /obj/item/rogueweapon/whip/psywhip_lesser = TAT_ITEM_ENTRY("Psydonic Whip", 3, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "whip"), \ + /obj/item/rogueweapon/handclaw = TAT_ITEM_ENTRY("Ravager Claws", 2.5, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "unarmed"), \ + /obj/item/rogueweapon/handclaw/gronn/silver = TAT_ITEM_ENTRY("Silver Claws", 5, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "unarmed"), \ + /obj/item/rogueweapon/sword/long/oldpsysword = TAT_ITEM_ENTRY("Enduring Longsword", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/quiver/javelin/bronze = TAT_ITEM_ENTRY("Bronze Javelins", 3, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "munition"), \ + /obj/item/quiver/javelin/iron = TAT_ITEM_ENTRY("Iron Javelins", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "munition"), \ + /obj/item/quiver/javelin/steel = TAT_ITEM_ENTRY("Steel Javelins", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "munition"), \ + /obj/item/quiver/bolt/bronze = TAT_ITEM_ENTRY("Bronze Bolts", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "munition"), \ + /obj/item/quiver/Warrows = TAT_ITEM_ENTRY("Water Arrows", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "munition"), \ + /obj/item/runicflask = TAT_ITEM_ENTRY("Runic Flask", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "munition"), \ + /obj/item/twstrap/bombstrap/firebomb = TAT_ITEM_ENTRY("Explosive's Belt", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "bombs"), \ + /obj/item/twstrap/bombstrap/bomb_and_fire = TAT_ITEM_ENTRY("Greater Explosive's Belt", 3, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "bombs"), \ + /obj/item/smokeshell = TAT_ITEM_ENTRY("Empty Bomb Shell", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "bombs"), \ + /obj/item/quiver/sling/fire_pot = TAT_ITEM_ENTRY("Fire Pots for Slings", 2.5, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "munition"), \ + /obj/item/rogueweapon/wand = TAT_ITEM_ENTRY("Lesser Wand", 3, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "magic"), \ + /obj/item/rogueweapon/wand/greater = TAT_ITEM_ENTRY("Greater Wand", 5, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "magic"), \ + /obj/item/rogueweapon/woodstaff = TAT_ITEM_ENTRY("Wooden Staff", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "magic"), \ + /obj/item/rogueweapon/woodstaff/implement = TAT_ITEM_ENTRY("Lesser Staff", 3, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "magic"), \ + /obj/item/rogueweapon/woodstaff/implement/greater = TAT_ITEM_ENTRY("Greater Staff", 5, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "magic"), \ + /obj/item/rogueweapon/woodstaff/implement/grand/naledi = TAT_ITEM_ENTRY("Naledi Staff", 8, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "magic"), \ + /obj/item/rogueweapon/spear/billhook = TAT_ITEM_ENTRY("Billhook", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "polearm"), \ + /obj/item/rogueweapon/spear/stone/copper = TAT_ITEM_ENTRY("Copper Spear", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "polearm"), \ + /obj/item/clothing/gloves/roguetown/chain/contraption/voltic = TAT_ITEM_ENTRY("Voltic Gauntlet", 4, "weapon", "weapon_supply", TAT_SUPPLY_ARTIFACTS, "artifact"), \ + /obj/item/clothing/ring/active/shimmeringlens = TAT_ITEM_ENTRY("Shimmering Lens", 6, "weapon", "weapon_supply", TAT_SUPPLY_ARTIFACTS, "artifact"), \ + /obj/item/lantern/fog_repelling/empty = TAT_ITEM_ENTRY("Necran Lamptern", 1, "weapon", "weapon_supply", TAT_SUPPLY_ARTIFACTS, "artifact"), \ + /obj/item/reagent_containers/glass/bottle/sanctified_oil = TAT_ITEM_ENTRY("Sacrificed Oil", 0.5, "weapon", "weapon_supply", TAT_SUPPLY_ARTIFACTS, "artifact"), \ + /obj/item/flashlight/flare/torch/lantern/bronzelamptern/malums_lamptern = TAT_ITEM_ENTRY("Malum's Shield", 5, "weapon", "weapon_supply", TAT_SUPPLY_ARTIFACTS, "artifact"), \ + /obj/item/rogueweapon/mace/mushroom = TAT_ITEM_ENTRY("Lithmyc Mace", 13, "weapon", "weapon_supply", TAT_SUPPLY_ARTIFACTS, "artifact"), \ + /obj/item/rogueweapon/huntingknife/idagger/steel/fire = TAT_ITEM_ENTRY("Fire Dagger", 5, "weapon", "weapon_supply", TAT_SUPPLY_ARTIFACTS, "artifact"), \ + /obj/item/rogueweapon/mace/goden/deepduke = TAT_ITEM_ENTRY("Duke's Mace", 6, "weapon", "weapon_supply", TAT_SUPPLY_ARTIFACTS, "artifact"), \ + /obj/item/rogueweapon/stoneaxe/battle/ice = TAT_ITEM_ENTRY("Deathfrost Axe", 10, "weapon", "weapon_supply", TAT_SUPPLY_ARTIFACTS, "artifact"), \ + /obj/item/rogueweapon/sword/long/exe/berserk = TAT_ITEM_ENTRY("Berserk Sword", 11, "weapon", "weapon_supply", TAT_SUPPLY_ARTIFACTS, "artifact"), \ + /obj/item/rogueweapon/sword/sabre/bane = TAT_ITEM_ENTRY("Bane's Edge", 13, "weapon", "weapon_supply", TAT_SUPPLY_ARTIFACTS, "artifact"), \ + /obj/item/rogueweapon/shield/tower/metal/psy = TAT_ITEM_ENTRY("Psydonic Shield", 5, "weapon", "weapon_supply", TAT_SUPPLY_ARTIFACTS, "artifact"), \ + /obj/item/rope/chain = TAT_ITEM_ENTRY("Chain", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/rope = TAT_ITEM_ENTRY("Rope", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/bomb/smoke = TAT_ITEM_ENTRY("Smoke Bomb", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "bombs"), \ + /obj/item/bomb = TAT_ITEM_ENTRY("Bottle Bomb", 0.5, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "bombs"), \ + /obj/item/impact_grenade = TAT_ITEM_ENTRY("Impact Bomb", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "bombs"), \ + /obj/item/folding_alchstation_stored = TAT_ITEM_ENTRY("Alchemical station", 3, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "adventur' supply"), \ + /obj/item/folding_alchcauldron_stored = TAT_ITEM_ENTRY("Alchemical cauldron", 3, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "adventur' supply"), \ + /obj/item/ration = TAT_ITEM_ENTRY("Ration paper", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "food"), \ + /obj/item/natural/bundle/cloth/bandage/full = TAT_ITEM_ENTRY("Roll of Bandages", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/natural/cloth/bandage = TAT_ITEM_ENTRY("Dirty Brown Bandage", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/tent_kit = TAT_ITEM_ENTRY("Tent", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/tent_kit/ger = TAT_ITEM_ENTRY("Ger Tent", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/tent_kit/yurt = TAT_ITEM_ENTRY("Yurt Tent", 1.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/reagent_containers/glass/bottle/waterskin = TAT_ITEM_ENTRY("Water Skin", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/clothing/gloves/roguetown/bandages/weighted = TAT_ITEM_ENTRY("Weighted Bandages", 2, "clothing", "armor_family", TAT_ARMOR_CLOTH, "gloves"), \ + /obj/item/clothing/suit/roguetown/shirt/robe/pointfex = TAT_ITEM_ENTRY("Pointfex's Qaba", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "cloak"), \ + /obj/item/clothing/suit/roguetown/shirt/robe/hierophant = TAT_ITEM_ENTRY("Hierophant's Handys", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "cloak"), \ + /obj/item/clothing/suit/roguetown/armor/heartfelt = TAT_ITEM_ENTRY("Lordly Plate", 3.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/heartfelt/hand = TAT_ITEM_ENTRY("Coat of Plate", 3.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/suit/roguetown/armor/brigandine/heavy = TAT_ITEM_ENTRY("Coat of Plates", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/gloves/roguetown/chain/gronn = TAT_ITEM_ENTRY("Gronn Byrine Gloves", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "gloves"), \ + /obj/item/clothing/gloves/roguetown/chain = TAT_ITEM_ENTRY("Сhain Gauntlets", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "gloves"), \ + /obj/item/clothing/gloves/roguetown/chain/iron = TAT_ITEM_ENTRY("Iron Сhain Gauntlets", 1.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "gloves"), \ + /obj/item/clothing/under/roguetown/heavy_leather_pants/eastpants1 = TAT_ITEM_ENTRY("Cut-throat pants", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "pants"), \ + /obj/item/clothing/neck/roguetown/leather = TAT_ITEM_ENTRY("Hardened Leather Gorget", 0.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "neck"), \ + /obj/item/gun/ballistic/revolver/grenadelauncher/bow = TAT_ITEM_ENTRY("Crude Selfbow", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "ranged"), \ + /obj/item/rogueweapon/shield/atgervi = TAT_ITEM_ENTRY("Gronnic Kite Shield", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "shield"), \ + /obj/item/clothing/head/roguetown/helmet/bascinet/atgervi = TAT_ITEM_ENTRY("Owl Helmet", 3.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/suit/roguetown/armor/chainmail/hauberk/atgervi = TAT_ITEM_ENTRY("Varangian Hauberk", 3.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "unterarmor"), \ + /obj/item/reagent_containers/glass/bottle/alchemical/healthpot = TAT_ITEM_ENTRY("Health Vial", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/reagent_containers/glass/bottle/alchemical/manapot = TAT_ITEM_ENTRY("Mana Vial", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/storage/belt/rogue/pouch/medicine = TAT_ITEM_ENTRY("Medical supplies", 2.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/reagent_containers/glass/bottle/alchemical/strpot = TAT_ITEM_ENTRY("Strength Vial", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/reagent_containers/glass/bottle/alchemical/perpot = TAT_ITEM_ENTRY("Perception Vial", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/reagent_containers/glass/bottle/alchemical/conpot = TAT_ITEM_ENTRY("Constitution Vial", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/reagent_containers/glass/bottle/alchemical/spdpot = TAT_ITEM_ENTRY("Haste Vial", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/reagent_containers/glass/bottle/alchemical/lucpot = TAT_ITEM_ENTRY("Lucky Vial", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/reagent_containers/glass/bottle/rogue/manapot = TAT_ITEM_ENTRY("Mana Bottle", 3, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/repair_kit/metal/bad = TAT_ITEM_ENTRY("Scrap Kit", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/repair_kit/metal = TAT_ITEM_ENTRY("Plate's kit", 4, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/repair_kit/bad = TAT_ITEM_ENTRY("Fabric Patch", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/storage/hip/headhook = TAT_ITEM_ENTRY("Head Hook", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/storage/hip/headhook/bronze = TAT_ITEM_ENTRY("Bronze Head Hook", 2, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "adventur' supply"), \ + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/fluted/ornate = TAT_ITEM_ENTRY("Psydonic Cuirass", 2.5, "clothing", "armor_family", TAT_ARMOR_MAIL, "armor"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/knight/old/iron = TAT_ITEM_ENTRY("Iron Knight's Helm", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/knight/old = TAT_ITEM_ENTRY("Knight's Helm", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/knight = TAT_ITEM_ENTRY("Knight Armet", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/knight/armet = TAT_ITEM_ENTRY("Armet", 2.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/knight/iron = TAT_ITEM_ENTRY("Knight Helmet", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/sheriff = TAT_ITEM_ENTRY("Barred Helmet", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/guard/bogman = TAT_ITEM_ENTRY("Steel Bogman's Helmet", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/guard = TAT_ITEM_ENTRY("Guard Helmet", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/rogueweapon/woodstaff/quarterstaff = TAT_ITEM_ENTRY("Wooden Battle Staff", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "polearm"), \ + /obj/item/rogueweapon/woodstaff/quarterstaff/iron = TAT_ITEM_ENTRY("Iron Quatterstaff", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "polearm"), \ + /obj/item/clothing/neck/roguetown/fencerguard = TAT_ITEM_ENTRY("Fencer Collar", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "neck"), \ + /obj/item/rogueweapon/woodstaff/quarterstaff/steel = TAT_ITEM_ENTRY("Steel Quatterstaff", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "polearm"), \ + /obj/item/clothing/neck/roguetown/gorget/steel/kazengun = TAT_ITEM_ENTRY("Kazengunite Gorget", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "neck"), \ + /obj/item/clothing/suit/roguetown/armor/leather/heavy/coat/gravecoat = TAT_ITEM_ENTRY("Gravetender's Coat", 2, "clothing", "weapon_supply", TAT_SUPPLY_SILVER, "armor"),\ + /obj/item/clothing/head/roguetown/inqhat/gravehat = TAT_ITEM_ENTRY("Gravetender's Hat", 1.5, "clothing", "weapon_supply", TAT_SUPPLY_SILVER, "head"),\ + /obj/item/clothing/neck/roguetown/psicross/noc = TAT_ITEM_ENTRY("Noc Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/naledi = TAT_ITEM_ENTRY("Naledian Bracelet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/noc/bronze = TAT_ITEM_ENTRY("Bronze Noc Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/noc/aalloy = TAT_ITEM_ENTRY("Decreipt Noc Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross = TAT_ITEM_ENTRY("Psycross", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/reform = TAT_ITEM_ENTRY("Reformist Cross", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/inhumen/aalloy = TAT_ITEM_ENTRY("Zizo Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/inhumen/iron = TAT_ITEM_ENTRY("Zizo Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/inhumen/matthios = TAT_ITEM_ENTRY("Matthios Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/inhumen/graggar = TAT_ITEM_ENTRY("Graggar Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/inhumen/baotha = TAT_ITEM_ENTRY("Baotha Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/undivided = TAT_ITEM_ENTRY("Tennit Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/astrata = TAT_ITEM_ENTRY("Astrata Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/abyssor = TAT_ITEM_ENTRY("Abyssor Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/dendor = TAT_ITEM_ENTRY("Dendor Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/necra = TAT_ITEM_ENTRY("Necra Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/pestra = TAT_ITEM_ENTRY("Pestra Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/ravox = TAT_ITEM_ENTRY("Ravox Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/inhumen/bronze = TAT_ITEM_ENTRY("Bronze Zizo Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/ravox/bronze = TAT_ITEM_ENTRY("Bronze Ravox Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/astrata/bronze = TAT_ITEM_ENTRY("Bronze Astrata Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/malum/bronze = TAT_ITEM_ENTRY("Bronze Malum Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/inhumen/graggar/bronze = TAT_ITEM_ENTRY("Bronze Graggar Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/malum = TAT_ITEM_ENTRY("Malum Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/eora = TAT_ITEM_ENTRY("Eora Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/xylix = TAT_ITEM_ENTRY("Xylix Amulet", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/wood = TAT_ITEM_ENTRY("Wooden Psycross", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/bronze = TAT_ITEM_ENTRY("Bronze Psycross", 0, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "cross"),\ + /obj/item/clothing/shoes/roguetown/grenzelhoft/freifechter = TAT_ITEM_ENTRY("Fencer Boots", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "shoes"), \ + /obj/item/reagent_containers/food/snacks/rogue/crackerscooked = TAT_ITEM_ENTRY("Crackers", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "food"), \ + /obj/item/reagent_containers/food/snacks/rogue/raisinbread = TAT_ITEM_ENTRY("Raisin Bread", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "food"), \ + /obj/item/reagent_containers/food/snacks/rogue/meat/coppiette = TAT_ITEM_ENTRY("Coppiette", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "food"), \ + /obj/item/reagent_containers/food/snacks/rogue/bread = TAT_ITEM_ENTRY("Bread", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "food"), \ + /obj/item/reagent_containers/glass/bottle/rogue/beer = TAT_ITEM_ENTRY("Beer", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "food"), \ + /obj/item/reagent_containers/food/snacks/rogue/meat/salami = TAT_ITEM_ENTRY("Salami", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "food"), \ + /obj/item/clothing/head/roguetown/headband/monk/barbarian = TAT_ITEM_ENTRY("Hunter's Headband", 1, "clothing", "armor_family", TAT_ARMOR_LEATHER, "head"), \ + /obj/item/clothing/head/roguetown/grenzelhofthat = TAT_ITEM_ENTRY("Plume Hat", 1, "clothing", "armor_family", TAT_ARMOR_LEATHER, "head"), \ + /obj/item/gun/ballistic/revolver/grenadelauncher/crossbow/heavy = TAT_ITEM_ENTRY("Siegebow", 4, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "ranged"), \ + /obj/item/quiver/bolt/heavy/standard = TAT_ITEM_ENTRY("Heavy Bolts", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "munition"), \ + /obj/item/quiver/bolt/heavy/bronze = TAT_ITEM_ENTRY("Heavy Bronze Bolts", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "munition"), \ + /obj/item/quiver/bolt/heavy/blunt = TAT_ITEM_ENTRY("Heavy Blunt Bolts", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "munition"), \ + /obj/item/quiver/bolt/heavy/silver = TAT_ITEM_ENTRY("Heavy Silver Bolts", 2, "weapon", "weapon_supply", TAT_SUPPLY_SILVER, "munition"), \ + /obj/item/needle = TAT_ITEM_ENTRY("Needle", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/needle/thorn = TAT_ITEM_ENTRY("Needle", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/needle/bronze = TAT_ITEM_ENTRY("Needle", 0, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "medical"), \ + /obj/item/skillbook/unfinished = TAT_ITEM_ENTRY("Unfinished Skill Book", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "lyfe"), \ + /obj/item/storage/meatbag = TAT_ITEM_ENTRY("Game Satchel", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "lyfe"), \ + /obj/item/pestle = TAT_ITEM_ENTRY("Pestle", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/reagent_containers/glass/mortar = TAT_ITEM_ENTRY("Mortar", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/chalk = TAT_ITEM_ENTRY("Chalk", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "magic"), \ + /obj/item/quiver/sling/iron = TAT_ITEM_ENTRY("Iron Slingshots ", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "munition"), \ + /obj/item/quiver/sling/steel = TAT_ITEM_ENTRY("Steel Slingshots", 2, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "munition"), \ + /obj/item/quiver/sling/stone = TAT_ITEM_ENTRY("Stone Slingshots", 0, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "munition"), \ + /obj/item/quiver/sling/bronze = TAT_ITEM_ENTRY("Bronze Slingshots", 1, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "munition"), \ + /obj/item/gun/ballistic/revolver/grenadelauncher/sling = TAT_ITEM_ENTRY("Sling ", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "ranged"), \ + /obj/item/rogue/instrument/lute = TAT_ITEM_ENTRY("Lute", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "music"), \ + /obj/item/rogue/instrument/accord = TAT_ITEM_ENTRY("Accord", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "music"), \ + /obj/item/rogue/instrument/guitar = TAT_ITEM_ENTRY("Guitar", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "music"), \ + /obj/item/rogue/instrument/harp = TAT_ITEM_ENTRY("Harp", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "music"), \ + /obj/item/rogue/instrument/flute = TAT_ITEM_ENTRY("Flute", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "music"), \ + /obj/item/rogue/instrument/drum = TAT_ITEM_ENTRY("Drum", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "music"), \ + /obj/item/rogue/instrument/shamisen = TAT_ITEM_ENTRY("Shamisen", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "music"), \ + /obj/item/rogue/instrument/vocals = TAT_ITEM_ENTRY("Vocal's Talisman", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "music"), \ + /obj/item/rogue/instrument/viola = TAT_ITEM_ENTRY("Viola", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "music"), \ + /obj/item/clothing/suit/roguetown/armor/gambeson = TAT_ITEM_ENTRY("Gambeson", 2, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/rogueweapon/hoe = TAT_ITEM_ENTRY("Hoe", 0.5, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "misc"), \ + /obj/item/rogueweapon/hoe/bronze = TAT_ITEM_ENTRY("Bronze Hoe", 0.5, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "misc"), \ + /obj/item/rogueweapon/shovel/bronze = TAT_ITEM_ENTRY("Bronze Shovel", 0.5, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "misc"), \ + /obj/item/rogueweapon/sickle = TAT_ITEM_ENTRY("Sickle", 0.5, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "misc"), \ + /obj/item/fishingrod/crafted = TAT_ITEM_ENTRY("Fishing Rod", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "lyfe"), \ + /obj/item/fishingrod/bronze = TAT_ITEM_ENTRY("Bronze Fishing Rod", 0.5, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "lyfe"), \ + /obj/item/cooking/pan = TAT_ITEM_ENTRY("Frying Pan", 0.5, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "misc"), \ + /obj/item/reagent_containers/glass/bucket/pot/bronze = TAT_ITEM_ENTRY("Bronze Pot", 0, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "misc"), \ + /obj/item/reagent_containers/glass/bucket/pot/stone = TAT_ITEM_ENTRY("Stone Pot", 0, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "misc"), \ + /obj/item/reagent_containers/glass/bucket/pot = TAT_ITEM_ENTRY("Iron Pot", 0.5, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "misc"), \ + /obj/item/reagent_containers/glass/bucket/pot/kettle/tankard = TAT_ITEM_ENTRY("Stein", 0.5, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "misc"), \ + /obj/item/cooking/pan/bronze = TAT_ITEM_ENTRY("Bronze Frying Pan", 0, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "misc"), \ + /obj/item/cooking/pan/aalloy = TAT_ITEM_ENTRY("Frying Pan", 0, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "misc"), \ + /obj/item/reagent_containers/glass/bottle/waterskin/milk = TAT_ITEM_ENTRY("Not Milk....", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "food"), \ + /obj/item/storage/backpack/rogue/satchel/short = TAT_ITEM_ENTRY("Short Satchel", 1.5, "misc", "armor_family", TAT_ARMOR_CLOTH, "back"), \ + /obj/item/storage/backpack/rogue/satchel = TAT_ITEM_ENTRY("Satchel", 1, "misc", "armor_family", TAT_ARMOR_CLOTH, "back"), \ + /obj/item/clothing/gloves/roguetown/fingerless = TAT_ITEM_ENTRY("Fingerless Gloves", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "gloves"), \ + /obj/item/clothing/gloves/roguetown/fingerless_leather = TAT_ITEM_ENTRY("Fingerless Leather Gloves", 1, "clothing", "armor_family", TAT_ARMOR_CLOTH, "gloves"), \ + /obj/item/clothing/under/roguetown/heavy_leather_pants/shadowpants = TAT_ITEM_ENTRY("Silk Tightss", 1.5, "clothing", "armor_family", TAT_ARMOR_LEATHER, "pants"), \ + /obj/item/clothing/suit/roguetown/shirt/shadowshirt = TAT_ITEM_ENTRY("Silk Shirt", 1, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/shirt/robe = TAT_ITEM_ENTRY("Robe", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/shirt/undershirt/lowcut = TAT_ITEM_ENTRY("Low-cut Tunic", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/shirt/robe/necra = TAT_ITEM_ENTRY("Mourning Robe", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "under cloak"), \ + /obj/item/clothing/suit/roguetown/shirt/robe/dendor = TAT_ITEM_ENTRY("Dendorit Robe", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "under cloak"), \ + /obj/item/clothing/suit/roguetown/shirt/robe/abyssor = TAT_ITEM_ENTRY("Abyssor Robe", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "under cloak"), \ + /obj/item/clothing/suit/roguetown/shirt/robe/noc = TAT_ITEM_ENTRY("Noc Robe", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "under cloak"), \ + /obj/item/clothing/suit/roguetown/shirt/robe/astrata = TAT_ITEM_ENTRY("Astratan Robe", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "under cloak"), \ + /obj/item/clothing/suit/roguetown/shirt/tunic = TAT_ITEM_ENTRY("Tunic", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "suit"), \ + /obj/item/clothing/suit/roguetown/shirt/undershirt/artificer = TAT_ITEM_ENTRY("Tinker Jacket", 0, "clothing", "weapon_supply", TAT_SUPPLY_BRONZE, "suit"), \ + /obj/item/clothing/suit/roguetown/armor/leather/jacket/artijacket = TAT_ITEM_ENTRY("Tinker Suit", 0, "clothing", "weapon_supply", TAT_SUPPLY_BRONZE, "armor"), \ + /obj/item/book/rogue/bibble/psy = TAT_ITEM_ENTRY("Psy Bible", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "lyfe"), \ + /obj/item/book/rogue/bibble = TAT_ITEM_ENTRY("Tennit Bible", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "lyfe"), \ + /obj/item/bottle_kit = TAT_ITEM_ENTRY("Bottling Kit", 4, "misc", "weapon_supply", TAT_SUPPLY_IRON, "lyfe"), \ + /obj/item/natural/worms/leech/cheele = TAT_ITEM_ENTRY("Cheele", 3, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/heart_blood_canister/filled = TAT_ITEM_ENTRY("Heartblood Canister", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/bait/leech = TAT_ITEM_ENTRY("Leech Bait", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "medical"), \ + /obj/item/flint = TAT_ITEM_ENTRY("Flint", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/folding_table_stored = TAT_ITEM_ENTRY("Folding Table", 1, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "adventur' supply"), \ + /obj/item/rogueweapon/pick = TAT_ITEM_ENTRY("Pickaxe", 2, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "misc"), \ + /obj/item/rogueweapon/pick/steel = TAT_ITEM_ENTRY("Steel Pickaxe", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "misc"), \ + /obj/item/rogueweapon/pick/copper = TAT_ITEM_ENTRY("Copper Pickaxe", 1, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "misc"), \ + /obj/item/rogueweapon/pick/bronze = TAT_ITEM_ENTRY("Dolabra", 2, "weapon", "weapon_supply", TAT_SUPPLY_BRONZE, "axe"), \ + /obj/item/repair_kit = TAT_ITEM_ENTRY("Fabric Patch", 4, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/millstone = TAT_ITEM_ENTRY("Millstone", 2, "misc", "weapon_supply", TAT_SUPPLY_IRON, "lyfe"), \ + /obj/item/kitchen/rollingpin = TAT_ITEM_ENTRY("Rolling Pin", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "lyfe"), \ + /obj/item/gun/ballistic/arquebus_pistol = TAT_ITEM_ENTRY("Arquebus Pistol", 7, "weapon", "weapon_supply", TAT_SUPPLY_FIREARMS, "blackpowder"), \ + /obj/item/natural/feather = TAT_ITEM_ENTRY("Feather", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "lyfe"), \ + /obj/item/clothing/head/roguetown/helmet/heavy/bucket/gronn = TAT_ITEM_ENTRY("Gronn Norsii horned helmet", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "head"), \ + /obj/item/clothing/head/roguetown/articap = TAT_ITEM_ENTRY("Tinker Hat", 0, "clothing", "weapon_supply", TAT_SUPPLY_BRONZE, "head"), \ + /obj/item/clothing/suit/roguetown/armor/plate/iron/gronn = TAT_ITEM_ENTRY("Gronn Norsii Iron Plate", 3.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "armor"), \ + /obj/item/clothing/gloves/roguetown/plate/iron/gronn = TAT_ITEM_ENTRY("Gronn Norsii Iron Gauntlets", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "gloves"), \ + /obj/item/clothing/gloves/roguetown/plate/iron/banded = TAT_ITEM_ENTRY("Branded Iron Gauntlets", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "gloves"), \ + /obj/item/clothing/under/roguetown/platelegs/iron/gronn = TAT_ITEM_ENTRY("Gronn Norsii Plate Legs", 1.5, "clothing", "armor_family", TAT_ARMOR_PLATE, "pants"), \ + /obj/item/clothing/shoes/roguetown/boots/armor/iron/gronn = TAT_ITEM_ENTRY("Gronn Norsii Iron Plated Boots", 1, "clothing", "armor_family", TAT_ARMOR_PLATE, "shoes"), \ + /obj/item/gun/ballistic/handgonne = TAT_ITEM_ENTRY("Culverin", 7, "weapon", "weapon_supply", TAT_SUPPLY_FIREARMS, "blackpowder"), \ + /obj/item/grapplinghook = TAT_ITEM_ENTRY("Grappling Hook", 8, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "adventur' supply"), \ + /obj/item/rogueweapon/chisel = TAT_ITEM_ENTRY("Chisel", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "wood work"), \ + /obj/item/rogueweapon/hammer/wood = TAT_ITEM_ENTRY("Wooden Hammer", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "wood work"), \ + /obj/item/rogueweapon/chisel/bronze = TAT_ITEM_ENTRY("Bronze Chisel", 0.5, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "wood work"), \ + /obj/item/rogueweapon/handsaw = TAT_ITEM_ENTRY("Handsaw", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "wood work"), \ + /obj/item/rogueweapon/handsaw/bronze = TAT_ITEM_ENTRY("Bronze Handsaw", 0.5, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "wood work"), \ + /obj/item/clothing/cloak/tabard = TAT_ITEM_ENTRY("Tabard", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/tabard/stabard = TAT_ITEM_ENTRY("Tabard", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/tabard/stabard/surcoat = TAT_ITEM_ENTRY("Surcoat", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/tabard/stabard/surcoat/short = TAT_ITEM_ENTRY("Short Surcoat", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/apron/cook = TAT_ITEM_ENTRY("Cook Tabard", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/sleevedtabard = TAT_ITEM_ENTRY("Sleeved Tabard", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/under/roguetown/tights/explorerpants = TAT_ITEM_ENTRY("Explorer Pants", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "pants"), \ + /obj/item/clothing/cloak/hierophant = TAT_ITEM_ENTRY("Hierophant Sash", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/templar/pestran = TAT_ITEM_ENTRY("Pestran Tabard", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/templar/malumite = TAT_ITEM_ENTRY("Malumite Tabard", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/templar/necran = TAT_ITEM_ENTRY("Necran Tabard", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/suit/roguetown/shirt/robe/eora = TAT_ITEM_ENTRY("Eoran Tabard", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/tabard/abyssorite = TAT_ITEM_ENTRY("Abyssor Tabard", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/tabard/devotee/ravox = TAT_ITEM_ENTRY("Ravox Tabard", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/templar/astratan = TAT_ITEM_ENTRY("Astratan Tabard", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/templar/undivided = TAT_ITEM_ENTRY("Undivided Tabard", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/templar/undivided_alt = TAT_ITEM_ENTRY("Undivided Tabard", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/eastcloak1 = TAT_ITEM_ENTRY("Leather Cloak", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/clothing/cloak/tabard/psydontabard = TAT_ITEM_ENTRY("Psydon Tabard", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "cloak"), \ + /obj/item/rogueweapon/handclaw/steel = TAT_ITEM_ENTRY("Steel Mantis Claws", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "unarmed"), \ + /obj/item/storage/magebag = TAT_ITEM_ENTRY("Scholar's Pouch", 1.5, "weapon", "weapon_supply", TAT_SUPPLY_IRON, "magic"), \ + /obj/item/clothing/head/roguetown/spellcasterhat = TAT_ITEM_ENTRY("Spellsinger Hat", 1, "clothing", "armor_family", TAT_ARMOR_LEATHER, "head"), \ + /obj/item/clothing/suit/roguetown/shirt/robe/spellcasterrobe = TAT_ITEM_ENTRY("Spellsinger Robes", 2, "clothing", "armor_family", TAT_ARMOR_LEATHER, "armor"), \ + /obj/item/rogueweapon/sword/sabre/shamshir = TAT_ITEM_ENTRY("Shamshir", 3, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/rogueweapon/sword/long/marlin = TAT_ITEM_ENTRY("Shalal", 3.5, "weapon", "weapon_supply", TAT_SUPPLY_STEEL, "sword"), \ + /obj/item/roguegear = TAT_ITEM_ENTRY("Cog", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"), \ + /obj/item/contraption/linker = TAT_ITEM_ENTRY("Wrench", 1, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "adventur' supply"), \ + /obj/item/reagent_containers/glass/bottle/waterskin/purifier = TAT_ITEM_ENTRY("Purifier", 3, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "adventur' supply"), \ + /obj/item/mobilestove = TAT_ITEM_ENTRY("Stove Kit", 1, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "adventur' supply"), \ + /obj/item/contraption/wood_metalizer = TAT_ITEM_ENTRY("Metallizer", 3, "misc", "weapon_supply", TAT_SUPPLY_BRONZE, "adventur' supply"), \ + /obj/item/clothing/shoes/roguetown/horseshoes = TAT_ITEM_ENTRY("Horseshoes", 0, "clothing", "armor_family", TAT_ARMOR_CLOTH, "shoes"), \ + /obj/item/clothing/shoes/roguetown/horseshoes/steel = TAT_ITEM_ENTRY("Horseshoes", 1.5, "clothing", "armor_family", TAT_ARMOR_CLOTH, "shoes"), \ + /obj/item/customlock = TAT_ITEM_ENTRY("Unfinished Lock", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "lyfe"), \ + /obj/item/roguekey/custom = TAT_ITEM_ENTRY("Custom Key", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "lyfe"), \ + /obj/item/clothing/neck/roguetown/psicross/inhumen/gronn = TAT_ITEM_ENTRY("Plotting Talisman", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/inhumen/baothagronn = TAT_ITEM_ENTRY("Relishing Talisman", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/inhumen/matthios/gronn = TAT_ITEM_ENTRY("Starving Talisman", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/inhumen/graggar/gronn = TAT_ITEM_ENTRY("Grinning Talisman", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/dendor/gronn = TAT_ITEM_ENTRY("Volfskinned Talisman", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/abyssor/gronn = TAT_ITEM_ENTRY("Hadal Talisman", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/clothing/neck/roguetown/psicross/inhumen/gronn/special = TAT_ITEM_ENTRY("Familial Talisman", 0, "misc", "weapon_supply", TAT_SUPPLY_IRON, "cross"),\ + /obj/item/book_crafting_kit = TAT_ITEM_ENTRY("Book kit", 1, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"),\ + /obj/item/paper = TAT_ITEM_ENTRY("Paper sheet", 0.5, "misc", "weapon_supply", TAT_SUPPLY_IRON, "adventur' supply"),\ + +GLOBAL_LIST_INIT(tat_available_items, list(TAT_AVAILABLE_ITEMS_LIST)) + +/proc/build_tat_item_icon_payload(item_path) + if(!ispath(item_path, /obj/item)) + return null + var/obj/item/path = item_path + var/icon_file = initial(path.icon) + var/icon_state_value = initial(path.icon_state) + if(!icon_file) + return null + var/icon/render_icon = icon(icon_file, icon_state_value, SOUTH, 1) + if(!render_icon) + return null + return list( + "icon" = icon2base64(render_icon), + "icon_state" = "[icon_state_value]", + ) + +/proc/warm_tat_item_catalog() + if(GLOB.tat_item_icon_cache_ready) + return + if(GLOB.tat_item_icon_cache_warming) + UNTIL(GLOB.tat_item_icon_cache_ready) + return + GLOB.tat_item_icon_cache_warming = TRUE + var/list/catalog = list() + for(var/item_path in GLOB.tat_available_items) + var/list/entry = GLOB.tat_available_items[item_path] + if(!islist(entry)) + continue + var/list/icon_payload = build_tat_item_icon_payload(item_path) + catalog["[item_path]"] = list( + "name" = entry["name"], + "cost" = entry["cost"], + "category" = entry["category"], + "unlock_type" = entry["unlock_type"], + "unlock_key" = entry["unlock_key"], + "slot_group" = entry["slot_group"], + "donat_tier" = round(entry["donat_tier"] || 0), + "loadout_only" = !!entry["loadout_only"], + "icon" = icon_payload?["icon"], + "icon_state" = icon_payload?["icon_state"], + ) + CHECK_TICK + GLOB.tat_item_catalog_cache = catalog + GLOB.tat_item_icon_cache_ready = TRUE + GLOB.tat_item_icon_cache_warming = FALSE diff --git a/modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_skills.dm b/modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_skills.dm new file mode 100644 index 00000000000..131c5c83076 --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_skills.dm @@ -0,0 +1,203 @@ +#define TAT_SKILL_COMBAT_CAP_DEFAULT 3 +#define TAT_SKILL_COMBAT_CAP_TRAIT_EXPERT 4 +#define TAT_SKILL_COMBAT_CAP_TRAIT_MASTER 5 + +#define TAT_SKILL_NONCOMBAT_CAP_BASIC_SYSTEM 5 +#define TAT_SKILL_NONCOMBAT_CAP_UNTRAITED 2 +#define TAT_SKILL_NONCOMBAT_CAP_SPECTRAIT 4 +#define TAT_SKILL_NONCOMBAT_CAP_ABSOLUTE 6 + +#define TAT_SKILL_BASIC_BOOST 2 +#define TAT_SKILL_DISCOUNT_BOOST 1 + +#define TAT_COMBAT_EXPERT_SKILL_LIMIT 2 +#define TAT_COMBAT_MASTER_SKILL_LIMIT 1 + +#define TAT_SKILL_DOMAIN_COMBAT "combat" +#define TAT_SKILL_DOMAIN_WANDERING "wandering" +#define TAT_SKILL_DOMAIN_GATHERING "gathering" +#define TAT_SKILL_DOMAIN_CRAFTING "crafting" +#define TAT_SKILL_DOMAIN_MISC "misc" + +#define TAT_SKILLS_COMBAT list( \ + /datum/skill/combat/knives, \ + /datum/skill/combat/swords, \ + /datum/skill/combat/polearms, \ + /datum/skill/combat/maces, \ + /datum/skill/combat/axes, \ + /datum/skill/combat/whipsflails, \ + /datum/skill/combat/bows, \ + /datum/skill/combat/crossbows, \ + /datum/skill/combat/wrestling, \ + /datum/skill/combat/unarmed, \ + /datum/skill/combat/shields, \ + /datum/skill/combat/slings, \ + /datum/skill/combat/staves, \ + /datum/skill/combat/firearms \ +) + +#define TAT_SKILLS_WANDERING list( \ + /datum/skill/misc/athletics, \ + /datum/skill/misc/climbing, \ + /datum/skill/misc/swimming, \ + /datum/skill/misc/riding, \ + /datum/skill/misc/tracking \ +) + +#define TAT_SKILLS_GATHERING list( \ + /datum/skill/labor/farming, \ + /datum/skill/labor/mining, \ + /datum/skill/labor/fishing, \ + /datum/skill/labor/butchering, \ + /datum/skill/labor/lumberjacking, \ + /datum/skill/misc/hunting \ +) + +#define TAT_SKILLS_CRAFTING list( \ + /datum/skill/craft/crafting, \ + /datum/skill/craft/weaponsmithing, \ + /datum/skill/craft/armorsmithing, \ + /datum/skill/craft/blacksmithing, \ + /datum/skill/craft/smelting, \ + /datum/skill/craft/carpentry, \ + /datum/skill/craft/masonry, \ + /datum/skill/craft/traps, \ + /datum/skill/craft/engineering, \ + /datum/skill/craft/cooking, \ + /datum/skill/craft/sewing, \ + /datum/skill/craft/tanning, \ + /datum/skill/craft/ceramics, \ + /datum/skill/craft/alchemy \ +) + +#define TAT_SKILLS_MISC list( \ + /datum/skill/misc/reading, \ + /datum/skill/misc/stealing, \ + /datum/skill/misc/sneaking, \ + /datum/skill/misc/lockpicking, \ + /datum/skill/misc/music, \ + /datum/skill/misc/medicine, \ + /datum/skill/magic/holy, \ + /datum/skill/magic/arcane, \ + /datum/skill/magic/druidic \ +) + +#define TAT_SKILLS_ALL (TAT_SKILLS_COMBAT + TAT_SKILLS_WANDERING + TAT_SKILLS_GATHERING + TAT_SKILLS_CRAFTING + TAT_SKILLS_MISC) + +#define TAT_DEFAULT_SKILL_DOMAIN_POINTS list( \ + TAT_SKILL_DOMAIN_COMBAT = 12, \ + TAT_SKILL_DOMAIN_WANDERING = 9, \ + TAT_SKILL_DOMAIN_GATHERING = 3, \ + TAT_SKILL_DOMAIN_CRAFTING = 6, \ + TAT_SKILL_DOMAIN_MISC = 10 \ +) + +#define TAT_VIRTUE_CHOICE_SKILLED_BSMITH "Blacksmith Apprentice" +#define TAT_VIRTUE_CHOICE_SKILLED_TAILOR "Tailor Apprentice" +#define TAT_VIRTUE_CHOICE_SKILLED_HUNTER "Hunter Apprentice" +#define TAT_VIRTUE_CHOICE_SKILLED_PHYS "Physician Apprentice" +#define TAT_VIRTUE_CHOICE_SKILLED_FORESTER "Forester Apprentice" +#define TAT_VIRTUE_CHOICE_SKILLED_ARTIF "Artificer Apprentice" + +#define TAT_VIRTUE_CHOICE_COMBAT_SWORDS "Swords Skill (JMAN)" +#define TAT_VIRTUE_CHOICE_COMBAT_SHIELDS "Shield Skill (JMAN)" +#define TAT_VIRTUE_CHOICE_COMBAT_DAGGERS "Dagger Skill (JMAN)" +#define TAT_VIRTUE_CHOICE_COMBAT_UNARMED "Unarmed Skill (JMAN)" +#define TAT_VIRTUE_CHOICE_COMBAT_SLINGS "Sling Skill (JMAN)" +#define TAT_VIRTUE_CHOICE_COMBAT_AXES "Axe Skill (JMAN)" +#define TAT_VIRTUE_CHOICE_COMBAT_WHIPS "Whip Skill (JMAN)" +#define TAT_VIRTUE_CHOICE_COMBAT_MACES "Mace Skill (JMAN)" +#define TAT_VIRTUE_CHOICE_COMBAT_POLEARMS "Polearm Skill (JMAN)" +#define TAT_VIRTUE_CHOICE_COMBAT_STAVES "Staves Skill (JMAN)" + +#define TAT_VIRTUE_CHOICE_APPRENTICE_MINING "Mining Skill (+3, Up to Legendary)" +#define TAT_VIRTUE_CHOICE_APPRENTICE_LUMBERJACKING "Lumberjacking Skill (+3, Up to Legendary)" + +#define TAT_VIRTUE_CHOICE_PROWLER_SNEAKING "Sneak Skill (+2, Up to Legendary)" +#define TAT_VIRTUE_CHOICE_PROWLER_LOCKPICKING "Lockpick Skill (+3, Up to Legendary)" + +#define TAT_VIRTUE_SKILL_BONUS_RULES list( \ + /datum/virtue/combat/bowman = list(/datum/skill/combat/bows = 1), \ + /datum/virtue/combat/crossbowman = list(/datum/skill/combat/crossbows = 1), \ + /datum/virtue/combat/magical_potential = list(/datum/skill/magic/arcane = 1), \ + /datum/virtue/combat/devotee = list(/datum/skill/magic/holy = 1), \ + /datum/virtue/utility/skilled = list(/datum/skill/craft/crafting = 2), \ + /datum/virtue/items/arsonist = list(/datum/skill/craft/alchemy = 1, /datum/skill/craft/traps = 3), \ + /datum/virtue/utility/riding = list(/datum/skill/misc/riding = 1), \ + /datum/virtue/utility/noble = list(/datum/skill/misc/reading = 1), \ + /datum/virtue/utility/intellectual = list(/datum/skill/misc/reading = 3), \ + /datum/virtue/utility/performer = list(/datum/skill/misc/music = 4), \ + /datum/virtue/utility/granary = list(/datum/skill/craft/cooking = 3, /datum/skill/labor/fishing = 2), \ + /datum/virtue/utility/homesteader = list(/datum/skill/labor/farming = TAT_SKILL_BASIC_BOOST, /datum/skill/labor/mining = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/cooking = TAT_SKILL_BASIC_BOOST, /datum/skill/labor/fishing = TAT_SKILL_BASIC_BOOST, /datum/skill/labor/butchering = TAT_SKILL_BASIC_BOOST, /datum/skill/labor/lumberjacking = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/masonry = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/ceramics = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/sewing = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/tanning = TAT_SKILL_BASIC_BOOST), \ + /datum/virtue/utility/tracker = list(/datum/skill/misc/tracking = 3), \ + /datum/virtue/utility/bronzelimbs = list(/datum/skill/craft/engineering = 1), \ + /datum/virtue/utility/feytouched = list(/datum/skill/misc/medicine = 1, /datum/skill/craft/alchemy = 1) \ +) + +#define TAT_VIRTUE_SKILL_CAP_BONUS_RULES list( \ + /datum/virtue/combat/bowman = list(/datum/skill/combat/bows = 3), \ + /datum/virtue/combat/crossbowman = list(/datum/skill/combat/crossbows = 3), \ + /datum/virtue/combat/magical_potential = list(/datum/skill/magic/arcane = 1), \ + /datum/virtue/combat/devotee = list(/datum/skill/magic/holy = 1), \ + /datum/virtue/utility/performer = list(/datum/skill/misc/music = 6) \ +) + +#define TAT_VIRTUE_CHOICE_SKILL_BONUS_RULES list( \ + /datum/virtue/utility/skilled = list( \ + TAT_VIRTUE_CHOICE_SKILLED_BSMITH = list(/datum/skill/craft/weaponsmithing = 2, /datum/skill/craft/armorsmithing = 2, /datum/skill/craft/blacksmithing = 2, /datum/skill/craft/smelting = 2), \ + TAT_VIRTUE_CHOICE_SKILLED_TAILOR = list(/datum/skill/labor/butchering = 2, /datum/skill/craft/sewing = 3, /datum/skill/craft/tanning = 2), \ + TAT_VIRTUE_CHOICE_SKILLED_HUNTER = list(/datum/skill/craft/traps = 2, /datum/skill/misc/tracking = 2, /datum/skill/labor/butchering = 2, /datum/skill/craft/sewing = 2, /datum/skill/craft/tanning = 2, /datum/skill/misc/hunting = 2), \ + TAT_VIRTUE_CHOICE_SKILLED_PHYS = list(/datum/skill/craft/alchemy = 2, /datum/skill/misc/medicine = 2), \ + TAT_VIRTUE_CHOICE_SKILLED_FORESTER = list(/datum/skill/craft/cooking = 2, /datum/skill/misc/athletics = 2, /datum/skill/labor/farming = 2, /datum/skill/labor/fishing = 2, /datum/skill/labor/lumberjacking = 2), \ + TAT_VIRTUE_CHOICE_SKILLED_ARTIF = list(/datum/skill/craft/carpentry = 2, /datum/skill/craft/masonry = 2, /datum/skill/craft/engineering = 2, /datum/skill/craft/smelting = 2, /datum/skill/craft/ceramics = 2) \ + ), \ + /datum/virtue/combat/combat_virtue = list( \ + TAT_VIRTUE_CHOICE_COMBAT_SWORDS = list(/datum/skill/combat/swords = SKILL_LEVEL_JOURNEYMAN), \ + TAT_VIRTUE_CHOICE_COMBAT_SHIELDS = list(/datum/skill/combat/shields = SKILL_LEVEL_JOURNEYMAN), \ + TAT_VIRTUE_CHOICE_COMBAT_DAGGERS = list(/datum/skill/combat/knives = SKILL_LEVEL_JOURNEYMAN), \ + TAT_VIRTUE_CHOICE_COMBAT_UNARMED = list(/datum/skill/combat/unarmed = SKILL_LEVEL_JOURNEYMAN), \ + TAT_VIRTUE_CHOICE_COMBAT_SLINGS = list(/datum/skill/combat/slings = SKILL_LEVEL_JOURNEYMAN), \ + TAT_VIRTUE_CHOICE_COMBAT_AXES = list(/datum/skill/combat/axes = SKILL_LEVEL_JOURNEYMAN), \ + TAT_VIRTUE_CHOICE_COMBAT_WHIPS = list(/datum/skill/combat/whipsflails = SKILL_LEVEL_JOURNEYMAN), \ + TAT_VIRTUE_CHOICE_COMBAT_MACES = list(/datum/skill/combat/maces = SKILL_LEVEL_JOURNEYMAN), \ + TAT_VIRTUE_CHOICE_COMBAT_POLEARMS = list(/datum/skill/combat/polearms = SKILL_LEVEL_JOURNEYMAN), \ + TAT_VIRTUE_CHOICE_COMBAT_STAVES = list(/datum/skill/combat/staves = SKILL_LEVEL_JOURNEYMAN) \ + ), \ + /datum/virtue/utility/apprentice = list( \ + TAT_VIRTUE_CHOICE_APPRENTICE_MINING = list(/datum/skill/labor/mining = SKILL_LEVEL_JOURNEYMAN), \ + TAT_VIRTUE_CHOICE_APPRENTICE_LUMBERJACKING = list(/datum/skill/labor/lumberjacking = SKILL_LEVEL_JOURNEYMAN) \ + ), \ + /datum/virtue/utility/prowler = list( \ + TAT_VIRTUE_CHOICE_PROWLER_SNEAKING = list(/datum/skill/misc/sneaking = SKILL_LEVEL_APPRENTICE), \ + TAT_VIRTUE_CHOICE_PROWLER_LOCKPICKING = list(/datum/skill/misc/lockpicking = SKILL_LEVEL_JOURNEYMAN) \ + ) \ +) + +#define TAT_VIRTUE_CHOICE_SKILL_CAP_BONUS_RULES list( \ + /datum/virtue/utility/apprentice = list( \ + TAT_VIRTUE_CHOICE_APPRENTICE_MINING = list(/datum/skill/labor/mining = SKILL_LEVEL_LEGENDARY), \ + TAT_VIRTUE_CHOICE_APPRENTICE_LUMBERJACKING = list(/datum/skill/labor/lumberjacking = SKILL_LEVEL_LEGENDARY) \ + ), \ + /datum/virtue/utility/prowler = list( \ + TAT_VIRTUE_CHOICE_PROWLER_SNEAKING = list(/datum/skill/misc/sneaking = SKILL_LEVEL_LEGENDARY), \ + TAT_VIRTUE_CHOICE_PROWLER_LOCKPICKING = list(/datum/skill/misc/lockpicking = SKILL_LEVEL_LEGENDARY) \ + ) \ +) + +GLOBAL_LIST_INIT(tat_virtue_skill_bonus_rules, TAT_VIRTUE_SKILL_BONUS_RULES) +GLOBAL_LIST_INIT(tat_virtue_skill_cap_bonus_rules, TAT_VIRTUE_SKILL_CAP_BONUS_RULES) +GLOBAL_LIST_INIT(tat_virtue_choice_skill_bonus_rules, TAT_VIRTUE_CHOICE_SKILL_BONUS_RULES) +GLOBAL_LIST_INIT(tat_virtue_choice_skill_cap_bonus_rules, TAT_VIRTUE_CHOICE_SKILL_CAP_BONUS_RULES) + +/proc/tat_get_skill_domain(skill_type) + if(skill_type in TAT_SKILLS_COMBAT) + return TAT_SKILL_DOMAIN_COMBAT + if(skill_type in TAT_SKILLS_WANDERING) + return TAT_SKILL_DOMAIN_WANDERING + if(skill_type in TAT_SKILLS_GATHERING) + return TAT_SKILL_DOMAIN_GATHERING + if(skill_type in TAT_SKILLS_CRAFTING) + return TAT_SKILL_DOMAIN_CRAFTING + if(skill_type in TAT_SKILLS_MISC) + return TAT_SKILL_DOMAIN_MISC + return null diff --git a/modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_stats.dm b/modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_stats.dm new file mode 100644 index 00000000000..14ca9704246 --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_stats.dm @@ -0,0 +1,22 @@ +#define TAT_STAT_MAXIMUM 13 +#define TAT_STAT_USEFULL_MINIMUM 8 +#define TAT_BASIC_STAT_POINTS 4 + +#define TAT_AVAILABLE_STATS_LIST \ + STATKEY_STR = TAT_STAT_ENTRY("Strength", 2, 10, TAT_STAT_USEFULL_MINIMUM, TAT_STAT_MAXIMUM), \ + STATKEY_PER = TAT_STAT_ENTRY("Perception", 1, 10, TAT_STAT_USEFULL_MINIMUM, TAT_STAT_MAXIMUM), \ + STATKEY_INT = TAT_STAT_ENTRY("Intelligence", 1, 10, TAT_STAT_USEFULL_MINIMUM, TAT_STAT_MAXIMUM), \ + STATKEY_CON = TAT_STAT_ENTRY("Constitution", 1, 10, TAT_STAT_USEFULL_MINIMUM, TAT_STAT_MAXIMUM), \ + STATKEY_WIL = TAT_STAT_ENTRY("Willpower", 1, 10, TAT_STAT_USEFULL_MINIMUM, TAT_STAT_MAXIMUM), \ + STATKEY_SPD = TAT_STAT_ENTRY("Speed", 2, 10, TAT_STAT_USEFULL_MINIMUM, TAT_STAT_MAXIMUM), \ + STATKEY_LCK = TAT_STAT_ENTRY("Fortune", 0.5, 10, TAT_STAT_USEFULL_MINIMUM, 16) + +#define TAT_STATS_ORDER_LIST list( \ + STATKEY_STR, \ + STATKEY_PER, \ + STATKEY_INT, \ + STATKEY_CON, \ + STATKEY_WIL, \ + STATKEY_SPD, \ + STATKEY_LCK \ +) diff --git a/modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_traits.dm b/modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_traits.dm new file mode 100644 index 00000000000..adf6bbdaad8 --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_traits.dm @@ -0,0 +1,526 @@ +#define TAT_TRAIT_WARRIOR_EXPERT "tat_warrior_expert" +#define TAT_TRAIT_WARRIOR_MASTER "tat_warrior_master" +#define TAT_TRAIT_RESIDENT "tat_resident" +#define TAT_TRAIT_MASTER_OF_WANDERING "tat_master_of_wandering" +#define TAT_TRAIT_PLIANT_RENAME "tat_pliant_rename" +#define TAT_TRAIT_SAVAGE_SKIN "tat_savage_skin" +#define TAT_TRAIT_SAVAGE_RAGE "tat_savage_rage" +#define TAT_TRAIT_BERSERKER_RAGE "tat_berserker_rage" +#define TAT_TRAIT_TRADER_LICENSE "tat_trader_license" +#define TAT_TRAIT_SADDLEBORN "tat_saddleborn" + +#define TAT_TRAIT_STEEL_SUPPLIER "tat_steel_supplier" +#define TAT_TRAIT_SILVER_SUPPLIER "tat_silver_supplier" +#define TAT_TRAIT_BRONZE_SUPPLIER "tat_bronze_supplier" +#define TAT_TRAIT_LEATHER_SUPPLIER "tat_leather_supplier" +#define TAT_TRAIT_MAIL_SUPPLIER "tat_mail_supplier" +#define TAT_TRAIT_FIREARMS_SUPPLIER "tat_firearms_supplier" +#define TAT_TRAIT_ARTIFACTS_SUPPLIER "tat_artifacts_supplier" +#define TAT_TRAIT_PLATE_SUPPLIER "tat_plate_supplier" +#define TAT_TRAIT_SPELLBLADE "tat_spellblade" + +#define TAT_TRAIT_BARDIC_INSPIRATION_T1 "tat_bardic_inspiration_t1" +#define TAT_TRAIT_BARDIC_INSPIRATION_T2 "tat_bardic_inspiration_t2" +#define TAT_TRAIT_PARTY_LEADER "tat_party_leader" +#define TAT_TRAIT_BONUS_STAT_POOL "tat_bonus_stat_pool" +#define TAT_TRAIT_WANTED "tat_wanted" +#define TAT_TRAIT_HERETIC "tat_heretic" +#define TAT_TRAIT_LOOTRAT "tat_lootrat" +#define TAT_TRAIT_LOOTRAT_2 "tat_lootrat_2" + +#define TAT_TRAIT_DIVINE_INITIATE "tat_divine_initiate" +#define TAT_TRAIT_MAGE_INITIATE "tat_mage_initiate" + +#define TAT_TRAIT_DIVINE_BOON_1 "tat_divine_boon_1" +#define TAT_TRAIT_DIVINE_BOON_2 "tat_divine_boon_2" +#define TAT_TRAIT_DIVINE_BOON_3 "tat_divine_boon_3" + +#define TAT_TRAIT_MAGE_MAJOR_SLOT "tat_mage_major_slot" +#define TAT_TRAIT_MAGE_MINOR_SLOT_1 "tat_mage_minor_slot_1" +#define TAT_TRAIT_MAGE_MINOR_SLOT_2 "tat_mage_minor_slot_2" +#define TAT_TRAIT_MAGE_UTILITY_SLOT "tat_mage_utility_slot" + +#define TAT_TRAIT_TRAINEE_SMITH "tat_trainee_smith" +#define TAT_TRAIT_TRAINEE_ARMORER "tat_trainee_armorer" +#define TAT_TRAIT_TRAINEE_WEAPONSMITH "tat_trainee_weaponsmith" +#define TAT_TRAIT_TRAINEE_WOODSMAN "tat_trainee_woodsman" +#define TAT_TRAIT_TRAINEE_SURVIVALIST "tat_trainee_survivalist" +#define TAT_TRAIT_TRAINEE_POACHER "tat_trainee_poacher" +#define TAT_TRAIT_TRAINEE_SKULKER "tat_trainee_skulker" +#define TAT_TRAIT_TRAINEE_VAGABOND "tat_trainee_vagabond" +#define TAT_TRAIT_TRAINEE_RIDER "tat_trainee_rider" +#define TAT_TRAIT_TRAINEE_MARINER "tat_trainee_mariner" +#define TAT_TRAIT_TRAINEE_CLOTHIER "tat_trainee_clothier" +#define TAT_TRAIT_TRAINEE_HOMESTEADER "tat_trainee_homesteader" +#define TAT_TRAIT_TRAINEE_ARTISAN "tat_trainee_artisan" +#define TAT_TRAIT_TRAINEE_CHIRURGEON "tat_trainee_chirurgeon" +#define TAT_TRAIT_TRAINEE_TROUBADOUR "tat_trainee_troubadour" + +#define TAT_TRAIT_CONVERT_COMBAT_TO_WANDERING "tat_convert_combat_to_wandering" +#define TAT_TRAIT_CONVERT_COMBAT_TO_GATHERING "tat_convert_combat_to_gathering" +#define TAT_TRAIT_CONVERT_COMBAT_TO_CRAFTING "tat_convert_combat_to_crafting" +#define TAT_TRAIT_CONVERT_COMBAT_TO_MISC "tat_convert_combat_to_misc" +#define TAT_TRAIT_CONVERT_WANDERING_TO_GATHERING "tat_convert_wandering_to_gathering" +#define TAT_TRAIT_CONVERT_WANDERING_TO_CRAFTING "tat_convert_wandering_to_crafting" +#define TAT_TRAIT_CONVERT_WANDERING_TO_MISC "tat_convert_wandering_to_misc" +#define TAT_TRAIT_CONVERT_GATHERING_TO_WANDERING "tat_convert_gathering_to_wandering" +#define TAT_TRAIT_CONVERT_GATHERING_TO_CRAFTING "tat_convert_gathering_to_crafting" +#define TAT_TRAIT_CONVERT_GATHERING_TO_MISC "tat_convert_gathering_to_misc" +#define TAT_TRAIT_CONVERT_CRAFTING_TO_WANDERING "tat_convert_crafting_to_wandering" +#define TAT_TRAIT_CONVERT_CRAFTING_TO_GATHERING "tat_convert_crafting_to_gathering" +#define TAT_TRAIT_CONVERT_CRAFTING_TO_MISC "tat_convert_crafting_to_misc" +#define TAT_TRAIT_CONVERT_MISC_TO_WANDERING "tat_convert_misc_to_wandering" +#define TAT_TRAIT_CONVERT_MISC_TO_GATHERING "tat_convert_misc_to_gathering" +#define TAT_TRAIT_CONVERT_MISC_TO_CRAFTING "tat_convert_misc_to_crafting" + +#define TAT_TRAIT_MASTER_OF_CRAFTING "tat_master_of_crafting" +#define TAT_TRAIT_STRAYING_SOUL "tat_straying_soul" +#define TAT_TRAIT_STOCKPILER "tat_stockpiler" + +#define TAT_TRAIT_SKILLED_FORGEHAND "tat_skilled_forgehand" +#define TAT_TRAIT_SKILLED_ARMORER "tat_skilled_armorer" +#define TAT_TRAIT_SKILLED_WEAPONSMITH "tat_skilled_weaponsmith" +#define TAT_TRAIT_SKILLED_ARTISAN "tat_skilled_artisan" +#define TAT_TRAIT_SKILLED_MASON "tat_skilled_mason" +#define TAT_TRAIT_SKILLED_CLOTHIER "tat_skilled_clothier" +#define TAT_TRAIT_SKILLED_SURVIVALIST "tat_skilled_survivalist" +#define TAT_TRAIT_SKILLED_HOMESTEADER "tat_skilled_homesteader" +#define TAT_TRAIT_SKILLED_PHYSICKER "tat_skilled_physicker" +#define TAT_TRAIT_SKILLED_ALCHEMIST "tat_skilled_alchemist" + +#define TAT_TRAIT_SKILL_CAP_BONUS_RULES list( \ + TRAIT_SMITHING_EXPERT = list(/datum/skill/craft/blacksmithing = 6, /datum/skill/craft/smelting = 6, /datum/skill/craft/engineering = 6, /datum/skill/labor/mining = 6, /datum/skill/craft/masonry = 6, /datum/skill/craft/ceramics = 6), \ + TAT_TRAIT_SKILLED_FORGEHAND = list(/datum/skill/craft/blacksmithing = 5, /datum/skill/craft/smelting = 5, /datum/skill/craft/engineering = 5), \ + TAT_TRAIT_SKILLED_ARMORER = list(/datum/skill/craft/armorsmithing = 5, /datum/skill/craft/masonry = 5), \ + TAT_TRAIT_SKILLED_WEAPONSMITH = list(/datum/skill/craft/weaponsmithing = 5, /datum/skill/craft/engineering = 5), \ + TAT_TRAIT_SKILLED_ARTISAN = list(/datum/skill/craft/crafting = 5, /datum/skill/craft/ceramics = 5), \ + TAT_TRAIT_SKILLED_MASON = list(/datum/skill/craft/masonry = 5, /datum/skill/craft/ceramics = 5), \ + TRAIT_ALCHEMY_EXPERT = list(/datum/skill/craft/alchemy = 6), \ + TAT_TRAIT_SKILLED_ALCHEMIST = list(/datum/skill/craft/alchemy = 5), \ + TRAIT_MEDICINE_EXPERT = list(/datum/skill/misc/medicine = 6), \ + TAT_TRAIT_SKILLED_PHYSICKER = list(/datum/skill/misc/medicine = 5), \ + TRAIT_HOMESTEAD_EXPERT = list(/datum/skill/labor/farming = 6, /datum/skill/labor/mining = 6, /datum/skill/craft/cooking = 6, /datum/skill/labor/fishing = 6, /datum/skill/labor/butchering = 6, /datum/skill/labor/lumberjacking = 6, /datum/skill/craft/masonry = 6, /datum/skill/craft/ceramics = 6, /datum/skill/craft/sewing = 3, /datum/skill/craft/tanning = 3), \ + TAT_TRAIT_SKILLED_HOMESTEADER = list(/datum/skill/labor/farming = 5, /datum/skill/craft/cooking = 5, /datum/skill/labor/fishing = 5), \ + TRAIT_SURVIVAL_EXPERT = list(/datum/skill/craft/cooking = 6, /datum/skill/labor/fishing = 6, /datum/skill/labor/butchering = 6, /datum/skill/craft/tanning = 6, /datum/skill/craft/sewing = 3), \ + TAT_TRAIT_SKILLED_SURVIVALIST = list(/datum/skill/labor/butchering = 5, /datum/skill/craft/traps = 5, /datum/skill/craft/tanning = 5), \ + TRAIT_SEWING_EXPERT = list(/datum/skill/craft/sewing = 6, /datum/skill/craft/tanning = 6, /datum/skill/labor/butchering = 6), \ + TAT_TRAIT_SKILLED_CLOTHIER = list(/datum/skill/craft/sewing = 5, /datum/skill/craft/tanning = 5), \ + TRAIT_SEEDKNOW = list(/datum/skill/labor/farming = 5), \ + TRAIT_CAUTIOUS_FISHER = list(/datum/skill/labor/fishing = 5), \ + TRAIT_SQUIRE_REPAIR = list(/datum/skill/craft/armorsmithing = 5, /datum/skill/craft/weaponsmithing = 5), \ + TRAIT_SELF_SUSTENANCE = list(/datum/skill/craft/crafting = 3, /datum/skill/craft/weaponsmithing = 3, /datum/skill/craft/armorsmithing = 3, /datum/skill/craft/blacksmithing = 3, /datum/skill/craft/smelting = 3, /datum/skill/craft/carpentry = 3, /datum/skill/craft/masonry = 3, /datum/skill/craft/traps = 3, /datum/skill/craft/engineering = 3, /datum/skill/craft/cooking = 3, /datum/skill/craft/sewing = 3, /datum/skill/craft/tanning = 3, /datum/skill/craft/ceramics = 3, /datum/skill/craft/alchemy = 3, /datum/skill/labor/farming = 3, /datum/skill/labor/mining = 3, /datum/skill/labor/fishing = 3, /datum/skill/labor/butchering = 3, /datum/skill/labor/lumberjacking = 3), \ + TRAIT_MASTERFUL_HUNTER = list(/datum/skill/misc/hunting = 6, /datum/skill/misc/tracking = 6, /datum/skill/labor/butchering = 6), \ + TRAIT_EXPERT_HUNTER = list(/datum/skill/misc/hunting = 5, /datum/skill/misc/tracking = 5), \ + TAT_TRAIT_SADDLEBORN = list(/datum/skill/misc/riding = 6), \ +) + +GLOBAL_LIST_INIT(tat_trait_skill_cap_bonus_rules, TAT_TRAIT_SKILL_CAP_BONUS_RULES) + +#define TAT_BUILD_STAT_BONUS_EXTRA_STATS 3 +#define TAT_BUILD_STAT_BONUS_WANTED 5 +#define TAT_BUILD_ITEM_BONUS_WANTED 10 +#define TAT_BUILD_ITEM_BONUS_LOOTRAT 10 +#define TAT_BUILD_ITEM_BONUS_LOOTRAT_2 15 +#define TAT_TRAIT_PLIANT_RENAME_PQ_MINIMUM 50 + +#define TAT_CATEGORY_CLASS_MODULE "class_module" +#define TAT_CATEGORY_CLASS_MODULE_NAME "Class Modules" + +#define TAT_CATEGORY_COMBAT_MASTERY "combat_mastery" +#define TAT_CATEGORY_COMBAT_MASTERY_NAME "Combat Mastery" + +#define TAT_CATEGORY_DEFENSE "defense" +#define TAT_CATEGORY_DEFENSE_NAME "Defense" + +#define TAT_CATEGORY_SUPPLY "supply" +#define TAT_CATEGORY_SUPPLY_NAME "Supply" + +#define TAT_CATEGORY_ENHANCEMENT "enhancement" +#define TAT_CATEGORY_ENHANCEMENT_NAME "Enhancement" + +#define TAT_CATEGORY_CRAFT "craft" +#define TAT_CATEGORY_CRAFT_NAME "Craft" + +#define TAT_CATEGORY_UTILITY "utility" +#define TAT_CATEGORY_UTILITY_NAME "Utility" + +#define TAT_CATEGORY_ODDITY "oddity" +#define TAT_CATEGORY_ODDITY_NAME "Oddities" + +#define TAT_CATEGORY_MAJOR_FLAW "major_flaw" +#define TAT_CATEGORY_MAJOR_FLAW_NAME "Major Flaws" + +#define TAT_NEGATIVE_TRAIT_CREDIT_CAP 20 +#define TAT_CAPPED_NEGATIVE_TRAITS list( \ + TAT_TRAIT_HERETIC, \ + TRAIT_TECHNOPHOBE, \ + TRAIT_BAD_MOOD, \ + TRAIT_PACIFISM, \ + TRAIT_CRITICAL_WEAKNESS, \ + TRAIT_NUDIST, \ + TRAIT_INHUMEN_ANATOMY, \ + TRAIT_DISFIGURED, \ + TRAIT_SPELLCOCKBLOCK, \ + TRAIT_NOSLEEP, \ + TRAIT_NORUN, \ + TRAIT_NUDE_SLEEPER, \ + TRAIT_EASYDISMEMBER, \ + TRAIT_PERMAMUTE, \ + TRAIT_SHIRTLESS, \ + TRAIT_NODEF, \ + TRAIT_REVERSE_GUIDANCE, \ + TRAIT_LESSER_REVERSE_GUIDANCE \ +) + +#define TAT_CATEGORY_SKILL_DISCOUNT "skill_discount" +#define TAT_CATEGORY_SKILL_DISCOUNT_NAME "Skill Discount" +#define TAT_CATEGORY_SKILL_CONVERSION "skill_conversion" +#define TAT_CATEGORY_SKILL_CONVERSION_NAME "Skill Conversion" + +#define TAT_RESIDENT_PUGILIST_DEFAULT "Dropkick - Pushback + Extra Damage" +#define TAT_TRAIT_DISCOUNT 10 + +#define TAT_ARMOR_SUPPLIER_CROSS_DISCOUNT 10 +#define TAT_ARMOR_TRAINING_SUPPLIER_DISCOUNT 10 +#define TAT_MATERIAL_SUPPLIER_CROSS_DISCOUNT 5 + +#define TAT_ARMOR_SUPPLIER_TRAITS list( \ + TAT_TRAIT_LEATHER_SUPPLIER, \ + TAT_TRAIT_MAIL_SUPPLIER, \ + TAT_TRAIT_PLATE_SUPPLIER \ +) + +#define TAT_MATERIAL_SUPPLIER_TRAITS list( \ + TAT_TRAIT_BRONZE_SUPPLIER, \ + TAT_TRAIT_SILVER_SUPPLIER, \ + TAT_TRAIT_STEEL_SUPPLIER \ +) + +#define TAT_ARMOR_TRAINING_SUPPLIER_DISCOUNT_RULES list( \ + TRAIT_MEDIUMARMOR = TAT_TRAIT_MAIL_SUPPLIER, \ + TRAIT_HEAVYARMOR = TAT_TRAIT_PLATE_SUPPLIER \ +) + +// TAT_TRAIT_CONTRACTOR = TAT_TRAIT_ENTRY("Contractor", 80, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Awakens the contract-bearing curse in your veins. The whole world is waiting your gifts and deals."), +#define TAT_AVAILABLE_TRAITS_LIST \ + TAT_TRAIT_SPELLBLADE = TAT_TRAIT_ENTRY("Spellblade", 10, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Grants a set of weapon-binding spells."), \ + TAT_TRAIT_RESIDENT = TAT_TRAIT_ENTRY("Resident", 10, TAT_CATEGORY_MAJOR_FLAW, TAT_CATEGORY_MAJOR_FLAW_NAME, "Grants a Meister account and ownership of a house in the city."), \ + TAT_TRAIT_TRADER_LICENSE = TAT_TRAIT_ENTRY("Merchant's Writ", 30, TAT_CATEGORY_MAJOR_FLAW, TAT_CATEGORY_MAJOR_FLAW_NAME, "Unlocks sealed trader caches in the TAT item list. Conflicts with Resident, Wanted, Outlander, and Heretic."), \ + TAT_TRAIT_BARDIC_INSPIRATION_T1 = TAT_TRAIT_ENTRY("Bardic Inspiration I", 15, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Gain tier 1 bardic inspiration, audience management verbs, and a songbook."), \ + TAT_TRAIT_BARDIC_INSPIRATION_T2 = TAT_TRAIT_ENTRY("Bardic Inspiration II", 15, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Upgrades bardic inspiration to tier 2, increasing audience size and songs known."), \ + TAT_TRAIT_PARTY_LEADER = TAT_TRAIT_ENTRY("Party Leader", 30, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Improves the core Fellowship: nearby fellows gain +1 CON; while at least one fellow is nearby, the leader gains +1 CON, +1 WIL, and +0.5 Fortune per nearby fellow."), \ + TAT_TRAIT_BONUS_STAT_POOL = TAT_TRAIT_ENTRY("Natural Potential", 20, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Gain +3 stat points in the build pool."), \ + TAT_TRAIT_WANTED = TAT_TRAIT_ENTRY("Wanted", -30, TAT_CATEGORY_MAJOR_FLAW, TAT_CATEGORY_MAJOR_FLAW_NAME, "Gain +5 stat points in the build pool, become an Outlaw, gain Forbidden Knowledge, and receive a bounty."), \ + TAT_TRAIT_WARRIOR_EXPERT = TAT_TRAIT_ENTRY("Expert Warrior", 40, TAT_CATEGORY_COMBAT_MASTERY, TAT_CATEGORY_COMBAT_MASTERY_NAME, "Raises the combat skill cap from 3 to 4."), \ + TAT_TRAIT_WARRIOR_MASTER = TAT_TRAIT_ENTRY("Master Warrior", 30, TAT_CATEGORY_COMBAT_MASTERY, TAT_CATEGORY_COMBAT_MASTERY_NAME, "Raises the combat skill cap from 4 to 5. Requires Expert Warrior."), \ + TRAIT_DODGEEXPERT = TAT_TRAIT_ENTRY("Expert Dodger", 30, TAT_CATEGORY_DEFENSE, TAT_CATEGORY_DEFENSE_NAME, "Much better at dodging incoming strikes in light armor or with little armor. Heavy armor is too cumbersome for this style."), \ + TRAIT_PARRYEXPERT = TAT_TRAIT_ENTRY("Expert Parry", 30, TAT_CATEGORY_DEFENSE, TAT_CATEGORY_DEFENSE_NAME, "Much better at parrying incoming strikes, with a higher chance to deflect blows using a weapon."), \ + TRAIT_HEAVYARMOR = TAT_TRAIT_ENTRY("Plate Training", 30, TAT_CATEGORY_DEFENSE, TAT_CATEGORY_DEFENSE_NAME, "Can move freely in heavy armor."), \ + TRAIT_MEDIUMARMOR = TAT_TRAIT_ENTRY("Maille Training", 20, TAT_CATEGORY_DEFENSE, TAT_CATEGORY_DEFENSE_NAME, "Can move freely in medium armor."), \ + TRAIT_NOPAINSTUN = TAT_TRAIT_ENTRY("Enduring", 20, TAT_CATEGORY_DEFENSE, TAT_CATEGORY_DEFENSE_NAME, "Pain does not impair you as easily. You can endure more burns before collapsing."), \ + TAT_TRAIT_SAVAGE_SKIN = TAT_TRAIT_ENTRY("Savage Skin", 0, TAT_CATEGORY_DEFENSE, TAT_CATEGORY_DEFENSE_NAME, "Requires Enduring. Starts you with barbarian regenerating skin equipped in the armor slot."), \ + TAT_TRAIT_SAVAGE_RAGE = TAT_TRAIT_ENTRY("Savage Rage", 5, TAT_CATEGORY_DEFENSE, TAT_CATEGORY_DEFENSE_NAME, "Requires Savage Skin. Grants the Rage ability."), \ + TAT_TRAIT_BERSERKER_RAGE = TAT_TRAIT_ENTRY("Berserkers Rage", 10, TAT_CATEGORY_DEFENSE, TAT_CATEGORY_DEFENSE_NAME, "Requires Savage Skin and Heretic. Grants the Berserkers Rage ability."), \ + TRAIT_CRITICAL_RESISTANCE = TAT_TRAIT_ENTRY("Critical Resistance", 30, TAT_CATEGORY_DEFENSE, TAT_CATEGORY_DEFENSE_NAME, "Your constitution is iron-clad. You can resist the first critical wounds that would fell others, though repeated punishment will overwhelm you."), \ + TRAIT_HARDDISMEMBER = TAT_TRAIT_ENTRY("Hard Dismemberment", 20, TAT_CATEGORY_DEFENSE, TAT_CATEGORY_DEFENSE_NAME, "Your limbs are harder to dismember."), \ + TRAIT_STEELHEARTED = TAT_TRAIT_ENTRY("Steelhearted", 5, TAT_CATEGORY_DEFENSE, TAT_CATEGORY_DEFENSE_NAME, "Hardened nerves. You do not waiver from the sight of violence in battle."), \ + TRAIT_CIVILIZEDBARBARIAN = TAT_TRAIT_ENTRY("Expert Pugilist", 20, TAT_CATEGORY_DEFENSE, TAT_CATEGORY_DEFENSE_NAME, "Turns you into a living weapon: stronger unarmed strikes, broader unarmed reach, and much better parrying with bracers, knuckles, or bandages."), \ + TRAIT_FENCERDEXTERITY = TAT_TRAIT_ENTRY("Fencer's Dexterity", 10, TAT_CATEGORY_DEFENSE, TAT_CATEGORY_DEFENSE_NAME, "I've trained my entire lyfe around the art of unarmoured fencing, affording myself unmatched speed when wearing very light armour. I'm very choosy otherwise. WARNING: YOU CAN WEAR ONLY LIGHT ARMOR"), \ + TAT_TRAIT_BRONZE_SUPPLIER = TAT_TRAIT_ENTRY("Bronze Supplier", 10, TAT_CATEGORY_SUPPLY, TAT_CATEGORY_SUPPLY_NAME, "Unlocks bronze-tier weapons."), \ + TAT_TRAIT_SILVER_SUPPLIER = TAT_TRAIT_ENTRY("Silver Supplier", 30, TAT_CATEGORY_SUPPLY, TAT_CATEGORY_SUPPLY_NAME, "Unlocks silver-tier weapons."), \ + TAT_TRAIT_STEEL_SUPPLIER = TAT_TRAIT_ENTRY("Steel Supplier", 15, TAT_CATEGORY_SUPPLY, TAT_CATEGORY_SUPPLY_NAME, "Unlocks steel-tier weapons."), \ + TAT_TRAIT_FIREARMS_SUPPLIER = TAT_TRAIT_ENTRY("Firearms Supplier", 25, TAT_CATEGORY_SUPPLY, TAT_CATEGORY_SUPPLY_NAME, "Unlocks blackpowder weapons and supplies."), \ + TAT_TRAIT_LEATHER_SUPPLIER = TAT_TRAIT_ENTRY("Leather Supplier", 10, TAT_CATEGORY_SUPPLY, TAT_CATEGORY_SUPPLY_NAME, "Unlocks leather gear in all supported slots."), \ + TAT_TRAIT_MAIL_SUPPLIER = TAT_TRAIT_ENTRY("Mail Supplier", 20, TAT_CATEGORY_SUPPLY, TAT_CATEGORY_SUPPLY_NAME, "Unlocks mail gear in all supported slots."), \ + TAT_TRAIT_PLATE_SUPPLIER = TAT_TRAIT_ENTRY("Plate Supplier", 30, TAT_CATEGORY_SUPPLY, TAT_CATEGORY_SUPPLY_NAME, "Unlocks plate gear in all supported slots."), \ + TRAIT_INTELLECTUAL = TAT_TRAIT_ENTRY("Intellectual", 20, TAT_CATEGORY_ENHANCEMENT, TAT_CATEGORY_ENHANCEMENT_NAME, "You have a keen eye and can assess a person's prowess in wit and blade."), \ + TAT_TRAIT_LOOTRAT = TAT_TRAIT_ENTRY("Loot Rat", 10, TAT_CATEGORY_COMBAT_MASTERY, TAT_CATEGORY_ENHANCEMENT_NAME, "Somehow in your journeys or life you collect a lot of different things and exotic treasures. Increase loot points by 10."), \ + TAT_TRAIT_LOOTRAT_2 = TAT_TRAIT_ENTRY("Enormous Rat", 20, TAT_CATEGORY_COMBAT_MASTERY, TAT_CATEGORY_ENHANCEMENT_NAME, "You work on Guild with mountains of gold or you're just a lucky dungeon mudskipper. Increase loot points by 15."), \ + TRAIT_ARCYNE = TAT_TRAIT_ENTRY("Arcyne Training", 10, TAT_CATEGORY_COMBAT_MASTERY, TAT_CATEGORY_ENHANCEMENT_NAME, "You are trained in the Arcyne arts, allowing you to wield magyck. Basis trait for magic-build classes. Gives +3 Arcane skill if there is no defensive lockout trait."), \ + TRAIT_JACKOFALLTRADES = TAT_TRAIT_ENTRY("Jack of All Trades", 15, TAT_CATEGORY_ENHANCEMENT, TAT_CATEGORY_ENHANCEMENT_NAME, "Skills cost half as much for you to raise."), \ + TAT_TRAIT_MASTER_OF_WANDERING = TAT_TRAIT_ENTRY("Master of Wandering", 30, TAT_CATEGORY_ENHANCEMENT, TAT_CATEGORY_ENHANCEMENT_NAME, "Gives +20 Misc skill points and a discount on Misc skills. Conflicts with Resident."), \ + TAT_TRAIT_PLIANT_RENAME = TAT_TRAIT_ENTRY("Pliant Class Name", 0, TAT_CATEGORY_ENHANCEMENT, TAT_CATEGORY_ENHANCEMENT_NAME, "Requires 50+ player quality. Lets you rename your displayed class while keeping the Pliant admin marker prefix. Resident class selection is applied first, then you may choose the current class or a matching skill title as the base, and finally use that name, your active TAT slot name, or custom input."), \ + TRAIT_EMPATH = TAT_TRAIT_ENTRY("Empath", 5, TAT_CATEGORY_ENHANCEMENT, TAT_CATEGORY_ENHANCEMENT_NAME, "You can notice when people are in pain."), \ + TRAIT_NOSTINK = TAT_TRAIT_ENTRY("Dead Nose", 10, TAT_CATEGORY_ENHANCEMENT, TAT_CATEGORY_ENHANCEMENT_NAME, "Your nose is numb to the smell of decay."), \ + TRAIT_NOBLE = TAT_TRAIT_ENTRY("Noble Blooded", 10, TAT_CATEGORY_ENHANCEMENT, TAT_CATEGORY_ENHANCEMENT_NAME, "You are of noble blood."), \ + TRAIT_CICERONE = TAT_TRAIT_ENTRY("Cicerone", 5, TAT_CATEGORY_UTILITY, TAT_CATEGORY_UTILITY_NAME, "You are well-versed in brews and spirits, and can tell them apart at a glance."), \ + TRAIT_SEEPRICES = TAT_TRAIT_ENTRY("Appraiser", 10, TAT_CATEGORY_UTILITY, TAT_CATEGORY_UTILITY_NAME, "You can tell the prices of things down to the zenny."), \ + TRAIT_OUTLANDER = TAT_TRAIT_ENTRY("Outlander", -20, TAT_CATEGORY_MAJOR_FLAW, TAT_CATEGORY_MAJOR_FLAW_NAME, "The locals see you as not of their land."), \ + TRAIT_GRAVEROBBER = TAT_TRAIT_ENTRY("Experienced Grave Robber", 20, TAT_CATEGORY_UTILITY, TAT_CATEGORY_UTILITY_NAME, "Your experience with 'post-mortem artifact recovery' helps you resist Necra's curse placed on those who disturb resting places."), \ + TRAIT_PURITAN_ADVENTURER = TAT_TRAIT_ENTRY("Interrogator", 20, TAT_CATEGORY_UTILITY, TAT_CATEGORY_UTILITY_NAME, "With a silver psycross, you can force the restrained to kneel before a crucifix and proclaim their true allegiance."), \ + TRAIT_DECEIVING_MEEKNESS = TAT_TRAIT_ENTRY("Deceiving Meekness", 15, TAT_CATEGORY_UTILITY, TAT_CATEGORY_UTILITY_NAME, "People think you are weak. They are mistaken. You have learned to hide your vices and true beliefs from others."), \ + TRAIT_NASTY_EATER = TAT_TRAIT_ENTRY("Inhumen Digestion", 15, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "You can eat bad food, and water toxic to humen does not affect you."), \ + TRAIT_GOODLOVER = TAT_TRAIT_ENTRY("Fabled Lover", 10, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "It is a lucky thing to share your bed."), \ + TRAIT_NUTCRACKER = TAT_TRAIT_ENTRY("Nutcracker", 5, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "You love kicking idiots in the nuts."), \ + TAT_TRAIT_DIVINE_INITIATE = TAT_TRAIT_ENTRY("Divine Initiate", 15, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Grants miracles and devotion. Additional divine boon traits increase miracle access."), \ + TAT_TRAIT_DIVINE_BOON_1 = TAT_TRAIT_ENTRY("Divine Boon I", 15, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Requires Divine Initiate. Raises your miracle package by one tier."), \ + TAT_TRAIT_DIVINE_BOON_2 = TAT_TRAIT_ENTRY("Divine Boon II", 15, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Requires Divine Initiate and Divine Boon I. Raises your miracle package by one tier."), \ + TAT_TRAIT_DIVINE_BOON_3 = TAT_TRAIT_ENTRY("Divine Boon III", 15, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Requires Divine Initiate and Divine Boon II. Raises your miracle package by one tier."), \ + TAT_TRAIT_MAGE_INITIATE = TAT_TRAIT_ENTRY("Mage Initiate", 15, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Grants one minor spell, three utility spells, plus one extra utility per Arcane skill level."), \ + TAT_TRAIT_MAGE_MAJOR_SLOT = TAT_TRAIT_ENTRY("Arcane Major Slot", 15, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Requires Mage Initiate. Grants +1 major spell slot."), \ + TAT_TRAIT_MAGE_MINOR_SLOT_1 = TAT_TRAIT_ENTRY("Arcane Minor Slot I", 10, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Requires Mage Initiate. Grants +1 minor spell slot."), \ + TAT_TRAIT_MAGE_MINOR_SLOT_2 = TAT_TRAIT_ENTRY("Arcane Minor Slot II", 10, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Requires Mage Initiate. Grants +1 minor spell slot."), \ + TAT_TRAIT_MAGE_UTILITY_SLOT = TAT_TRAIT_ENTRY("Arcane Utility Slot", 10, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Requires Mage Initiate. Grants +1 utility spell slot."), \ + TRAIT_EXPLOSIVE_SUPPLY = TAT_TRAIT_ENTRY("Explosive Supply", 10, TAT_CATEGORY_UTILITY, TAT_CATEGORY_CLASS_MODULE_NAME, "Grants explosives gifts from your friends. Luck scaled."), \ + TAT_TRAIT_ARTIFACTS_SUPPLIER = TAT_TRAIT_ENTRY("Artifacts Bearer", 50, TAT_CATEGORY_SUPPLY, TAT_CATEGORY_SUPPLY_NAME, "You're one of the adventurers with stories about your raids. Now, you have one of the deadliest weapons in Grimmoria. REQUIRES: Party Leader"), \ + TAT_TRAIT_TRAINEE_SMITH = TAT_TRAIT_ENTRY("Trainee Smith", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Blacksmithing, Smelting, and the first two levels of Maces by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_TRAINEE_ARMORER = TAT_TRAIT_ENTRY("Trainee Armorer", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Armorsmithing, Masonry, and the first two levels of Shields by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_TRAINEE_WEAPONSMITH = TAT_TRAIT_ENTRY("Trainee Weaponsmith", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Weaponsmithing, Engineering, and the first two levels of Swords by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_TRAINEE_WOODSMAN = TAT_TRAIT_ENTRY("Trainee Woodsman", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Lumberjacking, Carpentry, and the first two levels of Axes by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_TRAINEE_SURVIVALIST = TAT_TRAIT_ENTRY("Trainee Survivalist", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Butchering, Hunting, and the first two levels of Archery by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_TRAINEE_POACHER = TAT_TRAIT_ENTRY("Trainee Poacher", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Tracking, Trapmaking, and the first two levels of Crossbows by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_TRAINEE_SKULKER = TAT_TRAIT_ENTRY("Trainee Skulker", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Sneaking, Lockpicking, and the first two levels of Knives by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_TRAINEE_VAGABOND = TAT_TRAIT_ENTRY("Trainee Vagabond", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Pickpocketing, Climbing, and the first two levels of Slings by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_TRAINEE_RIDER = TAT_TRAIT_ENTRY("Trainee Rider", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Riding, Athletics, and the first two levels of Polearms by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_TRAINEE_MARINER = TAT_TRAIT_ENTRY("Trainee Mariner", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Swimming, Fishing, and the first two levels of Staves by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_TRAINEE_CLOTHIER = TAT_TRAIT_ENTRY("Trainee Clothier", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Sewing, Skincrafting, and the first two levels of Whips & Flails by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_TRAINEE_HOMESTEADER = TAT_TRAIT_ENTRY("Trainee Homesteader", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Farming, Cooking, and the first two levels of Wrestling by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_TRAINEE_ARTISAN = TAT_TRAIT_ENTRY("Trainee Artisan", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Crafting, Pottery, and the first two levels of Unarmed by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_TRAINEE_CHIRURGEON = TAT_TRAIT_ENTRY("Trainee Chirurgeon", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Medicine, Literacy, and the first two levels of Staves by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_TRAINEE_TROUBADOUR = TAT_TRAIT_ENTRY("Trainee Troubadour", 10, TAT_CATEGORY_SKILL_DISCOUNT, TAT_CATEGORY_SKILL_DISCOUNT_NAME, "Reduces the cost of Music, Literacy, and the first two levels of Knives by 1. Does not stack with Resident or other discount traits on the same skill."), \ + TAT_TRAIT_CONVERT_COMBAT_TO_WANDERING = TAT_TRAIT_ENTRY("Convert Combat -> Wandering", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Combat to Wandering."), \ + TAT_TRAIT_CONVERT_COMBAT_TO_GATHERING = TAT_TRAIT_ENTRY("Convert Combat -> Gathering", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Combat to Gathering."), \ + TAT_TRAIT_CONVERT_COMBAT_TO_CRAFTING = TAT_TRAIT_ENTRY("Convert Combat -> Crafting", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Combat to Crafting."), \ + TAT_TRAIT_CONVERT_COMBAT_TO_MISC = TAT_TRAIT_ENTRY("Convert Combat -> Misc", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Combat to Misc."), \ + TAT_TRAIT_CONVERT_WANDERING_TO_GATHERING = TAT_TRAIT_ENTRY("Convert Wandering -> Gathering", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Wandering to Gathering."), \ + TAT_TRAIT_CONVERT_WANDERING_TO_CRAFTING = TAT_TRAIT_ENTRY("Convert Wandering -> Crafting", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Wandering to Crafting."), \ + TAT_TRAIT_CONVERT_WANDERING_TO_MISC = TAT_TRAIT_ENTRY("Convert Wandering -> Misc", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Wandering to Misc."), \ + TAT_TRAIT_CONVERT_GATHERING_TO_WANDERING = TAT_TRAIT_ENTRY("Convert Gathering -> Wandering", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Gathering to Wandering."), \ + TAT_TRAIT_CONVERT_GATHERING_TO_CRAFTING = TAT_TRAIT_ENTRY("Convert Gathering -> Crafting", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Gathering to Crafting."), \ + TAT_TRAIT_CONVERT_GATHERING_TO_MISC = TAT_TRAIT_ENTRY("Convert Gathering -> Misc", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Gathering to Misc."), \ + TAT_TRAIT_CONVERT_CRAFTING_TO_WANDERING = TAT_TRAIT_ENTRY("Convert Crafting -> Wandering", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Crafting to Wandering."), \ + TAT_TRAIT_CONVERT_CRAFTING_TO_GATHERING = TAT_TRAIT_ENTRY("Convert Crafting -> Gathering", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Crafting to Gathering."), \ + TAT_TRAIT_CONVERT_CRAFTING_TO_MISC = TAT_TRAIT_ENTRY("Convert Crafting -> Misc", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Crafting to Misc."), \ + TAT_TRAIT_CONVERT_MISC_TO_WANDERING = TAT_TRAIT_ENTRY("Convert Misc -> Wandering", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Misc to Wandering."), \ + TAT_TRAIT_CONVERT_MISC_TO_GATHERING = TAT_TRAIT_ENTRY("Convert Misc -> Gathering", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Misc to Gathering."), \ + TAT_TRAIT_CONVERT_MISC_TO_CRAFTING = TAT_TRAIT_ENTRY("Convert Misc -> Crafting", 0, TAT_CATEGORY_SKILL_CONVERSION, TAT_CATEGORY_SKILL_CONVERSION_NAME, "Repeatable. Move 1 skill point from Misc to Crafting."), \ + TRAIT_STRONGBITE = TAT_TRAIT_ENTRY("Strong Bite", 35, TAT_CATEGORY_COMBAT_MASTERY, TAT_CATEGORY_COMBAT_MASTERY_NAME, "Increases crit chance and damage from biting"), \ + TRAIT_BITERHELM = TAT_TRAIT_ENTRY("Curse of Twisting Metall", 10, TAT_CATEGORY_COMBAT_MASTERY, TAT_CATEGORY_COMBAT_MASTERY_NAME, "Gives cursed skill to bite through your own helmet"), \ + TAT_TRAIT_MASTER_OF_CRAFTING = TAT_TRAIT_ENTRY("Master of Handicraft", 20, TAT_CATEGORY_ENHANCEMENT, TAT_CATEGORY_ENHANCEMENT_NAME, "Gain extra Crafting skill-domain points."), \ + TAT_TRAIT_STOCKPILER = TAT_TRAIT_ENTRY("Stockpiler", 20, TAT_CATEGORY_ENHANCEMENT, TAT_CATEGORY_ENHANCEMENT_NAME, "Gain extra Gathering skill-domain points."), \ + TRAIT_SMITHING_EXPERT = TAT_TRAIT_ENTRY("Expert Forgehand", 20, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Experienced with smithing and engineering. Smithing, Smelting, Engineering, Mining, Masonry and Pottery can progress to Legendary levels."), \ + TAT_TRAIT_SKILLED_FORGEHAND = TAT_TRAIT_ENTRY("Skilled Forgehand", 10, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Skilled with forge-work. Smithing, Smelting and Engineering can progress to Master levels."), \ + TAT_TRAIT_SKILLED_ARMORER = TAT_TRAIT_ENTRY("Skilled Armorer", 20, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Skilled with armor repair and fitting. Armorsmithing and Masonry can progress to Master levels."), \ + TAT_TRAIT_SKILLED_WEAPONSMITH = TAT_TRAIT_ENTRY("Skilled Weaponsmith", 10, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Skilled with weapon work. Weaponsmithing and Engineering can progress to Master levels."), \ + TAT_TRAIT_SKILLED_ARTISAN = TAT_TRAIT_ENTRY("Skilled Artisan", 10, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Skilled with general craftwork. Crafting and Pottery can progress to Master levels."), \ + TAT_TRAIT_SKILLED_MASON = TAT_TRAIT_ENTRY("Skilled Architect", 10, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Skilled with stone and kiln work. Masonry, Woodwork and Pottery can progress to Master levels."), \ + TRAIT_ALCHEMY_EXPERT = TAT_TRAIT_ENTRY("Expert Alchemist", 20, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Deep, intricate knowledge of the alchemical arts. Alchemy can progress to Legendary levels."), \ + TAT_TRAIT_SKILLED_ALCHEMIST = TAT_TRAIT_ENTRY("Skilled Alchemist", 10, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Skilled with alchemical work. Alchemy can progress to Master levels."), \ + TRAIT_MEDICINE_EXPERT = TAT_TRAIT_ENTRY("Expert Physicker", 20, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Deep, intricate knowledge of medicine. Medicine can progress to Legendary levels."), \ + TAT_TRAIT_SKILLED_PHYSICKER = TAT_TRAIT_ENTRY("Skilled Physicker", 10, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Skilled with medical practice. Medicine can progress to Master levels."), \ + TRAIT_HOMESTEAD_EXPERT = TAT_TRAIT_ENTRY("Expert Homesteader", 25, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Experienced with the arts of homesteading. Farming, Mining, Cooking, Fishing, Butchering, Lumberjacking, Masonry and Pottery can progress to Legendary levels. Sewing and Skincrafting can progress to Journeyman levels."), \ + TRAIT_SELF_SUSTENANCE = TAT_TRAIT_ENTRY("Self-Sustenance", 15, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Years of running from the law and living off the land have made you a jack of all trades. All crafting and labor skills can progress to Journeyman levels."), \ + TRAIT_SURVIVAL_EXPERT = TAT_TRAIT_ENTRY("Expert Survivalist", 20, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Experienced with survival in the wild. Cooking, Fishing, Butchering and Skincrafting can progress to Legendary levels. Sewing can progress to Journeyman levels."), \ + TAT_TRAIT_SKILLED_SURVIVALIST = TAT_TRAIT_ENTRY("Skilled Survivalist", 10, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Skilled with survival work. Butchering, Trapmaking and Skincrafting can progress to Master levels."), \ + TRAIT_SEWING_EXPERT = TAT_TRAIT_ENTRY("Expert Clothier", 20, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Experienced with sewing and leathercraft. Sewing, Skincrafting and Butchering can progress to Legendary levels."), \ + TAT_TRAIT_SKILLED_CLOTHIER = TAT_TRAIT_ENTRY("Skilled Clothier", 10, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Skilled with cloth and leather. Sewing and Skincrafting can progress to Master levels."), \ + TRAIT_SEEDKNOW = TAT_TRAIT_ENTRY("Seed Knower", 5, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "You know which seeds grow which crops."), \ + TRAIT_CAUTIOUS_FISHER = TAT_TRAIT_ENTRY("Cautious Fisher", 5, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "You know the dangers of fishing and how to avoid unwanted attention from the depths."), \ + TRAIT_SQUIRE_REPAIR = TAT_TRAIT_ENTRY("Squire Knowledge", 15, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "You can restore gear with time and polish it until it gleams like new."), \ + TAT_TRAIT_SADDLEBORN = TAT_TRAIT_ENTRY("Saddleborn", 15, TAT_CATEGORY_ENHANCEMENT, TAT_CATEGORY_ENHANCEMENT_NAME, "You can select and mount a specific mount."), \ + TRAIT_MASTERFUL_HUNTER = TAT_TRAIT_ENTRY("Masterful Hunter", 10, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "To hunt well is to know the land. You know watering holes, feeding grounds and bent thickets."), \ + TRAIT_EXPERT_HUNTER = TAT_TRAIT_ENTRY("Expert Hunter", 5, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "To hunt well is to know the land. You know the common signs of prey and trails."), \ + TAT_TRAIT_STRAYING_SOUL = TAT_TRAIT_ENTRY("Straying Soul", 10, TAT_CATEGORY_CRAFT, TAT_CATEGORY_CRAFT_NAME, "Your feet walks a lot roads. Gives you +9 points in wandering skill tree."), \ + TAT_TRAIT_HERETIC = TAT_TRAIT_ENTRY("Heretic", 0, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Gain cool HERETIC mark on your face."), \ + TRAIT_RITUALIST = TAT_TRAIT_ENTRY("Ritualist", 30, TAT_CATEGORY_CLASS_MODULE, TAT_CATEGORY_CLASS_MODULE_NAME, "Gives God's favour for thy's rituals. Adds ritual chalk to your stash."), \ + TRAIT_TECHNOPHOBE = TAT_TRAIT_ENTRY("Technophobe", -5, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "You cannot use Meister devices."), \ + TRAIT_BAD_MOOD = TAT_TRAIT_ENTRY("Bad Mood", -5, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "All stress you receive is doubled."), \ + TRAIT_PACIFISM = TAT_TRAIT_ENTRY("Pacifist", -20, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "You cannot harm living beings."), \ + TRAIT_CRITICAL_WEAKNESS = TAT_TRAIT_ENTRY("Critical Weakness", -15, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "You are far more vulnerable to critical wounds."), \ + TRAIT_NUDIST = TAT_TRAIT_ENTRY("Nudist", -10, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "You refuse to wear clothes."), \ + TRAIT_INHUMEN_ANATOMY = TAT_TRAIT_ENTRY("Inhumen Anatomy", -5, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "Your anatomy prevents you from wearing hats and boots."), \ + TRAIT_DISFIGURED = TAT_TRAIT_ENTRY("Disfigured", 5, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "You are unknowned to everyone."), \ + TRAIT_SPELLCOCKBLOCK = TAT_TRAIT_ENTRY("Bewitched", -5, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "You cannot cast spells."), \ + TRAIT_NOSLEEP = TAT_TRAIT_ENTRY("Sleepless", -5, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "You cannot fall asleep without drugs or a blow to the head."), \ + TRAIT_NORUN = TAT_TRAIT_ENTRY("No Running", -10, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "You cannot run."), \ + TRAIT_NUDE_SLEEPER = TAT_TRAIT_ENTRY("Nude Sleeper", -5, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "You can only sleep while fully naked."), \ + TRAIT_EASYDISMEMBER = TAT_TRAIT_ENTRY("Easy Dismemberment", -15, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "Your limbs are much easier to dismember."), \ + TRAIT_PERMAMUTE = TAT_TRAIT_ENTRY("Permanent Mute", -10, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "You are permanently mute and cannot speak."), \ + TRAIT_NODEF = TAT_TRAIT_ENTRY("No Defense", -20, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "You expose yourself completely in battle."), \ + TRAIT_REVERSE_GUIDANCE = TAT_TRAIT_ENTRY("Reverse Guidance", -10, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "Something hinders you in battle. Anti-guidance: 20%."), \ + TRAIT_LESSER_REVERSE_GUIDANCE = TAT_TRAIT_ENTRY("Lesser Reverse Guidance", -5, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "Something faintly hinders you in battle. Anti-guidance: 10%."), \ + +#define TAT_TRAIT_STAT_POINT_RULES list( \ + TAT_TRAIT_BONUS_STAT_POOL = TAT_BUILD_STAT_BONUS_EXTRA_STATS, \ + TAT_TRAIT_WANTED = TAT_BUILD_STAT_BONUS_WANTED, \ +) + +#define TAT_TRAIT_ITEM_POINT_RULES list( \ + TAT_TRAIT_WANTED = TAT_BUILD_ITEM_BONUS_WANTED, \ + TAT_TRAIT_LOOTRAT = TAT_BUILD_ITEM_BONUS_LOOTRAT, \ + TAT_TRAIT_LOOTRAT_2 = TAT_BUILD_ITEM_BONUS_LOOTRAT_2, \ +) + +#define TAT_TRAIT_ITEM_UNLOCK_RULES list( \ + TAT_UNLOCK_TYPE_WEAPON_SUPPLY = list(TAT_SUPPLY_BRONZE = TAT_TRAIT_BRONZE_SUPPLIER, TAT_SUPPLY_SILVER = TAT_TRAIT_SILVER_SUPPLIER, TAT_SUPPLY_STEEL = TAT_TRAIT_STEEL_SUPPLIER, TAT_SUPPLY_FIREARMS = TAT_TRAIT_FIREARMS_SUPPLIER, TAT_SUPPLY_ARTIFACTS = TAT_TRAIT_ARTIFACTS_SUPPLIER), \ + TAT_UNLOCK_TYPE_ARMOR_FAMILY = list(TAT_ARMOR_LEATHER = TAT_TRAIT_LEATHER_SUPPLIER, TAT_ARMOR_MAIL = TAT_TRAIT_MAIL_SUPPLIER, TAT_ARMOR_PLATE = TAT_TRAIT_PLATE_SUPPLIER) \ +) + +#define TAT_TRAIT_PQ_LOCK_RULES list( \ + TAT_TRAIT_PLIANT_RENAME = TAT_TRAIT_PLIANT_RENAME_PQ_MINIMUM \ +) + +#define TAT_TRAIT_REPEATABLE_MAXIMUMS list( \ + TAT_TRAIT_CONVERT_COMBAT_TO_WANDERING = 99, TAT_TRAIT_CONVERT_COMBAT_TO_GATHERING = 99, TAT_TRAIT_CONVERT_COMBAT_TO_CRAFTING = 99, TAT_TRAIT_CONVERT_COMBAT_TO_MISC = 99, \ + TAT_TRAIT_CONVERT_WANDERING_TO_GATHERING = 99, TAT_TRAIT_CONVERT_WANDERING_TO_CRAFTING = 99, TAT_TRAIT_CONVERT_WANDERING_TO_MISC = 99, \ + TAT_TRAIT_CONVERT_GATHERING_TO_WANDERING = 99, TAT_TRAIT_CONVERT_GATHERING_TO_CRAFTING = 99, TAT_TRAIT_CONVERT_GATHERING_TO_MISC = 99, \ + TAT_TRAIT_CONVERT_CRAFTING_TO_WANDERING = 99, TAT_TRAIT_CONVERT_CRAFTING_TO_GATHERING = 99, TAT_TRAIT_CONVERT_CRAFTING_TO_MISC = 99, \ + TAT_TRAIT_CONVERT_MISC_TO_WANDERING = 99, TAT_TRAIT_CONVERT_MISC_TO_GATHERING = 99, TAT_TRAIT_CONVERT_MISC_TO_CRAFTING = 99 \ +) + +#define TAT_TRAIT_SKILL_DOMAIN_CONVERSION_RULES list( \ + TAT_TRAIT_CONVERT_COMBAT_TO_WANDERING = list("from" = TAT_SKILL_DOMAIN_COMBAT, "to" = TAT_SKILL_DOMAIN_WANDERING, "amount" = 1), \ + TAT_TRAIT_CONVERT_COMBAT_TO_GATHERING = list("from" = TAT_SKILL_DOMAIN_COMBAT, "to" = TAT_SKILL_DOMAIN_GATHERING, "amount" = 1), \ + TAT_TRAIT_CONVERT_COMBAT_TO_CRAFTING = list("from" = TAT_SKILL_DOMAIN_COMBAT, "to" = TAT_SKILL_DOMAIN_CRAFTING, "amount" = 1), \ + TAT_TRAIT_CONVERT_COMBAT_TO_MISC = list("from" = TAT_SKILL_DOMAIN_COMBAT, "to" = TAT_SKILL_DOMAIN_MISC, "amount" = 1), \ + TAT_TRAIT_CONVERT_WANDERING_TO_GATHERING = list("from" = TAT_SKILL_DOMAIN_WANDERING, "to" = TAT_SKILL_DOMAIN_GATHERING, "amount" = 1), \ + TAT_TRAIT_CONVERT_WANDERING_TO_CRAFTING = list("from" = TAT_SKILL_DOMAIN_WANDERING, "to" = TAT_SKILL_DOMAIN_CRAFTING, "amount" = 1), \ + TAT_TRAIT_CONVERT_WANDERING_TO_MISC = list("from" = TAT_SKILL_DOMAIN_WANDERING, "to" = TAT_SKILL_DOMAIN_MISC, "amount" = 1), \ + TAT_TRAIT_CONVERT_GATHERING_TO_WANDERING = list("from" = TAT_SKILL_DOMAIN_GATHERING, "to" = TAT_SKILL_DOMAIN_WANDERING, "amount" = 1), \ + TAT_TRAIT_CONVERT_GATHERING_TO_CRAFTING = list("from" = TAT_SKILL_DOMAIN_GATHERING, "to" = TAT_SKILL_DOMAIN_CRAFTING, "amount" = 1), \ + TAT_TRAIT_CONVERT_GATHERING_TO_MISC = list("from" = TAT_SKILL_DOMAIN_GATHERING, "to" = TAT_SKILL_DOMAIN_MISC, "amount" = 1), \ + TAT_TRAIT_CONVERT_CRAFTING_TO_WANDERING = list("from" = TAT_SKILL_DOMAIN_CRAFTING, "to" = TAT_SKILL_DOMAIN_WANDERING, "amount" = 1), \ + TAT_TRAIT_CONVERT_CRAFTING_TO_GATHERING = list("from" = TAT_SKILL_DOMAIN_CRAFTING, "to" = TAT_SKILL_DOMAIN_GATHERING, "amount" = 1), \ + TAT_TRAIT_CONVERT_CRAFTING_TO_MISC = list("from" = TAT_SKILL_DOMAIN_CRAFTING, "to" = TAT_SKILL_DOMAIN_MISC, "amount" = 1), \ + TAT_TRAIT_CONVERT_MISC_TO_WANDERING = list("from" = TAT_SKILL_DOMAIN_MISC, "to" = TAT_SKILL_DOMAIN_WANDERING, "amount" = 1), \ + TAT_TRAIT_CONVERT_MISC_TO_GATHERING = list("from" = TAT_SKILL_DOMAIN_MISC, "to" = TAT_SKILL_DOMAIN_GATHERING, "amount" = 1), \ + TAT_TRAIT_CONVERT_MISC_TO_CRAFTING = list("from" = TAT_SKILL_DOMAIN_MISC, "to" = TAT_SKILL_DOMAIN_CRAFTING, "amount" = 1) \ +) + +#define TAT_TRAIT_SKILL_POINT_RULES list( \ + TAT_TRAIT_MASTER_OF_WANDERING = list( \ + TAT_SKILL_DOMAIN_WANDERING = 15, \ + TAT_SKILL_DOMAIN_MISC = 10 \ + ), \ + TAT_TRAIT_STRAYING_SOUL = list( \ + TAT_SKILL_DOMAIN_WANDERING = 9, \ + ), \ + TAT_TRAIT_MASTER_OF_CRAFTING = list( \ + TAT_SKILL_DOMAIN_CRAFTING = 25 \ + ), \ + TAT_TRAIT_STOCKPILER = list( \ + TAT_SKILL_DOMAIN_GATHERING = 20 \ + ), \ + TAT_TRAIT_WARRIOR_EXPERT = list( \ + TAT_SKILL_DOMAIN_COMBAT = 15 \ + ), \ + TAT_TRAIT_WARRIOR_MASTER = list( \ + TAT_SKILL_DOMAIN_COMBAT = 10 \ + ), \ + TAT_TRAIT_MAGE_INITIATE = list( \ + TAT_SKILL_DOMAIN_MISC = 2 \ + ), \ + TAT_TRAIT_MAGE_MAJOR_SLOT = list( \ + TAT_SKILL_DOMAIN_MISC = 4 \ + ), \ + TAT_TRAIT_MAGE_MINOR_SLOT_2 = list( \ + TAT_SKILL_DOMAIN_MISC = 3 \ + ), \ + TAT_TRAIT_MAGE_UTILITY_SLOT = list( \ + TAT_SKILL_DOMAIN_MISC = 4 \ + ), \ + TAT_TRAIT_DIVINE_BOON_1 = list( \ + TAT_SKILL_DOMAIN_MISC = 3 \ + ), \ + TAT_TRAIT_DIVINE_BOON_2 = list( \ + TAT_SKILL_DOMAIN_MISC = 4 \ + ), \ + TAT_TRAIT_DIVINE_BOON_3 = list( \ + TAT_SKILL_DOMAIN_MISC = 5 \ + ), \ + TAT_TRAIT_BARDIC_INSPIRATION_T1 = list( \ + TAT_SKILL_DOMAIN_MISC = 4 \ + ), \ + TAT_TRAIT_BARDIC_INSPIRATION_T2 = list( \ + TAT_SKILL_DOMAIN_MISC = 5 \ + ), \ +) + +#define TAT_TRAIT_SKILL_BONUS_RULES list( \ + TRAIT_SMITHING_EXPERT = list(/datum/skill/craft/blacksmithing = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/smelting = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/engineering = TAT_SKILL_BASIC_BOOST, /datum/skill/labor/mining = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/masonry = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/ceramics = TAT_SKILL_BASIC_BOOST), \ + TAT_TRAIT_SKILLED_FORGEHAND = list(/datum/skill/craft/blacksmithing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/smelting = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/engineering = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_SKILLED_ARMORER = list(/datum/skill/craft/armorsmithing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/masonry = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_SKILLED_WEAPONSMITH = list(/datum/skill/craft/weaponsmithing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/engineering = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_SKILLED_ARTISAN = list(/datum/skill/craft/crafting = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/ceramics = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_SKILLED_MASON = list(/datum/skill/craft/masonry = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/carpentry = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/ceramics = TAT_SKILL_DISCOUNT_BOOST), \ + TRAIT_ALCHEMY_EXPERT = list(/datum/skill/craft/alchemy = TAT_SKILL_BASIC_BOOST), \ + TAT_TRAIT_SKILLED_ALCHEMIST = list(/datum/skill/craft/alchemy = TAT_SKILL_DISCOUNT_BOOST), \ + TRAIT_MEDICINE_EXPERT = list(/datum/skill/misc/medicine = TAT_SKILL_BASIC_BOOST), \ + TAT_TRAIT_SKILLED_PHYSICKER = list(/datum/skill/misc/medicine = TAT_SKILL_DISCOUNT_BOOST), \ + TRAIT_HOMESTEAD_EXPERT = list(/datum/skill/labor/farming = TAT_SKILL_BASIC_BOOST, /datum/skill/labor/mining = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/cooking = TAT_SKILL_BASIC_BOOST, /datum/skill/labor/fishing = TAT_SKILL_BASIC_BOOST, /datum/skill/labor/butchering = TAT_SKILL_BASIC_BOOST, /datum/skill/labor/lumberjacking = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/masonry = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/ceramics = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/sewing = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/tanning = TAT_SKILL_BASIC_BOOST), \ + TAT_TRAIT_SKILLED_HOMESTEADER = list(/datum/skill/labor/farming = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/cooking = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/labor/fishing = TAT_SKILL_DISCOUNT_BOOST), \ + TRAIT_SURVIVAL_EXPERT = list(/datum/skill/craft/cooking = TAT_SKILL_BASIC_BOOST, /datum/skill/labor/fishing = TAT_SKILL_BASIC_BOOST, /datum/skill/labor/butchering = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/tanning = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/sewing = TAT_SKILL_BASIC_BOOST), \ + TAT_TRAIT_SKILLED_SURVIVALIST = list(/datum/skill/labor/butchering = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/traps = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/tanning = TAT_SKILL_DISCOUNT_BOOST), \ + TRAIT_SEWING_EXPERT = list(/datum/skill/craft/sewing = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/tanning = TAT_SKILL_BASIC_BOOST, /datum/skill/labor/butchering = TAT_SKILL_BASIC_BOOST), \ + TAT_TRAIT_SKILLED_CLOTHIER = list(/datum/skill/craft/sewing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/tanning = TAT_SKILL_DISCOUNT_BOOST), \ + TRAIT_SEEDKNOW = list(/datum/skill/labor/farming = TAT_SKILL_DISCOUNT_BOOST), \ + TRAIT_CAUTIOUS_FISHER = list(/datum/skill/labor/fishing = TAT_SKILL_DISCOUNT_BOOST), \ + TRAIT_SQUIRE_REPAIR = list(/datum/skill/craft/armorsmithing = TAT_SKILL_BASIC_BOOST, /datum/skill/craft/weaponsmithing = TAT_SKILL_BASIC_BOOST), \ + TRAIT_SELF_SUSTENANCE = list(/datum/skill/craft/crafting = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/weaponsmithing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/armorsmithing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/blacksmithing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/smelting = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/carpentry = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/masonry = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/traps = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/engineering = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/cooking = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/sewing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/tanning = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/ceramics = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/alchemy = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/labor/farming = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/labor/mining = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/labor/fishing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/labor/butchering = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/labor/lumberjacking = TAT_SKILL_DISCOUNT_BOOST), \ + TRAIT_MASTERFUL_HUNTER = list(/datum/skill/misc/hunting = TAT_SKILL_BASIC_BOOST, /datum/skill/misc/tracking = TAT_SKILL_BASIC_BOOST, /datum/skill/labor/butchering = TAT_SKILL_BASIC_BOOST), \ + TRAIT_EXPERT_HUNTER = list(/datum/skill/misc/hunting = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/misc/tracking = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_SMITH = list(/datum/skill/craft/blacksmithing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/smelting = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/maces = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_ARMORER = list(/datum/skill/craft/armorsmithing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/masonry = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/shields = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_WEAPONSMITH = list(/datum/skill/craft/weaponsmithing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/engineering = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/swords = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_WOODSMAN = list(/datum/skill/labor/lumberjacking = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/carpentry = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/axes = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_SURVIVALIST = list(/datum/skill/labor/butchering = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/traps = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/bows = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_POACHER = list(/datum/skill/misc/tracking = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/traps = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/crossbows = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_SKULKER = list(/datum/skill/misc/sneaking = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/misc/lockpicking = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/knives = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_VAGABOND = list(/datum/skill/misc/stealing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/misc/climbing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/slings = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_RIDER = list(/datum/skill/misc/riding = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/misc/athletics = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/polearms = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_MARINER = list(/datum/skill/misc/swimming = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/labor/fishing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/staves = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_CLOTHIER = list(/datum/skill/craft/sewing = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/tanning = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/whipsflails = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_HOMESTEADER = list(/datum/skill/labor/farming = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/cooking = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/wrestling = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_ARTISAN = list(/datum/skill/craft/crafting = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/craft/ceramics = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/unarmed = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_CHIRURGEON = list(/datum/skill/misc/medicine = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/misc/reading = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/staves = TAT_SKILL_DISCOUNT_BOOST), \ + TAT_TRAIT_TRAINEE_TROUBADOUR = list(/datum/skill/misc/music = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/misc/reading = TAT_SKILL_DISCOUNT_BOOST, /datum/skill/combat/knives = TAT_SKILL_DISCOUNT_BOOST) \ +) + +#define TAT_TRAIT_SKILL_DISCOUNT_RULES list( \ + TAT_TRAIT_TRAINEE_SMITH = list(/datum/skill/craft/blacksmithing, /datum/skill/craft/smelting, /datum/skill/combat/maces), \ + TAT_TRAIT_TRAINEE_ARMORER = list(/datum/skill/craft/armorsmithing, /datum/skill/craft/masonry, /datum/skill/combat/shields), \ + TAT_TRAIT_TRAINEE_WEAPONSMITH = list(/datum/skill/craft/weaponsmithing, /datum/skill/craft/engineering, /datum/skill/combat/swords), \ + TAT_TRAIT_TRAINEE_WOODSMAN = list(/datum/skill/labor/lumberjacking, /datum/skill/craft/carpentry, /datum/skill/combat/axes), \ + TAT_TRAIT_TRAINEE_SURVIVALIST = list(/datum/skill/labor/butchering, /datum/skill/craft/traps, /datum/skill/combat/bows), \ + TAT_TRAIT_TRAINEE_POACHER = list(/datum/skill/misc/tracking, /datum/skill/craft/traps, /datum/skill/combat/crossbows), \ + TAT_TRAIT_TRAINEE_SKULKER = list(/datum/skill/misc/sneaking, /datum/skill/misc/lockpicking, /datum/skill/combat/knives), \ + TAT_TRAIT_TRAINEE_VAGABOND = list(/datum/skill/misc/stealing, /datum/skill/misc/climbing, /datum/skill/combat/slings), \ + TAT_TRAIT_TRAINEE_RIDER = list(/datum/skill/misc/riding, /datum/skill/misc/athletics, /datum/skill/combat/polearms), \ + TAT_TRAIT_TRAINEE_MARINER = list(/datum/skill/misc/swimming, /datum/skill/labor/fishing, /datum/skill/combat/staves), \ + TAT_TRAIT_TRAINEE_CLOTHIER = list(/datum/skill/craft/sewing, /datum/skill/craft/tanning, /datum/skill/combat/whipsflails), \ + TAT_TRAIT_TRAINEE_HOMESTEADER = list(/datum/skill/labor/farming, /datum/skill/craft/cooking, /datum/skill/combat/wrestling), \ + TAT_TRAIT_TRAINEE_ARTISAN = list(/datum/skill/craft/crafting, /datum/skill/craft/ceramics, /datum/skill/combat/unarmed), \ + TAT_TRAIT_TRAINEE_CHIRURGEON = list(/datum/skill/misc/medicine, /datum/skill/misc/reading, /datum/skill/combat/staves), \ + TAT_TRAIT_TRAINEE_TROUBADOUR = list(/datum/skill/misc/music, /datum/skill/misc/reading, /datum/skill/combat/knives) \ +) + +GLOBAL_LIST_INIT(tat_available_traits, list(TAT_AVAILABLE_TRAITS_LIST)) +GLOBAL_LIST_INIT(tat_capped_negative_traits, TAT_CAPPED_NEGATIVE_TRAITS) +GLOBAL_LIST_EMPTY(tat_trait_conflict_map) +GLOBAL_LIST_EMPTY(tat_trait_requirement_map) +GLOBAL_LIST_INIT(tat_trait_stat_point_rules, TAT_TRAIT_STAT_POINT_RULES) +GLOBAL_LIST_INIT(tat_trait_item_point_rules, TAT_TRAIT_ITEM_POINT_RULES) +GLOBAL_LIST_INIT(tat_trait_item_unlock_rules, TAT_TRAIT_ITEM_UNLOCK_RULES) +GLOBAL_LIST_INIT(tat_trait_skill_point_rules, TAT_TRAIT_SKILL_POINT_RULES) +GLOBAL_LIST_INIT(tat_trait_skill_bonus_rules, TAT_TRAIT_SKILL_BONUS_RULES) +GLOBAL_LIST_INIT(tat_trait_skill_discount_rules, TAT_TRAIT_SKILL_DISCOUNT_RULES) +GLOBAL_LIST_INIT(tat_trait_skill_domain_conversion_rules, TAT_TRAIT_SKILL_DOMAIN_CONVERSION_RULES) +GLOBAL_LIST_INIT(tat_trait_pq_lock_rules, TAT_TRAIT_PQ_LOCK_RULES) +GLOBAL_LIST_INIT(tat_armor_supplier_traits, TAT_ARMOR_SUPPLIER_TRAITS) +GLOBAL_LIST_INIT(tat_material_supplier_traits, TAT_MATERIAL_SUPPLIER_TRAITS) +GLOBAL_LIST_INIT(tat_trait_armor_training_supplier_discount_rules, TAT_ARMOR_TRAINING_SUPPLIER_DISCOUNT_RULES) +GLOBAL_LIST_INIT(tat_supplier_trait_unlocks, build_tat_supplier_trait_unlocks()) + +/proc/build_tat_supplier_trait_unlocks() + var/list/result = list() + var/list/rules = TAT_TRAIT_ITEM_UNLOCK_RULES + for(var/unlock_type in rules) + var/list/keys = rules[unlock_type] + if(!islist(keys)) + continue + for(var/unlock_key in keys) + var/trait_id = keys[unlock_key] + if(!trait_id) + continue + result[trait_id] = list("unlock_type" = unlock_type, "unlock_key" = unlock_key) + return result diff --git a/modular_twilight_axis/code/datums/tat_system/core/tat_admin_panel.dm b/modular_twilight_axis/code/datums/tat_system/core/tat_admin_panel.dm new file mode 100644 index 00000000000..705d3a4e5e8 --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/core/tat_admin_panel.dm @@ -0,0 +1,324 @@ +/// TGUI admin integration for SQL-backed TAT role locks. +/// +/// The actual restriction rows are normal Twilight-Axis role bans in SS13_ban: +/// TAT Towner, TAT Trader, TAT Adventurer. This panel is only a pleasant TGUI +/// front-end over the stock ban pipeline. + +/client/var/datum/tat_role_locks_admin_panel/tat_role_locks_panel + +/proc/tat_get_role_lock_state_text(raw_key, bucket) + return tat_is_role_bucket_locked(raw_key, bucket) ? "Locked" : "Open" + +/proc/tat_apply_restriction_side_effects_to_online_client(raw_key) + var/key = tat_normalize_ckey(raw_key) + if(!key) + return FALSE + + for(var/client/C in GLOB.clients) + if(C.ckey != key) + continue + + if(!ishuman(C.mob)) + return TRUE + + var/mob/living/carbon/human/H = C.mob + var/datum/tat_build/build = C.prefs?.tat_build + if(!build) + return TRUE + + build.attach_preferences_from_mob(H) + + if(build.is_owner_tat_banned(H) || build.is_owner_tat_role_locked(H)) + build.disable_from_human(H) + + if(build.is_owner_tat_banned(H)) + tat_tell_banned(H) + else + to_chat(H, span_warning(build.get_owner_tat_role_lock_message(H))) + + return TRUE + + return FALSE + +/client/proc/tat_admin_set_role_bucket_for_ckey(raw_key, bucket, allow_role = null, reason = null, duration = null, interval = TAT_ROLE_LOCK_DEFAULT_INTERVAL, severity = TAT_ROLE_LOCK_DEFAULT_SEVERITY, applies_to_admins = FALSE) + if(!tat_admin_can_manage_role_locks(src)) + return FALSE + + var/key = tat_normalize_ckey(raw_key) + if(!key) + to_chat(src, span_warning("Invalid ckey.")) + return FALSE + + if(!tat_is_valid_role_bucket(bucket)) + to_chat(src, span_warning("Invalid TAT role bucket.")) + return FALSE + + if(isnull(allow_role)) + allow_role = tat_is_role_bucket_locked(key, bucket) + + var/bucket_name = tat_role_bucket_display_name(bucket) + + if(allow_role) + if(!tat_remove_role_lock(src, key, bucket, reason)) + to_chat(src, span_warning("Failed to unlock [bucket_name] TAT role for [key].")) + return FALSE + + tat_apply_restriction_side_effects_to_online_client(key) + return TRUE + + if(!istext(reason) || !length(trim(reason))) + reason = TAT_ROLE_LOCK_DEFAULT_REASON + else + reason = trim(reason) + + if(!tat_create_role_lock(src, key, bucket, duration, interval, severity, reason, applies_to_admins)) + to_chat(src, span_warning("Failed to lock [bucket_name] TAT role for [key].")) + return FALSE + + tat_apply_restriction_side_effects_to_online_client(key) + return TRUE + +/// Builds a tiny legacy href entry for the INDIVIDUAL player panel. +/// The actual controls are TGUI; this is only the launch button for the old browse() player panel. +/proc/tat_build_admin_role_locks_html(raw_key, href_prefix, refresh_ref = null) + var/key = tat_normalize_ckey(raw_key) + if(!key || !istext(href_prefix) || !length(href_prefix)) + return "" + + return "

TAT role locks: Open TGUI" + +/// Call this near the top of /datum/admins/Topic(), after CheckAdminHref(). +/// Returns TRUE when the href was a TAT action and was fully handled. +/client/proc/tat_handle_admin_panel_href(list/href_list) + if(!holder || !islist(href_list)) + return FALSE + + var/key = href_list["tat_open_role_locks"] + if(!key) + return FALSE + + tat_open_role_locks_panel(key) + return TRUE + +/client/proc/tat_open_role_locks_panel(raw_key = null) + if(!tat_admin_can_manage_role_locks(src)) + to_chat(src, span_warning("You need ban permissions to manage TAT role locks.")) + return FALSE + + if(QDELETED(tat_role_locks_panel)) + tat_role_locks_panel = null + + if(!tat_role_locks_panel) + tat_role_locks_panel = new(src) + + if(raw_key) + tat_role_locks_panel.set_selected_ckey(raw_key) + + tat_role_locks_panel.ui_interact(mob) + return TRUE + +/client/proc/tat_role_locks_panel() + set name = "TAT Role Locks" + set category = "-Admin-" + + tat_open_role_locks_panel() + +/proc/tat_cmp_client_ckey_asc(client/A, client/B) + return sorttext(A?.ckey || "", B?.ckey || "") + +/datum/ui_state/tat_role_locks_admin_state/can_use_topic(src_object, mob/user) + if(tat_admin_can_manage_role_locks(user?.client)) + return UI_INTERACTIVE + + return UI_CLOSE + +/datum/tat_role_locks_admin_panel + var/client/admin_client + var/selected_ckey + var/filter = "" + var/default_reason = TAT_ROLE_LOCK_DEFAULT_REASON + var/duration = TAT_ROLE_LOCK_DEFAULT_DURATION + var/interval = TAT_ROLE_LOCK_DEFAULT_INTERVAL + var/permanent = FALSE + var/severity = TAT_ROLE_LOCK_DEFAULT_SEVERITY + var/applies_to_admins = FALSE + +/datum/tat_role_locks_admin_panel/New(client/C) + . = ..() + admin_client = C + + if(C?.ckey) + selected_ckey = C.ckey + +/datum/tat_role_locks_admin_panel/proc/set_selected_ckey(raw_key) + var/key = tat_normalize_ckey(raw_key) + if(!key) + return FALSE + + selected_ckey = key + return TRUE + +/datum/tat_role_locks_admin_panel/ui_state(mob/user) + var/static/datum/ui_state/tat_role_locks_admin_state/state = new + return state + +/datum/tat_role_locks_admin_panel/ui_interact(mob/user, datum/tgui/ui) + if(!tat_admin_can_manage_role_locks(user?.client)) + return + + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "TatRoleLocksPanel") + ui.open() + +/datum/tat_role_locks_admin_panel/proc/build_player_rows() + var/list/result = list() + var/filter_key = lowertext(trim(filter || "")) + + for(var/client/C in sort_list(GLOB.clients, GLOBAL_PROC_REF(tat_cmp_client_ckey_asc))) + if(!C?.ckey) + continue + + if(length(filter_key) && !findtext(lowertext(C.ckey), filter_key) && !findtext(lowertext(C.key), filter_key)) + continue + + result += list(list( + "key" = C.key, + "ckey" = C.ckey, + "mob_name" = C.mob ? "[C.mob]" : "No mob", + "selected" = (C.ckey == selected_ckey), + )) + + return result + +/datum/tat_role_locks_admin_panel/proc/build_role_rows(raw_key) + var/key = tat_normalize_ckey(raw_key) + var/list/result = list() + var/list/names = tat_role_bucket_names() + + for(var/bucket in names) + var/list/active_entry = key ? tat_get_locked_role_entry(key, bucket) : null + var/list/history_entry = key ? tat_get_role_lock_history_entry(key, bucket) : null + var/active_lock = islist(active_entry) + + var/list/display_entry = active_lock ? active_entry : history_entry + + var/expires = "" + if(active_lock) + expires = active_entry["expiration_time"] ? "Expires [active_entry["expiration_time"]]" : "Permanent" + else if(islist(history_entry)) + if(history_entry["unbanned_datetime"]) + expires = "Removed [history_entry["unbanned_datetime"]]" + else if(history_entry["expiration_time"]) + expires = "Expired [history_entry["expiration_time"]]" + else + expires = "Inactive" + + var/reason = "Open" + if(active_lock) + reason = active_entry["reason"] || TAT_ROLE_LOCK_DEFAULT_REASON + else if(islist(history_entry)) + if(history_entry["reason"]) + reason = "Previous: [history_entry["reason"]]" + else + reason = "Previous lock exists" + + result += list(list( + "id" = bucket, + "name" = names[bucket], + "locked" = active_lock, + "state" = active_lock ? "Locked" : "Open", + "reason" = reason, + "locked_by" = islist(display_entry) ? (display_entry["locked_by"] || "") : "", + "locked_at" = islist(display_entry) ? (display_entry["bantime"] || "") : "", + "expires" = expires, + "ban_id" = islist(display_entry) ? (display_entry["id"] || "") : "", + "expired_entry" = islist(history_entry) && !active_lock, + )) + + return result + +/datum/tat_role_locks_admin_panel/ui_data(mob/user) + if(!tat_admin_can_manage_role_locks(user?.client)) + return list() + + var/list/data = list() + data["players"] = build_player_rows() + data["selected_ckey"] = selected_ckey + data["filter"] = filter + data["default_reason"] = default_reason + data["duration"] = duration + data["interval"] = interval + data["permanent"] = permanent + data["severity"] = severity + data["applies_to_admins"] = applies_to_admins + data["roles"] = build_role_rows(selected_ckey) + + return data + +/datum/tat_role_locks_admin_panel/ui_act(action, list/params) + . = ..() + if(.) + return + + if(!tat_admin_can_manage_role_locks(usr?.client)) + return FALSE + + switch(action) + if("select_player") + return set_selected_ckey(params["ckey"]) + + if("set_filter") + filter = copytext(params["filter"] || "", 1, 64) + return TRUE + + if("set_manual_ckey") + return set_selected_ckey(params["ckey"]) + + if("set_default_reason") + default_reason = copytext(params["reason"] || TAT_ROLE_LOCK_DEFAULT_REASON, 1, 512) + return TRUE + + if("set_duration") + duration = max(1, text2num(params["duration"] || TAT_ROLE_LOCK_DEFAULT_DURATION)) + return TRUE + + if("set_interval") + var/new_interval = uppertext(params["interval"] || TAT_ROLE_LOCK_DEFAULT_INTERVAL) + if(new_interval in list("SECOND", "MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "YEAR")) + interval = new_interval + + return TRUE + + if("set_permanent") + permanent = !!params["permanent"] + return TRUE + + if("set_severity") + var/new_severity = params["severity"] || TAT_ROLE_LOCK_DEFAULT_SEVERITY + if(new_severity in list("None", "Minor", "Medium", "High")) + severity = new_severity + + return TRUE + + if("set_applies_to_admins") + applies_to_admins = !!params["applies_to_admins"] + return TRUE + + if("toggle_role") + var/key = tat_normalize_ckey(params["ckey"] || selected_ckey) + var/bucket = params["bucket"] + + if(!key || !tat_is_valid_role_bucket(bucket)) + return FALSE + + var/allow_role = tat_is_role_bucket_locked(key, bucket) + var/reason = params["reason"] || default_reason || TAT_ROLE_LOCK_DEFAULT_REASON + var/actual_duration = permanent ? null : duration + + if(allow_role) + return tat_remove_role_lock(key, bucket, reason) + + return usr.client.tat_admin_set_role_bucket_for_ckey(key, bucket, allow_role, reason, actual_duration, interval, severity, applies_to_admins) + + return FALSE diff --git a/modular_twilight_axis/code/datums/tat_system/core/tat_bans.dm b/modular_twilight_axis/code/datums/tat_system/core/tat_bans.dm new file mode 100644 index 00000000000..d05d03f02b5 --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/core/tat_bans.dm @@ -0,0 +1,405 @@ +/proc/tat_normalize_ckey(raw_key) + if(!istext(raw_key)) + return null + var/key = ckey(raw_key) + if(!length(key)) + return null + return key + +/proc/tat_role_bucket_names() + var/list/names = list() + names[TAT_ROLE_BUCKET_TOWNER] = "Towner" + names[TAT_ROLE_BUCKET_TRADER] = "Trader" + names[TAT_ROLE_BUCKET_ADVENTURER] = "Adventurer" + names[TAT_ROLE_BUCKET_WRETCH] = "Wretch" + return names + +/proc/tat_is_valid_role_bucket(bucket) + if(!istext(bucket)) + return FALSE + return bucket in tat_role_bucket_names() + +/proc/tat_role_bucket_display_name(bucket) + var/list/names = tat_role_bucket_names() + return names[bucket] || "Unknown" + +/proc/tat_role_bucket_to_ban_role(bucket) + switch(bucket) + if(TAT_ROLE_BUCKET_TOWNER) + return TAT_SQL_ROLE_TOWNER + if(TAT_ROLE_BUCKET_TRADER) + return TAT_SQL_ROLE_TRADER + if(TAT_ROLE_BUCKET_ADVENTURER) + return TAT_SQL_ROLE_ADVENTURER + if(TAT_ROLE_BUCKET_WRETCH) + return TAT_SQL_ROLE_WRETCH + return null + +/proc/tat_ban_role_to_role_bucket(role) + switch(role) + if(TAT_SQL_ROLE_TOWNER) + return TAT_ROLE_BUCKET_TOWNER + if(TAT_SQL_ROLE_TRADER) + return TAT_ROLE_BUCKET_TRADER + if(TAT_SQL_ROLE_ADVENTURER) + return TAT_ROLE_BUCKET_ADVENTURER + if(TAT_SQL_ROLE_WRETCH) + return TAT_ROLE_BUCKET_WRETCH + return null + +/proc/tat_all_sql_role_locks() + return list(TAT_SQL_ROLE_TOWNER, TAT_SQL_ROLE_TRADER, TAT_SQL_ROLE_ADVENTURER, TAT_SQL_ROLE_WRETCH) + +/proc/tat_is_ckey_banned(raw_key) + var/key = tat_normalize_ckey(raw_key) + if(!key) + return FALSE + return is_banned_from(key, TAT_SQL_ROLE_SYSTEM) + +/proc/tat_get_ban_reason(raw_key) + var/list/entry = tat_get_sql_ban_entry(raw_key, TAT_SQL_ROLE_SYSTEM) + if(!islist(entry)) + return null + var/reason = entry["reason"] + if(!istext(reason) || !length(reason)) + return TAT_BAN_DEFAULT_REASON + return reason + +/proc/tat_is_mob_banned(mob/user) + if(!user?.ckey) + return FALSE + return tat_is_ckey_banned(user.ckey) + +/proc/tat_tell_banned(mob/user) + if(!user) + return FALSE + var/reason = tat_get_ban_reason(user.ckey) || TAT_BAN_DEFAULT_REASON + to_chat(user, span_warning("You are banned from using the TAT build system. Reason: [reason]")) + return TRUE + +/proc/tat_get_sql_ban_entry(raw_key, role) + var/key = tat_normalize_ckey(raw_key) + if(!key || !istext(role) || !length(role)) + return null + if(!SSdbcore.Connect()) + return null + + var/datum/DBQuery/query_get_tat_ban = SSdbcore.NewQuery({" + SELECT id, bantime, round_id, role, expiration_time, TIMESTAMPDIFF(MINUTE, bantime, expiration_time), reason, a_ckey + FROM [format_table_name("ban")] + WHERE ckey = :ckey + AND role = :role + AND unbanned_datetime IS NULL + AND (expiration_time IS NULL OR expiration_time > NOW()) + ORDER BY bantime DESC + LIMIT 1 + "}, list("ckey" = key, "role" = role)) + if(!query_get_tat_ban.warn_execute()) + qdel(query_get_tat_ban) + return null + + var/list/result + if(query_get_tat_ban.NextRow()) + result = list( + "id" = query_get_tat_ban.item[1], + "bantime" = query_get_tat_ban.item[2], + "round_id" = query_get_tat_ban.item[3], + "role" = query_get_tat_ban.item[4], + "expiration_time" = query_get_tat_ban.item[5], + "duration_minutes" = query_get_tat_ban.item[6], + "reason" = query_get_tat_ban.item[7], + "locked_by" = query_get_tat_ban.item[8], + ) + + qdel(query_get_tat_ban) + return result + +/proc/tat_get_locked_role_entry(raw_key, bucket) + if(!tat_is_valid_role_bucket(bucket)) + return null + return tat_get_sql_ban_entry(raw_key, tat_role_bucket_to_ban_role(bucket)) + +/proc/tat_is_role_bucket_locked(raw_key, bucket) + var/key = tat_normalize_ckey(raw_key) + if(!key || !tat_is_valid_role_bucket(bucket)) + return FALSE + + return islist(tat_get_locked_role_entry(key, bucket)) + +/proc/tat_get_sql_ban_history_entry(raw_key, role) + var/key = tat_normalize_ckey(raw_key) + if(!key || !istext(role) || !length(role)) + return null + if(!SSdbcore.Connect()) + return null + + var/datum/DBQuery/query_get_tat_ban_history = SSdbcore.NewQuery({" + SELECT id, bantime, round_id, role, expiration_time, TIMESTAMPDIFF(MINUTE, bantime, expiration_time), reason, a_ckey, unbanned_datetime, unbanned_ckey + FROM [format_table_name("ban")] + WHERE ckey = :ckey + AND role = :role + ORDER BY bantime DESC + LIMIT 1 + "}, list( + "ckey" = key, + "role" = role, + )) + + if(!query_get_tat_ban_history.warn_execute()) + qdel(query_get_tat_ban_history) + return null + + var/list/result + if(query_get_tat_ban_history.NextRow()) + result = list( + "id" = query_get_tat_ban_history.item[1], + "bantime" = query_get_tat_ban_history.item[2], + "round_id" = query_get_tat_ban_history.item[3], + "role" = query_get_tat_ban_history.item[4], + "expiration_time" = query_get_tat_ban_history.item[5], + "duration_minutes" = query_get_tat_ban_history.item[6], + "reason" = query_get_tat_ban_history.item[7], + "locked_by" = query_get_tat_ban_history.item[8], + "unbanned_datetime" = query_get_tat_ban_history.item[9], + "unbanned_by" = query_get_tat_ban_history.item[10], + ) + + qdel(query_get_tat_ban_history) + return result + + +/proc/tat_get_role_lock_history_entry(raw_key, bucket) + if(!tat_is_valid_role_bucket(bucket)) + return null + return tat_get_sql_ban_history_entry(raw_key, tat_role_bucket_to_ban_role(bucket)) + +/proc/tat_get_role_lock_reason(raw_key, bucket) + var/list/entry = tat_get_locked_role_entry(raw_key, bucket) + if(!islist(entry)) + return null + var/reason = entry["reason"] + if(!istext(reason) || !length(reason)) + return TAT_ROLE_LOCK_DEFAULT_REASON + return reason + +/proc/tat_format_duration_message(duration, interval) + if(isnull(duration)) + return "permanently" + + var/amount = text2num(duration) + if(amount <= 0) + return "permanently" + + if(!(interval in list("SECOND", "MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "YEAR"))) + interval = "MINUTE" + + var/time_message = "[amount] [lowertext(interval)]" + if(amount != 1) + time_message += "s" + + return "for [time_message]" + +/proc/tat_refresh_ban_cache_for_ckey(raw_key) + var/key = tat_normalize_ckey(raw_key) + if(!key) + return FALSE + + var/client/C = GLOB.directory[key] + if(C) + build_ban_cache(C) + + return TRUE + +/proc/tat_sql_safe_ip(raw_ip) + if(istext(raw_ip) && length(raw_ip)) + return raw_ip + return "127.0.0.1" + +/proc/tat_sql_safe_cid(raw_cid) + if(!isnull(raw_cid) && "[raw_cid]" != "") + return raw_cid + return "0" + +/proc/tat_collect_online_lists_for_ban() + var/list/clients_online = GLOB.clients.Copy() + var/list/admins_online = list() + + for(var/client/C in clients_online) + if(C.holder) + admins_online += C + + return list( + "who" = clients_online.Join(", "), + "adminwho" = admins_online.Join(", "), + ) + +/proc/tat_ban_target_string(player_key, player_ip, player_cid) + var/list/parts = list() + + if(istext(player_key) && length(player_key)) + parts += player_key + + if(istext(player_ip) && length(player_ip)) + parts += "IP: [player_ip]" + + if(!isnull(player_cid) && "[player_cid]" != "") + parts += "CID: [player_cid]" + + if(!length(parts)) + return "unknown target" + + return parts.Join(" / ") + +/proc/tat_create_role_lock(client/admin, raw_key, bucket, duration = null, interval = TAT_ROLE_LOCK_DEFAULT_INTERVAL, severity = TAT_ROLE_LOCK_DEFAULT_SEVERITY, reason = TAT_ROLE_LOCK_DEFAULT_REASON, applies_to_admins = FALSE) + if(!admin?.holder || !check_rights_for(admin, R_BAN)) + return FALSE + + if(!SSdbcore.Connect()) + to_chat(admin, span_danger("Failed to establish database connection.")) + return FALSE + + var/key = tat_normalize_ckey(raw_key) + if(!key || !tat_is_valid_role_bucket(bucket)) + return FALSE + + if(tat_is_role_bucket_locked(key, bucket)) + return TRUE + + if(!istext(reason) || !length(trim(reason))) + reason = TAT_ROLE_LOCK_DEFAULT_REASON + else + reason = trim(reason) + + if(!(severity in list("None", "Minor", "Medium", "High"))) + severity = TAT_ROLE_LOCK_DEFAULT_SEVERITY + + if(!(interval in list("SECOND", "MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "YEAR"))) + interval = TAT_ROLE_LOCK_DEFAULT_INTERVAL + + duration = isnull(duration) ? null : max(1, text2num(duration)) + + var/sql_role = tat_role_bucket_to_ban_role(bucket) + var/bucket_name = tat_role_bucket_display_name(bucket) + var/time_message = tat_format_duration_message(duration, interval) + var/note_reason = "Banned from Roles: [sql_role] [time_message] - [reason]" + + var/player_key = key + var/player_ip = "127.0.0.1" + var/player_cid = "0" + + var/client/target_client = GLOB.directory[key] + if(target_client) + player_key = target_client.key || key + player_ip = tat_sql_safe_ip(target_client.address) + player_cid = tat_sql_safe_cid(target_client.computer_id) + + var/list/online_data = tat_collect_online_lists_for_ban() + var/admin_ip = tat_sql_safe_ip(admin.address) + var/admin_cid = tat_sql_safe_cid(admin.computer_id) + var/server_ip = tat_sql_safe_ip(world.internet_address) + var/round_id = GLOB.round_id || 0 + + var/datum/DBQuery/query_create_tat_role_lock = SSdbcore.NewQuery({" + INSERT INTO [format_table_name("ban")] + (server_ip, server_port, round_id, role, expiration_time, applies_to_admins, reason, ckey, ip, computerid, a_ckey, a_ip, a_computerid, who, adminwho, bantime) + VALUES + (INET_ATON(:server_ip), :server_port, :round_id, :role, IF(:duration IS NULL, NULL, NOW() + INTERVAL :duration [interval]), :applies_to_admins, :reason, :ckey, INET_ATON(:player_ip), :player_cid, :admin_ckey, INET_ATON(:admin_ip), :admin_cid, :who, :adminwho, NOW()) + "}, list( + "server_ip" = server_ip, + "server_port" = world.port || 0, + "round_id" = round_id, + "role" = sql_role, + "duration" = duration, + "applies_to_admins" = applies_to_admins, + "reason" = reason, + "ckey" = key, + "player_ip" = player_ip, + "player_cid" = player_cid, + "admin_ckey" = admin.ckey, + "admin_ip" = admin_ip, + "admin_cid" = admin_cid, + "who" = online_data["who"], + "adminwho" = online_data["adminwho"], + )) + + if(!query_create_tat_role_lock.warn_execute()) + qdel(query_create_tat_role_lock) + return FALSE + + qdel(query_create_tat_role_lock) + + create_message("note", key, admin.ckey, note_reason, null, null, 0, 0, null, 0, severity) + + var/target = tat_ban_target_string(player_key, player_ip, player_cid) + var/kn = key_name(admin) + var/kna = key_name_admin(admin) + var/msg = "has created a [isnull(duration) ? "permanent" : "temporary [time_message]"] TAT role lock for [target]." + + log_admin_private("[kn] [msg] Role: [sql_role] Reason: [reason]") + message_admins("[kna] [msg]
Role: [sql_role]
Reason: [reason]") + + tat_refresh_ban_cache_for_ckey(key) + + if(target_client) + to_chat(target_client, span_boldannounce("You have been [applies_to_admins ? "admin " : ""]banned by [admin.key] from TAT [bucket_name].
Reason: [reason]
This ban is [isnull(duration) ? "permanent." : "temporary, it will be removed in [time_message]."] The round ID is [round_id].")) + return TRUE + +/proc/tat_remove_role_lock(raw_key, bucket, reason = null) + var/client/admin = usr.client + if(!admin?.holder || !check_rights_for(admin, R_BAN)) + return FALSE + + var/key = tat_normalize_ckey(raw_key) + if(!key || !tat_is_valid_role_bucket(bucket)) + return FALSE + + if(!SSdbcore.Connect()) + to_chat(usr, span_danger("Failed to establish database connection.")) + return FALSE + + var/sql_role = tat_role_bucket_to_ban_role(bucket) + var/list/entry = tat_get_sql_ban_entry(key, sql_role) + if(!islist(entry)) + return TRUE + + var/datum/DBQuery/query_unlock_tat_role = SSdbcore.NewQuery({" + UPDATE [format_table_name("ban")] + SET unbanned_datetime = NOW(), + unbanned_ckey = :admin_ckey, + unbanned_ip = INET_ATON(:admin_ip), + unbanned_computerid = :admin_cid, + unbanned_round_id = :round_id + WHERE ckey = :ckey + AND role = :role + AND unbanned_datetime IS NULL + AND (expiration_time IS NULL OR expiration_time > NOW()) + "}, list( + "admin_ckey" = admin.ckey, + "admin_ip" = tat_sql_safe_ip(admin.address), + "admin_cid" = tat_sql_safe_cid(admin.computer_id), + "round_id" = GLOB.round_id || 0, + "ckey" = key, + "role" = sql_role, + )) + + if(!query_unlock_tat_role.warn_execute()) + qdel(query_unlock_tat_role) + return FALSE + + qdel(query_unlock_tat_role) + + var/bucket_name = tat_role_bucket_display_name(bucket) + var/note_reason = "TAT role lock removed: [bucket_name][istext(reason) && length(trim(reason)) ? " - [trim(reason)]" : ""]" + + create_message("note", key, admin.ckey, note_reason, null, null, 0, 0, null, 0, "None") + log_admin_private("[key_name(admin)] removed TAT role lock [bucket_name] from [key].") + message_admins("[key_name_admin(admin)] removed TAT role lock [bucket_name] from [key].") + + var/client/C = GLOB.directory[key] + if(C) + build_ban_cache(C) + to_chat(C, span_boldannounce("[admin.key] has removed your TAT [bucket_name] role lock.")) + return TRUE + +/proc/tat_admin_can_manage_role_locks(client/C) + return C?.holder && check_rights_for(C, R_BAN) diff --git a/modular_twilight_axis/code/datums/tat_system/core/tat_build.dm b/modular_twilight_axis/code/datums/tat_system/core/tat_build.dm new file mode 100644 index 00000000000..52e3de49662 --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/core/tat_build.dm @@ -0,0 +1,833 @@ +/datum/tat_build +/datum/tat_build + var/datum/preferences/owner_preferences = null + + var/datum/tat_stats/stats + var/datum/tat_items/items + var/datum/tat_traits/traits + var/datum/tat_skills/skills + + var/list/magic_profile = list() + var/list/_cached_active_virtues = null + var/_cached_active_virtues_key = null + var/_cached_preference_loadout_key = null + var/list/_cached_ui_data = null + var/_ui_data_cache_dirty = TRUE + + var/last_exported_json = null + var/last_json_error = null + var/last_json_notice = null + + var/list/tat_slots = list() + var/active_tat_slot = 1 + var/list/tat_presets = list() + var/list/ui_tat_presets_cache = null + + var/list/ui_items_state_cache = null + var/list/ui_loadout_cache = null + var/list/ui_skills_cache = null + var/list/ui_tat_slots_cache = null + + var/dirty = FALSE + +/datum/tat_build/New(datum/preferences/P) + . = ..() + owner_preferences = P + stats = new(src) + items = new(src) + traits = new(src) + skills = new(src) + reset() + init_tat_slots() + +/datum/tat_build/proc/reset() + traits.reset() + stats.reset() + skills.reset() + items.reset() + magic_profile = list() + _cached_preference_loadout_key = null + dirty = FALSE + invalidate_ui_data_cache() + return TRUE + +/datum/tat_build/proc/attach_preferences(datum/preferences/P) + owner_preferences = P + return TRUE + +/datum/tat_build/proc/get_owner_ckey() + if(owner_preferences) + var/client/parent_client = owner_preferences.vars["parent"] + if(parent_client?.ckey) + return parent_client.ckey + var/client/direct_client = owner_preferences.vars["client"] + if(direct_client?.ckey) + return direct_client.ckey + var/stored_ckey = owner_preferences.vars["last_ckey"] + if(istext(stored_ckey) && length(stored_ckey)) + return ckey(stored_ckey) + if(usr?.ckey) + return usr.ckey + return null + +/datum/tat_build/proc/get_owner_client() + if(owner_preferences) + var/client/parent_client = owner_preferences.vars["parent"] + if(parent_client) + return parent_client + var/client/direct_client = owner_preferences.vars["client"] + if(direct_client) + return direct_client + if(usr?.client) + return usr.client + return null + +/datum/tat_build/proc/is_owner_admin() + var/client/owner_client = get_owner_client() + if(owner_client?.holder) + return TRUE + + var/owner_ckey = get_owner_ckey() + if(owner_ckey && usr?.client?.holder && usr.ckey == owner_ckey) + return TRUE + + return FALSE + +/datum/tat_build/proc/can_select_contractor_trait() + return is_owner_admin() //TRUE для фулланлока + +/datum/tat_build/proc/is_owner_tat_banned(mob/user = null) + if(user?.ckey) + return tat_is_ckey_banned(user.ckey) + var/key = get_owner_ckey() + if(!key) + return FALSE + return tat_is_ckey_banned(key) + +/datum/tat_build/proc/is_owner_tat_role_locked(mob/user = null) + var/key = user?.ckey || get_owner_ckey() + if(!key) + return FALSE + return tat_is_role_bucket_locked(key, get_role_bucket()) + +/datum/tat_build/proc/get_owner_tat_role_lock_message(mob/user = null) + var/key = user?.ckey || get_owner_ckey() + var/bucket = get_role_bucket() + var/bucket_name = tat_role_bucket_display_name(bucket) + var/reason = key ? tat_get_role_lock_reason(key, bucket) : null + if(!reason) + reason = TAT_ROLE_LOCK_DEFAULT_REASON + return "You are locked out of the TAT [bucket_name] role bucket. Reason: [reason]" + +/datum/tat_build/proc/get_owner_playerquality() + var/key = get_owner_ckey() + if(!key) + return 0 + return round(get_playerquality(key)) + +/datum/tat_build/proc/invalidate_ui_caches() + return invalidate_ui_data_cache() + +/datum/tat_build/proc/get_active_tat_slot_name() + init_tat_slots() + var/datum/tat_slot/slot = get_tat_slot(active_tat_slot) + if(!slot || !istext(slot.name) || !length(trim(slot.name))) + return get_default_tat_slot_name(active_tat_slot) + return trim(slot.name) + +/datum/tat_build/proc/set_dirty(flag = TRUE) + dirty = !!flag + invalidate_ui_data_cache() + return dirty + +/datum/tat_build/proc/invalidate_ui_data_cache() + _cached_ui_data = null + _ui_data_cache_dirty = TRUE + ui_items_state_cache = null + ui_loadout_cache = null + ui_skills_cache = null + ui_tat_slots_cache = null + return TRUE + +/datum/tat_build/proc/get_active_virtues_cache_key(datum/preferences/P) + if(!P) + return null + + var/list/parts = list() + var/list/virtues = list(P.virtue, P.virtuetwo) + for(var/virtue_entry in virtues) + var/datum/virtue/virtue = virtue_entry + if(!virtue || istype(virtue, /datum/virtue/none)) + parts += "none" + continue + + var/part = "[virtue.type]:[REF(virtue)]" + if(islist(virtue.picked_choices)) + for(var/choice in virtue.picked_choices) + part += ":[choice]" + parts += part + return parts.Join("|") + + +/datum/tat_build/proc/attach_preferences_from_mob(mob/user) + if(!user?.client?.prefs) + return FALSE + var/datum/preferences/P = user.client.prefs + if(P.tat_build != src) + return FALSE + + var/new_virtues_key = get_active_virtues_cache_key(P) + var/preferences_changed = owner_preferences != P + var/virtues_changed = _cached_active_virtues_key != new_virtues_key + + owner_preferences = P + + if(preferences_changed || virtues_changed) + _cached_active_virtues = null + _cached_active_virtues_key = null + skills?.sanitize(FALSE) + invalidate_ui_data_cache() + + if(!preferences_changed && !virtues_changed) + attach_preferences(P) + + return TRUE + +/datum/tat_build/proc/get_active_virtues() + var/cache_key = get_active_virtues_cache_key(owner_preferences) + if(islist(_cached_active_virtues) && _cached_active_virtues_key == cache_key) + return _cached_active_virtues + var/list/result = list() + if(!owner_preferences) + _cached_active_virtues = result + _cached_active_virtues_key = cache_key + return result + + if(owner_preferences.virtue && !istype(owner_preferences.virtue, /datum/virtue/none)) + result += owner_preferences.virtue + + if(owner_preferences.virtuetwo && !istype(owner_preferences.virtuetwo, /datum/virtue/none)) + if(!(owner_preferences.virtuetwo in result)) + result += owner_preferences.virtuetwo + + _cached_active_virtues = result + _cached_active_virtues_key = cache_key + return result + +/datum/tat_build/proc/invalidate_active_virtues_cache() + _cached_active_virtues = null + _cached_active_virtues_key = null + +/datum/tat_build/proc/get_magic_value(key, default_value = null) + if(!istext(key) || !length(key)) + return default_value + if(!(key in magic_profile)) + return default_value + return magic_profile[key] + +/datum/tat_build/proc/set_magic_value(key, value) + if(!istext(key) || !length(key)) + return FALSE + if(isnull(value)) + magic_profile -= key + else + magic_profile[key] = value + set_dirty() + return TRUE + +/datum/tat_build/proc/has_trait(trait_id) + return traits.has_trait(trait_id) + +/datum/tat_build/proc/get_trait_cost_display(trait_id) + return traits.get_display_cost(trait_id) + +/datum/tat_build/proc/get_stat_value(stat_id) + return stats.get_value(stat_id) + +/datum/tat_build/proc/get_skill_value(skill_type) + return skills.get_total_value(skill_type) + +/datum/tat_build/proc/get_invested_skill_value(skill_type) + return skills.get_invested_value(skill_type) + +/datum/tat_build/proc/get_item_amount(item_path) + return items.get_amount(item_path) + +/datum/tat_build/proc/get_bonus_stat_points() + return traits.get_bonus_stat_points() + +/datum/tat_build/proc/get_bonus_item_points() + return traits.get_bonus_item_points() + +/datum/tat_build/proc/get_bonus_skill_domain_points(domain) + return traits.get_bonus_skill_domain_points(domain) + +/datum/tat_build/proc/get_bonus_skill_value(skill_type) + var/trait_bonus = traits.get_bonus_skill_value(skill_type) + var/virtue_bonus = skills.get_virtue_bonus_value(skill_type) + return round(trait_bonus + virtue_bonus) + +/datum/tat_build/proc/get_skill_cap_bonus_value(skill_type) + var/trait_cap = traits.get_skill_cap_bonus_value(skill_type) + var/virtue_cap = skills.get_virtue_skill_cap_bonus(skill_type) + return round(max(trait_cap, virtue_cap)) + +/datum/tat_build/proc/get_skill_cost_discount(skill_type, target_level) + return traits.get_skill_cost_discount(skill_type, target_level) + +/datum/tat_build/proc/can_keep_item(item_path) + return items.check_item(item_path) + +/datum/tat_build/proc/get_effective_divine_tier() + return traits.get_effective_divine_tier() + +/datum/tat_build/proc/get_divine_passive_gain_for_tier(cleric_tier) + return traits.get_divine_passive_gain_for_tier(cleric_tier) + +/datum/tat_build/proc/get_divine_devotion_limit_for_tier(cleric_tier) + return traits.get_divine_devotion_limit_for_tier(cleric_tier) + +/datum/tat_build/proc/build_mage_aspects(scale_with_arcane = TRUE) + return traits.build_mage_aspects(scale_with_arcane) + +/datum/tat_build/proc/can_train_arcane() + return traits.can_train_arcane() + +/datum/tat_build/proc/can_train_holy() + return traits.can_train_holy() + +/datum/tat_build/proc/can_train_druidic() + return traits.can_train_druidic() + +/datum/tat_build/proc/has_invalid_trait_dependencies() + return traits.has_invalid_trait_dependencies() + +/datum/tat_build/proc/has_invalid_supply_items() + return items.has_invalid_supply_items() + +/datum/tat_build/proc/get_validation_issues() + var/list/issues = list() + + if(stats.get_remaining_points() < 0) + issues += "Spent too many stat points." + if(skills.get_any_negative_remaining()) + issues += "Spent too many skill points." + if(traits.get_remaining_points() < 0) + issues += "Spent too many trait points." + if(items.get_remaining_points() < 0) + issues += "Spent too many item points." + + var/list/trait_issues = traits.has_invalid_trait_dependencies() + if(length(trait_issues)) + issues += trait_issues + + var/list/item_issues = items.has_invalid_supply_items() + if(length(item_issues)) + issues += item_issues + + return issues + +/datum/tat_build/proc/is_budget_valid() + return !length(get_validation_issues()) + +/datum/tat_build/proc/has_mind_spell(mob/living/carbon/human/H, spell_type) + if(!H || !H.mind || !ispath(spell_type)) + return FALSE + + if(islist(H.mind.spell_list)) + for(var/datum/existing_spell as anything in H.mind.spell_list) + if(istype(existing_spell, spell_type)) + return TRUE + + if(islist(H.actions)) + for(var/datum/action/existing_action as anything in H.actions) + if(istype(existing_action, spell_type)) + return TRUE + + return FALSE + +/datum/tat_build/proc/grant_mind_spell_if_missing(mob/living/carbon/human/H, spell_type) + if(!H || !H.mind || !ispath(spell_type)) + return FALSE + if(has_mind_spell(H, spell_type)) + return FALSE + var/datum/new_spell = new spell_type + if(!new_spell) + return FALSE + H.mind.AddSpell(new_spell) + return TRUE + +/datum/tat_build/proc/get_resident_skill_value(skill_type) + if(skill_type == /datum/skill/misc/reading) + return 3 + return 0 + +/datum/tat_build/proc/get_resident_pugilist_spell_choice(mob/living/carbon/human/H) + var/list/options = list( + "Headbutt - Vulnerable Debuff", + "Chokeslam - Stamina Damage", + "Stunner - Dazed Debuff", + "Dropkick - Pushback + Extra Damage" + ) + if(!H?.client) + return TAT_RESIDENT_PUGILIST_DEFAULT + return tgui_input_list(H, "Choose your resident pugilist style.", "Resident Pugilist", options) || TAT_RESIDENT_PUGILIST_DEFAULT + +/datum/tat_build/proc/get_resident_pugilist_spell_type(choice) + switch(choice) + if("Dropkick - Pushback + Extra Damage") + return /obj/effect/proc_holder/spell/invoked/dropkick + if("Chokeslam - Stamina Damage") + return /obj/effect/proc_holder/spell/invoked/chokeslam + if("Stunner - Dazed Debuff") + return /obj/effect/proc_holder/spell/invoked/stunner + return /obj/effect/proc_holder/spell/invoked/headbutt + +/datum/tat_build/proc/sanitize() + traits.sanitize() + stats.sanitize() + skills.sanitize() + items.sanitize() + dirty = FALSE + invalidate_ui_data_cache() + return TRUE + + +/datum/tat_build/proc/build_slot_summary_from_data(list/build_data) + if(!islist(build_data)) + return list("stats" = 0, "skills" = 0, "traits" = 0, "items" = 0) + + var/stats_spent = 0 + var/list/stat_data = build_data["stats"] + if(islist(stat_data)) + var/list/all_stats = list(TAT_AVAILABLE_STATS_LIST) + for(var/stat_id in TAT_STATS_ORDER_LIST) + var/list/entry = all_stats[stat_id] + if(!islist(entry)) + continue + + var/base = isnum(entry["base"]) ? entry["base"] : 10 + var/minimum = isnum(entry["min"]) ? entry["min"] : 1 + var/cost = isnum(entry["cost"]) ? entry["cost"] : 0 + var/value = isnum(stat_data[stat_id]) ? stat_data[stat_id] : base + + if(value > base) + stats_spent += (value - base) * cost + else + stats_spent += (max(value, minimum) - base) * cost + + var/skills_spent = 0 + var/list/skill_data = build_data["skills"] + var/list/invested_skills = null + + if(islist(skill_data)) + if(islist(skill_data["invested"])) + invested_skills = skill_data["invested"] + else + invested_skills = skill_data + + if(islist(invested_skills)) + for(var/skill_type in invested_skills) + if(skill_type == "bonus" || skill_type == "invested") + continue + + var/level = round(invested_skills[skill_type] || 0) + for(var/i in 1 to level) + skills_spent += i + + var/traits_spent = 0 + var/capped_negative_trait_credit = 0 + var/list/trait_data = build_data["traits"] + + if(islist(trait_data)) + var/list/all_traits = GLOB.tat_available_traits + var/has_outlander = (TRAIT_OUTLANDER in trait_data) || !!trait_data[TRAIT_OUTLANDER] + + for(var/key in trait_data) + var/trait_id = key + var/count = 1 + if(!islist(all_traits[trait_id])) + trait_id = trait_data[key] + count = 1 + else if(isnum(trait_data[key])) + count = max(0, round(trait_data[key])) + else if(!trait_data[key]) + count = 0 + + var/list/entry = all_traits[trait_id] + if(!islist(entry) || count <= 0) + continue + + var/cost = isnum(entry["cost"]) ? entry["cost"] : 0 + + if(trait_id == TAT_TRAIT_BONUS_STAT_POOL && has_outlander) + cost -= TAT_TRAIT_DISCOUNT + + var/total_cost = cost * count + if((trait_id in GLOB.tat_capped_negative_traits) && total_cost < 0) + capped_negative_trait_credit += -total_cost + else + traits_spent += total_cost + + traits_spent -= min(capped_negative_trait_credit, TAT_NEGATIVE_TRAIT_CREDIT_CAP) + + var/items_spent = 0 + var/list/item_data = build_data["items"] + var/list/selected_items = null + + if(islist(item_data)) + if(islist(item_data["selected"])) + selected_items = item_data["selected"] + else + selected_items = item_data + + if(islist(selected_items)) + var/list/all_items = GLOB.tat_available_items + for(var/item_path in selected_items) + if(item_path == "selected" || item_path == "item_loadout") + continue + + var/list/entry = all_items[item_path] + if(!islist(entry)) + continue + + var/cost = isnum(entry["cost"]) ? entry["cost"] : 0 + var/amount = round(selected_items[item_path] || 0) + + items_spent += cost * amount + + return list( + "stats" = stats_spent, + "skills" = skills_spent, + "traits" = traits_spent, + "items" = items_spent, + ) + +/datum/tat_build/proc/export_slot_build_to_list() + return list( + "stats" = stats.export_to_list(), + "items" = items.export_to_list(), + "traits" = traits.export_to_list(), + "skills" = skills.export_to_list(), + "magic_profile" = magic_profile.Copy(), + "magic_config" = magic_profile.Copy(), + ) + +/datum/tat_build/proc/export_to_list() + init_tat_slots() + return list( + "stats" = stats.export_to_list(), + "items" = items.export_to_list(), + "traits" = traits.export_to_list(), + "skills" = skills.export_to_list(), + "magic_profile" = magic_profile.Copy(), + "magic_config" = magic_profile.Copy(), + "tat_slots" = export_tat_slots_to_list(), + "active_tat_slot" = active_tat_slot, + ) + +/datum/tat_build/proc/load_slot_build_from_list(list/data) + reset() + if(!islist(data)) + return FALSE + traits.import_from_list(data["traits"]) + stats.import_from_list(data["stats"]) + skills.import_from_list(data["skills"]) + items.import_from_list(data["items"]) + if(islist(data["magic_profile"])) + var/list/temp = data["magic_profile"] + magic_profile = temp.Copy() + else if(islist(data["magic_config"])) + var/list/temp = data["magic_config"] + magic_profile = temp.Copy() + sanitize() + return TRUE + +/datum/tat_build/proc/load_from_list(list/data) + reset() + + if(!islist(data)) + load_tat_slots_from_list(null, 1) + return FALSE + + traits.import_from_list(data["traits"]) + stats.import_from_list(data["stats"]) + skills.import_from_list(data["skills"]) + items.import_from_list(data["items"]) + + if(islist(data["magic_profile"])) + var/list/temp = data["magic_profile"] + magic_profile = temp.Copy() + else if(islist(data["magic_config"])) + var/list/temp = data["magic_config"] + magic_profile = temp.Copy() + + var/list/_tat_slots = data["tat_slots"] + var/_active_tat_slot = data["active_tat_slot"] + + if(islist(_tat_slots) || !isnull(_active_tat_slot)) + load_tat_slots_from_list(_tat_slots, _active_tat_slot) + var/datum/tat_slot/active_slot = get_tat_slot(active_tat_slot) + var/list/active_data = active_slot?.get_build_data() + + if(islist(active_data) && length(active_data)) + load_slot_build_from_list(active_data) + else + load_tat_slots_from_list(null, 1) + + sanitize() + dirty = FALSE + invalidate_ui_data_cache() + + return TRUE + +/datum/tat_build/proc/apply_pre_client_to_human(mob/living/carbon/human/H) + attach_preferences_from_mob(H) + + if(!H) + return FALSE + + if(is_owner_tat_banned(H)) + tat_tell_banned(H) + return FALSE + + H.tat_handles_preference_loadout = TRUE + items?.sync_external_grants() + + sanitize() + + traits.apply_instant_to_human(H) + items.apply_to_human(H) + + return TRUE + +/datum/tat_build/proc/apply_post_client_to_human(mob/living/carbon/human/H) + attach_preferences_from_mob(H) + + if(!H || !H.client) + return FALSE + + if(is_owner_tat_banned(H)) + tat_tell_banned(H) + return FALSE + + sanitize() + + traits.apply_deferred_to_human(H) + stats.apply_to_human(H) + skills.apply_to_human(H) + + return TRUE + +/datum/tat_build/proc/apply_to_human(mob/living/carbon/human/H) + if(!apply_pre_client_to_human(H)) + return FALSE + if(!H.client) + return TRUE + return apply_post_client_to_human(H) + +/datum/tat_build/proc/disable_from_human(mob/living/carbon/human/H) + if(!H) + return FALSE + items.disable_from_human(H) + skills.disable_from_human(H) + traits.disable_from_human(H) + stats.disable_from_human(H) + return TRUE + +/datum/tat_build/proc/get_default_tat_slot_name(slot_id) + return "Slot [slot_id]" + +/datum/tat_build/proc/normalize_tat_slot_index(slot_id) + var/index = round(text2num("[slot_id]")) + if(index < 1) + index = 1 + if(index > TAT_SLOT_COUNT) + index = TAT_SLOT_COUNT + return index + +/datum/tat_build/proc/init_tat_slots() + if(!islist(tat_slots)) + tat_slots = list() + + while(tat_slots.len < TAT_SLOT_COUNT) + tat_slots += null + + for(var/i in 1 to TAT_SLOT_COUNT) + var/datum/tat_slot/slot = tat_slots[i] + if(!istype(slot, /datum/tat_slot)) + slot = new /datum/tat_slot(get_default_tat_slot_name(i)) + tat_slots[i] = slot + if(!istext(slot.name) || !length(slot.name)) + slot.name = get_default_tat_slot_name(i) + if(!islist(slot.build_data)) + slot.set_build_data(list()) + + active_tat_slot = normalize_tat_slot_index(active_tat_slot) + return TRUE + +/datum/tat_build/proc/get_tat_slot(slot_id) as /datum/tat_slot + init_tat_slots() + var/index = normalize_tat_slot_index(slot_id) + var/datum/tat_slot/slot = tat_slots[index] + if(!istype(slot, /datum/tat_slot)) + slot = new /datum/tat_slot(get_default_tat_slot_name(index)) + tat_slots[index] = slot + if(!istext(slot.name) || !length(slot.name)) + slot.name = get_default_tat_slot_name(index) + if(!islist(slot.build_data)) + slot.set_build_data(list()) + return slot + +/datum/tat_build/proc/save_current_to_slot(slot_id) + init_tat_slots() + var/datum/tat_slot/slot = get_tat_slot(slot_id) + if(!slot) + return FALSE + slot.set_build_data(export_slot_build_to_list(), src) + invalidate_ui_data_cache() + return TRUE + +/datum/tat_build/proc/save_current_to_active_slot() + if(!save_current_to_slot(active_tat_slot)) + return FALSE + dirty = FALSE + invalidate_ui_data_cache() + return TRUE + +/datum/tat_build/proc/load_slot_into_current(slot_id) + init_tat_slots() + var/datum/tat_slot/slot = get_tat_slot(slot_id) + if(!slot) + return FALSE + var/list/build_data = slot.get_build_data() + if(!islist(build_data) || !length(build_data)) + reset() + dirty = FALSE + invalidate_ui_data_cache() + return TRUE + load_slot_build_from_list(build_data) + dirty = FALSE + invalidate_ui_data_cache() + return TRUE + +/datum/tat_build/proc/set_active_tat_slot(slot_id) + init_tat_slots() + active_tat_slot = normalize_tat_slot_index(slot_id) + if(!load_slot_into_current(active_tat_slot)) + return FALSE + dirty = FALSE + invalidate_ui_data_cache() + return TRUE + +/datum/tat_build/proc/rename_tat_slot(slot_id, new_name) + init_tat_slots() + var/datum/tat_slot/slot = get_tat_slot(slot_id) + if(!slot || !istext(new_name)) + return FALSE + new_name = trim(new_name) + if(!length(new_name)) + return FALSE + new_name = copytext(new_name, 1, 50) + slot.name = new_name + invalidate_ui_data_cache() + return TRUE + +/datum/tat_build/proc/export_tat_slots_to_list() + init_tat_slots() + var/list/result = list() + for(var/i in 1 to TAT_SLOT_COUNT) + var/datum/tat_slot/slot = get_tat_slot(i) + result += list(slot.export_to_list()) + return result + +/datum/tat_build/proc/load_tat_slots_from_list(list/slots_data, active_slot = 1) + tat_slots = list() + for(var/i in 1 to TAT_SLOT_COUNT) + var/datum/tat_slot/slot = new /datum/tat_slot(get_default_tat_slot_name(i)) + var/list/raw_slot = null + if(islist(slots_data)) + if(i <= length(slots_data) && islist(slots_data[i])) + raw_slot = slots_data[i] + else + var/text_index = "[i]" + if(!isnull(slots_data[text_index]) && islist(slots_data[text_index])) + raw_slot = slots_data[text_index] + if(islist(raw_slot)) + slot.load_from_list(raw_slot, src) + if(!istext(slot.name) || !length(slot.name)) + slot.name = get_default_tat_slot_name(i) + if(!islist(slot.build_data)) + slot.set_build_data(list()) + tat_slots += slot + active_tat_slot = normalize_tat_slot_index(active_slot) + dirty = FALSE + invalidate_ui_data_cache() + return TRUE + +/datum/tat_build/proc/export_to_json() + invalidate_ui_data_cache() + last_json_error = null + last_json_notice = null + + var/list/data = list() + data["version"] = 1 + data["stats"] = stats?.export_to_json_list() + data["skills"] = skills?.export_to_json_list() + data["traits"] = traits?.export_to_json_list() + data["items"] = items?.export_to_json_list() + + last_exported_json = json_encode(data) + last_json_notice = "Build exported." + return last_exported_json + +/datum/tat_build/proc/import_from_json(raw) + invalidate_ui_data_cache() + last_json_error = null + last_json_notice = null + + if(!istext(raw) || !length(raw)) + last_json_error = "Empty JSON." + return FALSE + + var/list/data + try + data = json_decode(raw) + catch() + last_json_error = "Invalid JSON." + return FALSE + + if(!islist(data)) + last_json_error = "JSON root must be an object." + return FALSE + + var/raw_version = data["version"] + var/version = round(text2num("[raw_version]") || 1) + if(version != 1) + last_json_error = "Unsupported TAT build JSON version: [version]." + return FALSE + + reset() + traits.import_from_json_list(data["traits"]) + stats.import_from_json_list(data["stats"]) + skills.import_from_json_list(data["skills"]) + items.import_from_json_list(data["items"]) + + sanitize() + set_dirty(TRUE) + + last_exported_json = raw + last_json_notice = "Build imported." + return TRUE + +/datum/tat_build/proc/get_role_bucket() + if(traits?.has_trait(TAT_TRAIT_RESIDENT)) + return TAT_ROLE_BUCKET_TOWNER + + if(traits?.has_trait(TRAIT_OUTLANDER)) + return TAT_ROLE_BUCKET_ADVENTURER + + if(traits?.has_trait(TAT_TRAIT_WANTED)) + return TAT_ROLE_BUCKET_WRETCH + + return TAT_ROLE_BUCKET_TRADER diff --git a/modular_twilight_axis/code/datums/tat_system/core/tat_slot.dm b/modular_twilight_axis/code/datums/tat_system/core/tat_slot.dm new file mode 100644 index 00000000000..c7d4abaee8c --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/core/tat_slot.dm @@ -0,0 +1,55 @@ +/datum/tat_slot + var/name = "Slot" + var/list/build_data = list() + var/list/summary_cache = null + var/summary_dirty = TRUE + +/datum/tat_slot/New(slot_name = "Slot") + . = ..() + if(istext(slot_name) && length(slot_name)) + name = slot_name + if(!islist(build_data)) + build_data = list() + summary_dirty = TRUE + +/datum/tat_slot/proc/export_to_list() + return list( + "name" = name, + "build_data" = islist(build_data) ? build_data.Copy() : list(), + ) + +/datum/tat_slot/proc/load_from_list(list/L, datum/tat_build/owner_build = null) + if(!islist(L)) + name = "Slot" + build_data = list() + summary_cache = null + summary_dirty = TRUE + return FALSE + + name = istext(L["name"]) ? L["name"] : "Slot" + var/list/data = L["build_data"] + build_data = islist(data) ? data.Copy() : list() + refresh_summary(owner_build) + return TRUE + +/datum/tat_slot/proc/set_build_data(list/L, datum/tat_build/owner_build = null) + build_data = islist(L) ? L.Copy() : list() + refresh_summary(owner_build) + return TRUE + +/datum/tat_slot/proc/get_build_data() + return islist(build_data) ? build_data.Copy() : list() + +/datum/tat_slot/proc/refresh_summary(datum/tat_build/owner_build = null) + if(owner_build) + summary_cache = owner_build.build_slot_summary_from_data(build_data) + summary_dirty = FALSE + else + summary_cache = null + summary_dirty = TRUE + return summary_cache + +/datum/tat_slot/proc/get_summary(datum/tat_build/owner_build) + if(!summary_dirty && islist(summary_cache)) + return summary_cache + return refresh_summary(owner_build) diff --git a/modular_twilight_axis/code/datums/tat_system/core/tat_ui.dm b/modular_twilight_axis/code/datums/tat_system/core/tat_ui.dm new file mode 100644 index 00000000000..7a353d3bc0a --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/core/tat_ui.dm @@ -0,0 +1,789 @@ +/// UI-facing layer for TAT build (backend side). + +/datum/tat_build/proc/get_ui_skill_domain_key(domain) + if(domain == TAT_SKILL_DOMAIN_COMBAT) + return "combat" + if(domain == TAT_SKILL_DOMAIN_WANDERING) + return "wandering" + if(domain == TAT_SKILL_DOMAIN_GATHERING) + return "gathering" + if(domain == TAT_SKILL_DOMAIN_CRAFTING) + return "crafting" + if(domain == TAT_SKILL_DOMAIN_MISC) + return "misc" + return "misc" + +/datum/tat_build/proc/get_all_ui_skill_types() + var/list/result = list() + result += TAT_SKILLS_COMBAT + result += TAT_SKILLS_WANDERING + result += TAT_SKILLS_GATHERING + result += TAT_SKILLS_CRAFTING + result += TAT_SKILLS_MISC + return result + +/datum/tat_build/proc/get_stat_entry(stat_id) + return stats?.get_entry(stat_id) + +/datum/tat_build/proc/get_skill_entry(skill_type) + if(!ispath(skill_type, /datum/skill)) + return null + + if(GLOB.tat_skill_entry_cache_ready && ("[skill_type]" in GLOB.tat_skill_entry_cache)) + return GLOB.tat_skill_entry_cache["[skill_type]"] + + var/datum/skill/S = new skill_type + if(!S) + return null + + var/domain = skills?.get_domain(skill_type) + var/ui_domain = get_ui_skill_domain_key(domain) + + var/list/result = list( + "name" = S.name, + "desc" = S.desc, + "category" = ui_domain, + "is_combat" = !!ispath(skill_type, /datum/skill/combat), + ) + + qdel(S) + GLOB.tat_skill_entry_cache["[skill_type]"] = result + return result + +/datum/tat_build/proc/get_trait_entry(trait_id) + return traits?.get_entry(trait_id) + +/datum/tat_build/proc/get_item_entry(item_path) + return items?.get_entry(item_path) + +/datum/tat_build/proc/get_skill_cap(skill_type) + return skills?.get_maximum(skill_type) || 0 + +/datum/tat_build/proc/get_skill_next_cost(skill_type) + var/current_invested = get_invested_skill_value(skill_type) + return skills?.get_step_cost(skill_type, current_invested + 1) || 0 + +/datum/tat_build/proc/get_effective_stat_points_total() + return stats?.get_total_maximum() || 0 + +/datum/tat_build/proc/get_remaining_stat_points() + return stats?.get_remaining_points() || 0 + +/datum/tat_build/proc/get_effective_skill_points_total() + if(!skills) + return 0 + var/total = 0 + for(var/domain in skills.domain_points) + total += skills.get_total_maximum(domain) + return total + +/datum/tat_build/proc/get_remaining_skill_points() + if(!skills) + return 0 + var/total = 0 + for(var/domain in skills.domain_points) + total += skills.get_remaining_points(domain) + return total + +/datum/tat_build/proc/give_skill_domain_points(domain, amount = 1) + if(!skills) + return FALSE + var/ok = skills.give_skill_domain_points(domain, text2num("[amount]") || 1) + if(ok) + skills.sanitize(FALSE) + invalidate_ui_data_cache() + return ok + +/datum/tat_build/proc/take_skill_domain_points(domain, amount = 1) + if(!skills) + return FALSE + var/ok = skills.take_skill_domain_points(domain, text2num("[amount]") || 1) + if(ok) + skills.sanitize(FALSE) + invalidate_ui_data_cache() + return ok + +/datum/tat_build/proc/build_ui_skill_conversion_state() + if(!skills) + return list() + return skills.build_skill_conversion_state() + +/datum/tat_build/proc/get_remaining_trait_points() + return traits?.get_remaining_points() || 0 + +/datum/tat_build/proc/get_remaining_item_points() + return items?.get_remaining_points() || 0 + +/datum/tat_build/proc/build_ui_skill_points_by_domain() + var/list/result = list( + "combat" = 0, + "wandering" = 0, + "gathering" = 0, + "crafting" = 0, + "misc" = 0, + ) + + if(!skills) + return result + + result["combat"] = skills.get_total_maximum(TAT_SKILL_DOMAIN_COMBAT) + result["wandering"] = skills.get_total_maximum(TAT_SKILL_DOMAIN_WANDERING) + result["gathering"] = skills.get_total_maximum(TAT_SKILL_DOMAIN_GATHERING) + result["crafting"] = skills.get_total_maximum(TAT_SKILL_DOMAIN_CRAFTING) + result["misc"] = skills.get_total_maximum(TAT_SKILL_DOMAIN_MISC) + + return result + +/datum/tat_build/proc/build_ui_skill_points_remaining_by_domain() + var/list/result = list( + "combat" = 0, + "wandering" = 0, + "gathering" = 0, + "crafting" = 0, + "misc" = 0, + ) + + if(!skills) + return result + + result["combat"] = skills.get_remaining_points(TAT_SKILL_DOMAIN_COMBAT) + result["wandering"] = skills.get_remaining_points(TAT_SKILL_DOMAIN_WANDERING) + result["gathering"] = skills.get_remaining_points(TAT_SKILL_DOMAIN_GATHERING) + result["crafting"] = skills.get_remaining_points(TAT_SKILL_DOMAIN_CRAFTING) + result["misc"] = skills.get_remaining_points(TAT_SKILL_DOMAIN_MISC) + + return result + +/datum/tat_build/proc/can_save() + if(is_owner_tat_banned()) + return FALSE + return is_budget_valid() + +/datum/tat_build/proc/reset_build() + return reset() + +/datum/tat_build/proc/reset_stats() + stats?.reset() + sanitize() + set_dirty() + return TRUE + +/datum/tat_build/proc/reset_skills() + skills?.reset() + sanitize() + set_dirty() + return TRUE + +/datum/tat_build/proc/reset_traits() + traits?.reset() + sanitize() + set_dirty() + return TRUE + +/datum/tat_build/proc/reset_items() + items?.reset() + sanitize() + set_dirty() + return TRUE + +/datum/tat_build/proc/add_stat(id, amount = 1) + if(!stats) + return FALSE + var/current = stats.get_value(id) + var/ok = stats.set_value(id, current + (text2num("[amount]") || 1)) + if(ok) + stats?.sanitize() + return ok + +/datum/tat_build/proc/remove_stat(id, amount = 1) + if(!stats) + return FALSE + var/current = stats.get_value(id) + var/ok = stats.set_value(id, current - (text2num("[amount]") || 1), TRUE) + return ok + +/datum/tat_build/proc/add_skill(skill_type, amount = 1) + if(!skills) + return FALSE + var/current = skills.get_invested_value(skill_type) + var/ok = skills.set_invested_value(skill_type, current + (text2num("[amount]") || 1)) + if(ok) + skills?.sanitize() + return ok + +/datum/tat_build/proc/remove_skill(skill_type, amount = 1) + if(!skills) + return FALSE + var/current = skills.get_invested_value(skill_type) + var/ok = skills.set_invested_value(skill_type, current - (text2num("[amount]") || 1), TRUE) + return ok + +/datum/tat_build/proc/add_trait(trait_id) + var/ok = traits?.add_trait(trait_id) + if(ok) + traits?.sanitize() + stats?.sanitize() + skills?.refresh_after_trait_change() + items?.sanitize() + return ok + +/datum/tat_build/proc/remove_trait(trait_id, amount = 1) + if(!traits) + return FALSE + var/count = max(1, text2num("[amount]") || 1) + var/changed = FALSE + for(var/i in 1 to count) + if(!traits.has_trait(trait_id)) + break + if(traits.remove_trait(trait_id)) + changed = TRUE + else + break + if(changed) + traits?.sanitize() + stats?.sanitize() + skills?.refresh_after_trait_change() + items?.sanitize() + return changed + +/datum/tat_build/proc/add_item(path, amount = 1) + if(!items) + return FALSE + var/current = items.get_paid_amount(path) + var/ok = items.set_amount(path, current + (text2num("[amount]") || 1)) + if(ok) + items?.sanitize() + return ok + +/datum/tat_build/proc/remove_item(path, amount = 1) + if(!items) + return FALSE + var/current = items.get_paid_amount(path) + var/ok = items.set_amount(path, current - (text2num("[amount]") || 1), TRUE) + return ok + +/datum/tat_build/proc/move_item_to_bag(path, amount = 1) + if(!items) + return FALSE + var/ok = items.move_item_from_stash_to_bag(path, text2num("[amount]") || 1) + if(ok) + set_dirty() + return ok + +/datum/tat_build/proc/move_item_to_stash(path, amount = 1) + if(!items) + return FALSE + var/ok = items.move_item_from_bag_to_stash(path, text2num("[amount]") || 1) + if(ok) + set_dirty() + return ok + +/datum/tat_build/proc/paint_loadout_item(path, mob/user = null) + if(!items) + return FALSE + var/ok = items.paint_loadout_item(path, user || usr) + if(ok) + set_dirty() + return ok + +/datum/tat_build/proc/move_item_to_equip(path, amount = 1) + if(!items) + return FALSE + var/count = max(1, text2num("[amount]") || 1) + var/total = items.get_amount(path) + if(total <= 0) + return FALSE + var/changed = FALSE + for(var/i in 1 to count) + if(items.get_assigned_loadout_slot_count(path) >= total) + break + if(items.assign_item_to_first_available_loadout_slot(path)) + changed = TRUE + else + break + items.normalize_loadout(path) + if(changed) + set_dirty() + return changed + +/datum/tat_build/proc/assign_item_to_loadout_slot(path, slot_id) + if(!items) + return FALSE + var/ok = items.assign_item_to_loadout_slot(path, slot_id) + if(ok) + set_dirty() + return ok + +/datum/tat_build/proc/clear_loadout_slot(slot_id) + if(!items) + return FALSE + var/ok = items.clear_loadout_slot(slot_id) + if(ok) + set_dirty() + return ok + +/datum/tat_build/proc/build_ui_stats() + var/list/result = list() + for(var/stat_id in TAT_STATS_ORDER_LIST) + var/list/entry = get_stat_entry(stat_id) + if(!islist(entry)) + continue + result[stat_id] = get_stat_value(stat_id) + return result + +/datum/tat_build/proc/build_ui_stat_entries() + var/list/result = list() + for(var/stat_id in TAT_STATS_ORDER_LIST) + var/list/entry = get_stat_entry(stat_id) + if(islist(entry)) + result[stat_id] = entry + return result + +/datum/tat_build/proc/build_ui_skill_entries() + if(GLOB.tat_skill_entry_cache_ready) + return GLOB.tat_skill_entry_cache + + var/list/result = list() + for(var/skill_type in get_all_ui_skill_types()) + var/list/entry = get_skill_entry(skill_type) + if(!islist(entry)) + continue + result["[skill_type]"] = entry + + GLOB.tat_skill_entry_cache = result + GLOB.tat_skill_entry_cache_ready = TRUE + return result + +/datum/tat_build/proc/build_ui_skills() + if(islist(ui_skills_cache)) + return ui_skills_cache + + var/list/result = list() + if(!skills) + for(var/skill_type in get_all_ui_skill_types()) + result["[skill_type]"] = list("level" = 0, "cap" = 0, "next_cost" = 0, "bonus" = 0, "invested" = 0) + ui_skills_cache = result + return result + + for(var/skill_type in get_all_ui_skill_types()) + var/cap = skills.get_maximum(skill_type) + var/bonus_value = round(skills.bonus[skill_type] || 0) + var/invested_value = round(skills.invested[skill_type] || 0) + var/total_value = clamp(invested_value + bonus_value, 0, cap) + var/invested_cap = max(0, cap - bonus_value) + var/next_target = invested_value + 1 + var/next_cost = 0 + if(next_target > 0 && next_target <= invested_cap) + next_cost = max(1, next_target - get_skill_cost_discount(skill_type, next_target)) + + result["[skill_type]"] = list( + "level" = total_value, + "cap" = cap, + "next_cost" = next_cost, + "bonus" = bonus_value, + "invested" = invested_value, + ) + ui_skills_cache = result + return result + +/datum/tat_build/proc/build_ui_selected_traits() + var/list/result = list() + if(!traits) + return result + for(var/trait_id in traits.selected) + var/count = traits.get_trait_count(trait_id) + for(var/i in 1 to count) + result += trait_id + return result + +/datum/tat_build/proc/build_ui_trait_counts() + var/list/result = list() + if(!traits) + return result + for(var/trait_id in traits.selected) + var/count = traits.get_trait_count(trait_id) + if(count > 0) + result[trait_id] = count + return result + +/datum/tat_build/proc/build_ui_effective_traits() + var/list/result = list() + if(!traits) + return result + var/list/effective_traits = traits.get_effective_trait_counts() + for(var/trait_id in effective_traits) + var/count = round(effective_traits[trait_id] || 0) + for(var/i in 1 to count) + result += trait_id + return result + +/datum/tat_build/proc/build_ui_effective_trait_counts() + var/list/result = list() + if(!traits) + return result + var/list/effective_traits = traits.get_effective_trait_counts() + for(var/trait_id in effective_traits) + var/count = round(effective_traits[trait_id] || 0) + if(count > 0) + result[trait_id] = count + return result + +/datum/tat_build/proc/build_ui_external_trait_counts() + var/list/result = list() + if(!traits) + return result + var/list/external_traits = traits.get_external_traits() + for(var/trait_id in external_traits) + result[trait_id] = 1 + return result + +/datum/tat_build/proc/build_ui_trait_entries() + var/list/result = list() + for(var/trait_id in GLOB.tat_available_traits) + var/list/entry = get_trait_entry(trait_id) + if(islist(entry) && entry["category"] == TAT_CATEGORY_SKILL_CONVERSION) + continue + if(!islist(entry)) + continue + result[trait_id] = list( + "name" = entry["name"], + "cost" = get_trait_cost_display(trait_id), + "category" = entry["category"], + "category_name" = entry["category_name"], + "desc" = entry["desc"], + "repeatable" = traits?.is_repeatable_trait(trait_id), + "maximum" = traits?.get_trait_maximum(trait_id), + "external" = traits?.has_external_trait(trait_id), + ) + return result + +/datum/tat_build/proc/build_ui_items_static() + items?.sync_external_grants() + if(!GLOB.tat_item_icon_cache_ready) + warm_tat_item_catalog() + return GLOB.tat_item_catalog_cache + +/datum/tat_build/proc/build_ui_items_state() + if(islist(ui_items_state_cache)) + return ui_items_state_cache + + var/list/result = list() + if(!items) + ui_items_state_cache = result + return result + + items.sync_external_grants() + for(var/item_path in GLOB.tat_available_items) + var/list/entry = GLOB.tat_available_items[item_path] + if(!islist(entry)) + continue + + // The Items tab is the TAT purchase shop, not the full roundstart loadout. + // Donor/preference loadout copies are stored and shown in the Loadout stash, + // but they do not count as bought items and do not consume slot/category caps. + var/unlocked = items.can_use_item_entry(entry) + var/amount = items.get_paid_amount(item_path) + var/maximum = unlocked ? items.get_maximum(item_path) : 0 + + result["[item_path]"] = list( + "amount" = amount, + "unlocked" = unlocked, + "maximum" = maximum, + "can_add" = amount < maximum, + ) + ui_items_state_cache = result + return result + +/datum/tat_build/proc/build_ui_loadout() + if(islist(ui_loadout_cache)) + return ui_loadout_cache + + var/list/result = list() + if(!items) + ui_loadout_cache = result + return result + items.sync_external_grants() + for(var/item_path in items.get_all_item_paths()) + var/amount = items.get_amount(item_path) + if(amount <= 0) + continue + items.normalize_loadout(item_path) + var/list/loadout = items.get_loadout(item_path) + var/list/exported_slots = list() + var/list/slots = loadout["slots"] + if(islist(slots)) + for(var/slot_id in slots) + exported_slots[slot_id] = TRUE + var/list/icon_payload = items.build_loadout_item_icon_payload(item_path) + result["[item_path]"] = list( + "amount" = amount, + "equip" = round(loadout["equip"] || 0), + "bag" = round(loadout["bag"] || 0), + "stash" = round(loadout["stash"] || 0), + "slots" = exported_slots, + "valid_slots" = items.get_valid_loadout_ui_slots_for_item(item_path), + "sources" = items.get_source_counts_for_ui(item_path), + "paint" = items.get_paint_data_for_ui(item_path), + "icon" = icon_payload?["icon"], + "icon_state" = icon_payload?["icon_state"], + ) + ui_loadout_cache = result + return result + +/datum/tat_build/proc/build_ui_tat_slot(slot_id) + var/datum/tat_slot/slot = get_tat_slot(slot_id) + var/list/summary = slot.get_summary(src) + var/name = istext(slot.name) && length(slot.name) ? slot.name : get_default_tat_slot_name(slot_id) + return list( + "id" = slot_id, + "name" = name, + "active" = (active_tat_slot == slot_id), + "summary" = list( + "stats" = isnum(summary["stats"]) ? summary["stats"] : 0, + "skills" = isnum(summary["skills"]) ? summary["skills"] : 0, + "traits" = isnum(summary["traits"]) ? summary["traits"] : 0, + "items" = isnum(summary["items"]) ? summary["items"] : 0, + ), + ) + +/datum/tat_build/proc/build_ui_tat_slots() + if(islist(ui_tat_slots_cache)) + return ui_tat_slots_cache + + init_tat_slots() + var/list/result = list() + for(var/i in 1 to TAT_SLOT_COUNT) + result += list(build_ui_tat_slot(i)) + ui_tat_slots_cache = result + return result + +/datum/tat_build/ui_state(mob/user) + return GLOB.always_state + +/datum/tat_build/ui_interact(mob/user, datum/tgui/ui) + attach_preferences_from_mob(user) + if(is_owner_tat_banned(user)) + tat_tell_banned(user) + return + // Opening the window must sync external loadout grants immediately. + // Otherwise donor/preference loadout changes can stay hidden behind a valid cached UI payload + // until Save or another action forces sanitize/cache invalidation. + items?.sync_external_grants() + invalidate_ui_data_cache() + if(!islist(_cached_active_virtues)) + skills?.sanitize(FALSE) + invalidate_ui_data_cache() + if(ui) + ui.set_autoupdate(FALSE) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "TATBuild") + ui.set_autoupdate(FALSE) + ui.open() + +/datum/tat_build/ui_static_data(mob/user) + attach_preferences_from_mob(user) + if(is_owner_tat_banned(user)) + return list() + items?.sync_external_grants() + return list( + "available_stats" = build_ui_stat_entries(), + "available_skills" = build_ui_skill_entries(), + "available_traits" = build_ui_trait_entries(), + "available_items" = build_ui_items_static(), + ) + +/datum/tat_build/ui_data(mob/user) + attach_preferences_from_mob(user) + if(is_owner_tat_banned(user)) + return list( + "disabled" = TRUE, + "disabled_reason" = tat_get_ban_reason(user?.ckey) || TAT_BAN_DEFAULT_REASON, + "can_save" = FALSE, + ) + if(islist(_cached_ui_data) && !_ui_data_cache_dirty) + return _cached_ui_data + var/list/_skp_total = build_ui_skill_points_by_domain() + var/list/_skp_rem = build_ui_skill_points_remaining_by_domain() + var/list/_skp_conversion_state = build_ui_skill_conversion_state() + var/_skp_conversion_pool = skills?.skill_point_conversion_pool || 0 + var/_p_skills_total = 0 + var/_p_skills_rem = 0 + var/_skills_any_negative = FALSE + for(var/_d in _skp_total) + _p_skills_total += _skp_total[_d] + for(var/_d in _skp_rem) + _p_skills_rem += _skp_rem[_d] + if(_skp_rem[_d] < 0) + _skills_any_negative = TRUE + var/_p_stats_total = get_effective_stat_points_total() + var/_p_stats_rem = get_remaining_stat_points() + var/_p_traits_total = traits.get_total_maximum() + var/_p_traits_rem = get_remaining_trait_points() + var/_p_traits_capped_negative_raw = traits.get_capped_negative_credit_raw() + var/_p_traits_capped_negative_used = traits.get_capped_negative_credit_used() + items?.sync_external_grants() + var/_p_items_total = items.get_total_maximum() + var/_p_items_rem = get_remaining_item_points() + + var/list/validation = list() + if(_p_stats_rem < 0) + validation += "Spent too many stat points." + if(_skills_any_negative) + validation += "Spent too many skill points." + if(_p_traits_rem < 0) + validation += "Spent too many trait points." + if(_p_items_rem < 0) + validation += "Spent too many item points." + var/list/trait_issues = traits.has_invalid_trait_dependencies() + if(length(trait_issues)) + validation += trait_issues + var/list/item_issues = items.has_invalid_supply_items() + if(length(item_issues)) + validation += item_issues + + if(is_owner_tat_role_locked(user)) + validation += get_owner_tat_role_lock_message(user) + + var/can_save_build = !length(validation) + var/list/_stats = build_ui_stats() + var/list/_skills = build_ui_skills() + var/list/_sel_traits = build_ui_selected_traits() + var/list/_trait_counts = build_ui_trait_counts() + var/list/_effective_traits = build_ui_effective_traits() + var/list/_effective_trait_counts = build_ui_effective_trait_counts() + var/list/_external_trait_counts = build_ui_external_trait_counts() + var/list/_items_state = build_ui_items_state() + var/list/_loadout = build_ui_loadout() + var/list/_tat_slots = build_ui_tat_slots() + + _cached_ui_data = list( + "stats" = _stats, + "skills" = _skills, + "traits" = _sel_traits, + "trait_counts" = _trait_counts, + "effective_traits" = _effective_traits, + "effective_trait_counts" = _effective_trait_counts, + "external_trait_counts" = _external_trait_counts, + "available_traits" = build_ui_trait_entries(), + "items_state" = _items_state, + "loadout" = _loadout, + + "points_stats" = _p_stats_total, + "points_stats_remaining" = _p_stats_rem, + + "points_skills" = _p_skills_total, + "points_skills_remaining" = _p_skills_rem, + "skill_points_by_domain" = _skp_total, + "skill_points_remaining_by_domain" = _skp_rem, + "skill_conversion_pool" = _skp_conversion_pool, + "skill_conversion_state" = _skp_conversion_state, + + "points_traits" = _p_traits_total, + "points_traits_remaining" = _p_traits_rem, + "negative_trait_credit_raw" = _p_traits_capped_negative_raw, + "negative_trait_credit_used" = _p_traits_capped_negative_used, + "negative_trait_credit_cap" = TAT_NEGATIVE_TRAIT_CREDIT_CAP, + + "points_items" = _p_items_total, + "points_items_remaining" = _p_items_rem, + + "tat_slots" = _tat_slots, + "active_tat_slot" = active_tat_slot, + "can_save" = can_save_build, + "validation_issues" = validation, + "build_json" = last_exported_json, + "last_json_error" = last_json_error, + "last_json_notice" = last_json_notice, + "dirty" = dirty, + ) + _ui_data_cache_dirty = FALSE + return _cached_ui_data + +/datum/tat_build/ui_act(action, list/params) + if(usr) + attach_preferences_from_mob(usr) + if(is_owner_tat_banned(usr)) + tat_tell_banned(usr) + return FALSE + else if(is_owner_tat_banned()) + return FALSE + . = ..() + if(.) + return + switch(action) + if("add_stat") + return add_stat(params["id"], text2num(params["amount"]) || 1) + if("remove_stat") + return remove_stat(params["id"], text2num(params["amount"]) || 1) + if("add_skill") + return add_skill(text2path(params["path"]), text2num(params["amount"]) || 1) + if("remove_skill") + return remove_skill(text2path(params["path"]), text2num(params["amount"]) || 1) + if("give_skill_domain_points") + return give_skill_domain_points(params["domain"], text2num(params["amount"]) || 1) + if("take_skill_domain_points") + return take_skill_domain_points(params["domain"], text2num(params["amount"]) || 1) + if("add_trait") + return add_trait(params["id"]) + if("remove_trait") + return remove_trait(params["id"], text2num(params["amount"]) || 1) + if("add_item") + return add_item(text2path(params["path"]), text2num(params["amount"]) || 1) + if("remove_item") + return remove_item(text2path(params["path"]), text2num(params["amount"]) || 1) + if("move_item_to_equip") + return move_item_to_equip(text2path(params["path"]), text2num(params["amount"]) || 1) + if("move_item_to_bag") + return move_item_to_bag(text2path(params["path"]), text2num(params["amount"]) || 1) + if("move_item_to_stash") + return move_item_to_stash(text2path(params["path"]), text2num(params["amount"]) || 1) + if("paint_loadout_item") + return paint_loadout_item(text2path(params["path"]), usr) + if("assign_item_to_loadout_slot") + return assign_item_to_loadout_slot(text2path(params["path"]), params["slot_id"]) + if("clear_loadout_slot") + return clear_loadout_slot(params["slot_id"]) + if("activate_tat_slot") + return set_active_tat_slot(text2num(params["slot_id"])) + if("rename_tat_slot") + return rename_tat_slot(text2num(params["slot_id"]), params["name"]) + if("reset_all") + return reset_build() + if("reset_stats") + return reset_stats() + if("reset_skills") + return reset_skills() + if("reset_traits") + return reset_traits() + if("reset_items") + return reset_items() + if("save") + if(is_owner_tat_role_locked(usr)) + to_chat(usr, span_warning(get_owner_tat_role_lock_message(usr))) + return FALSE + if(!can_save()) + return FALSE + return save_current_to_active_slot() + if("export_json") + export_to_json() + return TRUE + if("import_json") + return import_from_json(params["json"]) + + return FALSE + +/proc/tat_item_entry_is_slot_limited(list/entry) + if(!islist(entry)) + return FALSE + + if(entry["category"] != TAT_ITEM_CATEGORY_CLOTHING) + return FALSE + + var/slot_group = entry["slot_group"] + if(!slot_group) + return FALSE + if(slot_group == "misc") + return FALSE + + return TRUE diff --git a/modular_twilight_axis/code/datums/tat_system/domains/tat_items.dm b/modular_twilight_axis/code/datums/tat_system/domains/tat_items.dm new file mode 100644 index 00000000000..f9788f715d7 --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/domains/tat_items.dm @@ -0,0 +1,1978 @@ +/datum/tat_items +/datum/tat_items + var/datum/tat_build/owner_build + var/list/selected = list() + var/list/item_loadout = list() + var/list/item_grants = list() + var/list/item_paint = list() + var/base_points = 20 + var/list/equip_slots_cache = list() + +/datum/tat_items/New(datum/tat_build/B) + . = ..() + owner_build = B + +/datum/tat_items/proc/reset() + selected = list() + item_loadout = list() + item_grants = list() + item_paint = list() + return TRUE + +/datum/tat_items/proc/get_entry(item_path) + return GLOB.tat_available_items[item_path] + +/datum/tat_items/proc/get_paid_amount(item_path) + return round(selected[item_path] || 0) + +/datum/tat_items/proc/get_granted_amount(item_path, source = null) + var/list/sources = item_grants[item_path] + if(!islist(sources)) + return 0 + if(!isnull(source)) + return round(sources[source] || 0) + var/total = 0 + for(var/source_key in sources) + total += round(sources[source_key] || 0) + return total + +/datum/tat_items/proc/get_amount(item_path) + return get_paid_amount(item_path) + get_granted_amount(item_path) + +/datum/tat_items/proc/get_non_donor_amount(item_path) + return get_paid_amount(item_path) + get_granted_amount(item_path, TAT_ITEM_SOURCE_TRAIT) + +/datum/tat_items/proc/get_external_granted_amount(item_path) + return get_granted_amount(item_path, TAT_ITEM_SOURCE_TRAIT) + get_granted_amount(item_path, TAT_ITEM_SOURCE_DONOR_LOADOUT) + +/datum/tat_items/proc/get_purchase_limit_amount(item_path) + // Donor/preference loadout grants are external freebies. They must be visible in + // the loadout stash, but must never count as TAT-purchased items and must not + // consume the category/slot caps used by the Items purchase tab. Trait grants + // still count here because they are part of the TAT build itself. + return get_non_donor_amount(item_path) + +/datum/tat_items/proc/is_loadout_only_entry(list/entry) + return islist(entry) && !!entry["loadout_only"] + +/datum/tat_items/proc/get_all_item_paths() + var/list/result = list() + for(var/item_path in selected) + if(!(item_path in result)) + result += item_path + for(var/item_path in item_grants) + if(get_granted_amount(item_path) <= 0) + continue + if(!(item_path in result)) + result += item_path + return result + +/datum/tat_items/proc/get_source_counts_for_ui(item_path) + var/list/result = list() + var/paid = get_paid_amount(item_path) + if(paid > 0) + result[TAT_ITEM_SOURCE_PAID] = paid + var/list/sources = item_grants[item_path] + if(islist(sources)) + for(var/source_key in sources) + var/count = round(sources[source_key] || 0) + if(count > 0) + result[source_key] = count + return result + +/datum/tat_items/proc/get_cost(item_path) + var/list/entry = get_entry(item_path) + if(!islist(entry)) + return 0 + + var/cost = entry["cost"] + if(!isnum(cost)) + return 0 + + return cost + +/datum/tat_items/proc/get_total_maximum() + return base_points + (owner_build ? owner_build.get_bonus_item_points() : 0) + +/datum/tat_items/proc/can_use_weapon_supply_type(supply_type) + switch(supply_type) + if(TAT_SUPPLY_IRON) + return TRUE + if(TAT_SUPPLY_BRONZE) + return !!owner_build?.has_trait(TAT_TRAIT_BRONZE_SUPPLIER) + if(TAT_SUPPLY_SILVER) + return !!owner_build?.has_trait(TAT_TRAIT_SILVER_SUPPLIER) + if(TAT_SUPPLY_STEEL) + return !!owner_build?.has_trait(TAT_TRAIT_STEEL_SUPPLIER) + if(TAT_SUPPLY_FIREARMS) + return !!owner_build?.has_trait(TAT_TRAIT_FIREARMS_SUPPLIER) + if(TAT_SUPPLY_ARTIFACTS) + return !!owner_build?.has_trait(TAT_TRAIT_ARTIFACTS_SUPPLIER) + return FALSE + +/datum/tat_items/proc/can_use_armor_family(armor_family) + switch(armor_family) + if(TAT_ARMOR_CLOTH) + return TRUE + if(TAT_ARMOR_LEATHER) + return !!owner_build?.has_trait(TAT_TRAIT_LEATHER_SUPPLIER) + if(TAT_ARMOR_MAIL) + return !!owner_build?.has_trait(TAT_TRAIT_MAIL_SUPPLIER) + if(TAT_ARMOR_PLATE) + return !!owner_build?.has_trait(TAT_TRAIT_PLATE_SUPPLIER) + return FALSE + +/proc/tat_ckey_in_ckey_list(key, list/ckey_list) + key = ckey(key) + if(!key || !islist(ckey_list)) + return FALSE + if(key in ckey_list) + return TRUE + for(var/list_key in ckey_list) + if(ckey(list_key) == key) + return TRUE + return FALSE + +/proc/tat_can_ckey_use_donation_item(key, required_tier, list/entry = null) + required_tier = round(required_tier || 0) + if(required_tier <= 0) + return TRUE + + key = ckey(key) + if(!key) + return FALSE + if(tat_ckey_in_ckey_list(key, GLOB.tat_donation_access_all_ckeys)) + return TRUE + if(islist(entry) && tat_ckey_in_ckey_list(key, entry["donat_ignore"])) + return TRUE + + return round(check_patreon_lvl(key) || 0) >= required_tier + +/datum/tat_items/proc/can_use_item_entry(list/entry) + if(!islist(entry)) + return FALSE + if(is_loadout_only_entry(entry)) + return FALSE + var/donat_tier = round(entry["donat_tier"] || 0) + if(donat_tier > 0 && !tat_can_ckey_use_donation_item(owner_build?.get_owner_ckey(), donat_tier, entry)) + return FALSE + var/unlock_type = entry["unlock_type"] + var/unlock_key = entry["unlock_key"] + switch(unlock_type) + if(TAT_UNLOCK_TYPE_WEAPON_SUPPLY) + return can_use_weapon_supply_type(unlock_key) + if(TAT_UNLOCK_TYPE_ARMOR_FAMILY) + return can_use_armor_family(unlock_key) + if(TAT_UNLOCK_TYPE_TRAIT) + return !!owner_build?.has_trait(unlock_key) + return TRUE + +/datum/tat_items/proc/check_item(item_path) + var/list/entry = get_entry(item_path) + if(!islist(entry)) + return FALSE + if(!can_use_item_entry(entry)) + return FALSE + return TRUE + +/datum/tat_items/proc/is_item_slot_limited(list/entry) + return tat_item_entry_is_slot_limited(entry) + +/datum/tat_items/proc/get_slot_group_item_count(slot_group, category, exclude_item_path = null) + if(!slot_group) + return 0 + var/total = 0 + for(var/item_path in get_all_item_paths()) + if(!isnull(exclude_item_path) && item_path == exclude_item_path) + continue + var/list/entry = GLOB.tat_available_items[item_path] + if(!islist(entry)) + continue + if(entry["slot_group"] != slot_group) + continue + if(entry["category"] != category) + continue + var/amount = get_purchase_limit_amount(item_path) + if(amount <= 0) + continue + total += amount + return total + +/datum/tat_items/proc/get_item_total_allowed_amount(path) + var/list/entry = get_entry(path) + if(!islist(entry) || is_loadout_only_entry(entry)) + return 0 + var/cost = entry["cost"] + if(!isnum(cost)) + cost = 0 + var/category = entry["category"] + if(cost <= 0 && (category == "misc" || category == "weapon")) + return 1 + if(!tat_item_entry_is_slot_limited(entry)) + return INFINITY + if(!entry["slot_group"]) + return INFINITY + return 1 + +/datum/tat_items/proc/get_maximum(item_path) + var/list/entry = get_entry(item_path) + if(!islist(entry)) + return 0 + if(!can_use_item_entry(entry)) + return 0 + var/trait_granted = get_granted_amount(item_path, TAT_ITEM_SOURCE_TRAIT) + var/cost = entry["cost"] + if(!isnum(cost)) + cost = 0 + var/category = entry["category"] + if(cost <= 0 && (category == "misc" || category == "weapon")) + return max(0, 1 - trait_granted) + if(!tat_item_entry_is_slot_limited(entry)) + return max(0, 99 - trait_granted) + var/slot_group = entry["slot_group"] + if(!slot_group) + return max(0, 99 - trait_granted) + var/already_taken_elsewhere = get_slot_group_item_count(slot_group, category, item_path) + return max(0, 1 - already_taken_elsewhere - trait_granted) + +/datum/tat_items/proc/set_amount(item_path, amount, ignore_limits = FALSE) + if(!islist(get_entry(item_path))) + return FALSE + var/old_total = get_amount(item_path) + amount = round(amount) + if(ignore_limits) + amount = max(0, amount) + else + amount = clamp(amount, 0, get_maximum(item_path)) + if(amount <= 0) + selected -= item_path + else + selected[item_path] = amount + var/new_total = get_amount(item_path) + if(new_total <= 0) + item_loadout -= item_path + item_paint -= item_path + else + var/list/loadout = get_loadout(item_path) + if(new_total > old_total) + // Newly acquired TAT item copies start in stash. The player explicitly moves + // them to backpack before equipping or spawning them into the round. + loadout["stash"] = round(loadout["stash"] || 0) + (new_total - old_total) + normalize_loadout(item_path) + owner_build?.set_dirty() + return TRUE + +/datum/tat_items/proc/get_loadout(item_path) + if(!(item_path in item_loadout) || !islist(item_loadout[item_path])) + // Unknown/fresh loadout state must not silently dump items into backpack. + // Stash is the safe default; backpack is opt-in via move_item_from_stash_to_bag(). + item_loadout[item_path] = list("equip" = 0, "bag" = 0, "stash" = get_amount(item_path), "slots" = list()) + if(isnull(item_loadout[item_path]["bag"])) + item_loadout[item_path]["bag"] = 0 + if(isnull(item_loadout[item_path]["stash"])) + item_loadout[item_path]["stash"] = 0 + return item_loadout[item_path] + +/datum/tat_items/proc/normalize_loadout(item_path) + var/amount = get_amount(item_path) + if(amount <= 0) + item_loadout -= item_path + item_paint -= item_path + return + var/list/loadout = get_loadout(item_path) + var/list/slots = loadout["slots"] + if(!islist(slots)) + slots = list() + loadout["slots"] = slots + + var/list/valid_slots = get_valid_loadout_ui_slots_for_item(item_path) + for(var/slot_id in slots.Copy()) + if(!(slot_id in valid_slots)) + slots -= slot_id + + while(length(slots) > amount) + var/drop_slot = slots[length(slots)] + slots -= drop_slot + + var/equip = length(slots) + var/non_slot_amount = max(0, amount - equip) + var/bag = max(0, round(loadout["bag"] || 0)) + var/stash = max(0, round(loadout["stash"] || 0)) + + while((bag + stash) > non_slot_amount) + if(stash > 0) + stash-- + else if(bag > 0) + bag-- + else + break + + if((bag + stash) < non_slot_amount) + // Any missing loose copies are restored into stash, never backpack. + stash += non_slot_amount - (bag + stash) + + loadout["equip"] = equip + loadout["bag"] = bag + loadout["stash"] = stash + +/datum/tat_items/proc/set_item_grant_amount(item_path, source, amount, default_to_stash = TRUE, preserve_loadout_on_zero = FALSE) + if(!ispath(item_path) || !istext(source) || !length(source)) + return FALSE + ensure_runtime_item_entry(item_path, null, TRUE) + amount = max(0, round(amount || 0)) + var/list/sources = item_grants[item_path] + if(!islist(sources)) + sources = list() + item_grants[item_path] = sources + var/old_source_amount = round(sources[source] || 0) + if(amount <= 0) + sources -= source + else + sources[source] = amount + if(!length(sources)) + item_grants -= item_path + var/new_total = get_amount(item_path) + if(new_total <= 0) + if(!preserve_loadout_on_zero) + item_loadout -= item_path + item_paint -= item_path + else + var/list/loadout = get_loadout(item_path) + var/source_delta = amount - old_source_amount + if(source_delta > 0) + // Trait and donor-loadout grants are born in stash. They never appear in + // backpack unless the player explicitly moves them there from the loadout UI. + if(default_to_stash) + loadout["stash"] = round(loadout["stash"] || 0) + source_delta + else + loadout["bag"] = round(loadout["bag"] || 0) + source_delta + normalize_loadout(item_path) + return TRUE + +/datum/tat_items/proc/add_grant_amount(list/result, item_path, amount = 1) + if(!ispath(item_path) || amount <= 0) + return + result[item_path] = round(result[item_path] || 0) + round(amount) + +/datum/tat_items/proc/build_trait_granted_item_amounts() + var/list/result = list() + if(!owner_build?.traits) + return result + if(owner_build.has_trait(TRAIT_RITUALIST)) + add_grant_amount(result, /obj/item/ritechalk) + if(owner_build.has_trait(TAT_TRAIT_MAGE_INITIATE)) + add_grant_amount(result, /obj/item/book/spellbook) + add_grant_amount(result, /obj/item/chalk) + return result + +/datum/tat_items/proc/sync_trait_granted_items() + var/list/wanted = build_trait_granted_item_amounts() + var/list/current_trait_grants = list() + for(var/item_path in item_grants) + if(get_granted_amount(item_path, TAT_ITEM_SOURCE_TRAIT) > 0) + current_trait_grants += item_path + for(var/item_path in wanted) + set_item_grant_amount(item_path, TAT_ITEM_SOURCE_TRAIT, wanted[item_path], TRUE) + for(var/item_path in current_trait_grants) + if(!(item_path in wanted)) + set_item_grant_amount(item_path, TAT_ITEM_SOURCE_TRAIT, 0, TRUE) + return TRUE + +/datum/tat_items/proc/infer_runtime_item_category(item_path) + if(ispath(item_path, /obj/item/clothing) || ispath(item_path, /obj/item/storage/belt)) + return TAT_ITEM_CATEGORY_CLOTHING + if(ispath(item_path, /obj/item/rogueweapon) || ispath(item_path, /obj/item/gun) || ispath(item_path, /obj/item/ammo_casing) || ispath(item_path, /obj/item/quiver)) + return TAT_ITEM_CATEGORY_WEAPON + return "misc" + +/datum/tat_items/proc/infer_runtime_item_slot_group(item_path) + if(!ispath(item_path, /obj/item)) + return "misc" + + var/obj/item/I = item_path + var/flags = initial(I.slot_flags) + + if(flags & ITEM_SLOT_BELT) + return "belt" + if(flags & ITEM_SLOT_NECK) + return "neck" + if(flags & ITEM_SLOT_MASK) + return "mask" + if(flags & ITEM_SLOT_HEAD) + return "head" + if(flags & ITEM_SLOT_CLOAK) + return "cloak" + if(flags & ITEM_SLOT_ARMOR || flags & ITEM_SLOT_OCLOTHING) + return "armor" + if(flags & ITEM_SLOT_SHIRT || flags & ITEM_SLOT_ICLOTHING) + return "shirt" + if(flags & ITEM_SLOT_PANTS) + return "pants" + if(flags & ITEM_SLOT_WRISTS) + return "wrists" + if(flags & ITEM_SLOT_GLOVES) + return "gloves" + if(flags & ITEM_SLOT_SHOES) + return "shoes" + if(flags & ITEM_SLOT_RING) + return "ring" + + if(ispath(item_path, /obj/item/storage/belt)) + return "belt" + + return "misc" + +/datum/tat_items/proc/get_runtime_item_name(item_path) + if(!ispath(item_path, /obj/item)) + return "Unknown item" + var/obj/item/I = item_path + return initial(I.name) || "Unknown item" + +/datum/tat_items/proc/ensure_runtime_item_entry(item_path, override_name = null, loadout_only = FALSE) + if(!ispath(item_path, /obj/item)) + return FALSE + + var/inferred_category = infer_runtime_item_category(item_path) + var/inferred_slot_group = infer_runtime_item_slot_group(item_path) + var/list/existing_entry = GLOB.tat_available_items[item_path] + if(islist(existing_entry)) + if(is_loadout_only_entry(existing_entry)) + var/changed = FALSE + if((!existing_entry["slot_group"] || existing_entry["slot_group"] == "misc") && inferred_slot_group != "misc") + existing_entry["slot_group"] = inferred_slot_group + changed = TRUE + if((!existing_entry["category"] || existing_entry["category"] == "misc") && inferred_category != "misc") + existing_entry["category"] = inferred_category + changed = TRUE + if(istext(override_name) && length(override_name) && existing_entry["name"] != override_name) + existing_entry["name"] = override_name + changed = TRUE + if(changed) + GLOB.tat_item_loadout_slots_cache -= item_path + equip_slots_cache -= item_path + GLOB.tat_item_icon_cache_ready = FALSE + return TRUE + + GLOB.tat_available_items[item_path] = list( + "name" = istext(override_name) && length(override_name) ? override_name : get_runtime_item_name(item_path), + "cost" = 0, + "category" = inferred_category, + "unlock_type" = null, + "unlock_key" = null, + "slot_group" = inferred_slot_group, + "loadout_only" = !!loadout_only, + ) + GLOB.tat_item_icon_cache_ready = FALSE + return TRUE + +/datum/tat_items/proc/ensure_external_grants_start_in_stash() + for(var/item_path in get_all_item_paths()) + var/external_amount = get_external_granted_amount(item_path) + if(external_amount <= 0) + continue + var/list/loadout = get_loadout(item_path) + var/already_initialized = round(loadout["external_stash_initialized"] || 0) + if(already_initialized >= external_amount) + continue + + var/missing_external = external_amount - already_initialized + var/bag = max(0, round(loadout["bag"] || 0)) + var/stash = max(0, round(loadout["stash"] || 0)) + var/move_from_bag = min(missing_external, bag) + if(move_from_bag > 0) + loadout["bag"] = bag - move_from_bag + loadout["stash"] = stash + move_from_bag + + // Mark only after the first automatic stash placement. From this point on, + // player-made bag/stash choices are preserved across normal UI syncs. + loadout["external_stash_initialized"] = external_amount + normalize_loadout(item_path) + return TRUE + +/datum/tat_items/proc/sync_external_grants() + sync_trait_granted_items() + ensure_external_grants_start_in_stash() + for(var/item_path in get_all_item_paths()) + normalize_loadout(item_path) + return TRUE + +/datum/tat_items/proc/get_spent_points() + var/total = 0 + for(var/item_path in selected) + total += get_cost(item_path) * get_paid_amount(item_path) + return total + +/datum/tat_items/proc/get_remaining_points() + return get_total_maximum() - get_spent_points() + +/datum/tat_items/proc/has_invalid_supply_items() + var/list/issues = list() + for(var/item_path in selected) + var/amount = selected[item_path] + if(!isnum(amount) || amount <= 0) + continue + var/list/entry = get_entry(item_path) + if(!islist(entry)) + issues += "\"[item_path]\" is missing from item definitions." + continue + if(!can_use_item_entry(entry)) + issues += "\"[entry["name"]]\" is no longer unlocked by current traits." + return issues + +/datum/tat_items/proc/sanitize() + sync_external_grants() + for(var/item_path in selected.Copy()) + if(!check_item(item_path)) + selected -= item_path + if(get_amount(item_path) <= 0) + item_loadout -= item_path + continue + set_amount(item_path, get_paid_amount(item_path)) + while(get_remaining_points() < 0) + var/changed = FALSE + for(var/item_path in selected.Copy()) + var/amount = get_paid_amount(item_path) + if(amount > 0) + set_amount(item_path, amount - 1) + changed = TRUE + if(get_remaining_points() >= 0) + break + if(!changed) + break + for(var/item_path in get_all_item_paths()) + normalize_loadout(item_path) + return TRUE + +/datum/tat_items/proc/append_unique_text(list/values, value) + if(!istext(value) || !length(value)) + return + if(!(value in values)) + values += value + +/datum/tat_items/proc/append_music_loadout_ui_slots(list/slots) + append_unique_text(slots, "shoulder_l") + append_unique_text(slots, "shoulder_r") + append_unique_text(slots, "belt") + append_unique_text(slots, "belt_l") + append_unique_text(slots, "belt_r") + append_unique_text(slots, "hand_l") + append_unique_text(slots, "hand_r") + +/datum/tat_items/proc/append_music_equip_slots(list/slots) + append_unique_equip_slot(slots, SLOT_BACK_L) + append_unique_equip_slot(slots, SLOT_BACK_R) + append_unique_equip_slot(slots, SLOT_BACK) + append_unique_equip_slot(slots, SLOT_BELT) + append_unique_equip_slot(slots, SLOT_BELT_L) + append_unique_equip_slot(slots, SLOT_BELT_R) + append_unique_equip_slot(slots, SLOT_HANDS) + +/datum/tat_items/proc/is_weapon_loadout_group(slot_group) + var/group = lowertext("[slot_group]") + return group in list("blackpowder", "ranged", "munition", "knife", "sword", "greatsword", "axe", "blunt", "polearm", "whip", "sheath", "artifact", "unarmed") + +/datum/tat_items/proc/is_light_loadout_group(slot_group) + var/group = lowertext("[slot_group]") + return group in list("adventur' supply", "adventur supply", "adventure supply", "light", "lamp", "lantern", "torch") + +/datum/tat_items/proc/is_light_loadout_item(item_path, list/entry = null) + if(ispath(item_path, /obj/item/flashlight/flare/torch)) + return TRUE + if(ispath(item_path, /obj/item/flashlight)) + return TRUE + if(islist(entry) && is_light_loadout_group(entry["slot_group"])) + return TRUE + return FALSE + +/datum/tat_items/proc/append_light_loadout_ui_slots(list/slots) + append_unique_text(slots, "belt") + append_unique_text(slots, "belt_l") + append_unique_text(slots, "belt_r") + append_unique_text(slots, "hand_l") + append_unique_text(slots, "hand_r") + +/datum/tat_items/proc/append_light_equip_slots(list/slots) + append_unique_equip_slot(slots, SLOT_BELT) + append_unique_equip_slot(slots, SLOT_BELT_L) + append_unique_equip_slot(slots, SLOT_BELT_R) + append_unique_equip_slot(slots, SLOT_HANDS) + +/datum/tat_items/proc/is_amulet_loadout_group(slot_group) + var/group = lowertext("[slot_group]") + return group in list("cross", "amulet", "amulets", "talisman", "talismans", "charm", "charms", "necklace", "necklaces") + +/datum/tat_items/proc/is_amulet_loadout_item(item_path, list/entry = null) + if(islist(entry) && is_amulet_loadout_group(entry["slot_group"])) + return TRUE + var/path_text = lowertext("[item_path]") + if(findtext(path_text, "amulet") || findtext(path_text, "talisman") || findtext(path_text, "charm") || findtext(path_text, "necklace") || findtext(path_text, "psicross") || findtext(path_text, "cross")) + return TRUE + if(islist(entry)) + var/name_text = lowertext("[entry["name"]]") + if(findtext(name_text, "amulet") || findtext(name_text, "talisman") || findtext(name_text, "charm") || findtext(name_text, "necklace") || findtext(name_text, "cross")) + return TRUE + return FALSE + +/datum/tat_items/proc/append_amulet_loadout_ui_slots(list/slots) + append_unique_text(slots, "neck") + append_unique_text(slots, "ring") + +/datum/tat_items/proc/append_amulet_equip_slots(list/slots) + append_unique_equip_slot(slots, SLOT_NECK) + append_unique_equip_slot(slots, SLOT_RING) + +/datum/tat_items/proc/append_weapon_loadout_ui_slots(list/slots, slot_group = null) + var/group = lowertext("[slot_group]") + if(group in list("greatsword", "polearm")) + append_unique_text(slots, "shoulder_l") + append_unique_text(slots, "shoulder_r") + append_unique_text(slots, "hand_l") + append_unique_text(slots, "hand_r") + return + if(group == "sheath") + append_unique_text(slots, "belt") + append_unique_text(slots, "belt_l") + append_unique_text(slots, "belt_r") + append_unique_text(slots, "shoulder_l") + append_unique_text(slots, "shoulder_r") + return + append_unique_text(slots, "belt") + append_unique_text(slots, "belt_l") + append_unique_text(slots, "belt_r") + append_unique_text(slots, "shoulder_l") + append_unique_text(slots, "shoulder_r") + append_unique_text(slots, "hand_l") + append_unique_text(slots, "hand_r") + +/datum/tat_items/proc/append_weapon_equip_slots(list/slots, slot_group = null) + var/group = lowertext("[slot_group]") + if(group in list("greatsword", "polearm")) + append_unique_equip_slot(slots, SLOT_BACK_L) + append_unique_equip_slot(slots, SLOT_BACK_R) + append_unique_equip_slot(slots, SLOT_BACK) + append_unique_equip_slot(slots, SLOT_HANDS) + return + if(group == "sheath") + append_unique_equip_slot(slots, SLOT_BELT) + append_unique_equip_slot(slots, SLOT_BELT_L) + append_unique_equip_slot(slots, SLOT_BELT_R) + append_unique_equip_slot(slots, SLOT_BACK_L) + append_unique_equip_slot(slots, SLOT_BACK_R) + append_unique_equip_slot(slots, SLOT_BACK) + return + append_unique_equip_slot(slots, SLOT_BELT) + append_unique_equip_slot(slots, SLOT_BELT_L) + append_unique_equip_slot(slots, SLOT_BELT_R) + append_unique_equip_slot(slots, SLOT_BACK_L) + append_unique_equip_slot(slots, SLOT_BACK_R) + append_unique_equip_slot(slots, SLOT_BACK) + append_unique_equip_slot(slots, SLOT_HANDS) + +/datum/tat_items/proc/get_loadout_ui_slot_ids() + return list( + "neck", + "mask", + "head", + "mouth", + "cloak", + "armor", + "suit", + "belt", + "legs", + "boots", + "wrists", + "gloves", + "ring", + "shoulder_l", + "shoulder_r", + "belt_l", + "belt_r", + "hand_l", + "hand_r", + ) + +/datum/tat_items/proc/get_loadout_slot_equip_slot(slot_id) + switch(slot_id) + if("neck") + return SLOT_NECK + if("mask") + return SLOT_WEAR_MASK + if("head") + return SLOT_HEAD + if("mouth") + return SLOT_MOUTH + if("cloak") + return SLOT_CLOAK + if("armor") + return SLOT_ARMOR + if("suit") + return SLOT_SHIRT + if("belt") + return SLOT_BELT + if("legs") + return SLOT_PANTS + if("boots") + return SLOT_SHOES + if("wrists") + return SLOT_WRISTS + if("gloves") + return SLOT_GLOVES + if("ring") + return SLOT_RING + if("shoulder_l") + return SLOT_BACK_L + if("shoulder_r") + return SLOT_BACK_R + if("belt_l") + return SLOT_BELT_L + if("belt_r") + return SLOT_BELT_R + if("hand_l", "hand_r") + return SLOT_HANDS + return null + +/datum/tat_items/proc/append_loadout_ui_slots_for_slot_group(list/slots, slot_group) + var/group = lowertext("[slot_group]") + switch(group) + if("neck") + append_unique_text(slots, "neck") + if("mask") + append_unique_text(slots, "mask") + if("head") + append_unique_text(slots, "head") + if("mouth") + append_unique_text(slots, "mouth") + if("cloak") + append_unique_text(slots, "cloak") + if("armor") + append_unique_text(slots, "armor") + if("suit", "shirt", "under") + append_unique_text(slots, "suit") + if("belt") + append_unique_text(slots, "belt") + append_unique_text(slots, "belt_l") + append_unique_text(slots, "belt_r") + if("pants") + append_unique_text(slots, "legs") + if("shoes") + append_unique_text(slots, "boots") + if("wrists") + append_unique_text(slots, "wrists") + if("gloves") + append_unique_text(slots, "gloves") + if("ring") + append_unique_text(slots, "ring") + if("cross", "amulet", "amulets", "talisman", "talismans", "charm", "charms", "necklace", "necklaces") + append_amulet_loadout_ui_slots(slots) + if("back") + append_unique_text(slots, "shoulder_l") + append_unique_text(slots, "shoulder_r") + if("back_l") + append_unique_text(slots, "shoulder_l") + if("back_r") + append_unique_text(slots, "shoulder_r") + if("belt_l") + append_unique_text(slots, "belt_l") + if("belt_r") + append_unique_text(slots, "belt_r") + if("music") + append_music_loadout_ui_slots(slots) + if("adventur' supply", "adventur supply", "adventure supply", "light", "lamp", "lantern", "torch") + append_light_loadout_ui_slots(slots) + if("blackpowder", "ranged", "munition", "knife", "sword", "greatsword", "axe", "blunt", "polearm", "whip", "sheath", "artifact", "unarmed") + append_weapon_loadout_ui_slots(slots, group) + +/datum/tat_items/proc/append_loadout_ui_slots_for_equip_slot(list/slots, slot_id) + switch(slot_id) + if(SLOT_NECK) + append_unique_text(slots, "neck") + if(SLOT_WEAR_MASK) + append_unique_text(slots, "mask") + if(SLOT_HEAD) + append_unique_text(slots, "head") + if(SLOT_MOUTH) + append_unique_text(slots, "mouth") + if(SLOT_CLOAK) + append_unique_text(slots, "cloak") + if(SLOT_ARMOR) + append_unique_text(slots, "armor") + if(SLOT_SHIRT) + append_unique_text(slots, "suit") + if(SLOT_BELT) + append_unique_text(slots, "belt") + if(SLOT_PANTS) + append_unique_text(slots, "legs") + if(SLOT_SHOES) + append_unique_text(slots, "boots") + if(SLOT_WRISTS) + append_unique_text(slots, "wrists") + if(SLOT_GLOVES) + append_unique_text(slots, "gloves") + if(SLOT_RING) + append_unique_text(slots, "ring") + if(SLOT_BACK_L) + append_unique_text(slots, "shoulder_l") + if(SLOT_BACK_R) + append_unique_text(slots, "shoulder_r") + if(SLOT_BACK) + append_unique_text(slots, "shoulder_l") + append_unique_text(slots, "shoulder_r") + if(SLOT_BELT_L) + append_unique_text(slots, "belt_l") + if(SLOT_BELT_R) + append_unique_text(slots, "belt_r") + if(SLOT_HANDS) + append_unique_text(slots, "hand_l") + append_unique_text(slots, "hand_r") + +/datum/tat_items/proc/append_hand_slots_if_reasonable(list/slots, item_path, list/entry) + var/category = lowertext("[entry["category"]]") + var/slot_group = lowertext("[entry["slot_group"]]") + if(slot_group == "music" || ispath(item_path, /obj/item/rogue/instrument)) + append_music_loadout_ui_slots(slots) + return + if(is_light_loadout_item(item_path, entry)) + append_light_loadout_ui_slots(slots) + return + if(is_amulet_loadout_item(item_path, entry)) + append_amulet_loadout_ui_slots(slots) + return + if(category == TAT_ITEM_CATEGORY_WEAPON || is_weapon_loadout_group(slot_group)) + append_weapon_loadout_ui_slots(slots, slot_group) + +/datum/tat_items/proc/get_cached_equip_slots_for_item(item_path) + if(item_path in equip_slots_cache) + return equip_slots_cache[item_path] + + var/list/result = list() + if(ispath(item_path, /obj/item)) + var/obj/item/I = new item_path(null) + if(I) + result = get_equip_slots_for_item(I, item_path) + qdel(I) + equip_slots_cache[item_path] = result + return result + +/datum/tat_items/proc/get_valid_loadout_ui_slots_for_item(item_path) + if(!ispath(item_path)) + item_path = text2path("[item_path]") + if(!item_path) + return list() + + var/list/cached = GLOB.tat_item_loadout_slots_cache[item_path] + if(islist(cached)) + return cached + + var/list/result = list() + var/list/entry = get_entry(item_path) + if(!islist(entry)) + return result + + append_loadout_ui_slots_for_slot_group(result, entry["slot_group"]) + + for(var/slot_id in get_cached_equip_slots_for_item(item_path)) + append_loadout_ui_slots_for_equip_slot(result, slot_id) + + append_hand_slots_if_reasonable(result, item_path, entry) + GLOB.tat_item_loadout_slots_cache[item_path] = result + return result + +/datum/tat_items/proc/get_assigned_loadout_slot_count(item_path) + var/list/loadout = get_loadout(item_path) + var/list/slots = loadout["slots"] + if(!islist(slots)) + return 0 + return length(slots) + +/datum/tat_items/proc/clear_loadout_slot(slot_id) + if(!istext(slot_id) || !length(slot_id)) + return FALSE + var/changed = FALSE + for(var/item_path in item_loadout) + var/list/loadout = item_loadout[item_path] + if(!islist(loadout)) + continue + var/list/slots = loadout["slots"] + if(!islist(slots) || !(slot_id in slots)) + continue + slots -= slot_id + // Clearing an equipped slot is the one explicit path that returns that copy + // to backpack; all other newly loose copies default to stash. + loadout["bag"] = round(loadout["bag"] || 0) + 1 + normalize_loadout(item_path) + changed = TRUE + if(changed) + owner_build?.set_dirty() + return changed + +/datum/tat_items/proc/assign_item_to_loadout_slot(item_path, slot_id) + if(!istext(slot_id) || !length(slot_id)) + return FALSE + if(!(slot_id in get_loadout_ui_slot_ids())) + return FALSE + if(get_amount(item_path) <= 0) + return FALSE + var/list/valid_slots = get_valid_loadout_ui_slots_for_item(item_path) + if(!(slot_id in valid_slots)) + return FALSE + + var/list/loadout = get_loadout(item_path) + if(round(loadout["bag"] || 0) <= 0) + return FALSE + + clear_loadout_slot(slot_id) + + loadout = get_loadout(item_path) + var/list/slots = loadout["slots"] + if(!islist(slots)) + slots = list() + loadout["slots"] = slots + if(!(slot_id in slots)) + while(length(slots) >= get_amount(item_path)) + var/drop_slot = slots[length(slots)] + slots -= drop_slot + loadout["bag"] = max(0, round(loadout["bag"] || 0) - 1) + slots[slot_id] = TRUE + normalize_loadout(item_path) + owner_build?.set_dirty() + return TRUE + +/datum/tat_items/proc/move_item_from_bag_to_stash(item_path, amount = 1) + if(get_amount(item_path) <= 0) + return FALSE + var/list/loadout = get_loadout(item_path) + var/count = min(max(1, round(amount || 1)), round(loadout["bag"] || 0)) + if(count <= 0) + return FALSE + loadout["bag"] = round(loadout["bag"] || 0) - count + loadout["stash"] = round(loadout["stash"] || 0) + count + normalize_loadout(item_path) + owner_build?.set_dirty() + return TRUE + +/datum/tat_items/proc/move_item_from_stash_to_bag(item_path, amount = 1) + if(get_amount(item_path) <= 0) + return FALSE + var/list/loadout = get_loadout(item_path) + var/count = min(max(1, round(amount || 1)), round(loadout["stash"] || 0)) + if(count <= 0) + return FALSE + loadout["stash"] = round(loadout["stash"] || 0) - count + loadout["bag"] = round(loadout["bag"] || 0) + count + normalize_loadout(item_path) + owner_build?.set_dirty() + return TRUE + +/datum/tat_items/proc/assign_item_to_first_available_loadout_slot(item_path) + var/list/valid_slots = get_valid_loadout_ui_slots_for_item(item_path) + for(var/slot_id in valid_slots) + var/taken = FALSE + for(var/other_path in item_loadout) + var/list/other_loadout = item_loadout[other_path] + var/list/other_slots = islist(other_loadout) ? other_loadout["slots"] : null + if(islist(other_slots) && (slot_id in other_slots)) + taken = TRUE + break + if(taken) + continue + return assign_item_to_loadout_slot(item_path, slot_id) + return FALSE + +/datum/tat_items/proc/append_unique_equip_slot(list/slots, slot_id) + if(!(slot_id in slots)) + slots += slot_id + +/datum/tat_items/proc/get_equip_slots_for_item(obj/item/I, item_path = null) + var/list/slots = list() + if(!I) + return slots + + var/list/entry = item_path ? get_entry(item_path) : null + var/slot_group = islist(entry) ? lowertext("[entry["slot_group"]]") : null + + // Prefer explicit TAT slot groups. Backpacks and satchels in RogueTown/Twilight Axis + // are shoulder/back items first, and slot_flags alone is not reliable enough here. + switch(slot_group) + if("back") + append_unique_equip_slot(slots, SLOT_BACK_L) + append_unique_equip_slot(slots, SLOT_BACK_R) + append_unique_equip_slot(slots, SLOT_BACK) + if("belt") + append_unique_equip_slot(slots, SLOT_BELT) + append_unique_equip_slot(slots, SLOT_BELT_L) + append_unique_equip_slot(slots, SLOT_BELT_R) + if("cloak") + append_unique_equip_slot(slots, SLOT_CLOAK) + if("neck") + append_unique_equip_slot(slots, SLOT_NECK) + if("head") + append_unique_equip_slot(slots, SLOT_HEAD) + if("mask") + append_unique_equip_slot(slots, SLOT_WEAR_MASK) + if("armor", "suit") + append_unique_equip_slot(slots, SLOT_ARMOR) + if("shirt", "under") + append_unique_equip_slot(slots, SLOT_SHIRT) + if("pants") + append_unique_equip_slot(slots, SLOT_PANTS) + if("wrists") + append_unique_equip_slot(slots, SLOT_WRISTS) + if("gloves") + append_unique_equip_slot(slots, SLOT_GLOVES) + if("shoes") + append_unique_equip_slot(slots, SLOT_SHOES) + if("ring") + append_unique_equip_slot(slots, SLOT_RING) + if("cross", "amulet", "amulets", "talisman", "talismans", "charm", "charms", "necklace", "necklaces") + append_amulet_equip_slots(slots) + if("music") + append_music_equip_slots(slots) + if("adventur' supply", "adventur supply", "adventure supply", "light", "lamp", "lantern", "torch") + append_light_equip_slots(slots) + if("blackpowder", "ranged", "munition", "knife", "sword", "greatsword", "axe", "blunt", "polearm", "whip", "sheath", "artifact", "unarmed") + append_weapon_equip_slots(slots, slot_group) + + if(ispath(item_path, /obj/item/rogue/instrument)) + append_music_equip_slots(slots) + if(is_light_loadout_item(item_path, entry)) + append_light_equip_slots(slots) + if(is_amulet_loadout_item(item_path, entry)) + append_amulet_equip_slots(slots) + if(islist(entry) && lowertext("[entry["category"]]") == TAT_ITEM_CATEGORY_WEAPON) + append_weapon_equip_slots(slots, slot_group) + + var/flags = I.slot_flags + if(flags & ITEM_SLOT_HEAD) + append_unique_equip_slot(slots, SLOT_HEAD) + if(flags & ITEM_SLOT_MASK) + append_unique_equip_slot(slots, SLOT_WEAR_MASK) + if(flags & ITEM_SLOT_NECK) + append_unique_equip_slot(slots, SLOT_NECK) + if(is_amulet_loadout_item(item_path, entry)) + append_unique_equip_slot(slots, SLOT_RING) + if(flags & ITEM_SLOT_CLOAK) + append_unique_equip_slot(slots, SLOT_CLOAK) + if(flags & ITEM_SLOT_ARMOR || flags & ITEM_SLOT_OCLOTHING) + append_unique_equip_slot(slots, SLOT_ARMOR) + if(flags & ITEM_SLOT_SHIRT) + append_unique_equip_slot(slots, SLOT_SHIRT) + if(flags & ITEM_SLOT_PANTS) + append_unique_equip_slot(slots, SLOT_PANTS) + if(flags & ITEM_SLOT_ICLOTHING) + append_unique_equip_slot(slots, SLOT_SHIRT) + append_unique_equip_slot(slots, SLOT_PANTS) + if(flags & ITEM_SLOT_WRISTS) + append_unique_equip_slot(slots, SLOT_WRISTS) + if(flags & ITEM_SLOT_GLOVES) + append_unique_equip_slot(slots, SLOT_GLOVES) + if(flags & ITEM_SLOT_SHOES) + append_unique_equip_slot(slots, SLOT_SHOES) + if(flags & ITEM_SLOT_RING) + append_unique_equip_slot(slots, SLOT_RING) + if(flags & ITEM_SLOT_BELT) + append_unique_equip_slot(slots, SLOT_BELT) + return slots + + +/datum/tat_items/proc/get_paint_data(item_path) + var/list/paint = item_paint[item_path] + if(!islist(paint)) + paint = list() + item_paint[item_path] = paint + return paint + +/datum/tat_items/proc/get_paint_data_for_ui(item_path) + var/list/paint = item_paint[item_path] + if(!islist(paint) || !length(paint)) + return null + return paint.Copy() + +/datum/tat_items/proc/build_loadout_item_icon_payload(item_path) + if(!ispath(item_path, /obj/item)) + return null + var/obj/item/preview_item = new item_path(null) + if(!preview_item) + return null + + var/list/paint = item_paint[item_path] + if(islist(paint) && length(paint)) + apply_paint_to_item(item_path, preview_item) + + var/icon/preview_icon = new /icon() + preview_icon.Insert(new /icon(preview_item.icon, preview_item.icon_state), "", SOUTH, 0) + + if(islist(paint) && istext(paint["primary"])) + preview_icon.Blend(paint["primary"], ICON_MULTIPLY) + + if(islist(paint) && istext(paint["detail"]) && preview_item.detail_tag && preview_item.detail_color) + var/icon/detail_overlay = new /icon() + detail_overlay.Insert(new /icon(preview_item.icon, "[preview_item.icon_state][preview_item.detail_tag]"), "", SOUTH, 0) + detail_overlay.Blend(paint["detail"], ICON_MULTIPLY) + preview_icon.Blend(detail_overlay, ICON_OVERLAY) + + if(islist(paint) && istext(paint["altdetail"]) && preview_item.altdetail_tag && preview_item.altdetail_color) + var/icon/altdetail_overlay = new /icon() + altdetail_overlay.Insert(new /icon(preview_item.icon, "[preview_item.icon_state][preview_item.altdetail_tag]"), "", SOUTH, 0) + altdetail_overlay.Blend(paint["altdetail"], ICON_MULTIPLY) + preview_icon.Blend(altdetail_overlay, ICON_OVERLAY) + + var/list/result = list( + "icon" = icon2base64(preview_icon), + "icon_state" = "[preview_item.icon_state]", + ) + qdel(preview_item) + return result + +/datum/tat_items/proc/can_paint_item_path(item_path) + if(!ispath(item_path, /obj/item)) + return FALSE + var/obj/item/I = new item_path(null) + if(!I) + return FALSE + var/can_paint = is_type_in_list(I, list( + /obj/item/clothing, + /obj/item/storage, + /obj/item/bedroll, + /obj/item/flowercrown, + /obj/item/legwears, + /obj/item/undies, + /obj/item/natural/cloth, + /obj/item/caparison, + /obj/item/reagent_containers/glass/bottle/clayvase, + /obj/item/reagent_containers/glass/bottle/clayfancyvase, + /obj/item/reagent_containers/glass/cup/claycup, + /obj/item/reagent_containers/glass/bottle/claybottle, + /obj/item/roguestatue/clay, + /obj/item/roguestatue/glass, + )) + qdel(I) + return can_paint + +/datum/tat_items/proc/pick_tat_dye(mob/user, current_color = "#FFFFFF", prompt_title = "Loadout Dye") + if(!user) + return null + if(alert(user, "Input Choice", prompt_title, "Color Wheel", "Color Preset") == "Color Wheel") + var/c = sanitize_hexcolor(color_pick_sanitized(user, "Choose your dye:", "Dyes", current_color), 6, TRUE) + return (c == "#000000") ? "#FFFFFF" : c + + var/list/colors_to_pick = list() + if(GLOB.lordprimary) + colors_to_pick["Primary Keep Color"] = GLOB.lordprimary + if(GLOB.lordsecondary) + colors_to_pick["Secondary Keep Color"] = GLOB.lordsecondary + colors_to_pick += COLOR_MAP + colors_to_pick += pridelist + var/picked = input(user, "Choose your dye:", "Dyes", null) as null|anything in colors_to_pick + if(!picked) + return null + return colors_to_pick[picked] + +/datum/tat_items/proc/paint_loadout_item(item_path, mob/user) + if(get_amount(item_path) <= 0) + return FALSE + if(!can_paint_item_path(item_path)) + to_chat(user, span_warning("This loadout item cannot be dyed.")) + return FALSE + + var/obj/item/preview = new item_path(null) + var/list/options = list("Primary color", "Clear primary", "Clear all") + if(preview?.detail_color) + options += "Detail color" + options += "Clear detail" + if(preview?.altdetail_color) + options += "Alt detail color" + options += "Clear alt detail" + qdel(preview) + + var/choice = tgui_input_list(user, "Choose which loadout dye to edit.", "Loadout Dye", options) + if(!choice) + return FALSE + + var/list/paint = get_paint_data(item_path) + switch(choice) + if("Primary color") + var/color = pick_tat_dye(user, paint["primary"] || "#FFFFFF", "Primary Dye") + if(!color) + return FALSE + paint["primary"] = color + if("Detail color") + var/color = pick_tat_dye(user, paint["detail"] || "#FFFFFF", "Secondary Dye") + if(!color) + return FALSE + paint["detail"] = color + if("Alt detail color") + var/color = pick_tat_dye(user, paint["altdetail"] || "#FFFFFF", "Tertiary Dye") + if(!color) + return FALSE + paint["altdetail"] = color + if("Clear primary") + paint -= "primary" + if("Clear detail") + paint -= "detail" + if("Clear alt detail") + paint -= "altdetail" + if("Clear all") + item_paint -= item_path + owner_build?.set_dirty() + return TRUE + + if(islist(paint) && !length(paint)) + item_paint -= item_path + owner_build?.set_dirty() + return TRUE + +/datum/tat_items/proc/apply_paint_to_item(item_path, obj/item/I) + if(!I || QDELETED(I)) + return FALSE + var/list/paint = item_paint[item_path] + if(!islist(paint) || !length(paint)) + return FALSE + if(istext(paint["primary"])) + I.add_atom_colour(paint["primary"], FIXED_COLOUR_PRIORITY) + if(istext(paint["detail"]) && I.detail_color) + I.detail_color = paint["detail"] + if(istext(paint["altdetail"]) && I.altdetail_color) + I.altdetail_color = paint["altdetail"] + I.update_icon() + return TRUE + + +/datum/tat_items/proc/get_mind_stash_item_name(item_path) + var/list/entry = get_entry(item_path) + if(islist(entry) && istext(entry["name"]) && length(entry["name"])) + return entry["name"] + if(ispath(item_path, /obj/item)) + var/obj/item/I = item_path + return initial(I.name) || "[item_path]" + return "[item_path]" + +/datum/tat_items/proc/get_unique_mind_stash_key(mob/living/carbon/human/H, item_path) + if(!H?.mind) + return null + var/base_name = get_mind_stash_item_name(item_path) + if(!istext(base_name) || !length(base_name)) + base_name = "[item_path]" + if(!islist(H.mind.special_items)) + H.mind.special_items = list() + if(!(base_name in H.mind.special_items)) + return base_name + for(var/index in 2 to 999) + var/candidate = "[base_name] ([index])" + if(!(candidate in H.mind.special_items)) + return candidate + return "[base_name] ([world.time])" + +/datum/tat_items/proc/add_item_path_to_mind_stash(mob/living/carbon/human/H, item_path, amount = 1) + if(!H?.mind || !ispath(item_path, /obj/item)) + return FALSE + if(!islist(H.mind.special_items)) + H.mind.special_items = list() + var/count = max(0, round(amount || 0)) + if(count <= 0) + return FALSE + var/added = FALSE + for(var/i in 1 to count) + var/key = get_unique_mind_stash_key(H, item_path) + if(!key) + continue + H.mind.special_items[key] = item_path + added = TRUE + return added + +/datum/tat_items/proc/remove_item_path_from_mind_stash(mob/living/carbon/human/H, item_path, amount = 1) + if(!H?.mind || !ispath(item_path, /obj/item) || !islist(H.mind.special_items)) + return FALSE + var/count = max(0, round(amount || 0)) + if(count <= 0) + return FALSE + var/removed = 0 + for(var/key in H.mind.special_items.Copy()) + if(removed >= count) + break + if(H.mind.special_items[key] != item_path) + continue + H.mind.special_items -= key + removed++ + return removed > 0 + +/datum/tat_items/proc/get_loadout_assigned_slot_count(item_path) + var/list/loadout = get_loadout(item_path) + var/list/slots = loadout["slots"] + if(!islist(slots)) + return 0 + return length(slots) + +/datum/tat_items/proc/get_consumed_donor_loadout_amount(item_path) + var/donor_amount = get_granted_amount(item_path, TAT_ITEM_SOURCE_DONOR_LOADOUT) + if(donor_amount <= 0) + return 0 + var/list/loadout = get_loadout(item_path) + var/assigned = get_loadout_assigned_slot_count(item_path) + var/bag = max(0, round(loadout["bag"] || 0)) + return min(donor_amount, assigned + bag) + +/datum/tat_items/proc/remove_consumed_preference_loadout_from_mind_stash(mob/living/carbon/human/H) + if(!H?.mind || !islist(H.mind.special_items)) + return FALSE + var/changed = FALSE + for(var/item_path in get_all_item_paths()) + var/consumed = get_consumed_donor_loadout_amount(item_path) + if(consumed <= 0) + continue + if(remove_item_path_from_mind_stash(H, item_path, consumed)) + changed = TRUE + return changed + +/datum/tat_items/proc/get_effective_stash_spawn_amount(item_path) + if(get_amount(item_path) <= 0) + return 0 + normalize_loadout(item_path) + var/list/loadout = get_loadout(item_path) + var/amount = get_amount(item_path) + var/assigned = get_loadout_assigned_slot_count(item_path) + var/bag = max(0, round(loadout["bag"] || 0)) + var/stash = max(0, round(loadout["stash"] || 0)) + var/max_stash = max(0, amount - assigned - bag) + return min(stash, max_stash) + +/datum/tat_items/proc/stash_existing_item_for_later(obj/item/I, mob/living/carbon/human/H, item_path = null) + if(!I || QDELETED(I)) + return FALSE + if(!ispath(item_path, /obj/item)) + item_path = I.type + if(add_item_path_to_mind_stash(H, item_path, 1)) + qdel(I) + return TRUE + return FALSE + +/datum/tat_items/proc/get_storage_targets(mob/living/carbon/human/H) + var/list/targets = list() + if(!H) + return targets + for(var/slot_id in list(SLOT_BACK_L, SLOT_BACK_R, SLOT_BELT_L, SLOT_BELT_R, SLOT_BACK, SLOT_BELT, SLOT_CLOAK)) + var/obj/item/I = H.get_item_by_slot(slot_id) + if(I && !(I in targets)) + targets += I + return targets + +/datum/tat_items/proc/try_insert_into_storage(obj/item/I, atom/storage_owner, mob/living/carbon/human/H) + if(!I || !storage_owner) + return FALSE + return !!SEND_SIGNAL(storage_owner, COMSIG_TRY_STORAGE_INSERT, I, null, TRUE, TRUE) + +/datum/tat_items/proc/try_put_into_any_storage_or_drop(obj/item/I, mob/living/carbon/human/H, item_path = null) + if(!I || !H || QDELETED(I)) + return FALSE + for(var/storage_owner in get_storage_targets(H)) + if(QDELETED(I)) + return FALSE + if(try_insert_into_storage(I, storage_owner, H)) + return TRUE + if(QDELETED(I)) + return FALSE + if(stash_existing_item_for_later(I, H, item_path)) + return TRUE + I.forceMove(get_turf(H)) + return FALSE + + +/datum/tat_items/proc/is_coin_pouch_path(path) + if(!ispath(path)) + return FALSE + return ispath(path, /obj/item/storage/belt/rogue/pouch/coins) + +/datum/tat_items/proc/merge_coin_stacks_in_container(atom/container) + if(!container) + return FALSE + + var/list/coins_by_type = list() + for(var/obj/item/roguecoin/coin in container.contents) + if(QDELETED(coin)) + continue + if(!coin.base_type) + continue + + var/coin_type = coin.type + if(!coins_by_type[coin_type]) + coins_by_type[coin_type] = list() + coins_by_type[coin_type] += coin + + for(var/coin_type in coins_by_type) + var/list/coins = coins_by_type[coin_type] + var/list/active_stacks = list() + + for(var/obj/item/roguecoin/coin as anything in coins) + if(QDELETED(coin) || coin.quantity <= 0) + continue + + var/was_merged = FALSE + for(var/obj/item/roguecoin/target as anything in active_stacks) + if(QDELETED(target) || target.quantity >= 20) + continue + + target.merge(coin, null) + was_merged = TRUE + + if(QDELETED(coin) || coin.quantity <= 0) + break + + if(!was_merged && !QDELETED(coin) && coin.quantity > 0) + active_stacks += coin + + return TRUE + +/datum/tat_items/proc/spawn_stacked_coin_pouch_into_bag_or_fallback(mob/living/carbon/human/H, path, amount = 1) + if(!H || !is_coin_pouch_path(path)) + return FALSE + + amount = max(1, round(amount || 1)) + + var/turf/drop_turf = get_turf(H) + if(!drop_turf) + return FALSE + + var/obj/item/storage/belt/rogue/pouch/coins/pouch = new path(drop_turf) + if(!pouch || QDELETED(pouch)) + return FALSE + + // The first pouch has already populated itself during normal initialization. + // Additional purchased pouch copies are represented by repeating the native + // pouch population on this same pouch. This preserves map-specific currency + // logic, SSwardrobe use, random pile sizes, and Rockhill goldkrona behavior. + if(amount > 1) + for(var/i in 2 to amount) + if(QDELETED(pouch)) + return FALSE + pouch.PopulateContents() + + merge_coin_stacks_in_container(pouch) + apply_paint_to_item(path, pouch) + try_put_into_any_storage_or_drop(pouch, H, path) + return TRUE + +/datum/tat_items/proc/spawn_item_into_bag_or_fallback(mob/living/carbon/human/H, path, amount = 1) + if(!H || !ispath(path)) + return FALSE + + amount = max(1, round(amount || 1)) + if(is_coin_pouch_path(path)) + return spawn_stacked_coin_pouch_into_bag_or_fallback(H, path, amount) + + var/success = FALSE + for(var/i in 1 to amount) + var/obj/item/I = new path(get_turf(H)) + if(!I) + continue + apply_paint_to_item(path, I) + try_put_into_any_storage_or_drop(I, H, path) + success = TRUE + return success + +/datum/tat_items/proc/spawn_item_equipped_or_fallback(mob/living/carbon/human/H, path) + if(!H || !ispath(path)) + return FALSE + var/obj/item/I = new path(get_turf(H)) + if(!I) + return FALSE + apply_paint_to_item(path, I) + var/list/slots = get_equip_slots_for_item(I, path) + for(var/slot_id in slots) + if(QDELETED(I)) + return FALSE + if(H.get_item_by_slot(slot_id)) + continue + if(H.equip_to_slot_if_possible(I, slot_id, FALSE, TRUE, TRUE, TRUE)) + return TRUE + try_put_into_any_storage_or_drop(I, H, path) + return FALSE + +/datum/tat_items/proc/get_item_slot_group_lower(path) + var/list/entry = get_entry(path) + if(!islist(entry)) + return null + return lowertext("[entry["slot_group"]]") + +/datum/tat_items/proc/try_put_into_loadout_hand(mob/living/carbon/human/H, obj/item/I, slot_id) + if(!H || !I || QDELETED(I)) + return FALSE + + if(slot_id == "hand_l") + H.put_in_l_hand(I, TRUE) + else if(slot_id == "hand_r") + H.put_in_r_hand(I, TRUE) + else + return FALSE + + if(QDELETED(I)) + return TRUE + return I.loc == H + +/datum/tat_items/proc/try_equip_existing_item_to_exact_slot(mob/living/carbon/human/H, obj/item/I, equip_slot) + if(!H || !I || QDELETED(I) || !equip_slot) + return FALSE + if(H.get_item_by_slot(equip_slot)) + return FALSE + if(H.equip_to_slot_if_possible(I, equip_slot, FALSE, TRUE, TRUE, TRUE)) + return H.get_item_by_slot(equip_slot) == I || I.loc == H + return FALSE + +/datum/tat_items/proc/get_hand_loadout_wearable_fallback_slots(item_path, preferred_hand_slot_id = null) + var/list/result = list() + var/list/valid_ui_slots = get_valid_loadout_ui_slots_for_item(item_path) + var/list/preferred_ui_slots = list("shoulder_l", "shoulder_r", "belt", "belt_l", "belt_r") + for(var/ui_slot in preferred_ui_slots) + if(!(ui_slot in valid_ui_slots)) + continue + var/equip_slot = get_loadout_slot_equip_slot(ui_slot) + if(equip_slot) + append_unique_equip_slot(result, equip_slot) + for(var/equip_slot in get_cached_equip_slots_for_item(item_path)) + if(equip_slot == SLOT_HANDS) + continue + append_unique_equip_slot(result, equip_slot) + return result + +/datum/tat_items/proc/try_equip_existing_item_to_hand_fallback_slot(mob/living/carbon/human/H, obj/item/I, item_path, preferred_hand_slot_id = null) + if(!H || !I || QDELETED(I) || !ispath(item_path)) + return FALSE + for(var/equip_slot in get_hand_loadout_wearable_fallback_slots(item_path, preferred_hand_slot_id)) + if(QDELETED(I)) + return FALSE + if(try_equip_existing_item_to_exact_slot(H, I, equip_slot)) + return TRUE + return FALSE + +/datum/tat_items/proc/spawn_item_to_exact_slot_or_bag(mob/living/carbon/human/H, path, equip_slot) + if(!H || !ispath(path) || !equip_slot) + return FALSE + var/obj/item/I = new path(get_turf(H)) + if(!I) + return FALSE + apply_paint_to_item(path, I) + if(H.get_item_by_slot(equip_slot)) + try_put_into_any_storage_or_drop(I, H, path) + return FALSE + if(H.equip_to_slot_if_possible(I, equip_slot, FALSE, TRUE, TRUE, TRUE)) + if(H.get_item_by_slot(equip_slot) == I) + return TRUE + if(!QDELETED(I)) + try_put_into_any_storage_or_drop(I, H, path) + return FALSE + try_put_into_any_storage_or_drop(I, H, path) + return FALSE + +/datum/tat_items/proc/spawn_item_to_loadout_hand(mob/living/carbon/human/H, path, slot_id, allow_fallback = TRUE) + if(!H || !ispath(path) || !is_hand_loadout_slot(slot_id)) + return FALSE + var/obj/item/I = new path(get_turf(H)) + if(!I) + return FALSE + apply_paint_to_item(path, I) + if(try_put_into_loadout_hand(H, I, slot_id)) + return TRUE + if(allow_fallback) + if(try_equip_existing_item_to_hand_fallback_slot(H, I, path, slot_id)) + return TRUE + try_put_into_any_storage_or_drop(I, H, path) + else + qdel(I) + return FALSE + +/datum/tat_items/proc/spawn_item_to_loadout_slot_or_bag(mob/living/carbon/human/H, path, slot_id) + if(!H || !ispath(path)) + return FALSE + if(is_hand_loadout_slot(slot_id)) + return spawn_item_to_loadout_hand(H, path, slot_id, TRUE) + var/equip_slot = get_loadout_slot_equip_slot(slot_id) + if(!equip_slot) + return FALSE + return spawn_item_to_exact_slot_or_bag(H, path, equip_slot) + +/datum/tat_items/proc/is_hand_loadout_slot(slot_id) + return slot_id == "hand_l" || slot_id == "hand_r" + +/datum/tat_items/proc/spawn_assigned_loadout_items(mob/living/carbon/human/H, hands_only = FALSE, allow_hand_fallback = TRUE) + for(var/slot_id in get_loadout_ui_slot_ids()) + if(is_hand_loadout_slot(slot_id) != hands_only) + continue + for(var/item_path in get_all_item_paths()) + var/list/loadout = get_loadout(item_path) + var/list/slots = loadout["slots"] + if(!islist(slots) || !(slot_id in slots)) + continue + if(is_hand_loadout_slot(slot_id)) + spawn_item_to_loadout_hand(H, item_path, slot_id, allow_hand_fallback) + else + spawn_item_to_loadout_slot_or_bag(H, item_path, slot_id) + break + +/datum/tat_items/proc/get_assigned_item_for_loadout_slot(slot_id) + if(!is_hand_loadout_slot(slot_id)) + return null + for(var/item_path in get_all_item_paths()) + var/list/loadout = get_loadout(item_path) + var/list/slots = loadout["slots"] + if(islist(slots) && (slot_id in slots)) + return item_path + return null + +/datum/tat_items/proc/spawn_hand_loadout_items(mob/living/carbon/human/H) + if(!H || QDELETED(H)) + return FALSE + + var/any_success = FALSE + + for(var/slot_id in list("hand_l", "hand_r")) + var/item_path = get_assigned_item_for_loadout_slot(slot_id) + if(!item_path) + continue + + if(spawn_item_to_loadout_hand(H, item_path, slot_id, TRUE)) + any_success = TRUE + + return any_success + +/datum/tat_items/proc/spawn_equipped_items_for_slot_group(mob/living/carbon/human/H, target_slot_group) + for(var/item_path in get_all_item_paths()) + if(get_item_slot_group_lower(item_path) != lowertext("[target_slot_group]")) + continue + var/list/loadout = get_loadout(item_path) + for(var/i in 1 to round(loadout["equip"] || 0)) + spawn_item_equipped_or_fallback(H, item_path) + +/datum/tat_items/proc/spawn_equipped_items_except_slot_groups(mob/living/carbon/human/H, list/excluded_groups) + for(var/item_path in get_all_item_paths()) + var/slot_group = get_item_slot_group_lower(item_path) + if(islist(excluded_groups) && (slot_group in excluded_groups)) + continue + var/list/loadout = get_loadout(item_path) + for(var/i in 1 to round(loadout["equip"] || 0)) + spawn_item_equipped_or_fallback(H, item_path) + +/datum/tat_items/proc/spawn_bag_items(mob/living/carbon/human/H) + for(var/item_path in get_all_item_paths()) + var/list/loadout = get_loadout(item_path) + var/bag_amount = round(loadout["bag"] || 0) + if(bag_amount <= 0) + continue + spawn_item_into_bag_or_fallback(H, item_path, bag_amount) + +/datum/tat_items/proc/is_roundstart_bag_path(path) + if(!ispath(path)) + return FALSE + return ispath(path, /obj/item/storage/backpack/rogue) + +/datum/tat_items/proc/is_roundstart_bag_replacer_path(path) + if(!ispath(path)) + return FALSE + if(ispath(path, /obj/item/storage/backpack/rogue/backpack)) + return TRUE + return FALSE + +/datum/tat_items/proc/has_selected_roundstart_backpack() + for(var/item_path in get_all_item_paths()) + if(get_amount(item_path) <= 0) + continue + if(is_roundstart_bag_replacer_path(item_path)) + return TRUE + return FALSE + +/datum/tat_items/proc/has_existing_roundstart_bag(mob/living/carbon/human/H) + if(!H) + return FALSE + for(var/equip_slot in list(SLOT_BACK_L, SLOT_BACK_R, SLOT_BACK)) + var/obj/item/I = H.get_item_by_slot(equip_slot) + if(I && is_roundstart_bag_path(I.type)) + return TRUE + return FALSE + +/datum/tat_items/proc/spawn_roundstart_bag_to_slot_or_drop(mob/living/carbon/human/H, path, equip_slot) + if(!H || !ispath(path)) + return FALSE + var/obj/item/I = new path(get_turf(H)) + if(!I) + return FALSE + apply_paint_to_item(path, I) + if(equip_slot && !H.get_item_by_slot(equip_slot) && H.equip_to_slot_if_possible(I, equip_slot, FALSE, TRUE, TRUE, TRUE)) + return TRUE + if(!QDELETED(I)) + I.forceMove(get_turf(H)) + return TRUE + +/datum/tat_items/proc/get_reserved_loadout_equip_slots() + var/list/reserved = list() + for(var/item_path in get_all_item_paths()) + var/list/loadout = get_loadout(item_path) + var/list/slots = loadout["slots"] + if(!islist(slots)) + continue + for(var/slot_id in slots) + var/equip_slot = get_loadout_slot_equip_slot(slot_id) + if(equip_slot) + append_unique_equip_slot(reserved, equip_slot) + return reserved + +/datum/tat_items/proc/grant_default_roundstart_bag(mob/living/carbon/human/H) + if(!H) + return FALSE + if(has_selected_roundstart_backpack() || has_existing_roundstart_bag(H)) + return FALSE + var/list/reserved_slots = get_reserved_loadout_equip_slots() + for(var/equip_slot in list(SLOT_BACK_L, SLOT_BACK_R, SLOT_BACK)) + if(equip_slot in reserved_slots) + continue + if(H.get_item_by_slot(equip_slot)) + continue + return spawn_roundstart_bag_to_slot_or_drop(H, /obj/item/storage/backpack/rogue/satchel, equip_slot) + return spawn_roundstart_bag_to_slot_or_drop(H, /obj/item/storage/backpack/rogue/satchel, null) + + +/datum/tat_items/proc/spawn_stash_items(mob/living/carbon/human/H) + if(!H?.mind) + return FALSE + var/added = FALSE + for(var/item_path in get_all_item_paths()) + var/stash_amount = get_effective_stash_spawn_amount(item_path) + if(stash_amount <= 0) + continue + if(add_item_path_to_mind_stash(H, item_path, stash_amount)) + added = TRUE + return added + +/datum/tat_items/proc/apply_to_human(mob/living/carbon/human/H) + if(!H) + return FALSE + + sync_external_grants() + if(!length(get_all_item_paths())) + return TRUE + + for(var/item_path in get_all_item_paths()) + normalize_loadout(item_path) + + // If the legacy preference-loadout block already placed donor items into + // mind.special_items, remove copies that the TAT loadout will spawn in bag or + // equipped slots. This keeps old integration order from duplicating Pliant gear. + remove_consumed_preference_loadout_from_mind_stash(H) + spawn_stash_items(H) + spawn_assigned_loadout_items(H, FALSE) + grant_default_roundstart_bag(H) + spawn_bag_items(H) + spawn_hand_loadout_items(H) + + return TRUE + +/datum/tat_items/proc/disable_from_human(mob/living/carbon/human/H) + return TRUE + +/datum/tat_items/proc/export_to_list() + return list( + "selected" = selected.Copy(), + "item_loadout" = item_loadout.Copy(), + "item_grants" = item_grants.Copy(), + "item_paint" = item_paint.Copy(), + ) + +/datum/tat_items/proc/import_from_list(list/data) + reset() + if(!islist(data)) + return FALSE + + var/list/imported_selected = null + if(islist(data["selected"])) + imported_selected = data["selected"] + else + imported_selected = data + + for(var/item_path in imported_selected) + if(item_path == "selected" || item_path == "item_loadout" || item_path == "item_grants" || item_path == "item_paint") + continue + set_amount(item_path, imported_selected[item_path]) + + if(islist(data["item_grants"])) + var/list/temp_grants = data["item_grants"] + item_grants = temp_grants.Copy() + if(islist(data["item_loadout"])) + var/list/temp_loadout = data["item_loadout"] + item_loadout = temp_loadout.Copy() + if(islist(data["item_paint"])) + var/list/temp_paint = data["item_paint"] + item_paint = temp_paint.Copy() + + sync_external_grants() + for(var/item_path in get_all_item_paths()) + normalize_loadout(item_path) + + return TRUE + +/datum/tat_items/proc/export_to_json_list() + var/list/exported_selected = list() + for(var/item_path in selected) + var/amount = get_paid_amount(item_path) + if(amount > 0) + exported_selected["[item_path]"] = amount + + var/list/exported_grants = list() + for(var/item_path in item_grants) + var/list/sources = item_grants[item_path] + if(!islist(sources)) + continue + var/list/exported_sources = list() + for(var/source_key in sources) + var/count = round(sources[source_key] || 0) + if(count > 0) + exported_sources[source_key] = count + if(length(exported_sources)) + exported_grants["[item_path]"] = exported_sources + + var/list/exported_loadout = list() + for(var/item_path in item_loadout) + if(get_amount(item_path) <= 0) + continue + var/list/loadout = item_loadout[item_path] + if(!islist(loadout)) + continue + var/list/exported_slots = list() + var/list/slots = loadout["slots"] + if(islist(slots)) + for(var/slot_id in slots) + exported_slots[slot_id] = TRUE + exported_loadout["[item_path]"] = list( + "equip" = round(loadout["equip"] || 0), + "bag" = round(loadout["bag"] || 0), + "stash" = round(loadout["stash"] || 0), + "external_stash_initialized" = round(loadout["external_stash_initialized"] || 0), + "slots" = exported_slots, + ) + + var/list/exported_paint = list() + for(var/item_path in item_paint) + var/list/paint = item_paint[item_path] + if(islist(paint) && length(paint)) + exported_paint["[item_path]"] = paint.Copy() + + return list( + "selected" = exported_selected, + "item_grants" = exported_grants, + "item_loadout" = exported_loadout, + "item_paint" = exported_paint, + ) + +/datum/tat_items/proc/import_from_json_list(list/data) + reset() + if(!islist(data)) + return FALSE + + var/list/imported_selected = null + if(islist(data["selected"])) + imported_selected = data["selected"] + else + imported_selected = data + + for(var/raw_path in imported_selected) + if(raw_path == "selected" || raw_path == "item_loadout" || raw_path == "item_grants" || raw_path == "item_paint") + continue + var/item_path = ispath(raw_path) ? raw_path : text2path("[raw_path]") + if(!item_path) + continue + set_amount(item_path, text2num("[imported_selected[raw_path]]")) + + if(islist(data["item_grants"])) + for(var/raw_path in data["item_grants"]) + var/item_path = ispath(raw_path) ? raw_path : text2path("[raw_path]") + if(!item_path) + continue + var/list/source_data = data["item_grants"][raw_path] + if(!islist(source_data)) + continue + for(var/source_key in source_data) + set_item_grant_amount(item_path, source_key, text2num("[source_data[source_key]]"), TRUE) + + if(islist(data["item_loadout"])) + for(var/raw_path in data["item_loadout"]) + var/item_path = ispath(raw_path) ? raw_path : text2path("[raw_path]") + if(!item_path) + continue + var/list/source_loadout = data["item_loadout"][raw_path] + if(!islist(source_loadout)) + continue + var/raw_equip = source_loadout["equip"] + var/raw_bag = source_loadout["bag"] + var/raw_stash = source_loadout["stash"] + var/raw_external_stash_initialized = source_loadout["external_stash_initialized"] + var/list/imported_slots = list() + if(islist(source_loadout["slots"])) + var/list/source_slots = source_loadout["slots"] + for(var/slot_id in source_slots) + if(source_slots[slot_id]) + imported_slots[slot_id] = TRUE + item_loadout[item_path] = list( + "equip" = round(text2num("[raw_equip]") || 0), + "bag" = round(text2num("[raw_bag]") || 0), + "stash" = round(text2num("[raw_stash]") || 0), + "external_stash_initialized" = round(text2num("[raw_external_stash_initialized]") || 0), + "slots" = imported_slots, + ) + + if(islist(data["item_paint"])) + for(var/raw_path in data["item_paint"]) + var/item_path = ispath(raw_path) ? raw_path : text2path("[raw_path]") + if(!item_path) + continue + var/list/source_paint = data["item_paint"][raw_path] + if(islist(source_paint)) + item_paint[item_path] = source_paint.Copy() + + sync_external_grants() + for(var/item_path in get_all_item_paths()) + normalize_loadout(item_path) + + return TRUE + +/proc/tat_role_text_matches_pliant(value) + if(isnull(value)) + return FALSE + var/text = lowertext("[value]") + if(findtext(text, "pliant")) + return TRUE + // TAT roundstart roles may be stored as SQL buckets or job titles rather than + // literally containing "Pliant". Treat those as TAT-managed loadout roles too. + if(text == lowertext(TAT_SQL_ROLE_TOWNER)) + return TRUE + if(text == lowertext(TAT_SQL_ROLE_TRADER)) + return TRUE + if(text == lowertext(TAT_SQL_ROLE_ADVENTURER)) + return TRUE + if(text == lowertext(TAT_SQL_ROLE_WRETCH)) + return TRUE + if(findtext(text, "tat ")) + return TRUE + if(findtext(text, "tat_")) + return TRUE + return FALSE + +/proc/tat_is_pliant_roundstart_character(mob/living/carbon/human/character) + if(!character) + return FALSE + + if(character.tat_handles_preference_loadout) + return TRUE + + if(tat_role_text_matches_pliant(character.tat_pliant_title)) + return TRUE + + if(tat_role_text_matches_pliant(character.advjob)) + return TRUE + + var/assigned_role = character.mind?.assigned_role + if(tat_role_text_matches_pliant(assigned_role)) + return TRUE + + var/datum/job/assigned_job = SSjob.GetJob(assigned_role) + if(assigned_job) + if(tat_role_text_matches_pliant("[assigned_job.type]")) + return TRUE + if(tat_role_text_matches_pliant(assigned_job.title)) + return TRUE + + return FALSE diff --git a/modular_twilight_axis/code/datums/tat_system/domains/tat_party_leader.dm b/modular_twilight_axis/code/datums/tat_system/domains/tat_party_leader.dm new file mode 100644 index 00000000000..18567e7e4f6 --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/domains/tat_party_leader.dm @@ -0,0 +1,169 @@ +/datum/component/tat_party_leader + var/mob/living/carbon/human/leader + var/list/applied_bonuses = list() + var/refresh_queued = FALSE + +/datum/component/tat_party_leader/Initialize() + . = ..() + if(!ishuman(parent)) + return COMPONENT_INCOMPATIBLE + leader = parent + queue_refresh() + return + +/datum/component/tat_party_leader/Destroy(force) + clear_all_bonuses() + leader = null + applied_bonuses = null + return ..() + +/datum/component/tat_party_leader/proc/queue_refresh() + if(refresh_queued || QDELETED(src)) + return + refresh_queued = TRUE + addtimer(CALLBACK(src, PROC_REF(refresh_bonus)), TAT_PARTY_LEADER_REFRESH_INTERVAL) + +/datum/component/tat_party_leader/proc/refresh_bonus() + refresh_queued = FALSE + if(QDELETED(src)) + return + if(!leader || QDELETED(leader)) + clear_all_bonuses() + return + + var/list/desired_bonuses = build_desired_bonuses() + reconcile_bonuses(desired_bonuses) + queue_refresh() + +/datum/component/tat_party_leader/proc/build_desired_bonuses() + var/list/desired = list() + if(!can_leader_project_aura()) + return desired + + var/datum/fellowship/fellowship = leader.current_fellowship + if(!fellowship || fellowship.get_leader() != leader) + return desired + + var/list/nearby_members = list() + for(var/mob/living/carbon/human/member as anything in fellowship.get_members()) + if(member == leader) + continue + if(!can_receive_fellowship_bonus(member)) + continue + nearby_members += member + desired[member] = list(STATKEY_CON = TAT_PARTY_LEADER_MEMBER_CON) + + if(length(nearby_members)) + desired[leader] = list( + STATKEY_CON = TAT_PARTY_LEADER_BONUS_CON, + STATKEY_WIL = TAT_PARTY_LEADER_BONUS_WIL, + STATKEY_LCK = TAT_PARTY_LEADER_LUCK_PER_MEMBER * length(nearby_members), + ) + + return desired + +/datum/component/tat_party_leader/proc/can_leader_project_aura() + if(!leader || QDELETED(leader)) + return FALSE + if(leader.stat == DEAD) + return FALSE + if(!leader.current_fellowship) + return FALSE + return TRUE + +/datum/component/tat_party_leader/proc/can_receive_fellowship_bonus(mob/living/carbon/human/member) + if(!member || QDELETED(member)) + return FALSE + if(member.stat == DEAD) + return FALSE + if(member.z != leader.z) + return FALSE + return get_dist(leader, member) <= TAT_PARTY_LEADER_AURA_RANGE + +/datum/component/tat_party_leader/proc/reconcile_bonuses(list/desired_bonuses) + if(!applied_bonuses) + applied_bonuses = list() + + for(var/mob/living/carbon/human/target as anything in applied_bonuses.Copy()) + if(!(target in desired_bonuses)) + remove_bonus_set(target, applied_bonuses[target]) + applied_bonuses -= target + continue + var/list/old_stats = applied_bonuses[target] + var/list/new_stats = desired_bonuses[target] + reconcile_bonus_set(target, old_stats, new_stats) + applied_bonuses[target] = new_stats.Copy() + + for(var/mob/living/carbon/human/target as anything in desired_bonuses) + if(target in applied_bonuses) + continue + var/list/new_stats = desired_bonuses[target] + apply_bonus_set(target, new_stats) + applied_bonuses[target] = new_stats.Copy() + +/datum/component/tat_party_leader/proc/reconcile_bonus_set(mob/living/carbon/human/target, list/old_stats, list/new_stats) + if(!target || QDELETED(target)) + return + for(var/stat_id in old_stats) + var/old_delta = old_stats[stat_id] + var/new_delta = new_stats[stat_id] + if(!new_delta) + target.change_stat(stat_id, -old_delta) + continue + var/diff = new_delta - old_delta + if(diff) + target.change_stat(stat_id, diff) + for(var/stat_id in new_stats) + if(stat_id in old_stats) + continue + var/new_delta = new_stats[stat_id] + if(new_delta) + target.change_stat(stat_id, new_delta) + +/datum/component/tat_party_leader/proc/apply_bonus_set(mob/living/carbon/human/target, list/stats) + if(!target || QDELETED(target)) + return + for(var/stat_id in stats) + var/delta = stats[stat_id] + if(delta) + target.change_stat(stat_id, delta) + +/datum/component/tat_party_leader/proc/remove_bonus_set(mob/living/carbon/human/target, list/stats) + if(!target || QDELETED(target)) + return + for(var/stat_id in stats) + var/delta = stats[stat_id] + if(delta) + target.change_stat(stat_id, -delta) + +/datum/component/tat_party_leader/proc/clear_all_bonuses() + if(!applied_bonuses) + return + for(var/mob/living/carbon/human/target as anything in applied_bonuses.Copy()) + remove_bonus_set(target, applied_bonuses[target]) + applied_bonuses.Cut() + +/proc/tat_try_fellowship_headpat_mood(mob/living/carbon/human/patter, mob/living/carbon/human/target) + if(!patter || !target || QDELETED(patter) || QDELETED(target)) + return FALSE + var/datum/fellowship/fellowship = patter.current_fellowship + if(!fellowship || fellowship.get_leader() != patter || !fellowship.has_member(target)) + return FALSE + if(target == patter) + return FALSE + if(target.z != patter.z || get_dist(patter, target) > 1) + return FALSE + if(target.stat == DEAD) + return FALSE + target.add_stress(/datum/stressevent/fellowship_headpat) + return TRUE + +/datum/stressevent/fellowship_headpat + timer = 5 MINUTES + stressadd = -1 + desc = span_green("My leader's reassurance steadies me.") + + +/datum/emote/living/pat/adjacentaction(mob/user, mob/target) + . = ..() + tat_try_fellowship_headpat_mood(user, target) diff --git a/modular_twilight_axis/code/datums/tat_system/domains/tat_skills.dm b/modular_twilight_axis/code/datums/tat_system/domains/tat_skills.dm new file mode 100644 index 00000000000..3497c354db5 --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/domains/tat_skills.dm @@ -0,0 +1,676 @@ +/datum/tat_skills + var/datum/tat_build/owner_build + var/list/invested = list() + var/list/bonus = list() + var/list/domain_points = list() + var/skill_point_conversion_pool = 0 + var/list/spent_points_cache = list() + var/_cached_combat_expert_count = -1 + var/_cached_combat_master_count = -1 + +/datum/tat_skills/proc/invalidate_combat_count_cache() + _cached_combat_expert_count = -1 + _cached_combat_master_count = -1 + +/datum/tat_skills/New(datum/tat_build/B) + . = ..() + owner_build = B + reset() + +/datum/tat_skills/proc/reset() + invested = list() + bonus = list() + invalidate_combat_count_cache() + invalidate_spent_points_cache() + + var/list/default_domain_points = TAT_DEFAULT_SKILL_DOMAIN_POINTS + domain_points = default_domain_points.Copy() + skill_point_conversion_pool = 0 + + return TRUE + +/datum/tat_skills/proc/invalidate_spent_points_cache() + spent_points_cache = list() + return TRUE + +/datum/tat_skills/proc/get_domain(skill_type) + return tat_get_skill_domain(skill_type) + +/datum/tat_skills/proc/normalize_skill_domain(domain) + if(domain == TAT_SKILL_DOMAIN_COMBAT) + return TAT_SKILL_DOMAIN_COMBAT + if(domain == TAT_SKILL_DOMAIN_WANDERING) + return TAT_SKILL_DOMAIN_WANDERING + if(domain == TAT_SKILL_DOMAIN_GATHERING) + return TAT_SKILL_DOMAIN_GATHERING + if(domain == TAT_SKILL_DOMAIN_CRAFTING) + return TAT_SKILL_DOMAIN_CRAFTING + if(domain == TAT_SKILL_DOMAIN_MISC) + return TAT_SKILL_DOMAIN_MISC + return null + +/datum/tat_skills/proc/can_give_skill_domain_points(domain, amount = 1) + domain = normalize_skill_domain(domain) + if(!domain) + return FALSE + amount = max(1, round(amount || 1)) + if(round(domain_points[domain] || 0) < amount) + return FALSE + return get_remaining_points(domain) >= amount + +/datum/tat_skills/proc/can_take_skill_domain_points(domain, amount = 1) + domain = normalize_skill_domain(domain) + if(!domain || domain == TAT_SKILL_DOMAIN_COMBAT) + return FALSE + amount = max(1, round(amount || 1)) + return skill_point_conversion_pool >= amount + +/datum/tat_skills/proc/give_skill_domain_points(domain, amount = 1) + domain = normalize_skill_domain(domain) + if(!domain) + return FALSE + amount = max(1, round(amount || 1)) + if(!can_give_skill_domain_points(domain, amount)) + return FALSE + domain_points[domain] = round(domain_points[domain] || 0) - amount + skill_point_conversion_pool += amount + invalidate_spent_points_cache() + owner_build?.set_dirty() + return TRUE + +/datum/tat_skills/proc/take_skill_domain_points(domain, amount = 1) + domain = normalize_skill_domain(domain) + if(!domain) + return FALSE + amount = max(1, round(amount || 1)) + if(!can_take_skill_domain_points(domain, amount)) + return FALSE + domain_points[domain] = round(domain_points[domain] || 0) + amount + skill_point_conversion_pool -= amount + invalidate_spent_points_cache() + owner_build?.set_dirty() + return TRUE + +/datum/tat_skills/proc/build_skill_conversion_state() + var/list/result = list() + for(var/domain in list(TAT_SKILL_DOMAIN_COMBAT, TAT_SKILL_DOMAIN_WANDERING, TAT_SKILL_DOMAIN_GATHERING, TAT_SKILL_DOMAIN_CRAFTING, TAT_SKILL_DOMAIN_MISC)) + result[domain] = list( + "can_give" = can_give_skill_domain_points(domain), + "can_take" = can_take_skill_domain_points(domain), + ) + return result + + +/datum/tat_skills/proc/get_invested_value(skill_type) + return round(invested[skill_type] || 0) + +/datum/tat_skills/proc/get_bonus_value(skill_type) + if(!check_skill(skill_type)) + return 0 + if(owner_build) + return round(owner_build.get_bonus_skill_value(skill_type) || 0) + return round(bonus[skill_type] || 0) + +/datum/tat_skills/proc/virtue_matches_rule(virtue_entry, virtue_rule) + if(!virtue_entry || !virtue_rule) + return FALSE + if(ispath(virtue_entry)) + return virtue_entry == virtue_rule || ispath(virtue_entry, virtue_rule) + if(istype(virtue_entry, /datum/virtue)) + return istype(virtue_entry, virtue_rule) + return virtue_entry == virtue_rule + +/datum/tat_skills/proc/add_virtue_rule_value(skill_type, list/rules, list/virtues) + var/total = 0 + if(!islist(rules) || !islist(virtues) || !length(virtues)) + return 0 + + for(var/virtue_entry in virtues) + for(var/virtue_rule in rules) + if(!virtue_matches_rule(virtue_entry, virtue_rule)) + continue + + var/list/skill_map = rules[virtue_rule] + if(islist(skill_map)) + total += round(skill_map[skill_type] || 0) + + return total + +/datum/tat_skills/proc/add_virtue_choice_rule_value(skill_type, list/rules, list/virtues) + var/total = 0 + if(!islist(rules) || !islist(virtues) || !length(virtues)) + return 0 + + for(var/virtue_entry in virtues) + if(!istype(virtue_entry, /datum/virtue)) + continue + var/datum/virtue/virtue_datum = virtue_entry + if(!LAZYLEN(virtue_datum.picked_choices)) + continue + + for(var/virtue_rule in rules) + if(!virtue_matches_rule(virtue_datum, virtue_rule)) + continue + + var/list/choice_map = rules[virtue_rule] + if(!islist(choice_map)) + continue + + for(var/choice in virtue_datum.picked_choices) + var/list/skill_map = choice_map[choice] + if(islist(skill_map)) + total += round(skill_map[skill_type] || 0) + + return total + +/datum/tat_skills/proc/get_virtue_bonus_value(skill_type) + var/list/virtues = owner_build?.get_active_virtues() + if(!length(virtues)) + return 0 + return add_virtue_rule_value(skill_type, GLOB.tat_virtue_skill_bonus_rules, virtues) + add_virtue_choice_rule_value(skill_type, GLOB.tat_virtue_choice_skill_bonus_rules, virtues) + +/datum/tat_skills/proc/get_virtue_skill_cap_bonus(skill_type) + var/list/virtues = owner_build?.get_active_virtues() + if(!length(virtues)) + return 0 + return add_virtue_rule_value(skill_type, GLOB.tat_virtue_skill_cap_bonus_rules, virtues) + add_virtue_choice_rule_value(skill_type, GLOB.tat_virtue_choice_skill_cap_bonus_rules, virtues) + +/datum/tat_skills/proc/rebuild_bonus_values() + bonus = list() + invalidate_spent_points_cache() + + for(var/skill_type in TAT_SKILLS_ALL) + var/value = owner_build ? owner_build.get_bonus_skill_value(skill_type) : 0 + if(value > 0) + bonus[skill_type] = round(value) + + return TRUE + +/datum/tat_skills/proc/check_skill(skill_type) + return !!get_domain(skill_type) + +/datum/tat_skills/proc/get_total_maximum(domain) + return round((domain_points[domain] || 0) + (owner_build ? owner_build.get_bonus_skill_domain_points(domain) : 0)) + +/datum/tat_skills/proc/get_combat_expert_count(except_skill_type = null) + if(!except_skill_type && _cached_combat_expert_count >= 0) + return _cached_combat_expert_count + + var/count = 0 + for(var/skill_type in TAT_SKILLS_COMBAT) + if(skill_type == except_skill_type) + continue + if(ispath(skill_type, /datum/skill/combat/firearms)) + continue + if(get_raw_total_value(skill_type) >= TAT_SKILL_COMBAT_CAP_TRAIT_EXPERT) + count++ + + if(!except_skill_type) + _cached_combat_expert_count = count + return count + +/datum/tat_skills/proc/get_combat_master_count(except_skill_type = null) + if(!except_skill_type && _cached_combat_master_count >= 0) + return _cached_combat_master_count + + var/count = 0 + for(var/skill_type in TAT_SKILLS_COMBAT) + if(skill_type == except_skill_type) + continue + if(ispath(skill_type, /datum/skill/combat/firearms)) + continue + if(get_raw_total_value(skill_type) >= TAT_SKILL_COMBAT_CAP_TRAIT_MASTER) + count++ + + if(!except_skill_type) + _cached_combat_master_count = count + return count + +/datum/tat_skills/proc/get_raw_total_value(skill_type, invested_override = null) + var/invested_value = isnull(invested_override) ? get_invested_value(skill_type) : max(0, round(invested_override)) + return invested_value + get_bonus_value(skill_type) + +/datum/tat_skills/proc/is_limited_combat_skill(skill_type) + if(!ispath(skill_type, /datum/skill/combat)) + return FALSE + if(ispath(skill_type, /datum/skill/combat/firearms)) + return FALSE + return TRUE + +/datum/tat_skills/proc/get_hypothetical_combat_threshold_count(threshold, changed_skill_type = null, changed_invested_value = null) + var/count = 0 + for(var/skill_type in TAT_SKILLS_COMBAT) + if(!is_limited_combat_skill(skill_type)) + continue + + var/invested_override = null + if(skill_type == changed_skill_type) + invested_override = changed_invested_value + + if(get_raw_total_value(skill_type, invested_override) >= threshold) + count++ + + return count + +/datum/tat_skills/proc/would_violate_combat_hardcaps(skill_type, invested_value) + if(!is_limited_combat_skill(skill_type)) + return FALSE + + var/expert_count = get_hypothetical_combat_threshold_count(TAT_SKILL_COMBAT_CAP_TRAIT_EXPERT, skill_type, invested_value) + if(expert_count > TAT_COMBAT_EXPERT_SKILL_LIMIT) + return TRUE + + var/master_count = get_hypothetical_combat_threshold_count(TAT_SKILL_COMBAT_CAP_TRAIT_MASTER, skill_type, invested_value) + if(master_count > TAT_COMBAT_MASTER_SKILL_LIMIT) + return TRUE + + return FALSE + +/datum/tat_skills/proc/get_combat_threshold_overflow_skill(threshold) + var/limit = (threshold >= TAT_SKILL_COMBAT_CAP_TRAIT_MASTER) ? TAT_COMBAT_MASTER_SKILL_LIMIT : TAT_COMBAT_EXPERT_SKILL_LIMIT + if(get_hypothetical_combat_threshold_count(threshold) <= limit) + return null + + var/best_skill = null + var/best_score = -999999999 + for(var/skill_type in TAT_SKILLS_COMBAT) + if(!is_limited_combat_skill(skill_type)) + continue + + var/invested_value = get_invested_value(skill_type) + if(invested_value <= 0) + continue + + var/total_value = get_raw_total_value(skill_type) + if(total_value < threshold) + continue + + // Prefer removing the point that actually drops the skill below the overflowing threshold. + // Bonus-only skills still count against the quota, but they cannot be fixed by stripping TAT points. + var/drops_below_threshold = (get_raw_total_value(skill_type, invested_value - 1) < threshold) + var/score = 0 + if(drops_below_threshold) + score += 10000 + score += get_bonus_value(skill_type) * 100 + score += invested_value + + if(score > best_score) + best_score = score + best_skill = skill_type + + return best_skill + +/datum/tat_skills/proc/enforce_combat_hardcaps() + var/changed = FALSE + + while(get_hypothetical_combat_threshold_count(TAT_SKILL_COMBAT_CAP_TRAIT_MASTER) > TAT_COMBAT_MASTER_SKILL_LIMIT) + var/skill_type = get_combat_threshold_overflow_skill(TAT_SKILL_COMBAT_CAP_TRAIT_MASTER) + if(!skill_type) + break + var/current = get_invested_value(skill_type) + if(current <= 0) + break + invested[skill_type] = current - 1 + if(invested[skill_type] <= 0) + invested -= skill_type + changed = TRUE + invalidate_combat_count_cache() + invalidate_spent_points_cache() + + while(get_hypothetical_combat_threshold_count(TAT_SKILL_COMBAT_CAP_TRAIT_EXPERT) > TAT_COMBAT_EXPERT_SKILL_LIMIT) + var/skill_type = get_combat_threshold_overflow_skill(TAT_SKILL_COMBAT_CAP_TRAIT_EXPERT) + if(!skill_type) + break + var/current = get_invested_value(skill_type) + if(current <= 0) + break + invested[skill_type] = current - 1 + if(invested[skill_type] <= 0) + invested -= skill_type + changed = TRUE + invalidate_combat_count_cache() + invalidate_spent_points_cache() + + if(changed) + owner_build?.set_dirty() + + return changed + +/datum/tat_skills/proc/get_trait_cap_bonus(skill_type) + return owner_build ? owner_build.get_skill_cap_bonus_value(skill_type) : 0 + +/datum/tat_skills/proc/skill_has_trait_cap_rule(skill_type) + var/list/rules = GLOB.tat_trait_skill_cap_bonus_rules + + for(var/trait_id in rules) + var/list/skill_map = rules[trait_id] + if(!islist(skill_map)) + continue + + if(skill_type in skill_map) + return TRUE + + return FALSE + +/datum/tat_skills/proc/get_firearms_skill_cap(skill_type) + var/cap = TAT_SKILL_NONCOMBAT_CAP_UNTRAITED + + if(owner_build?.has_trait(TAT_TRAIT_WARRIOR_MASTER)) + cap = TAT_SKILL_COMBAT_CAP_TRAIT_MASTER + else if(owner_build?.has_trait(TAT_TRAIT_WARRIOR_EXPERT)) + cap = TAT_SKILL_COMBAT_CAP_TRAIT_EXPERT + + return clamp(cap, 0, TAT_SKILL_NONCOMBAT_CAP_ABSOLUTE) + +/datum/tat_skills/proc/get_combat_skill_cap(skill_type) + if(!ispath(skill_type, /datum/skill/combat)) + return TAT_SKILL_NONCOMBAT_CAP_BASIC_SYSTEM + + if(ispath(skill_type, /datum/skill/combat/firearms)) + return get_firearms_skill_cap(skill_type) + + var/base_cap = TAT_SKILL_COMBAT_CAP_DEFAULT + var/expert_cap = TAT_SKILL_COMBAT_CAP_TRAIT_EXPERT + var/master_cap = TAT_SKILL_COMBAT_CAP_TRAIT_MASTER + + var/has_expert = !!owner_build?.has_trait(TAT_TRAIT_WARRIOR_EXPERT) + var/has_master = !!owner_build?.has_trait(TAT_TRAIT_WARRIOR_MASTER) + + var/current_invested = get_invested_value(skill_type) + var/bonus_value = get_bonus_value(skill_type) + + var/cap = base_cap + + if(has_expert) + var/expert_invested_target = max(current_invested, expert_cap - bonus_value) + if(expert_invested_target >= 0 && get_raw_total_value(skill_type, expert_invested_target) >= expert_cap && !would_violate_combat_hardcaps(skill_type, expert_invested_target)) + cap = expert_cap + + if(has_master && cap >= expert_cap) + var/master_invested_target = max(current_invested, master_cap - bonus_value) + if(master_invested_target >= 0 && get_raw_total_value(skill_type, master_invested_target) >= master_cap && !would_violate_combat_hardcaps(skill_type, master_invested_target)) + cap = master_cap + + var/cap_bonus = get_trait_cap_bonus(skill_type) + if(cap_bonus > 0) + cap = max(cap, base_cap + cap_bonus) + + return clamp(cap, 0, TAT_SKILL_NONCOMBAT_CAP_ABSOLUTE) + +/datum/tat_skills/proc/get_magic_skill_cap(skill_type) + var/cap = 0 + + if(skill_type == /datum/skill/magic/arcane) + if(owner_build?.has_trait(TRAIT_ARCYNE)) + cap = 6 + + else if(skill_type == /datum/skill/magic/holy) + if(owner_build?.has_trait(TAT_TRAIT_DIVINE_BOON_3)) + cap = 6 + else if(owner_build?.has_trait(TAT_TRAIT_DIVINE_BOON_2)) + cap = 5 + else if(owner_build?.has_trait(TAT_TRAIT_DIVINE_BOON_1)) + cap = 3 + else if(owner_build?.has_trait(TAT_TRAIT_DIVINE_INITIATE)) + cap = 1 + + var/cap_bonus = get_trait_cap_bonus(skill_type) + get_virtue_skill_cap_bonus(skill_type) + if(cap_bonus > 0) + if(cap > 0) + cap += cap_bonus + else + cap = cap_bonus + + return clamp(cap, 0, TAT_SKILL_NONCOMBAT_CAP_ABSOLUTE) + +/datum/tat_skills/proc/get_noncombat_skill_cap(skill_type) + var/base_cap = TAT_SKILL_NONCOMBAT_CAP_BASIC_SYSTEM + + if(skill_has_trait_cap_rule(skill_type)) + base_cap = TAT_SKILL_NONCOMBAT_CAP_UNTRAITED + + var/cap = base_cap + get_trait_cap_bonus(skill_type) + return clamp(cap, 0, TAT_SKILL_NONCOMBAT_CAP_ABSOLUTE) + +/datum/tat_skills/proc/get_maximum(skill_type) + if(!check_skill(skill_type)) + return 0 + + if(ispath(skill_type, /datum/skill/magic)) + return get_magic_skill_cap(skill_type) + + if(ispath(skill_type, /datum/skill/combat)) + return get_combat_skill_cap(skill_type) + + return get_noncombat_skill_cap(skill_type) + +/datum/tat_skills/proc/get_invested_maximum(skill_type) + var/domain = get_domain(skill_type) + if(!domain) + return 0 + + return max(0, get_maximum(skill_type) - get_bonus_value(skill_type)) + +/datum/tat_skills/proc/get_total_value(skill_type) + return clamp(get_invested_value(skill_type) + get_bonus_value(skill_type), 0, get_maximum(skill_type)) + +/datum/tat_skills/proc/get_step_cost(skill_type, target_level) + if(target_level <= 0) + return 0 + if(target_level > get_invested_maximum(skill_type)) + return 0 + + var/discount = owner_build ? owner_build.get_skill_cost_discount(skill_type, target_level) : 0 + return max(1, target_level - discount) + +/datum/tat_skills/proc/get_total_cost_for_level(skill_type, level) + var/total = 0 + + for(var/i in 1 to level) + total += get_step_cost(skill_type, i) + + return total + +/datum/tat_skills/proc/get_spent_points(domain) + if(domain in spent_points_cache) + return spent_points_cache[domain] + + var/total = 0 + for(var/skill_type in invested) + if(get_domain(skill_type) != domain) + continue + total += get_total_cost_for_level(skill_type, get_invested_value(skill_type)) + + spent_points_cache[domain] = total + return total + +/datum/tat_skills/proc/get_remaining_points(domain) + return get_total_maximum(domain) - get_spent_points(domain) + +/datum/tat_skills/proc/get_any_negative_remaining() + for(var/domain in domain_points) + if(get_remaining_points(domain) < 0) + return TRUE + + return FALSE + +/datum/tat_skills/proc/set_invested_value(skill_type, value, ignore_budget = FALSE) + var/domain = get_domain(skill_type) + if(!domain) + return FALSE + + value = round(value) + value = max(0, value) + + var/invested_cap = get_invested_maximum(skill_type) + + if(value > invested_cap) + value = invested_cap + + var/old_value = get_invested_value(skill_type) + if(value == old_value) + return TRUE + + if(value > old_value && would_violate_combat_hardcaps(skill_type, value)) + return FALSE + + var/old_cost = get_total_cost_for_level(skill_type, old_value) + var/new_cost = get_total_cost_for_level(skill_type, value) + + var/current_domain_spent = get_spent_points(domain) + var/new_domain_spent = current_domain_spent - old_cost + new_cost + var/domain_max = get_total_maximum(domain) + + if(!ignore_budget && new_domain_spent > domain_max) + return FALSE + + if(value <= 0) + invested -= skill_type + else + invested[skill_type] = value + + invalidate_combat_count_cache() + invalidate_spent_points_cache() + owner_build?.set_dirty() + return TRUE + +/datum/tat_skills/proc/refresh_after_trait_change() + return sanitize(FALSE) + +/datum/tat_skills/proc/sanitize(enforce_budget = TRUE) + invalidate_combat_count_cache() + invalidate_spent_points_cache() + rebuild_bonus_values() + + for(var/skill_type in invested.Copy()) + if(!check_skill(skill_type)) + invested -= skill_type + continue + + var/current = get_invested_value(skill_type) + set_invested_value(skill_type, current) + + enforce_combat_hardcaps() + + if(!enforce_budget) + return TRUE + + for(var/domain in domain_points) + while(get_remaining_points(domain) < 0) + var/changed = FALSE + + for(var/skill_type in invested.Copy()) + if(get_domain(skill_type) != domain) + continue + + var/current = get_invested_value(skill_type) + if(current <= 0) + continue + + if(set_invested_value(skill_type, current - 1)) + changed = TRUE + if(get_remaining_points(domain) >= 0) + break + + if(!changed) + break + + return TRUE + +/datum/tat_skills/proc/apply_to_human(mob/living/carbon/human/H) + if(!H) + return FALSE + + for(var/skill_type in TAT_SKILLS_ALL) + var/level = get_total_value(skill_type) + if(level > 0) + H.adjust_skillrank_up_to(skill_type, level, TRUE) + + return TRUE + +/datum/tat_skills/proc/disable_from_human(mob/living/carbon/human/H) + return TRUE + +/datum/tat_skills/proc/export_to_list() + return list( + "invested" = invested.Copy(), + "bonus" = bonus.Copy(), + "domain_points" = domain_points.Copy(), + "skill_point_conversion_pool" = skill_point_conversion_pool, + ) + +/datum/tat_skills/proc/import_from_list(list/data) + reset() + + if(!islist(data)) + return FALSE + + if(islist(data["domain_points"])) + var/list/imported_domains = data["domain_points"] + for(var/domain in imported_domains) + var/normalized_domain = normalize_skill_domain(domain) + if(normalized_domain) + domain_points[normalized_domain] = max(0, round(text2num("[imported_domains[domain]]") || 0)) + var/raw_conversion_pool = data["skill_point_conversion_pool"] + skill_point_conversion_pool = max(0, round(text2num("[raw_conversion_pool]") || 0)) + + var/list/imported_invested = null + if(islist(data["invested"])) + imported_invested = data["invested"] + else + imported_invested = data + + for(var/skill_type in imported_invested) + if(skill_type == "bonus") + continue + if(skill_type == "invested") + continue + set_invested_value(skill_type, imported_invested[skill_type]) + + rebuild_bonus_values() + sanitize() + return TRUE + +/datum/tat_skills/proc/export_to_json_list() + var/list/exported_invested = list() + for(var/skill_type in invested) + var/value = get_invested_value(skill_type) + if(value > 0) + exported_invested["[skill_type]"] = value + return list( + "invested" = exported_invested, + "domain_points" = domain_points.Copy(), + "skill_point_conversion_pool" = skill_point_conversion_pool, + ) + +/datum/tat_skills/proc/import_from_json_list(list/data) + reset() + if(!islist(data)) + return FALSE + + if(islist(data["domain_points"])) + var/list/imported_domains = data["domain_points"] + for(var/domain in imported_domains) + var/normalized_domain = normalize_skill_domain(domain) + if(normalized_domain) + domain_points[normalized_domain] = max(0, round(text2num("[imported_domains[domain]]") || 0)) + var/raw_conversion_pool = data["skill_point_conversion_pool"] + skill_point_conversion_pool = max(0, round(text2num("[raw_conversion_pool]") || 0)) + + var/list/imported_invested = null + if(islist(data["invested"])) + imported_invested = data["invested"] + else + imported_invested = data + + for(var/raw_path in imported_invested) + if(raw_path == "bonus" || raw_path == "invested") + continue + var/skill_type = ispath(raw_path) ? raw_path : text2path("[raw_path]") + if(!skill_type) + continue + set_invested_value(skill_type, text2num("[imported_invested[raw_path]]")) + + rebuild_bonus_values() + sanitize() + return TRUE diff --git a/modular_twilight_axis/code/datums/tat_system/domains/tat_stats.dm b/modular_twilight_axis/code/datums/tat_system/domains/tat_stats.dm new file mode 100644 index 00000000000..ef0fef2a36b --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/domains/tat_stats.dm @@ -0,0 +1,154 @@ +/datum/tat_stats + var/datum/tat_build/owner_build + var/list/values = list() + var/base_points = TAT_BASIC_STAT_POINTS + +/datum/tat_stats/New(datum/tat_build/B) + . = ..() + owner_build = B + +/datum/tat_stats/proc/reset() + values = list() + return TRUE + +/datum/tat_stats/proc/get_entry(stat_id) + var/list/all = list(TAT_AVAILABLE_STATS_LIST) + if(!(stat_id in all)) + return null + return all[stat_id] + +/datum/tat_stats/proc/get_base(stat_id) + var/list/entry = get_entry(stat_id) + if(!islist(entry)) + return 10 + return isnum(entry["base"]) ? entry["base"] : 10 + +/datum/tat_stats/proc/get_minimum(stat_id) + var/list/entry = get_entry(stat_id) + if(!islist(entry)) + return 1 + return isnum(entry["min"]) ? entry["min"] : 1 + +/datum/tat_stats/proc/get_hard_minimum(stat_id) + return 1 + +/datum/tat_stats/proc/get_maximum(stat_id) + var/list/entry = get_entry(stat_id) + if(!islist(entry)) + return 20 + return isnum(entry["max"]) ? entry["max"] : 20 + +/datum/tat_stats/proc/get_cost(stat_id) + var/list/entry = get_entry(stat_id) + if(!islist(entry)) + return 0 + return isnum(entry["cost"]) ? entry["cost"] : 0 + +/datum/tat_stats/proc/get_total_maximum() + return base_points + (owner_build ? owner_build.get_bonus_stat_points() : 0) + +/datum/tat_stats/proc/get_value(stat_id) + if(stat_id in values) + return values[stat_id] + return get_base(stat_id) + +/datum/tat_stats/proc/set_value(stat_id, value, ignore_budget = FALSE) + if(!islist(get_entry(stat_id))) + return FALSE + value = round(value) + value = clamp(value, get_hard_minimum(stat_id), get_maximum(stat_id)) + + var/old_value = get_value(stat_id) + if(value == old_value) + return TRUE + + if(!ignore_budget) + var/old_cost = get_point_delta_for_value(stat_id, old_value) + var/new_cost = get_point_delta_for_value(stat_id, value) + var/new_spent = get_spent_points() - old_cost + new_cost + if(new_spent > get_total_maximum()) + return FALSE + + if(value == get_base(stat_id)) + values -= stat_id + else + values[stat_id] = value + owner_build?.set_dirty() + return TRUE + +/datum/tat_stats/proc/get_point_delta_for_value(stat_id, value) + var/base = get_base(stat_id) + var/cost = get_cost(stat_id) + var/refund_floor = get_minimum(stat_id) + + value = clamp(value, get_hard_minimum(stat_id), get_maximum(stat_id)) + if(value > base) + return (value - base) * cost + var/effective_value = max(value, refund_floor) + return (effective_value - base) * cost + +/datum/tat_stats/proc/get_spent_points() + var/total = 0 + var/list/order = TAT_STATS_ORDER_LIST + for(var/stat_id in order) + total += get_point_delta_for_value(stat_id, get_value(stat_id)) + return total + +/datum/tat_stats/proc/get_remaining_points() + return get_total_maximum() - get_spent_points() + +/datum/tat_stats/proc/sanitize() + var/list/order = TAT_STATS_ORDER_LIST + for(var/stat_id in values.Copy()) + if(!islist(get_entry(stat_id))) + values -= stat_id + for(var/stat_id in order) + set_value(stat_id, get_value(stat_id), TRUE) + while(get_remaining_points() < 0) + var/changed = FALSE + for(var/stat_id in order) + var/current = get_value(stat_id) + var/base = get_base(stat_id) + if(current > base) + set_value(stat_id, current - 1, TRUE) + changed = TRUE + if(get_remaining_points() >= 0) + break + if(!changed) + break + return TRUE + +/datum/tat_stats/proc/apply_to_human(mob/living/carbon/human/H) + if(!H) + return FALSE + for(var/stat_id in TAT_STATS_ORDER_LIST) + var/diff = get_value(stat_id) - get_base(stat_id) + if(diff) + H.change_stat(stat_id, diff) + return TRUE + +/datum/tat_stats/proc/disable_from_human(mob/living/carbon/human/H) + if(!H) + return FALSE + for(var/stat_id in TAT_STATS_ORDER_LIST) + var/diff = get_value(stat_id) - get_base(stat_id) + if(diff) + H.change_stat(stat_id, -diff) + return TRUE + +/datum/tat_stats/proc/export_to_list() + return values.Copy() + +/datum/tat_stats/proc/import_from_list(list/data) + values = list() + if(!islist(data)) + return FALSE + for(var/stat_id in data) + set_value(stat_id, data[stat_id], TRUE) + return TRUE + +/datum/tat_stats/proc/export_to_json_list() + return export_to_list() + +/datum/tat_stats/proc/import_from_json_list(list/data) + return import_from_list(data) diff --git a/modular_twilight_axis/code/datums/tat_system/domains/tat_trader_lootboxes.dm b/modular_twilight_axis/code/datums/tat_system/domains/tat_trader_lootboxes.dm new file mode 100644 index 00000000000..b3f34ef9298 --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/domains/tat_trader_lootboxes.dm @@ -0,0 +1,597 @@ +#define TAT_TRADER_LOOTBOX_CHEAP 1 +#define TAT_TRADER_LOOTBOX_MEDIUM 2 +#define TAT_TRADER_LOOTBOX_EXPENSIVE 3 +#define TAT_TRADER_LOOTBOX_POTION 4 +#define TAT_TRADER_LOOTBOX_CLOTHES 5 + +GLOBAL_LIST_INIT(tat_trader_lootbox_cheap_base_pool, list( + /obj/item/clothing/suit/roguetown/armor/plate/full/bronze = 4, + /obj/item/clothing/suit/roguetown/armor/plate/full/bronze/alt = 4, + /obj/item/clothing/neck/roguetown/bevor/bronze = 10, + /obj/item/clothing/neck/roguetown/gorget/bronze = 10, + /obj/item/clothing/neck/roguetown/gorget/copper = 10, + /obj/item/clothing/neck/roguetown/luckcharm = 10, + /obj/item/clothing/neck/roguetown/psicross/shell = 10, + /obj/item/clothing/neck/roguetown/psicross/shell/bracelet = 10, + /obj/item/clothing/neck/roguetown/shalal = 10, + /obj/item/storage/gadget/messkit = 13, + /obj/item/mobilestove = 13, + /obj/item/tent_kit = 11, + /obj/item/tent_kit/ger = 12, + /obj/item/tent_kit/yurt = 9, + /obj/item/folding_table_stored = 13, + /obj/item/clothing/ring/aalloy = 10, + /obj/item/clothing/ring/amber = 10, + /obj/item/clothing/ring/band = 10, + /obj/item/clothing/ring/band/aalloy = 10, + /obj/item/clothing/ring/band/bronze = 10, + /obj/item/clothing/ring/band/gold = 10, + /obj/item/clothing/ring/band/paalloy = 10, + /obj/item/clothing/ring/bronze = 10, + /obj/item/clothing/ring/rose = 10, + /obj/item/clothing/ring/shell = 10, + /obj/item/clothing/ring/silver = 10, + /obj/item/clothing/suit/roguetown/armor/chainmail/aalloy = 10, + /obj/item/clothing/suit/roguetown/armor/chainmail/hauberk/aalloy = 10, + /obj/item/clothing/suit/roguetown/armor/plate/bronze = 10, + /obj/item/clothing/suit/roguetown/armor/plate/bronze/light = 10, + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/copper = 10, + /obj/item/clothing/neck/roguetown/psicross/astrata/bronze = 12, + /obj/item/clothing/neck/roguetown/psicross/bronze = 12, + /obj/item/clothing/neck/roguetown/psicross/inhumen/bronze = 12, + /obj/item/clothing/neck/roguetown/psicross/inhumen/graggar/bronze = 12, + /obj/item/clothing/neck/roguetown/psicross/malum/bronze = 12, + /obj/item/clothing/neck/roguetown/psicross/noc/bronze = 12, + /obj/item/clothing/neck/roguetown/psicross/ravox/bronze = 12, + /obj/item/flashlight/flare/torch/lantern/bronzelamptern = 12, + /obj/item/folding_alchcauldron_stored = 5, + /obj/item/folding_alchstation_stored = 5, + /obj/item/grapplinghook = 4, + /obj/item/quiver/bolt/bronze = 9, + /obj/item/quiver/bolt/heavy/bronze = 9, + /obj/item/quiver/javelin/bronze = 5, + /obj/item/quiver/sling/bronze = 12, + /obj/item/rogueweapon/flail/bronze = 9, + /obj/item/rogueweapon/greataxe/bronze = 5, + /obj/item/rogueweapon/huntingknife/bronze = 12, + /obj/item/rogueweapon/huntingknife/combat/bronze = 9, + /obj/item/rogueweapon/katar/bronze = 9, + /obj/item/rogueweapon/katar/bronze/gladiator = 9, + /obj/item/rogueweapon/mace/bronze = 9, + /obj/item/rogueweapon/mace/warhammer/bronze = 9, + /obj/item/rogueweapon/pick/bronze = 9, + /obj/item/rogueweapon/shield/bronze = 9, + /obj/item/rogueweapon/shield/bronze/great = 5, + /obj/item/rogueweapon/spear/bronze = 9, + /obj/item/rogueweapon/spear/bronze/strapless = 9, + /obj/item/rogueweapon/spear/bronze/winged = 5, + /obj/item/rogueweapon/spear/bronze/winged/strapless = 5, + /obj/item/rogueweapon/spear/trident = 2, + /obj/item/rogueweapon/stoneaxe/woodcut/bronzebattleaxe = 9, + /obj/item/rogueweapon/sword/bronze = 9, + /obj/item/rogueweapon/sword/falchion/militia/bronze = 9, + /obj/item/rogueweapon/sword/long/broadsword/bronze = 5, + /obj/item/rogueweapon/sword/sabre/bronzekhopesh = 5, + /obj/item/rogueweapon/sword/short/gladius = 9, + /obj/item/rogueweapon/sword/short/messer/bronze = 9, + /obj/item/rogueweapon/whip/bronze = 9, + /obj/item/storage/hip/headhook/bronze = 9 +)) + +GLOBAL_LIST_INIT(tat_trader_lootbox_medium_base_pool, list( + /obj/item/clothing/neck/roguetown/bevor = 7, + /obj/item/clothing/neck/roguetown/chaincoif = 7, + /obj/item/clothing/neck/roguetown/chaincoif/chainmantle = 7, + /obj/item/clothing/neck/roguetown/chaincoif/full = 7, + /obj/item/clothing/neck/roguetown/chaincoif/iron = 7, + /obj/item/clothing/neck/roguetown/fencerguard = 7, + /obj/item/clothing/neck/roguetown/gorget/steel = 7, + /obj/item/clothing/neck/roguetown/horus = 7, + /obj/item/clothing/neck/roguetown/ornateamulet = 7, + /obj/item/clothing/neck/roguetown/ornateamulet/noble = 7, + /obj/item/clothing/neck/roguetown/psicross/g = 7, + /obj/item/clothing/neck/roguetown/psicross/malum = 7, + /obj/item/clothing/neck/roguetown/psicross/silver = 7, + /obj/item/clothing/neck/roguetown/psicross/silver/astrata = 7, + /obj/item/clothing/neck/roguetown/psicross/silver/necra = 7, + /obj/item/clothing/neck/roguetown/psicross/silver/noc = 7, + /obj/item/clothing/neck/roguetown/psicross/silver/undivided = 7, + /obj/item/clothing/neck/roguetown/skullamulet = 7, + /obj/item/clothing/neck/roguetown/talkstone = 7, + /obj/item/clothing/ring/coral = 7, + /obj/item/clothing/ring/diamond = 7, + /obj/item/clothing/ring/diamonds = 7, + /obj/item/clothing/ring/duelist = 7, + /obj/item/clothing/ring/emerald = 7, + /obj/item/clothing/ring/emeralds = 7, + /obj/item/clothing/ring/gold = 7, + /obj/item/clothing/ring/jade = 7, + /obj/item/clothing/ring/onyxa = 7, + /obj/item/clothing/ring/opal = 7, + /obj/item/clothing/ring/quartz = 7, + /obj/item/clothing/ring/quartzs = 7, + /obj/item/clothing/ring/ruby = 7, + /obj/item/clothing/ring/rubys = 7, + /obj/item/clothing/ring/sapphire = 7, + /obj/item/clothing/ring/sapphires = 7, + /obj/item/clothing/ring/signet = 7, + /obj/item/clothing/ring/signet/silver = 7, + /obj/item/clothing/ring/silver/cleric = 7, + /obj/item/clothing/ring/topaz = 7, + /obj/item/clothing/ring/topazs = 7, + /obj/item/clothing/ring/turq = 7, + /obj/item/clothing/suit/roguetown/armor/brigandine = 7, + /obj/item/clothing/suit/roguetown/armor/brigandine/heavy = 7, + /obj/item/clothing/suit/roguetown/armor/brigandine/light = 7, + /obj/item/clothing/suit/roguetown/armor/chainmail = 7, + /obj/item/clothing/suit/roguetown/armor/chainmail/bikini = 7, + /obj/item/clothing/suit/roguetown/armor/chainmail/hauberk = 7, + /obj/item/clothing/suit/roguetown/armor/chainmail/hauberk/heavy = 7, + /obj/item/clothing/suit/roguetown/armor/chainmail/hauberk/iron = 7, + /obj/item/clothing/suit/roguetown/armor/chainmail/hauberk/iron/heavy = 7, + /obj/item/clothing/suit/roguetown/armor/chainmail/iron = 7, + /obj/item/clothing/suit/roguetown/armor/chainmail/light = 7, + /obj/item/clothing/suit/roguetown/armor/chainmail/light/fencer = 7, + /obj/item/clothing/suit/roguetown/armor/plate = 7, + /obj/item/clothing/suit/roguetown/armor/plate/bikini = 7, + /obj/item/clothing/suit/roguetown/armor/plate/cuirass = 7, + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/fencer = 7, + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/fluted = 7, + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/iron = 7, + /obj/item/clothing/suit/roguetown/armor/plate/iron = 7, + /obj/item/clothing/suit/roguetown/armor/plate/iron/banded = 7, + /obj/item/clothing/suit/roguetown/armor/plate/scale = 7, + /obj/item/clothing/gloves/roguetown/knuckles = 6, + /obj/item/gun/ballistic/revolver/grenadelauncher/crossbow = 6, + /obj/item/gun/ballistic/revolver/grenadelauncher/crossbow/heavy = 5, + /obj/item/gun/ballistic/revolver/grenadelauncher/crossbow/slurbow = 9, + /obj/item/quiver/bodkin = 6, + /obj/item/quiver/bolt/heavy/standard = 9, + /obj/item/quiver/javelin/steel = 8, + /obj/item/quiver/sling/steel = 9, + /obj/item/rogueweapon/eaglebeak = 6, + /obj/item/rogueweapon/estoc = 3, + /obj/item/rogueweapon/flail/alt = 9, + /obj/item/rogueweapon/flail/peasantwarflail = 9, + /obj/item/rogueweapon/flail/sflail = 6, + /obj/item/rogueweapon/greataxe/steel = 3, + /obj/item/rogueweapon/greataxe/steel/doublehead = 3, + /obj/item/rogueweapon/greataxe/steel/knight = 3, + /obj/item/rogueweapon/greatsword = 6, + /obj/item/rogueweapon/greatsword/grenz = 3, + /obj/item/rogueweapon/greatsword/grenz/flamberge = 3, + /obj/item/rogueweapon/halberd = 3, + /obj/item/rogueweapon/halberd/bardiche = 3, + /obj/item/rogueweapon/halberd/glaive = 1, + /obj/item/rogueweapon/hammer/steel = 9, + /obj/item/rogueweapon/handclaw = 3, + /obj/item/rogueweapon/handclaw/steel = 1, + /obj/item/rogueweapon/huntingknife/chefknife = 9, + /obj/item/rogueweapon/huntingknife/chefknife/cleaver = 9, + /obj/item/rogueweapon/huntingknife/combat/fencerguy = 9, + /obj/item/rogueweapon/huntingknife/idagger/navaja = 6, + /obj/item/rogueweapon/huntingknife/idagger/steel = 9, + /obj/item/rogueweapon/huntingknife/idagger/steel/kazengun = 9, + /obj/item/rogueweapon/huntingknife/idagger/steel/parrying = 6, + /obj/item/rogueweapon/huntingknife/scissors/steel = 9, + /obj/item/rogueweapon/katar = 9, + /obj/item/rogueweapon/katar/punchdagger = 9, + /obj/item/rogueweapon/mace/cudgel/flanged = 6, + /obj/item/rogueweapon/mace/cudgel/psy/old = 6, + /obj/item/rogueweapon/mace/cudgel/psyclassic/old = 6, + /obj/item/rogueweapon/mace/goden/kanabo = 6, + /obj/item/rogueweapon/mace/goden/steel = 6, + /obj/item/rogueweapon/mace/maul/grand = 3, + /obj/item/rogueweapon/mace/steel = 6, + /obj/item/rogueweapon/mace/steel/morningstar = 6, + /obj/item/rogueweapon/mace/warhammer/steel = 6, + /obj/item/rogueweapon/pick/steel = 6, + /obj/item/rogueweapon/scabbard/sheath/kazengun = 9, + /obj/item/rogueweapon/scabbard/sword/kazengun = 3, + /obj/item/rogueweapon/scabbard/sword/kazengun/kodachi = 9, + /obj/item/rogueweapon/shield/tower/metal = 6, + /obj/item/rogueweapon/spear/assegai = 6, + /obj/item/rogueweapon/spear/billhook = 6, + /obj/item/rogueweapon/spear/boar = 6, + /obj/item/rogueweapon/spear/naginata = 6, + /obj/item/rogueweapon/spear/partizan = 3, + /obj/item/rogueweapon/spear/psyspear/old = 6, + /obj/item/rogueweapon/stoneaxe/battle = 6, + /obj/item/rogueweapon/stoneaxe/battle/steppesman/chupa = 3, + /obj/item/rogueweapon/stoneaxe/oath = 1, + /obj/item/rogueweapon/stoneaxe/woodcut/troll = 6, + /obj/item/rogueweapon/sword/cutlass = 6, + /obj/item/rogueweapon/sword/falx = 6, + /obj/item/rogueweapon/sword/long/broadsword/steel = 6, + /obj/item/rogueweapon/sword/long/greatkhopesh = 6, + /obj/item/rogueweapon/sword/long/kriegmesser = 3, + /obj/item/rogueweapon/sword/long/kriegmesser/ssangsudo = 3, + /obj/item/rogueweapon/sword/long/oldpsysword = 6, + /obj/item/rogueweapon/sword/rapier = 6, + /obj/item/rogueweapon/sword/sabre = 6, + /obj/item/rogueweapon/sword/sabre/mulyeog = 6, + /obj/item/rogueweapon/sword/short/falchion = 6, + /obj/item/rogueweapon/sword/short/kazengun = 6, + /obj/item/rogueweapon/sword/short/messer = 9, + /obj/item/rogueweapon/sword/short/messer/alt = 9, + /obj/item/rogueweapon/woodstaff/quarterstaff/steel = 6, + /obj/item/storage/belt/rogue/leather/knifebelt/black/kazengun = 9, + /obj/item/storage/belt/rogue/leather/knifebelt/black/steel = 9, +)) + +GLOBAL_LIST_INIT(tat_trader_lootbox_expensive_base_pool, list( + /obj/item/clothing/ring/amber = 10, + /obj/item/clothing/neck/roguetown/gorget/gold = 4, + /obj/item/clothing/neck/roguetown/gorget/gold/king = 4, + /obj/item/clothing/neck/roguetown/gorget/steel/kazengun = 4, + /obj/item/clothing/neck/roguetown/psicross/bpearl = 4, + /obj/item/clothing/neck/roguetown/psicross/inhumen/g = 4, + /obj/item/clothing/ring/active/nomag = 4, + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/blacksteel = 3, + /obj/item/clothing/suit/roguetown/armor/brigandine/banneret = 4, + /obj/item/clothing/suit/roguetown/armor/brigandine/haraate = 4, + /obj/item/clothing/suit/roguetown/armor/heartfelt = 4, + /obj/item/clothing/suit/roguetown/armor/heartfelt/hand = 4, + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/fluted/gold = 4, + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/fluted/gold/heroic = 4, + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/fluted/gold/king = 4, + /obj/item/clothing/suit/roguetown/armor/plate/fluted = 4, + /obj/item/clothing/suit/roguetown/armor/plate/full = 4, + /obj/item/clothing/suit/roguetown/armor/plate/full/bikini = 4, + /obj/item/clothing/suit/roguetown/armor/plate/full/fluted = 4, + /obj/item/clothing/suit/roguetown/armor/plate/otavan = 4, + /obj/item/clothing/suit/roguetown/armor/plate/silver = 4, + /obj/item/clothing/suit/roguetown/armor/heartfelt = 4, + /obj/item/rogueweapon/mace/mushroom = 4, + /obj/item/rogueweapon/shield/tower/metal/psy = 4, + /obj/item/rogueweapon/stoneaxe/battle/ice = 4, + /obj/item/rogueweapon/sword/sabre/bane = 4, + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/fencer/psydon = 2, + /obj/item/quiver/bolt/heavy/silver = 5, + /obj/item/quiver/bolt/silver = 5, + /obj/item/quiver/silver = 5, + /obj/item/rogueweapon/flail/sflail/silver = 3, + /obj/item/rogueweapon/greataxe/silver = 3, + /obj/item/rogueweapon/greatsword/bsword/psy = 5, + /obj/item/rogueweapon/greatsword/silver = 3, + /obj/item/rogueweapon/handclaw/gronn/silver = 3, + /obj/item/rogueweapon/huntingknife/idagger/silver = 5, + /obj/item/rogueweapon/huntingknife/idagger/silver/stake = 8, + /obj/item/rogueweapon/katar/silver = 5, + /obj/item/rogueweapon/mace/cudgel/flanged/silver = 3, + /obj/item/rogueweapon/mace/goden/psymace = 3, + /obj/item/rogueweapon/mace/steel/silver = 3, + /obj/item/rogueweapon/mace/warhammer/steel/silver = 3, + /obj/item/rogueweapon/shovel/silver/preblessed = 8, + /obj/item/rogueweapon/spear/silver = 3, + /obj/item/rogueweapon/stoneaxe/woodcut/silver = 3, + /obj/item/rogueweapon/sword/long/exe/silver = 3, + /obj/item/rogueweapon/sword/long/kriegmesser/silver = 3, + /obj/item/rogueweapon/sword/long/silver = 3, + /obj/item/rogueweapon/sword/rapier/silver = 3, + /obj/item/rogueweapon/sword/short/silver = 5, + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/legacy = 2, + /obj/item/rogueweapon/sword/silver = 3, + /obj/item/rogueweapon/whip/psywhip_lesser = 5, + /obj/item/rogueweapon/whip/silver = 5, + /obj/item/rogueweapon/woodstaff/quarterstaff/silver = 5, + /obj/item/storage/belt/rogue/leather/knifebelt/black/silver = 5, + /obj/item/clothing/ring/statamythortz = 2, + /obj/item/clothing/ring/statgemerald = 2, + /obj/item/clothing/ring/statonyx = 2, + /obj/item/clothing/ring/statrontz = 2, + /obj/item/clothing/ring/statdorpel = 2, + /obj/item/reagent_containers/glass/bucket/pot/kettle/tankard/silver = 8, + /obj/item/clothing/suit/roguetown/armor/plate/full/legacy = 2, + /obj/item/clothing/suit/roguetown/armor/plate/legacy = 2, + /obj/item/clothing/suit/roguetown/armor/plate/full/fluted/legacy = 2, + /obj/item/clothing/suit/roguetown/armor/plate/aalloy = 2, +)) + +GLOBAL_LIST_INIT(tat_trader_lootbox_jackpot_pool, list( + /obj/item/clothing/head/roguetown/helmet/blacksteel/modern = 1, + /obj/item/clothing/gloves/roguetown/plate/blacksteel/modern = 3, + /obj/item/clothing/under/roguetown/platelegs/blacksteel = 2, + /obj/item/rogueweapon/greatsword/grenz/flamberge/blacksteel = 2, + /obj/item/clothing/head/roguetown/helmet/heavy/ordinatorhelm = 1, + /obj/item/clothing/shoes/roguetown/boots/armor/blacksteel/modern = 2, + /obj/item/clothing/suit/roguetown/armor/plate/blacksteel/modern = 1, + /obj/item/clothing/ring/blacksteel = 1, + /obj/item/clothing/ring/diamondbs = 1, + /obj/item/clothing/ring/dragon_ring = 1, + /obj/item/clothing/ring/emeraldbs = 1, + /obj/item/clothing/ring/quartzbs = 1, + /obj/item/clothing/ring/rubybs = 1, + /obj/item/clothing/ring/sapphirebs = 1, + /obj/item/clothing/ring/topazbs = 1, + /obj/item/clothing/shoes/roguetown/boots/armor/gold = 2, + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/fluted/ornate = 1, + /obj/item/clothing/suit/roguetown/armor/plate/fluted/ornate = 2, + /obj/item/clothing/suit/roguetown/armor/plate/full/fluted/ornate = 2, + /obj/item/clothing/suit/roguetown/armor/plate/full/fluted/ornate/ordinator = 2, + /obj/item/clothing/suit/roguetown/armor/plate/paalloy/artificer = 3, + /obj/item/reagent_containers/glass/bucket/pot/kettle/tankard/blacksteel = 5, + /obj/item/clothing/gloves/roguetown/chain/contraption/voltic = 3, + /obj/item/clothing/ring/active/shimmeringlens = 4, + /obj/item/flashlight/flare/torch/lantern/bronzelamptern/malums_lamptern = 2, + /obj/item/rogueweapon/mace/mushroom = 2, + /obj/item/rogueweapon/huntingknife/idagger/steel/fire = 3, + /obj/item/rogueweapon/mace/goden/deepduke = 3, + /obj/item/rogueweapon/stoneaxe/battle/ice = 3, + /obj/item/rogueweapon/sword/long/exe/berserk = 2, + /obj/item/rogueweapon/sword/sabre/bane = 2, + +)) + +GLOBAL_LIST_INIT(tat_trader_lootbox_potion_heal_pool, list( + /obj/item/reagent_containers/glass/bottle/rogue/healthpot = 3, + /obj/item/reagent_containers/glass/bottle/rogue/healthpotnew = 2, + /obj/item/reagent_containers/glass/bottle/rogue/manapot = 3, + /obj/item/reagent_containers/glass/bottle/rogue/strongmanapot = 2, + /obj/item/reagent_containers/glass/bottle/rogue/stampot = 3, + /obj/item/reagent_containers/glass/bottle/rogue/strongstampot = 2, + /obj/item/reagent_containers/food/snacks/grown/apple/gold = 1, + /obj/item/reagent_containers/glass/bottle/alchemical/strpot = 2, + /obj/item/reagent_containers/glass/bottle/alchemical/perpot = 2, + /obj/item/reagent_containers/glass/bottle/alchemical/conpot = 2, + /obj/item/reagent_containers/glass/bottle/alchemical/spdpot = 2, + /obj/item/reagent_containers/glass/bottle/alchemical/lucpot = 2, + /obj/item/reagent_containers/powder/ozium = 3, + /obj/item/reagent_containers/powder/moondust = 3, + /obj/item/reagent_containers/powder/moondust_purest = 3, + /obj/item/reagent_containers/powder/spice = 3, + /obj/item/reagent_containers/powder/starsugar = 3, + /obj/item/reagent_containers/powder/herozium = 3, +)) + +GLOBAL_LIST_INIT(tat_trader_lootbox_potion_poison_pool, list( + /obj/item/reagent_containers/glass/bottle/rogue/stampoison = 3, + /obj/item/reagent_containers/glass/bottle/rogue/strongpoison = 3, + /obj/item/reagent_containers/glass/bottle/rogue/poison = 1, + /obj/item/reagent_containers/glass/bottle/rogue/berrypoison = 4, +)) + +GLOBAL_LIST_INIT(tat_trader_lootbox_clothing_grenzel_pool, list( + /obj/item/clothing/gloves/roguetown/angle/grenzelgloves = 2, + /obj/item/clothing/shoes/roguetown/grenzelhoft = 2, + /obj/item/clothing/under/roguetown/heavy_leather_pants/grenzelpants = 2, + /obj/item/clothing/suit/roguetown/armor/gambeson/heavy/grenzelhoft = 2, + /obj/item/clothing/head/roguetown/grenzelhofthat = 2 +)) + +GLOBAL_LIST_INIT(tat_trader_lootbox_clothing_kazengun_pool, list( + /obj/item/clothing/gloves/roguetown/eastgloves1 = 2, + /obj/item/clothing/gloves/roguetown/eastgloves2= 2, + /obj/item/clothing/head/roguetown/mentorhat = 2, + /obj/item/clothing/suit/roguetown/armor/basiceast/mentorsuit = 2, + /obj/item/clothing/suit/roguetown/armor/basiceast = 2, + /obj/item/clothing/shoes/roguetown/armor/rumaclan = 2, + /obj/item/clothing/cloak/eastcloak1 = 2, + /obj/item/clothing/suit/roguetown/shirt/undershirt/eastshirt1 = 1, + /obj/item/clothing/suit/roguetown/shirt/undershirt/eastshirt2 = 1 +)) + +GLOBAL_LIST_INIT(tat_trader_lootbox_clothing_aavnr_pool, list( + /obj/item/clothing/under/roguetown/heavy_leather_pants/otavan/shepherd = 2, + /obj/item/clothing/suit/roguetown/armor/leather/heavy/shepherd = 2, + /obj/item/clothing/shoes/roguetown/grenzelhoft/freifechter = 2, + /obj/item/clothing/neck/roguetown/fencerguard = 2, + /obj/item/clothing/suit/roguetown/armor/plate/cuirass/fencer = 1, + /obj/item/clothing/shoes/roguetown/boots/nobleboot/steppesman = 2, + /obj/item/clothing/suit/roguetown/armor/leather/heavy/coat/steppe = 2, + /obj/item/clothing/suit/roguetown/shirt/freifechter = 1, + /obj/item/clothing/under/roguetown/heavy_leather_pants/otavan/generic = 2 +)) + +GLOBAL_LIST_INIT(tat_trader_lootbox_clothing_gronn_pool, list( + /obj/item/clothing/suit/roguetown/armor/leather/heavy/gronn = 2, + /obj/item/clothing/gloves/roguetown/angle/gronn = 2, + /obj/item/clothing/under/roguetown/trou/leather/gronn = 2, + /obj/item/clothing/shoes/roguetown/boots/leather/atgervi = 2, + /obj/item/clothing/gloves/roguetown/angle/atgervi = 2 +)) + +GLOBAL_LIST_INIT(tat_trader_lootbox_clothing_otava_pool, list( + /obj/item/clothing/suit/roguetown/armor/gambeson/heavy/otavan = 2, + /obj/item/clothing/gloves/roguetown/otavan = 2, + /obj/item/clothing/shoes/roguetown/boots/otavan = 2, + /obj/item/clothing/gloves/roguetown/chain/psydon = 2, + /obj/item/clothing/shoes/roguetown/boots/psydonboots = 2, + /obj/item/clothing/under/roguetown/heavy_leather_pants/otavan = 2 +)) + +/proc/tat_pick_weighted_lootbox_path(list/weighted_paths) + if(!islist(weighted_paths) || !length(weighted_paths)) + return null + + var/total_weight = 0 + for(var/item_path in weighted_paths) + var/weight = round(weighted_paths[item_path] || 0) + if(weight > 0) + total_weight += weight + + if(total_weight <= 0) + return null + + var/roll = rand(1, total_weight) + for(var/item_path in weighted_paths) + var/weight = round(weighted_paths[item_path] || 0) + if(weight <= 0) + continue + roll -= weight + if(roll <= 0) + return item_path + + return null + +/proc/tat_add_weighted_lootbox_reward(list/rewards, list/weighted_paths, amount = 1) + if(!islist(rewards) || amount <= 0) + return FALSE + + for(var/i in 1 to amount) + var/item_path = tat_pick_weighted_lootbox_path(weighted_paths) + if(item_path) + rewards += item_path + + return TRUE + +/obj/item/tat_trader_lootbox + name = "sealed trader cache" + desc = "A sealed cache of uncertain wares. Only someone with a merchant's writ should break the seal." + icon = 'icons/roguetown/misc/structure.dmi' + icon_state = "chest1" + w_class = WEIGHT_CLASS_SMALL + var/tier = TAT_TRADER_LOOTBOX_CHEAP + var/opened = FALSE + +/obj/item/tat_trader_lootbox/cheap + name = "cheap trader cache" + icon_state = "chest1" + desc = "A cheap sealed cache. Mostly trinkets, utensils, bronze pieces, and small devotional goods." + tier = TAT_TRADER_LOOTBOX_CHEAP + +/obj/item/tat_trader_lootbox/medium + name = "merchant trader cache" + icon_state = "chest3s" + desc = "A sealed merchant cache. Usually jewelry, steel-grade gear, silver devotional items, or bulk lesser goods." + tier = TAT_TRADER_LOOTBOX_MEDIUM + +/obj/item/tat_trader_lootbox/expensive + name = "luxury trader cache" + icon_state = "chestweird1" + desc = "An expensive sealed cache. It may contain silver arms, rare gear, artifacts, or relic-grade treasures." + tier = TAT_TRADER_LOOTBOX_EXPENSIVE + +/obj/item/tat_trader_lootbox/potion + name = "Alchemical trader cache" + icon = 'modular/Neu_food/icons/cookware/ration.dmi' + icon_state = "ration_large" + desc = "A paper sealed cache. Mostly Good or Bad potions." + tier = TAT_TRADER_LOOTBOX_POTION + +/obj/item/tat_trader_lootbox/clothes + name = "Sewing trader cache" + icon_state = "chestfancy_neu" + desc = "A paper sealed cache. Mostly Good or Bad potions." + tier = TAT_TRADER_LOOTBOX_CLOTHES + +/obj/item/tat_trader_lootbox/attack_self(mob/living/user) + . = ..() + if(opened) + return + if(!ishuman(user)) + return + if(!HAS_TRAIT(user, TAT_TRAIT_TRADER_LICENSE)) + to_chat(user, span_warning("You lack the merchant's writ needed to break this trader seal.")) + return + + opened = TRUE + open_lootbox(user) + qdel(src) + +/obj/item/tat_trader_lootbox/proc/open_lootbox(mob/living/carbon/human/user) + if(!user) + return FALSE + + var/list/rewards = generate_rewards() + if(!length(rewards)) + to_chat(user, span_warning("The cache breaks open, but there is nothing useful inside.")) + return FALSE + + for(var/item_path in rewards) + spawn_reward(user, item_path) + + user.visible_message(span_notice("[user] breaks the seal on [src]."), span_notice("You break the trader seal and claim the contents.")) + return TRUE + +/obj/item/tat_trader_lootbox/proc/spawn_reward(mob/living/carbon/human/user, item_path) + if(!user || !ispath(item_path)) + return FALSE + + var/obj/item/reward = new item_path(get_turf(user)) + if(!reward) + return FALSE + + if(!user.put_in_hands(reward)) + reward.forceMove(get_turf(user)) + return TRUE + +/obj/item/tat_trader_lootbox/proc/generate_rewards() + var/list/rewards = list() + + switch(tier) + if(TAT_TRADER_LOOTBOX_CHEAP) + generate_cheap_rewards(rewards) + if(TAT_TRADER_LOOTBOX_MEDIUM) + generate_medium_rewards(rewards) + if(TAT_TRADER_LOOTBOX_EXPENSIVE) + generate_expensive_rewards(rewards) + if(TAT_TRADER_LOOTBOX_POTION) + generate_potion_rewards(rewards) + if(TAT_TRADER_LOOTBOX_CLOTHES) + generate_clothes_rewards(rewards) + else + generate_cheap_rewards(rewards) + + return rewards + +/obj/item/tat_trader_lootbox/proc/generate_cheap_rewards(list/rewards) + var/roll = rand(1, 100) + + if(roll <= 85) + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_cheap_base_pool, 3) + else if(roll <= 99) + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_medium_base_pool, 2) + else + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_expensive_base_pool, 1) + + return TRUE + +/obj/item/tat_trader_lootbox/proc/generate_medium_rewards(list/rewards) + var/roll = rand(1, 100) + + if(roll <= 80) + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_medium_base_pool, 3) + else if(roll <= 95) + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_cheap_base_pool, 5) + else + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_expensive_base_pool, 2) + + return TRUE + +/obj/item/tat_trader_lootbox/proc/generate_expensive_rewards(list/rewards) + var/roll = rand(1, 100) + + if(roll <= 50) + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_medium_base_pool, 5) + else if(roll <= 90) + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_expensive_base_pool, 4) + else + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_jackpot_pool, 2) + + return TRUE + +/obj/item/tat_trader_lootbox/proc/generate_potion_rewards(list/rewards) + var/roll = rand(1, 100) + + if(roll <= 85) + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_potion_heal_pool, 6) + else + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_potion_poison_pool, 4) + + return TRUE + +/obj/item/tat_trader_lootbox/proc/generate_clothes_rewards(list/rewards) + var/roll = rand(1, 100) + + if(roll <= 20) + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_clothing_grenzel_pool, 5) + else if(roll <= 40) + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_clothing_kazengun_pool, 5) + else if(roll <= 60) + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_clothing_gronn_pool, 5) + else if(roll <= 80) + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_clothing_otava_pool, 5) + else + tat_add_weighted_lootbox_reward(rewards, GLOB.tat_trader_lootbox_clothing_aavnr_pool, 5) + return TRUE diff --git a/modular_twilight_axis/code/datums/tat_system/domains/tat_traits.dm b/modular_twilight_axis/code/datums/tat_system/domains/tat_traits.dm new file mode 100644 index 00000000000..269bc2ed6f6 --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/domains/tat_traits.dm @@ -0,0 +1,1204 @@ +/mob/living/carbon/human + var/tat_pliant_title + var/tat_handles_preference_loadout = FALSE + +/datum/tat_traits + var/datum/tat_build/owner_build + var/list/selected = list() + var/base_points = 100 + +/datum/tat_traits/New(datum/tat_build/B) + . = ..() + owner_build = B + +/datum/tat_traits/proc/reset() + selected = list() + return TRUE + +/datum/tat_traits/proc/get_entry(trait_id) + return GLOB.tat_available_traits[trait_id] + +/datum/tat_traits/proc/get_trait_count(trait_id) + var/value = selected[trait_id] + if(isnum(value)) + return max(0, round(value)) + return value ? 1 : 0 + +/datum/tat_traits/proc/has_trait(trait_id) + return get_trait_count(trait_id) > 0 + +/datum/tat_traits/proc/get_external_traits() + var/list/result = list() + var/list/virtues = owner_build?.get_active_virtues() + if(!length(virtues)) + return result + + for(var/virtue_entry in virtues) + if(!istype(virtue_entry, /datum/virtue)) + continue + + var/datum/virtue/virtue = virtue_entry + if(!("added_traits" in virtue.vars)) + continue + + var/list/added_traits = virtue.vars["added_traits"] + if(!islist(added_traits)) + continue + + for(var/trait_id in added_traits) + if(!check_trait(trait_id)) + continue + result[trait_id] = TRUE + + return result + +/datum/tat_traits/proc/get_external_trait_count(trait_id) + var/list/external_traits = get_external_traits() + return external_traits[trait_id] ? 1 : 0 + +/datum/tat_traits/proc/has_external_trait(trait_id) + return get_external_trait_count(trait_id) > 0 + +/datum/tat_traits/proc/get_effective_trait_count(trait_id) + return max(get_trait_count(trait_id), get_external_trait_count(trait_id)) + +/datum/tat_traits/proc/has_effective_trait(trait_id) + return get_effective_trait_count(trait_id) > 0 + +/datum/tat_traits/proc/get_effective_trait_counts() + var/list/result = list() + + for(var/trait_id in selected) + var/count = get_trait_count(trait_id) + if(count > 0) + result[trait_id] = count + + var/list/external_traits = get_external_traits() + for(var/trait_id in external_traits) + result[trait_id] = max(round(result[trait_id] || 0), 1) + + return result + +/datum/tat_traits/proc/is_repeatable_trait(trait_id) + var/list/repeatables = TAT_TRAIT_REPEATABLE_MAXIMUMS + return !!repeatables[trait_id] + +/datum/tat_traits/proc/get_trait_maximum(trait_id) + if(!is_repeatable_trait(trait_id)) + return 1 + var/list/repeatables = TAT_TRAIT_REPEATABLE_MAXIMUMS + return max(1, round(repeatables[trait_id] || 1)) + +/datum/tat_traits/proc/get_trait_display_name(trait_id) + var/list/entry = get_entry(trait_id) + if(!islist(entry)) + return "[trait_id]" + return "[entry["name"]]" + +/datum/tat_traits/proc/get_total_maximum() + return base_points + +/datum/tat_traits/proc/get_base_cost(trait_id) + var/list/entry = get_entry(trait_id) + if(!islist(entry)) + return 0 + return round((isnum(entry["cost"]) ? entry["cost"] : 0)) + +/datum/tat_traits/proc/is_armor_supplier_trait(trait_id) + return trait_id in GLOB.tat_armor_supplier_traits + +/datum/tat_traits/proc/is_material_supplier_trait(trait_id) + return trait_id in GLOB.tat_material_supplier_traits + +/datum/tat_traits/proc/get_first_selected_supplier_trait(list/supplier_traits) + if(!islist(supplier_traits)) + return null + + for(var/selected_trait_id in selected) + if(!(selected_trait_id in supplier_traits)) + continue + if(get_trait_count(selected_trait_id) <= 0) + continue + return selected_trait_id + + return null + +/datum/tat_traits/proc/get_supplier_cross_discount(trait_id, list/supplier_traits, discount) + if(!(trait_id in supplier_traits)) + return 0 + + var/first_selected_trait_id = get_first_selected_supplier_trait(supplier_traits) + if(!first_selected_trait_id || first_selected_trait_id == trait_id) + return 0 + + return discount + +/datum/tat_traits/proc/get_armor_supplier_cross_discount(trait_id) + return get_supplier_cross_discount(trait_id, GLOB.tat_armor_supplier_traits, TAT_ARMOR_SUPPLIER_CROSS_DISCOUNT) + +/datum/tat_traits/proc/get_material_supplier_cross_discount(trait_id) + return get_supplier_cross_discount(trait_id, GLOB.tat_material_supplier_traits, TAT_MATERIAL_SUPPLIER_CROSS_DISCOUNT) + +/datum/tat_traits/proc/get_armor_training_supplier_discount(trait_id) + if(!is_armor_supplier_trait(trait_id)) + return 0 + + var/list/rules = GLOB.tat_trait_armor_training_supplier_discount_rules + for(var/training_trait_id in selected) + if(rules[training_trait_id] != trait_id) + continue + if(get_trait_count(training_trait_id) <= 0) + continue + return TAT_ARMOR_TRAINING_SUPPLIER_DISCOUNT + + return 0 + +/datum/tat_traits/proc/get_cost_modifier(trait_id) + var/modifier = 0 + modifier -= get_armor_supplier_cross_discount(trait_id) + modifier -= get_material_supplier_cross_discount(trait_id) + modifier -= get_armor_training_supplier_discount(trait_id) + modifier -= get_outlander_natural_potential_discount(trait_id) + return modifier + +/datum/tat_traits/proc/get_display_cost(trait_id) + var/cost = get_base_cost(trait_id) + get_cost_modifier(trait_id) + if(is_armor_supplier_trait(trait_id) || is_material_supplier_trait(trait_id)) + return max(0, cost) + return cost + +/datum/tat_traits/proc/check_trait(trait_id) + return islist(get_entry(trait_id)) + +/datum/tat_traits/proc/get_pq_lock_minimum(trait_id) + var/list/rules = GLOB.tat_trait_pq_lock_rules + return round(rules[trait_id] || 0) + +/datum/tat_traits/proc/is_pq_locked_trait(trait_id) + return get_pq_lock_minimum(trait_id) > 0 + +/datum/tat_traits/proc/can_select_trait(trait_id) + if(!check_trait(trait_id)) + return FALSE + var/pq_minimum = get_pq_lock_minimum(trait_id) + if(pq_minimum > 0 && (owner_build?.get_owner_playerquality() || 0) < pq_minimum) + return FALSE + // Virtue-granted flaws are already real character traits. Do not allow buying + // the same negative TAT trait again just to farm trait points. Positive + // traits stay buyable: external traits must not satisfy requirement chains. + if(has_external_trait(trait_id) && get_base_cost(trait_id) < 0) + return FALSE + return TRUE + +/datum/tat_traits/proc/add_trait(trait_id) + if(!can_select_trait(trait_id)) + return FALSE + if(is_repeatable_trait(trait_id)) + var/current = get_trait_count(trait_id) + var/maximum = get_trait_maximum(trait_id) + if(current >= maximum) + return FALSE + selected[trait_id] = current + 1 + else + selected[trait_id] = TRUE + owner_build?.set_dirty() + return TRUE + +/datum/tat_traits/proc/remove_trait(trait_id) + if(is_repeatable_trait(trait_id)) + var/current = get_trait_count(trait_id) + if(current > 1) + selected[trait_id] = current - 1 + else + selected -= trait_id + else + selected -= trait_id + owner_build?.set_dirty() + return TRUE + +/datum/tat_traits/proc/get_bonus_stat_points() + var/total = 0 + var/list/rules = GLOB.tat_trait_stat_point_rules + for(var/trait_id in selected) + if(trait_id in rules) + total += round(rules[trait_id]) * get_trait_count(trait_id) + return total + +/datum/tat_traits/proc/get_bonus_item_points() + var/total = 0 + var/list/rules = GLOB.tat_trait_item_point_rules + for(var/trait_id in selected) + if(trait_id in rules) + total += round(rules[trait_id]) * get_trait_count(trait_id) + return total + +/datum/tat_traits/proc/get_bonus_skill_domain_points(domain) + var/total = 0 + var/list/rules = GLOB.tat_trait_skill_point_rules + for(var/trait_id in selected) + var/list/domain_map = rules[trait_id] + if(islist(domain_map)) + total += round(domain_map[domain] || 0) * get_trait_count(trait_id) + return total + +/datum/tat_traits/proc/get_bonus_skill_value(skill_type) + var/total = 0 + var/list/rules = GLOB.tat_trait_skill_bonus_rules + + for(var/trait_id in selected) + var/list/skill_map = rules[trait_id] + if(islist(skill_map)) + total += round(skill_map[skill_type] || 0) + + if(has_trait(TRAIT_ARCYNE) && skill_type == /datum/skill/magic/arcane && !has_defensive_trait_lockout()) + total += 3 + + if(has_trait(TAT_TRAIT_MAGE_INITIATE) && skill_type == /datum/skill/magic/arcane) + total += 1 + + if(has_trait(TAT_TRAIT_SADDLEBORN) && skill_type == /datum/skill/misc/riding) + total += 1 + + if(has_trait(TAT_TRAIT_DIVINE_INITIATE) && skill_type == /datum/skill/magic/holy) + total += 1 + + return total + +/datum/tat_traits/proc/get_skill_cap_bonus_value(skill_type) + var/highest_cap = 0 + var/has_rule = FALSE + var/list/rules = GLOB.tat_trait_skill_cap_bonus_rules + + for(var/trait_id in rules) + var/list/skill_map = rules[trait_id] + if(!islist(skill_map) || !(skill_type in skill_map)) + continue + + has_rule = TRUE + if(has_trait(trait_id)) + highest_cap = max(highest_cap, round(skill_map[skill_type] || 0)) + + if(highest_cap > 0) + return highest_cap + + return has_rule ? TAT_SKILL_NONCOMBAT_CAP_UNTRAITED : 0 + +/datum/tat_traits/proc/get_required_trait_for_unlock(unlock_type, unlock_key) + var/list/rules = GLOB.tat_trait_item_unlock_rules + var/list/type_rules = rules[unlock_type] + if(!islist(type_rules)) + return null + return type_rules[unlock_key] + +/datum/tat_traits/proc/get_skill_cost_discount(skill_type, target_level) + if(!ispath(skill_type, /datum/skill) || target_level <= 0) + return 0 + + if(has_trait(TAT_TRAIT_RESIDENT) && (ispath(skill_type, /datum/skill/misc) || ispath(skill_type, /datum/skill/labor) || ispath(skill_type, /datum/skill/craft))) + return 1 + + if(has_trait(TAT_TRAIT_MASTER_OF_WANDERING) && ispath(skill_type, /datum/skill/misc)) + return 1 + + if(has_trait(TRAIT_SELF_SUSTENANCE) && (ispath(skill_type, /datum/skill/craft) || ispath(skill_type, /datum/skill/labor))) + return 1 + + var/list/rules = GLOB.tat_trait_skill_discount_rules + for(var/trait_id in selected) + var/list/discounted = rules[trait_id] + if(!islist(discounted) || !(skill_type in discounted)) + continue + if(ispath(skill_type, /datum/skill/combat)) + return (target_level <= 2) ? 1 : 0 + return 1 + return 0 + +/datum/tat_traits/proc/is_capped_negative_credit_trait(trait_id) + return trait_id in GLOB.tat_capped_negative_traits + +/datum/tat_traits/proc/get_capped_negative_credit_raw() + var/total = 0 + for(var/trait_id in selected) + if(!is_capped_negative_credit_trait(trait_id)) + continue + var/cost = get_display_cost(trait_id) * get_trait_count(trait_id) + if(cost >= 0) + continue + total += -cost + return total + +/datum/tat_traits/proc/get_capped_negative_credit_used() + return min(get_capped_negative_credit_raw(), TAT_NEGATIVE_TRAIT_CREDIT_CAP) + +/datum/tat_traits/proc/get_spent_points() + var/total = 0 + var/capped_negative_credit = 0 + for(var/trait_id in selected) + var/cost = get_display_cost(trait_id) * get_trait_count(trait_id) + if(is_capped_negative_credit_trait(trait_id) && cost < 0) + capped_negative_credit += -cost + continue + total += cost + total -= min(capped_negative_credit, TAT_NEGATIVE_TRAIT_CREDIT_CAP) + return total + +/datum/tat_traits/proc/get_remaining_points() + return get_total_maximum() - get_spent_points() + +/datum/tat_traits/proc/get_trait_conflict_map() + if(length(GLOB.tat_trait_conflict_map)) + return GLOB.tat_trait_conflict_map + GLOB.tat_trait_conflict_map = list( + TAT_TRAIT_RESIDENT = list(TRAIT_OUTLANDER, TAT_TRAIT_WANTED, TAT_TRAIT_BONUS_STAT_POOL, TAT_TRAIT_MASTER_OF_WANDERING, TAT_TRAIT_HERETIC, TRAIT_STRONGBITE, TAT_TRAIT_TRADER_LICENSE, TAT_TRAIT_WARRIOR_EXPERT), + TAT_TRAIT_TRADER_LICENSE = list(TAT_TRAIT_RESIDENT, TAT_TRAIT_WANTED, TRAIT_OUTLANDER, TAT_TRAIT_HERETIC), + TRAIT_OUTLANDER = list(TAT_TRAIT_WANTED), + TAT_TRAIT_WANTED = list(TRAIT_OUTLANDER, TAT_TRAIT_RESIDENT, TRAIT_TECHNOPHOBE), + TAT_TRAIT_BONUS_STAT_POOL = list(TAT_TRAIT_WANTED), + TRAIT_DODGEEXPERT = list(TRAIT_PARRYEXPERT, TAT_TRAIT_MAGE_MINOR_SLOT_2, TAT_TRAIT_MAGE_MAJOR_SLOT), + TRAIT_HEAVYARMOR = list(TRAIT_CRITICAL_RESISTANCE, TAT_TRAIT_MAGE_INITIATE), + TRAIT_MEDIUMARMOR = list(TRAIT_CRITICAL_RESISTANCE, TAT_TRAIT_MAGE_INITIATE), + TAT_TRAIT_SPELLBLADE = list(TAT_TRAIT_DIVINE_BOON_2, TAT_TRAIT_MAGE_MAJOR_SLOT), + TAT_TRAIT_BARDIC_INSPIRATION_T2 = list(TAT_TRAIT_SPELLBLADE, TAT_TRAIT_DIVINE_BOON_3), + TAT_TRAIT_MAGE_MAJOR_SLOT = list(TAT_TRAIT_DIVINE_BOON_3, TAT_TRAIT_SPELLBLADE), + TAT_TRAIT_DIVINE_BOON_3 = list(TAT_TRAIT_MAGE_MAJOR_SLOT, TAT_TRAIT_MAGE_MINOR_SLOT_2, TAT_TRAIT_MAGE_UTILITY_SLOT), + TRAIT_CRITICAL_RESISTANCE = list(TAT_TRAIT_MAGE_INITIATE, TAT_TRAIT_DIVINE_INITIATE), + TAT_TRAIT_WARRIOR_EXPERT = list(TAT_TRAIT_DIVINE_BOON_2, TAT_TRAIT_MAGE_MINOR_SLOT_1, TAT_TRAIT_MAGE_MAJOR_SLOT), + TAT_TRAIT_WARRIOR_MASTER = list(TRAIT_DODGEEXPERT, TRAIT_PARRYEXPERT, TRAIT_CRITICAL_RESISTANCE, TRAIT_MEDIUMARMOR, TRAIT_HEAVYARMOR), + TAT_TRAIT_SAVAGE_RAGE = list(TAT_TRAIT_BERSERKER_RAGE), + TRAIT_EASYDISMEMBER = list(TRAIT_HARDDISMEMBER), + TRAIT_HARDDISMEMBER = list(TRAIT_EASYDISMEMBER), + TRAIT_FENCERDEXTERITY = list(TAT_TRAIT_SAVAGE_SKIN), + TRAIT_NUDE_SLEEPER = list(TRAIT_NUDIST, TAT_TRAIT_SAVAGE_SKIN, TRAIT_NOSLEEP), + TAT_TRAIT_LOOTRAT = list(TAT_TRAIT_WANTED), + TAT_TRAIT_LOOTRAT_2 = list(TAT_TRAIT_SPELLBLADE, TAT_TRAIT_MAGE_INITIATE, TAT_TRAIT_DIVINE_BOON_2, TAT_TRAIT_HERETIC, TAT_TRAIT_WARRIOR_EXPERT, TAT_TRAIT_BONUS_STAT_POOL, TRAIT_PARRYEXPERT, TRAIT_DODGEEXPERT, TRAIT_CRITICAL_RESISTANCE, TRAIT_MEDIUMARMOR, TRAIT_HEAVYARMOR, TRAIT_CIVILIZEDBARBARIAN), + TRAIT_NOSLEEP = list(TRAIT_RITUALIST), + TRAIT_REVERSE_GUIDANCE = list(TRAIT_LESSER_REVERSE_GUIDANCE), + TRAIT_NOPAINSTUN = list(TAT_TRAIT_MAGE_INITIATE), + TRAIT_HARDDISMEMBER = list(TRAIT_EASYDISMEMBER) + ) + return GLOB.tat_trait_conflict_map + +/datum/tat_traits/proc/get_trait_requirement_map() + if(length(GLOB.tat_trait_requirement_map)) + return GLOB.tat_trait_requirement_map + GLOB.tat_trait_requirement_map = list( + TAT_TRAIT_WARRIOR_MASTER = list("all" = list(TAT_TRAIT_WARRIOR_EXPERT), "message" = "\"[get_trait_display_name(TAT_TRAIT_WARRIOR_MASTER)]\" requires \"[get_trait_display_name(TAT_TRAIT_WARRIOR_EXPERT)]\"."), + TAT_TRAIT_BARDIC_INSPIRATION_T2 = list("all" = list(TAT_TRAIT_BARDIC_INSPIRATION_T1), "message" = "\"[get_trait_display_name(TAT_TRAIT_BARDIC_INSPIRATION_T2)]\" requires \"[get_trait_display_name(TAT_TRAIT_BARDIC_INSPIRATION_T1)]\"."), + TAT_TRAIT_DIVINE_BOON_1 = list("all" = list(TAT_TRAIT_DIVINE_INITIATE), "message" = "\"[get_trait_display_name(TAT_TRAIT_DIVINE_BOON_1)]\" requires \"[get_trait_display_name(TAT_TRAIT_DIVINE_INITIATE)]\"."), + TAT_TRAIT_DIVINE_BOON_2 = list("all" = list(TAT_TRAIT_DIVINE_INITIATE, TAT_TRAIT_DIVINE_BOON_1), "message" = "\"[get_trait_display_name(TAT_TRAIT_DIVINE_BOON_2)]\" requires previous divine progression."), + TAT_TRAIT_DIVINE_BOON_3 = list("all" = list(TAT_TRAIT_DIVINE_INITIATE, TAT_TRAIT_DIVINE_BOON_2), "message" = "\"[get_trait_display_name(TAT_TRAIT_DIVINE_BOON_3)]\" requires previous divine progression."), + TAT_TRAIT_MAGE_INITIATE = list("all" = list(TRAIT_ARCYNE), "message" = "\"[get_trait_display_name(TAT_TRAIT_MAGE_INITIATE)]\" requires \"[get_trait_display_name(TRAIT_ARCYNE)]\"."), + TAT_TRAIT_SPELLBLADE = list("all" = list(TAT_TRAIT_MAGE_INITIATE, TRAIT_ARCYNE), "message" = "\"[get_trait_display_name(TAT_TRAIT_SPELLBLADE)]\" requires mage initiation and arcyne."), + TAT_TRAIT_MAGE_MINOR_SLOT_2 = list("all" = list(TAT_TRAIT_MAGE_MINOR_SLOT_1), "message" = "\"[get_trait_display_name(TAT_TRAIT_MAGE_MINOR_SLOT_2)]\" requires \"[get_trait_display_name(TAT_TRAIT_MAGE_MINOR_SLOT_1)]\"."), + TRAIT_BITERHELM = list("all" = list(TAT_TRAIT_HERETIC), "message" = "\"[get_trait_display_name(TRAIT_BITERHELM)]\" requires \"[get_trait_display_name(TAT_TRAIT_HERETIC)]\"."), + TRAIT_RITUALIST = list("all" = list(TAT_TRAIT_HERETIC, TAT_TRAIT_DIVINE_BOON_2), "message" = "\"[get_trait_display_name(TRAIT_RITUALIST)]\" requires \"[get_trait_display_name(TAT_TRAIT_HERETIC)]\" and \"[get_trait_display_name(TAT_TRAIT_DIVINE_BOON_2)]\"."), + TAT_TRAIT_ARTIFACTS_SUPPLIER = list("all" = list(TAT_TRAIT_PARTY_LEADER), "message" = "\"[get_trait_display_name(TAT_TRAIT_ARTIFACTS_SUPPLIER)]\" requires \"[get_trait_display_name(TAT_TRAIT_PARTY_LEADER)]\"."), + TAT_TRAIT_SAVAGE_SKIN = list("all" = list(TRAIT_NOPAINSTUN), "message" = "\"[get_trait_display_name(TAT_TRAIT_SAVAGE_SKIN)]\" requires \"[get_trait_display_name(TRAIT_NOPAINSTUN)]\"."), + TRAIT_STRONGBITE = list("all" = list(TAT_TRAIT_SAVAGE_SKIN), "message" = "\"[get_trait_display_name(TRAIT_STRONGBITE)]\" requires \"[get_trait_display_name(TAT_TRAIT_SAVAGE_SKIN)]\"."), + TAT_TRAIT_SAVAGE_RAGE = list("all" = list(TAT_TRAIT_SAVAGE_SKIN), "message" = "\"[get_trait_display_name(TAT_TRAIT_SAVAGE_RAGE)]\" requires \"[get_trait_display_name(TAT_TRAIT_SAVAGE_SKIN)]\"."), + TAT_TRAIT_BERSERKER_RAGE = list("all" = list(TAT_TRAIT_SAVAGE_SKIN, TAT_TRAIT_HERETIC), "message" = "\"[get_trait_display_name(TAT_TRAIT_BERSERKER_RAGE)]\" requires savage skin and heretic."), + TAT_TRAIT_LOOTRAT_2 = list("all" = list(TAT_TRAIT_LOOTRAT, TAT_TRAIT_TRADER_LICENSE), "message" = "\"[get_trait_display_name(TAT_TRAIT_LOOTRAT_2)]\" requires merchant writ and lootrat."), + ) + return GLOB.tat_trait_requirement_map + +/datum/tat_traits/proc/trait_requirement_is_met(list/rule) + if(!islist(rule)) + return TRUE + var/list/all_requirements = rule["all"] + if(islist(all_requirements)) + for(var/required_trait in all_requirements) + if(!has_trait(required_trait)) + return FALSE + return TRUE + +/datum/tat_traits/proc/has_defensive_trait_lockout() + if(has_effective_trait(TRAIT_DODGEEXPERT)) + return TRUE + if(has_effective_trait(TRAIT_PARRYEXPERT)) + return TRUE + if(has_effective_trait(TRAIT_CRITICAL_RESISTANCE)) + return TRUE + if(has_effective_trait(TRAIT_MEDIUMARMOR)) + return TRUE + if(has_effective_trait(TRAIT_HEAVYARMOR)) + return TRUE + return FALSE + +/datum/tat_traits/proc/are_traits_mutually_exclusive(trait_a, trait_b) + if(!trait_a || !trait_b || trait_a == trait_b) + return null + + if(has_trait(TAT_TRAIT_WANTED)) + if((trait_a == TRAIT_NOPAINSTUN && (trait_b == TAT_TRAIT_MAGE_INITIATE || trait_b == TAT_TRAIT_DIVINE_BOON_2)) || (trait_b == TRAIT_NOPAINSTUN && (trait_a == TAT_TRAIT_MAGE_INITIATE || trait_a == TAT_TRAIT_DIVINE_BOON_2))) + return null + + var/list/conflicts = get_trait_conflict_map() + var/list/a_conflicts = conflicts[trait_a] + if(islist(a_conflicts) && (trait_b in a_conflicts)) + return "\"[get_trait_display_name(trait_a)]\" conflicts with \"[get_trait_display_name(trait_b)]\"." + var/list/b_conflicts = conflicts[trait_b] + if(islist(b_conflicts) && (trait_a in b_conflicts)) + return "\"[get_trait_display_name(trait_a)]\" conflicts with \"[get_trait_display_name(trait_b)]\"." + if(((trait_a == TAT_TRAIT_DIVINE_BOON_3 || trait_b == TAT_TRAIT_DIVINE_BOON_3) && has_defensive_trait_lockout()) && !(has_trait(TAT_TRAIT_HERETIC) || has_trait(TAT_TRAIT_WANTED))) + return "\"[get_trait_display_name(TAT_TRAIT_DIVINE_BOON_3)]\" conflicts with current defensive trait setup or lack wanted/heretic traits." + return null + +/datum/tat_traits/proc/has_invalid_trait_dependencies() + var/list/issues = list() + var/list/requirements = get_trait_requirement_map() + for(var/trait_id in requirements) + if(!has_trait(trait_id)) + continue + var/list/rule = requirements[trait_id] + if(trait_requirement_is_met(rule)) + continue + issues += (rule["message"] || "Trait has unmet requirements.") + if((has_trait(TAT_TRAIT_MAGE_MAJOR_SLOT) || has_trait(TAT_TRAIT_MAGE_MINOR_SLOT_1) || has_trait(TAT_TRAIT_MAGE_UTILITY_SLOT)) && !has_trait(TAT_TRAIT_MAGE_INITIATE)) + issues += "Mage spell slots require \"[get_trait_display_name(TAT_TRAIT_MAGE_INITIATE)]\"." + var/list/effective_traits = get_effective_trait_counts() + for(var/trait_a in effective_traits) + for(var/trait_b in effective_traits) + if(trait_a == trait_b) + continue + if("[trait_a]" >= "[trait_b]") + continue + var/reason = are_traits_mutually_exclusive(trait_a, trait_b) + if(reason) + issues += reason + return issues + +/datum/tat_traits/proc/get_effective_divine_tier() + var/tier = CLERIC_T0 + if(has_trait(TAT_TRAIT_DIVINE_BOON_1)) + tier++ + if(has_trait(TAT_TRAIT_DIVINE_BOON_2)) + tier++ + if(has_trait(TAT_TRAIT_DIVINE_BOON_3)) + tier++ + return clamp(tier, CLERIC_T0, CLERIC_T4) + +/datum/tat_traits/proc/get_divine_passive_gain_for_tier(cleric_tier) + if(cleric_tier >= CLERIC_T1) + return CLERIC_REGEN_MINOR + return CLERIC_REGEN_WITCH + +/datum/tat_traits/proc/get_divine_devotion_limit_for_tier(cleric_tier) + switch(cleric_tier) + if(CLERIC_T4) + return CLERIC_REQ_4 + if(CLERIC_T3) + return CLERIC_REQ_3 + if(CLERIC_T2) + return CLERIC_REQ_2 + return CLERIC_REQ_1 + +/datum/tat_traits/proc/build_mage_aspects(scale_with_arcane = TRUE) + var/major = 0 + var/minor = 1 + var/utilities = 3 + if(has_trait(TAT_TRAIT_MAGE_MAJOR_SLOT)) + major += 1 + if(has_trait(TAT_TRAIT_MAGE_MINOR_SLOT_1)) + minor += 1 + if(has_trait(TAT_TRAIT_MAGE_MINOR_SLOT_2)) + minor += 1 + if(has_trait(TAT_TRAIT_MAGE_UTILITY_SLOT)) + utilities += 1 + if(scale_with_arcane) + utilities += owner_build?.get_skill_value(/datum/skill/magic/arcane) || 0 + return list("mastery" = FALSE, "major" = major, "minor" = minor, "utilities" = utilities, "ward" = TRUE) + +/datum/tat_traits/proc/can_train_arcane() + return TRUE + +/datum/tat_traits/proc/can_train_holy() + return TRUE + +/datum/tat_traits/proc/can_train_druidic() + return TRUE + +/datum/tat_traits/proc/sanitize() + for(var/trait_id in selected.Copy()) + if(!can_select_trait(trait_id)) + selected -= trait_id + continue + var/count = get_trait_count(trait_id) + var/maximum = get_trait_maximum(trait_id) + if(count <= 0) + selected -= trait_id + else if(is_repeatable_trait(trait_id) && count > maximum) + selected[trait_id] = maximum + while(get_remaining_points() < 0) + var/changed = FALSE + for(var/trait_id in selected.Copy()) + selected -= trait_id + changed = TRUE + if(get_remaining_points() >= 0) + break + if(!changed) + break + return TRUE + +/datum/tat_traits/proc/try_apply_party_leader(mob/living/carbon/human/H) + if(has_trait(TAT_TRAIT_PARTY_LEADER)) + H.LoadComponent(/datum/component/tat_party_leader) + +/datum/tat_traits/proc/apply_resident_package(mob/living/carbon/human/H) + if(!H) + return + ADD_TRAIT(H, TRAIT_RESIDENT, TAT_TRAIT_SOURCE) + if(H in SStreasury.bank_accounts) + SStreasury.give_money_account(ECONOMIC_LOWER_MIDDLE_CLASS, H, "Savings.") + else + SStreasury.create_bank_account(H, ECONOMIC_LOWER_MIDDLE_CLASS) + var/bonus_reading = owner_build?.get_resident_skill_value(/datum/skill/misc/reading) || 0 + if(bonus_reading > 0) + H.adjust_skillrank_up_to(/datum/skill/misc/reading, bonus_reading, TRUE) + + apply_resident_skill_spells(H) + +/datum/tat_traits/proc/apply_resident_pugilist_package(mob/living/carbon/human/H) + if(!H || !has_trait(TRAIT_CIVILIZEDBARBARIAN)) + return + var/spell_type = owner_build?.get_resident_pugilist_spell_type(owner_build?.get_resident_pugilist_spell_choice(H)) + if(spell_type) + owner_build?.grant_mind_spell_if_missing(H, spell_type) + +/datum/tat_traits/proc/apply_divine_package(mob/living/carbon/human/H) + if(!H || !has_trait(TAT_TRAIT_DIVINE_INITIATE)) + return + var/cleric_tier = get_effective_divine_tier() + var/passive_gain = get_divine_passive_gain_for_tier(cleric_tier) + var/devotion_limit = get_divine_devotion_limit_for_tier(cleric_tier) + var/datum/devotion/D = new /datum/devotion(H, H.patron) + D.grant_miracles(H, cleric_tier = cleric_tier, passive_gain = passive_gain, devotion_limit = devotion_limit) + H.adjust_skillrank_up_to(/datum/skill/magic/holy, max(1, owner_build?.get_skill_value(/datum/skill/magic/holy) || 1), TRUE) + +/datum/tat_traits/proc/apply_mage_package(mob/living/carbon/human/H) + if(!H || !has_trait(TAT_TRAIT_MAGE_INITIATE) || !H.mind) + return + ADD_TRAIT(H, TRAIT_ARCYNE, TAT_TRAIT_SOURCE) + var/list/aspects = build_mage_aspects(TRUE) + H.mind.setup_mage_aspects(aspects) + owner_build?.set_magic_value("mage_aspects", aspects.Copy()) + // Spellbook/chalk are synchronized into the TAT loadout stash by /datum/tat_items. + +/datum/tat_traits/proc/apply_spellblade_base_package(mob/living/carbon/human/H) + if(!H || !has_trait(TAT_TRAIT_SPELLBLADE)) + return + ADD_TRAIT(H, TRAIT_ARCYNE, TAT_TRAIT_SOURCE) + +/datum/tat_traits/proc/apply_spellblade_specialization_package(mob/living/carbon/human/H) + if(!H || !has_trait(TAT_TRAIT_SPELLBLADE)) + return + if(!H.mind) + return + to_chat(H, span_warning("You start with Bind Weapon. Remember to Bind your weapon so you can use your abilities and build up Arcyne Momentum.")) + var/list/subclass_list = list("Blade", "Phalangite", "Macebearer") + var/subclass_selected = H.client ? tgui_input_list(H, "Who are you?", "The spellblade specialization", subclass_list) : null + if(!subclass_selected) + subclass_selected = "Blade" + switch(subclass_selected) + if("Blade") + H.mind.AddSpell(new /datum/action/cooldown/spell/caedo) + H.mind.AddSpell(new /datum/action/cooldown/spell/air_strike) + H.mind.AddSpell(new /datum/action/cooldown/spell/leyline_anchor) + H.mind.AddSpell(new /datum/action/cooldown/spell/projectile/blade_storm) + if("Phalangite") + H.mind.AddSpell(new /datum/action/cooldown/spell/azurean_phalanx) + H.mind.AddSpell(new /datum/action/cooldown/spell/projectile/azurean_pilum) + H.mind.AddSpell(new /datum/action/cooldown/spell/advance) + H.mind.AddSpell(new /datum/action/cooldown/spell/gate_of_reckoning) + if("Macebearer") + H.mind.AddSpell(new /datum/action/cooldown/spell/projectile/kastvyl) + H.mind.AddSpell(new /datum/action/cooldown/spell/tremor) + H.mind.AddSpell(new /datum/action/cooldown/spell/charge) + H.mind.AddSpell(new /datum/action/cooldown/spell/cataclysm) + H.mind.setup_mage_aspects(build_mage_aspects(FALSE)) + H.mind.AddSpell(new /datum/action/cooldown/spell/recall_weapon) + H.mind.AddSpell(new /datum/action/cooldown/spell/empower_weapon) + H.mind.AddSpell(new /datum/action/cooldown/spell/bind_weapon) + H.mind.AddSpell(new /datum/action/cooldown/spell/mending) + +/datum/tat_traits/proc/get_pliant_rename_prefix() + if(!has_trait(TRAIT_OUTLANDER) && !has_trait(TAT_TRAIT_RESIDENT)) + return "Straying Pliant" + if(has_trait(TRAIT_OUTLANDER)) + return "Wandering Pliant" + if(has_trait(TAT_TRAIT_RESIDENT)) + return "Local Pliant" + return "Pliant" + +/datum/tat_traits/proc/get_pliant_default_class_name() + return "Towner" + +/datum/tat_traits/proc/get_pliant_current_class_name(mob/living/carbon/human/H) + // Automatic, silent base title generation. + // Used by Pliant Rename as the "current/selected class" option. + // Do not open choice dialogs here. + var/class_name = get_pliant_best_role_title() + if(!length(class_name)) + class_name = trim("[H?.advjob]") + if(!length(class_name)) + class_name = get_pliant_default_class_name() + return get_pliant_safe_class_name(class_name) + +/datum/tat_traits/proc/get_pliant_slot_class_name(fallback = null) + var/slot_name = trim("[owner_build?.get_active_tat_slot_name()]") + if(!length(slot_name)) + if(length("[fallback]")) + return get_pliant_safe_class_name(fallback) + return get_pliant_default_class_name() + return get_pliant_safe_class_name(slot_name) + +/datum/tat_traits/proc/get_pliant_safe_class_name(class_name, fallback = null) + class_name = trim("[class_name]") + if(!length(class_name)) + if(length("[fallback]")) + class_name = fallback + else + class_name = get_pliant_default_class_name() + return copytext(class_name, 1, 50) + +/datum/tat_traits/proc/get_pliant_skill_role_rules() + return list( + list("title" = "Sellsword", "minimum" = 3, "skills" = list(/datum/skill/combat/swords, /datum/skill/combat/knives, /datum/skill/combat/maces, /datum/skill/combat/axes, /datum/skill/combat/polearms, /datum/skill/combat/whipsflails, /datum/skill/combat/staves, /datum/skill/combat/shields)), + list("title" = "Archer", "minimum" = 3, "skills" = list(/datum/skill/combat/bows, /datum/skill/combat/crossbows, /datum/skill/combat/slings)), + list("title" = "Pugilist", "minimum" = 3, "skills" = list(/datum/skill/combat/unarmed, /datum/skill/combat/wrestling)), + list("title" = "Gunslinger", "minimum" = 3, "skills" = list(/datum/skill/combat/firearms)), + list("title" = "Hunter", "minimum" = 3, "skills" = list(/datum/skill/misc/hunting, /datum/skill/misc/tracking, /datum/skill/labor/butchering, /datum/skill/combat/bows, /datum/skill/combat/crossbows)), + list("title" = "Forester", "minimum" = 3, "skills" = list(/datum/skill/labor/lumberjacking, /datum/skill/misc/tracking, /datum/skill/misc/climbing, /datum/skill/misc/athletics)), + list("title" = "Miner", "minimum" = 3, "skills" = list(/datum/skill/labor/mining, /datum/skill/craft/smelting, /datum/skill/craft/masonry)), + list("title" = "Farmer", "minimum" = 3, "skills" = list(/datum/skill/labor/farming, /datum/skill/craft/cooking)), + list("title" = "Fisher", "minimum" = 3, "skills" = list(/datum/skill/labor/fishing, /datum/skill/craft/cooking)), + list("title" = "Cook", "minimum" = 3, "skills" = list(/datum/skill/craft/cooking, /datum/skill/labor/fishing, /datum/skill/labor/butchering)), + list("title" = "Blacksmith", "minimum" = 3, "skills" = list(/datum/skill/craft/blacksmithing, /datum/skill/craft/weaponsmithing, /datum/skill/craft/armorsmithing, /datum/skill/craft/smelting)), + list("title" = "Tailor", "minimum" = 3, "skills" = list(/datum/skill/craft/sewing, /datum/skill/craft/tanning)), + list("title" = "Carpenter", "minimum" = 3, "skills" = list(/datum/skill/craft/carpentry, /datum/skill/craft/masonry, /datum/skill/craft/crafting)), + list("title" = "Engineer", "minimum" = 3, "skills" = list(/datum/skill/craft/engineering, /datum/skill/craft/traps, /datum/skill/craft/carpentry)), + list("title" = "Alchemist", "minimum" = 3, "skills" = list(/datum/skill/craft/alchemy, /datum/skill/misc/medicine, /datum/skill/misc/reading)), + list("title" = "Physician", "minimum" = 3, "skills" = list(/datum/skill/misc/medicine, /datum/skill/craft/alchemy, /datum/skill/misc/reading)), + list("title" = "Scholar", "minimum" = 3, "skills" = list(/datum/skill/misc/reading, /datum/skill/magic/arcane, /datum/skill/magic/holy, /datum/skill/magic/druidic)), + list("title" = "Bard", "minimum" = 3, "skills" = list(/datum/skill/misc/music, /datum/skill/misc/reading)), + list("title" = "Rogue", "minimum" = 3, "skills" = list(/datum/skill/misc/stealing, /datum/skill/misc/sneaking, /datum/skill/misc/lockpicking)), + list("title" = "Scout", "minimum" = 3, "skills" = list(/datum/skill/misc/athletics, /datum/skill/misc/climbing, /datum/skill/misc/swimming, /datum/skill/misc/riding, /datum/skill/misc/tracking)), + list("title" = "Acolyte", "minimum" = 1, "skills" = list(/datum/skill/magic/holy)), + list("title" = "Mage", "minimum" = 1, "skills" = list(/datum/skill/magic/arcane)), + list("title" = "Druid", "minimum" = 1, "skills" = list(/datum/skill/magic/druidic)) + ) + +/datum/tat_traits/proc/get_pliant_trait_role_scores() + var/list/roles = list() + if(has_trait(TAT_TRAIT_BARDIC_INSPIRATION_T1) || has_trait(TAT_TRAIT_BARDIC_INSPIRATION_T2)) + roles["Minstrel"] = 600000 + return roles + +/datum/tat_traits/proc/get_pliant_skill_role_score(list/rule) + if(!owner_build || !islist(rule)) + return 0 + + var/list/skills = rule["skills"] + if(!islist(skills) || !length(skills)) + return 0 + + var/highest_skill = 0 + var/total_skill = 0 + for(var/skill_type in skills) + var/skill_value = owner_build.get_skill_value(skill_type) + if(skill_value <= 0) + continue + highest_skill = max(highest_skill, skill_value) + total_skill += skill_value + + var/minimum = round(rule["minimum"] || 3) + if(highest_skill < minimum) + return 0 + + return total_skill + +/datum/tat_traits/proc/get_pliant_skill_role_title_score(title) + if(!istext(title) || !length(title)) + return 0 + for(var/rule_entry in get_pliant_skill_role_rules()) + var/list/rule = rule_entry + if(!islist(rule)) + continue + var/rule_title = get_pliant_safe_class_name(rule["title"]) + if(lowertext(rule_title) != lowertext(title)) + continue + return get_pliant_skill_role_score(rule) + return 0 + +/datum/tat_traits/proc/get_pliant_role_title_score(title) + if(!istext(title) || !length(title)) + return 0 + var/list/trait_roles = get_pliant_trait_role_scores() + if(title in trait_roles) + return round(trait_roles[title] || 0) + return get_pliant_skill_role_title_score(title) + +/datum/tat_traits/proc/get_pliant_best_role_title() + var/best_title = null + var/best_score = 0 + + var/list/trait_roles = get_pliant_trait_role_scores() + for(var/title in trait_roles) + var/score = round(trait_roles[title] || 0) + if(score <= best_score) + continue + best_score = score + best_title = title + + for(var/rule_entry in get_pliant_skill_role_rules()) + var/list/rule = rule_entry + if(!islist(rule)) + continue + var/title = get_pliant_safe_class_name(rule["title"]) + var/score = get_pliant_skill_role_score(rule) + if(score <= best_score) + continue + best_score = score + best_title = title + + return best_title + +/datum/tat_traits/proc/add_pliant_role_choice(list/display_to_title, title, score, source_label = null, excluded_title = null) + if(!islist(display_to_title) || !istext(title) || !length(title)) + return FALSE + if(istext(excluded_title) && length(excluded_title) && lowertext(title) == lowertext(excluded_title)) + return FALSE + + var/display = source_label ? "[title] ([source_label])" : "[title] ([score])" + if(display in display_to_title) + return FALSE + display_to_title[display] = title + return TRUE + +/datum/tat_traits/proc/build_pliant_role_title_choices(excluded_title = null) + var/list/display_to_title = list() + + var/list/trait_roles = get_pliant_trait_role_scores() + for(var/title in trait_roles) + add_pliant_role_choice(display_to_title, title, trait_roles[title], "trait", excluded_title) + + for(var/rule_entry in get_pliant_skill_role_rules()) + var/list/rule = rule_entry + if(!islist(rule)) + continue + var/title = get_pliant_safe_class_name(rule["title"]) + var/score = get_pliant_skill_role_score(rule) + if(score <= 0 || !length(title)) + continue + add_pliant_role_choice(display_to_title, title, score, null, excluded_title) + + return display_to_title + +/datum/tat_traits/proc/build_pliant_skill_role_choices(current_class_name) + return build_pliant_role_title_choices(current_class_name) + +/datum/tat_traits/proc/get_single_pliant_role_choice(list/display_to_title) + if(!islist(display_to_title) || length(display_to_title) != 1) + return null + for(var/display in display_to_title) + return display_to_title[display] + return null + +/datum/tat_traits/proc/get_pliant_base_class_title(mob/living/carbon/human/H) + // Pliant Rename uses this silently. The actual dialog for rename must only ask + // between current/slot/custom, not open a separate role picker first. + return get_pliant_current_class_name(H) + +/datum/tat_traits/proc/get_pliant_plain_class_title(mob/living/carbon/human/H) + var/fallback = get_pliant_current_class_name(H) + var/list/display_to_title = build_pliant_role_title_choices() + if(!length(display_to_title)) + return fallback + + var/single_title = get_single_pliant_role_choice(display_to_title) + if(single_title) + return get_pliant_safe_class_name(single_title, fallback) + + var/list/options = list() + for(var/display in display_to_title) + options += display + + var/choice = H.client ? tgui_input_list(H, "Choose which class title should be used for your Pliant identity.", "CHOOSE YOUR CLASS", options) : null + if(choice && display_to_title[choice]) + return get_pliant_safe_class_name(display_to_title[choice], fallback) + return fallback + +/datum/tat_traits/proc/get_pliant_rename_title(mob/living/carbon/human/H) + var/base_class_name = get_pliant_base_class_title(H) + var/slot_name = get_pliant_slot_class_name(base_class_name) + + var/current_choice = "Use selected class ([base_class_name])" + var/slot_choice = "Use active TAT slot ([slot_name])" + var/input_choice = "Input class name" + var/list/display_to_title = list() + display_to_title[current_choice] = base_class_name + var/list/options = list(current_choice) + + if(lowertext(slot_name) != lowertext(base_class_name)) + options += slot_choice + display_to_title[slot_choice] = slot_name + + options += input_choice + + var/choice = H.client ? tgui_input_list(H, "Choose how your displayed class name should be written.", "CHOOSE YOUR DESTINY", options) : null + var/class_name = base_class_name + + if(choice == input_choice) + class_name = H.client ? tgui_input_text(H, "What is name of your destiny?", "YOUR CLASS NAME", encode = FALSE) : base_class_name + if(!length(trim("[class_name]"))) + class_name = base_class_name + else if(choice && display_to_title[choice]) + class_name = display_to_title[choice] + + class_name = get_pliant_safe_class_name(class_name, base_class_name) + return "[get_pliant_rename_prefix()] [class_name]" + +/datum/tat_traits/proc/get_pliant_default_title(mob/living/carbon/human/H) + var/class_name = get_pliant_plain_class_title(H) + class_name = get_pliant_safe_class_name(class_name) + return "[get_pliant_rename_prefix()] [class_name]" + +/datum/tat_traits/proc/apply_pliant_title(mob/living/carbon/human/H) + if(!H) + return FALSE + + var/class_name = null + var/new_title = null + if(has_trait(TAT_TRAIT_PLIANT_RENAME)) + new_title = get_pliant_rename_title(H) + class_name = copytext(new_title, length(get_pliant_rename_prefix()) + 2) + else + class_name = get_pliant_plain_class_title(H) + new_title = "[get_pliant_rename_prefix()] [get_pliant_safe_class_name(class_name)]" + + if(!length(new_title)) + return FALSE + + H.tat_pliant_title = new_title + if(length(class_name)) + owner_build?.set_magic_value("pliant_selected_role_title", get_pliant_safe_class_name(class_name)) + return TRUE + +/datum/tat_traits/proc/apply_pliant_rename(mob/living/carbon/human/H) + return apply_pliant_title(H) + +/datum/tat_traits/proc/apply_savage_skin_package(mob/living/carbon/human/H) + if(!H || !has_trait(TAT_TRAIT_SAVAGE_SKIN)) + return FALSE + + var/skin_path = /obj/item/clothing/suit/roguetown/armor/regenerating/skin/disciple/barbarian + return owner_build.items.spawn_item_to_exact_slot_or_bag(H, skin_path, SLOT_ARMOR) + +/datum/tat_traits/proc/apply_savage_rage_package(mob/living/carbon/human/H) + if(!H || !has_trait(TAT_TRAIT_SAVAGE_RAGE) || !H.mind) + return FALSE + if(owner_build?.grant_mind_spell_if_missing(H, /obj/effect/proc_holder/spell/self/ragebad)) + return TRUE + if(!owner_build) + H.mind.AddSpell(new /obj/effect/proc_holder/spell/self/ragebad) + ADD_TRAIT(H, TRAIT_RAGE, TAT_TRAIT_SOURCE) + return TRUE + return FALSE + +/datum/tat_traits/proc/apply_berserker_rage_package(mob/living/carbon/human/H) + if(!H || !has_trait(TAT_TRAIT_BERSERKER_RAGE) || !H.mind) + return FALSE + if(owner_build?.grant_mind_spell_if_missing(H, /obj/effect/proc_holder/spell/self/rage)) + return TRUE + if(!owner_build) + H.mind.AddSpell(new /obj/effect/proc_holder/spell/self/rage) + ADD_TRAIT(H, TRAIT_RAGE, TAT_TRAIT_SOURCE) + return TRUE + return FALSE + +/datum/tat_traits/proc/apply_instant_to_human(mob/living/carbon/human/H) + if(!H) + return FALSE + for(var/trait_id in selected) + if(is_repeatable_trait(trait_id)) + continue + switch(trait_id) + if(TAT_TRAIT_WARRIOR_EXPERT, TAT_TRAIT_WARRIOR_MASTER, TAT_TRAIT_RESIDENT, TAT_TRAIT_STEEL_SUPPLIER, TAT_TRAIT_SILVER_SUPPLIER, TAT_TRAIT_BRONZE_SUPPLIER, TAT_TRAIT_LEATHER_SUPPLIER, TAT_TRAIT_MAIL_SUPPLIER, TAT_TRAIT_PLATE_SUPPLIER, TAT_TRAIT_SPELLBLADE, TAT_TRAIT_BARDIC_INSPIRATION_T1, TAT_TRAIT_BARDIC_INSPIRATION_T2, TAT_TRAIT_PARTY_LEADER, TAT_TRAIT_BONUS_STAT_POOL, TAT_TRAIT_WANTED, TAT_TRAIT_DIVINE_INITIATE, TAT_TRAIT_MAGE_INITIATE, TAT_TRAIT_ARTIFACTS_SUPPLIER, TAT_TRAIT_FIREARMS_SUPPLIER, TAT_TRAIT_MASTER_OF_WANDERING, TAT_TRAIT_STRAYING_SOUL, TAT_TRAIT_PLIANT_RENAME, TAT_TRAIT_SAVAGE_SKIN, TAT_TRAIT_SAVAGE_RAGE, TAT_TRAIT_HERETIC, TAT_TRAIT_BERSERKER_RAGE, TAT_TRAIT_LOOTRAT, TRAIT_SHIRTLESS, TAT_TRAIT_LOOTRAT_2) + continue + else + ADD_TRAIT(H, trait_id, TAT_TRAIT_SOURCE) + if(has_trait(TAT_TRAIT_RESIDENT)) + apply_resident_package(H) + if(has_trait(TAT_TRAIT_SPELLBLADE)) + apply_spellblade_base_package(H) + + // Ritual chalk, spellbook and chalk are synchronized into the TAT loadout stash by /datum/tat_items. + if(has_trait(TAT_TRAIT_BARDIC_INSPIRATION_T1) || has_trait(TAT_TRAIT_BARDIC_INSPIRATION_T2)) + var/bard_tier = BARD_T1 + if(has_trait(TAT_TRAIT_BARDIC_INSPIRATION_T2)) + bard_tier = BARD_T2 + if(!H.inspiration) + var/datum/inspiration/I = new /datum/inspiration(H) + I.grant_inspiration(H, bard_tier) + else + H.inspiration.grant_inspiration(H, bard_tier) + try_apply_party_leader(H) + apply_savage_skin_package(H) + apply_savage_rage_package(H) + apply_berserker_rage_package(H) + if(has_trait(TAT_TRAIT_WARRIOR_MASTER)) + ADD_TRAIT(H, TRAIT_BADTRAINER, TAT_TRAIT_SOURCE) + if(has_trait(TAT_TRAIT_WANTED)) + ADD_TRAIT(H, TRAIT_OUTLAW, TAT_TRAIT_SOURCE) + ADD_TRAIT(H, TRAIT_HERESIARCH, TAT_TRAIT_SOURCE) + if(has_trait(TAT_TRAIT_HERETIC)) + GLOB.excommunicated_players += H.real_name + apply_divine_package(H) + apply_mage_package(H) + return TRUE + +/datum/tat_traits/proc/apply_deferred_to_human(mob/living/carbon/human/H) + if(!H?.client) + return FALSE + if(has_trait(TAT_TRAIT_RESIDENT)) + apply_resident_pugilist_package(H) + if(has_trait(TAT_TRAIT_SPELLBLADE)) + apply_spellblade_specialization_package(H) + if(has_trait(TAT_TRAIT_WANTED)) + wretch_select_bounty(H) + if(has_trait(TAT_TRAIT_SADDLEBORN)) + if(!H.HasSpell(/obj/effect/proc_holder/spell/self/choose_riding_virtue_mount)) + H.AddSpell(new /obj/effect/proc_holder/spell/self/choose_riding_virtue_mount) + ADD_TRAIT(H, TRAIT_EQUESTRIAN, TAT_TRAIT_SOURCE) + apply_pliant_title(H) + if(has_trait(TAT_TRAIT_RESIDENT)) + apply_resident_advjob(H) + return TRUE + +/datum/tat_traits/proc/apply_to_human(mob/living/carbon/human/H) + if(!H) + return FALSE + apply_instant_to_human(H) + apply_deferred_to_human(H) + return TRUE + +/datum/tat_traits/proc/disable_from_human(mob/living/carbon/human/H) + if(!H) + return FALSE + for(var/trait_id in selected) + REMOVE_TRAIT(H, trait_id, TAT_TRAIT_SOURCE) + REMOVE_TRAIT(H, TRAIT_RESIDENT, TAT_TRAIT_SOURCE) + REMOVE_TRAIT(H, TRAIT_ARCYNE, TAT_TRAIT_SOURCE) + REMOVE_TRAIT(H, TRAIT_BADTRAINER, TAT_TRAIT_SOURCE) + REMOVE_TRAIT(H, TRAIT_OUTLAW, TAT_TRAIT_SOURCE) + REMOVE_TRAIT(H, TRAIT_HERESIARCH, TAT_TRAIT_SOURCE) + REMOVE_TRAIT(H, TRAIT_DEATHSIGHT, TAT_TRAIT_SOURCE) + return TRUE + +/datum/tat_traits/proc/export_to_list() + return selected.Copy() + +/datum/tat_traits/proc/import_from_list(list/data) + reset() + if(!islist(data)) + return FALSE + for(var/trait_id in data) + if(!check_trait(trait_id)) + continue + var/count = isnum(data[trait_id]) ? round(data[trait_id]) : (data[trait_id] ? 1 : 0) + for(var/i in 1 to count) + add_trait(trait_id) + return TRUE + +/datum/tat_traits/proc/export_to_json_list() + var/list/result = list() + for(var/trait_id in selected) + var/count = get_trait_count(trait_id) + for(var/i in 1 to count) + result += trait_id + return result + +/datum/tat_traits/proc/import_from_json_list(list/data) + reset() + if(!islist(data)) + return FALSE + for(var/key in data) + if(check_trait(key)) + add_trait(key) + continue + if(data[key] && check_trait("[key]")) + var/count = isnum(data[key]) ? round(data[key]) : 1 + for(var/i in 1 to count) + add_trait("[key]") + return TRUE + +/datum/tat_traits/proc/get_resident_skill_spell_rules() + return list( + /datum/skill/misc/medicine = list( + /obj/effect/proc_holder/spell/invoked/diagnose/secular, + ), + /datum/skill/misc/hunting = list( + /obj/effect/proc_holder/spell/invoked/huntersyell, + ), + /datum/skill/craft/ceramics = list( + /obj/effect/proc_holder/spell/invoked/digclay, + ), + /datum/skill/craft/sewing = list( + /obj/effect/proc_holder/spell/invoked/fittedclothing, + ), + ) + +/datum/tat_traits/proc/apply_resident_skill_spells(mob/living/carbon/human/H) + if(!H || !H.mind || !has_trait(TAT_TRAIT_RESIDENT)) + return FALSE + + var/list/rules = get_resident_skill_spell_rules() + for(var/skill_type in rules) + if((owner_build?.get_skill_value(skill_type) || 0) <= 3) + continue + + var/list/rewards = rules[skill_type] + if(!islist(rewards)) + continue + + for(var/reward_type in rewards) + if(ispath(reward_type, /datum/component)) + H.AddComponent(reward_type) + continue + + owner_build.grant_mind_spell_if_missing(H, reward_type) + + return TRUE + +/datum/tat_traits/proc/get_tat_resident_advjob_title_to_path_map() + return list( + "Blacksmith" = "/datum/advclass/blacksmith", + "Miner" = "/datum/advclass/miner", + "Hunter" = "/datum/advclass/hunter", + "Farmer" = "/datum/advclass/farmer", + "Fisher" = "/datum/advclass/fisher", + "Cook" = "/datum/advclass/cook", + "Tailor" = "/datum/advclass/seamstress", + "Carpenter" = "/datum/advclass/woodworker", + "Engineer" = "/datum/advclass/engineer", + "Alchemist" = "/datum/advclass/alchemist", + "Physician" = "/datum/advclass/physician", + "Scholar" = "/datum/advclass/scholar", + "Bard" = "/datum/advclass/bard", + "Rogue" = "/datum/advclass/rogue", + ) + +/datum/tat_traits/proc/get_tat_resident_special_role_titles() + return list( + "Sellsword", + "Archer", + "Pugilist", + "Gunslinger", + "Forester", + "Scout", + "Acolyte", + "Mage", + "Druid", + ) + +/datum/tat_traits/proc/is_tat_resident_special_role_title(title) + if(!istext(title) || !length(title)) + return FALSE + return title in get_tat_resident_special_role_titles() + +/datum/tat_traits/proc/get_tat_resident_advjob_path_for_title(title) + if(!istext(title) || !length(title)) + return null + var/list/title_to_path = get_tat_resident_advjob_title_to_path_map() + var/path_text = title_to_path[title] + if(!istext(path_text) || !length(path_text)) + return null + return text2path(path_text) + +/datum/tat_traits/proc/get_tat_resident_role_choice_for_title(title) + if(!has_trait(TAT_TRAIT_RESIDENT) || !istext(title) || !length(title)) + return null + + title = get_pliant_safe_class_name(title) + if(is_tat_resident_special_role_title(title)) + return null + + var/score = get_pliant_skill_role_title_score(title) + if(score <= 0) + return null + + return list( + "title" = title, + "path" = get_tat_resident_advjob_path_for_title(title), + "score" = score, + ) + +/datum/tat_traits/proc/get_tat_resident_role_choice() + if(!has_trait(TAT_TRAIT_RESIDENT)) + return null + + var/selected_title = owner_build?.get_magic_value("pliant_selected_role_title") + var/list/selected_choice = get_tat_resident_role_choice_for_title(selected_title) + if(islist(selected_choice)) + return selected_choice + + var/list/rules = get_pliant_skill_role_rules() + var/list/best_choice = null + var/best_score = 0 + + for(var/rule_entry in rules) + var/list/rule = rule_entry + if(!islist(rule)) + continue + + var/title = get_pliant_safe_class_name(rule["title"]) + if(!length(title) || is_tat_resident_special_role_title(title)) + continue + + var/score = get_pliant_skill_role_score(rule) + if(score <= best_score) + continue + + best_score = score + best_choice = list( + "title" = title, + "path" = get_tat_resident_advjob_path_for_title(title), + "score" = score, + ) + + return best_choice + +/datum/tat_traits/proc/get_tat_resident_advjob() + var/list/choice = get_tat_resident_role_choice() + if(!islist(choice)) + return null + return choice["path"] + +/datum/tat_traits/proc/apply_resident_advjob(mob/living/carbon/human/H) + if(!H || !has_trait(TAT_TRAIT_RESIDENT)) + return + + var/list/choice = get_tat_resident_role_choice() + if(!islist(choice)) + return + + var/title = get_pliant_safe_class_name(choice["title"]) + var/resident_advjob_type = choice["path"] + var/applied_name = title + + if(resident_advjob_type) + var/datum/advclass/advclass = new resident_advjob_type + if(advclass) + applied_name = get_pliant_safe_class_name(advclass.name, title) + qdel(advclass) + + if(!length(applied_name)) + return + + H.advjob = applied_name + +/datum/tat_traits/proc/get_outlander_natural_potential_discount(trait_id) + if(trait_id != TAT_TRAIT_BONUS_STAT_POOL) + return 0 + if(!has_trait(TRAIT_OUTLANDER)) + return 0 + return 10 diff --git a/modular_twilight_axis/code/datums/tat_system/tat_defines.dm b/modular_twilight_axis/code/datums/tat_system/tat_defines.dm new file mode 100644 index 00000000000..72c4d78428f --- /dev/null +++ b/modular_twilight_axis/code/datums/tat_system/tat_defines.dm @@ -0,0 +1,40 @@ +#define TAT_TRAIT_SOURCE "tat_build" +#define TAT_ITEM_SOURCE_PAID "tat" +#define TAT_ITEM_SOURCE_TRAIT "trait" +#define TAT_ITEM_SOURCE_DONOR_LOADOUT "donor_loadout" + + +#define TAT_PARTY_LEADER_AURA_RANGE 7 +#define TAT_PARTY_LEADER_REFRESH_INTERVAL (2 SECONDS) +#define TAT_PARTY_LEADER_BONUS_CON 1 +#define TAT_PARTY_LEADER_BONUS_WIL 1 +#define TAT_PARTY_LEADER_MEMBER_CON 1 +#define TAT_PARTY_LEADER_LUCK_PER_MEMBER 0.5 + +#define TAT_STAT_ENTRY(_name, _cost, _base, _min, _max) list("name" = (_name), "cost" = (_cost), "base" = (_base), "min" = (_min), "max" = (_max)) +#define TAT_TRAIT_ENTRY(_name, _cost, _category, _category_name, _desc) list("name" = (_name), "cost" = (_cost), "category" = (_category), "category_name" = (_category_name), "desc" = (_desc)) +#define TAT_ITEM_ENTRY(_name, _cost, _category, _unlock_type, _unlock_key, _slot_group) list("name" = (_name), "cost" = (_cost), "category" = (_category), "unlock_type" = (_unlock_type), "unlock_key" = (_unlock_key), "slot_group" = (_slot_group)) +#define TAT_DONATION_ITEM_ENTRY_EX(_name, _cost, _category, _unlock_type, _unlock_key, _slot_group, _donat_tier, _donat_ignore) list("name" = (_name), "cost" = (_cost), "category" = (_category), "unlock_type" = (_unlock_type), "unlock_key" = (_unlock_key), "slot_group" = (_slot_group), "donat_tier" = (_donat_tier), "donat_ignore" = (_donat_ignore)) +#define TAT_DONATION_ITEM_ENTRY(_name, _cost, _category, _unlock_type, _unlock_key, _slot_group, _donat_tier) list("name" = (_name), "cost" = (_cost), "category" = (_category), "unlock_type" = (_unlock_type), "unlock_key" = (_unlock_key), "slot_group" = (_slot_group), "donat_tier" = (_donat_tier), "donat_ignore" = null) + +#define TAT_SLOT_COUNT 9 + +#define TAT_ROLE_BUCKET_TOWNER "towner" +#define TAT_ROLE_BUCKET_TRADER "trader" +#define TAT_ROLE_BUCKET_ADVENTURER "adventurer" +#define TAT_ROLE_BUCKET_WRETCH "wretch" + +#define TAT_SQL_ROLE_TOWNER "TAT Towner" +#define TAT_SQL_ROLE_TRADER "TAT Trader" +#define TAT_SQL_ROLE_ADVENTURER "TAT Adventurer" +#define TAT_SQL_ROLE_WRETCH "TAT Wretch" +#define TAT_SQL_ROLE_SYSTEM "TAT System" + +#define TAT_BAN_DEFAULT_REASON "TAT system access revoked." +#define TAT_ROLE_LOCK_DEFAULT_REASON "TAT role access revoked." +#define TAT_ROLE_LOCK_DEFAULT_SEVERITY "Medium" +#define TAT_ROLE_LOCK_DEFAULT_DURATION 10080 +#define TAT_ROLE_LOCK_DEFAULT_INTERVAL "MINUTE" + +GLOBAL_LIST_EMPTY(tat_skill_entry_cache) +GLOBAL_VAR_INIT(tat_skill_entry_cache_ready, FALSE) diff --git a/modular_twilight_axis/code/modules/jobs/job_types/roguetown/adventurer/adventurer.dm b/modular_twilight_axis/code/modules/jobs/job_types/roguetown/adventurer/adventurer.dm new file mode 100644 index 00000000000..53739dbf40c --- /dev/null +++ b/modular_twilight_axis/code/modules/jobs/job_types/roguetown/adventurer/adventurer.dm @@ -0,0 +1,5 @@ +/datum/job/roguetown/adventurer/New() + job_subclasses += list( + /datum/advclass/tat_class/adventurer + ) + . = ..() diff --git a/modular_twilight_axis/code/modules/jobs/job_types/roguetown/adventurer/wretch.dm b/modular_twilight_axis/code/modules/jobs/job_types/roguetown/adventurer/wretch.dm new file mode 100644 index 00000000000..06a77cc028b --- /dev/null +++ b/modular_twilight_axis/code/modules/jobs/job_types/roguetown/adventurer/wretch.dm @@ -0,0 +1,5 @@ +/datum/job/roguetown/wretch/New() + job_subclasses += list( + /datum/advclass/tat_class/wretch + ) + . = ..() diff --git a/modular_twilight_axis/code/modules/jobs/job_types/roguetown/pilgrim/pilgrim.dm b/modular_twilight_axis/code/modules/jobs/job_types/roguetown/pilgrim/pilgrim.dm new file mode 100644 index 00000000000..b01f4cd186f --- /dev/null +++ b/modular_twilight_axis/code/modules/jobs/job_types/roguetown/pilgrim/pilgrim.dm @@ -0,0 +1,5 @@ +/datum/job/roguetown/villager/New() + job_subclasses += list( + /datum/advclass/tat_class/towner + ) + . = ..() diff --git a/modular_twilight_axis/code/modules/jobs/job_types/roguetown/tat_build/tat_class.dm b/modular_twilight_axis/code/modules/jobs/job_types/roguetown/tat_build/tat_class.dm new file mode 100644 index 00000000000..b6d5b850fda --- /dev/null +++ b/modular_twilight_axis/code/modules/jobs/job_types/roguetown/tat_build/tat_class.dm @@ -0,0 +1,210 @@ +/proc/get_client_active_tat_build(client/C) + if(!C?.prefs) + return null + + return C.prefs.tat_build + +/proc/client_can_use_tat_role_bucket(client/C, required_bucket) + if(!required_bucket) + return TRUE + + if(!C?.ckey) + return FALSE + + if(tat_is_role_bucket_locked(C.ckey, required_bucket)) + return FALSE + + return TRUE + +/proc/human_can_use_tat_role_bucket(mob/living/carbon/human/H, required_bucket) + if(!required_bucket) + return TRUE + + if(!H) + return FALSE + + var/key = H.ckey || H.client?.ckey + if(!key) + return FALSE + + if(tat_is_role_bucket_locked(key, required_bucket)) + return FALSE + + return TRUE + +/proc/client_has_tat_role_bucket(client/C, required_bucket) + if(!required_bucket) + return TRUE + + if(!client_can_use_tat_role_bucket(C, required_bucket)) + return FALSE + + var/datum/tat_build/build = get_client_active_tat_build(C) + if(!build) + return FALSE + + if(!build.can_save()) + return FALSE + + return build.get_role_bucket() == required_bucket + +/proc/tat_build_has_role_bucket(datum/tat_build/build, required_bucket) + if(!required_bucket) + return TRUE + + if(!build) + return FALSE + + if(!build.can_save()) + return FALSE + + return build.get_role_bucket() == required_bucket + +/proc/human_has_tat_role_bucket(mob/living/carbon/human/H, required_bucket) + if(!required_bucket) + return TRUE + + if(!human_can_use_tat_role_bucket(H, required_bucket)) + return FALSE + + if(H?.active_tat_build) + return tat_build_has_role_bucket(H.active_tat_build, required_bucket) + + if(!H?.client) + return FALSE + + return client_has_tat_role_bucket(H.client, required_bucket) + +/proc/get_human_active_tat_build(mob/living/carbon/human/H) + if(!H) + return null + + if(H.client) + H.active_tat_build = get_client_active_tat_build(H.client) + + return H.active_tat_build + +/mob/living/carbon/human + var/datum/tat_build/active_tat_build = null + var/tat_build_pre_client_applied = FALSE + var/tat_build_post_client_applied = FALSE + +/datum/advclass/tat_class + name = "Pliant Soul" + tutorial = "A freeform class used for the TAT build system." + + allowed_sexes = list(MALE, FEMALE) + + outfit = /datum/outfit/job/roguetown/tat_class/basic + + subclass_stats = list() + subclass_skills = list() + traits_applied = list() + + var/required_tat_bucket = null + +/datum/advclass/tat_class/check_requirements(mob/living/carbon/human/H) + var/key = H?.ckey || H?.client?.ckey + if(key) + tat_refresh_ban_cache_for_ckey(key) + + if(!..()) + return FALSE + + if(!human_can_use_tat_role_bucket(H, required_tat_bucket)) + return FALSE + + return human_has_tat_role_bucket(H, required_tat_bucket) + +/datum/advclass/tat_class/towner + name = "Pliant Towner" + tutorial = "A custom-built local resident of Psydonia. Your home, work, and place among the townfolk are defined by your active TAT build." + + category_tags = list(CTAG_TOWNER) + required_tat_bucket = TAT_ROLE_BUCKET_TOWNER + +/datum/advclass/tat_class/trader + name = "Pliant Trader" + tutorial = "A custom-built traveler, supplier, artisan, or free tradesoul. This path is for TAT builds without resident, wanted, or outlander status." + + category_tags = list(CTAG_TRADER) + class_select_category = CLASS_CAT_TRADER + required_tat_bucket = TAT_ROLE_BUCKET_TRADER + +/datum/advclass/tat_class/adventurer + name = "Pliant Adventurer" + tutorial = "A custom-built wanderer-outlander, or dangerous free soul. This path is for TAT builds with Outlander." + + class_select_category = CLASS_CAT_NOMAD + category_tags = list(CTAG_ADVENTURER, CTAG_COURTAGENT) + required_tat_bucket = TAT_ROLE_BUCKET_ADVENTURER + +/datum/advclass/tat_class/wretch + name = "Pliant Wretch" + tutorial = "A custom-built outlaw, a nightmare free soul. This path is for TAT builds with Wanted." + + class_select_category = CLASS_CAT_NOMAD + category_tags = list(CTAG_WRETCH) + required_tat_bucket = TAT_ROLE_BUCKET_WRETCH + +/datum/outfit/job/roguetown/tat_class + name = "Pliant Soul" + +/datum/outfit/job/roguetown/tat_class/basic/pre_equip(mob/living/carbon/human/H) + ..() + +/datum/outfit/job/roguetown/tat_class/basic/post_equip(mob/living/carbon/human/H, visualsOnly = FALSE) + . = ..() + if(visualsOnly) + return + + if(!H || !H.mind) + return + + apply_tat_build_pre_client(H) + +/datum/outfit/job/roguetown/tat_class/basic/proc/apply_tat_build_pre_client(mob/living/carbon/human/H) + if(!H || !H.mind) + return + + if(H.tat_build_pre_client_applied) + addtimer(CALLBACK(src, PROC_REF(apply_tat_build_post_client), H), 10) + return + + var/datum/tat_build/build = get_human_active_tat_build(H) + if(!build) + addtimer(CALLBACK(src, PROC_REF(apply_tat_build_pre_client), H), 10) + return + + if(!build.can_save()) + return + + if(!build.apply_pre_client_to_human(H)) + return + + H.tat_build_pre_client_applied = TRUE + + addtimer(CALLBACK(src, PROC_REF(apply_tat_build_post_client), H), 10) + +/datum/outfit/job/roguetown/tat_class/basic/proc/apply_tat_build_post_client(mob/living/carbon/human/H) + if(!H || !H.mind) + return + + if(H.tat_build_post_client_applied) + return + + if(!H.client) + addtimer(CALLBACK(src, PROC_REF(apply_tat_build_post_client), H), 10) + return + + var/datum/tat_build/build = get_human_active_tat_build(H) + if(!build) + return + + if(!build.can_save()) + return + + if(!build.apply_post_client_to_human(H)) + return + + H.tat_build_post_client_applied = TRUE diff --git a/modular_twilight_axis/code/modules/jobs/job_types/roguetown/trader/trader.dm b/modular_twilight_axis/code/modules/jobs/job_types/roguetown/trader/trader.dm new file mode 100644 index 00000000000..623f66294c0 --- /dev/null +++ b/modular_twilight_axis/code/modules/jobs/job_types/roguetown/trader/trader.dm @@ -0,0 +1,5 @@ +/datum/job/roguetown/trader/New() + job_subclasses += list( + /datum/advclass/tat_class/trader + ) + . = ..() diff --git a/roguetown.dme b/roguetown.dme index d1e3cfedc76..d6ed1d70ea7 100644 --- a/roguetown.dme +++ b/roguetown.dme @@ -3628,6 +3628,29 @@ #include "modular_causticcove\code\modules\events\adventure\random_bosses\random_boss.dm" #include "modular_causticcove\code\game\objects\items\clothes\causthats.dm" +//Modular TA files +#include "modular_twilight_axis\code\datums\tat_system\tat_defines.dm" +#include "modular_twilight_axis\code\datums\tat_system\_defines\tat_defines_items.dm" +#include "modular_twilight_axis\code\datums\tat_system\_defines\tat_defines_skills.dm" +#include "modular_twilight_axis\code\datums\tat_system\_defines\tat_defines_stats.dm" +#include "modular_twilight_axis\code\datums\tat_system\_defines\tat_defines_traits.dm" +#include "modular_twilight_axis\code\datums\tat_system\core\tat_build.dm" +#include "modular_twilight_axis\code\datums\tat_system\core\tat_slot.dm" +#include "modular_twilight_axis\code\datums\tat_system\core\tat_ui.dm" +#include "modular_twilight_axis\code\datums\tat_system\core\tat_admin_panel.dm" +#include "modular_twilight_axis\code\datums\tat_system\core\tat_bans.dm" +#include "modular_twilight_axis\code\datums\tat_system\domains\tat_items.dm" +#include "modular_twilight_axis\code\datums\tat_system\domains\tat_trader_lootboxes.dm" +#include "modular_twilight_axis\code\datums\tat_system\domains\tat_party_leader.dm" +#include "modular_twilight_axis\code\datums\tat_system\domains\tat_skills.dm" +#include "modular_twilight_axis\code\datums\tat_system\domains\tat_stats.dm" +#include "modular_twilight_axis\code\datums\tat_system\domains\tat_traits.dm" +#include "modular_twilight_axis\code\modules\jobs\job_types\roguetown\pilgrim\pilgrim.dm" +#include "modular_twilight_axis\code\modules\jobs\job_types\roguetown\trader\trader.dm" +#include "modular_twilight_axis\code\modules\jobs\job_types\roguetown\tat_build\tat_class.dm" +#include "modular_twilight_axis\code\modules\jobs\job_types\roguetown\adventurer\adventurer.dm" +#include "modular_twilight_axis\code\modules\jobs\job_types\roguetown\adventurer\wretch.dm" + //Modular OV files! #include "modular_ochrevalley\code\datums\stress\negative_events.dm" #include "modular_ochrevalley\code\game\items\teablends.dm" diff --git a/tgui/packages/tgui/interfaces/TATBuild.tsx b/tgui/packages/tgui/interfaces/TATBuild.tsx new file mode 100644 index 00000000000..791d7bab432 --- /dev/null +++ b/tgui/packages/tgui/interfaces/TATBuild.tsx @@ -0,0 +1,2248 @@ +import { useEffect, useMemo, useState } from 'react'; +import { ReactNode } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Window } from 'tgui/layouts'; +import { + Box, + Button, + Input, + NoticeBox, + Section, + Stack, + Tabs, + TextArea, +} from 'tgui-core/components'; + +type StatEntry = { + name: string; + cost: number; + base: number; + min: number; + max: number; +}; + +type SkillEntry = { + name: string; + desc?: string; + is_combat: boolean; + category?: string; +}; + +type SkillState = { + level: number; + cap: number; + next_cost: number; + bonus?: number; + invested?: number; +}; + +type TraitEntry = { + name: string; + cost: number; + category: string; + category_name: string; + desc?: string; + repeatable?: boolean; + maximum?: number; +}; + +type TraitState = { + amount: number; + can_add?: boolean; + maximum?: number; +}; + +type ItemEntry = { + name: string; + cost: number; + category?: string; + unlock_type?: string; + unlock_key?: string; + slot_group?: string | null; + icon?: string | null; + icon_state?: string | null; +}; + +type ItemState = { + amount: number; + unlocked: boolean; + maximum?: number; + can_add?: boolean; +}; + +type LoadoutPaintState = { + primary?: string; + detail?: string; + altdetail?: string; +}; + +type LoadoutState = { + amount: number; + equip: number; + bag: number; + stash: number; + slots?: Record; + valid_slots?: string[]; + sources?: Record; + paint?: LoadoutPaintState | null; + icon?: string | null; + icon_state?: string | null; +}; + +type SlotSummary = { + stats: number; + skills: number; + traits: number; + items: number; +}; + +type TatSlotEntry = { + id: number; + name: string; + active?: boolean; + summary?: SlotSummary; +}; + +type SkillDomainKey = + | 'combat' + | 'wandering' + | 'gathering' + | 'crafting' + | 'misc'; + +type SkillConversionDomainState = { + can_give?: boolean; + can_take?: boolean; + give_text?: string; + take_text?: string; +}; + +type Data = { + stats: Record; + skills: Record; + traits: string[]; + trait_counts?: Record; + traits_state?: Record; + items_state: Record; + loadout: Record; + + available_stats: Record; + available_skills: Record; + available_traits: Record; + available_items: Record; + + points_stats: number; + points_stats_remaining: number; + points_skills: number; + points_skills_remaining: number; + points_traits: number; + points_traits_remaining: number; + points_items: number; + points_items_remaining: number; + + skill_points_by_domain?: Partial>; + skill_points_remaining_by_domain?: Partial>; + skill_conversion_pool?: number; + skill_conversion_state?: Partial>; + + tat_slots?: TatSlotEntry[] | Record; + active_tat_slot?: number; + + build_json?: string | null; + last_json_error?: string | null; + last_json_notice?: string | null; + + can_save: boolean; + validation_issues?: string[]; + dirty: boolean; +}; + +type TabKey = 'control' | 'stats' | 'skills' | 'traits' | 'items' | 'loadout'; +type BackendAct = (action: string, payload?: Record) => void; + +type NumericRowProps = { + title: string; + value: number; + onAdd: () => void; + onRemove: () => void; + disabledAdd?: boolean; + disabledRemove?: boolean; + extra?: ReactNode; +}; + +type HoverCardData = { + name: string; + desc?: string; + slot?: string | null; + category?: string | null; + costText?: string; + total?: number; + bag?: number; + stash?: number; + equip?: number; + level?: number; + cap?: number; + bonus?: number; + invested?: number; + domainRemaining?: number | null; + maximum?: number; + canAdd?: boolean; + leftHelp?: string; + rightHelp?: string; +}; + +type ItemViewEntry = ItemEntry & ItemState; +type LoadoutViewEntry = ItemEntry & LoadoutState; + +const MAX_RENDERED_ITEMS_PER_SLOT = 80; + +const SKILL_DOMAIN_TITLES: Record = { + combat: 'Combat', + wandering: 'Wandering', + gathering: 'Gathering', + crafting: 'Crafting', + misc: 'Misc', +}; + +const SKILL_DOMAIN_ORDER: SkillDomainKey[] = [ + 'combat', + 'wandering', + 'gathering', + 'crafting', + 'misc', +]; + +const normalizeSearch = (value: unknown): string => + String(value ?? '') + .toLowerCase() + .trim(); + +const matchesSearch = (search: string, ...parts: Array): boolean => { + if (!search) { + return true; + } + const normalized = normalizeSearch(search); + return parts.some((part) => normalizeSearch(part).includes(normalized)); +}; + +const normalizeTatSlots = ( + raw: Data['tat_slots'], + activeSlotId?: number +): TatSlotEntry[] => { + const makeSummary = (summary?: SlotSummary): SlotSummary => ({ + stats: Number(summary?.stats) || 0, + skills: Number(summary?.skills) || 0, + traits: Number(summary?.traits) || 0, + items: Number(summary?.items) || 0, + }); + + if (!raw) { + return []; + } + + if (Array.isArray(raw)) { + return raw + .filter(Boolean) + .map((slot, index) => { + const id = Number(slot?.id) || index + 1; + return { + id, + name: String(slot?.name || `Slot ${id}`), + active: Number(activeSlotId) === id || !!slot?.active, + summary: makeSummary(slot?.summary), + }; + }) + .sort((a, b) => a.id - b.id); + } + + return Object.entries(raw) + .map(([key, slot], index) => { + const id = Number(slot?.id) || Number(key) || index + 1; + return { + id, + name: String(slot?.name || `Slot ${id}`), + active: Number(activeSlotId) === id || !!slot?.active, + summary: makeSummary(slot?.summary), + }; + }) + .sort((a, b) => a.id - b.id); +}; + +const SLOT_LABELS: Record = { + head: 'Head', + mask: 'Mask', + neck: 'Neck', + cloak: 'Cloak', + armor: 'Armor', + suit: 'Suit', + shirt: 'Shirt', + pants: 'Pants', + under: 'Under', + gloves: 'Gloves', + shoes: 'Shoes', + wrists: 'Wrists', + ring: 'Ring', + belt: 'Belt', + belt_l: 'Belt Left', + belt_r: 'Belt Right', + back: 'Back', + back_l: 'Back Left', + back_r: 'Back Right', + mouth: 'Mouth', + blackpowder: 'Blackpowder', + ranged: 'Ranged', + munition: 'Munition', + knife: 'Knives', + sword: 'Swords', + greatsword: 'Greatswords', + axe: 'Axes', + blunt: 'Blunt', + polearm: 'Polearms', + whip: 'Whips', + misc: 'Misc', + other: 'Other', +}; + +const CATEGORY_LABELS: Record = { + clothing: 'Clothing', + weapon: 'Weapons', + other: 'Other', + misc: 'Misc', +}; + +const CATEGORY_ORDER: Record = { + clothing: 0, + weapon: 1, + misc: 2, + other: 3, +}; + +const SLOT_ORDER: Record = { + head: 0, + mask: 1, + neck: 2, + cloak: 3, + armor: 4, + suit: 5, + shirt: 6, + under: 7, + gloves: 8, + wrists: 9, + belt: 10, + shoes: 11, + back: 12, + blackpowder: 20, + ranged: 21, + munition: 22, + knife: 23, + sword: 24, + greatsword: 25, + axe: 26, + blunt: 27, + polearm: 28, + whip: 29, + misc: 30, + other: 999, +}; + +const getSlotLabel = (slot?: string | null) => { + if (!slot) { + return 'Other'; + } + return SLOT_LABELS[slot.toLowerCase()] || slot; +}; + +const getCategoryLabel = (category?: string | null) => { + if (!category) { + return 'Other'; + } + return CATEGORY_LABELS[category.toLowerCase()] || category; +}; + +const normalizeSkillDomain = (value?: string | null): SkillDomainKey => { + const normalized = normalizeSearch(value); + if ( + normalized === 'combat' || + normalized === 'wandering' || + normalized === 'gathering' || + normalized === 'crafting' || + normalized === 'misc' + ) { + return normalized; + } + return 'misc'; +}; + +const formatSkillDisplayValue = (state?: SkillState) => { + const total = Number(state?.level) || 0; + const bonus = Number(state?.bonus) || 0; + return bonus > 0 ? `${total}(${bonus})` : `${total}`; +}; + +const formatDomainPoints = (data: Data, domain: SkillDomainKey) => { + const total = data.skill_points_by_domain?.[domain]; + const remaining = data.skill_points_remaining_by_domain?.[domain]; + + if (typeof total === 'number' && typeof remaining === 'number') { + return `${remaining} / ${total}`; + } + + return '? / ?'; +}; + +const getDomainRemainingPoints = (data: Data, domain: SkillDomainKey) => { + const remaining = data.skill_points_remaining_by_domain?.[domain]; + return typeof remaining === 'number' ? remaining : null; +}; + +const getTraitAmount = (data: Data, traitId: string): number => { + const stateAmount = Number(data.traits_state?.[traitId]?.amount); + if (Number.isFinite(stateAmount) && stateAmount > 0) { + return stateAmount; + } + + const countAmount = Number(data.trait_counts?.[traitId]); + if (Number.isFinite(countAmount) && countAmount > 0) { + return countAmount; + } + + return (data.traits || []).filter((id) => id === traitId).length; +}; + +const canAddTrait = (data: Data, traitId: string, entry: TraitEntry): boolean => { + const state = data.traits_state?.[traitId]; + if (typeof state?.can_add === 'boolean') { + return state.can_add; + } + + const amount = getTraitAmount(data, traitId); + const maximum = Number(state?.maximum ?? entry.maximum); + const repeatable = !!entry.repeatable; + + if (!repeatable && amount > 0) { + return false; + } + + if (Number.isFinite(maximum) && maximum >= 0 && amount >= maximum) { + return false; + } + + return data.points_traits_remaining >= (Number(entry.cost) || 0); +}; + +type LoadoutDollSlot = { + id: string; + label: string; + shortLabel?: string; + top: string; + left: string; + width: string; + height: string; +}; + +const LOADOUT_DOLL_SLOTS: LoadoutDollSlot[] = [ + { id: 'mask', label: 'Mask', shortLabel: 'Mask', top: '16px', left: '24px', width: '88px', height: '88px' }, + { id: 'head', label: 'Head', shortLabel: 'Head', top: '16px', left: '144px', width: '88px', height: '88px' }, + { id: 'mouth', label: 'Mouth', shortLabel: 'Mouth', top: '16px', left: '264px', width: '88px', height: '88px' }, + { id: 'shoulder_r', label: 'Right Shoulder', shortLabel: 'R Sh', top: '114px', left: '24px', width: '88px', height: '88px' }, + { id: 'cloak', label: 'Cloak', shortLabel: 'Cloak', top: '114px', left: '144px', width: '88px', height: '88px' }, + { id: 'shoulder_l', label: 'Left Shoulder', shortLabel: 'L Sh', top: '114px', left: '264px', width: '88px', height: '88px' }, + { id: 'neck', label: 'Neck', shortLabel: 'Neck', top: '212px', left: '24px', width: '88px', height: '88px' }, + { id: 'armor', label: 'Armor', shortLabel: 'Armor', top: '212px', left: '144px', width: '88px', height: '88px' }, + { id: 'wrists', label: 'Wrists', shortLabel: 'Wrst', top: '212px', left: '264px', width: '88px', height: '88px' }, + { id: 'ring', label: 'Ring', shortLabel: 'Ring', top: '310px', left: '24px', width: '88px', height: '88px' }, + { id: 'suit', label: 'Suit', shortLabel: 'Suit', top: '310px', left: '144px', width: '88px', height: '88px' }, + { id: 'gloves', label: 'Gloves', shortLabel: 'Glv', top: '310px', left: '264px', width: '88px', height: '88px' }, + { id: 'belt_r', label: 'Right Belt Pocket', shortLabel: 'R Belt', top: '408px', left: '24px', width: '88px', height: '88px' }, + { id: 'belt', label: 'Belt', shortLabel: 'Belt', top: '408px', left: '144px', width: '88px', height: '88px' }, + { id: 'belt_l', label: 'Left Belt Pocket', shortLabel: 'L Belt', top: '408px', left: '264px', width: '88px', height: '88px' }, + { id: 'hand_r', label: 'Right Hand', shortLabel: 'R Hand', top: '506px', left: '24px', width: '88px', height: '88px' }, + { id: 'legs', label: 'Legs', shortLabel: 'Legs', top: '506px', left: '144px', width: '88px', height: '88px' }, + { id: 'hand_l', label: 'Left Hand', shortLabel: 'L Hand', top: '506px', left: '264px', width: '88px', height: '88px' }, + { id: 'boots', label: 'Boots', shortLabel: 'Boots', top: '604px', left: '144px', width: '88px', height: '88px' }, +]; + +const getLoadoutValidSlots = (entry?: LoadoutViewEntry): string[] => { + if (!Array.isArray(entry?.valid_slots)) { + return []; + } + return entry.valid_slots.map((slot) => String(slot)); +}; + +const entryCanUseLoadoutSlot = (entry: LoadoutViewEntry, slotId: string): boolean => + getLoadoutValidSlots(entry).includes(slotId); + +const entryIsAssignedToLoadoutSlot = (entry: LoadoutViewEntry, slotId: string): boolean => + !!entry.slots?.[slotId]; + +const getAssignedEntryForLoadoutSlot = ( + entries: Array<[string, LoadoutViewEntry]>, + slotId: string +): [string, LoadoutViewEntry] | null => { + return entries.find(([, entry]) => entryIsAssignedToLoadoutSlot(entry, slotId)) || null; +}; + +const getLoadoutSlotCounts = ( + entries: Array<[string, LoadoutViewEntry]>, + slot: LoadoutDollSlot +) => { + return entries.reduce( + (acc, [, entry]) => { + if (!entryCanUseLoadoutSlot(entry, slot.id)) { + return acc; + } + acc.total += Number(entry.amount) || 0; + if (entryIsAssignedToLoadoutSlot(entry, slot.id)) { + acc.equip += 1; + } + acc.bag += Number(entry.bag) || 0; + acc.stash += Number(entry.stash) || 0; + return acc; + }, + { total: 0, equip: 0, bag: 0, stash: 0 } + ); +}; + +const getLoadoutSlotLabel = (slotId: string): string => { + const slot = LOADOUT_DOLL_SLOTS.find((entry) => entry.id === slotId); + return slot?.shortLabel || slot?.label || slotId; +}; + +const getLoadoutSourceText = (entry: LoadoutViewEntry): string => { + const sources = entry.sources || {}; + const parts: string[] = []; + if (sources.tat) { + parts.push(`TAT ${sources.tat}`); + } + if (sources.trait) { + parts.push(`Trait ${sources.trait}`); + } + if (sources.donor_loadout) { + parts.push(`Donor`); + } + return parts.join(' · '); +}; + +const getLoadoutPaintText = (entry: LoadoutViewEntry): string => { + const paint = entry.paint; + if (!paint) { + return ''; + } + const parts: string[] = []; + if (paint.primary) { + parts.push(`P ${paint.primary}`); + } + if (paint.detail) { + parts.push(`D ${paint.detail}`); + } + if (paint.altdetail) { + parts.push(`A ${paint.altdetail}`); + } + return parts.join(' · '); +}; + +const groupEntriesByCategoryAndSlot = < + T extends { slot_group?: string | null; category?: string | null; name?: string }, +>( + entries: Record, + matcher: (path: string, entry: T) => boolean +) => { + const grouped: Record>> = {}; + + Object.entries(entries || {}) + .filter(([path, entry]) => matcher(path, entry)) + .forEach(([path, entry]) => { + const categoryKey = (entry.category || 'other').toLowerCase(); + const slotKey = (entry.slot_group || 'other').toLowerCase(); + + if (!grouped[categoryKey]) { + grouped[categoryKey] = {}; + } + if (!grouped[categoryKey][slotKey]) { + grouped[categoryKey][slotKey] = []; + } + + grouped[categoryKey][slotKey].push([path, entry]); + }); + + Object.values(grouped).forEach((slotGroups) => { + Object.values(slotGroups).forEach((items) => { + items.sort((a, b) => (a[1].name || a[0]).localeCompare(b[1].name || b[0])); + }); + }); + + return Object.entries(grouped) + .sort(([catA], [catB]) => { + const aOrder = CATEGORY_ORDER[catA] ?? CATEGORY_ORDER.other; + const bOrder = CATEGORY_ORDER[catB] ?? CATEGORY_ORDER.other; + if (aOrder !== bOrder) { + return aOrder - bOrder; + } + return getCategoryLabel(catA).localeCompare(getCategoryLabel(catB)); + }) + .map(([categoryKey, slotGroups]) => { + const sortedSlots = Object.entries(slotGroups).sort(([slotA], [slotB]) => { + const aOrder = SLOT_ORDER[slotA] ?? SLOT_ORDER.other; + const bOrder = SLOT_ORDER[slotB] ?? SLOT_ORDER.other; + if (aOrder !== bOrder) { + return aOrder - bOrder; + } + return getSlotLabel(slotA).localeCompare(getSlotLabel(slotB)); + }); + + return [categoryKey, sortedSlots] as const; + }); +}; + +const NumericRow = ({ + title, + value, + onAdd, + onRemove, + disabledAdd, + disabledRemove, + extra, +}: NumericRowProps) => { + return ( + + + {title} + {!!extra && ( + + {extra} + + )} + + + + + + + + + + {value} + + + + + + + + + ); +}; + +const TileIcon = ({ icon, name }: { icon?: string | null; name: string }) => { + return ( +
+ {icon ? ( + {name} + ) : ( +
No icon
+ )} +
+ ); +}; + +const HoverCard = ({ data }: { data: HoverCardData | null }) => { + if (!data) { + return null; + } + + return ( + + + + + {data.name} + + + {!!data.desc && ( + + {data.desc} + + )} + + {!!data.slot && ( + + Slot: {data.slot} + + )} + + {!!data.category && ( + + Type: {data.category} + + )} + + {typeof data.level === 'number' && ( + + Level: {data.level} / {data.cap} + + )} + + {typeof data.bonus === 'number' && data.bonus > 0 && ( + + Bonus: +{data.bonus} + + )} + + + + {!!data.costText && ( + + Cost: {data.costText} + + )} + + {typeof data.total === 'number' && ( + + Total: {data.total} + + )} + + {typeof data.maximum === 'number' && data.maximum >= 0 && ( + + Maximum: {data.maximum} + + )} + + {typeof data.canAdd === 'boolean' && ( + + Can add: {data.canAdd ? 'Yes' : 'No'} + + )} + + {typeof data.bag === 'number' && typeof data.equip === 'number' && ( + + Bag: {data.bag} | Stash: {data.stash || 0} | Equip:{' '} + {data.equip} + + )} + + {typeof data.invested === 'number' && ( + + Invested: {data.invested} + + )} + + {typeof data.domainRemaining === 'number' && ( + + Free: {data.domainRemaining} + + )} + + {!!data.leftHelp && ( + + {data.leftHelp} + + )} + + {!!data.rightHelp && ( + + {data.rightHelp} + + )} + + + + ); +}; + +const ItemTile = ({ + name, + topRightText, + bottomLeftText, + bottomRightText, + icon, + onLeftClick, + onRightClick, + onHoverStart, + onHoverEnd, + glow, + disabled, +}: { + name: string; + topRightText?: string | number; + bottomLeftText?: string | number; + bottomRightText?: string | number; + icon?: string | null; + onLeftClick: () => void; + onRightClick?: () => void; + onHoverStart?: () => void; + onHoverEnd?: () => void; + glow?: string; + disabled?: boolean; +}) => { + return ( + +
{ + if (!disabled) { + onLeftClick(); + } + }} + onContextMenu={(event) => { + event.preventDefault(); + event.stopPropagation(); + onRightClick?.(); + }} + onMouseEnter={onHoverStart} + onMouseLeave={onHoverEnd} + style={{ + position: 'relative', + width: '88px', + height: '88px', + borderRadius: '6px', + background: disabled ? 'rgba(80,80,80,0.08)' : 'rgba(255,255,255,0.03)', + border: disabled + ? '1px solid rgba(255,255,255,0.04)' + : '1px solid rgba(255,255,255,0.08)', + boxShadow: glow ? `inset 0 0 0 1px ${glow}` : 'none', + cursor: disabled ? 'not-allowed' : 'pointer', + userSelect: 'none', + overflow: 'hidden', + opacity: disabled ? 0.55 : 1, + }}> +
+ +
+ + {topRightText !== undefined && topRightText !== null && topRightText !== '' && ( +
+ {topRightText} +
+ )} + + {bottomLeftText !== undefined && bottomLeftText !== null && bottomLeftText !== '' && ( +
+ {bottomLeftText} +
+ )} + + {bottomRightText !== undefined && bottomRightText !== null && bottomRightText !== '' && ( +
+ {bottomRightText} +
+ )} +
+
+ ); +}; + +const SectionTitleWithMeta = ({ + title, + meta, +}: { + title: string; + meta?: ReactNode; +}) => { + return ( + + + {title} + + + {meta} + + + ); +}; + +const SlotCards = ({ slots, act }: { slots: TatSlotEntry[]; act: BackendAct }) => { + const [renameDrafts, setRenameDrafts] = useState>({}); + + useEffect(() => { + setRenameDrafts((prev) => { + const next = { ...prev }; + const validIds = new Set(); + + slots.forEach((slot) => { + validIds.add(slot.id); + if (!(slot.id in next)) { + next[slot.id] = slot.name || `Slot ${slot.id}`; + } + }); + + Object.keys(next).forEach((key) => { + const id = Number(key); + if (!validIds.has(id)) { + delete next[id]; + } + }); + + return next; + }); + }, [slots]); + + return ( +
+ Activate = load slot into current build + + }> + {!slots.length ? ( + No slot data received from backend. + ) : ( + + {slots.map((slot) => { + const draftName = renameDrafts[slot.id] ?? slot.name ?? `Slot ${slot.id}`; + const summary = slot.summary || { stats: 0, skills: 0, traits: 0, items: 0 }; + + return ( + + + + + {slot.name} + + + {slot.active ? ( + + ACTIVE + + ) : null} + + + + + Spent: Stats - {summary.stats} | Skills - {summary.skills} | Traits -{' '} + {summary.traits} | Items - {summary.items} + + + + + setRenameDrafts((prev) => ({ ...prev, [slot.id]: String(value) })) + } + /> + + + + + + + + + + + + + ); + })} + + )} +
+ ); +}; + +const JsonExchangePanel = ({ + act, + buildJson, + lastJsonError, + lastJsonNotice, +}: { + act: BackendAct; + buildJson?: string | null; + lastJsonError?: string | null; + lastJsonNotice?: string | null; +}) => { + const [jsonDraft, setJsonDraft] = useState(''); + + useEffect(() => { + if (typeof buildJson === 'string' && buildJson.length > 0) { + setJsonDraft(buildJson); + } + }, [buildJson]); + + return ( +
+ + + + + + + + }> + {!!lastJsonNotice && {lastJsonNotice}} + {!!lastJsonError && {lastJsonError}} + + + Export creates a portable JSON build. Import rebuilds the current build through backend + validation, so invalid or outdated entries should be sanitized by the server. + + +