Hello! Welcome back to our JRPG development journey.
In our last lesson, we built a complete, data-driven shop system. We mastered the art of creating a stateful UI scene that manages multiple windows, lists, and interaction modes, all while cleanly interacting with our inventory and currency APIs.
Today, we will apply and extend these UI construction skills to create the single most important interface in any JRPG: the battle screen. Our learning outcome is to implement the battle UI, including party/enemy status displays, command menus, and a target selection cursor. This is a substantial task, but it primarily involves composing the UI components and patterns we've already developed into a new, more dynamic context. We'll be bringing the combat system we architected in Module 5 to life visually.
1. The Architecture of a Battle UI
A battle UI for a turn-based JRPG needs to present a lot of information without feeling cluttered. Players need to see party health, enemy names, and their available actions at a glance. Your experience as a front-end lead has shown you the value of breaking down complex UIs into smaller, manageable components. We will do exactly that.
We will create a main BattleHUD component that acts as a container for several specialized sub-components:
- A window to display the party's status.
- Windows for the command, skill, and item menus.
- A component for the enemy list.
- A dynamic cursor for target selection.
The idea of splitting a complex UI into logical groups is a universal principle for organization and performance. While we aren't using a complex engine like Unity, the reasoning behind this approach is identical.
Unity UI canvas modes and canvas scaler explained
This video from Christina Creates Games, although for Unity, explains the benefits of dividing a complex UI into multiple logical pieces. This is the core architectural principle we will follow.
Please watch the short section 'Working with Multiple Canvas Objects' (01:40 - 03:15). Focus on the 'why'—how splitting the UI aids organization, modularity, and can improve performance by isolating dynamic elements. We will achieve this by creating separate UI components for each part of our battle HUD.
Our BattleHUD will be responsible for drawing all these elements and, crucially, for showing or hiding them based on the current state of the battle, which is managed by the BattleScene's state machine (INPUT, EXECUTION, etc.).
2. Party and Enemy Status Displays
The most static part of the battle UI is the party status window. It needs to clearly display the name, HP, and MP for each active party member. The design of this is a staple of the genre.
RPG Game Design (Fundamentals, Patterns, Mechanics)
The article 'RPG Game Design' provides a high-level overview of different UI elements in an RPG. We'll look at its section on Combat UI to frame our goals.
Read the subsection titled 'Combat UI'. Notice the emphasis on keeping the UI clean so players can easily find relevant information. This is what we'll aim for with our status displays.
We'll create a PartyStatusWindow component. Inside, we'll iterate over the gameState.party array and draw the details for each member. This is a direct implementation of the data-binding concept you're familiar with from front-end frameworks.
A tutorial for RPG Maker's HUD Maker plugin shows a visual approach to this exact task, which we can replicate in code.
HUD Maker Tutorial #5 - Battle HUD Walkthrough
This HUD Maker tutorial by SomeRanDev demonstrates building a battle HUD piece-by-piece. We will follow a similar logic, but by writing JavaScript to draw onto our canvas.
Watch the first two sections, 'Setting up a basic battle HUD for the party leader' (00:37 - 02:06) and 'Extending the HUD for multiple party members' (02:06 - 04:06). Pay attention to how each piece of data (face, name, HP) is an individual element tied to the character's data, and how this is replicated for each party member with a condition to check if they exist.
Let's start structuring our BattleHUD and its PartyStatusWindow.
// /scenes/ui/BattleHUD.js
import { UIComponent } from '../../components/UIComponent.js';
import { Window } from '../../components/Window.js';
import { gameState } from '../../state/gameState.js';
import { itemData } from '../../data/itemData.js'; // We will need this soon
// ... other imports
class PartyStatusWindow extends Window {
constructor(config) {
super(config);
}
draw(context) {
super.draw(context); // Draws the window frame
const party = gameState.party;
party.forEach((actor, index) => {
if (actor) {
const yPos = 30 + index * 60; // Simple vertical layout
// Draw actor's name
context.font = '20px Arial';
context.fillStyle = 'white';
context.fillText(actor.name, this.x + 20, this.y + yPos);
// Draw HP
context.font = '18px Arial';
context.fillStyle = 'lightgrey';
context.fillText('HP:', this.x + 20, this.y + yPos + 25);
context.fillStyle = 'white';
context.fillText(`${actor.hp}/${actor.maxHp}`, this.x + 60, this.y + yPos + 25);
// TODO: Draw MP similarly
}
});
}
}
export class BattleHUD extends UIComponent {
constructor() {
super({ x: 0, y: 0, width: 816, height: 624 });
// A window for the party's status
this.partyStatusWindow = new PartyStatusWindow({ x: 550, y: 350, width: 250, height: 250 });
this.addChild(this.partyStatusWindow);
// We will add command windows and other elements here
}
update(input) {
// Handle input for the active menu
}
draw(context) {
this.children.forEach(child => child.draw(context));
}
}
The EnemyStatusDisplay can be even simpler: a window that lists the names of the enemies present in the battle. In classic JRPGs, enemy HP is usually hidden, so we only need to display their names. This can be another Window component that gets its data from the BattleScene.
3. Command Menus and Sub-Menus
When a character's turn begins, they need to select an action. This is handled by a command menu. If you have a character with dozens of skills, a single flat list is poor UX. We'll implement a categorized menu system, a classic JRPG solution.
[XP]Enhancing the Default Battle System
This forum post from Chaos Project, though for the older RPG Maker XP, is a treasure trove of classic JRPG mechanics. We'll use its idea for sorting skills.
Read the section 'Sorting Gamuts of Spells'. It describes a system where skills are assigned a 'dummy element' that acts as a category (e.g., Black Magic, White Magic). When the player selects the 'Magic' command, they first see these categories, and then the skills within them. We will adopt this exact data structure and UI flow.
First, let's update our skillData.js to include a category.
// /data/skillData.js (example)
export const skillData = {
'fire1': {
name: 'Fire',
mpCost: 4,
power: 10,
category: 'Black Magic', // Our new category property
target: 'single_enemy',
// ... other properties
},
'cure1': {
name: 'Cure',
mpCost: 3,
power: 15,
category: 'White Magic',
target: 'single_ally',
// ... other properties
}
};
Now, we can add the command windows to our BattleHUD. We'll need several SelectableList instances that we show and hide as needed.
// Add to BattleHUD class
// In constructor:
this.commandWindow = new Window({ x: 20, y: 450, width: 200, height: 150, visible: false });
this.skillCategoryWindow = new Window({ x: 230, y: 450, width: 200, height: 150, visible: false });
this.skillListWindow = new Window({ x: 440, y: 350, width: 300, height: 250, visible: false });
this.commandList = new SelectableList({ /* ... */ });
this.skillCategoryList = new SelectableList({ /* ... */ });
this.skillList = new SelectableList({ /* ... */ });
this.commandWindow.addChild(this.commandList);
// ... and so on
// We also need a way to show the initial command menu for a character
showCommandMenu(actor) {
this.activeActor = actor;
const commands = [{ text: 'Attack', value: 'attack' }];
// Only show 'Skill' if the actor has skills
if (actor.skills.length > 0) {
commands.push({ text: 'Skill', value: 'skill' });
}
// ... add Item, Defend, etc.
this.commandList.setItems(commands);
this.commandWindow.visible = true;
this.commandList.activate();
}
When the player selects 'Skill', we would deactivate commandList, populate skillCategoryList with the unique categories from the actor's available skills, and show its window. Selecting a category would then populate skillList. This nested menu flow is something you've implemented before in the shop scene, just with a different data source.
4. The Target Selection Cursor
After selecting an action, the player must choose a target. This requires a dynamic visual indicator—a cursor—that can be moved between valid targets.
The "Target Anyone" script discussed in the Chaos Project forum post provides the perfect mental model for this. Skills are tagged with who they can target (allies or foes), and the game uses this to determine where the selection arrow can go.
[XP]Enhancing the Default Battle System
Let's revisit the 'Enhancing the Default Battle System' article to see how it handles flexible targeting.
Read the section 'Time out! You just healed a ghost! What?? (Targeting Anyone)'. The key concept is adding a property to a skill to define its targeting scope. The game then lets the cursor move between enemies OR allies based on this property. Notice the limitation mentioned: it only works for single-target skills in that implementation. We will build ours to handle both single and group targets.
We already have a target property in our skill data (e.g., 'single_enemy', 'single_ally'). The BattleScene will use this to determine the list of valid targets. It will then pass this list to the BattleHUD.
The BattleHUD will contain a TargetingCursor component.
// A simple cursor component
class TargetingCursor extends UIComponent {
constructor(config) {
super(config);
this.validTargets = []; // An array of target objects {id, x, y}
this.selectedIndex = 0;
this.visible = false;
}
setTargets(targets) {
this.validTargets = targets;
this.selectedIndex = 0;
this.updatePosition();
}
update(input) {
if (!this.visible) return;
if (input.isPressed('ArrowDown')) {
this.selectedIndex = (this.selectedIndex + 1) % this.validTargets.length;
}
if (input.isPressed('ArrowUp')) {
this.selectedIndex = (this.selectedIndex - 1 + this.validTargets.length) % this.validTargets.length;
}
this.updatePosition();
}
updatePosition() {
if (this.validTargets.length > 0) {
const currentTarget = this.validTargets[this.selectedIndex];
this.x = currentTarget.x - 30; // Position cursor to the left of the target
this.y = currentTarget.y;
}
}
getSelectedTarget() {
return this.validTargets[this.selectedIndex];
}
draw(context) {
if (!this.visible) return;
// Draw an arrow or some indicator at this.x, this.y
context.fillStyle = 'red';
context.fillText('➤', this.x, this.y);
}
}
The BattleScene will be responsible for providing the screen coordinates (x, y) of each character sprite (both party and enemies). The BattleHUD will receive these coordinates along with the list of valid target IDs, activate the cursor, and let the player choose.
Test your understanding!
Our TargetingCursor currently handles single-target selection. Many JRPG skills target a group (e.g., 'All Enemies' or 'All Allies').
How would you modify the TargetingCursor and its draw method to visually represent group targeting? You don't need to write full code, just describe the logic.
Show answer
Instead of moving a single cursor, you could change the cursor's behavior based on the skill's target type (e.g., all_enemies).
- Modify
setTargets: TheBattleScenewould pass a flag indicating the target type.setTargets(targets, 'all_enemies'). - Modify
update: For 'all' target types, you would disable the up/down input, as there is no choice to make. The selection is the entire group. - Modify
draw: Instead of drawing one cursor, you would loop through allvalidTargetsand draw a cursor next to each one, visually indicating that the entire group is selected. Alternatively, you could draw a single, large highlighting box around the entire group.
This shows how the targeting UI can adapt based on the data properties of the selected skill.
Conclusion
Fantastic work today! We have designed and started implementing the entire battle UI, arguably the most complex and interactive screen in our game. By breaking it down into logical components, we made the task manageable and built upon our existing UI framework.
Key Takeaways:
- Component-Based UI Architecture: We structured the battle UI as a
BattleHUDcontaining multiple, specialized child components (PartyStatusWindow,CommandWindow,TargetingCursor), which is a robust and scalable approach. - Data-Driven Menus: We designed a categorized skill menu system by adding a
categoryproperty to our skill data, creating a much better user experience for players with many abilities. - Dynamic UI State: The visibility and content of our UI components (command menus, target cursor) are driven by the state of the
BattleScene, creating a clean separation between game logic and presentation. - Interactive Targeting: We implemented a
TargetingCursorthat allows players to select their targets, with its behavior determined by thetargetproperty of the chosen skill or item.
Preview of the next lesson:
We are now nearing the end of our UI module. With the world map, menus, and battle screen interfaces in place, only one critical piece of the classic JRPG experience is missing: persistence. In our next lesson, we will implement a save/load screen that interacts with multiple save slots, allowing players to save their progress and return to their adventure later.
Can't find a good explanation? Sign up and we'll make it for you
Sign up