Here is one way to do it:
CalculateCharacterStatEvent
Define a CalculateCharacterStatEvent with properties sender (the Character object that raised the event), stat (the name of the stat that is to be calculated), bonusAbsolute and bonusPercentageBased and some setter functions to manipulate the bonus properties, like increaseBonusAbsolute().
Items and event listeners
Add the item's bonuses in the item's reactToCalculateCharacterStatEvent() method. For example:
public function reactToCalculateCharacterStatEvent($event) {
// The event listener only reacts if the talk skill is calculated
if($event->getStat() == "talk") {
// Increase the item bonus depending on the Character's intelligence
if($event->sender->intelligence < 6) {
$event->increaseBonusAbsolute(1);
} else {
$event->increaseBonusAbsolute(2);
}
}
}
Event listener registration
When you load the items, register the item's event handler reactToCalculateCharacterStatEvent at the Character object, so that it is called whenever the Character object raises a CalculateCharacterStatEvent.
Character class and event raisers
In the Character class, define functions that return character stats, like getAttribute() or getSkillLevel(). At the beginning of your getStat() functions, raise a CalculateCharacterStatEvent. This will give the item's event listeners a chance to respond to the event by changing its bonusAbsolute and bonusPercentage values. Use these values to modify the result of your getStat() methods. For example:
getSkillTalking() {
$event = new CalculateCharacterStatEvent($this, "talking");
$this->raiseEvent($event);
$base = 5; // or some logic to determine the base stats
return $base * $event->getBonusPercentage + $event->getBonusAbsolute();
}
Option to automatically succeed
In order to add functionalities to items, you can just add new event classes, event raisers in the Character class, and new event listeners in the item classes.
For example: you could add an event called FailedSkillCheckEvent and an event listener reactToFailedSkillCheckEvent() in the item classes. When a skill check fails, the Character class raises a FailedSkillCheckEvent. The items's event listeners then have an opportunity to react by adding recovery options to the event object. After raising the FailedSkillCheckEvent, the skill check method continues depending on whether or not there are recovery options listed in the FailedSkillcheckEvent.
Further enhancements
Decorators. This basic pattern can be made more flexible by turning the item's reactTo-methods into a decorator class (Decorator pattern), which can then also be used by other game elements like status effects, passive skills, etc.
Link with data. reactToCalculateCharacterStatEvent() and other event listeners can also be linked to the item's properties so that you can edit the bonuses in a database without having to change any code for bonus assignments. (You can still code unique item behavior if you want to.)
More complex events. In the examples above, the item's event listeners have access to the sender of the event, i.e. the Character object for whom some stat is to be calculated. You could define more complex events that can pass more information to the items's event listeners. For example, you pass an object that represents the enemy of the Character object in a battle context. Your items's bonuses could then depend on the enemy's stats:
// in Character class
getSkillMelee($battle) {
$event = new CalculateCharacterBattleStatEvent($this, $battle->enemy, "melee");
$this->raiseEvent($event);
$base = 5; // or some logic to determine the base stats
return $base * $event->getBonusPercentage + $event->getBonusAbsolute();
}
// in Item class
public function reactToCalculateCharacterBattleStatEvent($event) {
if($event->getStat() == "melee") {
// Increase the item bonus depending on the Character's enemy's intelligence
if($event->enemy->intelligence < 6) {
$event->increaseBonusAbsolute(1);
} else {
$event->increaseBonusAbsolute(2);
}
}
}