From f249599ab5b91ae0a249ea7f893f70a421856411 Mon Sep 17 00:00:00 2001 From: gsemaj Date: Fri, 22 Jul 2022 06:47:52 -0700 Subject: [PATCH] YET ANOTHER ITERATION of the new ability system I am very tired --- src/Abilities.cpp | 23 +++++++------ src/Abilities.hpp | 2 +- src/Buffs.cpp | 82 +++++++++++++++++++++++++++---------------- src/Buffs.hpp | 43 +++++++++++++---------- src/Combat.cpp | 88 ++++++++++++++++++++++++++--------------------- src/Entities.hpp | 67 ++++++------------------------------ src/EntityRef.hpp | 52 ++++++++++++++++++++++++++++ src/Items.cpp | 20 +++++------ src/Nanos.cpp | 4 --- src/Player.hpp | 2 +- 10 files changed, 214 insertions(+), 169 deletions(-) create mode 100644 src/EntityRef.hpp diff --git a/src/Abilities.cpp b/src/Abilities.cpp index 009c951..4b23152 100644 --- a/src/Abilities.cpp +++ b/src/Abilities.cpp @@ -104,9 +104,11 @@ int Abilities::getCSTBFromST(int eSkillType) { } #pragma region Skill Handlers -void Abilities::usePassiveNanoSkill(SkillData* skill, Player* plr, int boost) { +bool Abilities::usePassiveNanoSkill(SkillData* skill, Player* plr, int boost) { assert(skill->drainType == SkillDrainType::PASSIVE); + bool newBuffApplied = false; + EntityRef self = PlayerManager::getSockFromID(plr->iID); std::vector targets; if (skill->targetType == SkillTargetType::GROUP) { @@ -126,14 +128,6 @@ void Abilities::usePassiveNanoSkill(SkillData* skill, Player* plr, int boost) { value, self, BuffClass::NONE, // overwritten per target - [](EntityRef host, BuffStack* stack) { - Buffs::timeBuffUpdate(host, stack, ETBU_ADD); - // TODO SKILLUSE/SKILLUSESUCC, sSkillResult_Buff - }, - nullptr, - [](EntityRef host, BuffStack* stack) { - Buffs::timeBuffUpdate(host, stack, ETBU_DEL); - } }; for (EntityRef target : targets) { @@ -143,8 +137,17 @@ void Abilities::usePassiveNanoSkill(SkillData* skill, Player* plr, int boost) { passiveBuff.buffStackClass = target == self ? BuffClass::NANO : BuffClass::GROUP_NANO; ICombatant* combatant = dynamic_cast(entity); - combatant->addBuff(timeBuffId, &passiveBuff); + newBuffApplied |= combatant->addBuff(timeBuffId, + [](EntityRef self, Buff* buff, int status, BuffStack* stack) { + Buffs::timeBuffUpdate(self, buff, status, stack); + }, + [](EntityRef self, Buff* buff, time_t currTime) { + // TODO time buff tick + }, + &passiveBuff); } + + return newBuffApplied; } #pragma endregion diff --git a/src/Abilities.hpp b/src/Abilities.hpp index 4600339..22be0c7 100644 --- a/src/Abilities.hpp +++ b/src/Abilities.hpp @@ -47,7 +47,7 @@ namespace Abilities { std::vector matchTargets(SkillData*, int, int32_t*); int getCSTBFromST(int eSkillType); - void usePassiveNanoSkill(SkillData*, Player*, int); + bool usePassiveNanoSkill(SkillData*, Player*, int); void init(); } diff --git a/src/Buffs.cpp b/src/Buffs.cpp index 8ffdace..c40f7e1 100644 --- a/src/Buffs.cpp +++ b/src/Buffs.cpp @@ -4,19 +4,16 @@ using namespace Buffs; -void Buff::tick() { +void Buff::tick(time_t currTime) { auto it = stacks.begin(); while(it != stacks.end()) { BuffStack& stack = *it; - if(stack.onTick) stack.onTick(self, &stack); + if(onTick) onTick(self, this, currTime); if(stack.durationTicks == 0) { - // erase() destroys the callbacks - // with the stack struct, so we need - // to copy it first. BuffStack deadStack = stack; it = stacks.erase(it); - if(deadStack.onExpire) deadStack.onExpire(self, &deadStack); + if(onUpdate) onUpdate(self, this, ETBU_DEL, &deadStack); } else { if(stack.durationTicks > 0) stack.durationTicks--; it++; @@ -28,15 +25,13 @@ void Buff::clear() { while(!stacks.empty()) { BuffStack stack = stacks.back(); stacks.pop_back(); - if(stack.onExpire) stack.onExpire(self, &stack); + if(onUpdate) onUpdate(self, this, ETBU_DEL, &stack); } } void Buff::addStack(BuffStack* stack) { - BuffStack newStack = *stack; - newStack.buff = this; - if(newStack.onApply) newStack.onApply(self, &newStack); - stacks.push_back(newStack); + stacks.push_back(*stack); + if(onUpdate) onUpdate(self, this, ETBU_ADD, &stacks.back()); } bool Buff::hasClass(BuffClass buffClass) { @@ -56,34 +51,68 @@ BuffClass Buff::maxClass() { return buffClass; } +int Buff::getValue(BuffValueSelector selector) { + if(isStale()) return 0; + + int value = selector == BuffValueSelector::NET_TOTAL ? 0 : stacks.front().value; + for(BuffStack& stack : stacks) { + switch(selector) + { + case BuffValueSelector::NET_TOTAL: + value += stack.value; + break; + case BuffValueSelector::MIN_VALUE: + if(stack.value < value) value = stack.value; + break; + case BuffValueSelector::MAX_VALUE: + if(stack.value > value) value = stack.value; + break; + case BuffValueSelector::MIN_MAGNITUDE: + if(abs(stack.value) < abs(value)) value = stack.value; + break; + case BuffValueSelector::MAX_MAGNITUDE: + default: + if(abs(stack.value) > abs(value)) value = stack.value; + } + } + return value; +} + bool Buff::isStale() { return stacks.empty(); } +/* This will practically never do anything important, but it's here just in case */ +void Buff::updateCallbacks(BuffCallback fOnUpdate, BuffCallback fonTick) { + if(!onUpdate) onUpdate = fOnUpdate; + if(!onTick) onTick = fonTick; +} + #pragma region Handlers -void Buffs::timeBuffUpdate(EntityRef self, BuffStack* stack, int status) { +void Buffs::timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack) { if(self.kind != EntityKind::PLAYER) return; // not implemented Player* plr = (Player*)self.getEntity(); if(plr == nullptr) - return; + return; // sanity check - if(status == ETBU_DEL && plr->hasBuff(stack->buff->id)) - return; // no premature status removal! - - sTimeBuff payload{}; + if(status == ETBU_DEL && !buff->isStale()) + return; // no premature effect deletion int cbf = plr->getCompositeCondition(); + sTimeBuff payload{}; if(status == ETBU_ADD) { - if(stack->buff->id > 0) cbf |= CSB_FROM_ECSB(stack->buff->id); - payload.iValue = stack->value; - //payload.iTimeLimit = stack->durationTicks * MS_PER_PLAYER_TICK; + payload.iValue = buff->getValue(BuffValueSelector::MAX_MAGNITUDE); + // we need to explicitly add the ECSB for this buff, + // in case this is the first stack in and the entry + // in the buff map doesn't yet exist + if(buff->id > 0) cbf |= CSB_FROM_ECSB(buff->id); } INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt); - pkt.eCSTB = stack->buff->id; // eCharStatusTimeBuffID + pkt.eCSTB = buff->id; // eCharStatusTimeBuffID pkt.eTBU = status; // eTimeBuffUpdate pkt.eTBT = (int)stack->buffStackClass; pkt.iConditionBitFlag = cbf; @@ -91,20 +120,15 @@ void Buffs::timeBuffUpdate(EntityRef self, BuffStack* stack, int status) { self.sock->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE)); } -void Buffs::timeBuffTimeoutViewable(EntityRef self, BuffStack* stack, int ct) { - if(self.kind != EntityKind::PLAYER) - return; // not implemented - - Player* plr = (Player*)self.getEntity(); - if(plr == nullptr) - return; - +/* +void Buffs::timeBuffTimeoutViewable(EntityRef self, Buff* buff, int ct) { INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt); // send a buff timeout to other players pkt.eCT = ct; // 1 for eggs, at least pkt.iID = plr->iID; pkt.iConditionBitFlag = plr->getCompositeCondition(); PlayerManager::sendToViewable(self.sock, pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT); } +*/ /* MOVE TO EGG LAMBDA void Buffs::timeBuffTimeout(EntityRef self, BuffStack* buff) { diff --git a/src/Buffs.hpp b/src/Buffs.hpp index 2fc8163..1d0c8df 100644 --- a/src/Buffs.hpp +++ b/src/Buffs.hpp @@ -2,13 +2,15 @@ #include "core/Core.hpp" -#include "Entities.hpp" +#include "EntityRef.hpp" #include #include /* forward declaration(s) */ -struct BuffStack; +class Buff; +template +using BuffCallback = std::function; #define CSB_FROM_ECSB(x) (1 << (x - 1)) @@ -22,24 +24,19 @@ enum class BuffClass { CASH_ITEM = ETBT_CASHITEM }; -typedef std::function BuffCallback; +enum class BuffValueSelector { + MAX_VALUE, + MIN_VALUE, + MAX_MAGNITUDE, + MIN_MAGNITUDE, + NET_TOTAL +}; struct BuffStack { int durationTicks; int value; EntityRef source; BuffClass buffStackClass; - - /* called just before the stack is added */ - BuffCallback onApply; - - /* called when the stack is ticked */ - BuffCallback onTick; - - /* called just after the stack is removed */ - BuffCallback onExpire; - - Buff* buff; }; class Buff { @@ -49,8 +46,12 @@ private: public: int id; + /* called just after a stack is added or removed */ + BuffCallback onUpdate; + /* called when the buff is ticked */ + BuffCallback onTick; - void tick(); + void tick(time_t); void clear(); void addStack(BuffStack* stack); @@ -62,6 +63,8 @@ public: bool hasClass(BuffClass buffClass); BuffClass maxClass(); + int getValue(BuffValueSelector selector); + /* * In general, a Buff object won't exist * unless it has stacks. However, when @@ -71,13 +74,15 @@ public: */ bool isStale(); - Buff(int iid, EntityRef pSelf, BuffStack* firstStack) - : id(iid), self(pSelf) { + void updateCallbacks(BuffCallback fOnUpdate, BuffCallback fonTick); + + Buff(int iid, EntityRef pSelf, BuffCallback fOnUpdate, BuffCallback fOnTick, BuffStack* firstStack) + : self(pSelf), id(iid), onUpdate(fOnUpdate), onTick(fOnTick) { addStack(firstStack); } }; namespace Buffs { - void timeBuffUpdate(EntityRef self, BuffStack* stack, int status); - void timeBuffTimeoutViewable(EntityRef self, BuffStack* stack, int ct); + void timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack); + //void timeBuffTimeoutViewable(EntityRef self, Buff* buff, int ct); } diff --git a/src/Combat.cpp b/src/Combat.cpp index dbab50f..9b66305 100644 --- a/src/Combat.cpp +++ b/src/Combat.cpp @@ -19,10 +19,17 @@ using namespace Combat; /// Player Id -> Bullet Id -> Bullet std::map> Combat::Bullets; -void Player::addBuff(int buffId, BuffStack* stack) { +bool Player::addBuff(int buffId, BuffCallback onUpdate, BuffCallback onTick, BuffStack* stack) { EntityRef self = PlayerManager::getSockFromID(iID); - if(!hasBuff(buffId)) buffs[buffId] = new Buff(buffId, self, stack); - else buffs[buffId]->addStack(stack); + + if(!hasBuff(buffId)) { + buffs[buffId] = new Buff(buffId, self, onUpdate, onTick, stack); + return true; + } + + buffs[buffId]->updateCallbacks(onUpdate, onTick); + buffs[buffId]->addStack(stack); + return false; } Buff* Player::getBuff(int buffId) { @@ -88,7 +95,9 @@ void Player::step(time_t currTime) { // no-op } -void CombatNPC::addBuff(int buffId, BuffStack* stack) { /* stubbed */ } +bool CombatNPC::addBuff(int buffId, BuffCallback onUpdate, BuffCallback onTick, BuffStack* stack) { /* stubbed */ + return false; +} Buff* CombatNPC::getBuff(int buffId) { /* stubbed */ return nullptr; @@ -371,43 +380,20 @@ static void combatEnd(CNSocket *sock, CNPacketData *data) { plr->healCooldown = 4000; } -static void dotDamageOnOff(CNSocket *sock, CNPacketData *data) { - sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf; +static void dealGooDamage(CNSocket *sock) { Player *plr = PlayerManager::getPlayer(sock); + if(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE) + return; // ignore completely - // infection debuff toggles as the client asks it to, - // so we add and remove a permanent debuff - if (pkt->iFlag && !plr->hasBuff(ECSB_INFECTION)) { - BuffStack infection = { - -1, // infinite - NULL, - sock, // self-inflicted - BuffClass::ENVIRONMENT, - [](EntityRef host, BuffStack* stack) { - Buffs::timeBuffUpdate(host, stack, ETBU_ADD); - }, - nullptr, // client toggles for us! todo anticheat lol - [](EntityRef host, BuffStack* stack) { - Buffs::timeBuffUpdate(host, stack, ETBU_DEL); - } - }; - plr->addBuff(ECSB_INFECTION, &infection); - } else if(!pkt->iFlag && plr->hasBuff(ECSB_INFECTION)) { - plr->removeBuff(ECSB_INFECTION); - } -} - -static void dealGooDamage(CNSocket *sock, int amount) { size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_DotDamage); assert(resplen < CN_PACKET_BUFFER_SIZE - 8); uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; - Player *plr = PlayerManager::getPlayer(sock); - memset(respbuf, 0, resplen); sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf; sSkillResult_DotDamage *dmg = (sSkillResult_DotDamage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK)); + int amount = PC_MAXHEALTH(plr->level) * 3 / 20; Buff* protectionBuff = plr->getBuff(ECSB_PROTECT_INFECTION); if (protectionBuff != nullptr) { amount = -2; // -2 is the magic number for "Protected" to appear as the damage number @@ -444,6 +430,33 @@ static void dealGooDamage(CNSocket *sock, int amount) { PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen); } +static void dotDamageOnOff(CNSocket *sock, CNPacketData *data) { + sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf; + Player *plr = PlayerManager::getPlayer(sock); + + // infection debuff toggles as the client asks it to, + // so we add and remove a permanent debuff + if (pkt->iFlag && !plr->hasBuff(ECSB_INFECTION)) { + BuffStack infection = { + -1, // infinite + 0, // no value + sock, // self-inflicted + BuffClass::ENVIRONMENT + }; + plr->addBuff(ECSB_INFECTION, + [](EntityRef self, Buff* buff, int status, BuffStack* stack) { + Buffs::timeBuffUpdate(self, buff, status, stack); + }, + [](EntityRef self, Buff* buff, time_t currTime) { + if(self.kind == EntityKind::PLAYER) + dealGooDamage(self.sock); + }, + &infection); + } else if(!pkt->iFlag && plr->hasBuff(ECSB_INFECTION)) { + plr->removeBuff(ECSB_INFECTION); + } +} + static void pcAttackChars(CNSocket *sock, CNPacketData *data) { sP_CL2FE_REQ_PC_ATTACK_CHARs* pkt = (sP_CL2FE_REQ_PC_ATTACK_CHARs*)data->buf; Player *plr = PlayerManager::getPlayer(sock); @@ -745,11 +758,6 @@ static void playerTick(CNServer *serv, time_t currTime) { if (plr->HP <= 0) continue; - // fm patch/lake damage - if ((plr->hasBuff(ECSB_INFECTION)) - && !(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)) - dealGooDamage(sock, PC_MAXHEALTH(plr->level) * 3 / 20); - // heal if (currTime - lastHealTime >= 4000 && !plr->inCombat && plr->HP < PC_MAXHEALTH(plr->level)) { if (currTime - lastHealTime - plr->healCooldown >= 4000) { @@ -776,7 +784,10 @@ static void playerTick(CNServer *serv, time_t currTime) { if (skill->drainType == SkillDrainType::PASSIVE) { // apply passive buff drainRate = skill->batteryUse[boost * 3]; - Abilities::usePassiveNanoSkill(skill, plr, boost * 3); + if(Abilities::usePassiveNanoSkill(skill, plr, boost * 3)) { + // first buff, send back skill use packet(s) + // TODO + } } } @@ -812,9 +823,8 @@ static void playerTick(CNServer *serv, time_t currTime) { // process buffsets auto it = plr->buffs.begin(); while(it != plr->buffs.end()) { - int buffId = (*it).first; Buff* buff = (*it).second; - buff->tick(); + buff->tick(currTime); if(buff->isStale()) { // garbage collect it = plr->buffs.erase(it); diff --git a/src/Entities.hpp b/src/Entities.hpp index 5e98064..d6cd361 100644 --- a/src/Entities.hpp +++ b/src/Entities.hpp @@ -3,22 +3,16 @@ #include "core/Core.hpp" #include "Chunking.hpp" +#include "EntityRef.hpp" +#include "Buffs.hpp" + #include #include +#include /* forward declaration(s) */ -class Buff; -struct BuffStack; - -enum EntityKind { - INVALID, - PLAYER, - SIMPLE_NPC, - COMBAT_NPC, - MOB, - EGG, - BUS -}; +class Chunk; +struct Group; enum class AIState { INACTIVE, @@ -28,9 +22,6 @@ enum class AIState { DEAD }; -class Chunk; -struct Group; - struct Entity { EntityKind kind = EntityKind::INVALID; int x = 0, y = 0, z = 0; @@ -48,42 +39,6 @@ struct Entity { virtual void disappearFromViewOf(CNSocket *sock) = 0; }; -struct EntityRef { - EntityKind kind; - union { - CNSocket *sock; - int32_t id; - }; - - EntityRef(CNSocket *s); - EntityRef(int32_t i); - - bool isValid() const; - Entity *getEntity() const; - - bool operator==(const EntityRef& other) const { - if (kind != other.kind) - return false; - - if (kind == EntityKind::PLAYER) - return sock == other.sock; - - return id == other.id; - } - - // arbitrary ordering - bool operator<(const EntityRef& other) const { - if (kind == other.kind) { - if (kind == EntityKind::PLAYER) - return sock < other.sock; - else - return id < other.id; - } - - return kind < other.kind; - } -}; - /* * Interfaces */ @@ -92,10 +47,10 @@ public: ICombatant() {} virtual ~ICombatant() {} - virtual void addBuff(int buffId, BuffStack* stack) = 0; - virtual Buff* getBuff(int buffId) = 0; - virtual void removeBuff(int buffId) = 0; - virtual bool hasBuff(int buffId) = 0; + virtual bool addBuff(int, BuffCallback, BuffCallback, BuffStack*) = 0; + virtual Buff* getBuff(int) = 0; + virtual void removeBuff(int) = 0; + virtual bool hasBuff(int) = 0; virtual int getCompositeCondition() = 0; virtual int takeDamage(EntityRef, int) = 0; virtual void heal(EntityRef, int) = 0; @@ -159,7 +114,7 @@ struct CombatNPC : public BaseNPC, public ICombatant { virtual bool isExtant() override { return hp > 0; } - virtual void addBuff(int buffId, BuffStack* stack) override; + virtual bool addBuff(int buffId, BuffCallback onUpdate, BuffCallback onTick, BuffStack* stack) override; virtual Buff* getBuff(int buffId) override; virtual void removeBuff(int buffId) override; virtual bool hasBuff(int buffId) override; diff --git a/src/EntityRef.hpp b/src/EntityRef.hpp new file mode 100644 index 0000000..2aab56e --- /dev/null +++ b/src/EntityRef.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "core/Core.hpp" + +/* forward declaration(s) */ +struct Entity; + +enum EntityKind { + INVALID, + PLAYER, + SIMPLE_NPC, + COMBAT_NPC, + MOB, + EGG, + BUS +}; + +struct EntityRef { + EntityKind kind; + union { + CNSocket *sock; + int32_t id; + }; + + EntityRef(CNSocket *s); + EntityRef(int32_t i); + + bool isValid() const; + Entity *getEntity() const; + + bool operator==(const EntityRef& other) const { + if (kind != other.kind) + return false; + + if (kind == EntityKind::PLAYER) + return sock == other.sock; + + return id == other.id; + } + + // arbitrary ordering + bool operator<(const EntityRef& other) const { + if (kind == other.kind) { + if (kind == EntityKind::PLAYER) + return sock < other.sock; + else + return id < other.id; + } + + return kind < other.kind; + } +}; \ No newline at end of file diff --git a/src/Items.cpp b/src/Items.cpp index 94b2c20..0186d51 100644 --- a/src/Items.cpp +++ b/src/Items.cpp @@ -494,18 +494,18 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) { int durationMilliseconds = Abilities::SkillTable[144].durationTime[0] * 100; BuffStack gumballBuff = { durationMilliseconds / MS_PER_PLAYER_TICK, - NULL, + 0, sock, - BuffClass::CASH_ITEM, // or BuffClass::ITEM? - [](EntityRef host, BuffStack* stack) { - Buffs::timeBuffUpdate(host, stack, ETBU_ADD); - }, - nullptr, - [](EntityRef host, BuffStack* stack) { - Buffs::timeBuffUpdate(host, stack, ETBU_DEL); - } + BuffClass::CASH_ITEM // or BuffClass::ITEM? }; - player->addBuff(eCSB, &gumballBuff); + player->addBuff(eCSB, + [](EntityRef self, Buff* buff, int status, BuffStack* stack) { + Buffs::timeBuffUpdate(self, buff, status, stack); + }, + [](EntityRef self, Buff* buff, time_t currTime) { + // TODO passive tick + }, + &gumballBuff); sock->sendPacket((void*)&respbuf, P_FE2CL_REP_PC_ITEM_USE_SUCC, resplen); // update inventory serverside diff --git a/src/Nanos.cpp b/src/Nanos.cpp index 551da96..d5f83f9 100644 --- a/src/Nanos.cpp +++ b/src/Nanos.cpp @@ -208,10 +208,6 @@ static void nanoEquipHandler(CNSocket* sock, CNPacketData* data) { // Update player plr->equippedNanos[nano->iNanoSlotNum] = nano->iNanoID; - // Unbuff gumballs - int buffId = ECSB_STIMPAKSLOT1 + nano->iNanoSlotNum; - plr->removeBuff(buffId); - // unsummon nano if replaced if (plr->activeNano == plr->equippedNanos[nano->iNanoSlotNum]) summonNano(sock, -1); diff --git a/src/Player.hpp b/src/Player.hpp index 02b6b14..7f65840 100644 --- a/src/Player.hpp +++ b/src/Player.hpp @@ -89,7 +89,7 @@ struct Player : public Entity, public ICombatant { virtual void enterIntoViewOf(CNSocket *sock) override; virtual void disappearFromViewOf(CNSocket *sock) override; - virtual void addBuff(int buffId, BuffStack* stack) override; + virtual bool addBuff(int buffId, BuffCallback onUpdate, BuffCallback onTick, BuffStack* stack) override; virtual Buff* getBuff(int buffId) override; virtual void removeBuff(int buffId) override; virtual bool hasBuff(int buffId) override;