Hello! Welcome back to our JRPG development journey.
In our last session, we built the foundational logic for using skills, covering how to select targets based on a skill's scope and how to process resource costs from its cost array. This was a crucial step, but a skill is only as interesting as what it does.
Today, we'll implement the heart of a skill's impact, focusing on the learning outcome: Implement status effects (e.g., Poison, Sleep) and elemental properties. We'll expand the effects array of our skill data to inflict lasting conditions on combatants and to leverage elemental strengths and weaknesses, adding a rich layer of strategy to our combat system.
1. Why Bother with Status Effects and Elements?
Before we dive into architecture, let's establish the design philosophy. In a simple JRPG, combat can devolve into a "DPS race"—repeatedly using your strongest attack until one side's HP hits zero. Status effects and elemental properties are the primary tools designers use to break this monotony and encourage strategic thinking.
What's the Point of Status Effects? ~ Design Doc
The video 'What's the Point of Status Effects?' from the Design Doc channel provides an excellent overview of why these systems are so critical for engaging combat.
Please watch from the beginning of the 'Purpose of Status Effects' section until the end of the 'Defining Status Effects' section (02:12 - 04:11). Pay attention to how the narrator frames status effects as a way to 'spice up the moment-to-moment decision making'.
As the video explains, these mechanics introduce new questions for the player: Is it better to inflict Poison now for damage over time, or just attack directly? Should I use a turn to lower the boss's defense? This is the strategic depth we aim to create.
2. The Architecture of Status Effects
Given your background in front-end development, you know that managing state changes over time is a core challenge. A character might be poisoned for 5 turns, have their attack boosted for 3 turns, and be asleep for 1 turn, all at the same time. We need a robust architecture to manage this.
There are two excellent software design patterns that are highly effective here, and we can use them in combination.
Pattern 1: The Observer Pattern for Triggered Effects
Many status effects are event-driven:
- Poison: "At the start of your turn, take damage."
- Regen: "At the end of your turn, restore HP."
- Stop: "When it's your turn, skip it."
The Observer pattern is perfect for this. In this model, each combatant has a StatusManager that holds a list of active status effects. The game's central turn loop emits events like TURN_START, TURN_END, etc. Active status effects can "subscribe" to these events and execute their logic when they occur.
This is very similar to addEventListener in JavaScript.
The article 'Tactics RPG Status Effects' provides a fantastic, albeit in C#, breakdown of an architecture that separates the effect from its condition (like its duration). We will adapt this concept for our JavaScript engine.
Read the sections 'Status Conditions', 'Status Effects', 'Poison', and 'Status'. You can skim the C# code, but focus on understanding the architectural separation: StatusEffect: A component that contains the logic of the effect (e.g., the PoisonStatusEffect reduces HP). StatusCondition: A component that determines how long the effect lasts (e.g., the DurationStatusCondition counts down turns). Status (our StatusManager): A manager class that adds and removes these effect/condition pairs from a character.
Let's translate this into a JavaScript structure.
The StatusManager on each combatant:
This class will manage adding, removing, and querying active statuses.
class StatusManager {
constructor(owner) {
this.owner = owner; // The combatant this manager belongs to
this.activeStatuses = []; // Array of { effect, condition } objects
}
addStatus(effect, condition) {
// Prevent stacking the same status, or handle it as needed
const existing = this.activeStatuses.find(s => s.effect.id === effect.id);
if (existing) {
// e.g., reset duration
existing.condition.reset();
return;
}
this.activeStatuses.push({ effect, condition });
effect.onApply(this.owner);
}
removeStatus(effectId) {
const index = this.activeStatuses.findIndex(s => s.effect.id === effectId);
if (index > -1) {
const { effect } = this.activeStatuses[index];
effect.onRemove(this.owner);
this.activeStatuses.splice(index, 1);
}
}
// Called by the game's turn controller
handleEvent(eventName, eventData) {
this.activeStatuses.forEach(({ effect }) => {
effect.handleEvent(eventName, this.owner, eventData);
});
}
updateDurations() {
// Iterate backwards when removing items from an array
for (let i = this.activeStatuses.length - 1; i >= 0; i--) {
const status = this.activeStatuses[i];
status.condition.tick();
if (status.condition.isExpired()) {
this.removeStatus(status.effect.id);
}
}
}
}
Example: A PoisonEffect
This object contains the specific logic for poison.
const PoisonEffect = {
id: 'poison',
name: 'Poison',
onApply(target) {
console.log(`${target.name} is poisoned!`);
// Maybe apply a visual tint
},
onRemove(target) {
console.log(`${target.name} is no longer poisoned.`);
},
handleEvent(eventName, target) {
if (eventName === 'TURN_START') {
const damage = Math.floor(target.stats.maxHp * 0.1); // 10% of max HP
target.takeDamage(damage);
console.log(`${target.name} takes ${damage} poison damage!`);
}
}
};
The game's battle loop would call activeCombatant.statusManager.updateDurations() at the end of every turn and activeCombatant.statusManager.handleEvent('TURN_START') at the beginning.

This image from RPG Maker shows a designer-friendly UI for setting up a Damage Over Time (DOT) effect like Poison. The <HP Slip Damage: 100> notetag is their way of attaching data to a status, which our PoisonEffect object handles in code.
Pattern 2: The Decorator Pattern for Stat Modifications
What about effects like Attack Up (Bravery) or Defense Down (Break)? These don't trigger on events; they passively modify a character's stats. Trying to manage this with events can get messy.
A more elegant solution is the Decorator pattern. The idea is to "wrap" a character's base stats with modifiers. When the game asks for a stat, the final value is calculated by passing it through the chain of decorators.
Decorator design pattern for dynamic game stats
This article, 'Decorator design pattern for dynamic game stats', provides a great explanation of this pattern, contrasting it with less scalable approaches that you'll recognize as anti-patterns from your web dev experience.
Read the sections 'The technical perspective of the problem', 'The other solution', 'Step forward: decorator design pattern', and 'The implementation of decorator pattern'. This will walk you through the problem and how the Decorator pattern provides a clean, scalable solution for stacking stat changes.
Let's adapt this to our combat system. Instead of directly accessing character.stats.atk, we'll use a getter function that applies our decorators.
The getFinalStats method in our Combatant class:
class Combatant {
// ... constructor, etc.
getFinalStats() {
let finalStats = { ...this.baseStats }; // Start with a copy of base stats
// Apply status effect modifiers
this.statusManager.activeStatuses.forEach(({ effect }) => {
if (effect.modifyStats) {
finalStats = effect.modifyStats(finalStats);
}
});
// Apply equipment modifiers (covered in a future lesson)
// ...
return finalStats;
}
}
Example: A DefenseDownEffect
const DefenseDownEffect = {
id: 'def_down',
name: 'Defense Down',
// ... onApply, onRemove for visual feedback
modifyStats(currentStats) {
// Return a new stats object with the defense lowered
return {
...currentStats,
def: Math.floor(currentStats.def * 0.75) // Reduce defense by 25%
};
}
};
Now, whenever we calculate damage, we use defender.getFinalStats().def instead of defender.baseStats.def, ensuring all active buffs and debuffs are automatically included.
Test your understanding!
How would you implement a Haste status effect, which makes a character's ATB gauge fill twice as fast? Which of the two patterns would be more appropriate and why?
Show answer
The Observer Pattern is more appropriate. Haste is an event-driven effect. In an Active Time Battle (ATB) system, the game loop would emit an ATB_TICK event on every frame. The HasteEffect would subscribe to this event.
Its handleEvent logic would look something like this:
handleEvent(eventName, target) {
if (eventName === 'ATB_TICK') {
// The default ATB gain is, say, target.stats.agility.
// Haste adds an extra amount.
target.atbGauge += target.stats.agility; // Double speed!
}
}
The base ATB gain would also happen on this tick. The HasteEffect effectively doubles the gain. The Decorator pattern wouldn't work as well because Haste modifies a rate of change, not a static value.
3. Implementing Elemental Properties
Elemental properties add another layer of rock-paper-scissors strategy. The implementation is refreshingly straightforward and data-driven. It involves two parts: defining an attack's element and defining a character's resistances.
What's The Point of Elements in Games?
To understand the design principles, watch this short segment from 'What's The Point of Elements in Games?'. It explains the common balancing schemes used in JRPGs.
Watch the sections on 'Opposing Pairs' and 'Tiers and Cycles' (05:52 - 09:29). This will give you context on why games use systems like Fire > Ice, or the more complex Pokémon type chart.
Data Implementation
-
On the Attack: We add an
elementproperty to a skill's damage effect."effects": [ { "type": "damage", "formula": "a.atk * 4 - b.def * 2", "element": "fire" } ]

*This UI from an RPG Maker-like tool shows exactly this concept. The designer can set the Damage Type, the Formula, and, crucially, the 'Element' of the attack.*
2. On the Combatant: We add an elementalResistances map to a combatant's data.
```javascript
// In the data for an "Ice Golem" enemy
"elementalResistances": {
"fire": 2.0, // Takes 200% damage from Fire (Weakness)
"ice": 0, // Takes 0% damage from Ice (Immune/Absorb)
"water": 0.5, // Takes 50% damage from Water (Resist)
"lightning": 1.0 // Takes 100% damage from Lightning (Neutral)
}
```
Logic Implementation
Now, we update our damage calculation function to use this data.
function calculateDamage(attacker, defender, skillEffect) {
// 1. Evaluate the base damage from the formula
const attackerStats = attacker.getFinalStats();
const defenderStats = defender.getFinalStats();
let baseDamage = evaluateFormula(skillEffect.formula, attackerStats, defenderStats);
// 2. Get the elemental multiplier
const element = skillEffect.element || 'physical'; // Default to 'physical' if no element
const resistanceMap = defender.data.elementalResistances || {};
const multiplier = resistanceMap[element] !== undefined ? resistanceMap[element] : 1.0;
// 3. Apply multiplier
let finalDamage = Math.floor(baseDamage * multiplier);
// ... add variance and critical hit logic here ...
return finalDamage;
}
With this simple addition, our engine can now handle a complex web of elemental interactions, driven entirely by our JSON data.
Conclusion
Today, you've added two of the most important systems for creating strategic depth in a JRPG. By leveraging your software architecture knowledge, you can see how to build these systems in a way that is robust, scalable, and easy for a game designer (even if that designer is you!) to use.
Key Takeaways:
- Strategic Purpose: Status effects and elements exist to break up monotonous combat and reward thoughtful play.
- Architectural Patterns: The Observer pattern is ideal for event-driven effects (like Poison), while the Decorator pattern is perfect for passive stat modifications (like buffs/debuffs).
- Data-Driven Design: Both systems are powered by clean data. We can define a new status effect or an entire elemental chart just by adding new JSON objects, without touching the core engine code.
- System Integration: These new effects are easily integrated into the
effectsarray of our existing skill data structure, creating complex skills that can do damage and apply statuses simultaneously.
Preview of the next lesson:
Now that our combatants can be affected by a wide range of statuses and elemental attacks, how do we make our enemies react intelligently? In the next lesson, we will design and implement a simple enemy AI system for selecting actions based on predefined patterns or conditions. An enemy might learn to use a Fire spell against a party member it knows is weak to Fire, or prioritize healing its poisoned allies.
Can't find a good explanation? Sign up and we'll make it for you
Sign up