diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index 0820e9e6c2b..0cfce579796 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -313,6 +313,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(
@@ -572,6 +576,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 10b110ada10..89d1237fb1c 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -261,6 +261,7 @@ GLOBAL_LIST_EMPTY(chosen_names)
var/audio_preload
var/preloaded = FALSE //Bool Check
+ var/datum/tat_build/tat_build //CC + TA edit
//CC Edit - Roleplay Guidance Pref, whether you encourage PvP and wish to fight others if invited or discourage PvP and wish to avoid fighting,
//but does not exempt you from combat or the consequences of your own actions.
var/rp_guidance = 3 //Defaults to Default by Default.
@@ -269,6 +270,7 @@ GLOBAL_LIST_EMPTY(chosen_names)
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)
@@ -718,7 +720,7 @@ GLOBAL_LIST_EMPTY(chosen_names)
dat += "
NSFW Image Gallery: Add"
dat += "Clear Gallery"
dat += "
Preview Examine"
-
+ dat += "
Pliant Soul Settings: Change" //CC + TA edit
dat += "
Loadout: Open Menu"
dat += ""
dat += ""
@@ -2386,6 +2388,11 @@ 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
//CC Edit - Roleplay Guidance, this option should be FALSE BY DEFAULT, to encourage Conflict you MUST go into the Game Settings to enable it.
if("roleplay_guidance")
var/list/choices = list("Conflict Encouraged + Hunted", "Conflict Encouraged", "Conflict Discouraged", "Default")
diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm
index 520e8b081df..d29d7bf8940 100644
--- a/code/modules/client/preferences_savefile.dm
+++ b/code/modules/client/preferences_savefile.dm
@@ -229,7 +229,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)
@@ -273,6 +279,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
@@ -381,6 +388,8 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
WRITE_FILE(S["audio_preload"], audio_preload)
WRITE_FILE(S["rp_guidance"], rp_guidance)
///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 4948184324b..f3ca9ac88a9 100644
--- a/code/modules/mob/living/carbon/human/examine.dm
+++ b/code/modules/mob/living/carbon/human/examine.dm
@@ -76,6 +76,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
@@ -100,14 +102,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..c1d3dae6cbd
--- /dev/null
+++ b/modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_items.dm
@@ -0,0 +1,762 @@
+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/anthro = TAT_ITEM_ENTRY("Steppesman Beast 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/iron/bone = TAT_ITEM_ENTRY("Bone 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..b0eae0f9f76
--- /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_COMBAT_BOWS "Bows Skill (JMAN)"
+#define TAT_VIRTUE_CHOICE_COMBAT_CROSSBOWS "Crossbows 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/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/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), \
+ TAT_VIRTUE_CHOICE_COMBAT_BOWS = list(/datum/skill/combat/bows = SKILL_LEVEL_JOURNEYMAN), \
+ TAT_VIRTUE_CHOICE_COMBAT_CROSSBOWS = list(/datum/skill/combat/crossbows = 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..5ae1bd678a7
--- /dev/null
+++ b/modular_twilight_axis/code/datums/tat_system/_defines/tat_defines_traits.dm
@@ -0,0 +1,532 @@
+#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_FREEPTS "tat_freepts"
+
+#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 30
+
+#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_DEFILED_NOBLE, \
+ TRAIT_JESTERPHOBIA, \
+ 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."),
+//TAT_TRAIT_FREEPTS = TAT_TRAIT_ENTRY("Free points", -200, TAT_CATEGORY_COMBAT_MASTERY, TAT_CATEGORY_ENHANCEMENT_NAME, "You are an admin, congrats! Have some free points. Hope you will use it wisely."),
+#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", 0, 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 30+ 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_JESTERPHOBIA = TAT_TRAIT_ENTRY("Jesterphobic", 0, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "I have a severe irrational fear of Jesters."), \
+ TRAIT_DEFILED_NOBLE = TAT_TRAIT_ENTRY("Drained Noble Blood", 0, TAT_CATEGORY_ODDITY, TAT_CATEGORY_ODDITY_NAME, "I'm of noble blood but... Something feels off!"), \
+ 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..e5b6cd522e0
--- /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..cc83639bc40
--- /dev/null
+++ b/modular_twilight_axis/code/datums/tat_system/domains/tat_traits.dm
@@ -0,0 +1,1208 @@
+/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
+ //if(trait_id == TAT_TRAIT_FREEPTS && !owner_build?.can_select_contractor_trait())
+ //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, TRAIT_EASYDISMEMBER),
+ 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_SAVAGE_SKIN = (TRAIT_NUDIST),
+ 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_REGROW_LIMBS),
+ TRAIT_REVERSE_GUIDANCE = list(TRAIT_LESSER_REVERSE_GUIDANCE),
+ TRAIT_NOPAINSTUN = list(TAT_TRAIT_MAGE_INITIATE),
+ TRAIT_HARDDISMEMBER = list(TRAIT_EASYDISMEMBER),
+ TRAIT_DEFILED_NOBLE = list(TRAIT_NOBLE)
+ )
+ 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_WITCH
+ return CLERIC_REGEN_MINOR
+
+/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 ? (
+

+ ) : (
+
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 (
+
+ );
+};
+
+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.
+
+
+
+ );
+};
+
+const ControlTab = ({
+ slots,
+ act,
+ buildJson,
+ lastJsonError,
+ lastJsonNotice,
+}: {
+ slots: TatSlotEntry[];
+ act: BackendAct;
+ buildJson?: string | null;
+ lastJsonError?: string | null;
+ lastJsonNotice?: string | null;
+}) => {
+ return (
+
+
+
+
+ );
+};
+
+const StatsTab = ({ data, act, search }: { data: Data; act: BackendAct; search: string }) => {
+ const rows = useMemo(
+ () =>
+ Object.entries(data.available_stats || {}).filter(([statId, entry]) =>
+ matchesSearch(search, entry.name, statId)
+ ),
+ [data.available_stats, search]
+ );
+
+ return (
+ }>
+ {!rows.length ? (
+ No matches found.
+ ) : (
+
+ {rows.map(([statId, entry]) => {
+ const value = data.stats?.[statId] ?? entry.base;
+ return (
+ act('add_stat', { id: statId, amount: 1 })}
+ onRemove={() => act('remove_stat', { id: statId, amount: 1 })}
+ disabledAdd={value >= entry.max || data.points_stats_remaining < entry.cost}
+ disabledRemove={value <= 1}
+ extra={Base: {entry.base} | Refund floor: {entry.min} | Max: {entry.max} | Cost per step: {entry.cost}}
+ />
+ );
+ })}
+
+ )}
+
+ );
+};
+
+const SkillRow = ({
+ skillPath,
+ entry,
+ state,
+ act,
+ domainRemaining,
+ setHoveredItem,
+}: {
+ skillPath: string;
+ entry: SkillEntry;
+ state?: SkillState;
+ act: BackendAct;
+ domainRemaining: number | null;
+ setHoveredItem: (value: HoverCardData | null) => void;
+}) => {
+ const totalLevel = Number(state?.level) || 0;
+ const invested = Number(state?.invested) || 0;
+ const cap = Number(state?.cap) || 0;
+ const nextCost = Number(state?.next_cost) || 0;
+ const bonus = Number(state?.bonus) || 0;
+ const displayValue = formatSkillDisplayValue(state);
+
+ const disableRemove = invested <= 0;
+ const disableAdd = totalLevel >= cap || nextCost <= 0 || (domainRemaining !== null && domainRemaining < nextCost);
+
+ return (
+
+ setHoveredItem({
+ name: entry.name || skillPath,
+ desc: entry.desc,
+ category: entry.category,
+ level: totalLevel,
+ cap,
+ costText: `${nextCost} pts`,
+ bonus,
+ invested,
+ domainRemaining,
+ leftHelp: 'Press + to increase',
+ rightHelp: 'Press - to refund',
+ })
+ }
+ onMouseLeave={() => setHoveredItem(null)}>
+
+
+ {entry.name || skillPath}
+
+ Cost: {nextCost} | Type: {entry.category || 'unknown'} | Cap: {cap}
+ {bonus > 0 ? ` | Bonus: ${bonus}` : ''}
+
+
+
+
+
+
+
+
+
+ {displayValue}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const SkillDomainTitle = ({
+ domain,
+ data,
+ act,
+}: {
+ domain: SkillDomainKey;
+ data: Data;
+ act: BackendAct;
+}) => {
+ const domainState = data.skill_conversion_state?.[domain];
+ const pool = Number(data.skill_conversion_pool) || 0;
+ const remaining = getDomainRemainingPoints(data, domain) || 0;
+ const canGive = typeof domainState?.can_give === 'boolean' ? domainState.can_give : remaining > 0;
+ const canTake =
+ typeof domainState?.can_take === 'boolean'
+ ? domainState.can_take
+ : domain !== 'combat' && pool > 0;
+
+ return (
+
+
+ {SKILL_DOMAIN_TITLES[domain]}
+
+
+
+
+
+ {formatDomainPoints(data, domain)}
+
+
+
+
+
+ {domain !== 'combat' && (
+
+
+
+ )}
+
+
+
+ );
+};
+
+const SkillsDomainPanel = ({
+ domain,
+ rows,
+ data,
+ act,
+ setHoveredItem,
+}: {
+ domain: SkillDomainKey;
+ rows: Array<[string, SkillEntry]>;
+ data: Data;
+ act: BackendAct;
+ setHoveredItem: (value: HoverCardData | null) => void;
+}) => {
+ const domainRemaining = getDomainRemainingPoints(data, domain);
+
+ return (
+
+ }
+ fill
+ style={{ height: '320px' }}>
+ {!rows.length ? (
+ No skills in this group.
+ ) : (
+
+ {rows.map(([skillPath, entry]) => (
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+const SkillsTab = ({
+ data,
+ act,
+ search,
+ setHoveredItem,
+}: {
+ data: Data;
+ act: BackendAct;
+ search: string;
+ setHoveredItem: (value: HoverCardData | null) => void;
+}) => {
+ const groups = useMemo(() => {
+ const byDomain: Record> = {
+ combat: [],
+ wandering: [],
+ gathering: [],
+ crafting: [],
+ misc: [],
+ };
+
+ Object.entries(data.available_skills || {}).forEach(([skillPath, entry]) => {
+ if (!matchesSearch(search, skillPath, entry.name, entry.desc, entry.category, entry.is_combat ? 'combat' : 'non-combat')) {
+ return;
+ }
+ const domain = normalizeSkillDomain(entry.category);
+ byDomain[domain].push([skillPath, entry]);
+ });
+
+ SKILL_DOMAIN_ORDER.forEach((domain) => {
+ byDomain[domain].sort((a, b) => (a[1].name || a[0]).localeCompare(b[1].name || b[0]));
+ });
+
+ return byDomain;
+ }, [data.available_skills, search]);
+
+ const hasAny = SKILL_DOMAIN_ORDER.some((domain) => groups[domain].length > 0);
+
+ return (
+
+ }>
+ {!hasAny ? (
+ No matches found.
+ ) : (
+
+ {SKILL_DOMAIN_ORDER.map((domain) => (
+
+ ))}
+
+ )}
+
+ );
+};
+
+const TraitPill = ({
+ title,
+ cost,
+ amount,
+ repeatable,
+ selected,
+ disabledAdd,
+ disabledRemove,
+ onAdd,
+ onRemove,
+ onHoverStart,
+ onHoverEnd,
+}: {
+ title: string;
+ cost: number;
+ amount?: number;
+ repeatable?: boolean;
+ selected?: boolean;
+ disabledAdd?: boolean;
+ disabledRemove?: boolean;
+ onAdd: () => void;
+ onRemove: () => void;
+ onHoverStart?: () => void;
+ onHoverEnd?: () => void;
+}) => {
+ const countText = repeatable && amount && amount > 0 ? ` x${amount}` : '';
+ const fullyDisabled = !!disabledAdd && !!disabledRemove;
+
+ return (
+ {
+ event.preventDefault();
+ event.stopPropagation();
+ if (!disabledRemove) {
+ onRemove();
+ }
+ }}
+ onMouseEnter={onHoverStart}
+ onMouseLeave={onHoverEnd}
+ style={{ display: 'inline-block' }}>
+
+
+ );
+};
+
+const TraitsTab = ({
+ data,
+ act,
+ search,
+ setHoveredItem,
+}: {
+ data: Data;
+ act: BackendAct;
+ search: string;
+ setHoveredItem: (value: HoverCardData | null) => void;
+}) => {
+ const grouped = useMemo(() => {
+ const groups: Record; selected: Array<[string, TraitEntry]> }> = {};
+
+ Object.entries(data.available_traits || {})
+ .filter(([traitId, entry]) => matchesSearch(search, traitId, entry.name, entry.desc, entry.category, entry.category_name))
+ .forEach(([traitId, entry]) => {
+ const category = entry.category || 'other';
+ const categoryName = entry.category_name || 'Other';
+ if (!groups[category]) {
+ groups[category] = { categoryName, available: [], selected: [] };
+ }
+ if (getTraitAmount(data, traitId) > 0) {
+ groups[category].selected.push([traitId, entry]);
+ } else {
+ groups[category].available.push([traitId, entry]);
+ }
+ });
+
+ Object.values(groups).forEach((group) => {
+ group.available.sort((a, b) => (a[1].name || a[0]).localeCompare(b[1].name || b[0]));
+ group.selected.sort((a, b) => (a[1].name || a[0]).localeCompare(b[1].name || b[0]));
+ });
+
+ return Object.entries(groups).sort((a, b) => a[1].categoryName.localeCompare(b[1].categoryName));
+ }, [data, search]);
+
+ const buildTraitHover = (traitId: string, entry: TraitEntry): HoverCardData => {
+ const amount = getTraitAmount(data, traitId);
+ const canAdd = canAddTrait(data, traitId, entry);
+ return {
+ name: entry.name || traitId,
+ desc: entry.desc,
+ category: entry.category_name || entry.category,
+ costText: `${entry.cost || 0} pts`,
+ total: amount,
+ canAdd,
+ leftHelp: canAdd ? 'LMB: add trait / increase stack' : 'Cannot add more',
+ rightHelp: amount > 0 ? 'RMB: remove trait / decrease stack' : 'RMB: nothing to remove',
+ };
+ };
+
+ return (
+ }>
+ {!grouped.length ? (
+ No matches found.
+ ) : (
+
+ {grouped.map(([categoryKey, group]) => (
+
+
+ {group.categoryName}
+
+
+ Pool
+ {group.available.length ? (
+
+ {group.available.map(([traitId, entry]) => {
+ const amount = getTraitAmount(data, traitId);
+ const canAdd = canAddTrait(data, traitId, entry);
+ return (
+
+ act('add_trait', { id: traitId, amount: 1 })}
+ onRemove={() => act('remove_trait', { id: traitId, amount: 1 })}
+ onHoverStart={() => setHoveredItem(buildTraitHover(traitId, entry))}
+ onHoverEnd={() => setHoveredItem(null)}
+ />
+
+ );
+ })}
+
+ ) : (
+ No available traits in this group.
+ )}
+
+ Selected
+ {group.selected.length ? (
+
+ {group.selected.map(([traitId, entry]) => {
+ const amount = getTraitAmount(data, traitId);
+ const canAdd = canAddTrait(data, traitId, entry);
+ return (
+
+ act('add_trait', { id: traitId, amount: 1 })}
+ onRemove={() => act('remove_trait', { id: traitId, amount: 1 })}
+ onHoverStart={() => setHoveredItem(buildTraitHover(traitId, entry))}
+ onHoverEnd={() => setHoveredItem(null)}
+ />
+
+ );
+ })}
+
+ ) : (
+ No selected traits in this group.
+ )}
+
+ ))}
+
+ )}
+
+ );
+};
+
+const ItemsTab = ({
+ itemEntries,
+ act,
+ search,
+ setHoveredItem,
+ itemsAvailable,
+ data,
+}: {
+ itemEntries: Record;
+ act: BackendAct;
+ search: string;
+ setHoveredItem: (value: HoverCardData | null) => void;
+ itemsAvailable: boolean;
+ data: Data;
+}) => {
+ const groups = useMemo(() => {
+ return groupEntriesByCategoryAndSlot(
+ itemEntries || {},
+ (itemPath, entry) =>
+ !!entry.unlocked && matchesSearch(search, itemPath, entry.name, entry.category, entry.slot_group, entry.unlock_type, entry.unlock_key)
+ );
+ }, [itemEntries, search]);
+
+ return (
+ }>
+ {!itemsAvailable ? (
+ Loading items...
+ ) : !groups.length ? (
+ No matches found.
+ ) : (
+
+ {groups.map(([categoryKey, slotGroups]) => (
+
+
+ {getCategoryLabel(categoryKey)}
+
+
+ {slotGroups.map(([slotKey, items]) => {
+ const visibleItems = items.slice(0, MAX_RENDERED_ITEMS_PER_SLOT);
+ return (
+
+
+ {getSlotLabel(slotKey)}
+
+
+
+ {visibleItems.map(([itemPath, entry]) => {
+ const canAdd = entry.can_add !== false;
+ const maximum = Number(entry.maximum);
+ const amount = Number(entry.amount) || 0;
+ return (
+ 0 ? amount : undefined}
+ bottomRightText={!canAdd ? 'MAX' : undefined}
+ icon={entry.icon}
+ disabled={!canAdd}
+ onLeftClick={() => act('add_item', { path: itemPath, amount: 1 })}
+ onRightClick={() => act('remove_item', { path: itemPath, amount: 1 })}
+ onHoverStart={() =>
+ setHoveredItem({
+ name: entry.name || itemPath,
+ slot: getSlotLabel(entry.slot_group),
+ category: getCategoryLabel(entry.category),
+ costText: `${entry.cost || 0} pts`,
+ total: amount,
+ maximum: Number.isFinite(maximum) ? maximum : undefined,
+ canAdd,
+ leftHelp: canAdd ? 'LMB: add item' : 'Cannot add more',
+ rightHelp: 'RMB: remove item',
+ })
+ }
+ onHoverEnd={() => setHoveredItem(null)}
+ />
+ );
+ })}
+
+
+ {items.length > MAX_RENDERED_ITEMS_PER_SLOT && (
+ Showing first {MAX_RENDERED_ITEMS_PER_SLOT} items. Use search to narrow results.
+ )}
+
+ );
+ })}
+
+ ))}
+
+ )}
+
+ );
+};
+
+const LoadoutTab = ({
+ loadoutEntries,
+ act,
+ search,
+ setHoveredItem,
+}: {
+ loadoutEntries: Record;
+ act: BackendAct;
+ search: string;
+ setHoveredItem: (value: HoverCardData | null) => void;
+}) => {
+ const [selectedSlotId, setSelectedSlotId] = useState(LOADOUT_DOLL_SLOTS[0].id);
+ const [chooserOpen, setChooserOpen] = useState(false);
+
+ const visibleEntries = useMemo(
+ () =>
+ Object.entries(loadoutEntries || {}).filter(([itemPath, entry]) =>
+ matchesSearch(search, itemPath, entry.name, entry.category, entry.slot_group)
+ ),
+ [loadoutEntries, search]
+ );
+
+ const selectedSlot = LOADOUT_DOLL_SLOTS.find((slot) => slot.id === selectedSlotId) || LOADOUT_DOLL_SLOTS[0];
+
+ const selectedSlotEntries = useMemo(
+ () =>
+ visibleEntries
+ .filter(([, entry]) => {
+ if (!entryCanUseLoadoutSlot(entry, selectedSlot.id)) {
+ return false;
+ }
+ return (Number(entry.bag) || 0) > 0 || entryIsAssignedToLoadoutSlot(entry, selectedSlot.id);
+ })
+ .sort((a, b) => {
+ const assignedA = entryIsAssignedToLoadoutSlot(a[1], selectedSlot.id) ? 1 : 0;
+ const assignedB = entryIsAssignedToLoadoutSlot(b[1], selectedSlot.id) ? 1 : 0;
+ if (assignedA !== assignedB) {
+ return assignedB - assignedA;
+ }
+ const bagDiff = (Number(b[1].bag) || 0) - (Number(a[1].bag) || 0);
+ if (bagDiff !== 0) {
+ return bagDiff;
+ }
+ return (a[1].name || a[0]).localeCompare(b[1].name || b[0]);
+ }),
+ [visibleEntries, selectedSlot]
+ );
+
+ const backpackEntries = useMemo(
+ () =>
+ visibleEntries
+ .filter(([, entry]) => (Number(entry.bag) || 0) > 0)
+ .sort((a, b) => (a[1].name || a[0]).localeCompare(b[1].name || b[0])),
+ [visibleEntries]
+ );
+
+ const stashEntries = useMemo(
+ () =>
+ visibleEntries
+ .filter(([, entry]) => (Number(entry.stash) || 0) > 0)
+ .sort((a, b) => (a[1].name || a[0]).localeCompare(b[1].name || b[0])),
+ [visibleEntries]
+ );
+
+ const hasAnyEntries = visibleEntries.length > 0;
+ const selectedAssignedEntry = getAssignedEntryForLoadoutSlot(visibleEntries, selectedSlot.id);
+
+ const openSlotChooser = (slotId: string) => {
+ setSelectedSlotId(slotId);
+ setChooserOpen(true);
+ };
+
+ const buildInventoryHover = (itemPath: string, entry: LoadoutViewEntry, area: 'bag' | 'stash'): HoverCardData => {
+ const amount = Number(entry.amount) || 0;
+ const bag = Math.max(0, Math.min(Number(entry.bag) || 0, amount));
+ const stash = Math.max(0, Math.min(Number(entry.stash) || 0, amount));
+ const equip = Math.max(0, Number(entry.equip) || 0);
+ const sourceText = getLoadoutSourceText(entry);
+ const paintText = getLoadoutPaintText(entry);
+
+ return {
+ name: entry.name || itemPath,
+ slot: area === 'bag' ? 'Backpack' : 'Stash',
+ category: getCategoryLabel(entry.category),
+ total: amount,
+ bag,
+ stash,
+ equip,
+ desc: [sourceText, paintText].filter(Boolean).join('\n'),
+ leftHelp: area === 'bag' ? 'LMB: move to stash' : 'LMB: move to backpack',
+ rightHelp: 'RMB: dye / repaint item',
+ };
+ };
+
+ const renderInventoryRow = (itemPath: string, entry: LoadoutViewEntry, area: 'bag' | 'stash') => {
+ const amount = Number(entry.amount) || 0;
+ const bag = Math.max(0, Math.min(Number(entry.bag) || 0, amount));
+ const stash = Math.max(0, Math.min(Number(entry.stash) || 0, amount));
+ const equip = Math.max(0, Number(entry.equip) || 0);
+ const validSlotLabels = getLoadoutValidSlots(entry).map(getLoadoutSlotLabel).join(', ');
+ const sourceText = getLoadoutSourceText(entry);
+ const paintText = getLoadoutPaintText(entry);
+ const count = area === 'bag' ? bag : stash;
+
+ return (
+
+ act(area === 'bag' ? 'move_item_to_stash' : 'move_item_to_bag', { path: itemPath, amount: 1 })
+ }
+ onContextMenu={(event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ act('paint_loadout_item', { path: itemPath });
+ }}
+ onMouseEnter={() => setHoveredItem(buildInventoryHover(itemPath, entry, area))}
+ onMouseLeave={() => setHoveredItem(null)}
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ minHeight: '82px',
+ gap: '10px',
+ padding: '6px 8px',
+ marginBottom: '8px',
+ borderRadius: '6px',
+ background: area === 'bag' ? 'rgba(80,110,170,0.12)' : 'rgba(150,120,70,0.12)',
+ border: area === 'bag' ? '1px solid rgba(130,160,220,0.4)' : '1px solid rgba(220,180,110,0.35)',
+ cursor: 'pointer',
+ userSelect: 'none',
+ }}>
+
+
+
+
+
+
{entry.name || itemPath}
+
+ Bag {bag} · Stash {stash} · Equip {equip} · Total {amount}
+
+ {!!validSlotLabels &&
Slots: {validSlotLabels}
}
+ {!!sourceText &&
{sourceText}
}
+ {!!paintText &&
Paint: {paintText}
}
+
+
+
+
{count} here
+
{area === 'bag' ? 'LMB: stash' : 'LMB: bag'}
+
RMB: dye
+
+
+ );
+ };
+
+ return (
+
+ {!hasAnyEntries ? (
+ No matches found.
+ ) : (
+
+
+
}>
+
+ {LOADOUT_DOLL_SLOTS.map((slot) => {
+ const counts = getLoadoutSlotCounts(visibleEntries, slot);
+ const isSelected = selectedSlot.id === slot.id;
+ const assigned = getAssignedEntryForLoadoutSlot(visibleEntries, slot.id);
+ const hasCompatible = counts.total > 0;
+ const hasEquipped = !!assigned;
+
+ return (
+ openSlotChooser(slot.id)}
+ onContextMenu={(event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ setSelectedSlotId(slot.id);
+ if (assigned) {
+ act('paint_loadout_item', { path: assigned[0] });
+ }
+ }}
+ onMouseEnter={() => {
+ if (assigned) {
+ setHoveredItem({
+ name: assigned[1].name || assigned[0],
+ slot: slot.label,
+ category: getCategoryLabel(assigned[1].category),
+ total: Number(assigned[1].amount) || 0,
+ bag: Number(assigned[1].bag) || 0,
+ stash: Number(assigned[1].stash) || 0,
+ equip: Number(assigned[1].equip) || 0,
+ desc: [getLoadoutSourceText(assigned[1]), getLoadoutPaintText(assigned[1])].filter(Boolean).join('\n'),
+ leftHelp: `LMB: choose item for ${slot.label}`,
+ rightHelp: 'RMB: dye / repaint equipped item',
+ });
+ } else {
+ setHoveredItem({
+ name: slot.label,
+ slot: slot.label,
+ category: 'Loadout slot',
+ total: counts.total,
+ bag: counts.bag,
+ stash: counts.stash,
+ equip: counts.equip,
+ leftHelp: `LMB: choose item for ${slot.label}`,
+ });
+ }
+ }}
+ onMouseLeave={() => setHoveredItem(null)}
+ style={{
+ position: 'absolute',
+ top: slot.top,
+ left: slot.left,
+ width: slot.width,
+ height: slot.height,
+ borderRadius: '6px',
+ border: isSelected
+ ? '1px solid rgba(240,195,90,0.95)'
+ : hasEquipped
+ ? '1px solid rgba(120,200,120,0.65)'
+ : hasCompatible
+ ? '1px solid rgba(130,160,220,0.45)'
+ : '1px solid rgba(255,255,255,0.10)',
+ background: isSelected
+ ? 'rgba(240,195,90,0.16)'
+ : hasEquipped
+ ? 'rgba(80,160,90,0.14)'
+ : hasCompatible
+ ? 'rgba(80,110,170,0.12)'
+ : 'rgba(20,26,38,0.65)',
+ cursor: 'pointer',
+ boxShadow: isSelected ? '0 0 0 1px rgba(240,195,90,0.25)' : 'none',
+ overflow: 'hidden',
+ }}>
+
+ {assigned ? (
+
+
+
+ ) : (
+
{slot.shortLabel || slot.label}
+ )}
+
+
+
+ {slot.shortLabel || slot.label}
+
+
+ {hasCompatible && (
+
+ {hasEquipped ? 'EQ' : counts.total}
+
+ )}
+
+ );
+ })}
+
+ {chooserOpen && (
+
+
+
+ {selectedSlot.label}
+ {selectedSlotEntries.length} compatible bag item(s)
+
+
+
+ {!!selectedAssignedEntry && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ {!selectedSlotEntries.length ? (
+ No backpack items can be equipped into this slot.
+ ) : (
+
+ {selectedSlotEntries.slice(0, MAX_RENDERED_ITEMS_PER_SLOT).map(([itemPath, entry]) => {
+ const assigned = entryIsAssignedToLoadoutSlot(entry, selectedSlot.id);
+ const bag = Number(entry.bag) || 0;
+ const equip = Number(entry.equip) || 0;
+ return (
+ 0 ? `B${bag}` : ''}
+ bottomLeftText={equip > 0 ? `E${equip}` : ''}
+ bottomRightText={assigned ? selectedSlot.shortLabel || 'SET' : ''}
+ glow={assigned ? 'rgba(120,200,120,0.65)' : 'rgba(130,160,220,0.35)'}
+ disabled={!assigned && bag <= 0}
+ onLeftClick={() => {
+ if (bag > 0 || assigned) {
+ act('assign_item_to_loadout_slot', { path: itemPath, slot_id: selectedSlot.id });
+ setChooserOpen(false);
+ }
+ }}
+ onRightClick={() => act('paint_loadout_item', { path: itemPath })}
+ onHoverStart={() =>
+ setHoveredItem({
+ name: entry.name || itemPath,
+ slot: selectedSlot.label,
+ category: getCategoryLabel(entry.category),
+ total: Number(entry.amount) || 0,
+ bag,
+ stash: Number(entry.stash) || 0,
+ equip,
+ desc: [getLoadoutSourceText(entry), getLoadoutPaintText(entry)].filter(Boolean).join('\n'),
+ leftHelp: bag > 0 ? `LMB: equip to ${selectedSlot.label}` : 'No copy in backpack',
+ rightHelp: 'RMB: dye / repaint item',
+ })
+ }
+ onHoverEnd={() => setHoveredItem(null)}
+ />
+ );
+ })}
+
+ )}
+
+
+ LMB item: equip from backpack. Clear slot: moves equipped item back to backpack. RMB item: dye.
+
+
+ )}
+
+
+
+
+
+
+
+ } fill>
+ {!backpackEntries.length ? (
+ Backpack is empty.
+ ) : (
+
+ {backpackEntries.map(([itemPath, entry]) => renderInventoryRow(itemPath, entry, 'bag'))}
+
+ )}
+
+
+
+
+ } fill>
+ {!stashEntries.length ? (
+ Stash is empty.
+ ) : (
+
+ {stashEntries.map(([itemPath, entry]) => renderInventoryRow(itemPath, entry, 'stash'))}
+
+ )}
+
+
+
+
+
+ )}
+
+ );
+};
+
+export const TATBuild = () => {
+ const { act, data } = useBackend();
+ const [tab, setTab] = useState('control');
+ const [search, setSearch] = useState('');
+ const [hoveredItem, setHoveredItem] = useState(null);
+
+ const tatSlots = useMemo(() => normalizeTatSlots(data.tat_slots, data.active_tat_slot), [data.tat_slots, data.active_tat_slot]);
+
+ const itemEntries = useMemo>(() => {
+ const result: Record = {};
+ const staticEntries = data.available_items || {};
+ const states = data.items_state || {};
+
+ Object.entries(staticEntries).forEach(([itemPath, entry]) => {
+ const state = states[itemPath];
+ result[itemPath] = {
+ ...entry,
+ amount: state?.amount || 0,
+ unlocked: !!state?.unlocked,
+ maximum: state?.maximum,
+ can_add: state?.can_add,
+ };
+ });
+
+ return result;
+ }, [data.available_items, data.items_state]);
+
+ const loadoutEntries = useMemo>(() => {
+ const result: Record = {};
+ const staticEntries = data.available_items || {};
+ const loadoutStates = data.loadout || {};
+
+ Object.entries(loadoutStates).forEach(([itemPath, state]) => {
+ const entry = staticEntries[itemPath];
+ if (!entry) {
+ return;
+ }
+
+ result[itemPath] = {
+ ...entry,
+ amount: state.amount || 0,
+ equip: state.equip || 0,
+ bag: state.bag || 0,
+ stash: state.stash || 0,
+ slots: state.slots || {},
+ valid_slots: state.valid_slots || [],
+ sources: state.sources || {},
+ paint: state.paint || null,
+ icon: state.icon || entry.icon,
+ icon_state: state.icon_state || entry.icon_state,
+ };
+ });
+
+ return result;
+ }, [data.available_items, data.loadout]);
+
+ const itemsAvailable = !!data.available_items && Object.keys(data.available_items).length > 0;
+ const searchPlaceholder = tab === 'control' ? 'Search legacy presets...' : `Search in ${tab}...`;
+
+ return (
+
+
+
+
+
+ {data.dirty ? Build has unsaved changes. : Build is saved.}
+
+ {!data.can_save && (
+
+ Current build is invalid:
+ {data.validation_issues?.length ? (
+
+ {data.validation_issues.map((issue, index) => (
+ • {issue}
+ ))}
+
+ ) : (
+ Current build is invalid or exceeds available points.
+ )}
+
+ )}
+
+ Save writes current build into the active slot}>
+
+ setTab('control')}>Control
+ setTab('stats')}>Stats
+ setTab('skills')}>Skills
+ setTab('traits')}>Traits
+ setTab('items')}>Items
+ setTab('loadout')}>Loadout
+
+
+
+ {tab === 'control' && (
+
+ )}
+ {tab === 'stats' && }
+ {tab === 'skills' && }
+ {tab === 'traits' && }
+ {tab === 'items' && (
+
+ )}
+ {tab === 'loadout' && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default TATBuild;
diff --git a/tgui/packages/tgui/interfaces/TatRoleLocksPanel.tsx b/tgui/packages/tgui/interfaces/TatRoleLocksPanel.tsx
new file mode 100644
index 00000000000..2ac8bb28925
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/TatRoleLocksPanel.tsx
@@ -0,0 +1,274 @@
+import { useBackend } from 'tgui/backend';
+import { Window } from 'tgui/layouts';
+import {
+ Box,
+ Button,
+ Dropdown,
+ Input,
+ LabeledList,
+ NumberInput,
+ Section,
+ Stack,
+ Table,
+} from 'tgui-core/components';
+
+const ROLE_HINTS: Record = {
+ towner: 'Local / Resident / Towner Pliant bucket',
+ trader: 'Default trader-ish TAT bucket',
+ adventurer: 'Outlander / Wanted / Adventurer bucket',
+};
+
+const INTERVAL_OPTIONS = [
+ 'MINUTE',
+ 'HOUR',
+ 'DAY',
+ 'WEEK',
+ 'MONTH',
+ 'YEAR',
+];
+
+const SEVERITY_OPTIONS = [
+ 'None',
+ 'Minor',
+ 'Medium',
+ 'High',
+];
+
+type PlayerRow = {
+ key: string;
+ ckey: string;
+ mob_name: string;
+ selected: boolean;
+};
+
+type RoleRow = {
+ id: string;
+ name: string;
+ locked: boolean;
+ state: string;
+ reason: string;
+ locked_by: string;
+ locked_at: string;
+ expires: string;
+ ban_id: string;
+};
+
+type Data = {
+ players: PlayerRow[];
+ selected_ckey: string;
+ filter: string;
+ default_reason: string;
+ duration: number;
+ interval: string;
+ permanent: boolean;
+ severity: string;
+ applies_to_admins: boolean;
+ roles: RoleRow[];
+};
+
+export const TatRoleLocksPanel = () => {
+ const { act, data } = useBackend();
+ const {
+ players = [],
+ selected_ckey,
+ filter = '',
+ default_reason = '',
+ duration = 10080,
+ interval = 'MINUTE',
+ permanent = false,
+ severity = 'Medium',
+ applies_to_admins = false,
+ roles = [],
+ } = data;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Role
+ Status
+ Reason / metadata
+ Action
+
+
+ {roles.map((role) => (
+
+
+ {role.name}
+
+ {ROLE_HINTS[role.id] || ''}
+
+
+
+
+ {role.state}
+
+
+
+ {role.locked ? (
+ <>
+ {role.reason || 'No reason'}
+
+ {role.locked_by ? `by ${role.locked_by}` : ''}{' '}
+ {role.locked_at || ''}
+
+
+ {role.expires || 'Permanent'}
+
+ >
+ ) : (
+ Open
+ )}
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/layouts/Window.tsx b/tgui/packages/tgui/layouts/Window.tsx
index 6c979c7b612..ffb6a0e6b39 100644
--- a/tgui/packages/tgui/layouts/Window.tsx
+++ b/tgui/packages/tgui/layouts/Window.tsx
@@ -18,7 +18,8 @@ import type { KeyEvent } from 'tgui-core/events';
import { KEY_ALT } from 'tgui-core/keycodes';
import { type BooleanLike, classes } from 'tgui-core/react';
import { decodeHtmlEntities } from 'tgui-core/string';
-import { useBackend } from '../backend';
+import { backendSuspendStart, globalStore, useBackend } from '../backend';
+import { useDebug } from '../debug';
import {
dragStartHandler,
recallWindowGeometry,