Skip to content

Commit

Permalink
Support multiple "chain" powers (pre/post/wall) per Power
Browse files Browse the repository at this point in the history
  • Loading branch information
dorkster committed Aug 26, 2023
1 parent b0f9eaf commit 5eebc2d
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 73 deletions.
1 change: 1 addition & 0 deletions RELEASE_NOTES.txt
Expand Up @@ -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:

Expand Down
6 changes: 3 additions & 3 deletions docs/attribute-reference.html
Expand Up @@ -2068,11 +2068,11 @@ <h4>PowerManager: Powers</h4>

<p><strong>power.post_effect_src</strong> | <code>predefined_string, float, duration , float : Effect ID, Magnitude, Duration, Chance to apply</code> | Post effect to apply to caster. Duration is in 'ms' or 's'.</p>

<p><strong>power.pre_power</strong> | <code>power_id, float : Power, Chance to cast</code> | Trigger a power immediately when casting this one.</p>
<p><strong>power.pre_power</strong> | <code>repeatable(power_id, float) : Power, Chance to cast</code> | Trigger a power immediately when casting this one.</p>

<p><strong>power.post_power</strong> | <code>power_id, int : Power, Chance to cast</code> | Trigger a power if the hazard did damage. For 'block' type powers, this power will be triggered when the blocker takes damage.</p>
<p><strong>power.post_power</strong> | <code>repeatable(power_id, int) : Power, Chance to cast</code> | Trigger a power if the hazard did damage. For 'block' type powers, this power will be triggered when the blocker takes damage.</p>

<p><strong>power.wall_power</strong> | <code>power_id, int : Power, Chance to cast</code> | Trigger a power if the hazard hit a wall.</p>
<p><strong>power.wall_power</strong> | <code>repeatable(power_id, int) : Power, Chance to cast</code> | Trigger a power if the hazard hit a wall.</p>

<p><strong>power.wall_reflect</strong> | <code>bool</code> | Moving power will bounce off walls and keep going</p>

Expand Down
7 changes: 5 additions & 2 deletions src/Avatar.cpp
Expand Up @@ -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) {
Expand Down
19 changes: 11 additions & 8 deletions src/Entity.cpp
Expand Up @@ -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);
}
}
}

Expand Down Expand Up @@ -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);
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions src/EntityBehavior.cpp
Expand Up @@ -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;
Expand Down
11 changes: 7 additions & 4 deletions src/HazardManager.cpp
Expand Up @@ -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;
}
}
}

Expand Down
98 changes: 59 additions & 39 deletions src/PowerManager.cpp
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
}
}
}
}
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
}
}
}
Expand All @@ -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);
}
}
}
}
Expand All @@ -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
Expand Down
27 changes: 21 additions & 6 deletions src/PowerManager.h
Expand Up @@ -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 {
Expand Down Expand Up @@ -268,12 +287,8 @@ class Power {

std::vector<PostEffect> 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<ChainPower> chain_powers;

bool wall_reflect;

// spawn info
Expand Down
21 changes: 13 additions & 8 deletions src/StatBlock.cpp
Expand Up @@ -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);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Version.cpp
Expand Up @@ -30,7 +30,7 @@ FLARE. If not, see http://www.gnu.org/licenses/

#include <SDL.h>

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);

Expand Down

0 comments on commit 5eebc2d

Please sign in to comment.