diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt
index 202517d23..5c1f5cd41 100644
--- a/RELEASE_NOTES.txt
+++ b/RELEASE_NOTES.txt
@@ -6,6 +6,7 @@ Engine features:
* Add scaling based on item level/player level/primary stats to several Item attributes, such as 'bonus' and 'price'.
* Add 'loot_drops_max' property to Items for limiting the number of drops in a single loot event.
* Horizontal list widgets can now display 2 or fewer options at once
+* Added support for more than one of each chain power (pre_power/post_power/wall_power) to be attached to a single power.
Engine fixes:
diff --git a/docs/attribute-reference.html b/docs/attribute-reference.html
index 481d35f69..ad8e6fa22 100644
--- a/docs/attribute-reference.html
+++ b/docs/attribute-reference.html
@@ -2068,11 +2068,11 @@
PowerManager: Powers
power.post_effect_src | predefined_string, float, duration , float : Effect ID, Magnitude, Duration, Chance to apply
| Post effect to apply to caster. Duration is in 'ms' or 's'.
-power.pre_power | power_id, float : Power, Chance to cast
| Trigger a power immediately when casting this one.
+power.pre_power | repeatable(power_id, float) : Power, Chance to cast
| Trigger a power immediately when casting this one.
-power.post_power | power_id, int : Power, Chance to cast
| Trigger a power if the hazard did damage. For 'block' type powers, this power will be triggered when the blocker takes damage.
+power.post_power | repeatable(power_id, int) : Power, Chance to cast
| Trigger a power if the hazard did damage. For 'block' type powers, this power will be triggered when the blocker takes damage.
-power.wall_power | power_id, int : Power, Chance to cast
| Trigger a power if the hazard hit a wall.
+power.wall_power | repeatable(power_id, int) : Power, Chance to cast
| Trigger a power if the hazard hit a wall.
power.wall_reflect | bool
| Moving power will bounce off walls and keep going
diff --git a/src/Avatar.cpp b/src/Avatar.cpp
index 109259291..c3392ab3f 100644
--- a/src/Avatar.cpp
+++ b/src/Avatar.cpp
@@ -783,8 +783,11 @@ void Avatar::logic() {
stats.prevent_interrupt = power.prevent_interrupt;
- if (power.pre_power > 0 && Math::percentChanceF(power.pre_power_chance)) {
- powers->activate(power.pre_power, &stats, target);
+ for (size_t j = 0; j < power.chain_powers.size(); ++j) {
+ const ChainPower& chain_power = power.chain_powers[j];
+ if (chain_power.type == ChainPower::TYPE_PRE && Math::percentChanceF(chain_power.chance)) {
+ powers->activate(chain_power.id, &stats, target);
+ }
}
switch (power.new_state) {
diff --git a/src/Entity.cpp b/src/Entity.cpp
index cda3a73cb..f0c7bb183 100644
--- a/src/Entity.cpp
+++ b/src/Entity.cpp
@@ -609,8 +609,11 @@ bool Entity::takeHit(Hazard &h) {
stats.effects.removeEffectID(powers->powers[h.power_index].remove_effects);
// post power
- if (h.power->post_power > 0 && Math::percentChanceF(h.power->post_power_chance)) {
- powers->activate(h.power->post_power, h.src_stats, stats.pos);
+ for (size_t i = 0; i < h.power->chain_powers.size(); ++i) {
+ ChainPower& chain_power = h.power->chain_powers[i];
+ if (chain_power.type == ChainPower::TYPE_POST && Math::percentChanceF(chain_power.chance)) {
+ powers->activate(chain_power.id, h.src_stats, stats.pos);
+ }
}
}
@@ -674,12 +677,12 @@ bool Entity::takeHit(Hazard &h) {
// handle block post-power
if (stats.block_power != 0) {
- PowerID block_post_power = powers->powers[stats.block_power].post_power;
-
- if (block_post_power != 0 && stats.getPowerCooldown(block_post_power) == 0) {
- if (Math::percentChanceF(powers->powers[stats.block_power].post_power_chance)) {
- powers->activate(powers->powers[stats.block_power].post_power, &stats, stats.pos);
- stats.setPowerCooldown(block_post_power, powers->powers[block_post_power].cooldown);
+ Power& block_power = powers->powers[stats.block_power];
+ for (size_t i = 0; i < block_power.chain_powers.size(); ++i) {
+ ChainPower& chain_power = block_power.chain_powers[i];
+ if (chain_power.type == ChainPower::TYPE_POST && stats.getPowerCooldown(chain_power.id) == 0 && Math::percentChanceF(chain_power.chance)) {
+ powers->activate(chain_power.id, &stats, stats.pos);
+ stats.setPowerCooldown(chain_power.id, powers->powers[chain_power.id].cooldown);
}
}
}
diff --git a/src/EntityBehavior.cpp b/src/EntityBehavior.cpp
index a81242100..107638048 100644
--- a/src/EntityBehavior.cpp
+++ b/src/EntityBehavior.cpp
@@ -724,8 +724,12 @@ void EntityBehavior::updateState() {
// sound effect based on power type
if (e->activeAnimation->isFirstFrame()) {
- if (powers->powers[power_id].pre_power > 0 && Math::percentChanceF(powers->powers[power_id].pre_power_chance)) {
- powers->activate(powers->powers[power_id].pre_power, &e->stats, pursue_pos);
+ // pre power
+ for (size_t i = 0; i < powers->powers[power_id].chain_powers.size(); ++i) {
+ ChainPower& chain_power = powers->powers[power_id].chain_powers[i];
+ if (chain_power.type == ChainPower::TYPE_PRE && Math::percentChanceF(chain_power.chance)) {
+ powers->activate(chain_power.id, &e->stats, pursue_pos);
+ }
}
float attack_speed = (e->stats.effects.getAttackSpeed(powers->powers[power_id].attack_anim) * powers->powers[power_id].attack_speed) / 100.0f;
diff --git a/src/HazardManager.cpp b/src/HazardManager.cpp
index 02f19cc57..033aef9c9 100644
--- a/src/HazardManager.cpp
+++ b/src/HazardManager.cpp
@@ -72,11 +72,14 @@ void HazardManager::logic() {
EventManager::executeScript(h[i-1]->power->script, h[i-1]->pos.x, h[i-1]->pos.y);
}
- if (h[i-1]->power->wall_power > 0 && Math::percentChanceF(h[i-1]->power->wall_power_chance)) {
- powers->activate(h[i-1]->power->wall_power, h[i-1]->src_stats, h[i-1]->pos);
+ for (size_t j = 0; j < h[i-1]->power->chain_powers.size(); ++j) {
+ ChainPower& chain_power = h[i-1]->power->chain_powers[j];
+ if (chain_power.type == ChainPower::TYPE_WALL && Math::percentChanceF(chain_power.chance)) {
+ powers->activate(chain_power.id, h[i-1]->src_stats, h[i-1]->pos);
- if (powers->powers[h[i-1]->power->wall_power].directional) {
- powers->hazards.back()->animationKind = h[i-1]->animationKind;
+ if (powers->powers[chain_power.id].directional) {
+ powers->hazards.back()->animationKind = h[i-1]->animationKind;
+ }
}
}
diff --git a/src/PowerManager.cpp b/src/PowerManager.cpp
index ecf85b91d..6b7a8b9e6 100644
--- a/src/PowerManager.cpp
+++ b/src/PowerManager.cpp
@@ -146,12 +146,6 @@ Power::Power()
, buff_teleport(false)
, buff_party(false)
, buff_party_power_id(0)
- , pre_power(0)
- , pre_power_chance(100)
- , post_power(0)
- , post_power_chance(100)
- , wall_power(0)
- , wall_power_chance(100)
, wall_reflect(false)
, spawn_type("")
, target_neighbor(0)
@@ -827,27 +821,42 @@ void PowerManager::loadPowers() {
}
// pre and post power effects
else if (infile.key == "pre_power") {
- // @ATTR power.pre_power|power_id, float : Power, Chance to cast|Trigger a power immediately when casting this one.
- powers[input_id].pre_power = Parse::popFirstInt(infile.val);
+ // @ATTR power.pre_power|repeatable(power_id, float) : Power, Chance to cast|Trigger a power immediately when casting this one.
+ ChainPower chain_power;
+ chain_power.type = ChainPower::TYPE_PRE;
+ chain_power.id = Parse::popFirstInt(infile.val);
std::string chance = Parse::popFirstString(infile.val);
if (!chance.empty()) {
- powers[input_id].pre_power_chance = Parse::toFloat(chance);
+ chain_power.chance = Parse::toFloat(chance);
+ }
+ if (chain_power.id > 0) {
+ powers[input_id].chain_powers.push_back(chain_power);
}
}
else if (infile.key == "post_power") {
- // @ATTR power.post_power|power_id, int : Power, Chance to cast|Trigger a power if the hazard did damage. For 'block' type powers, this power will be triggered when the blocker takes damage.
- powers[input_id].post_power = Parse::popFirstInt(infile.val);
+ // @ATTR power.post_power|repeatable(power_id, int) : Power, Chance to cast|Trigger a power if the hazard did damage. For 'block' type powers, this power will be triggered when the blocker takes damage.
+ ChainPower chain_power;
+ chain_power.type = ChainPower::TYPE_POST;
+ chain_power.id = Parse::popFirstInt(infile.val);
std::string chance = Parse::popFirstString(infile.val);
if (!chance.empty()) {
- powers[input_id].post_power_chance = Parse::toFloat(chance);
+ chain_power.chance = Parse::toFloat(chance);
+ }
+ if (chain_power.id > 0) {
+ powers[input_id].chain_powers.push_back(chain_power);
}
}
else if (infile.key == "wall_power") {
- // @ATTR power.wall_power|power_id, int : Power, Chance to cast|Trigger a power if the hazard hit a wall.
- powers[input_id].wall_power = Parse::popFirstInt(infile.val);
+ // @ATTR power.wall_power|repeatable(power_id, int) : Power, Chance to cast|Trigger a power if the hazard hit a wall.
+ ChainPower chain_power;
+ chain_power.type = ChainPower::TYPE_WALL;
+ chain_power.id = Parse::popFirstInt(infile.val);
std::string chance = Parse::popFirstString(infile.val);
if (!chance.empty()) {
- powers[input_id].wall_power_chance = Parse::toFloat(chance);
+ chain_power.chance = Parse::toFloat(chance);
+ }
+ if (chain_power.id > 0) {
+ powers[input_id].chain_powers.push_back(chain_power);
}
}
else if (infile.key == "wall_reflect")
@@ -1038,9 +1047,12 @@ void PowerManager::loadPowers() {
power_animations[power_it->first] = anim->getAnimationSet(power.animation_name)->getAnimation("");
}
- // verify wall/post power ids
- power.wall_power = verifyID(power.wall_power, NULL, ALLOW_ZERO_ID);
- power.post_power = verifyID(power.post_power, NULL, ALLOW_ZERO_ID);
+ // verify chain power ids
+ for (size_t i = power.chain_powers.size(); i > 0; --i) {
+ power.chain_powers[i-1].id = verifyID(power.chain_powers[i-1].id, NULL, ALLOW_ZERO_ID);
+ if (power.chain_powers[i-1].id == 0)
+ power.chain_powers.erase(power.chain_powers.begin() + i-1);
+ }
// calculate effective combat range
{
@@ -1282,8 +1294,11 @@ void PowerManager::buff(PowerID power_index, StatBlock *src_stats, const FPoint&
src_stats->effects.removeEffectID(powers[power_index].remove_effects);
if (!powers[power_index].passive) {
- if (Math::percentChanceF(powers[power_index].post_power_chance)) {
- activate(powers[power_index].post_power, src_stats, src_stats->pos);
+ for (size_t i = 0; i < powers[power_index].chain_powers.size(); ++i) {
+ ChainPower& chain_power = powers[power_index].chain_powers[i];
+ if (chain_power.type == ChainPower::TYPE_POST && Math::percentChanceF(chain_power.chance)) {
+ activate(chain_power.id, src_stats, src_stats->pos);
+ }
}
}
}
@@ -1849,9 +1864,11 @@ bool PowerManager::activatePassiveByTrigger(PowerID power_id, StatBlock *src_sta
activate(power_id, src_stats, src_stats->pos);
src_stats->refresh_stats = true;
- PowerID post_power = powers[power_id].post_power;
- if (post_power > 0) {
- src_stats->setPowerCooldown(post_power, powers[post_power].cooldown);
+ for (size_t i = 0; i < powers[power_id].chain_powers.size(); ++i) {
+ ChainPower& chain_power = powers[power_id].chain_powers[i];
+ if (chain_power.type == ChainPower::TYPE_POST) {
+ src_stats->setPowerCooldown(chain_power.id, powers[chain_power.id].cooldown);
+ }
}
return true;
@@ -1871,9 +1888,11 @@ void PowerManager::activateSinglePassive(StatBlock *src_stats, PowerID id) {
src_stats->refresh_stats = true;
src_stats->effects.triggered_others = true;
- PowerID post_power = powers[id].post_power;
- if (post_power > 0) {
- src_stats->setPowerCooldown(post_power, powers[post_power].cooldown);
+ for (size_t i = 0; i < powers[id].chain_powers.size(); ++i) {
+ ChainPower& chain_power = powers[id].chain_powers[i];
+ if (chain_power.type == ChainPower::TYPE_POST) {
+ src_stats->setPowerCooldown(chain_power.id, powers[chain_power.id].cooldown);
+ }
}
}
}
@@ -1883,21 +1902,22 @@ void PowerManager::activateSinglePassive(StatBlock *src_stats, PowerID id) {
*/
void PowerManager::activatePassivePostPowers(StatBlock *src_stats) {
for (size_t i = 0; i < src_stats->powers_passive.size(); ++i) {
- const PowerID post_power = powers[src_stats->powers_passive[i]].post_power;
- if (post_power == 0)
- continue;
+ Power& passive_power = powers[src_stats->powers_passive[i]];
- if (powers[post_power].new_state != Power::STATE_INSTANT)
- continue;
+ for (size_t j = 0; j < passive_power.chain_powers.size(); ++j) {
+ ChainPower& chain_power = passive_power.chain_powers[j];
+ if (powers[chain_power.id].new_state != Power::STATE_INSTANT)
+ continue;
- // blocking powers use a passive trigger, but we only want to activate their post_power when the blocker takes a hit
- if (powers[src_stats->powers_passive[i]].type == Power::TYPE_BLOCK)
- continue;
+ // blocking powers use a passive trigger, but we only want to activate their post_power when the blocker takes a hit
+ if (passive_power.type == Power::TYPE_BLOCK)
+ continue;
- if (src_stats->getPowerCooldown(post_power) == 0 && src_stats->canUsePower(post_power, !StatBlock::CAN_USE_PASSIVE)) {
- if (Math::percentChanceF(powers[src_stats->powers_passive[i]].post_power_chance)) {
- activate(post_power, src_stats, src_stats->pos);
- src_stats->setPowerCooldown(post_power, powers[post_power].cooldown);
+ if (src_stats->getPowerCooldown(chain_power.id) == 0 && src_stats->canUsePower(chain_power.id, !StatBlock::CAN_USE_PASSIVE)) {
+ if (Math::percentChanceF(chain_power.chance)) {
+ activate(chain_power.id, src_stats, src_stats->pos);
+ src_stats->setPowerCooldown(chain_power.id, powers[chain_power.id].cooldown);
+ }
}
}
}
@@ -1913,7 +1933,7 @@ EffectDef* PowerManager::getEffectDef(const std::string& id) {
}
PowerID PowerManager::verifyID(PowerID power_id, FileParser* infile, bool allow_zero) {
- if (!allow_zero && power_id == 0) {
+ if ((!allow_zero && power_id == 0) || power_id >= powers.size()) {
if (infile != NULL)
infile->error("PowerManager: %d is not a valid power id.", power_id);
else
diff --git a/src/PowerManager.h b/src/PowerManager.h
index 452f80cdd..958387e40 100644
--- a/src/PowerManager.h
+++ b/src/PowerManager.h
@@ -76,6 +76,25 @@ class PowerRequiredItem {
{}
};
+class ChainPower {
+public:
+ enum {
+ TYPE_PRE = 0,
+ TYPE_POST,
+ TYPE_WALL,
+ };
+
+ PowerID id;
+ uint8_t type;
+ float chance;
+
+ ChainPower()
+ : id(0)
+ , type(0)
+ , chance(100)
+ {}
+};
+
class Power {
public:
enum {
@@ -268,12 +287,8 @@ class Power {
std::vector post_effects;
- PowerID pre_power;
- float pre_power_chance;
- PowerID post_power;
- float post_power_chance;
- PowerID wall_power;
- float wall_power_chance;
+ std::vector chain_powers;
+
bool wall_reflect;
// spawn info
diff --git a/src/StatBlock.cpp b/src/StatBlock.cpp
index b6380e572..d62456f5e 100644
--- a/src/StatBlock.cpp
+++ b/src/StatBlock.cpp
@@ -731,18 +731,23 @@ void StatBlock::load(const std::string& filename) {
powers_passive.clear();
std::string p = Parse::popFirstString(infile.val);
while (p != "") {
+ PowerID passive_id = Parse::toPowerID(p);
powers_passive.push_back(Parse::toPowerID(p));
- p = Parse::popFirstString(infile.val);
// if a passive power has a post power, add it to the AI power list so we can track its cooldown
- PowerID post_power = powers->powers[powers_passive.back()].post_power;
- if (post_power > 0) {
- AIPower passive_post_power;
- passive_post_power.type = AI_POWER_PASSIVE_POST;
- passive_post_power.id = post_power;
- passive_post_power.chance = 0; // post_power chance is used instead
- powers_ai.push_back(passive_post_power);
+ Power& passive_power = powers->powers[passive_id];
+ for (size_t i = 0; i < passive_power.chain_powers.size(); ++i) {
+ ChainPower& chain_power = passive_power.chain_powers[i];
+ if (chain_power.type == ChainPower::TYPE_POST) {
+ AIPower passive_post_power;
+ passive_post_power.type = AI_POWER_PASSIVE_POST;
+ passive_post_power.id = chain_power.id;
+ passive_post_power.chance = 0; // post_power chance is used instead
+ powers_ai.push_back(passive_post_power);
+ }
}
+
+ p = Parse::popFirstString(infile.val);
}
}
diff --git a/src/Version.cpp b/src/Version.cpp
index b49917f81..61d3361b8 100644
--- a/src/Version.cpp
+++ b/src/Version.cpp
@@ -30,7 +30,7 @@ FLARE. If not, see http://www.gnu.org/licenses/
#include
-Version VersionInfo::ENGINE(1, 14, 25);
+Version VersionInfo::ENGINE(1, 14, 26);
Version VersionInfo::MIN(0, 0, 0);
Version VersionInfo::MAX(USHRT_MAX, USHRT_MAX, USHRT_MAX);