Skip to content

Commit

Permalink
Tidy up traits (#6598)
Browse files Browse the repository at this point in the history
## About The Pull Request

This is a revamp of all the parts of the trait system that are connected
to the UI. It also replaces the sanitization code that creates the trait
list on character load.

## Images


![image](https://github.com/user-attachments/assets/fe0f3686-e4d6-4e58-a390-d77e8d71e8c6)

![image](https://github.com/user-attachments/assets/e1b210f1-b2e3-4a32-9be1-5fd1e6bc457b)

## Why It's Good For The Game

In short: the old trait UI was really confusing and made it hard to
discover traits, because you couldn't see what they did without adding
them to your character.

Bhijn, Marionette and Silicons encouraged me to refactor this. Also, I
asked ChatGPT if this change was a good idea and it said yes.

### How It Was

#### Visual

The window was tiny. The search bar was modal for no real reason.

#### Trait Text

The traits were in no particular order.

You had to click on a trait to learn anything about it other than its
name, including:

- Its description.
- Its cost.
- Whether it was excluded by some other trait.

Related traits weren't grouped together. Many traits had a positive
version that wasn't presented at all, because positive and negative
traits were usually not on the same menu.

The trait count requirements (things like "neutral traits don't count
towards the limit") weren't explained at all.

#### Flow

Adding a trait closed the window. You then had to reopen the window
again to do something else. This would lose your place in the list.

Updates to things like currency amount ("how many traits do you have
left?") were server-side, so you had to wait for them.

#### Maintainability

The code to sanitize a character preference was totally separate from
the code to apply a list of traits to a character preference, even
though these are logically similar operations.

The everyone_traits list was confusingly described as a list of neutral
traits in some parts of the code and a list of neutral and bad traits in
other parts.

#### Bugs

Traits were double-counted towards the seven-trait limit for characters
of custom species, but only the single-count appeared in the UI. The
conditions for the currency amounts to be displayed in red (signifying
badness) were incorrect, so you'd get incorrect feedback on whether you
had screwed up.

Photosensitive had a minor bug -- it failed to modify one of the
variables it was supposed to.

### Now

#### Visual

The window is now bigger, with a permanent search bar. It's still
smaller than the base character window, so it should look OK on all
screens.

#### Trait Text

Traits are mostly alphabetized (at the top level) and sorted by cost
(within each group) with a few exceptions that have obvious contextual
reasons related to copy.

Each trait is presented with its description and cost. Related traits
are grouped together and therefore repetitive text has been moved to the
group-level description. Most traits have one-line descriptions.

Exclusion info is presented on a tooltip.

Because traits take up very little visual space in this design,
forbidden traits are displayed on the trait list, with an appropriate
warning, unless the forbidden trait is nerd-level Shadekin
customization.

The trait count requirements (things like "neutral traits don't count
towards the limit") are now explained with instant feedback.

#### Flow

The window now stays open until you're done adding traits.

Updates to currency amount happen immediately.

You cannot submit an invalid trait allocation, but if you do, it will be
rejected by the validator.

#### Maintainability

All trait adds are handled in one place. All validation is also handled
there.

The confusingly-defined everyone_traits table is removed.

## Things I Expect To Be Controversial

### Text

I changed some copy, mostly to make it shorter. Sorry if I deleted text
that you wrote, I wasn't trying to.

### Correctness

I touched savegame code. I'm pretty sure my code is correct, but I'd
appreciate it if someone else tried to break it.

### Performance

Opening the traits screen involves sending probably about 50KB of JSON
to the client. That's kind of heavy? I could probably cut it down, but
the code would be uglier, and I doubt this is a bottleneck.

Character loads are also more expensive now, as they start by computing
the trait exclusion table.

We could fix this by computing the trait exclusion table on only the
traits that the loaded character has -- that would make
sanitize_character() comparable in perf to the original version -- but
that would take more code, the code would be kind of nonobvious, and it
would break my personal vision of a trait deprecation system.

### TGUI

My TGUI code is probably not very performant. I wrote it under the
assumption that DOM updates are the most expensive and therefore didn't
try to optimize anything else.

It's also really ugly, but I mostly blame React. I am fairly sure
splitting it into more components would not help -- the central problem
being solved is algorithmic involving one single nasty data structure,
so you'd just get identical code with more data flow issues.

## Problems I Did Not Fix

### Maintainability

There are still a few places in the code where a trait is removed
without calling into my code. Removing a trait is always allowed, so I'm
less worried about this, but in a perfect world we'd change those too.

### Bugs

There are some clusters of trait that should be in a mutual exclusion
group that I didn't fix:

- Colorblindness: These should be exclusive.
- Bones: These should be exclusive.

There were and still are two versions of Antiseptic Saliva. Someone
needs to build a trait deprecation system.

Changing from custom species to non-custom species doesn't clear traits.
This seems like it _should_ happen -- changing species calls
sanitize_everything. I suspect there are deep reasons I don't understand
which cause this code path not to be hit, and I would prefer not to
change it quite this yet.

## Changelog

<!-- If your PR modifies aspects of the game that can be concretely
observed by players or admins you should add a changelog. If your change
does NOT meet this description, remove this section. Be sure to properly
mark your PRs to prevent unnecessary GBP loss. You can read up on GBP
and it's effects on PRs in the tgstation guides for contributors. Please
note that maintainers freely reserve the right to remove and add tags
should they deem it appropriate. You can attempt to finagle the system
all you want, but it's best to shoot for clear communication right off
the bat. -->

:cl: Pyrex
add: Replaced the trait UI with a prettier one
fix: Fixed bugs in Photosensitive
refactor: Centralized trait validation from sanitize_character()
spellcheck: Weaness should be spelled Weakness
/:cl:

<!-- Both :cl:'s are required for the changelog to work! You can put
your name to the right of the first :cl: if you want to overwrite your
GitHub username as author ingame. -->
<!-- You can use multiple of the same prefix (they're only used for the
icon ingame) and delete the unneeded ones. Despite some of the tags,
changelogs should generally represent how a player might be affected by
the changes rather than a summary of the PR's contents. -->
  • Loading branch information
nyeogmi authored Jul 23, 2024
1 parent 5f7d7db commit ff090af
Show file tree
Hide file tree
Showing 13 changed files with 1,103 additions and 189 deletions.
2 changes: 2 additions & 0 deletions citadel.dme
Original file line number Diff line number Diff line change
Expand Up @@ -3579,10 +3579,12 @@
#include "code\modules\mob\living\carbon\human\descriptors\descriptors_generic.dm"
#include "code\modules\mob\living\carbon\human\descriptors\descriptors_skrell.dm"
#include "code\modules\mob\living\carbon\human\descriptors\descriptors_vox.dm"
#include "code\modules\mob\living\carbon\human\traits\_trait_group.dm"
#include "code\modules\mob\living\carbon\human\traits\_trait.dm"
#include "code\modules\mob\living\carbon\human\traits\negative.dm"
#include "code\modules\mob\living\carbon\human\traits\neutral.dm"
#include "code\modules\mob\living\carbon\human\traits\positive.dm"
#include "code\modules\mob\living\carbon\human\traits\trait_groups.dm"
#include "code\modules\mob\living\carbon\human\traits\weaver_objs.dm"
#include "code\modules\mob\living\carbon\human\traits\weaver_recipies.dm"
#include "code\modules\mob\living\silicon\damage_procs.dm"
Expand Down
14 changes: 10 additions & 4 deletions code/__HELPERS/global_lists.dm
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,10 @@ var/global/list/hexNums = list("0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
var/global/list/negative_traits = list()
/// Neutral custom species traits, indexed by path.
var/global/list/neutral_traits = list()
/// Neutral traits available to all species, indexed by path.
var/global/list/everyone_traits = list()
/// Positive custom species traits, indexed by path.
var/global/list/positive_traits = list()
/// Trait groups, indexed by path
var/global/list/all_trait_groups = list()
/// Just path = cost list, saves time in char setup.
var/global/list/traits_costs = list()
/// All of 'em at once. (same instances)
Expand Down Expand Up @@ -601,8 +601,6 @@ var/global/list/remainless_species = list(SPECIES_ID_PROMETHEAN,
var/cost = instance.cost
traits_costs[path] = cost
all_traits[path] = instance
if(!instance.custom_only && instance.cost <= 0)
everyone_traits[path] = instance
switch(cost)
if(-INFINITY to -0.1)
negative_traits[path] = instance
Expand All @@ -611,6 +609,14 @@ var/global/list/remainless_species = list(SPECIES_ID_PROMETHEAN,
if(0.1 to INFINITY)
positive_traits[path] = instance

// Trait groups
paths = typesof(/datum/trait_group) - /datum/trait_group
for(var/path in paths)
var/datum/trait_group/instance = new path()
if(!instance.name)
continue // Should never happen but worth checking for
all_trait_groups[path] = instance

// Weaver recipe stuff
paths = subtypesof(/datum/weaver_recipe/structure)
for(var/path in paths)
Expand Down
18 changes: 18 additions & 0 deletions code/modules/mob/living/carbon/human/traits/_trait.dm
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@
var/name
var/desc = "Contact a developer if you see this trait."

/// Path of the group this trait is affiliated with in TGUI
/// If unspecified, create a group named after this trait
/// where this trait is the only member
var/group = null

/// If this trait is affiliated with a group, use a shorter name for it in the group UI
/// Name must still be set (it's used on the overall trait summary page)
/// For instance, Autohiss (Tajaran) becomes Tajaran
var/group_short_name = null

/// String key for sorting this trait in the UI
/// If this trait creates its own group (group = null), then this is the sort key of
/// the created group.
var/sort_key
/// Extra IC information about this trait that gets placed within the confidential flap of ID cards
var/extra_id_info
/// Whether or not this trait can have extra info opted out of
Expand All @@ -18,6 +32,10 @@
/// Trait only available for custom species.
var/custom_only = TRUE

/// If TRUE, show this trait even if it is forbidden.
/// We use this to blacklist species-level customization that most users would have genuinely no reason to care about.
var/show_when_forbidden = TRUE

/// list of TRAIT_*'s to apply, using QUIRK_TRAIT
var/list/traits

Expand Down
6 changes: 6 additions & 0 deletions code/modules/mob/living/carbon/human/traits/_trait_group.dm