Complete roguelike tutorial using C++ and libtcod - extra 5: more generic items
Complete roguelike tutorial using C++ and libtcod -originally written by Jice Text in this tutorial was released under the Creative Commons Attribution-ShareAlike 3.0 Unported and the GNU Free Documentation License (unversioned, with no invariant sections, front-cover texts, or back-cover texts) on 2015-09-21. |
---|
|
This article is an optional "extra" that implements more generic items, making it possible to create new items with less code. It can be applied on article 9 source code.
In article 9, we brought more variety to the items, but each new item required a new class inheriting from Pickable. Yet, we can see some pattern in the way pickable actors behave :
They all select one or several targets :
- the wearer of the item (health potion)
- the wearer's closest enemy (lightning bolt)
- a selected actor (confusion)
- all actors close to a selected tile (fireball)
A fifth way that is not yet used but might be useful would be :
- all actors close to the wearer of the item
They all apply some effect to the selected targets :
- increase or decrease health (all but confusion)
- temporary replace the target's Ai (confusion)
So instead of implementing the target selection and the effect in a class inheriting from Pickable, why not add a TargetSelector and an Effect directly to the Pickable class ?
A generic target selector
All target selection algorithms but the one selecting the wearer use a range. So we create a class with an enum for the algorithm and a range field.
class TargetSelector { public : enum SelectorType { CLOSEST_MONSTER, SELECTED_MONSTER, WEARER_RANGE, SELECTED_RANGE }; TargetSelector(SelectorType type, float range); void selectTargets(Actor *wearer, TCODList<Actor *> & list); protected : SelectorType type; float range; };
There's no WEARER selector type. Well we'll simply assume that if the pickable has no TargetSelector, the effect applies to the wearer.
As usual, the constructor is trivial :
TargetSelector::TargetSelector(SelectorType type, float range) : type(type), range(range) { }
The selectTargets method populate the list with all selected actors, depending on the algorithm. CLOSEST_MONSTER grabs the monster closest to the wearer :
void TargetSelector::selectTargets(Actor *wearer, TCODList<Actor *> & list) { switch(type) { case CLOSEST_MONSTER : { Actor *closestMonster=engine.getClosestMonster(wearer->x,wearer->y,range); if ( closestMonster ) { list.push(closestMonster); } } break;
SELECTED_MONSTER asks the player to chose an actor (possibly the player himself) :
case SELECTED_MONSTER : { int x,y; engine.gui->message(TCODColor::cyan, "Left-click to select an enemy,\nor right-click to cancel."); if ( engine.pickATile(&x,&y,range)) { Actor *actor=engine.getActor(x,y); if ( actor ) { list.push(actor); } } } break;
WEARER_RANGE picks all actors close enough from the wearer (excluding the wearer himself) :
case WEARER_RANGE : for (Actor **iterator=engine.actors.begin(); iterator != engine.actors.end(); iterator++) { Actor *actor=*iterator; if ( actor != wearer && actor->destructible && !actor->destructible->isDead() && actor->getDistance(wearer->x,wearer->y) <= range) { list.push(actor); } } break;
and SELECTED_RANGE asks the player to pick a tile, then selects all actors close enough from this tile (possibly including the player) :
case SELECTED_RANGE : int x,y; engine.gui->message(TCODColor::cyan, "Left-click to select a tile,\nor right-click to cancel."); if ( engine.pickATile(&x,&y)) { for (Actor **iterator=engine.actors.begin(); iterator != engine.actors.end(); iterator++) { Actor *actor=*iterator; if ( actor->destructible && !actor->destructible->isDead() && actor->getDistance(x,y) <= range) { list.push(actor); } } } break;
Finally, if no monster has been selected, we display a message :
} if ( list.isEmpty() ) { engine.gui->message(TCODColor::lightGrey,"No enemy is close enough"); } }