Okay, the title of this article is completely hyperbolic and sensationalist. OF COURSE I didn't write an actual TUI. Well... not a complete one at least. But there's a germ of one here. To be fair, there almost always has been one since Eldoria was conceived, but it wasn't ever intended to be used in any context outside itself. Frankly it still isn't now, even with the most recent developments that I'm going to spout about here, and it may likely never be.
So now that I've negated any reasons you may have had for continuing to read, let me now elaborate on what the hell is going on here.
User Interfaces Are Hard
The header has bifurcated the audience, and that's completely fine. Some of it boils down to one's tolerance for certain kinds of programming and how their brain works. Others it lands somewhere else and I don't care to go there. However, consider the following fact: most UI libraries, in an apparent progression towards maturation, will inexorably seek to simplify the expression patterns developers are required to use to create interfaces with them. Instances of this are replete in the problem space: XAML for .NET, XML for Android, JavaFX for Java, QML for QT, hell even the RAD tooling in Visual Studio for creating forms and so-called "code-behind" (this also applies to web sites created with dynamic server-side languages). This is also why people make incredibly poor decisions like building complex user interfaces using frameworks like Electron (you know, because JavaScript is such a great language). While both the argument and practical proof can be made that you don't need to use these simplified dialects/practices to create user interfaces when you could just write them directly in the language of the project's choice, I ask have you done just that? Over the past twenty years, I've written my fair share of in-language UI code and while it's simple, it's the kind of boilerplate that makes you want to stab yourself in the eye. It's repetitive, cumbersome (especially given the nature of the language you may be using), and just not fun to do after the first pass.
To provide some examples of what I mean, let's examine some classic examples. The first is code that would create a simple window with a label, button, and a typical application menu in Java using Swing:
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.Random;
public class SwingWindowWithMenuExample extends JFrame {
private JLabel myLabel;
private String[] randomTexts = {"Hello, World!", "Java Swing is fun!", "Click me again!", "Another random message.", "Programming is awesome!"};
public SwingWindowWithMenuExample() {
super("Swing Window with Menu Example");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Set up the menu bar
this.setJMenuBar(createMenuBar());
// Set up the main content panel
JPanel contentPanel = new JPanel(new FlowLayout());
myLabel = new JLabel("Click the button to change this text.");
JButton myButton = new JButton("Change Text");
myButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Random random = new Random();
int randomIndex = random.nextInt(randomTexts.length);
myLabel.setText(randomTexts[randomIndex]);
}
});
contentPanel.add(myLabel);
contentPanel.add(myButton);
// Add the panel to the frame
this.add(contentPanel);
this.pack();
this.setLocationRelativeTo(null);
this.setVisible(true);
}
private JMenuBar createMenuBar() {
JMenuBar menuBar = new JMenuBar();
// Create the menus
JMenu fileMenu = new JMenu("File");
JMenu editMenu = new JMenu("Edit");
JMenu viewMenu = new JMenu("View");
JMenu helpMenu = new JMenu("Help");
// Set keyboard shortcuts for the main menus
fileMenu.setMnemonic(KeyEvent.VK_F);
editMenu.setMnemonic(KeyEvent.VK_E);
viewMenu.setMnemonic(KeyEvent.VK_V);
helpMenu.setMnemonic(KeyEvent.VK_H);
// Create and add menu items to the File menu
JMenuItem newItem = new JMenuItem("New");
JMenuItem openItem = new JMenuItem("Open...");
JMenuItem saveItem = new JMenuItem("Save");
JMenuItem exitItem = new JMenuItem("Exit");
fileMenu.add(newItem);
fileMenu.add(openItem);
fileMenu.add(saveItem);
fileMenu.addSeparator(); // Adds a horizontal line separator
fileMenu.add(exitItem);
// Add an action listener for the "Exit" menu item
exitItem.addActionListener(e -> System.exit(0));
// Create and add menu items to the Edit menu
JMenuItem cutItem = new JMenuItem("Cut");
JMenuItem copyItem = new JMenuItem("Copy");
JMenuItem pasteItem = new JMenuItem("Paste");
editMenu.add(cutItem);
editMenu.add(copyItem);
editMenu.add(pasteItem);
// Create and add menu items to the View menu
JCheckBoxMenuItem statusBarItem = new JCheckBoxMenuItem("Status Bar");
viewMenu.add(statusBarItem);
// Create and add menu items to the Help menu
JMenuItem helpContentsItem = new JMenuItem("Help Contents");
JMenuItem aboutItem = new JMenuItem("About");
helpMenu.add(helpContentsItem);
helpMenu.add(aboutItem);
// Add all menus to the menu bar
menuBar.add(fileMenu);
menuBar.add(editMenu);
menuBar.add(viewMenu);
menuBar.add(helpMenu);
return menuBar;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new SwingWindowWithMenuExample());
}
}
Here's another example of this same concept expressed in Qt:
#include <QApplication>
#include <QMainWindow>
#include <QLabel>
#include <QPushButton>
#include <QMenuBar>
#include <QMenu>
#include <QAction>
#include <QVBoxLayout>
#include <QWidget>
#include <QRandomGenerator>
class SwingWindowWithMenuExample : public QMainWindow {
Q_OBJECT
public:
SwingWindowWithMenuExample(QWidget *parent = nullptr)
: QMainWindow(parent) {
setWindowTitle("Qt Window with Menu Example");
// Central widget and layout
QWidget *centralWidget = new QWidget(this);
setCentralWidget(centralWidget);
QVBoxLayout *layout = new QVBoxLayout(centralWidget);
// Initialize components
myLabel = new QLabel("Click the button to change this text.", this);
myLabel->setAlignment(Qt::AlignCenter);
QPushButton *myButton = new QPushButton("Change Text", this);
// Add components to the layout
layout->addWidget(myLabel);
layout->addWidget(myButton);
// Connect the button's clicked signal to a lambda function
connect(myButton, &QPushButton::clicked, this, [this]() {
int randomIndex = QRandomGenerator::global()->bounded(randomTexts.size());
myLabel->setText(randomTexts[randomIndex]);
});
// Set up the menu bar
createMenuBar();
// Set a reasonable size and show the window
resize(400, 200);
}
private:
QLabel *myLabel;
QStringList randomTexts = {"Hello, World!", "Qt is awesome!", "Click me again!", "Another random message.", "Programming is awesome!"};
void createMenuBar() {
QMenuBar *menuBar = new QMenuBar(this);
setMenuBar(menuBar);
// Create the menus
QMenu *fileMenu = menuBar->addMenu("&File");
QMenu *editMenu = menuBar->addMenu("&Edit");
QMenu *viewMenu = menuBar->addMenu("&View");
QMenu *helpMenu = menuBar->addMenu("&Help");
// Create and add actions to the File menu
QAction *newAction = fileMenu->addAction("New");
QAction *openAction = fileMenu->addAction("Open...");
QAction *saveAction = fileMenu->addAction("Save");
fileMenu->addSeparator();
QAction *exitAction = fileMenu->addAction("Exit");
// Connect the exit action to the application's quit slot
connect(exitAction, &QAction::triggered, qApp, &QApplication::quit);
// Create and add actions to the Edit menu
editMenu->addAction("Cut");
editMenu->addAction("Copy");
editMenu->addAction("Paste");
// Create and add actions to the View menu
QAction *statusBarAction = viewMenu->addAction("Status Bar");
statusBarAction->setCheckable(true);
// Create and add actions to the Help menu
helpMenu->addAction("Help Contents");
helpMenu->addAction("About");
}
};
#include "main.moc"
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
SwingWindowWithMenuExample window;
window.show();
return app.exec();
}
Although these are inarguably modest examples, (A) they're not pretty even in the simplest of expressions and (B) it betrays a severe lack of simplicity if you dare make a slight change/improvement/feature add to the user interface with this approach. Yet this is how interfaces were wrangled in the past and can still be done to this day if you really don't like yourself.
Let's examine what these examples could look like when written in their simplified analogs: JavaFX and QML respectively:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Pos?>
<BorderPane xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="JavaFXController">
<top>
<MenuBar>
<menus>
<Menu text="File">
<items>
<MenuItem text="New" />
<MenuItem text="Open..." />
<MenuItem text="Save" />
<SeparatorMenuItem />
<MenuItem text="Exit" onAction="#handleExitAction" />
</items>
</Menu>
<Menu text="Edit">
<items>
<MenuItem text="Cut" />
<MenuItem text="Copy" />
<MenuItem text="Paste" />
</items>
</Menu>
<Menu text="View">
<items>
<CheckMenuItem text="Status Bar" />
</items>
</Menu>
<Menu text="Help">
<items>
<MenuItem text="Help Contents" />
<MenuItem text="About" />
</items>
</Menu>
</menus>
</MenuBar>
</top>
<center>
<VBox alignment="CENTER" spacing="10.0">
<Label fx:id="myLabel" text="Click the button to change this text." />
<Button text="Change Text" onAction="#handleButtonAction" />
</VBox>
</center>
</BorderPane>
And the Qt version translated to QML:
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15
Window {
id: window
width: 400
height: 200
visible: true
title: "QML Window Example"
// Define the application's logic in a separate C++ object
// This assumes a C++ class named 'AppLogic' is registered
// and exposed to QML.
// The following code is illustrative; an actual C++ object needs to be defined and registered
// For this simple example, we can keep all logic in QML.
// The following properties and functions are part of the QML logic.
property stringList randomTexts: ["Hello, World!", "QML is awesome!", "Click me again!", "Another random message.", "Programming is awesome!"]
property int randomIndex: 0
// This is the root layout
ColumnLayout {
anchors.fill: parent
// Create the menu bar
MenuBar {
id: menuBar
Layout.fillWidth: true
Menu {
title: "File"
MenuItem { text: "New" }
MenuItem { text: "Open..." }
MenuItem { text: "Save" }
MenuSeparator {}
MenuItem { text: "Exit"; onTriggered: Qt.quit() }
}
Menu {
title: "Edit"
MenuItem { text: "Cut" }
MenuItem { text: "Copy" }
MenuItem { text: "Paste" }
}
Menu {
title: "View"
MenuCheckableItem { text: "Status Bar"; checked: true }
}
Menu {
title: "Help"
MenuItem { text: "Help Contents" }
MenuItem { text: "About" }
}
}
// The main content area
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
spacing: 10
Label {
id: myLabel
text: "Click the button to change this text."
font.pointSize: 12
}
Button {
text: "Change Text"
onClicked: {
var randomNumber = Math.floor(Math.random() * randomTexts.length);
myLabel.text = randomTexts[randomNumber];
}
}
}
}
}
Each of these examples requires some so-called "code-behind" to parse and render the quasi-markup into the formal constructs that the respective UI frameworks are requiring, but even with this kind of boilerplate (mostly), the developer experience is dramatically improved. Frankly, it doesn't take more than a cursory inspection of the code examples to identify the major differences and how the latter blocks permit a more expressive style of programming. We can have debates over whether this style is appropriate in other contexts, especially with the emergence of "vibe coding", but here it inarguably makes a world of difference. The very slow evolution of Eldoria's TUI-like code has seen a similar genesis, going from being on a symbolic expression of some text approximating a "window" to a block using relative measurements to now having things like user interface controls sans a layout engine. This latter development has been the most crucial one since it's simplified the code base for windows. It also allows for a kind of development that was originally avoided as if it were the plague.
TUIs
Text-based User Interfaces (TUI) aren't too dissimilar from their Graphical User Interface (GUI) counterparts in terms of their design requirements. Many of the obvious differences emerge when you need to render the interface code inside the terminal as well as some interface expressions, hence a lot of the problem space remains the same. TUIs have largely been supplanted in recent decades by GUIs and web pages. My feel on the pulse of the community is that there's always been a niche interest in them, and I dare say that we're now seeing a Reniassance of sorts of the technology. What was for the longest time a domain owned by ncurses - for better or worse - has now seen a plethora of libraries taking different approaches and implementing awesome features to make TUIs a truly viable experience for mature and sophisticaed applications. There are of course wrappers for ncurses in different languages, but there are entirely fresh approaches to the subject in the form of FTXUI, imtui, PyTermTK, Rich, and Textual to name a few. In the PowerShell space specifically, we have the awesome Spectre.Console library that has been ported to PowerShell native by way of PwshSpectreConsole.
TUIs are special in that everything is quite literally a string. While we do have really awesome image-in-terminal specifications like ReGIS and Sixel, you often don't need to use them to create shockingly dynamic and rich interfaces becuase you could do pretty much anything with carefully crafted strings and some elbow grease. That said, there's still plenty of room for things to be different amongst the lot, so not every TUI is created equal. Hell, even mine isn't much the same as any other. We're all chefs, and we all make basically the same thing, but the ingredients, portions, ratios, etc. are all a little different. So it may be worthwhile to perform an anthropological analysis on my so-called user interface code.
On The Seventh Day
The very earliest hints of anything that smelled like user interface code came about (at the time of writing) three years ago. I wouldn't even remotely call this good code per-se, but it was a thing that worked and did something approximating a thing:
Function Write-GfmStatusWindow {
[CmdletBinding()]
Param ()
Process {
Switch ($(Test-GfmOs)) {
{ ($_ -EQ $Script:OsCheckLinux) -OR ($_ -EQ $Script:OsCheckMac) } {
$Script:Rui.CursorPosition = [Coordinates]::new($Script:UiStatusWindowDrawX, $Script:UiStatusWindowDrawY)
Write-GfmHostNnl -Message $Script:UiStatusWindowBorderHoirzontal `
-ForegroundColor $Script:UiStatusWindowBorderColor
$Script:Rui.CursorPosition = [Coordinates]::new($Script:UiStatusWindowDrawX, $Script:UiStatusWindowDrawY + $Script:UiStatusWindowHeight)
Write-GfmHostNnl -Message $Script:UiStatusWindowBorderHoirzontal `
-ForegroundColor $Script:UiStatusWindowBorderColor
For ($i = 1; $i -LT $Script:UiStatusWindowHeight; $i++) {
$Script:Rui.CursorPosition = [Coordinates]::new($Script:UiStatusWindowDrawX, $i)
Write-GfmHostNnl -Message $Script:UiStatusWindowBorderVertical `
-ForegroundColor $Script:UiStatusWindowBorderColor
$Script:Rui.CursorPosition = [Coordinates]::new(($Script:UiStatusWindowDrawX + $Script:UiStatusWindowWidth), $i)
Write-GfmHostNnl -Message $Script:UiStatusWindowBorderVertical `
-ForegroundColor $Script:UiStatusWindowBorderColor
}
}
$Script:OsCheckWindows {
# For the time being, I'm simply going to copypaste the code from the previous case
$Script:Rui.CursorPosition = [Coordinates]::new($Script:UiStatusWindowDrawX, $Script:UiStatusWindowDrawY)
Write-GfmHostNnl -Message $Script:UiStatusWindowBorderHoirzontal `
-ForegroundColor $Script:UiStatusWindowBorderColor
$Script:Rui.CursorPosition = [Coordinates]::new($Script:UiStatusWindowDrawX, $Script:UiStatusWindowDrawY + $Script:UiStatusWindowHeight)
Write-GfmHostNnl -Message $Script:UiStatusWindowBorderHoirzontal `
-ForegroundColor $Script:UiStatusWindowBorderColor
For ($i = 1; $i -LT $Script:UiStatusWindowHeight; $i++) {
$Script:Rui.CursorPosition = [Coordinates]::new($Script:UiStatusWindowDrawX, $i)
Write-GfmHostNnl -Message $Script:UiStatusWindowBorderVertical `
-ForegroundColor $Script:UiStatusWindowBorderColor
$Script:Rui.CursorPosition = [Coordinates]::new(($Script:UiStatusWindowDrawX + $Script:UiStatusWindowWidth), $i)
Write-GfmHostNnl -Message $Script:UiStatusWindowBorderVertical `
-ForegroundColor $Script:UiStatusWindowBorderColor
}
}
Default {
}
}
}
End {
Set-GfmDefaultCursorPosition
}
}
I'm omitting a lot of code for brevity, but some explanation of the context is warranted.
At this point, classes were largely not used. There were maybe six or seven of them, and the only thing they did was wed a string to a ConsoleColor
instance. Otherwise, every piece of data, be it player information, window borders, etc., were all contained in script-scoped variables. All actions were performed via functions with the Gfm*
namespace. You can see this with various calls to *-Gfm*
functions in this block. Aside from the fact this code is very sus, it illustrates the functionality at the time. So the obvious question here is where's the window data? That get's handled by yet other functions:
Function Invoke-GfmGamePlayScreenStarting {
[CmdletBinding()]
Param ()
Process {
Clear-Host
Write-GfmStatusWindow
Write-GfmSceneWindow
Write-GfmMessageWindow
Write-GfmCommandWindow
Write-GfmSceneImage -CellArray $Script:CurrentMap.GetTileAtPlayerCoordinates().BackgroundImage
Write-GfmPlayerName
Write-GfmPlayerHp
Write-GfmPlayerMp
Write-GfmPlayerGold
}
End {
Switch-GfmGameStateFlags -DesiredState [GameStateFlags]::Running
}
}
...
Function Write-GfmPlayerHp {
[CmdletBinding()]
Param ()
Process {
Switch ($Script:PlayerHitPointsState) {
# The fix for properly strong-typing the scoping is found here: https: //learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-switch?view=powershell-7.2#enum
([PlayerHpState]::Normal) {
Write-GfmPositionalString `
-Coordinates $([Coordinates]::new($Script:UiStatusWindowPlayerHpDrawX, $Script:UiStatusWindowPlayerHpDrawY)) `
-Message $(Format-GfmPlayerHitPoints) `
-ForegroundColor $Script:PlayerStatNumberDrawColorSafe
}
([PlayerHpState]::Caution) {
Write-GfmPositionalString `
-Coordinates $([Coordinates]::new($Script:UiStatusWindowPlayerHpDrawX, $Script:UiStatusWindowPlayerHpDrawY)) `
-Message $(Format-GfmPlayerHitPoints) `
-ForegroundColor $Script:PlayerStatNumberDrawColorCaution
}
([PlayerHpState]::Danger) {
Write-GfmPositionalString `
-Coordinates $([Coordinates]::new($Script:UiStatusWindowPlayerHpDrawX, $Script:UiStatusWindowPlayerHpDrawY)) `
-Message $(Format-GfmPlayerHitPoints) `
-ForegroundColor $Script:PlayerStatNumberDrawColorDanger
}
Default {
Write-GfmPositionalString `
-Coordinates $([Coordinates]::new($Script:UiStatusWindowPlayerHpDrawX, $Script:UiStatusWindowPlayerHpDrawY)) `
-Message $(Format-GfmPlayerHitPoints) `
-ForegroundColor $Script:PlayerStatNumberDrawColorDanger
}
}
}
End {
Set-GfmDefaultCursorPosition
}
}
I can already hear both Silbert Ganchez and PLAkira screaming at their monitors at the backticks. Don't worry friend-o's! They're not used any longer!
There were two primary motivators for this slop:
- This was truly disposable prototype code and I didn't have a freaking clue how any of my ideas were going to get into this language. This was still back when I wasn't even sure if PowerShell could be used to accomplish this.
- Aside from that, I REALLY SERIOUSLY didn't want to write out a full user interface spec and codify it. The KISS mantra was in full effect here, perhaps too much.
Despite this horrific scripting, I was able to produce then what's shown below:

The thing to keep in mind when looking at this is that all data was positioned absolutely in the terminal buffer. The "borders" around the "windows" were really nothing other than buffer cell noise; Rube Goldberg Machines in text form. Also, as I've said before, everything you're seeing here is literally all strings. Even the "image" that's shown in the window on the right side (it's supposed to be a road going north and east but looks like a potato L).
Necessity Is The Mother of... Something
What really sank the boat here is the need for inventory management. Originally, I'd created provisions for some kind of IM through the implement of the command parser dialect. While that worked, it was juvenile and shortsighted in a variety of ways that aren't worth getting into here. The point being is that I wanted a better way to perform IM and I wasn't convinced that trying to wrangle it into the rigid structure of the command parser dialect was going to work. I managed to build some confidence in PowerShell and myself at this point given what I'd done thus far, so I sat down to see if I could somehow build off what I had and provide some way, any way, to perform IM in the terminal that didn't require using the command parser dialect. I don't seem to have an older image from back when this was first running, but this window (this one specifically; it's no longer used) hasn't changed since its completion, so it looks like this:

This screenshot betrays quite a bit of progress from the earliest bits of code, even for itself, but there's a lot of history here with just this window.
Although at this point "windows" hadn't yet been formalized, I'd been getting away with the not-windows for what was put together on the main game screen, thus the not-UI code was doing just fine at the expense of looking ridiculous. But loftier ideas had crept in and taken root. What if I could actually do more than just display text or accept character-limited input from the player? (the latter part of that is more complicated than one would think). Inventory Management begged to have a bit more of an almost adult control scheme to it, and so that's what I tried to do.
My earliest sketches of this window wanted to show the player's inventory as a list in which they could use a chevron character to cycle through. Using a list implies a kind of pagination, which I can do but I'm not super fond of. In this case, it's much easier given that all we care about are item names (at the time). Concurrently, there's the matter of what axis the list should be displayed on. Virtually every list known to man renders vertically meaning that cycling the pagination scope is done by scrolling either up or down. Because I'm an asshole, I said no to that and wanted to setup horizontal scrolling. There was also the teeny little matter of showing and updating item metadata as the player navigated the chevron character through the list. In the earliest renditions, the inventory window provided little to no interactive ability for the player. The intent was only to provide a view into the inventory that previously wasn't possible because of the limited interactive capability of the command parser dialect.
As borderline useless as this sounds, just this spec alone set a decent challenge. There's a lot of drawing activity in this window, a lot of logic, and it setup yet another litmus test of sorts.
In the simplest design, the window was conceptualized as a book that showed all the player's inventory. Each page in this book is a view, a slice of the inventory. Slices can be no larger than ten items in frame. Pages are displayed split in half where the first five items would be in a left column while the last five are in a right column. The total number of pages in the book are determined by the number of items the player has in their inventory. The correlation of which plays out as follows:
- No items in the inventory - no pages populated in the book (called the "Zero Page")
- Less than ten items in the inventory - one page populated in the book
- More than ten items in the inventory - as many pages as can be evenly divisible by ten (the size of the slice frame) are populated in the book
A divider line is drawn beneath the five item column limit to deleniate where the items are displayed and where the item's metadata is displayed. The metadata are at least the item's description but also can include aspects like a player effect string and key item status indicator (key items being defined as those crucial to the game and can't be removed from the player's inventory).
As if that weren't enough, none of this covers the interaction model for the chevron and the implications that it has for the drawing of elements in the window. The player is able to obviously move the chevron up and down in a given column using the up and down arrow keys respectively. They're also able to move the chevron left or right between columns in the same row using the left and right arrow keys. If a book has more than one page, special paging chevrons are displayed in either the top-left or top-right corners of the window to indicate that you can cycle pages in a specific direction. The visual presentation of these paging chevrons is also governed by the current page of the book and the number of pages surrounding it. For example, if there are at least two pages in a book and the player is on the first page, the right-paging chevron is shown. If the player cycles pages to the next page on the right, there are no further pages to move to on the right so the player can only page left. So when they move to the last page in the book, only the left-paging chevron is visible. In other cases, when the player can page either left or right, both of the paging chevrons are visible. The player can press either the A or S keys to page left or right. Cycling pages has implications for both where the slice frame is viewing the player's inventory as well as the current position of the player's chevron. In other words, consider what would happen if on the current page the player's chevron was located in the right column on a specific item. If the player cycles the page, consequently changing the slice, what happens if the chevron is now located in column that doesn't exist because on the new page there are five or less items in the slice? Finally, because of how items are used in the command parser dialect (first available instance), the player would need some method to remove a specific instance of an item from their inventory lest they unintentionally use an item that has a deleterious effect on the player; there's no way to specify which of an item to use if more than one is owned.