4
\$\begingroup\$

I'm making a small text browser RPG game using PHP, MySQL and Java Script (and jQuery). All the items are saved in the MySQL database.

Every now and then a character have to make a skill roll (Talk, Climb, Sneak, etc. ), but I have the following problem - some of the items give passive bonuses to some skills. Let's say we have Official Suit item, it gives +1 to Talk rolls and One time use only - automatic success on a Talk roll.

My problem is the following - how can I implement a function for every item, that gives the bonuses or other options when the appropriate roll is made. I want when the player click the Talk option, a function to be run that makes the Talk roll and searches character's inventory for items that give him bonuses to that roll. If it sees that the player have such an item, let's say it's the Official Suit, it runs the item's function which gives the +1 Talk bonus and if the roll fails, the function gives an option to use the special ability of the item for automatic success.

So what is the best way to do this? Keywords to each item or some other way? I can't think of a scalable solution.

\$\endgroup\$
2
\$\begingroup\$

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);
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ Thank you for the great answer! However, there are some things that I couldn't understand fully: 1. What does the onCalculateCharacterStat function contain? Is it running the BonusCollectionEvent function or there is something that I'm missing? 2. What is the best way to send, an enemy stat to the reactToOnCalculateCharacterStat function if I want the item to work for example only if the enemy have Intelligence below 6? \$\endgroup\$ – Ziik Aug 17 '12 at 17:28
  • \$\begingroup\$ 1. Sorry for the confusion! onCalculateCharacterStat() is only needed in a particular framework in a particular language in which I am currently coding and that I had in mind when I was posting my answer. Please see my updated answer, which got rid of onCalculateCharacterStat() altogether. 2. See the example at the end. \$\endgroup\$ – BerndBrot Aug 17 '12 at 22:53
  • \$\begingroup\$ What does the raiseEvent contain and how I register the item's event handler reactToCalculateCharacterStatEvent at the Character object? You answer is more than sufficient but I've never used events and event handlers in PHP until now and it's a little hard for me то understand it fully, sorry for that. \$\endgroup\$ – Ziik Aug 18 '12 at 9:52
  • \$\begingroup\$ "raiseEvent" is pseudo-code and "register event listener" is generic for "do whatever is needed in your programming language and/or framework to do this". :D Since you are committed to PHP, I recommend using a PHP framework (phpframeworks.com). Yii, Symfony, and others have events (i.e. the Observer pattern) deeply integrated into their design, so that defining, dispatching, and reacting to events becomes very easy. See en.wikipedia.org/wiki/Observer_pattern for the Observer pattern and yiiframework.com/wiki/44/behaviors-events for how to do this in Yii. \$\endgroup\$ – BerndBrot Aug 18 '12 at 10:23
  • \$\begingroup\$ I'm using CodeIgniter and it looks like it doesn't have event dispatcher, so I will have to download a library for it (or maybe Symphony's can work with CodeIgniter's?). I knew that it would be better to use newer framework like Laravel maybe. Thank you very much for the time you spared explaining me all this stuff =] \$\endgroup\$ – Ziik Aug 18 '12 at 13:40
1
\$\begingroup\$

how can I implement a function for every item, that gives the bonuses or other options when the appropriate roll is made

First, there are two approaches to managing passive bonuses and such:

  1. Calculate resulting value whenever you need it.
  2. Calculate resulting value whenever circumstances change.

But there is also a third way: keep a list of effects and update it whenever circumstances change, and calculate a resulting value whenever you need it.

using PHP, MySQL

I would create an effects table with fields

  • character - character (PC/NPC) id/ref
  • attribute - target attribute id/ref
  • method - modification method enum('add'|'mult')
  • value - effect value

You can have a library of possible effects stored in some other table, so you can fetch attribute, method, value fields whenever you need to add an effect. You can reference to effects in this library from your items or spells/buffs.

Now, regarding your question, whenever you equip/unequip an item (or apply/remove a buff) you simply

  1. lookup for an effect on the library and
  2. add (insert character, ...effect) or remove (delete where character, ...effect) a record to the effects table.

And don't worry if you added two identical effects - it doesn't matter in which order are you going to remove them, as long as you remove both of them.

And whenever you need to get resulting value you can run a simple SELECT method, SUM(value) FROM effects WHERE character=$id AND attribute=$attr GROUP BY method. This will return sums of all +X effects and all of +X% effects. Then you can apply them in $attr = $attr * (1 + $mult / 100) + $add fashion.

DBFiddle example

And if you are comfortable with a bit more complicated DB queries (selecting), you can use library reference in the effects table instead of copying values.

\$\endgroup\$
0
\$\begingroup\$

What kind of effects do +you want to use? The answer may constrain the solution. For example if all bonuses are like "+2 skill X" or "-1 skill X", then it does not matter in what order they are applied. If the bonuses are like "+2 skill X" or "+50% skill X", suddenly it becomes important whether you apply the +50% bonus before the +2 bonus, giving 10 × 1.5 + 2 = 17, or after the +2 bonus, giving (10 + 2) × 1.5 = 18. And that was a very simple example! More complex example could increase skill X by 20% of skill Y, or give a bonus only at night, or only against some kinds of monsters/attacks, possibly with an area effect but only for members of the same guild, or one-time use with a timeout. And it could burn your mana or health while it is active. So it is probably good to specify in advance what the effects can and cannot do.

If you want to treat each object as a black box, and you want to avoid having the difference between "+2, +50%" and "+50%, +2" depending on the order of objects in inventory, you could do this:

For each object, make a function getEffects() which returns a set of Effect objects. When something important is happening, collect all the effects. Each effect has a priority and a function.

Sort all those effects by priority. A priority is a number you have specified for each effect. For example if you want to process all the additive effects (+2 to skill X) first, and all the multiplicative effects (+50% to skill X) later, assign them priorities 1 and 2. For other effects use other priority numbers depending on when you want them to apply.

Then put the character data (their base skills, and everything that could be relevant to the effects) into a variable, and gradually apply all the effects on this variable: $data = $effect->applyTo($data).

If you want some skills used only in some context, make it $data = $effect->applyTo($data, $context). So the item may activate only when $context is TALK_ROLL. If you want an automatic success on talk rolls, you could put a variable "automatic success" to the $data, and the item would set it on true (only if it was false previously, so you wouldn't waste two one-time bonuses on the same roll).

\$\endgroup\$
  • \$\begingroup\$ Thank you, that's another way to look at things. How do you see this Priority implemented? \$\endgroup\$ – Ziik Aug 17 '12 at 20:54
  • \$\begingroup\$ If you want to write any kind of crazy effect (e.g. "add 2 to WIS if INT is a prime number"), you probably need to write those effects in PHP code, and in the database store their names and parameters. So in the database you would have e.g. EffectName=AddIfPrime, EffectParam1=2, EffectParam2=WIS, EffectParam3=INT. Then in PHP you make EffectFactory with function createEffect(name, parameters). This function contains a long if-else chain, and for name=="AddIfPrime" it produces an object with given integer priority (hardcoded there) and a function that applies the effect. \$\endgroup\$ – Viliam Búr Aug 22 '12 at 7:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.