New buff framework (player implementation)

Get rid of `iConditionBitFlag` in favor of a system of individual buff
objects that get composited to a bitflag on-the-fly.
Buff objects can have callbacks for application, expiration, and tick,
making them pretty flexible. Scripting languages can eventually use
these for custom behavior, too.

TODO:
- Get rid of bitflag in BaseNPC
- Apply buffs from passive nano powers
- Apply buffs from active nano powers
- Move eggs to new system
- ???
This commit is contained in:
gsemaj
2022-07-16 23:33:57 -07:00
parent 306a75f469
commit 98634d5aa2
16 changed files with 309 additions and 54 deletions

122
src/Buffs.hpp Normal file
View File

@@ -0,0 +1,122 @@
#pragma once
#include "core/Core.hpp"
#include "Entities.hpp"
#include <assert.h>
#include <vector>
#include <set>
/* forward declaration(s) */
struct BuffStack;
#define CSB_FROM_ECSB(x) (1 << (x - 1))
enum class BuffClass {
NANO = 0,
CONSUMABLE,
EGG,
OTHER
};
typedef void (*BuffCallback)(EntityRef, BuffStack*);
struct BuffStack {
int id; // ECSB
int durationTicks;
EntityRef source;
BuffClass buffClass;
BuffCallback onApply;
BuffCallback onTick;
BuffCallback onExpire;
};
class Buff {
private:
EntityRef self;
std::vector<BuffStack> stacks;
public:
void onTick() {
assert(!stacks.empty());
std::set<BuffCallback> callbacks;
auto it = stacks.begin();
while(it != stacks.end()) {
BuffStack& stack = *it;
if(stack.onTick != nullptr && callbacks.count(stack.onTick) == 0) {
// unique callback
stack.onTick(self, &stack);
callbacks.insert(stack.onTick);
}
if(stack.durationTicks > 0) stack.durationTicks--;
if(stack.durationTicks == 0) {
it = stacks.erase(it);
stack.onExpire(self, &stack);
} else it++;
}
}
void onExpire() {
assert(!stacks.empty());
std::set<BuffCallback> callbacks;
while(!stacks.empty()) {
BuffStack stack = stacks.back();
stacks.pop_back();
if(stack.onExpire != nullptr && callbacks.count(stack.onExpire) == 0) {
// execute unique callback
callbacks.insert(stack.onExpire);
stack.onExpire(self, &stack);
}
}
}
void addStack(BuffStack* stack) {
stacks.push_back(*stack);
}
/*
* Why do this madness? Let me tell you why.
* We need to be able to distinguish whether a player's
* buff is from something like an egg vs. their nano,
* because there are cases where the behavior is different.
* Now, this is impossible to do when they all get composited
* to a single bitfield. So we use a "buff class" and pick
* the buff stack that is most "dominant".
*/
BuffStack* getDominantBuff() {
assert(!stacks.empty());
BuffStack* dominant = nullptr;
for(BuffStack& stack : stacks) {
if(stack.buffClass > dominant->buffClass)
dominant = &stack;
}
return dominant;
}
/*
* In general, a Buff object won't exist
* unless it has stacks. However, when
* popping stacks during iteration (onExpire),
* stacks will be empty for a brief moment
* when the last stack is popped.
*/
bool isStale() {
return stacks.empty();
}
Buff(EntityRef pSelf, BuffStack* firstStack)
: self(pSelf) {
addStack(firstStack);
}
};
namespace Buffs {
void timeBuffUpdateAdd(EntityRef self, BuffStack* buff);
void timeBuffUpdateDelete(EntityRef self, BuffStack* buff);
}