New Changeling UI (#94697)

## About The Pull Request

Updates the UI for Changeling, adding a categorization of abilities,
changes the old Pheromone Receptors ability sprite, adds a sprite for
Defibrillator Grasp.
This commit is contained in:
Maximal08
2026-01-11 23:17:56 +05:00
committed by GitHub
parent 0fdc759645
commit edbebf3256
24 changed files with 367 additions and 137 deletions
@@ -35,24 +35,23 @@
if(isnull(abilities))
abilities = list()
for(var/datum/action/changeling/ability_path as anything in changeling.all_powers)
var/dna_cost = initial(ability_path.dna_cost)
if(dna_cost < 0) // 0 = free, but negatives are invalid
continue
var/list/ability_data = list()
ability_data["name"] = initial(ability_path.name)
ability_data["desc"] = initial(ability_path.desc)
ability_data["path"] = ability_path
ability_data["helptext"] = initial(ability_path.helptext)
ability_data["genetic_point_required"] = dna_cost
ability_data["absorbs_required"] = initial(ability_path.req_absorbs) // compares against changeling true_absorbs
ability_data["dna_required"] = initial(ability_path.req_dna) // compares against changeling absorbed_count
var/list/ability_data = list(
"name" = initial(ability_path.name),
"desc" = initial(ability_path.desc),
"helptext" = initial(ability_path.helptext),
"path" = ability_path,
"genetic_point_required" = dna_cost,
"absorbs_required" = initial(ability_path.req_absorbs),
"dna_required" = initial(ability_path.req_dna),
"category" = initial(ability_path.category),
)
abilities += list(ability_data)
// Sorts abilities alphabetically by default
sortTim(abilities, /proc/cmp_assoc_list_name)
data["abilities"] = abilities
@@ -35,6 +35,8 @@
var/active = FALSE
/// Does this ability stop working if you are burning?
var/disabled_by_fire = TRUE
///Defines the ability category: 'stings', 'combat', 'stealth', 'utility'
var/category = "utility"
/*
changeling code now relies on on_purchase to grant powers.
@@ -3,6 +3,7 @@
desc = "We concentrate our chemicals into a potent stimulant, rendering our form stupendously robust against being incapacitated. Costs 25 chemicals."
helptext = "Disregard any condition that has stunned us and suffuse our form with FOUR units of Changeling Adrenaline; our form recovers massive stamina and simply disregards any pain or fatigue during its effects."
button_icon_state = "adrenaline"
category = "combat"
chemical_cost = 25 // similar cost to biodegrade, as they serve similar purposes
dna_cost = 2
req_human = FALSE
@@ -8,6 +8,7 @@
helptext = "Grants us x-ray vision or flash protection. \
We will become a lot more vulnerable to flash-based devices while x-ray vision is active."
button_icon_state = "augmented_eyesight"
category = "utility"
chemical_cost = 0
dna_cost = 2
// Active = Flash weakness and x-ray
@@ -3,6 +3,7 @@
desc = "Dissolves restraints or other objects preventing free movement. Costs 30 chemicals."
helptext = "This is obvious to nearby people, and can destroy standard restraints and closets. Works against grabs."
button_icon_state = "biodegrade"
category = "utility"
chemical_cost = 30
dna_cost = 2
req_human = TRUE
@@ -3,6 +3,7 @@
desc = "Our skin pigmentation rapidly changes to suit our current environment. Costs 10 chemicals."
helptext = "Allows us to become invisible after a few seconds of standing still. Can be toggled on and off."
button_icon_state = "chameleon_skin"
category = "stealth"
dna_cost = 1
chemical_cost = 10
req_human = TRUE
@@ -5,6 +5,7 @@
helptext = "Allows us to darken and change the translucency of our pigmentation, and adapt our eyes to see in dark conditions, \
The translucent effect works best in dark enviroments and garments. Can be toggled on and off."
button_icon_state = "darkness_adaptation"
category = "stealth"
dna_cost = 2
chemical_cost = 15
/// How much we slow chemical regeneration while active, in chems per second
@@ -4,6 +4,8 @@
we will snatch their arms off and instantly finalize our stasis."
helptext = "This ability is passive, and will trigger when a defibrillator paddle is applied to our chest \
while we are dead or in stasis. Will also stun cyborgs momentarily."
button_icon_state = "defibrillator_grasp"
category = "utility"
owner_has_control = FALSE
dna_cost = 0
disabled_by_fire = FALSE
@@ -2,7 +2,8 @@
name = "Digital Camouflage"
desc = "By evolving the ability to distort our form and proportions, we defeat common algorithms used to detect lifeforms on cameras."
helptext = "We cannot be tracked by camera or seen by AI units while using this skill. However, humans looking at us will find us... uncanny."
button_icon_state = "digital_camo"
button_icon_state = "digital_camouflage"
category = "stealth"
dna_cost = 1
active = FALSE
@@ -3,6 +3,7 @@
desc = "Our flesh rapidly regenerates, healing our burns, bruises, and shortness of breath, as well as hiding all of our scars. Costs 20 chemicals."
helptext = "If we are on fire, the healing effect will not function. Does not regrow limbs or restore lost blood. Functions while unconscious."
button_icon_state = "fleshmend"
category = "combat"
chemical_cost = 20
dna_cost = 2
req_stat = HARD_CRIT
@@ -6,6 +6,7 @@
desc = "We sacrifice our current body in a moment of need, violently expanding to break through obstacles and reforming as a headslug. Costs 20 chemicals."
helptext = "We will violently expand, destroying obstacles around us, then be placed in control of a small, fragile creature. We may attack a corpse like this to plant an egg which will slowly mature into a new form for us."
button_icon_state = "last_resort"
category = "utility"
chemical_cost = 20
dna_cost = 1
req_stat = DEAD
@@ -3,6 +3,7 @@
desc = "We debase ourselves and become lesser. We become a monkey. Costs 5 chemicals."
helptext = "The transformation greatly reduces our size, allowing us to slip out of cuffs and climb through vents."
button_icon_state = "lesser_form"
category = "stealth"
chemical_cost = 5
dna_cost = 1
/// Whether to allow the transformation animation to play
@@ -3,6 +3,7 @@
desc = "We shape our vocal glands to sound like a desired voice. Maintaining this power slows chemical production."
button_icon_state = "mimic_voice"
helptext = "Will turn our voice into the name that we enter. We must constantly expend chemicals to maintain our form like this."
category = "stealth"
chemical_cost = 0//constant chemical drain hardcoded
dna_cost = 1
req_human = TRUE
@@ -4,6 +4,7 @@
In order to maintain our secrecy, we can speak through the decoy as if a normal brain. \
The decoy brain will relay speech it hears to us in purple."
button_icon = 'icons/obj/devices/assemblies.dmi'
category = "stealth"
button_icon_state = "mmi_off"
dna_cost = CHANGELING_POWER_UNOBTAINABLE
ignores_fakedeath = TRUE // Can be used while fake dead
@@ -177,7 +177,8 @@
name = "Arm Blade"
desc = "We reform one of our arms into a deadly blade. Costs 20 chemicals."
helptext = "We may retract our armblade in the same manner as we form it. Cannot be used while in lesser form."
button_icon_state = "armblade"
button_icon_state = "arm_blade"
category = "combat"
chemical_cost = 20
dna_cost = 2
req_human = TRUE
@@ -273,6 +274,7 @@
In our combat stance, we will put the victim in our hold after catching them, and we will pull them in and impale them if we're also holding a sharp weapon, or have an armblade. This pierces armor. \
Cannot be used while in lesser form."
button_icon_state = "tentacle"
category = "combat"
chemical_cost = 10
dna_cost = 2
req_human = TRUE
@@ -489,6 +491,7 @@
desc = "We reform one of our arms into a hard shield. Costs 20 chemicals."
helptext = "Organic tissue cannot resist damage forever; the shield will break after it is hit too much. The more genomes we absorb, the stronger it is. Cannot be used while in lesser form."
button_icon_state = "organic_shield"
category = "combat"
chemical_cost = 20
dna_cost = 1
req_human = TRUE
@@ -546,6 +549,7 @@
desc = "We turn our skin into tough chitin to protect us from damage. Costs 20 chemicals."
helptext = "Upkeep of the armor requires a low expenditure of chemicals. The armor provides decent protection against brute force and energy weapons. Cannot be used in lesser form."
button_icon_state = "chitinous_armor"
category = "combat"
chemical_cost = 20
dna_cost = 1
req_human = TRUE
@@ -612,6 +616,7 @@
desc = "We coat our head in a waxy outing coating similar to a bee hive which can be used to manufacture bees to attack our enemies. Costs 15 chemicals."
helptext = "While the hive head does not provide much in the ways of armor, it does allow the user to send bees out to attack targets. Reagents can poured inside the hive to cause all bees released to inject said reagents."
button_icon_state = "hive_head"
category = "combat"
chemical_cost = 15
dna_cost = 2
req_human = FALSE
@@ -2,7 +2,8 @@
name = "Anatomic Panacea"
desc = "Expels impurities from our form: curing diseases, removing parasites, sobering us, purging toxins and radiation, curing traumas and brain damage, and resetting our genetic code completely. Costs 20 chemicals."
helptext = "Can be used while unconscious."
button_icon_state = "panacea"
button_icon_state = "anatomic_panacea"
category = "utility"
chemical_cost = 20
dna_cost = 1
req_stat = HARD_CRIT
@@ -7,8 +7,8 @@
name = "Pheromone Receptors"
desc = "We attune our senses to track other changelings by scent. The closer they are, the easier we can find them."
helptext = "We will know the general direction of nearby changelings, with closer scents being stronger. Our chemical generation is slowed while this is active."
button_icon = 'icons/mob/actions/actions_spells.dmi'
button_icon_state = "nose"
button_icon_state = "pheromone_receptors"
category = "utility"
chemical_cost = 0 //Reduces regain rate while active.
dna_cost = 2
var/receptors_active = FALSE
@@ -4,6 +4,7 @@
Best used to stop prey from escaping. Doesn't work well in a vacuum. Costs 20 chemicals."
helptext = "Emits a high-frequency sound that confuses and deafens humans to hamper their movement, blows out nearby lights and overloads cyborg sensors."
button_icon_state = "resonant_shriek"
category = "combat"
chemical_cost = 20
dna_cost = 1
req_human = TRUE
@@ -34,7 +35,8 @@
/datum/action/changeling/dissonant_shriek
name = "Technophagic Shriek"
desc = "We shift our vocal cords to release a high-frequency sound that overloads nearby electronics. Breaks headsets and cameras, and can sometimes break laser weaponry, doors, and modsuits. Costs 20 chemicals."
button_icon_state = "dissonant_shriek"
button_icon_state = "technophagic_shriek"
category = "combat"
chemical_cost = 20
dna_cost = 1
disabled_by_fire = FALSE
@@ -3,6 +3,7 @@
desc = "Our form divides, creating a cluster of eggs which will grow into a deadly arachnid. Costs 45 chemicals."
helptext = "The spiders are ruthless creatures, and may attack their creators when fully grown. Requires at least 3 DNA absorptions."
button_icon_state = "spread_infestation"
category = "utility"
chemical_cost = 45
dna_cost = 1
req_absorbs = 3
@@ -6,6 +6,7 @@
desc = "We evolve the ability to reduce the acid buildup in our muscles, allowing us to move much faster."
helptext = "The strain will make us tired, and we will rapidly become fatigued. Standard weight restrictions, like space suits, still apply. Cannot be used in lesser form."
button_icon_state = "strained_muscles"
category = "utility"
chemical_cost = 0
dna_cost = 1
req_human = TRUE
@@ -1,6 +1,7 @@
/datum/action/changeling/sting//parent path, not meant for users afaik
name = "Tiny Prick"
desc = "Stabby stabby"
category = "stings"
/datum/action/changeling/sting/Trigger(mob/clicker, trigger_flags)
var/mob/user = owner
@@ -143,7 +144,7 @@
name = "False Armblade Sting"
desc = "We silently sting a human, injecting a retrovirus that mutates their arm to temporarily appear as an armblade. Costs 20 chemicals."
helptext = "The victim will form an armblade much like a changeling would, except the armblade is dull and useless."
button_icon_state = "sting_armblade"
button_icon_state = "false_armblade_sting"
chemical_cost = 20
dna_cost = 1
@@ -194,7 +195,7 @@
name = "Extract DNA Sting"
desc = "We stealthily sting a target and extract their DNA. Costs 25 chemicals."
helptext = "Will give us the DNA of our target, allowing us to transform into them. This will render us unable to absorb their body fully later."
button_icon_state = "sting_extract"
button_icon_state = "extract_dna_sting"
chemical_cost = 25
dna_cost = 0
@@ -215,7 +216,7 @@
name = "Mute Sting"
desc = "We silently sting a human, completely silencing them for a short time. Costs 20 chemicals."
helptext = "Does not provide a warning to the victim that they have been stung, until they try to speak and cannot."
button_icon_state = "sting_mute"
button_icon_state = "mute_sting"
chemical_cost = 20
dna_cost = 2
@@ -229,7 +230,7 @@
name = "Blind Sting"
desc = "We temporarily blind our victim. Costs 25 chemicals."
helptext = "This sting completely blinds a target for a short time, and leaves them with blurred vision for a long time. Does not work if target has robotic or missing eyes."
button_icon_state = "sting_blind"
button_icon_state = "blind_sting"
chemical_cost = 25
dna_cost = 1
@@ -256,7 +257,7 @@
desc = "We cause mass terror to our victim. Costs 10 chemicals."
helptext = "We evolve the ability to sting a target with a powerful hallucinogenic chemical. \
The target does not notice they have been stung, and the effect occurs after 30 to 60 seconds."
button_icon_state = "sting_lsd"
button_icon_state = "hallucination_sting"
chemical_cost = 10
dna_cost = 1
@@ -275,7 +276,7 @@
name = "Cryogenic Sting"
desc = "We silently sting our victim with a cocktail of chemicals that freezes them from the inside. Costs 15 chemicals."
helptext = "Does not provide a warning to the victim, though they will likely realize they are suddenly freezing."
button_icon_state = "sting_cryo"
button_icon_state = "cryogenic_sting"
chemical_cost = 15
dna_cost = 2
@@ -4,6 +4,8 @@
helptext = "This ability is passive, and will automatically protect us in situations of extreme cold or vacuum, \
as well as removing our need to breathe oxygen, although we will still be affected by hazardous gases. \
While it is actively protecting us from temperature or pressure it reduces our chemical regeneration rate."
category = "utility"
button_icon_state = "void_adaption"
owner_has_control = FALSE
dna_cost = 2
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 10 KiB

+317 -114
View File
@@ -2,59 +2,116 @@ import { useState } from 'react';
import {
Box,
Button,
DmIcon,
Icon,
Input,
LabeledList,
NoticeBox,
Section,
Stack,
Tabs,
Tooltip,
} from 'tgui-core/components';
import type { BooleanLike } from 'tgui-core/react';
import { useBackend } from '../backend';
import { Window } from '../layouts';
type typePath = string;
type CellularEmporiumContext = {
abilities: Ability[];
can_readapt: number;
genetic_points_count: number;
owned_abilities: typePath[];
absorb_count: number;
dna_count: number;
};
// ==========
// Types
// ==========
type TypePath = string;
type Ability = {
name: string;
desc: string;
helptext: string;
path: typePath;
genetic_point_required: number; // Checks against genetic_points_count
absorbs_required: number; // Checks against absorb_count
dna_required: number; // Checks against dna_count
path: TypePath;
genetic_point_required: number;
absorbs_required: number;
dna_required: number;
category: string;
};
type CellularEmporiumContext = {
abilities: Ability[];
can_readapt: number;
genetic_points_count: number;
owned_abilities: TypePath[];
absorb_count: number;
dna_count: number;
};
// ==========
// Helper: convert ability name to icon_state
// Example: "Augmented Eyesight" → "augmented_eyesight"
// ==========
const nameToIconState = (name: string): string => {
return name.toLowerCase().replace(/\s+/g, '_');
};
// ==========
// Main Component
// ==========
export const CellularEmporium = (props) => {
const { act, data } = useBackend<CellularEmporiumContext>();
const [searchAbilities, setSearchAbilities] = useState('');
const {
abilities,
can_readapt,
genetic_points_count,
owned_abilities,
absorb_count,
dna_count,
} = data;
const { can_readapt, genetic_points_count } = data;
const readaptTracker = (can_readapt: number): string => {
const firstPart = 'Readapt(';
return firstPart.concat(can_readapt.toString(), ')');
const [searchText, setSearchText] = useState('');
const [compactMode, setCompactMode] = useState(false);
const CATEGORY_ORDER = ['combat', 'stealth', 'utility', 'stings'];
const allCategories = Array.from(new Set(abilities.map((a) => a.category)));
const sortedCategories = [
...CATEGORY_ORDER.filter((cat) => allCategories.includes(cat)),
...allCategories.filter((cat) => !CATEGORY_ORDER.includes(cat)),
];
const [selectedCategory, setSelectedCategory] = useState(
sortedCategories[0] || 'utility',
);
const filteredItems = (
searchText
? abilities.filter((item) =>
[item.name, item.desc, item.helptext]
.join(' ')
.toLowerCase()
.includes(searchText.toLowerCase()),
)
: abilities.filter((item) => item.category === selectedCategory)
).sort((a, b) => a.name.localeCompare(b.name));
const handleBuy = (ability: Ability) => {
act('evolve', { path: ability.path });
};
return (
<Window width={900} height={480}>
<Window.Content>
<Window width={900} height={520}>
<Window.Content
scrollable={false}
style={{
backgroundImage: "url('tgui-core/assets/bg-nanotrasen.svg')",
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundColor: '#1A1A1A',
overflowY: 'auto',
}}
>
<Section
fill
scrollable
title={'Genetic Points'}
buttons={
<Stack>
<Stack.Item fontSize="16px">
{genetic_points_count} <Icon name="dna" color="#DD66DD" />
scrollable={false}
title={
<Stack fill>
<Stack.Item fontSize="16px" color="#DD66DD" ml={1}>
<Icon name="dna" /> {genetic_points_count} DNA
</Stack.Item>
<Stack.Item grow />
<Stack.Item>
<Button
icon="undo"
@@ -62,113 +119,259 @@ export const CellularEmporium = (props) => {
disabled={!can_readapt}
tooltip={
can_readapt
? 'We readapt, un-evolving all evolved abilities \
and refunding our genetic points.'
: 'We cannot readapt until we absorb more DNA.'
? 'Un-evolve all abilities and refund genetic points.'
: 'Need more DNA to readapt.'
}
onClick={() => act('readapt')}
>
{readaptTracker(can_readapt)}
Readapt ({can_readapt})
</Button>
</Stack.Item>
<Stack.Item>
<Input
width="200px"
onChange={setSearchAbilities}
placeholder="Search Abilities..."
value={searchAbilities}
/>
</Stack.Item>
</Stack>
}
>
<AbilityList searchAbilities={searchAbilities} />
<Stack fill>
<Stack.Item width="180px">
<Stack vertical fill>
<Stack.Item>
<Input
autoFocus
value={searchText}
placeholder="Search..."
onChange={setSearchText}
fluid
/>
</Stack.Item>
<Stack.Item>
<Button
fluid
lineHeight={2}
textAlign="center"
icon={compactMode ? 'maximize' : 'minimize'}
tooltip={compactMode ? 'Detailed view' : 'Compact view'}
onClick={() => setCompactMode(!compactMode)}
/>
</Stack.Item>
<Stack.Item grow>
<Tabs vertical fill>
{sortedCategories.map((category) => (
<Tabs.Tab
key={category}
selected={category === selectedCategory}
onClick={() => {
setSelectedCategory(category);
if (searchText) setSearchText('');
}}
mt={1}
>
{category.charAt(0).toUpperCase() + category.slice(1)}
</Tabs.Tab>
))}
</Tabs>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item grow>
<Box height="100%" pr={1} mr={-1}>
{filteredItems.length === 0 ? (
<NoticeBox>
{searchText
? 'No abilities found.'
: 'No abilities in this category.'}
</NoticeBox>
) : (
<ItemList
compactMode={searchText.length > 0 || compactMode}
items={filteredItems}
owned_abilities={owned_abilities}
genetic_points_count={genetic_points_count}
absorb_count={absorb_count}
dna_count={dna_count}
handleBuy={handleBuy}
/>
)}
</Box>
</Stack.Item>
</Stack>
</Section>
</Window.Content>
</Window>
);
};
const AbilityList = (props: { searchAbilities: string }) => {
const { act, data } = useBackend<CellularEmporiumContext>();
const { searchAbilities } = props;
// ==========
// ItemList Component
// ==========
type ItemListProps = {
compactMode: BooleanLike;
items: Ability[];
owned_abilities: TypePath[];
genetic_points_count: number;
absorb_count: number;
dna_count: number;
handleBuy: (item: Ability) => void;
};
const ItemList = (props: ItemListProps) => {
const {
abilities,
compactMode,
items,
owned_abilities,
genetic_points_count,
absorb_count,
dna_count,
} = data;
handleBuy,
} = props;
const filteredAbilities =
searchAbilities.length <= 1
? abilities
: abilities.filter((ability) => {
return (
ability.name
.toLowerCase()
.includes(searchAbilities.toLowerCase()) ||
ability.desc
.toLowerCase()
.includes(searchAbilities.toLowerCase()) ||
ability.helptext
.toLowerCase()
.includes(searchAbilities.toLowerCase())
);
});
if (filteredAbilities.length === 0) {
return (
<NoticeBox>
{abilities.length === 0
? 'No abilities available to purchase. \
This is in error, contact your local hivemind today.'
: 'No abilities found.'}
</NoticeBox>
);
}
const iconSize = compactMode ? '32px' : '64px';
return (
<LabeledList>
{filteredAbilities.map((ability) => (
<LabeledList.Item
key={ability.name}
className="candystripe"
label={ability.name}
buttons={
<Stack>
<Stack.Item>{ability.genetic_point_required}</Stack.Item>
<Stack.Item>
<Icon
name="dna"
color={
owned_abilities.includes(ability.path) ? '#DD66DD' : 'gray'
}
/>
</Stack.Item>
<Stack.Item>
<Button
content={'Evolve'}
disabled={
owned_abilities.includes(ability.path) ||
ability.genetic_point_required > genetic_points_count ||
ability.absorbs_required > absorb_count ||
ability.dna_required > dna_count
}
onClick={() =>
act('evolve', {
path: ability.path,
})
}
/>
</Stack.Item>
</Stack>
}
>
{ability.desc}
<Box color="good">{ability.helptext}</Box>
</LabeledList.Item>
))}
</LabeledList>
<Section fill scrollable>
<Stack vertical mt={compactMode ? 0.5 : 0}>
{items.map((ability) => {
const owned = owned_abilities.includes(ability.path);
const canAfford =
!owned &&
ability.genetic_point_required <= genetic_points_count &&
ability.absorbs_required <= absorb_count &&
ability.dna_required <= dna_count;
const requirementTooltip = [
`${ability.genetic_point_required} DNA`,
...(ability.absorbs_required > 0
? [`${ability.absorbs_required} absorptions`]
: []),
...(ability.dna_required > 0
? [`${ability.dna_required} DNA`]
: []),
].join(', ');
const costDisplay =
ability.dna_required > 0
? `Cost: ${ability.dna_required} DNA`
: `Cost: ${ability.genetic_point_required} DNA`;
const iconState = nameToIconState(ability.name);
return (
<Stack.Item key={ability.path} mt={compactMode ? 0.5 : 1}>
<Section fitted={!!compactMode}>
<Stack>
<Stack.Item>
<Box ml={2}>
<Box
width={iconSize}
height={iconSize}
position="relative"
m={compactMode ? '2px' : 0}
mr={1}
>
<DmIcon
position="absolute"
top="0"
left="0"
icon="icons/mob/actions/actions_changeling.dmi"
icon_state="bg_changeling"
width={iconSize}
fallback={null}
/>
<DmIcon
position="absolute"
top="0"
left="0"
icon="icons/mob/actions/actions_changeling.dmi"
icon_state={iconState}
width={iconSize}
fallback={<Icon name="question-circle" size={3} />}
/>
</Box>
</Box>
</Stack.Item>
<Stack.Item grow>
{compactMode ? (
<Stack>
<Stack.Item
bold
grow
lineHeight="36px"
style={{
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
opacity: owned ? '0.5' : '1',
}}
>
{owned ? (
<Box color="#44bd46">
<Icon mr="8px" name="check" />
{ability.name}
</Box>
) : (
ability.name
)}
</Stack.Item>
<Stack.Item>
<Tooltip content={ability.helptext || ability.desc}>
<Icon name="info-circle" lineHeight="36px" />
</Tooltip>
</Stack.Item>
<Stack.Item>
<Tooltip content={requirementTooltip}>
<Button
m="8px"
color={canAfford ? 'average' : 'bad'}
disabled={owned || !canAfford}
onClick={() => handleBuy(ability)}
>
{costDisplay}
</Button>
</Tooltip>
</Stack.Item>
</Stack>
) : (
<Section
title={
<Box>
{ability.name}
{owned && (
<Box color="#44bd46" inline ml={1}>
(Owned)
</Box>
)}
</Box>
}
buttons={
<Tooltip content={requirementTooltip}>
<Box mt={-3}>
<Button
disabled={owned || !canAfford}
color={
owned ? 'good' : canAfford ? 'average' : 'bad'
}
onClick={() => handleBuy(ability)}
>
{costDisplay}
</Button>
</Box>
</Tooltip>
}
>
<Box opacity={0.8}>{ability.desc}</Box>
{ability.helptext && (
<Box color="#44bd46" mt={0.5}>
{ability.helptext}
</Box>
)}
</Section>
)}
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
);
})}
</Stack>
</Section>
);
};