From 5eebc2d8b04d6a4236b793bc09675a9ac302d696 Mon Sep 17 00:00:00 2001 From: Justin Jacobs Date: Sat, 26 Aug 2023 11:30:33 -0400 Subject: [PATCH] Support multiple "chain" powers (pre/post/wall) per Power --- RELEASE_NOTES.txt | 1 + docs/attribute-reference.html | 6 +-- src/Avatar.cpp | 7 ++- src/Entity.cpp | 19 ++++--- src/EntityBehavior.cpp | 8 ++- src/HazardManager.cpp | 11 ++-- src/PowerManager.cpp | 98 +++++++++++++++++++++-------------- src/PowerManager.h | 27 +++++++--- src/StatBlock.cpp | 21 +++++--- src/Version.cpp | 2 +- 10 files changed, 127 insertions(+), 73 deletions(-) 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);